commit be7c837e339e0c61df2f1210c74d596b07a4d835 Author: Ayush Saini Date: Mon Mar 29 03:24:13 2021 +0530 Private server diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..ca2f7c3 --- /dev/null +++ b/README.txt @@ -0,0 +1,18 @@ +To run this, simply cd into this directory and run ./ballisticacore_server +(on mac or linux) or launch_ballisticacore_server.bat (on windows). +You'll need to open a UDP port (43210 by default) so that the world can +communicate with your server. +You can configure your server by editing the config.yaml file. +(if you only see config_template.yaml, you can copy/rename that to config.yaml) + +-Add your account-id in dist/ba_root/mods/privateserver.py -> admin[] +-Restart server twice +-Add players account-id (pb-id) in whitelist.json manually or use chat command while whitelist is off. +-Use "/whitelist" to turn on/off whitelist. +-Use "/spectators" to turn on/off lobby kick. +-Use "/add " to whitelist player (turn off whitelist or spectators mode first). +-In config.yaml set party type to PUBLIC ; party will be PRIVATE automatically by smoothy haxx +-Increased Kickvote cooldown +-Kickvote logs with be logged in terminal (who kicking whom). +-player joined the party/player left the party message removed + \ No newline at end of file diff --git a/ballisticacore_server b/ballisticacore_server new file mode 100644 index 0000000..fe8b47a --- /dev/null +++ b/ballisticacore_server @@ -0,0 +1,847 @@ +#!/usr/bin/env python3.8 +# Released under the MIT License. See LICENSE for details. +# +"""BallisticaCore server manager.""" +from __future__ import annotations + +import json +import os +import signal +import subprocess +import sys +import time +from pathlib import Path +from threading import Lock, Thread, current_thread +from typing import TYPE_CHECKING + +# We make use of the bacommon and efro packages as well as site-packages +# included with our bundled Ballistica dist, so we need to add those paths +# before we import them. +sys.path += [ + str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')), + str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')) +] + +from bacommon.servermanager import ServerConfig, StartServerModeCommand +from efro.dataclasses import dataclass_from_dict, dataclass_validate +from efro.error import CleanError +from efro.terminal import Clr + +if TYPE_CHECKING: + from typing import Optional, List, Dict, Union, Tuple + from types import FrameType + from bacommon.servermanager import ServerCommand + +VERSION_STR = '1.2' + +# Version history: +# 1.2: +# Added optional --help arg +# Added --config arg for setting config path and --root for ba_root path +# Added noninteractive mode and --interactive/--noninteractive args to +# explicitly specify +# Added explicit control for auto-restart: --no-auto-restart +# Config file is now reloaded each time server binary is restarted; no more +# need to bring down server wrapper to pick up changes +# Now automatically restarts server binary when config file is modified +# (use --no-config-auto-restart to disable that behavior) +# 1.1.1: +# Switched config reading to use efro.dataclasses.dataclass_from_dict() +# 1.1.0: +# Added shutdown command +# Changed restart to default to immediate=True +# Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes +# 1.0.0: +# Initial release + + +class ServerManagerApp: + """An app which manages BallisticaCore server execution. + + Handles configuring, launching, re-launching, and otherwise + managing BallisticaCore operating in server mode. + """ + + # How many seconds we wait after asking our subprocess to do an immediate + # shutdown before bringing down the hammer. + IMMEDIATE_SHUTDOWN_TIME_LIMIT = 5.0 + + def __init__(self) -> None: + self._config_path = 'config.yaml' + self._user_provided_config_path = False + self._config = ServerConfig() + self._ba_root_path = os.path.abspath('dist/ba_root') + self._interactive = sys.stdin.isatty() + self._wrapper_shutdown_desired = False + self._done = False + self._subprocess_commands: List[Union[str, ServerCommand]] = [] + self._subprocess_commands_lock = Lock() + self._subprocess_force_kill_time: Optional[float] = None + self._auto_restart = True + self._config_auto_restart = True + self._config_mtime: Optional[float] = None + self._last_config_mtime_check_time: Optional[float] = None + self._should_report_subprocess_error = False + self._running = False + self._interpreter_start_time: Optional[float] = None + self._subprocess: Optional[subprocess.Popen[bytes]] = None + self._subprocess_launch_time: Optional[float] = None + self._subprocess_sent_config_auto_restart = False + self._subprocess_sent_clean_exit = False + self._subprocess_sent_unclean_exit = False + self._subprocess_thread: Optional[Thread] = None + self._subprocess_exited_cleanly: Optional[bool] = None + + # This may override the above defaults. + self._parse_command_line_args() + + # Do an initial config-load. If the config is invalid at this point + # we can cleanly die (we're more lenient later on reloads). + self.load_config(strict=True, print_confirmation=False) + + @property + def config(self) -> ServerConfig: + """The current config for the app.""" + return self._config + + @config.setter + def config(self, value: ServerConfig) -> None: + dataclass_validate(value) + self._config = value + + def _prerun(self) -> None: + """Common code at the start of any run.""" + + # Make sure we don't call run multiple times. + if self._running: + raise RuntimeError('Already running.') + self._running = True + + dbgstr = 'debug' if __debug__ else 'opt' + print( + f'{Clr.CYN}{Clr.BLD}BallisticaCore server manager {VERSION_STR}' + f' starting up ({dbgstr} mode)...{Clr.RST}', + flush=True) + + # Python will handle SIGINT for us (as KeyboardInterrupt) but we + # need to register a SIGTERM handler so we have a chance to clean + # up our subprocess when someone tells us to die. (and avoid + # zombie processes) + signal.signal(signal.SIGTERM, self._handle_term_signal) + + # During a run, we make the assumption that cwd is the dir + # containing this script, so make that so. Up until now that may + # not be the case (we support being called from any location). + os.chdir(os.path.abspath(os.path.dirname(__file__))) + + # Fire off a background thread to wrangle our server binaries. + self._subprocess_thread = Thread(target=self._bg_thread_main) + self._subprocess_thread.start() + + def _postrun(self) -> None: + """Common code at the end of any run.""" + print(f'{Clr.CYN}Server manager shutting down...{Clr.RST}', flush=True) + + assert self._subprocess_thread is not None + if self._subprocess_thread.is_alive(): + print(f'{Clr.CYN}Waiting for subprocess exit...{Clr.RST}', + flush=True) + + # Mark ourselves as shutting down and wait for the process to wrap up. + self._done = True + self._subprocess_thread.join() + + # If there's a server error we should care about, exit the + # entire wrapper uncleanly. + if self._should_report_subprocess_error: + raise CleanError('Server subprocess exited uncleanly.') + + def run(self) -> None: + """Do the thing.""" + if self._interactive: + self._run_interactive() + else: + self._run_noninteractive() + + def _run_noninteractive(self) -> None: + """Run the app loop to completion noninteractively.""" + self._prerun() + try: + while True: + time.sleep(1.234) + except KeyboardInterrupt: + # Gracefully bow out if we kill ourself via keyboard. + pass + except SystemExit: + # We get this from the builtin quit(), our signal handler, etc. + # Need to catch this so we can clean up, otherwise we'll be + # left in limbo with our process thread still running. + pass + self._postrun() + + def _run_interactive(self) -> None: + """Run the app loop to completion interactively.""" + import code + self._prerun() + + # Print basic usage info for interactive mode. + print( + f"{Clr.CYN}Interactive mode enabled; use the 'mgr' object" + f' to interact with the server.\n' + f"Type 'help(mgr)' for more information.{Clr.RST}", + flush=True) + + context = {'__name__': '__console__', '__doc__': None, 'mgr': self} + + # Enable tab-completion if possible. + self._enable_tab_completion(context) + + # Now just sit in an interpreter. + # TODO: make it possible to use IPython if the user has it available. + try: + self._interpreter_start_time = time.time() + code.interact(local=context, banner='', exitmsg='') + except SystemExit: + # We get this from the builtin quit(), our signal handler, etc. + # Need to catch this so we can clean up, otherwise we'll be + # left in limbo with our process thread still running. + pass + except BaseException as exc: + print( + f'{Clr.SRED}Unexpected interpreter exception:' + f' {exc} ({type(exc)}){Clr.RST}', + flush=True) + + self._postrun() + + def cmd(self, statement: str) -> None: + """Exec a Python command on the current running server subprocess. + + Note that commands are executed asynchronously and no status or + return value is accessible from this manager app. + """ + if not isinstance(statement, str): + raise TypeError(f'Expected a string arg; got {type(statement)}') + with self._subprocess_commands_lock: + self._subprocess_commands.append(statement) + self._block_for_command_completion() + + def _block_for_command_completion(self) -> None: + # Ideally we'd block here until the command was run so our prompt would + # print after it's results. We currently don't get any response from + # the app so the best we can do is block until our bg thread has sent + # it. In the future we can perhaps add a proper 'command port' + # interface for proper blocking two way communication. + while True: + with self._subprocess_commands_lock: + if not self._subprocess_commands: + break + time.sleep(0.1) + + # One last short delay so if we come out *just* as the command is sent + # we'll hopefully still give it enough time to process/print. + time.sleep(0.1) + + def screenmessage(self, + message: str, + color: Optional[Tuple[float, float, float]] = None, + clients: Optional[List[int]] = None) -> None: + """Display a screen-message. + + This will have no name attached and not show up in chat history. + They will show up in replays, however (unless clients is passed). + """ + from bacommon.servermanager import ScreenMessageCommand + self._enqueue_server_command( + ScreenMessageCommand(message=message, color=color, + clients=clients)) + + def chatmessage(self, + message: str, + clients: Optional[List[int]] = None) -> None: + """Send a chat message from the server. + + This will have the server's name attached and will be logged + in client chat windows, just like other chat messages. + """ + from bacommon.servermanager import ChatMessageCommand + self._enqueue_server_command( + ChatMessageCommand(message=message, clients=clients)) + + def clientlist(self) -> None: + """Print a list of connected clients.""" + from bacommon.servermanager import ClientListCommand + self._enqueue_server_command(ClientListCommand()) + self._block_for_command_completion() + + def kick(self, client_id: int, ban_time: Optional[int] = None) -> None: + """Kick the client with the provided id. + + If ban_time is provided, the client will be banned for that + length of time in seconds. If it is None, ban duration will + be determined automatically. Pass 0 or a negative number for no + ban time. + """ + from bacommon.servermanager import KickCommand + self._enqueue_server_command( + KickCommand(client_id=client_id, ban_time=ban_time)) + + def restart(self, immediate: bool = True) -> None: + """Restart the server subprocess. + + By default, the current server process will exit immediately. + If 'immediate' is passed as False, however, it will instead exit at + the next clean transition point (the end of a series, etc). + """ + from bacommon.servermanager import ShutdownCommand, ShutdownReason + self._enqueue_server_command( + ShutdownCommand(reason=ShutdownReason.RESTARTING, + immediate=immediate)) + + # If we're asking for an immediate restart but don't get one within + # the grace period, bring down the hammer. + if immediate: + self._subprocess_force_kill_time = ( + time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT) + + def shutdown(self, immediate: bool = True) -> None: + """Shut down the server subprocess and exit the wrapper. + + By default, the current server process will exit immediately. + If 'immediate' is passed as False, however, it will instead exit at + the next clean transition point (the end of a series, etc). + """ + from bacommon.servermanager import ShutdownCommand, ShutdownReason + self._enqueue_server_command( + ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate)) + + # An explicit shutdown means we know to bail completely once this + # subprocess completes. + self._wrapper_shutdown_desired = True + + # If we're asking for an immediate shutdown but don't get one within + # the grace period, bring down the hammer. + if immediate: + self._subprocess_force_kill_time = ( + time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT) + + def _parse_command_line_args(self) -> None: + """Parse command line args.""" + # pylint: disable=too-many-branches + + i = 1 + argc = len(sys.argv) + did_set_interactive = False + while i < argc: + arg = sys.argv[i] + if arg == '--help': + self.print_help() + sys.exit(0) + elif arg == '--config': + if i + 1 >= argc: + raise CleanError('Expected a config path as next arg.') + path = sys.argv[i + 1] + if not os.path.exists(path): + raise CleanError( + f"Supplied path does not exist: '{path}'.") + # We need an abs path because we may be in a different + # cwd currently than we will be during the run. + self._config_path = os.path.abspath(path) + self._user_provided_config_path = True + i += 2 + elif arg == '--root': + if i + 1 >= argc: + raise CleanError('Expected a path as next arg.') + path = sys.argv[i + 1] + # Unlike config_path, this one doesn't have to exist now. + # We do however need an abs path because we may be in a + # different cwd currently than we will be during the run. + self._ba_root_path = os.path.abspath(path) + i += 2 + elif arg == '--interactive': + if did_set_interactive: + raise CleanError('interactive/noninteractive can only' + ' be specified once.') + self._interactive = True + did_set_interactive = True + i += 1 + elif arg == '--noninteractive': + if did_set_interactive: + raise CleanError('interactive/noninteractive can only' + ' be specified once.') + self._interactive = False + did_set_interactive = True + i += 1 + elif arg == '--no-auto-restart': + self._auto_restart = False + i += 1 + elif arg == '--no-config-auto-restart': + self._config_auto_restart = False + i += 1 + else: + raise CleanError(f"Invalid arg: '{arg}'.") + + @classmethod + def _par(cls, txt: str) -> str: + """Spit out a pretty paragraph for our help text.""" + import textwrap + ind = ' ' * 2 + out = textwrap.fill(txt, 80, initial_indent=ind, subsequent_indent=ind) + return f'{out}\n' + + @classmethod + def print_help(cls) -> None: + """Print app help.""" + filename = os.path.basename(__file__) + out = ( + f'{Clr.BLD}{filename} usage:{Clr.RST}\n' + cls._par( + 'This script handles configuring, launching, re-launching,' + ' and otherwise managing BallisticaCore operating' + ' in server mode. It can be run with no arguments, but' + ' accepts the following optional ones:') + f'\n' + f'{Clr.BLD}--help:{Clr.RST}\n' + f' Show this help.\n' + f'\n' + f'{Clr.BLD}--config [path]{Clr.RST}\n' + cls._par( + 'Set the config file read by the server script. The config' + ' file contains most options for what kind of game to host.' + ' It should be in yaml format. Note that yaml is backwards' + ' compatible with json so you can just write json if you' + ' want to. If not specified, the script will look for a' + ' file named \'config.yaml\' in the same directory as the' + ' script.') + '\n' + f'{Clr.BLD}--root [path]{Clr.RST}\n' + cls._par( + 'Set the ballistica root directory. This is where the server' + ' binary will read and write its caches, state files,' + ' downloaded assets, etc. It needs to be a writable' + ' directory. If not specified, the script will use the' + ' \'dist/ba_root\' directory relative to itself.') + '\n' + f'{Clr.BLD}--interactive{Clr.RST}\n' + f'{Clr.BLD}--noninteractive{Clr.RST}\n' + cls._par( + 'Specify whether the script should run interactively.' + ' In interactive mode, the script creates a Python interpreter' + ' and reads commands from stdin, allowing for live interaction' + ' with the server. The server script will then exit when ' + 'end-of-file is reached in stdin. Noninteractive mode creates' + ' no interpreter and is more suited to being run in automated' + ' scenarios. By default, interactive mode will be used if' + ' a terminal is detected and noninteractive mode otherwise.') + + '\n' + f'{Clr.BLD}--no-auto-restart{Clr.RST}\n' + + cls._par('Auto-restart is enabled by default, which means the' + ' server manager will restart the server binary whenever' + ' it exits (even when uncleanly). Disabling auto-restart' + ' will cause the server manager to instead exit after a' + ' single run and also to return error codes if the' + ' server binary did so.') + '\n' + f'{Clr.BLD}--no-config-auto-restart{Clr.RST}\n' + cls._par( + 'By default, when auto-restart is enabled, the server binary' + ' will be automatically restarted if changes to the server' + ' config file are detected. This disables that behavior.')) + print(out) + + def load_config(self, strict: bool, print_confirmation: bool) -> None: + """Load the config. + + If strict is True, errors will propagate upward. + Otherwise, warnings will be printed and repeated attempts will be + made to load the config. Eventually the function will give up + and leave the existing config as-is. + """ + retry_seconds = 3 + maxtries = 11 + for trynum in range(maxtries): + try: + self._config = self._load_config_from_file( + print_confirmation=print_confirmation) + return + except Exception as exc: + if strict: + raise CleanError( + f'Error loading config file:\n{exc}') from exc + print(f'{Clr.RED}Error loading config file:\n{exc}.{Clr.RST}', + flush=True) + if trynum == maxtries - 1: + print( + f'{Clr.RED}Max-tries reached; giving up.' + f' Existing config values will be used.{Clr.RST}', + flush=True) + break + print( + f'{Clr.CYN}Please correct the error.' + f' Will re-attempt load in {retry_seconds}' + f' seconds. (attempt {trynum+1} of' + f' {maxtries-1}).{Clr.RST}', + flush=True) + + for _j in range(retry_seconds): + # If the app is trying to die, drop what we're doing. + if self._done: + return + time.sleep(1) + + def _load_config_from_file(self, print_confirmation: bool) -> ServerConfig: + + out: Optional[ServerConfig] = None + + if not os.path.exists(self._config_path): + + # Special case: + # If the user didn't specify a particular config file, allow + # gracefully falling back to defaults if the default one is + # missing. + if not self._user_provided_config_path: + if print_confirmation: + print( + f'{Clr.YLW}Default config file not found' + f' (\'{self._config_path}\'); using default' + f' settings.{Clr.RST}', + flush=True) + self._config_mtime = None + self._last_config_mtime_check_time = time.time() + return ServerConfig() + + # Don't be so lenient if the user pointed us at one though. + raise RuntimeError( + f"Config file not found: '{self._config_path}'.") + + import yaml + with open(self._config_path) as infile: + user_config_raw = yaml.safe_load(infile.read()) + + # An empty config file will yield None, and that's ok. + if user_config_raw is not None: + out = dataclass_from_dict(ServerConfig, user_config_raw) + + # Update our known mod-time since we know it exists. + self._config_mtime = Path(self._config_path).stat().st_mtime + self._last_config_mtime_check_time = time.time() + + # Go with defaults if we weren't able to load anything. + if out is None: + out = ServerConfig() + + if print_confirmation: + print(f'{Clr.CYN}Valid server config file loaded.{Clr.RST}', + flush=True) + return out + + def _enable_tab_completion(self, locs: Dict) -> None: + """Enable tab-completion on platforms where available (linux/mac).""" + try: + import readline + import rlcompleter + readline.set_completer(rlcompleter.Completer(locs).complete) + readline.parse_and_bind('tab:complete') + except ImportError: + # This is expected (readline doesn't exist under windows). + pass + + def _bg_thread_main(self) -> None: + """Top level method run by our bg thread.""" + while not self._done: + self._run_server_cycle() + + def _handle_term_signal(self, sig: int, frame: FrameType) -> None: + """Handle signals (will always run in the main thread).""" + del sig, frame # Unused. + sys.exit(1 if self._should_report_subprocess_error else 0) + + def _run_server_cycle(self) -> None: + """Spin up the server subprocess and run it until exit.""" + + # Reload our config, and update our overall behavior based on it. + # We do non-strict this time to give the user repeated attempts if + # if they mess up while modifying the config on the fly. + self.load_config(strict=False, print_confirmation=True) + + self._prep_subprocess_environment() + + # Launch the binary and grab its stdin; + # we'll use this to feed it commands. + self._subprocess_launch_time = time.time() + + # Set an environment var so the server process knows its being + # run under us. This causes it to ignore ctrl-c presses and other + # slight behavior tweaks. Hmm; should this be an argument instead? + os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1' + + print(f'{Clr.CYN}Launching server subprocess...{Clr.RST}', flush=True) + binary_name = ('ballisticacore_headless.exe' + if os.name == 'nt' else './ballisticacore_headless') + assert self._ba_root_path is not None + self._subprocess = None + + # Launch! + try: + self._subprocess = subprocess.Popen( + [binary_name, '-cfgdir', self._ba_root_path], + stdin=subprocess.PIPE, + cwd='dist') + except Exception as exc: + self._subprocess_exited_cleanly = False + print( + f'{Clr.RED}Error launching server subprocess: {exc}{Clr.RST}', + flush=True) + + # Do the thing. + try: + self._run_subprocess_until_exit() + except Exception as exc: + print(f'{Clr.RED}Error running server subprocess: {exc}{Clr.RST}', + flush=True) + + self._kill_subprocess() + + assert self._subprocess_exited_cleanly is not None + + # EW: it seems that if we die before the main thread has fully started + # up the interpreter, its possible that it will not break out of its + # loop via the usual SystemExit that gets sent when we die. + if self._interactive: + while (self._interpreter_start_time is None + or time.time() - self._interpreter_start_time < 0.5): + time.sleep(0.1) + + # Avoid super fast death loops. + if (not self._subprocess_exited_cleanly and self._auto_restart + and not self._done): + time.sleep(5.0) + + # If they don't want auto-restart, we'll exit the whole wrapper. + # (and with an error code if things ended badly). + if not self._auto_restart: + self._wrapper_shutdown_desired = True + if not self._subprocess_exited_cleanly: + self._should_report_subprocess_error = True + + self._reset_subprocess_vars() + + # If we want to die completely after this subprocess has ended, + # tell the main thread to die. + if self._wrapper_shutdown_desired: + + # Only do this if the main thread is not already waiting for + # us to die; otherwise it can lead to deadlock. + # (we hang in os.kill while main thread is blocked in Thread.join) + if not self._done: + self._done = True + + # This should break the main thread out of its blocking + # interpreter call. + os.kill(os.getpid(), signal.SIGTERM) + + def _prep_subprocess_environment(self) -> None: + """Write files that must exist at process launch.""" + + assert self._ba_root_path is not None + os.makedirs(self._ba_root_path, exist_ok=True) + cfgpath = os.path.join(self._ba_root_path, 'config.json') + if os.path.exists(cfgpath): + with open(cfgpath) as infile: + bincfg = json.loads(infile.read()) + else: + bincfg = {} + + # Some of our config values translate directly into the + # ballisticacore config file; the rest we pass at runtime. + bincfg['Port'] = self._config.port + bincfg['Auto Balance Teams'] = self._config.auto_balance_teams + bincfg['Show Tutorial'] = False + bincfg['Idle Exit Minutes'] = self._config.idle_exit_minutes + with open(cfgpath, 'w') as outfile: + outfile.write(json.dumps(bincfg)) + + def _enqueue_server_command(self, command: ServerCommand) -> None: + """Enqueue a command to be sent to the server. + + Can be called from any thread. + """ + with self._subprocess_commands_lock: + self._subprocess_commands.append(command) + + def _send_server_command(self, command: ServerCommand) -> None: + """Send a command to the server. + + Must be called from the server process thread. + """ + import pickle + assert current_thread() is self._subprocess_thread + assert self._subprocess is not None + assert self._subprocess.stdin is not None + val = repr(pickle.dumps(command)) + assert '\n' not in val + execcode = (f'import ba._servermode;' + f' ba._servermode._cmd({val})\n').encode() + self._subprocess.stdin.write(execcode) + self._subprocess.stdin.flush() + + def _run_subprocess_until_exit(self) -> None: + if self._subprocess is None: + return + + assert current_thread() is self._subprocess_thread + assert self._subprocess.stdin is not None + + # Send the initial server config which should kick things off. + # (but make sure its values are still valid first) + dataclass_validate(self._config) + self._send_server_command(StartServerModeCommand(self._config)) + + while True: + + # If the app is trying to shut down, nope out immediately. + if self._done: + break + + # Pass along any commands to our process. + with self._subprocess_commands_lock: + for incmd in self._subprocess_commands: + # If we're passing a raw string to exec, no need to wrap it + # in any proper structure. + if isinstance(incmd, str): + self._subprocess.stdin.write((incmd + '\n').encode()) + self._subprocess.stdin.flush() + else: + self._send_server_command(incmd) + self._subprocess_commands = [] + + # Request restarts/shut-downs for various reasons. + self._request_shutdowns_or_restarts() + + # If they want to force-kill our subprocess, simply exit this + # loop; the cleanup code will kill the process if its still + # alive. + if (self._subprocess_force_kill_time is not None + and time.time() > self._subprocess_force_kill_time): + print(f'{Clr.CYN}Force-killing subprocess...{Clr.RST}', + flush=True) + break + + # Watch for the server process exiting.. + code: Optional[int] = self._subprocess.poll() + if code is not None: + + clr = Clr.CYN if code == 0 else Clr.RED + print( + f'{clr}Server subprocess exited' + f' with code {code}.{Clr.RST}', + flush=True) + self._subprocess_exited_cleanly = (code == 0) + break + + time.sleep(0.25) + + def _request_shutdowns_or_restarts(self) -> None: + # pylint: disable=too-many-branches + assert current_thread() is self._subprocess_thread + assert self._subprocess_launch_time is not None + now = time.time() + minutes_since_launch = (now - self._subprocess_launch_time) / 60.0 + + # If we're doing auto-restart with config changes, handle that. + if (self._auto_restart and self._config_auto_restart + and not self._subprocess_sent_config_auto_restart): + if (self._last_config_mtime_check_time is None + or (now - self._last_config_mtime_check_time) > 3.123): + self._last_config_mtime_check_time = now + mtime: Optional[float] + if os.path.isfile(self._config_path): + mtime = Path(self._config_path).stat().st_mtime + else: + mtime = None + if mtime != self._config_mtime: + print( + f'{Clr.CYN}Config-file change detected;' + f' requesting immediate restart.{Clr.RST}', + flush=True) + self.restart(immediate=True) + self._subprocess_sent_config_auto_restart = True + + # Attempt clean exit if our clean-exit-time passes. + # (and enforce a 6 hour max if not provided) + clean_exit_minutes = 360.0 + if self._config.clean_exit_minutes is not None: + clean_exit_minutes = min(clean_exit_minutes, + self._config.clean_exit_minutes) + if clean_exit_minutes is not None: + if (minutes_since_launch > clean_exit_minutes + and not self._subprocess_sent_clean_exit): + opname = 'restart' if self._auto_restart else 'shutdown' + print( + f'{Clr.CYN}clean_exit_minutes' + f' ({clean_exit_minutes})' + f' elapsed; requesting soft' + f' {opname}.{Clr.RST}', + flush=True) + if self._auto_restart: + self.restart(immediate=False) + else: + self.shutdown(immediate=False) + self._subprocess_sent_clean_exit = True + + # Attempt unclean exit if our unclean-exit-time passes. + # (and enforce a 7 hour max if not provided) + unclean_exit_minutes = 420.0 + if self._config.unclean_exit_minutes is not None: + unclean_exit_minutes = min(unclean_exit_minutes, + self._config.unclean_exit_minutes) + if unclean_exit_minutes is not None: + if (minutes_since_launch > unclean_exit_minutes + and not self._subprocess_sent_unclean_exit): + opname = 'restart' if self._auto_restart else 'shutdown' + print( + f'{Clr.CYN}unclean_exit_minutes' + f' ({unclean_exit_minutes})' + f' elapsed; requesting immediate' + f' {opname}.{Clr.RST}', + flush=True) + if self._auto_restart: + self.restart(immediate=True) + else: + self.shutdown(immediate=True) + self._subprocess_sent_unclean_exit = True + + def _reset_subprocess_vars(self) -> None: + self._subprocess = None + self._subprocess_launch_time = None + self._subprocess_sent_config_auto_restart = False + self._subprocess_sent_clean_exit = False + self._subprocess_sent_unclean_exit = False + self._subprocess_force_kill_time = None + self._subprocess_exited_cleanly = None + + def _kill_subprocess(self) -> None: + """End the server subprocess if it still exists.""" + assert current_thread() is self._subprocess_thread + if self._subprocess is None: + return + + print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}', flush=True) + + # First, ask it nicely to die and give it a moment. + # If that doesn't work, bring down the hammer. + self._subprocess.terminate() + try: + self._subprocess.wait(timeout=10) + self._subprocess_exited_cleanly = ( + self._subprocess.returncode == 0) + except subprocess.TimeoutExpired: + self._subprocess_exited_cleanly = False + self._subprocess.kill() + print(f'{Clr.CYN}Subprocess stopped.{Clr.RST}', flush=True) + + +def main() -> None: + """Run the BallisticaCore server manager.""" + try: + ServerManagerApp().run() + except CleanError as exc: + # For clean errors, do a simple print and fail; no tracebacks/etc. + # Any others will bubble up and give us the usual mess. + exc.pretty_print() + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..7899d54 --- /dev/null +++ b/config.yaml @@ -0,0 +1,104 @@ +# To configure your server, create a config.yaml file in the same directory +# as the ballisticacore_server script. The config_template.yaml file can be +# copied or renamed as a convenient starting point. + +# Uncomment any of these values to override defaults. + +# Name of our server in the public parties list. +party_name: Smoothy PRIVATE PARTY + + +# KEEP THIS TRUE .... DONT WORRY PARTY WILL BE PRIVATE ONLY .. LET IT BE TRUE FOR NOW. +party_is_public: true + +# If true, all connecting clients will be authenticated through the master +# server to screen for fake account info. Generally this should always +# be enabled unless you are hosting on a LAN with no internet connection. +authenticate_clients: true + +# IDs of server admins. Server admins are not kickable through the default +# kick vote system and they are able to kick players without a vote. To get +# your account id, enter 'getaccountid' in settings->advanced->enter-code. +admins: +- pb-yOuRAccOuNtIdHErE +- pb-aNdMayBeAnotherHeRE + +# Whether the default kick-voting system is enabled. +#enable_default_kick_voting: true + +# UDP port to host on. Change this to work around firewalls or run multiple +# servers on one machine. +# 43210 is the default and the only port that will show up in the LAN +# browser tab. +port: 43210 + +# Max devices in the party. Note that this does *NOT* mean max players. +# Any device in the party can have more than one player on it if they have +# multiple controllers. Also, this number currently includes the server so +# generally make it 1 bigger than you need. Max-players is not currently +# exposed but I'll try to add that soon. +max_party_size: 8 + +# Options here are 'ffa' (free-for-all) and 'teams' +# This value is only used if you do not supply a playlist_code (see below). +# In that case the default teams or free-for-all playlist gets used. +#session_type: ffa + +# To host your own custom playlists, use the 'share' functionality in the +# playlist editor in the regular version of the game. +# This will give you a numeric code you can enter here to host that +# playlist. +#playlist_code: 12345 + +# Whether to shuffle the playlist or play its games in designated order. +#playlist_shuffle: true + +# If true, keeps team sizes equal by disallowing joining the largest team +# (teams mode only). +#auto_balance_teams: true + +# Whether to enable telnet access. +# IMPORTANT: This option is no longer available, as it was being used +# for exploits. Live access to the running server is still possible through +# the mgr.cmd() function in the server script. Run your server through +# tools such as 'screen' or 'tmux' and you can reconnect to it remotely +# over a secure ssh connection. +#enable_telnet: false + +# Series length in teams mode (7 == 'best-of-7' series; a team must +# get 4 wins) +#teams_series_length: 7 + +# Points to win in free-for-all mode (Points are awarded per game based on +# performance) +#ffa_series_length: 24 + +# If you provide a custom stats webpage for your server, you can use +# this to provide a convenient in-game link to it in the server-browser +# beside the server name. +# if ${ACCOUNT} is present in the string, it will be replaced by the +# currently-signed-in account's id. To fetch info about an account, +# your backend server can use the following url: +# http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE +#stats_url: https://mystatssite.com/showstats?player=${ACCOUNT} + +# If present, the server subprocess will attempt to gracefully exit after +# this amount of time. A graceful exit can occur at the end of a series +# or other opportune time. Server-managers set to auto-restart (the +# default) will then spin up a fresh subprocess. This mechanism can be +# useful to clear out any memory leaks or other accumulated bad state +# in the server subprocess. +#clean_exit_minutes: 60 + +# If present, the server subprocess will shut down immediately after this +# amount of time. This can be useful as a fallback for clean_exit_time. +# The server manager will then spin up a fresh server subprocess if +# auto-restart is enabled (the default). +#unclean_exit_minutes: 90 + +# If present, the server subprocess will shut down immediately if this +# amount of time passes with no activity from any players. The server +# manager will then spin up a fresh server subprocess if +# auto-restart is enabled (the default). +#idle_exit_minutes: 20 + diff --git a/dist/ba_data/data/langdata.json b/dist/ba_data/data/langdata.json new file mode 100644 index 0000000..011794e --- /dev/null +++ b/dist/ba_data/data/langdata.json @@ -0,0 +1,1471 @@ +{ + "lang_names_translated": { + "Turkish": "Türkçe", + "Hindi": "हिंदी", + "Korean": "한국어", + "Hungarian": "Magyar", + "French": "Français", + "Russian": "Русский", + "Serbian": "Српски", + "Gibberish": "Gibberish", + "Italian": "Italiano", + "German": "Deutsch", + "Czech": "Čeština", + "Persian": " فارسی", + "Slovak": "Slovenčina ", + "Polish": "Polski", + "Swedish": "Svenska", + "Vietnamese": "Tiếng Việt ", + "Romanian": "Română", + "Belarussian": "Беларуская", + "Dutch": "Nederlands", + "Danish": "Dansk", + "Indonesian": "Indonesia", + "Ukrainian": "Українська", + "ChineseTraditional": "繁體中文", + "Venetian": "Veneto", + "Greek": "Ελληνικά", + "Arabic": "عربى", + "Croatian": "Hrvatski", + "Portuguese": "Português", + "Chinese": "简体中文", + "Japanese": "日本語", + "Spanish": "Español", + "Esperanto": "Esperanto" + }, + "translation_contributors": [ + "!ParkuristTurist!", + "Даниил Рахов \"DaNiiRuSPlay\"", + "Ярослав \"Noiseaholic\"", + "Cristian Bote \"ZZAZZ\"", + "Muhammad Faqih ''None''", + "GG (9.2)", + "Rudransh Joshi (FireHead)", + "Spielfreake (Garke)", + "Tódor Gábor (Joshua)", + "Артём Зобков (KoLenka)", + "Андрей (Krays)", + "Kamil (Limak09)", + "Kamil Barański (Limak09)", + "Syed Fahrin (Mr.Lemoyne)", + "OyUnBoZaN (NEMUTLUTURKUMDİYENE)", + "Joel RG (Plar&Teporingo)", + "*** Adel NZ. ***", + "/in/dev/", + "1.4.139", + "123", + "123123123", + "Abdullatif Badinjki ExPeRt 1420", + "Mac 143338", + "233", + "26885", + "43210", + "5PH3X", + "99", + "Ivan Santos :)", + "Ромашка :3", + "Roma :D", + "Ihsan Maulana ( @ihsanm27)", + "@sametsunal", + "_Fami", + "Omar a", + "Bruno A.", + "aaalligator", + "aaalligator", + "aadesh", + "Aaron", + "Erik Abbevik", + "Abdo", + "Abduh", + "Abdul", + "Abdulloh", + "Abe", + "abhi", + "AbhinaY", + "Gifasa abidjahsi", + "Abir", + "Abraham", + "Roman Abramov", + "AC", + "adan", + "Adel", + "Rio Adi", + "Rio adi", + "Rayhan Adiansyah", + "Yonas Adiel", + "admin", + "Adonay", + "AdOwn69", + "Adrián", + "Aely", + "Aenigmus", + "Aether", + "Afrizal", + "Aga<3", + "Carlos Mario Agamez", + "ageng", + "Dimitris Aggelou", + "ariyan ahir", + "AHMAD", + "Aufan Ahmad", + "ahmed", + "ahmedzabara", + "Collin Ainge", + "Akbar", + "Bekir Akdemir", + "Aki", + "Abdullah Akkan", + "Berk Akkaya", + "AKYG", + "mohammed al-abri", + "Mohammed al-abri", + "Ali Al-Gattan", + "alaa", + "Manuel Alanis", + "Anna Alanis", + "alanjijuoo7fudu@gmail.com", + "Alej0hio", + "Pedro Alejandro", + "Gabrijel Aleksić", + "Alex", + "Alexander", + "Gros Alexandre", + "Alexey", + "Alexgmihai", + "Alexis", + "Alexis", + "Alexistb2904", + "Alexyze", + "Algene123456", + "Ali", + "Shadiq Ali", + "ALI", + "alireza", + "alirezaalidokht", + "ALISSON", + "Virgile Allard", + "Allinol", + "ahmed alomari", + "Alonso", + "Alper", + "Alpha", + "AlphaT", + "Althaf", + "altidor", + "Oguz Altindal", + "aly", + "Amar", + "alfredo jasper a ambel", + "Amedeo", + "amin.ir", + "Amir", + "amir22games", + "amir234", + "Amirul", + "Ange Kevin Amlaman", + "amr", + "Anandchaursiya", + "AnatoliyanChubukov", + "AnatoliyanChybycov", + "Bryan Enrique Perez de Anda", + "Wodson de Andrade", + "Andre", + "andrea", + "AndreaF", + "David Andrei", + "Andres", + "Andrew", + "Andria", + "Andria.J", + "Andriawan", + "Lazib andriyanto", + "Android", + "Android44561650", + "Andru", + "André", + "Andy", + "krish angad", + "vân anh", + "Aniol", + "Anmol", + "anonymous", + "Antonio", + "Antonio", + "Antoniom", + "Lucas Antunes", + "wassim aoufi", + "apis", + "Sagar April", + "Fernando Araise", + "Muhammad Arief", + "Arin", + "ARSHAD", + "Artem", + "Valentino Artizzu", + "Ashik", + "Ashish", + "Asraf", + "Asshold", + "Atalanta", + "Atilla", + "Atom", + "Audacious7214", + "Aufaghifari", + "Ausiàs", + "Autoskip", + "Avamander", + "awase2020@gmail.com", + "sev alaslam Awd", + "sev alaslam awd", + "Axel", + "ayub", + "masoud azad(fireboy)", + "Md azar", + "Azlan", + "Azoz", + "Burak Karadeniz (Myth B)", + "Myth B.", + "Balage8", + "BalaguerM", + "Peter Balind", + "Balqis", + "Balraj", + "Smutny Bambol", + "Alex Ban", + "Ryan Bandura", + "Bank", + "Ibrahim Baraka", + "Kamil Barański", + "Bardiaghasedipour", + "William Barnak", + "William Barnakk", + "Danillo Rodrigues Barros", + "Zalán Barta", + "Lorenzo Bartolini", + "Petr Barták", + "Basel", + "Bashkot", + "Matthias Bastron", + "Ralph Bathmann", + "Florian Bauernfeind", + "David BAUMANN", + "bayanus", + "Wojtek Bałut", + "Eduardo Beascochea", + "Eduan Bekker", + "ben", + "Mohamed benchrifa", + "Bendy", + "Sérgio Benevides", + "Simon Bengtsson", + "Benjamin", + "benjapol", + "Ori bennov", + "benybrot96", + "Bernardiny", + "Anton Bang Berner", + "Felix Bernhard", + "Davide Bigotto", + "Bima", + "Blackcat2960", + "BlackShadowQ", + "Daniel Block", + "BlueBlur", + "bombsquad", + "Bomby", + "Book", + "Lucas Borges", + "Abel Borso", + "Plasma Boson", + "Cristián Bote", + "Cristian Bote", + "botris", + "Botte", + "bouabdellah", + "Antoine Boulanger", + "Thomas Bouwmeester", + "Bořivoj", + "Paul braga", + "Sammy Braun", + "Brendan", + "Federico Brigante", + "Anderson Brito", + "Broi", + "Brojas", + "BrotheRuzz11", + "Bsam", + "bsam", + "Bsamhero", + "BSODPK", + "Marvin Bublitz", + "Vincent R. Buenaventura", + "Buskebam", + "Buto11", + "ByAdrianYT", + "Mohamad Hossein BZ", + "Christoffer Bünner", + "Cadødø", + "Calet", + "Kenneth Callaghan", + "Federico Campagnolo", + "Henry Abraham Kumul Canche", + "CandyMan101", + "Nicola Canigiani", + "Fabio Cannavacciuolo", + "CANOVER", + "Fedrigo Canpanjoło", + "mark Dave a carposo", + "Fabricio de Carvalho", + "Joshua Castañeda", + "Lisandro Castellanos", + "Catjuanda05", + "CatMax", + "Arthur Cazes", + "CerdoGordo", + "Ceren", + "chang", + "Charlie", + "kalpesh chauhan", + "CheesySquad", + "choi", + "Hans Christensen", + "Attilio Cianci", + "Kajus Cibulskis", + "Mateusz Ciochoń", + "Citytrain", + "Nick Clime", + "Jerome Collet", + "probably my. com", + "Francisco Law Cortez", + "David Cot", + "Nayib Méndez Coto", + "Dylan cotten", + "COVER", + "crac", + "CrazyBear", + "Frederick Cretton", + "Crisroco10", + "crisroco10", + "Cristian", + "Cristóbal", + "Criz", + "Cpt crook", + "Prashanth CrossFire", + "Cryfter", + "cukomus", + "D", + "Dada", + "Daivaras", + "Dakkat", + "Mikkel Damgaard", + "Daniel", + "Daniel3505", + "Iman Darius", + "DarkAnarcy", + "DarkEnergon8", + "Shibin das", + "Dasto", + "Davide", + "DaymanLP", + "Ddávid", + "Die or Dead", + "Привет от детей DeadLine", + "deepjith", + "Gianfranco Del Borrello", + "Gabriel Del Nero", + "delshe", + "Denis", + "Dennis", + "Dennys", + "Alex Derbenew", + "df", + "Guilherme Dias", + "Diase7en", + "ferbie Dicen", + "Diego788", + "DiGGDaGG", + "dikivan2000", + "Dimitriy", + "Martin Dimitrov", + "Diprone", + "djaber djafer", + "Fadhil djibran", + "Alexis Dk", + "DKua", + "dlw", + "DMarci", + "Dmirus", + "Count Su Doku", + "DominikSikora!", + "Gerardo Doro", + "DottorMorte", + "Dragomir", + "Drellsan", + "DrGhast", + "drov.drov", + "Davide DST", + "Bruno Duarte", + "Johann Duco", + "Dudow", + "Dustin", + "Paul Duvernay", + "Edson", + "Glen Edwards", + "ef", + "Ali ehs", + "Eiva", + "EK", + "EKFH", + "avatar por reina del carnaval en la que te lo mando el", + "Rezk ElAdawy", + "ElderLink", + "Elsans320_YT", + "Elskoser", + "ElVolKo", + "Ramy Emad", + "Kürti Emil", + "Muhammed emir", + "EmirSametEr", + "emm", + "EnderDust123", + "EnderKay", + "EnglandFirst", + "enzo", + "Erick", + "Jonas Ernst", + "Shayan Eskandari", + "ESMAEL", + "esmael", + "Esmael", + "Jose espinoza", + "ethanmigueltrinidad", + "ExplosiveDinosaurs.com", + "EXTENDOO", + "Eyder", + "fa9oly9", + "Fabian", + "Luca Facchinetti", + "Facundo", + "Jakub Fafek", + "faizal.faiz.ms@gmail.com", + "FanDolz.", + "Faqih", + "Luiz Henrique Faria", + "FaultyAdventure", + "Putra Riski Fauzi", + "fauziozan.23@gmail.com", + "Shaikh Fazal", + "Fea", + "FearLessCuBer", + "Federico", + "Fedrigo", + "Marco Fabián Feijoó", + "Fernando", + "David Fernández", + "FerranC", + "FertileChannelHD", + "FightBiscuit", + "Filip", + "filip", + "Filip117", + "Filip117", + "Firdaus", + "Daffa Firdaus", + "Aldereus Fire", + "Robert Fischer", + "Kai Fleischmann", + "Iancu Florin", + "Angelo Fontana", + "FortKing", + "Golden Freddy", + "Andrey Fridholm", + "FriskTheHuman303", + "Froshlee14", + "FuckIndoDick", + "Lukas Funk", + "Gustavo FunnyGuard28", + "Erick G", + "Roberto G", + "Fabian G.L.", + "G192", + "Gabriel", + "João Gabriel", + "Gabriele", + "Nihar Gajare", + "GalaxyNinja2003", + "Proff Gamer", + "Eduardo Gamer05", + "Taufiq Gamera", + "Altangerel Ganbaatar", + "Quentin Gangler", + "RUSLAN ABDUL GANI", + "Gaspard", + "krish gator", + "gene.mTs", + "GeoMatHeo", + "GHAIS", + "Omar Ghali", + "GhostGamer", + "Gian", + "Gianfranco", + "Gianluca11", + "Aldi gibran", + "Aidan Gil", + "Giovalli99", + "Giovanny", + "Dc superhero girl", + "Glu10free", + "Mr. Glu10free", + "Jhon Zion N. delos reyes gmail", + "God丶烛龙", + "박준서(PJS GoodNews)", + "Nicola Grassi", + "Nicola Grassi", + "Gerrit Grobler", + "Oliver Grosskloss", + "Alexis Guijarro", + "Guilherme", + "Victor Guillemot", + "Guillermo", + "Guillermo", + "SHOBHIT GUPTA", + "Gurad", + "Max Guskov", + "Rachmat Gusti", + "Tódor Gábor", + "Tymoteusz Górski", + "Thomas Günther", + "H.J.N", + "Hack", + "HackPlayer697", + "hadi", + "hafzanpajan", + "Haidar", + "جود حيدر/joud haidar", + "Joud haidar", + "HamCam1015", + "hamed", + "Zulfikar Hanif", + "Happaphus", + "Hariq", + "Abdi Haryadi", + "Mohammad hasan", + "Hasan", + "Emil Hauge", + "Ergin Haxhijaj", + "Florian Haxhijaj", + "Arian Haxhijaj", + "Hayate", + "Hayate16", + "Lukas Heim", + "Hugues Heitz", + "hellobro", + "Christoffer Helmfridsson", + "Hemra", + "Julian Henkes", + "henry", + "Heraltes", + "boy hero", + "bsam hero", + "herosteve22jajs", + "heymaxi", + "HiImBrala", + "Ayra Hikari", + "Hiking", + "Himesh", + "Yazan Hinnawi", + "Daffaa Hisyaam", + "Trung Hiếu", + "Nabil Hm", + "Nguyen Dang Hieu Hoa", + "Minh Hoang", + "Robin Hofmann", + "hola", + "Sebasian Varela Holguin", + "Jeremy Horbul", + "hosein", + "Hosein", + "hoseinا", + "Phan Lê Minh Hoàng", + "Jorge Isaac Huertero", + "Umair Hussain", + "Hussam", + "Adrian Höfer", + "Davide Iaccarino", + "iBearzGaming", + "Iboyindyza", + "Ibrahim", + "Ignacio", + "IgnUp21", + "Igomen15", + "Igor", + "IL_SERGIO", + "!YamGila (Syed Ilham)", + "illonis", + "Ily77788", + "Ilya", + "IlyxaGold", + "d imitris", + "Nik ali imran", + "Indecisive", + "indieGEARgames", + "Indohuman", + "Anestis Ioakimidis", + "Dragomir Ioan", + "Isa", + "Tobias Dencker Israelsen", + "Kegyes István", + "Itamar", + "ivan", + "Ivan", + "iViietZ", + "Al jabbar", + "Jacek", + "Jack556", + "Jhon Jairo", + "wahid jamaludin", + "Tarun Jangra", + "Aleksandar Janic", + "Martin Jansson", + "JasimGamer", + "Jbo", + "JCIBravo", + "Jd", + "JDOG253", + "Jeemboo", + "Jembhut", + "CrackerKSR (Kishor Jena)", + "CrackerKSR (Kishor Jena)", + "CrackerKSR (Kishor Jena))", + "Jeroen", + "Jetty", + "Jeulis", + "Jewellbenj", + "Zhou Jianchu", + "jimmy", + "Jiren", + "jitues", + "JM", + "Joan", + "JoaoVitorBF", + "joaquin", + "Lex Johannes", + "Steven john", + "Ksteven john", + "John", + "Johnny", + "joke", + "Jonatas", + "Jop", + "Joseph", + "Joshep", + "joshuapiper", + "Jossiney", + "juanramirez", + "Jules", + "juse", + "Justine", + "Jyothish", + "Oliver Jõgar", + "Nackter Jörg", + "Calvin Jünemann", + "Sayooj k", + "K'veen", + "Kacper", + "kadaradam", + "Efe Kahraman", + "KalakCZ", + "Adam Kalousek", + "kalpesh", + "kalyan", + "Kalyan", + "Kamal", + "Smurfit Kappa", + "Mustafa Karabacak", + "karabin", + "Burak Karadeniz", + "Burak karadeniz", + "Burak Karadeniz(MythB)", + "Daniel Karami", + "Karim", + "Kasra", + "Kaushik", + "KawaiiON", + "KD", + "Mani kelidari", + "Kelmine", + "Kenjie", + "Kerim", + "Khaild1717", + "muh khairul", + "Khalid", + "khalio", + "$RICO$ KhevenMito", + "Khwezi", + "kibro", + "Joop Kiefte", + "killer", + "killer313", + "King", + "KingCreeps", + "kinnie", + "kira", + "kirill", + "KirillMasich", + "Andrew Kmitto", + "Philipp Koch", + "Kolmat", + "komasio", + "KomodoRec", + "Niko Koren", + "Nikolay Korolyov", + "Kostas", + "Viktor Kostohryz", + "Mikhail Krasovsky", + "kripanshu", + "kris", + "krishAngad", + "krishuroy", + "kroш)", + "Patel krumil", + "Krunal", + "sarath kumar", + "Alok Kumar", + "Aapeli Kumpulainen", + "Aldhiza Kurniawan", + "Rasyid Kurniawan", + "Arif Kurniawan", + "Wahyu Kurniawan", + "KurtWagner", + "Daanii Kusnanta", + "Kyle", + "Jan Kölling", + "L_JK", + "John Patrick Lachica", + "m a lakum", + "Nicklas Larsen", + "K. Larsen", + "Shin Lasung", + "Sampo Launonen", + "Lazered", + "Lazydog", + "Elia Lazzari", + "Mick Lemmens", + "Leo", + "Lester", + "Szajkajó Levente", + "Szajkajkó Levente", + "Johannes Lex", + "Gastón Lezcano", + "Shuaibing Li", + "Tred Li", + "Juan Liao", + "Nicola Ligas", + "Limak09", + "lin", + "Dustin Lin", + "Kyle Lin", + "LiteBalt", + "LittleNyanCat", + "Lkham", + "Loex", + "Loko", + "Longkencok", + "Longkencok", + "looooooooou", + "LordHiohi", + "Lordigno", + "lorenzo", + "Lostguybrazil", + "mian louw", + "Jordan Vega Loza", + "Chenging Lu", + "Chengming Lu", + "João Lucas", + "Simone Luconi", + "Ludicrouswizard", + "satrio ludji", + "Ludovico", + "Jose Luis", + "luislinares", + "luispro25", + "Luka", + "Luke", + "Lukman", + "Lukman", + "Hermanni Luosujärvi", + "Lurã", + "Geogre Lyu", + "M.R.T", + "MaceracıMS", + "Samuel Maciel", + "Djawad madi", + "Mads Beier Madsen", + "Mahan", + "Ondřej Mahdalík", + "mahdi", + "mahdimahabadi", + "Mahmoud", + "maicol", + "Majestozão", + "Maks1212", + "Malaysian", + "MAMAD", + "Mani", + "Manimutharu", + "Ahmed Mansy", + "Manu", + "Mapk58", + "Marcel", + "Marchella", + "Marcin", + "Marco", + "Marcin Marek", + "Mariel", + "Marin", + "mariuxoGaming", + "Stefan Markovic", + "Marouene", + "Marošsko", + "Martin", + "martin", + "Philip Martin", + "MartinZG007", + "Martín", + "Taobao Mascot", + "Masood", + "Mathias", + "matias", + "matj1", + "Eduardo de Matos", + "Matteo", + "Matthias", + "Federico Mazzone", + "Andrea Mazzucchelli", + "Medic别闹我有药", + "German Medin", + "Martin Medina", + "Mehrdad", + "Kevin Mejía", + "MereCrack", + "Mert", + "MGH", + "Mick", + "Miguel", + "Miguelterrazas123", + "Mikael", + "mike", + "Milaner", + "Milk3n", + "Fabio Milocco", + "mimis", + "Mina", + "minh123456789thcsvk", + "MinhAn19203", + "Azfar Ahmed Mirza", + "Deepak Mishra", + "Skramh Miugrik", + "Mizzzzon", + "Mk", + "MKG", + "mobin", + "Moh", + "Mohamadali", + "Mohamed", + "Mohamed", + "mohammad", + "Mohammad", + "Mohammad11dembele", + "Mohammed", + "1n Mohhaamad", + "MONIRIE", + "carlos montalvo", + "Carlos Montalvo", + "Ederson Moraes", + "Eduardo Moreira", + "Danteo Moriarty", + "Kabir morya", + "Moses", + "mr", + "mr.Dark", + "Mr.Smoothy", + "MrDaniel715", + "MrGlu10free", + "Mrmaxmeier", + "MrNexis", + "Msta", + "Muhammed Muhsin", + "MujtabaFR", + "Muni", + "Mohammed Musthafa", + "Hisham Musthafa", + "MUZAMMIL", + "Mwss", + "mythbrk00@gmail.com", + "Sajti Márk", + "Samuel Mörling", + "Luca Müller", + "Nacho", + "nacho", + "Nagaarjun(pongal)", + "Nasser", + "Natasja", + "Nathan", + "naveentamizhan123456", + "Nayan", + "Nazar_1232", + "Behnam Nazemi", + "nazroy", + "Ndrio°o", + "NecroMeerkat", + "Neel", + "Nemeil", + "nevergpdia", + "Andrew Nevero", + "Newt", + "Hamid Neyzar", + "Nicholas", + "NichtEnno", + "Nico", + "Nico-iVekko", + "Nicola", + "Nicola", + "Nicolas", + "Frederik Nielsen", + "Nikali2007", + "Nima", + "XU NING", + "طارق محمد رضا سعيد NinjaStarXD", + "nino", + "Nintendero65", + "Nnubes256", + "Bu nny", + "Noam", + "Simone Nobili", + "NofaseCZ", + "Noisb", + "Noobslaya101", + "noorjandle1", + "Petter Nordlander", + "Ntinakos555", + "NullWizard", + "Dhimas Wildan Nz", + "Ognjen", + "Bastián Olea", + "Nikita Oleshko", + "Omar", + "On3GaMs", + "Adam Oros", + "Andrés Ortega", + "Zangar Orynbetov", + "Osmanlı2002", + "Osmanys", + "pack", + "PALASH", + "Giorgio Palmieri", + "Abhinay Pandey", + "PangpondTH", + "Gavin Park", + "Pastis69", + "Sagar patil", + "pato", + "patrick", + "paulo", + "Dominik Pavešić", + "PC189085", + "PC192082", + "pc192089", + "Pedro", + "Jiren/Juan Pedro", + "Peque", + "Jura Perić", + "Panumas Perkpin", + "Pero", + "Khoi Pete", + "Kacper Petryczko", + "pett", + "petulakulina", + "Petulakulina", + "Pez", + "Đào Xuân Phi", + "Philip", + "Philipp", + "piga", + "Stefano Pigozzi", + "Mario Donato Pilla", + "Danilo \"Logan\" Pirrone", + "PivotStickfigure12", + "Pixelcube", + "pixil", + "PizzaSlayer64", + "Elian Pj", + "Broi PL", + "Ziomek PL", + "Anestis Plithos", + "Pluisbaard", + "Jaideep Kumar PM", + "podolianyn", + "Pooya", + "pouriya", + "pranav", + "Luca Preibsch", + "Fabian Prinz", + "Private0201", + "Priyanshu", + "psychatrickivi12", + "pszlklismo", + "Pulidomedia.com", + "haris purnama", + "GABRIEL PUTRICK", + "Gangler Quentin", + "QŴE", + "Anbarasan R", + "Felo Raafat", + "Tim Rabatr", + "RadicalGamer", + "RafieMY", + "raghul", + "khaled rahma", + "Rayhan Rahmats", + "Ростислав RAMAGISTER", + "Rostislav RAMAGISTER", + "1. Ramagister", + "Lucas Ramalho", + "Rahul Raman", + "Vicente Ramirez", + "ramon", + "Randlator", + "Random_artz__", + "Rares", + "rashid", + "Yudha Febri Rastuama", + "Mayank Ravariya", + "Serious796 (Mayank Ravariya)", + "Ravi", + "Dhafin Rayhan", + "RayonLaser", + "Rayze", + "Razil", + "Jaiden Razo", + "RCSV159", + "realSamy", + "REDEJCR", + "redyan", + "De'Viren Reed", + "Cornelius Reimann", + "releaseHUN", + "renas", + "Renārs", + "Devair Restani", + "RetroB", + "Torsten Reuters", + "rexKis", + "Victor Jesus Arroyo Reyes", + "Mohammad Reza", + "rian", + "Bruno Ricardo", + "Riccardo", + "Rico", + "Ridzuan", + "Samuel Rieger", + "RieJoemar", + "Jeroen Rinzema", + "RioAdir", + "Max Rios", + "Rio枫叶", + "RiPe16", + "Rishabh", + "Rivki", + "rizaldy", + "Rodbert", + "Rodrigo", + "Marco Rodríguez", + "Giovanni Rodríguez", + "Rohan", + "Rohit", + "Bihary Roland", + "Jericho roldan", + "Roman", + "Mathias Romano", + "Ronald", + "Ronianagranada", + "roninjhie@gmail.com", + "Rori", + "Mario Roveda", + "Roy", + "Rubanen", + "kaj rumpff", + "Kaj Rumpff", + "Dosta Rumson", + "Hong Ruoyong", + "Philip Ruppert", + "LiÇViN:Cviatkoú Kanstançin Rygoravič", + "Ricky Joe S.Flores", + "Rami Sabbagh", + "Justin Saephan", + "sahel", + "Abdullah Saim", + "Audinta Sakti", + "Bassam bu salh", + "Bsam bu salh", + "M. Rizki Agus Salim", + "Salted", + "Guilherme Santana", + "Diamond Sanwich", + "SAO_OMH", + "Dimas Saptandi", + "Sara", + "ahmad sarnazih", + "sathish", + "sattar", + "Saverio", + "Jhon Rodel Sayo", + "Christiaan Schriel", + "Hendrik Schur", + "Semen", + "Mihai Serbanica", + "Daniel Balam Cabrera Serrano", + "Yefta Aditya Setiawan", + "sgx", + "Black Shadow", + "ShadowQ", + "shafay", + "Manan Shah", + "Sharvesh", + "Nalam Shashwath", + "Haige Shi", + "Shayan Shokry", + "Dominik Sikora", + "Sebastian Silva", + "Skick", + "sks", + "Max Sky", + "SlayTaniK", + "Igor Slobodchuk", + "Rasim Smaili", + "Nicola Smaniotto", + "Nico Smit", + "Snack", + "Mahteus Soares", + "Matheus Soares", + "sobhan", + "Nikhil sohan", + "SoK", + "Soto", + "SpacingBat3", + "Jack sparrow", + "speddy16", + "Spielfream", + "Spy", + "sss", + "Danny Stalman", + "stampycat", + "Bartosz Staniszewski", + "Stare", + "StarFighter", + "Stealives", + "Steffo", + "stelios", + "Stephanie", + "stephen", + "Janis Stolzenwald", + "SYED EPIC STUDIOS", + "sun.4810", + "Samet Sunal", + "sundar", + "Sven", + "Shannon Sy", + "syaifudib", + "Daniel Sykora", + "Sz™", + "Jorge Luis Sánchez", + "Daniel Sýkora", + "Arung Taftazani", + "Juancho Talarga", + "Emre Talha(Alienus)", + "talopl123", + "Talrev134", + "Kaustubh Tando", + "Kaustubh Tandon", + "Dmytro Tarasenko", + "Tarma", + "tarun", + "Tauras", + "tdho", + "Teals53", + "Teapoth", + "Michael Tebbe", + "Teforteem7395", + "Tejas", + "Nemanja Tekić", + "Marcel Teleznob", + "TempVolcano3200", + "Yan Teryokhin", + "testwindows8189", + "tgd4", + "Than", + "Thanakorn7215", + "thatFlaviooo", + "The_Blinded", + "thejoker190101", + "TheLLage", + "TheMikirog", + "Theo", + "Thiago_TRZ", + "Trevon Thrasher", + "Tiberiu", + "Cristian Ticu", + "Robert Tieber", + "TIGEE", + "Tim", + "Tingis2", + "Thura Tint", + "Nishant Tiwari", + "Toloche", + "Toloche", + "Tom", + "Juan Pablo Montoya Tomalá", + "TomasNoobCz", + "tomo", + "tongtong", + "Tory", + "tozeleal", + "TozeLeal", + "Trung Hieu Le Tran", + "Translator", + "Trivago", + "El Trolax", + "tseringlama", + "Konstantin Tsvetkov", + "Kontantin Tsvetkov", + "Tudikk", + "Jan Tymll", + "Zoltán Tóth", + "uDinnoo", + "Cristian Ugalde", + "Atchy-Dalama--Ancelly Ulrich", + "Syed Umar", + "Unknown", + "Uros", + "clarins usap", + "Uzinerz", + "Vader", + "Valentin", + "Valkan1975", + "Ante Vekić", + "Malte van het Veld", + "veme312", + "Venemos", + "Dmitry \"SqdDoom\" Verigo", + "Deepanshu Verma", + "Jop Vernooij", + "Via", + "Victor", + "paulo victor", + "Vigosl", + "vijay", + "vinicius", + "vinoth", + "VTOR", + "Fernando Véliz", + "Vít", + "Steven Völker", + "O mae wa", + "Nick Waas", + "Alland Christian Wagan", + "Julian Wagner", + "Shaiful Nezan Bin Abdul Wahid", + "wahyu", + "Vaibhav Wakchaure", + "Will Wang", + "Simon Wang", + "Tilman Weber", + "webparham", + "Wesley", + "whitipet", + "Wido2000", + "wildanae", + "Will", + "william", + "Windyy", + "Tobias Wohlfarth", + "Doni Wolf", + "Tommy Wong", + "WonkaWoe", + "Moury ji world", + "wsltshh", + "WurstSaft", + "Xavier", + "Francisco Xavier", + "xbarix123897", + "Peque XD", + "Xizruh", + "xxonx8", + "Ajeet yadav", + "yahya", + "Yamir", + "Yantohrmnt401", + "amr yasser", + "YellowTractor", + "Yasin YILMAZ", + "yoksoudraft", + "Kenneth Yoneyama", + "yossef", + "youcef", + "Yousuf", + "Yovan182Sunbreaker", + "YRTKING", + "Yrtking", + "All Star YT", + "Yudhis", + "yugo", + "yullian", + "NEEROOA Muhammad Yusuf", + "Yuuki", + "yyr_rs", + "Sam Z", + "z", + "Z@p€g@m€r", + "Dawn Zac", + "Zac", + "Zaidan64GT", + "Zain", + "Zajle", + "Zakaria\"Colonel_Bill\"Amtoug", + "Karol Zalewski", + "Zangar", + "ZaraMax", + "zecharaiah", + "Daniele Zennaro", + "zfuw668", + "Alex Zhao", + "Riven Zhao", + "Doge Zhao", + "jim ZHOU", + "Mohammad ziar", + "zJairO", + "ZkyweR", + "Nagy Zoltán", + "Lukáš Zounek", + "ZpeedTube", + "Adeel (AdeZ {@adez_})", + "|_Jenqa_|", + "¥¥S.A.N.A¥", + "Danijel Ćelić", + "Štěpán", + "Cristian Țicu", + "Cristian Țicu", + "Μπαρλάς Παύλος-Ιάσονας", + "Роман Абрамо", + "Роман Абрамов", + "Андрій", + "Богдан", + "Тот самый Вильян", + "опять Вильян", + "Влад", + "Евгений(Eugene)", + "Юстин Иглин", + "Игор", + "Кирилл", + "Кирилл Климов", + "Климов", + "Андрей Коваленко", + "Ваня Марков", + "Игор Милановић", + "Драган Милановић", + "Снежана Милановић", + "Марко Милановић", + "Михаил", + "Арсений Мостовщиков", + "Принцип", + "Михаил Радионов", + "Рома", + "Кирилл Рябцев", + "ZEPT\"Александр Фартунов\"", + "Өмүрзаков Эрсултан", + "данил", + "куатжан", + "михаил", + "boba (Бодік) доперекладав Укр мову", + "Пук-пук пук-пук", + "Қуатжан", + "қуатжан", + "اا", + "احمد اسامه", + "احمد سني اسماعيل", + "البطل", + "بسام البطل", + "ابو العواصف2020", + "اوتاكوDZ", + "بساام", + "جود", + "حسين حساني", + "جود حيدر", + "محمد خالد", + "امید رضازاده", + "محمد وائل سلطان", + "ص", + "عبداللہ صائم", + "boy hero بسام بو صالح", + "Adel NZ. | عادل", + "عبده", + "محمد کیان عرفان", + "محمد حسن عزیزی", + "علی", + "سيد عمر", + "امیر محمد", + "اللهم صل على محمد وآل محمد", + "سعید مهجوری", + "مهدی", + "سید احمد موسوی", + "سید احمد موسوی", + "عادل ن.", + "عادل نوروزی", + "ه۶۹", + "انا يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا", + "١٢٣٤٥", + "علیرضا پودینه", + "वेदाँश त्यागी", + "वेदाँश त्यागी", + "അർഷഖ് ഹസ്സൻ", + "วีรภัทร", + "แมวผงาด(JuniorMeowMeow)", + "✰ℭØØҜĬ£$✰", + "JPnatu なつ", + "クリーバー", + "ㅇㅇ", + "丁光正", + "中国玩家(日文区)", + "中国玩家ChinesePlayer", + "刘铭威", + "别闹我有药", + "别闹我有药/Medic", + "别闹我有药Medic", + "夏神(后期汉化修正)", + "小黑猫", + "张帅", + "徐安博", + "志夏", + "志夏。", + "志夏君deron", + "枫夜", + "毛毛毛大毛", + "熊老三", + "神仙", + "鲲鹏元帅", + "꧁ℤephyro꧂", + "꧁ℤephyro꧂", + "권찬근", + "김원재", + "넌", + "박건희", + "김대중 부관참시", + "사람사는 세상", + "신라성", + "이지민", + "일베저장소", + "전감호", + "BombsquadKorea 네이버 카페", + "The Bomboler 💣" + ] +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/arabic.json b/dist/ba_data/data/languages/arabic.json new file mode 100644 index 0000000..a3e789d --- /dev/null +++ b/dist/ba_data/data/languages/arabic.json @@ -0,0 +1,1848 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "لا يمكن لأسماء الحِسابَات ان تحتوي إيموجي أو حروف خاصة", + "accountProfileText": "معلومات اللاعبين", + "accountsText": "حسابات", + "achievementProgressText": "${TOTAL} من اصل ${COUNT} انجازاتك:انجزت", + "campaignProgressText": "تقدم الحملة [HARD]:${PROGRESS}", + "changeOncePerSeason": "يمكنك تغييره مرة واحدة في الموسم", + "changeOncePerSeasonError": "يجب عليك الانتظار حتى الموسم القادم لتغيير هذا مجددا (${NUM} أيام )", + "customName": "الإسم المخصص", + "linkAccountsEnterCodeText": "أدخل الرمز", + "linkAccountsGenerateCodeText": "أنشئ رمز", + "linkAccountsInfoText": "(مشاركة تقدمك مع الاجهزة الاخرى)", + "linkAccountsInstructionsNewText": "لربط حسابين،- انشئ رمز من الجهاز المراد انشاء الحساب فية*\n- ًوقم بأدخال الرمز في جهاز المربوط بة حساب مسبق\n\nالبيانات من الحساب الاول سوف يتم مشاركتها بين الجهازين*\n\n من الحسابات كحد اقصى ${COUNT} يمكنك انشاء*\n\n هام : اربط حسابات غير مستخدمة والتي تكون خاصة بك ومع اصدقاء يمكن الوثوق بهم\n\nلا يمكنك ان تلعب بنفس الحساب في جهازين في انٍ واحد", + "linkAccountsInstructionsText": "لربط حسابين, انتج كود على احد الحسابين \nو ادخل هذا الكود على الاخر.\nالتقدم و المخزون سيشتركا.\nيمكنك ربط حتى ${COUNT} حسابات.\n\nكن حذراً; هذا لا يمكن استرجاعه", + "linkAccountsText": "ربط حساب", + "linkedAccountsText": ": حساباتي المرتبطة", + "nameChangeConfirm": "?${NAME} هل تريد تغير اسم حسابك الى", + "resetProgressConfirmNoAchievementsText": "سوف يحذف هذا الخيار تقدمك في الحملات التعاونية ولم يحذف تذاكرك\nلا يمكن إلغاء هذا الخيار\nهل أنت متأكد ؟", + "resetProgressConfirmText": ":عند موافقتك على هذا الاخيار سوف يتم\n(حذف انجازاتك وتقدمك الحالي(لكن لن تخسر تَذَاكِرك\nاذا وافقت على هذا القرار لا يمكنك تراجع عنه\nهل أنت متأكد؟", + "resetProgressText": "إمسح تقدمك", + "setAccountName": "حدد إسم للحساب", + "setAccountNameDesc": "..اختر اسم لحسابك\nيمكنك اختيار نفس اسم حساباتك الاخرى\nولاكن يجب ان يكون مختلف قليلاً", + "signInInfoText": "قم بتسجيل دخولك لتجمع بطاقات, وتتحدى الاعبين حول العالم\nو لحفظ ونشر تقدمك عبر الاجهزة", + "signInText": "تسجيل الدخول", + "signInWithDeviceInfoText": "الحساب التلقائي متوفر فقط على هذا الجهاز", + "signInWithDeviceText": "سجل دخولك بحساب الجهاز", + "signInWithGameCircleText": "Game Circle سجل دخولك بواسطة", + "signInWithGooglePlayText": "Google Play سجل الدخول عبر", + "signInWithTestAccountInfoText": "(نوع حساب ارثي; استخدم حسابات الجهاز متجهه للامام)", + "signInWithTestAccountText": "سجل الدخول مع اختبار الحساب", + "signOutText": "تسجيل الخروج", + "signingInText": "...جاري تسجيل دخولك", + "signingOutText": "...جاري تسجيل خروجك", + "testAccountWarningOculusText": "تحذير: انت تقوم بتسجيل الدخول باستخدام حساب تجريبي.\nسيستبدل بحساب حقيقي خلال هذا العام الذي من خلاله\nسوف تقدر على شراء البطاقات ومزايا أخرى.\n\nإلى الان يمكنك الحصول على جميع البطافات في اللعبة.\n(على الرغم من ذلك، قم بالحصول على حساب متقدم مجانا)", + "ticketsText": "بطاقاتك الحالية:${COUNT}", + "titleText": "الحساب", + "unlinkAccountsInstructionsText": "حدد حسابا لإلغاء ربطة", + "unlinkAccountsText": "إلغاء ربط الحسابات", + "viaAccount": "(${NAME} عبر الحساب)", + "youAreSignedInAsText": ": قمت بتسجيل الدخول كـ" + }, + "achievementChallengesText": "إنجازات التحديات", + "achievementText": "إنجاز", + "achievements": { + "Boom Goes the Dynamite": { + "description": "TNT اقتل 3 خصوم بأستخدام صندوق", + "descriptionComplete": "TNTتم قتل 3 خصوم بصندوق ال", + "descriptionFull": "${LEVEL} اقتل 3 خصوم بالمتفجِّرات في", + "descriptionFullComplete": "${LEVEL} تم قتل 3 خصوم بالمتفجِّرات في", + "name": "انفجار قادم من الديناميت" + }, + "Boxer": { + "description": "فز بدون استخدامك للقنابل", + "descriptionComplete": "لقد فزت بدون استخدام القنابل", + "descriptionFull": "قم بإكمال ${LEVEL} بدون أستخدام أي قنابل", + "descriptionFullComplete": "أكمل ${LEVEL} بدون أستخدام أي قنابل", + "name": "مُلاكِمْ" + }, + "Dual Wielding": { + "descriptionFull": "{اتصل بجهازي تحكم عن بعد {جهاز او تطبيق", + "descriptionFullComplete": "{متصل بجهازي تحكم {جهاز او تطبيق", + "name": "اللكمة المزدوجة" + }, + "Flawless Victory": { + "description": "انتصر بدون تعرض للأذى", + "descriptionComplete": "لقد فزت بدون تعرضك للأذى", + "descriptionFull": "انتصر في ${LEVEL} بدون تعرضك للأذى", + "descriptionFullComplete": "لقد فزت في ${LEVEL} بدون تعرضك للأذى", + "name": "الفوز المستحق" + }, + "Free Loader": { + "descriptionFull": "إبدأ بلعب وضع الحر للجميع مع لاعبين أو أكثر", + "descriptionFullComplete": "تم بدء لعبة بوضع الحرية للجميع مع لاعِبَيْنْ أو أكثر", + "name": "الفريق المجاني" + }, + "Gold Miner": { + "description": "اقتل 6 خصوم بأستخدام الألغام الأرضيَّة", + "descriptionComplete": "تمَّ قتل 6 خصوم باستخدام الألغام الأرضيِّة", + "descriptionFull": "بأستخدام الألغام الأرضيَّة ${LEVEL} اقتل 6 خصوم في", + "descriptionFullComplete": "بأستخدام الألغام الأرضيَّة ${LEVEL} تمَّ قتل 6 خصوم في", + "name": "منقب الذهب" + }, + "Got the Moves": { + "description": "انتصر بدون استخدام اللكمات او القنابل", + "descriptionComplete": "لقد انتصرت بدون استخدامك للكمات او القنابل", + "descriptionFull": "بدون استخدام اللكمات أو القنابل ${LEVEL} فز في", + "descriptionFullComplete": "بدون استخدام اللكمات أو القنابل ${LEVEL} لقد ربحت في", + "name": "الاسلحة المخفية" + }, + "In Control": { + "descriptionFull": "(قم بتوصيل جهاز تحكم (جهاز أو تطبيق", + "descriptionFullComplete": "(تم توصيل جهاز تحكم (جهاز أو تطبيق", + "name": "تحت التحكم" + }, + "Last Stand God": { + "description": "سجل 1000 نقطة", + "descriptionComplete": "!لقد سجلت 1000 نقطة", + "descriptionFull": "${LEVEL} سجِّل 1000 نقطة في", + "descriptionFullComplete": "${LEVEL} لقد سجَّلت 1000 نقطة في", + "name": "القائد ${LEVEL}" + }, + "Last Stand Master": { + "description": "سجل 250 نقطة", + "descriptionComplete": "!سجلت 250 نقطة", + "descriptionFull": "${LEVEL} سجِّل 250 نقطة في", + "descriptionFullComplete": "${LEVEL} لقد سجَّلت 250 نقطة في", + "name": "معَلِّم ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "سجل 500 نقطة", + "descriptionComplete": "لقد سجَّلتَ 500 نقطة", + "descriptionFull": "${LEVEL} سجِّل 500 نقطة في", + "descriptionFullComplete": "${LEVEL} لقد سجَّلت 500 نقطة في", + "name": "ساحر ${LEVEL}" + }, + "Mine Games": { + "description": "اقتل 3 خصوم بإستخدام الالغام الأرضيَّة", + "descriptionComplete": "لقد قتلت 3 خصوم بلألغام الأرضيَّة", + "descriptionFull": "${LEVEL} اقتل 3 خصوم باستخدام الألغام الأرضيَّة في", + "descriptionFullComplete": "${LEVEL} لقد قتلت 3 خصوم بإستخدام الألغام الأرضيَّة في", + "name": "ألعاب الألغام" + }, + "Off You Go Then": { + "description": "إرمِ 3 خصوم خارج الحلبة", + "descriptionComplete": "لقد رميت 3 خصوم خارج الحلبة", + "descriptionFull": "${LEVEL} ارمِ 3 خصوم خارج الحلبة في", + "descriptionFullComplete": "${LEVEL} لقد رميت 3 خصوم خارج الحلبة في", + "name": "المنجنيق البشري" + }, + "Onslaught God": { + "description": "سجل 5000 نقطة", + "descriptionComplete": "!لقد سجلت 5000 نقطة", + "descriptionFull": "${LEVEL} سجِّل 5000 نقطة في", + "descriptionFullComplete": "${LEVEL} لقد سجَّلتَ 5000 نقطة في", + "name": "إله ${LEVEL}" + }, + "Onslaught Master": { + "description": "سجل 500 نقطة", + "descriptionComplete": "لقد سجَّلتَ 500 نقطة", + "descriptionFull": "${LEVEL} سجِّل 500 نقطة في", + "descriptionFullComplete": "${LEVEL} لقد سجَّلتَ 500 نقطة في", + "name": "${LEVEL} معلِّم" + }, + "Onslaught Training Victory": { + "description": "اهزم كل الموجات", + "descriptionComplete": "لقد هزمت كل الموجات", + "descriptionFull": "${LEVEL} اهزم كل الموجات في", + "descriptionFullComplete": "${LEVEL} اهزم كل الموجات في", + "name": "${LEVEL} انتصار" + }, + "Onslaught Wizard": { + "description": "سجل 1000 نقطة", + "descriptionComplete": "لقد سجَّلت 1000 نقطة", + "descriptionFull": "${LEVEL} سجِّل 1000 نقطة في", + "descriptionFullComplete": "${LEVEL} سجِّل 1000 نقطة في", + "name": "${LEVEL} ساحر" + }, + "Precision Bombing": { + "description": "powerups فز بدون اي", + "descriptionComplete": "powerups لقد فزتَ بدون أي", + "descriptionFull": "power-ups بدون اي ${LEVEL} فز في", + "descriptionFullComplete": "power-ups بدون اي ${LEVEL} لقد فزتَ في", + "name": "دقة القصف" + }, + "Pro Boxer": { + "description": "فز دون استخدام أي قنابل", + "descriptionComplete": "لقد فُزْتَ بدون استخدام أي قنابل", + "descriptionFull": "بدون استخدام اي قنابل ${LEVEL} اكمل", + "descriptionFullComplete": "بدون استخدام أي قنابل ${LEVEL} لقد اكْمَلْتَ", + "name": "الملاكم البارع" + }, + "Pro Football Shutout": { + "description": "فز بدون ان تسمح للاشرار بالتسجيل", + "descriptionComplete": "لقد فُزْتَ بدون السَّماح للأشرار بالتَّسجيل", + "descriptionFull": "بدون أن تسمح للأشرار بالتَّسجيل ${LEVEL} فز في", + "descriptionFullComplete": "بدون السَّماح للأشرار بالتَّسجيل ${LEVEL} لقد فُزْتَ في", + "name": "${LEVEL} انتصار ساحق في" + }, + "Pro Football Victory": { + "description": "فز في اللعبة", + "descriptionComplete": "لقد فُزْتَ في اللعبة", + "descriptionFull": "${LEVEL} فز في اللعبة في", + "descriptionFullComplete": "${LEVEL} لقد فُزْتَ في اللعبة في", + "name": "${LEVEL} النصر" + }, + "Pro Onslaught Victory": { + "description": "اهزم كل الموجات", + "descriptionComplete": "لقد هزمْتَ كل الموجات", + "descriptionFull": "${LEVEL} اهزم كل الموجات في", + "descriptionFullComplete": "${LEVEL} لقد هزمتَ كل الموجات في", + "name": "${LEVEL} نصر" + }, + "Pro Runaround Victory": { + "description": "اكمل كل الموجات", + "descriptionComplete": "اكمل كل الموجات", + "descriptionFull": "${LEVEL} اكمل كل الموجات في", + "descriptionFullComplete": "${LEVEL} لقد اكملتَ كل الموجات في", + "name": "${LEVEL} النصر" + }, + "Rookie Football Shutout": { + "description": "فز بدون السماح للأشرار بإحراز النقاط", + "descriptionComplete": "لقد فُزْتَ بدون السَّماح للأشرار بإحراز النِّقاط", + "descriptionFull": "بدون السَّماح للأشرار بإحراز النِّقاط ${LEVEL} فُزْ في", + "descriptionFullComplete": "بدون السَّماح للأشرار بإحراز النِّقاط ${LEVEL} لقد فُزْتَ في", + "name": "${LEVEL} إنتصار ساحق" + }, + "Rookie Football Victory": { + "description": "فز في المباراة", + "descriptionComplete": "فاز في المباراة", + "descriptionFull": "${LEVEL} فز المباراة في", + "descriptionFullComplete": "${LEVEL} فاز المباراة في", + "name": "${LEVEL} النصر" + }, + "Rookie Onslaught Victory": { + "description": "هزيمة كل الجولات", + "descriptionComplete": "هزم كل الجولات", + "descriptionFull": "${LEVEL} هزيمة كل الجولات في", + "descriptionFullComplete": "${LEVEL} هزم كل الجولات في", + "name": "${LEVEL} النصر" + }, + "Runaround God": { + "description": "أحرز 2000 نقطة", + "descriptionComplete": "أحرز 2000 نقطة", + "descriptionFull": "${LEVEL} أحرز 2000 نقطة في", + "descriptionFullComplete": "${LEVEL} أحرز 2000 نقطة في", + "name": "${LEVEL} خارق" + }, + "Runaround Master": { + "description": "أحرز 500 نقطة", + "descriptionComplete": "أحرز 500 نقطة", + "descriptionFull": "${LEVEL} أحرز 500 نقطة في", + "descriptionFullComplete": "${LEVEL} أحرز 500 نقطة في", + "name": "${LEVEL} سيد" + }, + "Runaround Wizard": { + "description": "أحرز 1000 نقطة", + "descriptionComplete": "أحرز 1000 نقطة", + "descriptionFull": "${LEVEL} أحرز 1000 نقطة في", + "descriptionFullComplete": "${LEVEL} أحرز 1000 نقطة في", + "name": "${LEVEL} ساحر" + }, + "Sharing is Caring": { + "descriptionFull": "شارك اللعبة مع صديق بنجاح", + "descriptionFullComplete": "شارك اللعبة مع صديق بنجاح", + "name": "المشاركة تعني الاهتمام" + }, + "Stayin' Alive": { + "description": "فز بدون أن تموت", + "descriptionComplete": "فاز بدون أن يموت", + "descriptionFull": "بدون أن تموت ${LEVEL} فز", + "descriptionFullComplete": "بدون أن يموت ${LEVEL} فاز", + "name": "إبقى حيا" + }, + "Super Mega Punch": { + "description": "إلحاق الضرر 100٪ بلكمة واحدة", + "descriptionComplete": "إلحاق الضرر 100٪ بلكمة واحدة", + "descriptionFull": "${LEVEL} إلحاق الضرر 100٪ بلكمة واحدة في", + "descriptionFullComplete": "${LEVEL} إلحاق الضرر 100٪ بلكمة واحدة في", + "name": "لكمة خارقة جبارة" + }, + "Super Punch": { + "description": "إلحاق الضرر 50٪ بلكمة واحدة", + "descriptionComplete": "إلحاق الضرر 50٪ بلكمة واحدة", + "descriptionFull": "${LEVEL} إلحاق الضرر 50٪ بلكمة واحدة", + "descriptionFullComplete": "${LEVEL} إلحاق الضرر 50٪ بلكمة واحدة", + "name": "لكمة خارقة" + }, + "TNT Terror": { + "description": "TNT أقتل 6 من الأشرار بواسطة", + "descriptionComplete": "TNT قتل 6 من الأشرار بواسطة", + "descriptionFull": "${LEVEL} في TNT أقتل 6 من الأشرار بواسطة", + "descriptionFullComplete": "${LEVEL} في TNT أقتل 6 من الأشرار بواسطة", + "name": "TNT رعب" + }, + "Team Player": { + "descriptionFull": "بدء لعبة الفريق مع 4+ اللاعبين", + "descriptionFullComplete": "بدء لعبة الفرق مع 4+ اللاعبين", + "name": "لاعب فريق" + }, + "The Great Wall": { + "description": "أوقف كل شخص سيء", + "descriptionComplete": "أوقف كل شخص سيء", + "descriptionFull": "${LEVEL} أوقف كل شخص سيء في", + "descriptionFullComplete": "${LEVEL} أوقف كل شخص سيء في", + "name": "السور العظيم" + }, + "The Wall": { + "description": "أوقف كل شخص سيء", + "descriptionComplete": "أوقف كل شخص سيء", + "descriptionFull": "${LEVEL} أوقف كل شخص سيء في", + "descriptionFullComplete": "${LEVEL} أوقف كل شخص سيء في", + "name": "الجدار" + }, + "Uber Football Shutout": { + "description": "فز بدون السماح للأشرار بإحراز النقاط", + "descriptionComplete": "فاز بدون السماح للأشرار بإحراز النقاط", + "descriptionFull": "بدون السماح للأشرار بإحراز النقاط ${LEVEL} فز", + "descriptionFullComplete": "بدون السماح للأشرار بإحراز النقاط ${LEVEL} فاز", + "name": "${LEVEL} إنتصار ساحق" + }, + "Uber Football Victory": { + "description": "فز بالمباراة", + "descriptionComplete": "فاز بالمباراة", + "descriptionFull": "${LEVEL} فز بالمباراة في", + "descriptionFullComplete": "${LEVEL} فاز بالمباراة في", + "name": "${LEVEL} إنتصار" + }, + "Uber Onslaught Victory": { + "description": "هزيمة كل الجولات", + "descriptionComplete": "هزيمة كل الجولات", + "descriptionFull": "${LEVEL} هزيمة كل الجولات في", + "descriptionFullComplete": "${LEVEL} هزيمة كل الجولات في", + "name": "${LEVEL} إنتصار" + }, + "Uber Runaround Victory": { + "description": "أكمل كل الجولات", + "descriptionComplete": "أكمل كل الجولات", + "descriptionFull": "اكمل كل الجولات في ${LEVEL}", + "descriptionFullComplete": "أكمل كل الجولات في ${LEVEL}", + "name": "${LEVEL} النصر" + } + }, + "achievementsRemainingText": "الإنجازات المتبقية", + "achievementsText": "الإنجازات", + "achievementsUnavailableForOldSeasonsText": "المعذرة، الإنجازات للمواسم القديمة غير متوفرة", + "addGameWindow": { + "getMoreGamesText": "الحصول على المزيد من الألعاب", + "titleText": "إضافة لعبة" + }, + "allowText": "السماح", + "alreadySignedInText": "تم تسجيل الدخول من حسابك من جهاز آخر.\n يرجى تبديل الحسابات أو إغلاق اللعبة على الأجهزة الأخرى\n وحاول مرة أخرى.", + "apiVersionErrorText": "خطأ في تحميل الجزء ${NAME}; انه مخصص للإصدار رقم ${VERSION_USED}; يجب استخدام الإصدار ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"ذاتي\" فعله فقط عندما تكون سماعات الأذن موصولة", + "headRelativeVRAudioText": "صوت VR معوافق مع حركة الرأس", + "musicVolumeText": "مستوى الموسيقى", + "soundVolumeText": "مستوى الموسيقى", + "soundtrackButtonText": "المقاطع الصوتية", + "soundtrackDescriptionText": "(اختر موسيقاك الخاصة لتعمل خلال اللعب)", + "titleText": "الصوت" + }, + "autoText": "ذاتي الاختيار", + "backText": "للخلف", + "banThisPlayerText": "حظر هاذا الاعب", + "bestOfFinalText": "الافضل في {COUNT} النهائية", + "bestOfSeriesText": "${COUNT}افضل سلسلة ل", + "bestOfUseFirstToInstead": 0, + "bestRankText": "افضل ما أحرزت #${RANK}", + "bestRatingText": "أفضل معدّل قد أحرزته ${RATING}", + "bombBoldText": "قنبلة", + "bombText": "قنبلة", + "boostText": "تقوية", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} تمّ ضبطه بالتطبيق ذاته", + "buttonText": "زر", + "canWeDebugText": "هل ترغب ان تقوم فرقة القنبلة تلقائيا بالتبليغ عن المشاكل والاخطاء التقنية \nوالفنيه وبعض المعلومات الاساسية الى موفر اللعبة ؟ \n\nهذه البيانات لا تحتوي على اي معلومات شخصيه و هي تساعد على ابقاء\n اللعبه تعمل بشكل سلس و بدون اخطاء.", + "cancelText": "إلغاء الأمر", + "cantConfigureDeviceText": "المعذرة، ${DEVICE} لا يمكن تخصيصه", + "challengeEndedText": "هذا التحدي قد انتهى", + "chatMuteText": "اسكات الدردشة", + "chatMutedText": "تم اسكات الدردشة", + "chatUnMuteText": "تحرير الدردشة", + "choosingPlayerText": "<يختار لاعب>", + "completeThisLevelToProceedText": "يجب أن تكمل هذه المرحلة ليتم الاجراء", + "completionBonusText": "علاوة الاكمال", + "configControllersWindow": { + "configureControllersText": "ضبط قبضات التحكّم", + "configureKeyboard2Text": "ضبط لوحة مفاتيح اللاعب الثاني", + "configureKeyboardText": "ضبط لوحة المفاتيخ", + "configureMobileText": "أجهزة المحمول كقبضة تحكّم", + "configureTouchText": "ضبط شاشة اللمس", + "ps3Text": "قبضات تحكّم PS3", + "titleText": "قبضات التحكم", + "wiimotesText": "Wiimotes", + "xbox360Text": "يد ألعاب أكس بوكس 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "ملاحظة: إن دعم قبضات التحكم يتباين تبعاً للجهاز و نظام ال Android", + "pressAnyButtonText": "اضغط أيّ زر على قبضة التحكّم التي تريد أن تضبطها", + "titleText": "ضبط قبضات التحكّم" + }, + "configGamepadWindow": { + "advancedText": "خيارات متطوّرة", + "advancedTitleText": "اعداد متقدم ليد الألعاب", + "analogStickDeadZoneDescriptionText": "(فعّل هذه إذا كانت شخصيّتك 'تنحرف' تحرّر عصى التحكّم)", + "analogStickDeadZoneText": "مجال الموت للعصى التماثلية", + "appliesToAllText": "(تنطبق على جميع قبضات التحكّم من هذا النوع)", + "autoRecalibrateDescriptionText": "(فعّل هذه إذا كانت شخصيّتك لاتتحرّك بالسرعة العظما)", + "autoRecalibrateText": "ضبط آلي للعصى التماثلية", + "axisText": "محور", + "clearText": "محو", + "dpadText": "أزرار أسهم", + "extraStartButtonText": "زر بدء إضافي", + "ifNothingHappensTryAnalogText": "اذا لم يحدث شيء، جرّب الاسناد للعصى التماثلية.", + "ifNothingHappensTryDpadText": "اذا لم يحدث شيء، جرّب الاسناد لأزرار الأسهم.", + "ignoreCompletelyDescriptionText": "(امنع هذه القبضة من التأثير على أحد العبة أو القوائم)", + "ignoreCompletelyText": "تجاهل كاملاً", + "ignoredButton1Text": "تمّ تجاهل الزر 1", + "ignoredButton2Text": "تمّ تجاهل الزرّ 2", + "ignoredButton3Text": "تمّ تجاهل الزر 3", + "ignoredButton4Text": "تمّ تجاهل الزر 4", + "ignoredButtonDescriptionText": "(استخدم هذه لتجنّب 'home' أو 'sync' من التأثير على واجهة المستخدم)", + "pressAnyAnalogTriggerText": "اضغط على أي محفّز تماثلي...", + "pressAnyButtonOrDpadText": "اضغط على أي زر أو أحد أزرار الأسهم", + "pressAnyButtonText": "اضغط أيّ زر...", + "pressLeftRightText": "اضغط لليمين أو لليسار", + "pressUpDownText": "اضغط للأعلى أو للأسفل", + "runButton1Text": "زر الركض 1", + "runButton2Text": "زر الركض 2", + "runTrigger1Text": "محفّز الركض 1", + "runTrigger2Text": "محفّز الركض 2", + "runTriggerDescriptionText": "(المحفّز التماثلي يسمح لك الركض بسرعات مختلفة)", + "secondHalfText": "استخدم هذه لضبط النصف الثاني من جهاز قبضتين في واحد الذي يظهر كقبضة واحدة.", + "secondaryEnableText": "تفعيل", + "secondaryText": "قبضة تحكّم ثانويّة", + "startButtonActivatesDefaultDescriptionText": "(الغي تفعيل هذه اذا كان زر البدء خاصّتك هو أكثر من زر 'قائمة')", + "startButtonActivatesDefaultText": "زر البدء يفعل الأداة الافتراضية", + "titleText": "اعداد يد التحكم", + "twoInOneSetupText": "إعدادات أداة التحكم 2 في 1", + "uiOnlyDescriptionText": "(منع هذا المراقب من الانضمام في الواقع لعبة)", + "uiOnlyText": "الحد من استخدام القائمة", + "unassignedButtonsRunText": "جميع الأزرار غير المعينة تشغيل", + "unsetText": "<إلغاء تعيين>", + "vrReorientButtonText": "VR زر إعادة التوجيه" + }, + "configKeyboardWindow": { + "configuringText": "تكوين ${DEVICE}", + "keyboard2NoteText": "ملاحظة: يمكن لمعظم لوحات المفاتيح تسجيل عدد قليل من ضغطات المفاتيح في\nمرة واحدة، لذلك وجود لاعب لوحة المفاتيح الثانية قد تعمل بشكل أفضل\nإذا كان هناك لوحة مفاتيح منفصلة تعلق لهم لاستخدامها.\nلاحظ أنك ستظل بحاجة إلى تعيين مفاتيح فريدة إلى\nلاعبين اثنين حتى في هذه الحالة." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "مقياس التحكم في العمل", + "actionsText": "أفعال", + "buttonsText": "الأزرار", + "dragControlsText": "< اسحب عناصر التحكم لإعادة وضعها >", + "joystickText": "عصا التحكم", + "movementControlScaleText": "مقياس مراقبة الحركة", + "movementText": "حركة", + "resetText": "إعادة تعيين", + "swipeControlsHiddenText": "إخفاء أيقونات السحب", + "swipeInfoText": "'انتقاد' الضوابط أسلوب تأخذ قليلا التعود على ولكن\nتجعل من السهل للعب دون النظر إلى الضوابط.", + "swipeText": "مسحة", + "titleText": "تهيئة شاشة اللمس" + }, + "configureItNowText": "هل تريد تهيئته الآن؟", + "configureText": "تهيئة", + "connectMobileDevicesWindow": { + "amazonText": "متجر تطبيقات أمازون", + "appStoreText": "المتجر", + "bestResultsText": "لتحقيق أفضل النتائح ستحتاج اتصال انترنت سريع .. يمكنك \nزيادة السرعة بايقاف الاجهزة الاخرة المتصلة بالشبكة، أو اللعب \nقرب موزع الشبكة، أو الاتصال بمخدم اللعبة باستخدام الكابل \nالمباشر الى الشبكة", + "explanationText": "لاستخدام الهاتف الذكي أو الكمبيوتر اللوحي باعتبارها وحدة تحكم لاسلكية،\nتثبيت التطبيق \"${REMOTE_APP_NAME}\"عليه. أي عدد من الأجهزة\nيمكن الاتصال لعبة ${APP_NAME}عبر واي-في، وأنه مجاني!", + "forAndroidText": "لأجهزة الأندرويد:", + "forIOSText": "لنظام التشغيل أيفون:", + "getItForText": "احصل على ${REMOTE_APP_NAME} لنظام التشغيل يوس في أبل أب ستور\nأو للأندرويد في متجر جوجل بلاي أو الأمازون أبستور", + "googlePlayText": "جوجل بلاي", + "titleText": "استخدام أجهزة الجوال كأجهزة تحكم:" + }, + "continuePurchaseText": "${PRICE}?اكمل ل", + "continueText": "تابع", + "controlsText": "ضوابط", + "coopSelectWindow": { + "activenessAllTimeInfoText": "هذا لا ينطبق على الترتيب في جميع الأوقات.", + "activenessInfoText": "هذا المضاعف يرتفع في أيام عندما كنت\nواللعب وقطرات على أيام عندما كنت لا.", + "activityText": "نشاط", + "campaignText": "حملة", + "challengesInfoText": "كسب الجوائز لاستكمال الألعاب المصغرة.\n\nالجوائز ومستويات صعوبة زيادة\nفي كل مرة يتم الانتهاء من التحدي و\nتنخفض عندما تنتهي صلاحية واحدة أو يتم مصادرتها.", + "challengesText": "التحديات", + "currentBestText": "الحالي أفضل", + "customText": "العادة", + "entryFeeText": "دخول", + "forfeitConfirmText": "هل تريد التخلي عن هذا التحدي؟", + "forfeitNotAllowedYetText": "ولا يمكن التغلب على هذا التحدي بعد.", + "forfeitText": "مصادرة", + "multipliersText": "مضاعفات", + "nextChallengeText": "التحدي القادم", + "nextPlayText": "اللعب التالي", + "ofTotalTimeText": "من ${TOTAL}", + "playNowText": "العب الان", + "pointsText": "نقاط", + "powerRankingFinishedSeasonUnrankedText": "(موسم مكتمل بدون ترتيب)", + "powerRankingNotInTopText": "( ليس في أول ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} نقطة", + "powerRankingPointsMultText": "(x ${NUMBER} النقاط)", + "powerRankingPointsText": "${NUMBER} النقاط", + "powerRankingPointsToRankedText": "{اجمع} من {المتبقي} النقاط", + "powerRankingText": "ترتيب الطاقة", + "prizesText": "الجوائز", + "proMultInfoText": "اللاعبين الذين لديهم الترقية ${PRO}\nالحصول على ${PERCENT}٪ بوينت بوست هنا.", + "seeMoreText": "المزيد . . .", + "skipWaitText": "تخطي الإنتظار", + "timeRemainingText": "الوقت المتبقي", + "toRankedText": "إلى المرتبة", + "totalText": "مجموع", + "tournamentInfoText": "تنافس على درجات عالية مع\nلاعبين آخرين في الدوري الخاص بك.\n\nيتم منح الجوائز إلى أعلى نقاط\nاللاعبين عند انتهاء وقت البطولة.", + "welcome1Text": "مرحبا بك في ${LEAGUE}. يمكنك تحسين الخاص بك\nترتيب الدوري من خلال كسب تقييمات النجوم، والانتهاء\nوالإنجازات، والفوز بالجوائز في البطولات.", + "welcome2Text": "يمكنك أيضا الحصول على تذاكر من العديد من الأنشطة نفسها.\nتذاكر يمكن استخدامها لفتح شخصيات جديدة، والخرائط، و\nالألعاب المصغرة، للدخول البطولات، وأكثر من ذلك.", + "yourPowerRankingText": "تصنيف الطاقة:" + }, + "copyOfText": "${NAME} نسخ", + "createEditPlayerText": "<اصنع او عدل حساب>", + "createText": "اصنع", + "creditsWindow": { + "additionalAudioArtIdeasText": "صوت إضافي، عمل فني مبكر، وأفكار حسب ${NAME}", + "additionalMusicFromText": "موسيقى إضافية من ${NAME}", + "allMyFamilyText": "جميع اصدقائي وعائلتي التي ساعدتني في لعب الاختبار", + "codingGraphicsAudioText": "الترميز والرسومات والصوت حسب ${NAME}", + "languageTranslationsText": "ترجمة اللغة", + "legalText": "القانونية:", + "publicDomainMusicViaText": "موسيقى النطاق العام عبر ${NAME}", + "softwareBasedOnText": "ويستند هذا البرنامج جزئيا على عمل ${NAME}", + "songCreditText": "${TITLE} يؤديه ${PERFORMER}\nيتكون من ${COMPOSER}، مرتبة حسب ${ARRANGER}، تم النشر بواسطة${PUBLISHER}،\nمن باب المجامله${SOURCE}", + "soundAndMusicText": "الصوت والموسيقى:", + "soundsText": "الاصوات (${SOURCE}):", + "specialThanksText": "شكر خاص", + "thanksEspeciallyToText": "شكرا بشكل خاص على ${NAME}", + "titleText": "${APP_NAME} من المساعدين", + "whoeverInventedCoffeeText": "هو الذي اخترع القهوة" + }, + "currentStandingText": "وضعك الحالي هو # ${RANK}", + "customizeText": "...تعديل", + "deathsTallyText": "${COUNT} وفيات", + "deathsText": "موت", + "debugText": "التصحيح", + "debugWindow": { + "reloadBenchmarkBestResultsText": "ملاحظة: فمن المستحسن أن قمت بتعيين إعدادات-> الرسومات-> القوام إلى 'عالية' أثناء اختبار هذا.", + "runCPUBenchmarkText": "تشغيل وحدة المعالجة المركزية المعيار", + "runGPUBenchmarkText": "تشغيل معيار معالج الرسومات", + "runMediaReloadBenchmarkText": "تشغيل معيار إعادة تحميل الوسائط", + "runStressTestText": "تشغيل اختبار الإجهاد", + "stressTestPlayerCountText": "عدد اللاعبين", + "stressTestPlaylistDescriptionText": "اختبار الإجهاد قائمة التشغيل", + "stressTestPlaylistNameText": "اسم قائمة التشغيل", + "stressTestPlaylistTypeText": "نوع قائمة التشغيل", + "stressTestRoundDurationText": "مدة الجولة", + "stressTestTitleText": "اختبار الإجهاد", + "titleText": "معايير واختبارات الإجهاد", + "totalReloadTimeText": "إجمالي وقت إعادة التحميل: ${TIME} (راجع سجل للحصول على التفاصيل)" + }, + "defaultGameListNameText": "الافتراضي ${PLAYMODE} قائمة التشغيل", + "defaultNewGameListNameText": "قائمة تشغيل ${PLAYMODE}", + "deleteText": "حذف", + "demoText": "عرض", + "denyText": "رفض", + "desktopResText": "ديسكتوب ريس", + "difficultyEasyText": "سهل", + "difficultyHardOnlyText": "الوضع الصعب فقط", + "difficultyHardText": "صعب", + "difficultyHardUnlockOnlyText": "لا يمكن فتح هذا المستوى إلا في الوضع الصعب.\n هل تعتقد أن لديك ما يلزم!؟!؟!", + "directBrowserToURLText": "يرجى توجيه متصفح ويب إلى عنوان ورل التالي:", + "disableRemoteAppConnectionsText": "تعطيل اتصالات التطبيق عن بعد", + "disableXInputDescriptionText": "يسمح أكثر من 4 وحدات تحكم ولكن قد لا تعمل كذلك.", + "disableXInputText": "xinput تعطيل", + "doneText": "تم", + "drawText": "تعادل", + "duplicateText": "مكرر", + "editGameListWindow": { + "addGameText": "إضافة\nلعبة", + "cantOverwriteDefaultText": "لا يمكن استبدال قائمة التشغيل الافتراضية!", + "cantSaveAlreadyExistsText": "قائمة تشغيل بهذا الاسم موجودة من قبل!", + "cantSaveEmptyListText": "لا يمكن حفظ قائمة تشغيل فارغة!", + "editGameText": "اضافة\nلعبه", + "listNameText": "قائمة اسماء الالعاب", + "nameText": "اسم", + "removeGameText": "محو\nالعبه", + "saveText": "قائمة الحفظ", + "titleText": "قائمة تشغيل محرر" + }, + "editProfileWindow": { + "accountProfileInfoText": "يحتوي هذا الملف الشخصي الخاص على اسم وأيقونة بناء على حسابك.\n${ICONS} \nقم بإنشاء ملفات تعريف مخصصة لاستخدام أسماء مختلفة أو أيقونات مخصصة.", + "accountProfileText": "(ملف تعريف الحساب)", + "availableText": "الاسم \"${NAME}\" متاح.", + "characterText": "شخصيه", + "checkingAvailabilityText": "جار التحقق من التوفر ل \"${NAME}\" ...", + "colorText": "اللون", + "getMoreCharactersText": "الحصول على المزيد من الشخصيات ...", + "getMoreIconsText": "الحصول على المزيد من الرموز ...", + "globalProfileInfoText": "ملامح اللاعب العالمي مضمونة للحصول على \n أسماء فريدة من نوعها في جميع أنحاء العالم. كما تشمل الرموز المخصصة.", + "globalProfileText": "(ملف شخصي عالمي)", + "highlightText": "تسليط الضوء", + "iconText": "أيقونة", + "localProfileInfoText": "ملامح لاعب المحلي ليس لديهم رموز وأسمائهم\nغير مضمونة لتكون فريدة من نوعها. الترقية إلى ملف شخصي عام\nلحجز اسم فريد وإضافة رمز مخصص.", + "localProfileText": "(الملف الشخصي المحلي)", + "nameDescriptionText": "اسم اللاعب", + "nameText": "الأسم", + "randomText": "عشوائي", + "titleEditText": "تعديل الملف الشخصي", + "titleNewText": "ملف شخصي جديد", + "unavailableText": "\"${NAME}\" غير متوفر؛ حاول اسم آخر.", + "upgradeProfileInfoText": "هذا سيحفظ اسم لاعب في جميع أنحاء العالم\nوتسمح لك بتعيين رمز مخصص لها.", + "upgradeToGlobalProfileText": "الترقية إلى الملف الشخصي العالمي" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "لا يمكنك حذف الصوت الافتراضي.", + "cantEditDefaultText": "لا يمكن التعديل على تسجيل الصوت الاساسي. قم بنسخه او أنشئ واحدا جديدا", + "cantOverwriteDefaultText": "لا يمكن الكتابة فوق الصوت الافتراضي", + "cantSaveAlreadyExistsText": "يوجد مقطع صوتي بهذا الاسم من قبل.", + "defaultGameMusicText": "<موسيقى اللعبة الافتراضية>", + "defaultSoundtrackNameText": "الصوت الافتراضي", + "deleteConfirmText": "حذف الموسيقى التصويرية:\n\n'${NAME}'؟", + "deleteText": "حذف\nتسجيل صوتي", + "duplicateText": "مكرر\nتسجيل صوتي", + "editSoundtrackText": "محرر الموسيقى التصويرية", + "editText": "تصحيح\nتسجيل صوتي", + "fetchingITunesText": "جارٍ جلب قوائم تشغيل تطبيق الموسيقى ...", + "musicVolumeZeroWarning": "تحذير: يتم ضبط مستوى صوت الموسيقى على 0", + "nameText": "اسم", + "newSoundtrackNameText": "الموسيقى التصويرية ${COUNT}", + "newSoundtrackText": "موسيقى تصويرية جديدة:", + "newText": "الجديد\nتسجيل صوتي", + "selectAPlaylistText": "حدد قائمة تشغيل", + "selectASourceText": "مصدر الموسيقى", + "testText": "اختبار", + "titleText": "الموسيقى التصويرية", + "useDefaultGameMusicText": "الافتراضي لعبة الموسيقى", + "useITunesPlaylistText": "قائمة تشغيل تطبيق الموسيقى", + "useMusicFileText": "ملف الموسيقى (mp3، الخ)", + "useMusicFolderText": "مجلد ملفات الموسيقى" + }, + "editText": "تعديل", + "endText": "نهايه", + "enjoyText": "استمتع", + "epicDescriptionFilterText": "${DESCRIPTION} في حركة بطيئة ملحمية.", + "epicNameFilterText": "الملحمي ${NAME}", + "errorAccessDeniedText": "تم الرفض", + "errorOutOfDiskSpaceText": "انتهت مساحة التخزين", + "errorText": "خطا", + "errorUnknownText": "خطا غير معروف", + "exitGameText": "هل تريد الخروج من ${APP_NAME}؟", + "exportSuccessText": "تم تصدير ${NAME} '.", + "externalStorageText": "تخزين خارجي", + "failText": "فشل", + "fatalErrorText": "آه؛ شيء مفقود أو مكسور.\nالرجاء محاولة إعادة تثبيت التطبيق أو\nاتصل ${EMAIL} للحصول على مساعدة.", + "fileSelectorWindow": { + "titleFileFolderText": "حدد ملف أو مجلد", + "titleFileText": "اختر ملف", + "titleFolderText": "اختر مجلد", + "useThisFolderButtonText": "استخدم هاذا المجلد" + }, + "filterText": "مصفاة", + "finalScoreText": "النتيجة النهائية", + "finalScoresText": "النتيجة النهائية", + "finalTimeText": "الوقت النهائي", + "finishingInstallText": "الانتهاء من التثبيت؛ لحظة واحدة ..", + "fireTVRemoteWarningText": "* للحصول على تجربة أفضل، واستخدام\nلعبة تحكم أو تثبيت\n'${REMOTE_APP_NAME}' التطبيق على الخاص بك\nالهواتف والأجهزة اللوحية.", + "firstToFinalText": "من الأول إلى - ${COUNT} نهائي", + "firstToSeriesText": "من الأول إلى - ${COUNT} السلسلة", + "fiveKillText": "خمسة قتل !!!", + "flawlessWaveText": "موجة لا تشوبه شائبة!", + "fourKillText": "قتل رباعي !!!", + "friendScoresUnavailableText": "نقاط الاصدقاء غير متوفره.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "لعبة ${COUNT} قادة", + "gameListWindow": { + "cantDeleteDefaultText": "لا يمكنك حذف قائمة التشغيل الافتراضية.", + "cantEditDefaultText": "لا يمكن تعديل قائمة التشغيل الافتراضية! تكراره أو إنشاء واحدة جديدة.", + "cantShareDefaultText": "لا يمكنك مشاركة قائمة التشغيل الافتراضية.", + "deleteConfirmText": "ازالة \"${LIST}\"?", + "deleteText": "محو\nقائمة العب", + "duplicateText": "مكرر\nقائمة التشغيل", + "editText": "تصحيح\nقائمة التشغيل", + "newText": "الجديد\nقائمة التشغيل", + "showTutorialText": "عرض البرنامج التعليمي", + "shuffleGameOrderText": "ترتيب لعبة المراوغة", + "titleText": "تخصيص ${TYPE} قوائم تشغيل" + }, + "gameSettingsWindow": { + "addGameText": "اضف لعبه" + }, + "gamesToText": "${WINCOUNT} من الألعاب إلى ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "تذكر: يمكن لأي جهاز في الحفلة الحصول علي\nاكثر من لاعب واحد إذا كان لديك ما يكفي من وحدات التحكم.", + "aboutDescriptionText": "استخدم علامات التبويب هذه لتجميع أحد الحفلات.\n\nتتيح لك الأطراف لعب الألعاب والبطولات\nمع أصدقائك عبر الأجهزة المختلفة.\n\nاستخدم الزر ${PARTY} في أعلى اليسار\nدردشة والتفاعل مع حزبكم.\n(على وحدة تحكم، اضغط ${BUTTON} بينما في القائمة)", + "aboutText": "حول", + "addressFetchErrorText": "<خطأ في جلب العناوين>", + "appInviteMessageText": "أرسل ${NAME} تذاكر ${COUNT} في ${APP_NAME}", + "appInviteSendACodeText": "إرسال لهم رمز", + "appInviteTitleText": "${APP_NAME} دعوة التطبيق", + "bluetoothAndroidSupportText": "(يعمل مع أي جهاز الروبوت دعم بلوتوث)", + "bluetoothDescriptionText": "المضيف / الانضمام إلى طرف عبر البلوتوث:", + "bluetoothHostText": "المضيف عبر البلوتوث", + "bluetoothJoinText": "الانضمام عبر البلوتوث", + "bluetoothText": "بلوتوث", + "checkingText": "تدقيق...", + "copyCodeConfirmText": "تم نسخ الرمز الى الحافظة", + "copyCodeText": "نسخ الرمز", + "dedicatedServerInfoText": "للحصول على أفضل النتائج، قم بإعداد خادم مخصص. اطلع على bombsquadgame.com/server لمعرفة كيفية إجراء ذلك.", + "disconnectClientsText": "سيؤدي هذا إلى فصل المشغل ${COUNT}\nفي حزبكم. هل أنت واثق؟", + "earnTicketsForRecommendingAmountText": "سيتلقى الأصدقاء تذاكر بقيمة ${COUNT} إذا جربو اللعبة\n(وستتلقى ${YOU_COUNT} لكل من يفعل)", + "earnTicketsForRecommendingText": "مشاركة اللعبة\nلتذاكر مجانية ...", + "emailItText": "البريد الإلكتروني", + "favoritesSaveText": "حفظ كمفضلة", + "favoritesText": "المفضلة", + "freeCloudServerAvailableMinutesText": "السيرفر التالي متوفر في ${MINUTES} من الدقائق", + "freeCloudServerAvailableNowText": "السيرفر التالي متوفر الأن!", + "freeCloudServerNotAvailableText": "لا توجد أي سيرفرات متاحة", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} تذاكر من ${NAME}", + "friendPromoCodeAwardText": "سوف تتلقى تذاكر ${COUNT} في كل مرة يتم استخدامها.", + "friendPromoCodeExpireText": "ستنتهي صلاحية الشفرة خلال ${EXPIRE_HOURS} ساعة وتعمل فقط للاعبين الجدد.", + "friendPromoCodeInstructionsText": "لاستخدامها ، افتح ${APP_NAME} وانتقل إلى \"الإعدادات-> متقدم-> إدخال الرمز\".\nانظر bombsquadgame.com للحصول على روابط التحميل لجميع المنصات المدعومة.", + "friendPromoCodeRedeemLongText": "ويمكن استرداد قيمتها بمبلغ ${MAX_USES} من التذاكر المجانية بقيمة ${COUNT}.", + "friendPromoCodeRedeemShortText": "ويمكن استبدالها ل ${COUNT} تذاكر في اللعبة.", + "friendPromoCodeWhereToEnterText": "(في \"الإعدادات -> متقدم -> أدخل الرمز\")", + "getFriendInviteCodeText": "احصل على كود دعوة من صديق", + "googlePlayDescriptionText": "دعوة لاعبين غوغل بلاي لحزبكم:", + "googlePlayInviteText": "دعوة", + "googlePlayReInviteText": "هناك ${COUNT} لاعب غوغل بلاي (s) في حفلك\nالذي سيتم فصله إذا قمت بدعوة دعوة جديدة.\nتضمينها في الدعوة الجديدة لاستعادتها.", + "googlePlaySeeInvitesText": "راجع الدعوات", + "googlePlayText": "غوغل بلاي", + "googlePlayVersionOnlyText": "(الروبوت / جوجل اللعب الإصدار)", + "hostPublicPartyDescriptionText": "صنع حفلة عامة", + "hostingUnavailableText": "صنع السيرفر غير متوفر", + "inDevelopmentWarningText": "ملحوظة:\n\nلعب الشبكة هو ميزة جديدة والتي لا تزال تتطور.\nفي الوقت الراهن، ينصح بشدة أن جميع\nاللاعبين على نفس شبكة واي فاي.", + "internetText": "انترنت", + "inviteAFriendText": "الأصدقاء ليس لديهم اللعبة؟ يمكنك دعوتهم إلى\nجربها وسيحصلون على ${COUNT} من التذاكر المجانية.", + "inviteFriendsText": "دعوة الاصدقاء", + "joinPublicPartyDescriptionText": "الإنضمام الى سيرفر عام", + "localNetworkDescriptionText": "(الإنضمام الى سيرفر بالقرب منك (وايفاي,بلوتوث,الخ", + "localNetworkText": "شبكة محليه", + "makePartyPrivateText": "اصنع حفلة خاصة", + "makePartyPublicText": "اصنع حزب بلدي العامة", + "manualAddressText": "العنوان", + "manualConnectText": "الاتصال", + "manualDescriptionText": "الانضمام إلى الحزب عن طريق العنوان:", + "manualJoinSectionText": "الإنضمام بواسطة العنوان", + "manualJoinableFromInternetText": "هل أنت مشترك من الإنترنت ؟:", + "manualJoinableNoWithAsteriskText": "لا*", + "manualJoinableYesText": "نعم", + "manualRouterForwardingText": "* لإصلاح ذلك، حاول تهيئة الموجه لإعادة توجيه منفذ أودب ${PORT} إلى عنوانك المحلي", + "manualText": "يدوي", + "manualYourAddressFromInternetText": "عنوانك من الإنترنت:", + "manualYourLocalAddressText": "عنوانك المباشر", + "nearbyText": "الأقرب", + "noConnectionText": "<لا يوجد اتصال>", + "otherVersionsText": "(اصدارات اخرى)", + "partyCodeText": "رمز السيرفر", + "partyInviteAcceptText": "قبول", + "partyInviteDeclineText": "رفض", + "partyInviteGooglePlayExtraText": "(see the 'Google Play' tab in the 'Gather' window)", + "partyInviteIgnoreText": "موافق", + "partyInviteText": "تمت دعوة ${NAME}\nلك للانضمام إلى حزبهم!", + "partyNameText": "اسم المجموعة", + "partyServerRunningText": "سيرفرك يعمل", + "partySizeText": "حجم المجموعه", + "partyStatusCheckingText": "جار التحقق من الحالة ...", + "partyStatusJoinableText": "حزبك الآن غير قابل للانضمام من الإنترنت", + "partyStatusNoConnectionText": "غير قادر على الإتصال بالسيرفر", + "partyStatusNotJoinableText": "حزبكم ليست قابلة للانضمام من الإنترنت", + "partyStatusNotPublicText": "حزبكم ليس عام", + "pingText": "Ping", + "portText": "البوابة", + "privatePartyCloudDescriptionText": "السيرفرات الخاصة تعمل على خادم في الهواء؛ لا تحتاج تجهيز الراوتر اودي بي", + "privatePartyHostText": "صنع حفلة خاصة", + "privatePartyJoinText": "دخول حفلة خاصة", + "privateText": "الخاص", + "publicHostRouterConfigText": "هذا قد يحتاج تجهيز منفذ اودي پي في الراوتر. لعمل اسهل، يمكنك صنع حفلة خاصة", + "publicText": "عام", + "requestingAPromoCodeText": "جار طلب رمز ...", + "sendDirectInvitesText": "إرسال دعوات مباشرة", + "shareThisCodeWithFriendsText": "شارك هذا الرمز مع الأصدقاء:", + "showMyAddressText": "عرض عنواني", + "startHostingPaidText": "صنع الحفلة الأن ب ${COST}", + "startHostingText": "صنع الحفل", + "startStopHostingMinutesText": "يمكنك بدأ وايقاف الحفلة مجانا خلال ${MINUTES} من الدقائق", + "stopHostingText": "ايقاف التشغيل", + "titleText": "متعدد الاعبين", + "wifiDirectDescriptionBottomText": "إذا كانت جميع الأجهزة تحتوي على لوحة \"واي-في مباشر\"، فيجب أن تكون قادرة على استخدامها للعثور عليها\nوالتواصل مع بعضها البعض. مرة واحدة يتم توصيل جميع الأجهزة، يمكنك تشكيل الأطراف\nهنا باستخدام علامة التبويب \"الشبكة المحلية\"، تماما كما هو الحال مع شبكة واي فاي العادية.\n\nللحصول على أفضل النتائج، يجب أن يكون مضيف واي-في ديريكت أيضا مضيف الطرف ${APP_NAME}.", + "wifiDirectDescriptionTopText": "واي فاي المباشر يمكن استخدامها لتوصيل أجهزة الروبوت مباشرة دون\nوالتي تحتاج إلى شبكة واي فاي. هذا يعمل بشكل أفضل على الروبوت 4.2 أو أحدث.\n\nلاستخدامه، افتح إعدادات واي-في وابحث عن \"واي-في ديريكت\" في القائمة.", + "wifiDirectOpenWiFiSettingsText": "افتح إعدادات واي-في", + "wifiDirectText": "واي فاي مباشر", + "worksBetweenAllPlatformsText": "(يعمل بين جميع المنصات)", + "worksWithGooglePlayDevicesText": "(يعمل مع الأجهزة التي تعمل على جوجل بلاي (أندرويد) نسخة من اللعبة)", + "youHaveBeenSentAPromoCodeText": "لقد تم إرسال رمز ترويجي بقيمة ${APP_NAME}:" + }, + "getTicketsWindow": { + "freeText": "مجانآ !", + "freeTicketsText": "تذاكر مجانية", + "inProgressText": "هناك معاملة قيد التنفيذ. يرجى المحاولة مرة أخرى في لحظة.", + "purchasesRestoredText": "تمت استعادة عمليات الشراء.", + "receivedTicketsText": "تم استلام تذاكر ${COUNT}!", + "restorePurchasesText": "استعادة المشتريات", + "ticketPack1Text": "حزمة تذكرة صغيرة", + "ticketPack2Text": "حزمة تذكرة متوسطة", + "ticketPack3Text": "حزمة تذكرة كبيرة", + "ticketPack4Text": "جمبو تذكرة حزمة", + "ticketPack5Text": "ماموث تذكرة حزمة", + "ticketPack6Text": "تذكرة حزمة هائلة", + "ticketsFromASponsorText": "احصل على تذاكر ${COUNT}\nمن مقدم مشروع القرار", + "ticketsText": "${COUNT} بطاقات", + "titleText": "أحصل على تذاكر", + "unavailableLinkAccountText": "عذرا، لا تتوفر عمليات الشراء على هذا النظام الأساسي.\nوكحل بديل، يمكنك ربط هذا الحساب بحساب في\nمنصة أخرى وجعل عمليات الشراء هناك.", + "unavailableTemporarilyText": "هذا غير متوفر حاليا؛ الرجاء معاودة المحاولة في وقت لاحق.", + "unavailableText": "عذرا، هذا غير متوفر.", + "versionTooOldText": "عذرا، هذا الإصدار من اللعبة قديم جدا؛ يرجى تحديث إلى أحدث واحد.", + "youHaveShortText": "لديك ${COUNT}", + "youHaveText": "لديك ${COUNT} تذاكر" + }, + "googleMultiplayerDiscontinuedText": "عذرًا ، خدمة جوجل متعددة اللاعبين لم تعد متاحة.\n أنا أعمل على بديل بأسرع وقت ممكن.\n حتى ذلك الحين ، يرجى تجربة طريقة اتصال أخرى.\n -إريك", + "googlePlayText": "جوجل بلاي", + "graphicsSettingsWindow": { + "alwaysText": "دائما", + "fullScreenCmdText": "ملء الشاشة (Cmd-F)", + "fullScreenCtrlText": "الشاشه كامله (Ctrl-F)", + "gammaText": "غاما", + "highText": "متوسط", + "higherText": "العالي", + "lowText": "منخفض", + "mediumText": "متوسط", + "neverText": "أبدا", + "resolutionText": "القرار", + "showFPSText": "اظهار عدد الكدرات في الثانية", + "texturesText": "القوام", + "titleText": "الرسومات", + "tvBorderText": "TV الحدود", + "verticalSyncText": "تزامن عمودي", + "visualsText": "صور" + }, + "helpWindow": { + "bombInfoText": "- قنبلة -\nأقوى من اللكمات، ولكن\nيمكن أن يؤدي إلى إصابة خطيرة.\nللحصول على أفضل النتائج، رمي نحو العدو قبل نفاذ الفتيل.", + "canHelpText": "يمكن أن يساعدك ${APP_NAME}.", + "controllersInfoText": "يمكنك تشغيل ${APP_NAME} مع الأصدقاء عبر شبكة، أو أنت\nيمكن أن تلعب جميع على نفس الجهاز إذا كان لديك ما يكفي من وحدات التحكم.\n${APP_NAME} يدعم مجموعة متنوعة منها؛ يمكنك حتى استخدام الهواتف\nكمحكمين عبر تطبيق '${REMOTE_APP_NAME}' المجاني.\nانظر إعدادات-> وحدات تحكم لمزيد من المعلومات.", + "controllersText": "التحكم", + "controlsSubtitleText": "يحتوي الطابع الصديق ${APP_NAME} على بعض الإجراءات الأساسية:", + "controlsText": "ضوابط", + "devicesInfoText": "يمكن تشغيل إصدار فر الذي يبلغ ${APP_NAME} عبر الشبكة\nالنسخة العادية، حتى سوط خارج الهواتف الإضافية، وأقراص،\nوأجهزة الكمبيوتر والحصول على اللعبة الخاصة بك على. بل يمكن أن يكون مفيدا ل\nربط نسخة منتظمة من اللعبة إلى الإصدار فر فقط ل\nالسماح للناس خارج لمشاهدة العمل.", + "devicesText": "الأجهزة", + "friendsGoodText": "هذه هي جيدة لديك. ${APP_NAME} أكثر متعة مع العديد\nلاعبين ويمكن أن تدعم ما يصل إلى 8 في وقت واحد، الأمر الذي يقودنا إلى:", + "friendsText": "الاصدقاء", + "jumpInfoText": "- القفز -\nالقفز لعبور الثغرات الصغيرة،\nلرمي الأشياء أعلى، و\nللتعبير عن مشاعر الفرح.", + "orPunchingSomethingText": "أو اللكم شيئا، ورميها من الهاوية، وتفجيرها على الطريق مع قنبلة لزجة.", + "pickUpInfoText": "- امسك -\nالاستيلاء على الأعلام، والأعداء، أو أي شيء\nوإلا لا انسحب على الأرض.\nاضغط مرة أخرى لرمي.", + "powerupBombDescriptionText": "يتيح لك سوط من ثلاث قنابل\nفي صف واحد بدلا من واحد فقط.", + "powerupBombNameText": "قنابل ثلاثية", + "powerupCurseDescriptionText": "ربما كنت ترغب في تجنب هذه.\n ...او هل انت؟", + "powerupCurseNameText": "لعنة", + "powerupHealthDescriptionText": "يسترجع صحتك كامله.\nلن تخمن ابدا.", + "powerupHealthNameText": "حزمه متوسطه", + "powerupIceBombsDescriptionText": "اضعف من القنابل العاديه\nولكن تجعل اعدائك مجمدين\nواكثر هشاشه", + "powerupIceBombsNameText": "قنابل الجليد", + "powerupImpactBombsDescriptionText": "أضعف قليلا من القنابل العادية،\nلكنها تنفجر على التأثير.", + "powerupImpactBombsNameText": "الزناد القنابل", + "powerupLandMinesDescriptionText": "هذه تأتي في حزم من 3؛\nمفيدة للدفاع الأساسي أو\nإيقاف الأعداء السريعة", + "powerupLandMinesNameText": "الالغام-الارضيه", + "powerupPunchDescriptionText": "يجعل لكم اللكمات أصعب،\nأسرع، أفضل، أقوى.", + "powerupPunchNameText": "قفازات الملاكمة", + "powerupShieldDescriptionText": "يمتص قليلا من الضرر\nحتى لا تضطر إلى ذلك.", + "powerupShieldNameText": "درع الطاقة", + "powerupStickyBombsDescriptionText": "امساك و ضرب الشي.\nلا يزال يجعلك سعيدا.", + "powerupStickyBombsNameText": "قنابل لاصقة", + "powerupsSubtitleText": "وبطبيعة الحال، لا لعبة كاملة دون قوه خارقه:", + "powerupsText": "قوه خارقه", + "punchInfoText": "-اللكمة-\nاللكمات تعطي ضرراً أكبر\n حسب سرعة حركة يدك،\n لذا إركض و إستدر مثل رجل مجنون.", + "runInfoText": "- الركض -\nامسك أي زر لتشغيله. يعمل مشغلات أو\nأزرار الكتف بشكل جيد إذا كان لديك.\nالجري يحصل لك على أماكن أسرع ولكن يجعل من الصعب تشغيله،\nلذلك احترس من المنحدرات", + "someDaysText": "في بعض الايام تشعر بالرغبة في ضرب شيئ.او تفجير شيئ .", + "titleText": "مساعدة ${APP_NAME}", + "toGetTheMostText": "للحصول على أقصى استفادة من هذه اللعبة، ستحتاج إلى:", + "welcomeText": "مرحبا بك في ${APP_NAME}!" + }, + "holdAnyButtonText": "<اضغط على أي زر>", + "holdAnyKeyText": "<اضغط على أي مفتاح>", + "hostIsNavigatingMenusText": "- ${HOST} الرئيس يقوم بالتنقل في القوائم -", + "importPlaylistCodeInstructionsText": "استخدم الكود التالي لاستيراد قائمة التشغيل هذه في مكان آخر:", + "importPlaylistSuccessText": "تم استيراد ${TYPE} قائمة تشغيل \"${NAME}\"", + "importText": "استيراد", + "importingText": "استيراد ...", + "inGameClippedNameText": "في اللعبة سوف يكون\n\"${NAME}\"", + "installDiskSpaceErrorText": "خطأ: تعذر إكمال التثبيت.\nقد تكون نفذت مساحه التخزين على جهازك.\nامسح بعض المساحة وحاول مرة أخرى.", + "internal": { + "arrowsToExitListText": "اضغط ${LEFT} أو ${RIGHT} للخروج من القائمة", + "buttonText": "زر", + "cantKickHostError": "لا يمكنك طرد المضيف.", + "chatBlockedText": "${NAME} تم حظر الدردشة لمدة ${TIME} ثانية.", + "connectedToGameText": "انضم '${NAME}'", + "connectedToPartyText": "انضم إلى حفلة ${NAME}!", + "connectingToPartyText": "توصيل...", + "connectionFailedHostAlreadyInPartyText": "فشل الاتصال؛ المضيف في حفله اخرى.", + "connectionFailedPartyFullText": "فشل الاتصال؛ الحزب الكامل.", + "connectionFailedText": "فشل الاتصال.", + "connectionFailedVersionMismatchText": "فشل الاتصال؛ المضيف يقوم بتشغيل نسخة مختلفة من اللعبة.\nتأكد من أنك و المضيف لديكما نفس النسخه وحاول مرة أخرى.", + "connectionRejectedText": "تم رفض الاتصال.", + "controllerConnectedText": "${CONTROLLER} متصل.", + "controllerDetectedText": "تم اكتشاف وحدة تحكم واحدة.", + "controllerDisconnectedText": "${CONTROLLER} تم القطع.", + "controllerDisconnectedTryAgainText": "انقطع الاتصال ${CONTROLLER}.الرجاء اعاده محاولة الاتصال مرة أخرى.", + "controllerForMenusOnlyText": "لا يمكن استخدام هذا المراقب للعب. فقط للتنقل القوائم.", + "controllerReconnectedText": "تم إعادة ربط ${CONTROLLER}.", + "controllersConnectedText": "تم توصيل وحدات تحكم ${COUNT}.", + "controllersDetectedText": "تم اكتشاف وحدات تحكم ${COUNT}.", + "controllersDisconnectedText": "تم فصل وحدات تحكم ${COUNT}.", + "corruptFileText": "تم اكتشاف ملف (ملفات) فاسدة. الرجاء محاولة إعادة التثبيت أو البريد الإلكتروني ${EMAIL}", + "errorPlayingMusicText": "خطأ في تشغيل الموسيقى: ${MUSIC}", + "errorResettingAchievementsText": "يتعذر إعادة تعيين الإنجازات عبر الإنترنت؛ الرجاء معاودة المحاولة في وقت لاحق.", + "hasMenuControlText": "يحتوي ${NAME} على عنصر تحكم في القائمة.", + "incompatibleNewerVersionHostText": "يقوم المضيف بتشغيل إصدار أحدث من اللعبة.\nتحديث إلى أحدث إصدار وحاول مرة أخرى.", + "incompatibleVersionHostText": "المضيف يقوم بتشغيل نسخة مختلفة من اللعبة.\nتأكد من أنك انت و المضيف لديكما نفس النسخه وحاول مرة أخرى.", + "incompatibleVersionPlayerText": "يعمل ${NAME} على إصدار مختلف من اللعبة.\nتأكد من اصدار اللعبه لديكما او تغيل نفس الاصدار وحاول مرة أخرى.", + "invalidAddressErrorText": "خطأ: عنوان غير صالح.", + "invalidNameErrorText": "خطأ: اسم غير صالح.", + "invalidPortErrorText": "خطأ: منفذ غير صالح.", + "invitationSentText": "ارسلت الدعوه.", + "invitationsSentText": "تم إرسال دعوات ${COUNT}.", + "joinedPartyInstructionsText": "انضم شخص ما إلى حفلتك.\nانتقل إلى \"اللعب\" لبدء اللعبة.", + "keyboardText": "لوحة المفاتيح", + "kickIdlePlayersKickedText": "الركل ${NAME} لكونه خاملا.", + "kickIdlePlayersWarning1Text": "سيتم ركل ${NAME} بمبلغ ${COUNT} ثانية إذا ظلت خاملة.", + "kickIdlePlayersWarning2Text": "(يمكنك إيقاف هذا في إعدادات -> متقدم)", + "leftGameText": "يسار '${NAME}'.", + "leftPartyText": "غادر ${NAME} من الحفله.", + "noMusicFilesInFolderText": "المجلد لا يحتوي على ملفات الموسيقى.", + "playerJoinedPartyText": "انضم ${NAME} إلى الحفله!", + "playerLeftPartyText": "غادر ${NAME} الحفله.", + "rejectingInviteAlreadyInPartyText": "رفض الدعوة (موجود بالفعل في أحد الحفلات).", + "serverRestartingText": "السيرفر يعاد تشغيله . يرجى إعادة الدخول بعد لحظة ...", + "serverShuttingDownText": "الخادم يغلق...", + "signInErrorText": "حدث خطأ أثناء تسجيل الدخول.", + "signInNoConnectionText": "تعذر تسجيل الدخول. (بدون اتصال بالإنترنت؟)", + "telnetAccessDeniedText": "خطأ: لم يمنح المستخدم حق الوصول إلى تلنيت.", + "timeOutText": "(من المرات في ${TIME} ثانية)", + "touchScreenJoinWarningText": "لقد انضممت مع شاشة اللمس.\nإذا كان هذا خطأ، اضغط 'القائمة-> ترك لعبة' معها.", + "touchScreenText": "شاشة اللمس", + "unableToResolveHostText": "خطأ: غير قادر على حل المضيف.", + "unavailableNoConnectionText": "هذا غير متاح حاليا (لا يوجد اتصال بالإنترنت؟)", + "vrOrientationResetCardboardText": "استخدام هذا لإعادة توجيه فر.\nللعب اللعبة سوف تحتاج إلى وحدة تحكم خارجية.", + "vrOrientationResetText": "فر توجيه إعادة تعيين.", + "willTimeOutText": "(سوف تنقضي المهلة إذا توقفت عن الحركة)" + }, + "jumpBoldText": "اقفز", + "jumpText": "قفز", + "keepText": "احتفظ", + "keepTheseSettingsText": "هل تريد الاحتفاظ بهذه الإعدادات؟", + "keyboardChangeInstructionsText": "اضغط مرتين على مفتاح المسافة لبدل لوحات المفاتيح", + "keyboardNoOthersAvailableText": "لا لوحة مفاتيح ثان موجود", + "keyboardSwitchText": "يتم بدل لوحة مفاتيح ل\"${NAME}\".", + "kickOccurredText": "تم ركل ${NAME}.", + "kickQuestionText": "ركل ${NAME}?", + "kickText": "ركل", + "kickVoteCantKickAdminsText": "لا يمكن ركل المضيف", + "kickVoteCantKickSelfText": "يمكن ركل نفسك", + "kickVoteFailedNotEnoughVotersText": "عدد الاعبين لا يكفي للتصويت.", + "kickVoteFailedText": "أخفق التصويت بالركلة.", + "kickVoteStartedText": "تم بدء تصويت ركلة مقابل ${NAME}.", + "kickVoteText": "التصويت لركلة", + "kickVotingDisabledText": "التصويت بركلة غير مفعل", + "kickWithChatText": "اكتب ${YES} في الدردشة من أجل نعم و ${NO} من أجل لا.", + "killsTallyText": "${COUNT} يقتل", + "killsText": "يقتل", + "kioskWindow": { + "easyText": "سهل", + "epicModeText": "وضع ملحمي", + "fullMenuText": "قائمة كاملة", + "hardText": "الصعب", + "mediumText": "متوسط", + "singlePlayerExamplesText": "لاعب واحد / التعاونية أمثلة", + "versusExamplesText": "مقابل أمثلة" + }, + "languageSetText": "اللغة الآن \"${LANGUAGE}\".", + "lapNumberText": "جوله ${CURRENT}/${TOTAL}", + "lastGamesText": "(آخر ${COUNT} مباراة)", + "leaderboardsText": "المتصدرين", + "league": { + "allTimeText": "كل الوقت", + "currentSeasonText": "الموسم الحالي (${NUMBER})", + "leagueFullText": "${NAME} الدوري", + "leagueRankText": "رتبه الدوري", + "leagueText": "الدوري", + "rankInLeagueText": "#${RANK}, ${NAME} الدوري${SUFFIX}", + "seasonEndedDaysAgoText": "انتهى الموسم قبل ${NUMBER} يوم.", + "seasonEndsDaysText": "ينتهي الموسم بعد ${NUMBER} من الأيام.", + "seasonEndsHoursText": "ينتهي الموسم بعد ${NUMBER} ساعة.", + "seasonEndsMinutesText": "ينتهي الموسم بعد ${NUMBER} من الدقائق.", + "seasonText": "الموسم ${NUMBER}", + "tournamentLeagueText": "يجب أن تصل إلى الدوري ${NAME} للدخول في هذه البطولة.", + "trophyCountsResetText": "سيتم إعادة تعيين عدد الكوؤس في الموسم المقبل." + }, + "levelBestScoresText": "أفضل النقاط على ${LEVEL}", + "levelBestTimesText": "أفضل الأوقات على ${LEVEL}", + "levelIsLockedText": "تم قفل ${LEVEL}.", + "levelMustBeCompletedFirstText": "يجب إكمال ${LEVEL} أولا.", + "levelText": "المرحله ${NUMBER}", + "levelUnlockedText": "فتحت المرحله", + "livesBonusText": "يعيش مكافأة", + "loadingText": "جار التحميل", + "loadingTryAgainText": "جار التحميل؛ حاول مرة أخرى في لحظة ...", + "macControllerSubsystemBothText": "كلا (غير مستحسن)", + "macControllerSubsystemClassicText": "كلاسيكي", + "macControllerSubsystemDescriptionText": "(حاول تغيير هذا إذا وحدات التحكم الخاصة بك لا تعمل)", + "macControllerSubsystemMFiNoteText": "تم العثور على وحدة التحكم التي تم إنشاؤها لنظام التشغيل يوس / ماك؛\nقد تحتاج إلى تمكين هذه في إعدادات -> وحدات تحكم", + "macControllerSubsystemMFiText": "المصممة خصيصا اي او اس / ماك", + "macControllerSubsystemTitleText": "دعم جهاز التحكم", + "mainMenu": { + "creditsText": "معلومات", + "demoMenuText": "عرض القائمة", + "endGameText": "نهاية لعبة", + "exitGameText": "الخروج من اللعبة", + "exitToMenuText": "هل تريد الخروج من القائمة؟", + "howToPlayText": "كيف ألعب", + "justPlayerText": "(فقط ${NAME})", + "leaveGameText": "أترك اللعبة", + "leavePartyConfirmText": "هل تريد حقا ترك الحفله؟", + "leavePartyText": "ترك الحفله", + "quitText": "اخرج", + "resumeText": "متابعه", + "settingsText": "الاعدادات" + }, + "makeItSoText": "اجعلها كذلك", + "mapSelectGetMoreMapsText": "الحصول على المزيد من الخرائط ...", + "mapSelectText": "تحديد...", + "mapSelectTitleText": "${GAME} خرائط", + "mapText": "خرائط", + "maxConnectionsText": "اتصالات مكتمل", + "maxPartySizeText": "اقصي حجم للحفله", + "maxPlayersText": "عدد لاعبين مكتمل", + "modeArcadeText": "وضع الأركيد", + "modeClassicText": "الوضع الكلاسيكي", + "modeDemoText": "الوضع التجريبي", + "mostValuablePlayerText": "اكثر قيمه للاعب", + "mostViolatedPlayerText": "اللاعب الأكثر انتهاكاً", + "mostViolentPlayerText": "معظم لاعب عنيف", + "moveText": "تحرك", + "multiKillText": "${COUNT}-قتل!!!", + "multiPlayerCountText": "${COUNT} الاعبين", + "mustInviteFriendsText": "ملاحظة: يجب دعوة الأصدقاء في\nلوحة \"${GATHER}\" أو إرفاقها\nوحدات تحكم للعب متعددة.", + "nameBetrayedText": "${NAME} خيانه ${VICTIM}.", + "nameDiedText": "${NAME} توفي.", + "nameKilledText": "${NAME} قتل ${VICTIM}.", + "nameNotEmptyText": "لا يمكن أن يكون الاسم فارغا!", + "nameScoresText": "${NAME} نقاط!", + "nameSuicideKidFriendlyText": "${NAME} توفي عن طريق الخطأ.", + "nameSuicideText": "${NAME} انتحر.", + "nameText": "اسم", + "nativeText": "محلي", + "newPersonalBestText": "أفضل شخصية جديدة!", + "newTestBuildAvailableText": "يتوفر اختبار اختبار أحدث! (${VERSION} بناء ${BUILD}).\nاحصل على ${ADDRESS}", + "newText": "الجديد", + "newVersionAvailableText": "يتوفر إصدار أحدث من ${APP_NAME}! (${VERSION})", + "nextAchievementsText": "الإنجازات التالية:", + "nextLevelText": "المرحلة التالية", + "noAchievementsRemainingText": "- لا شيء", + "noContinuesText": "(لا يستمر)", + "noExternalStorageErrorText": "لم يتم العثور على وحدة تخزين خارجية على هذا الجهاز", + "noGameCircleText": "خطأ: لم يتم تسجيل الدخول الئ gamecircle", + "noScoresYetText": "لا نقاط حتى الآن.", + "noThanksText": "لا شكرا", + "noTournamentsInTestBuildText": "تحذير: سيتم تجاهل نقاط البطولات في النسخة التجريبية", + "noValidMapsErrorText": "لا خرائط صالحة وجدت لهذا النوع اللعبة.", + "notEnoughPlayersRemainingText": "لا يكفي اللاعبين المتبقين؛ الخروج وبدء لعبة جديدة.", + "notEnoughPlayersText": "تحتاج على الأقل ${COUNT} لاعبين لبدء هذه اللعبة!", + "notNowText": "ليس الآن", + "notSignedInErrorText": "يجب ان تسجل الدخول لتفعل هذا", + "notSignedInGooglePlayErrorText": "عليك تسجيل الدخول لجوجل بلاي لتفعل هذا", + "notSignedInText": "لم تقم بتسجيل الدخول", + "nothingIsSelectedErrorText": "لا شئ تم اختياره!", + "numberText": "#${NUMBER}", + "offText": "ايقاف", + "okText": "حسنا", + "onText": "تشغيل", + "oneMomentText": "لحظة واحدة...", + "onslaughtRespawnText": "${PLAYER} سيخرج مجددا في الموجة ${WAVE}", + "orText": "${A} أو ${B}", + "otherText": "آخر...", + "outOfText": "(#${RANK} خرج من ${ALL})", + "ownFlagAtYourBaseWarning": "يجب على العلم الخاص بك ان يكون في قاعدتك\n!لتسجل نقطة", + "packageModsEnabledErrorText": "(انظر الاعدادات ــ> متقدم) local-package-mods اللعب على الشبكة غير مسموح عندما يكون", + "partyWindow": { + "chatMessageText": "رسالة الدردشة", + "emptyText": "حفلتك فارغة", + "hostText": "(مضيف)", + "sendText": "إرسال", + "titleText": "الحفلة الخاصة بك" + }, + "pausedByHostText": "(متوقف مؤقتا من قبل المضيف )", + "perfectWaveText": "المرحله المثالية", + "pickUpText": "إلتقط", + "playModes": { + "coopText": "اللعب التعاوني", + "freeForAllText": "الحرية-للجميع", + "multiTeamText": "فرق متعددة", + "singlePlayerCoopText": "لعب فردي / لعب تعاوني", + "teamsText": "فرق" + }, + "playText": "لعب", + "playWindow": { + "oneToFourPlayersText": "1-4 لاعبين", + "titleText": "إلعب", + "twoToEightPlayersText": "2-8 لاعبين" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerDelayedJoinText": "${PLAYER} سيدخل ببداية الجولة القادمة", + "playerInfoText": "معلومات اللاعب", + "playerLeftText": "${PLAYER} ترك اللعبة", + "playerLimitReachedText": "حد اللاعب ${COUNT} وصل", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "لايمكنك حذف حساب الملف الشخصي الخاص بك", + "deleteButtonText": "حذف\nالملف الشخصي", + "deleteConfirmText": "حذف '${PROFILE}'?", + "editButtonText": "تعديل\nالملف الشخصي", + "explanationText": "(اللاعب المخصص و المباراة لهذا الحساب)", + "newButtonText": "ملف شخصي\nجديد", + "titleText": "ملفات اللاعب الشخصي" + }, + "playerText": "لاعب", + "playlistNoValidGamesErrorText": "قائمة التشغيل هذه لا تحتوي على ألعاب مفتوحة صالحة", + "playlistNotFoundText": "لم يتم العثور على قائمة التشغيل", + "playlistText": "قائمة التشغيل", + "playlistsText": "قائمة العب", + "pleaseRateText": "يرجى اتخاذ لحظة وتقييمه ${APP_NAME} اذا كنت تستمتع بلعبة\nاو كتابة مراجعة فهاذا يوفر معلومات مفيدة ويوفر التطوير\nفي المستقبل\n\n!شكرًا\nاريك—", + "pleaseWaitText": "الرجاء الانتظار . . .", + "pluginsDetectedText": "تم العتور على اضافات جديدة. فعلها او عدلها في الاعدادات", + "pluginsText": "اضافات", + "practiceText": "تدريب", + "pressAnyButtonPlayAgainText": "اضغط اي زر للعب مجددا...", + "pressAnyButtonText": "اظغط اي زر للإستمرار...", + "pressAnyButtonToJoinText": "اضغط اي زر للإنضمام", + "pressAnyKeyButtonPlayAgainText": "اضغط اي زر /اي مفتاح للعب مجددا...", + "pressAnyKeyButtonText": "اضغظ اي مفتاح/زر للإستمرار", + "pressAnyKeyText": "اضغظ اي مفتاح...", + "pressJumpToFlyText": "** اضعط على القفز مرارا وتكرارا لطيران **", + "pressPunchToJoinText": "اضغط على زر اللكمة للإنضمام", + "pressToOverrideCharacterText": "اضغط ${BUTTONS} لتجاوز شخصيتك", + "pressToSelectProfileText": "اضغظ ${BUTTONS} لإختيار لاعب", + "pressToSelectTeamText": "اضغظ ${BUTTONS} لإختيار فريق", + "promoCodeWindow": { + "codeText": "كود", + "enterText": "ادخل" + }, + "promoSubmitErrorText": "خطأ في إرسال الكود: تحقق من اتصالك بالإنترنت", + "ps3ControllersWindow": { + "macInstructionsText": "إيقاف الطاقة على الجزء الخلفي من PS3 الخاص بك، تأكد\nيتم تمكين البلوتوث على جهاز ماك، ثم توصيل وحدة التحكم\nإلى ماك الخاص بك عن طريق كابل أوسب لإقران اثنين. من ذلك الحين، أنت\nيمكن استخدام زر المنزل وحدة تحكم لتوصيله إلى ماك الخاص بك\nفي أي من السلكي (أوسب) أو اللاسلكية (بلوتوث) واسطة.\n\nفي بعض أجهزة ماك قد يطلب منك رمز مرور عند الاقتران.\nفي حالة حدوث ذلك، راجع البرنامج التعليمي التالي أو غوغل للحصول على مساعدة.\n\n\n\n\nوحدات تحكم PS3 متصلة لاسلكيا يجب أن تظهر في الجهاز\nفي تفضيلات النظام -> بلوتوث. قد تحتاج إلى إزالتها\nمن تلك القائمة عندما تريد استخدامها مع PS3 الخاص بك مرة أخرى.\n\nتأكد أيضا من قطع الاتصال بهم من بلوتوث عندما لا تكون في\nاستخدام أو بطارياتهم سوف تستمر في استنزاف.\n\nبلوتوث يجب التعامل مع ما يصل إلى 7 أجهزة متصلة،\nعلى الرغم من عدد الكيلومترات قد تختلف.", + "ouyaInstructionsText": "لاستخدام وحدة تحكم PS3 مع أويا الخاص بك، ببساطة توصيله مع كابل أوسب\nمرة واحدة لإقران ذلك. القيام بذلك قد قطع وحدات التحكم الأخرى الخاصة بك، لذلك\nيجب عليك ثم إعادة تشغيل أويا وافصل كابل أوسب.\n\nمن ذلك الحين على يجب أن تكون قادرا على استخدام زر المنزل تحكم ل\nتوصيله لاسلكيا. عند الانتهاء من اللعب، اضغط على زر هوم\nلمدة 10 ثانية لتحويل وحدة تحكم قبالة؛ وإلا فإنه قد يبقى على\nو نفايات البطاريات.", + "pairingTutorialText": "الاقتران فيديو تعليمي", + "titleText": "استخدام وحدات تحكم PS3 مع ${APP_NAME}:" + }, + "punchBoldText": "لكمة", + "punchText": "لكمة", + "purchaseForText": "${PRICE} إشتري اللعبة", + "purchaseGameText": "إشتري اللعبة", + "purchasingText": "شراء...", + "quitGameText": "خروج ${APP_NAME}?", + "quittingIn5SecondsText": "جار الإقلاع خلال 5 ثوان ...", + "randomPlayerNamesText": "الأسماء الافتراضية", + "randomText": "عشوائي", + "rankText": "مرتبة", + "ratingText": "تقييم", + "reachWave2Text": "تصل موجة 2 إلى رتبة.", + "readyText": "جاهز", + "recentText": "الأخيرة", + "remoteAppInfoShortText": "${APP_NAME} أكثر متعة عندما يتم تشغيله مع العائلة والأصدقاء.\nقم بتوصيل وحدة تحكم أجهزة أو أكثر أو قم بتثبيت\n${REMOTE_APP_NAME} على الهواتف أو الأجهزة اللوحية لاستخدامها\nكما تحكم.", + "remote_app": { + "app_name": "بومبسكاد البعيد", + "app_name_short": "BSريموت", + "button_position": "زر الموضع", + "button_size": "حجم الزر", + "cant_resolve_host": "لا يمكن حل المضيف.", + "capturing": "اسر…", + "connected": "تم الاتصال", + "description": "استخدام الهاتف أو الكمبيوتر اللوحي كمحكم مع بومبسكاد.\nما يصل إلى 8 أجهزة يمكن الاتصال في وقت واحد ل ملحمة الجنون المحلي متعددة على جهاز تلفزيون واحد أو قرص.", + "disconnected": "قطع الاتصال من السيرفر.", + "dpad_fixed": "تم الاصلاح", + "dpad_floating": "يطفو على السطح", + "dpad_position": "D- الوسادة الموقف", + "dpad_size": "قياس اللوحة", + "dpad_type": "D- الوسادة نوع", + "enter_an_address": "أدخل عنوانا", + "game_full": "اللعبة كاملة أو لا تقبل الاتصالات.", + "game_shut_down": "العبة لقد اطفأة", + "hardware_buttons": "أزرار الأجهزة", + "join_by_address": "الانضمام بحسب العنوان ...", + "lag": "تاخر: ${SECONDS} الثانيه", + "reset": "إعادة تعيين إلى الافتراضي", + "run1": "تشغيل 1", + "run2": "تشغيل 2", + "searching": "جار البحث عن ألعاب بومبسكاد ...", + "searching_caption": "اضغط على اسم لعبة للانضمام إليه.\nتأكد من أنك على نفس شبكة واي فاي مثل اللعبة.", + "start": "بداية", + "version_mismatch": "عدم تطابق إصدار.\nتأكد من بومبسكاد و بومبسكاد البعيد\nهي أحدث الإصدارات وحاول مرة أخرى." + }, + "removeInGameAdsText": "إلغاء تأمين \"${PRO}\" في المتجر لإزالة الإعلانات داخل اللعبة.", + "renameText": "إعادة تسمية", + "replayEndText": "نهاية الإعادة", + "replayNameDefaultText": "آخر لعبة الإعادة", + "replayReadErrorText": "حدث خطأ أثناء قراءة ملف إعادة التشغيل.", + "replayRenameWarningText": "إعادة تسمية \"${REPLAY}\" بعد لعبة إذا كنت ترغب في الاحتفاظ بها. وإلا فإنه سيتم الكتابة فوقه.", + "replayVersionErrorText": "عذرا، تم إجراء هذا الإعادة في صورة مختلفة\nنسخة من اللعبة ولا يمكن استخدامها.", + "replayWatchText": "مشاهدة الإعادة", + "replayWriteErrorText": "حدث خطأ أثناء كتابة ملف إعادة التشغيل.", + "replaysText": "الاعادة", + "reportPlayerExplanationText": "استخدم هذه الرسالة الإلكترونية للإبلاغ عن الغش أو اللغة غير الملائمة أو أي سلوك سيئ آخر.\nيرجى وصف ما يلي:", + "reportThisPlayerCheatingText": "غش", + "reportThisPlayerLanguageText": "لغة غير لائقة", + "reportThisPlayerReasonText": "ماذا تريد أن تقدم؟", + "reportThisPlayerText": "تقرير هذا اللاعب", + "requestingText": "طلب ...", + "restartText": "اعادة التشغيل", + "retryText": "اعادة المحاولة", + "revertText": "العودة", + "runText": "جري", + "saveText": "حفظ", + "scanScriptsErrorText": "حدث خطأ (أخطاء) في مسح النصوص البرمجية ؛ انظر السجل للحصول على التفاصيل.", + "scoreChallengesText": "نقاط التحديات", + "scoreListUnavailableText": "قائمة النقاط غير متاحة.", + "scoreText": "نتيجة", + "scoreUnits": { + "millisecondsText": "ميلي ثانية", + "pointsText": "نقاط", + "secondsText": "ثواني" + }, + "scoreWasText": "(كان ${COUNT})", + "selectText": "اختيار", + "seriesWinLine1PlayerText": "الفوز", + "seriesWinLine1TeamText": "الفوز", + "seriesWinLine1Text": "الفوز", + "seriesWinLine2Text": "سلسلة", + "settingsWindow": { + "accountText": "الحساب", + "advancedText": "المتقدمة", + "audioText": "صوت", + "controllersText": "التحكم", + "graphicsText": "رسوميات", + "playerProfilesMovedText": "ملاحظة: انتقلت ملفات تعريف اللاعب إلى نافذة الحساب في القائمة الرئيسية.", + "titleText": "اعدادات" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(بسيطة، وحدة تحكم ودية على الشاشة لوحة المفاتيح لتحرير النص)", + "alwaysUseInternalKeyboardText": "استخدم لوحة المفاتيح الداخلية دائما", + "benchmarksText": "المعايير و الإجهاد الاختبارات", + "disableCameraGyroscopeMotionText": "تعطيل الكاميرا جيروسكوب الحركة", + "disableCameraShakeText": "تعطيل اهتزاز الكاميرا", + "disableThisNotice": "(يمكنك تعطيل هذا الإشعار في الإعدادات المتقدمة)", + "enablePackageModsDescriptionText": "(تمكن قدرات التعديل الإضافية ولكن تعطيل شبكة اللعب)", + "enablePackageModsText": "تمكين تعديل الحزمة المحلية", + "enterPromoCodeText": "أدخل الكود الترويجي", + "forTestingText": "ملاحظة: هذه القيم هي فقط للاختبار وسيتم فقدانها عند خروج التطبيق.", + "helpTranslateText": "${APP_NAME} الترجمات غير الإنجليزية هي منتدى\nبدعم الجهود. إذا كنت ترغب في المساهمة أو التصحيح\nترجمة، اتبع الرابط أدناه. شكرا مقدما!", + "kickIdlePlayersText": "ركلة اللاعبين الخمول", + "kidFriendlyModeText": "وضع الصديقة للطفل (انخفاض العنف، الخ)", + "languageText": "لغة", + "moddingGuideText": "دليل مودينغ", + "mustRestartText": "يجب إعادة تشغيل اللعبة حتى تصبح نافذة المفعول.", + "netTestingText": "اختبار الشبكة", + "resetText": "إعادة تعيين", + "showBombTrajectoriesText": "عرض مسارات القنبلة", + "showPlayerNamesText": "إظهار أسماء اللاعبين", + "showUserModsText": "عرض مجلد التعديل", + "titleText": "المتقدمة", + "translationEditorButtonText": "${APP_NAME} محرر الترجمة", + "translationFetchErrorText": "حالة الترجمة غير متاحة", + "translationFetchingStatusText": "جار التحقق من حالة الترجمة ...", + "translationInformMe": "أبلغني عندما تحتاج لغتي التحديثات", + "translationNoUpdateNeededText": "اللغة الحالية هي حتى الآن. محدثه!", + "translationUpdateNeededText": "** اللغة الحالية يحتاج التحديثات !! **", + "vrTestingText": "فر اختبار" + }, + "shareText": "شارك", + "sharingText": "مشاركة...", + "showText": "عرض", + "signInForPromoCodeText": "يجب تسجيل الدخول إلى حساب لكي يتم تفعيل الرموز.", + "signInWithGameCenterText": "لاستخدام حساب مركز الألعاب،\nسجل الدخول باستخدام تطبيق مركز الألعاب.", + "singleGamePlaylistNameText": "فقط ${GAME}", + "singlePlayerCountText": "1 لاعب", + "soloNameFilterText": "منفردا ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "اختار شخصيه", + "Chosen One": "المختار", + "Epic": "وضع الالعاب ملحمه", + "Epic Race": "سباق ملحمي", + "FlagCatcher": "أمسك العلم", + "Flying": "أفكار سعيدة", + "Football": "كرة القدم", + "ForwardMarch": "الاعتداءات", + "GrandRomp": "غزو", + "Hockey": "الهوكي", + "Keep Away": "ابتعد", + "Marching": "يركض حول", + "Menu": "القائمة الرئيسية", + "Onslaught": "هجوم", + "Race": "سباق", + "Scary": "ملك التل", + "Scores": "شاشة النتيجة", + "Survival": "إزالة", + "ToTheDeath": "مباراة الموت", + "Victory": "شاشه النتيجه النهائيه" + }, + "spaceKeyText": "الفراغ", + "statsText": "النتائج", + "storagePermissionAccessText": "وهذا يتطلب الوصول إلى التخزين", + "store": { + "alreadyOwnText": "أنت تملك بالفعل ${NAME}!", + "bombSquadProNameText": "${APP_NAME} برو", + "bombSquadProNewDescriptionText": "• يزيل الإعلانات في اللعبة والشاشات تذمر\n• يفتح المزيد من إعدادات اللعبة\n• تحتوي ايضا:", + "buyText": "شراء", + "charactersText": "الشخصيات", + "comingSoonText": "قريبا...", + "extrasText": "إضافات", + "freeBombSquadProText": "بومبسكاد هو الآن مجانا، ولكن منذ كنت أصلا اشتريت أنت\nوتلقي ترقية بومبسكاد برو و ${COUNT} تذاكر كما شكر لك.\nتتمتع الميزات الجديدة، وشكرا لكم على دعمكم!\nاريك", + "holidaySpecialText": "عطلة خاصة", + "howToSwitchCharactersText": "(انتقل إلى \"${SETTINGS} -> ${PLAYER_PROFILES}\" لتعيين وتخصيص الأحرف)", + "howToUseIconsText": "(إنشاء ملفات تعريف لاعب العالمية (في إطار الحساب) لاستخدام هذه)", + "howToUseMapsText": "(استخدم هذه الخرائط في فرقك الخاصة / قوائم التشغيل المجانية للجميع)", + "iconsText": "رموز", + "loadErrorText": "تعذر تحميل الصفحة.\nتحقق من اتصالك بالإنترنت.", + "loadingText": "جار التحميل", + "mapsText": "خرائط", + "miniGamesText": "ألعاب مصغرة", + "oneTimeOnlyText": "(مرة واحدة فقط)", + "purchaseAlreadyInProgressText": "هناك شراء لهذا العنصر قيد التقدم.", + "purchaseConfirmText": "هل تريد شراء ${ITEM}؟", + "purchaseNotValidError": "الشراء غير صالح.\nاتصل بال ${EMAIL} إذا كان هذا خطأ.", + "purchaseText": "شراء", + "saleBundleText": "حزمة بيع!", + "saleExclaimText": "تخفيض السعر!", + "salePercentText": "(${PERCENT}٪ أقل)", + "saleText": "تخفيض السعر", + "searchText": "بحث", + "teamsFreeForAllGamesText": "فرق / مجانا للجميع الألعاب", + "totalWorthText": "*** ${TOTAL_WORTH} قيمة! ***", + "upgradeQuestionText": "?ترقيه", + "winterSpecialText": "الشتاء خاص", + "youOwnThisText": "- انت تملك هذا -" + }, + "storeDescriptionText": "8 لاعب حفله لعبة الجنون!\n\nتفجير أصدقائك (أو الكمبيوتر) في البطولة من الألعاب المصغرة المتفجرة مثل القبض على العلم، منفذها هوكي، وملحمة بطيئة الحركة الموت الموت!\n\nضوابط بسيطة ودعم وحدة تحكم واسعة تجعل من السهل لمدة تصل إلى 8 أشخاص للحصول على في العمل. يمكنك حتى استخدام الأجهزة النقالة الخاصة بك عن طريق التحكم عن طريق الحرة 'بومبسكاد البعيد' التطبيق!\n\nالقنابل بعيدا!\n\nتحقق من www.froemling.net/bombsquad لمزيد من المعلومات.", + "storeDescriptions": { + "blowUpYourFriendsText": "تفجير أصدقائك.", + "competeInMiniGamesText": "تنافس في الألعاب المصغرة بدءا من السباق للطيران.", + "customize2Text": "تخصيص الشخصيات، الألعاب المصغرة، وحتى الموسيقى التصويرية.", + "customizeText": "تخصيص الشخصيات وإنشاء قوائم التشغيل الخاصة بك لعبة صغيرة.", + "sportsMoreFunText": "الرياضة أكثر متعة مع المتفجرات.", + "teamUpAgainstComputerText": "فريق ضد الكمبيوتر." + }, + "storeText": "متجر", + "submitText": "ارسال", + "submittingPromoCodeText": "تقديم الكود ...", + "teamNamesColorText": "اسماء/الوان الفرق...", + "telnetAccessGrantedText": "تم تمكين الوصول تلنيت.", + "telnetAccessText": "تم الكشف عن الوصول تلنيت. السماح؟", + "testBuildErrorText": "لم يعد بناء الاختبار نشطا؛ يرجى التحقق من إصدار جديد.", + "testBuildText": "اختبار البناء", + "testBuildValidateErrorText": "تعذر التحقق من صلاحية اختبار الاختبار. (لا يوجد اتصال صافي؟)", + "testBuildValidatedText": "اختبار بناء تم التحقق منه؛ استمتع!", + "thankYouText": "شكرا لدعمكم! استمتع باللعبة!!", + "threeKillText": "القتل الثلاثي", + "timeBonusText": "مكافأة الوقت", + "timeElapsedText": "الوقت المنقضي", + "timeExpiredText": "انتهى الوقت", + "timeSuffixDaysText": "${COUNT}ي", + "timeSuffixHoursText": "${COUNT}س", + "timeSuffixMinutesText": "${COUNT}د", + "timeSuffixSecondsText": "${COUNT}ث", + "tipText": "تلميح", + "titleText": "فرقة القنبلة", + "titleVRText": "فرقة القنبلة فر", + "topFriendsText": "أفضل الأصدقاء", + "tournamentCheckingStateText": "التحقق من حالة البطولة. أرجو الإنتظار...", + "tournamentEndedText": "انتهت هذه البطولة. وسوف تبدأ واحدة جديدة قريبا.", + "tournamentEntryText": "دخول البطولة", + "tournamentResultsRecentText": "نتائج البطولة الأخيرة", + "tournamentStandingsText": "ترتيب البطولة", + "tournamentText": "المسابقة", + "tournamentTimeExpiredText": "انتهت مدة البطولة", + "tournamentsText": "البطولات", + "translations": { + "characterNames": { + "Agent Johnson": "وكيل جونسون", + "B-9000": "بي-9000", + "Bernard": "الدب برنارد", + "Bones": "هيكل عظمي", + "Butch": "بوتش", + "Easter Bunny": "أرنب عيد الفصح", + "Flopsy": "فلوبسي", + "Frosty": "رجل ثلج", + "Gretel": "جريتل", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "جاك مرجان", + "Kronk": "كرونك", + "Lee": "لي", + "Lucky": "سعيد الحظ", + "Mel": "ميل", + "Middle-Man": "الرجل المتوسط", + "Minimus": "أدنى لا", + "Pascal": "بطريق", + "Pixel": "الفراشه", + "Sammy Slam": "سامي سلام", + "Santa Claus": "سانتا كلوس", + "Snake Shadow": "ظل الافعى", + "Spaz": "Spaz", + "Taobao Mascot": "التميمه تاوباو", + "Todd McBurton": "تود بيرتون", + "Zoe": "زوي", + "Zola": "زولا" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} التدريب", + "Infinite ${GAME}": "غير محدود ${GAME}", + "Infinite Onslaught": "هجمة لانهائية", + "Infinite Runaround": "لانهائي يركض حول", + "Onslaught Training": "التدريب هجمة", + "Pro ${GAME}": "برو ${GAME}", + "Pro Football": "كرة القدم الإحترافية", + "Pro Onslaught": "هجمه الاحترافيه", + "Pro Runaround": "يركض حول الاحترافيه", + "Rookie ${GAME}": "الصاعد ${GAME}", + "Rookie Football": "الصاعد كرة القدم", + "Rookie Onslaught": "هجمه الصاعد", + "The Last Stand": "الموقف الأخير", + "Uber ${GAME}": "اوبر ${GAME}", + "Uber Football": "اوبر لكرة القدم", + "Uber Onslaught": "اوبر الهجمة", + "Uber Runaround": "الجري حول: وضع صعوبة الاوبر" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "يكون اختيار واحد لفترة من الوقت للفوز.\nقتل اختيار واحد لتصبح عليه.", + "Bomb as many targets as you can.": "قنبلة العديد من الأهداف ما تستطيع.", + "Carry the flag for ${ARG1} seconds.": "حمل العلم مقابل ${ARG1} ثانية.", + "Carry the flag for a set length of time.": "احمل العلم لمدة محددة من الزمن", + "Crush ${ARG1} of your enemies.": "سحق ${ARG1} من أعدائك.", + "Defeat all enemies.": "هزيمة جميع الأعداء.", + "Dodge the falling bombs.": "دودج القنابل السقوط.", + "Final glorious epic slow motion battle to the death.": "النهائي المجيدة ملحمة حركة بطيئة معركة حتى الموت.", + "Gather eggs!": "جمع البيض!", + "Get the flag to the enemy end zone.": "الحصول على العلم إلى المنطقة نهاية العدو.", + "How fast can you defeat the ninjas?": "هل يمكنك هزيمه النينجا باسرع وقت ممكن؟", + "Kill a set number of enemies to win.": "قتل عدد معين من الأعداء للفوز.", + "Last one standing wins.": "آخر واحد يبقى يفوز.", + "Last remaining alive wins.": "آخر شخص يبقى حياً يفوز", + "Last team standing wins.": "آخر فريق يقف يفوز.", + "Prevent enemies from reaching the exit.": "امنع الأعداء من الوصول إلى النهاية", + "Reach the enemy flag to score.": "صِل إلى العلم العدو لتسجل", + "Return the enemy flag to score.": "عودة علم العدو ليسجل.", + "Run ${ARG1} laps.": "جري لفات ${ARG1}.", + "Run ${ARG1} laps. Your entire team has to finish.": "جري لفات ${ARG1}. يجب أن ينتهي الفريق بأكمله.", + "Run 1 lap.": "جري 1 اللفة.", + "Run 1 lap. Your entire team has to finish.": "جري 1 اللفة. يجب أن ينتهي الفريق بأكمله.", + "Run real fast!": "تشغيل سريع الحقيقي!", + "Score ${ARG1} goals.": "نقاط ${ARG1} اهداف.", + "Score ${ARG1} touchdowns.": "نقاط ${ARG1} الهبوط", + "Score a goal.": "سجل هدفا", + "Score a touchdown.": "يسجل هبوطا", + "Score some goals.": "تسجيل بعض الأهداف.", + "Secure all ${ARG1} flags.": "تأمين جميع أعلام ${ARG1}", + "Secure all flags on the map to win.": "تأمين جميع الأعلام على الخريطة للفوز.", + "Secure the flag for ${ARG1} seconds.": "تأمين العلم لمدة ${ARG1} ثانية.", + "Secure the flag for a set length of time.": "تأمين العلم لمدة محددة من الزمن.", + "Steal the enemy flag ${ARG1} times.": "سرقة العلم العدو ${ARG1} مرات.", + "Steal the enemy flag.": "سرقة علم العدو.", + "There can be only one.": "يمكن أن يكون هناك واحد فقط.", + "Touch the enemy flag ${ARG1} times.": "المس علم العدو ${ARG1} مرات.", + "Touch the enemy flag.": "المس علم العدو.", + "carry the flag for ${ARG1} seconds": "ثانية ${ARG1} احمل العلم لمدة", + "kill ${ARG1} enemies": "اعداء ${ARG1} اقتل", + "last one standing wins": "آخر واحد يقف يفوز", + "last team standing wins": "آخر فريق يتبقى يفوز", + "return ${ARG1} flags": "ارجاع ${ARG1} الاعلام", + "return 1 flag": "ارجاع 1 الاعلام", + "run ${ARG1} laps": "جري ${ARG1} لفات", + "run 1 lap": "جري 1 لفات", + "score ${ARG1} goals": "نقاط ${ARG1} اهداف", + "score ${ARG1} touchdowns": "نقاط ${ARG1} الهبوط", + "score a goal": "سجل هدفا", + "score a touchdown": "يسجل هبوطا", + "secure all ${ARG1} flags": "تأمين جميع أعلام $ {ARG1}", + "secure the flag for ${ARG1} seconds": "تأمين العلم مقابل ${ARG1} ثانية", + "touch ${ARG1} flags": "المس أعلام ${ARG1}", + "touch 1 flag": "المس 1 العلم" + }, + "gameNames": { + "Assault": "الاعتداءات", + "Capture the Flag": "أمسك العلم", + "Chosen One": "المختار", + "Conquest": "غزو", + "Death Match": "مباراة الموت", + "Easter Egg Hunt": "بيضة عيد الفصح هانت", + "Elimination": "إزالة", + "Football": "كرة القدم", + "Hockey": "الهوكي", + "Keep Away": "ابتعد", + "King of the Hill": "ملك التل", + "Meteor Shower": "دش النيازك", + "Ninja Fight": "قتال النينجا", + "Onslaught": "هجوم", + "Race": "السباق", + "Runaround": "يركض حول", + "Target Practice": "الممارسة المستهدفة", + "The Last Stand": "الموقف الأخير" + }, + "inputDeviceNames": { + "Keyboard": "لوحة المفاتيح", + "Keyboard P2": "لوحة المفاتيح P2" + }, + "languages": { + "Arabic": "عربى", + "Belarussian": "البيلاروسية", + "Chinese": "الصينية المبسطة", + "ChineseTraditional": "التقليدية الصينية", + "Croatian": "الكرواتية", + "Czech": "تشيكي", + "Danish": "دانماركي", + "Dutch": "هولندي", + "English": "الإنجليزية", + "Esperanto": "الاسبرانتو", + "Finnish": "اللغة الفنلندية", + "French": "الفرنسية", + "German": "الألمانية", + "Gibberish": "رطانة", + "Greek": "الإغريقية", + "Hindi": "الهندية", + "Hungarian": "الهنغارية", + "Indonesian": "الأندونيسية", + "Italian": "الإيطالي", + "Japanese": "اليابانية", + "Korean": "الكورية", + "Persian": "اللغة الفارسية", + "Polish": "البولندي", + "Portuguese": "البرتغالية", + "Romanian": "روماني", + "Russian": "الروسية", + "Serbian": "الصربية", + "Slovak": "السلوفاكية", + "Spanish": "الأسبانية", + "Swedish": "اللغة السويدية", + "Turkish": "اللغة التركية", + "Ukrainian": "الأوكراني", + "Venetian": "فينيسي", + "Vietnamese": "الفيتنامية" + }, + "leagueNames": { + "Bronze": "البرونزي", + "Diamond": "الماسي", + "Gold": "الذهبي", + "Silver": "الفضي" + }, + "mapsNames": { + "Big G": "كبير G", + "Bridgit": "Bridgit", + "Courtyard": "فناء", + "Crag Castle": "قلعة كراغ", + "Doom Shroom": "الموت شروم", + "Football Stadium": "ملعب كرة القدم", + "Happy Thoughts": "أفكار سعيدة", + "Hockey Stadium": "ملعب الهوكي", + "Lake Frigid": "بحيرة فريجيد", + "Monkey Face": "وجه القرد", + "Rampage": "ثورة", + "Roundabout": "الدوار", + "Step Right Up": "خطوة إلى أعلى", + "The Pad": "الوسادة", + "Tip Top": "أعلى الحافة", + "Tower D": "برج D", + "Zigzag": "متعرج" + }, + "playlistNames": { + "Just Epic": "مجرد ملحمة", + "Just Sports": "الرياضة فقط" + }, + "scoreNames": { + "Flags": "أعلام", + "Goals": "الأهداف", + "Score": "أحرز هدفاً", + "Survived": "نجا", + "Time": "الوقت", + "Time Held": "وقت عقد" + }, + "serverResponses": { + "A code has already been used on this account.": "تم استخدام رمز بالفعل في هذا الحساب.", + "A reward has already been given for that address.": "وقد تم بالفعل إعطاء مكافأة لهذا العنوان.", + "Account linking successful!": "تم ربط الحساب بنجاح!", + "Account unlinking successful!": "تم إلغاء ربط الحساب بنجاح!", + "Accounts are already linked.": "الحسابات مرتبطة بالفعل.", + "An error has occurred; (${ERROR})": "حدثت مشكلة; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "حدثت مشكلة; برجاء التواصل مع الدعم. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "حدث خطأ؛ يرجى الاتصال support@froemling.net.", + "An error has occurred; please try again later.": "حدث خطأ؛ الرجاء معاودة المحاولة في وقت لاحق.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "هل تريد بالتأكيد ربط هذه الحسابات؟\n\n$ {ACCOUNT1}\n$ {ACCOUNT2}\n\nهذا لا يمكن التراجع عنها!", + "BombSquad Pro unlocked!": "لقد فتحت bomb squad الإصدار الاحترافي!", + "Can't link 2 accounts of this type.": "لا يمكن ربط حسابين من هذا النوع.", + "Can't link 2 diamond league accounts.": "لا يمكن ربط 2 حسابات الدوري الماس.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "لا يمكن الارتباط؛ سيتجاوز الحد الأقصى للحسابات المرتبطة ب ${COUNT} من الحسابات.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "الغش الكشف عنها. تم تعليق العشرات والجوائز لمدة ${COUNT} من الأيام.", + "Could not establish a secure connection.": "تعذر إنشاء اتصال آمن.", + "Daily maximum reached.": "الحد الأقصى اليومي الذي تم الوصول إليه.", + "Entering tournament...": "جار دخول البطولة ...", + "Invalid code.": "الرمز غير صحيح.", + "Invalid payment; purchase canceled.": "دفعة غير صالحة؛ تم إلغاء الشراء.", + "Invalid promo code.": "الرمز الترويجي غير صالح.", + "Invalid purchase.": "عملية شراء غير صالحة.", + "Invalid tournament entry; score will be ignored.": "دخول البطولة غير صالح؛ سيتم تجاهل النتيجة.", + "Item unlocked!": "العنصر غير مقفلة!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "ربط مرفوض. ${ACCOUNT} يحتوي علي\nبيانات كبيرة والتي سيتم فقدانها.\nتستطيع الربط بالامر المعاكس اذا احببت\n(و فقدان بيانات هذا الحساب بالمقابل)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "هل تريد ربط الحساب ${ACCOUNT} بهذا الحساب؟\nسيتم فقد جميع البيانات الموجودة في ${ACCOUNT}.\nهذا لا يمكن التراجع عنها. هل أنت واثق؟", + "Max number of playlists reached.": "تم الوصول إلى أقصى عدد من قوائم التشغيل.", + "Max number of profiles reached.": "تم الوصول إلى أقصى عدد من الملفات الشخصية.", + "Maximum friend code rewards reached.": "تم الوصول إلى الحد الأقصى لمكافآت الرمز الصديق.", + "Message is too long.": "الرسالة طويلة جدا", + "Profile \"${NAME}\" upgraded successfully.": "تمت ترقية الملف الشخصي \"${NAME}\" بنجاح.", + "Profile could not be upgraded.": "تعذر ترقية الملف الشخصي.", + "Purchase successful!": "شراء ناجحة!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "تم استلام ${COUNT} من التذاكر لتسجيل الدخول.\nارجع غدا لاستلام ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "لم تعد معتمدة وظيفة الخادم في هذا الإصدار من اللعبة.\nالرجاء التحديث إلى إصدار أحدث.", + "Sorry, there are no uses remaining on this code.": "عذرا، لا توجد أية استخدامات متبقية في هذه الشفرة.", + "Sorry, this code has already been used.": "عذرا، تم استخدام هذه الشفرة من قبل.", + "Sorry, this code has expired.": "عذرا، انتهت صلاحية هذا الرمز.", + "Sorry, this code only works for new accounts.": "عذرا، لا تعمل هذه الشفرة إلا لحسابات جديدة.", + "Temporarily unavailable; please try again later.": "غير متاح مؤقتا؛ الرجاء معاودة المحاولة في وقت لاحق.", + "The tournament ended before you finished.": "انتهت البطولة قبل الانتهاء.", + "This account cannot be unlinked for ${NUM} days.": "لا يمكن إلغاء ربط هذا الحساب بمبلغ ${NUM} من الأيام.", + "This code cannot be used on the account that created it.": "لا يمكن استخدام هذا الرمز على الحساب الذي أنشأه.", + "This is currently unavailable; please try again later.": "ذلك غير متوفر حاليا؛ يرجى المحاولة لاحقا", + "This requires version ${VERSION} or newer.": "يتطلب هذا الإصدار ${VERSION} أو أحدث.", + "Tournaments disabled due to rooted device.": "تم تعطيل الدورات نظراً لوجود روت", + "Tournaments require ${VERSION} or newer": "تتطلب الدورات ${VERSION} أو أحدث", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "هل تريد إلغاء ربط ${ACCOUNT} من هذا الحساب؟\nسيتم إعادة تعيين جميع البيانات على ${ACCOUNT}.\n(باستثناء الإنجازات في بعض الحالات)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "تحذير: تم إصدار شكاوى تتعلق بالقرصنة ضد حسابك.\nسيتم حظر الحسابات التي تم العثور عليها عن طريق القرصنة. يرجى لعب عادل.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "هل تريد ربط حساب الجهاز بهذا الحساب؟\n\nحساب الجهاز هو ${ACCOUNT1}\nهذا الحساب هو ${ACCOUNT2}\n\nهذا سيسمح لك للحفاظ على التقدم المحرز الخاص بك.\nتحذير: لا يمكن التراجع عن ذلك!", + "You already own this!": "كنت تملك هذا بالفعل!", + "You can join in ${COUNT} seconds.": "يمكنك الانضمام في ${COUNT} ثانية.", + "You don't have enough tickets for this!": "ليس لديك ما يكفي من تذاكر لهذا!", + "You don't own that.": "أنت لا تملك ذلك.", + "You got ${COUNT} tickets!": "لقد حصلت على تذاكر ${COUNT}!", + "You got a ${ITEM}!": "لقد حصلت على ${ITEM}!", + "You have been promoted to a new league; congratulations!": "لقد تم ترقيتك إلى الدوري الجديد. تهانينا!", + "You must update to a newer version of the app to do this.": "يجب تحديث إلى إصدار أحدث من التطبيق للقيام بذلك.", + "You must update to the newest version of the game to do this.": "يجب عليك التحديث إلى الإصدار الأحدث من اللعبة للقيام بذلك.", + "You must wait a few seconds before entering a new code.": "يجب الانتظار بضع ثوان قبل إدخال رمز جديد.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "لقد حصلت على الترتيب # ${RANK} في البطولة الأخيرة. شكرا للعب!", + "Your account was rejected. Are you signed in?": "لقد حُذف حسابك.هل قمت بتسجيل دخولك?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "تم تعديل نسختك من اللعبة.\nيرجى إعادة أي تغييرات وإعادة المحاولة.", + "Your friend code was used by ${ACCOUNT}": "تم استخدام رمز صديقك بواسطة ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 دقيقة", + "1 Second": "1 ثانيه", + "10 Minutes": "10 دقائق", + "2 Minutes": "2 دقيقة", + "2 Seconds": "2 ثانية", + "20 Minutes": "20 دقيقة", + "4 Seconds": "4 ثوان", + "5 Minutes": "5 دقائق", + "8 Seconds": "8 ثوان", + "Allow Negative Scores": "السماح بالعشرات السلبية", + "Balance Total Lives": "توازن إجمالي الحياة", + "Bomb Spawning": "قنبلة التفريخ", + "Chosen One Gets Gloves": "اختيار واحد يحصل قفازات", + "Chosen One Gets Shield": "اختار واحد يحصل درع", + "Chosen One Time": "اختار مرة واحدة", + "Enable Impact Bombs": "تمكين القنابل الأثر", + "Enable Triple Bombs": "تمكين القنابل الثلاثي", + "Entire Team Must Finish": "فريق كامل يجب الانتهاء", + "Epic Mode": "وضع ملحمة", + "Flag Idle Return Time": "العلم الخمول وقت العودة", + "Flag Touch Return Time": "العلم لمس عودة الوقت", + "Hold Time": "تمسك بالوقت", + "Kills to Win Per Player": "يقتل للفوز لكل لاعب", + "Laps": "لفات", + "Lives Per Player": "يعيش لكل لاعب", + "Long": "طويل", + "Longer": "طويل", + "Mine Spawning": "منجم", + "No Mines": "لا مناجم", + "None": "لا شيء", + "Normal": "عادي", + "Pro Mode": "وضع برو", + "Respawn Times": "أوقات الفجر", + "Score to Win": "يسجل الفوز", + "Short": "قصيرة", + "Shorter": "أقصر", + "Solo Mode": "وضع فردي", + "Target Count": "عدد الأهداف", + "Time Limit": "الوقت المحدد" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "تم استبعاد ${TEAM} بسبب ترك ${PLAYER}", + "Killing ${NAME} for skipping part of the track!": "قتل ${NAME} لتخطي جزء من المسار!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "تحذير الي ${NAME}: السرعة غير العادية / تخريب الازرار يسقطك ارضا." + }, + "teamNames": { + "Bad Guys": "الأشرار", + "Blue": "ازرق", + "Good Guys": "الأخيار", + "Red": "أحمر" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "A توقيت تماما تشغيل القفز تدور لكمة يمكن أن تقتل في ضربة واحدة\nوكسب لك مدى الحياة الاحترام من أصدقائك.", + "Always remember to floss.": "تذكر دائما الخيط.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "إنشاء ملفات تعريف لاعب لنفسك وأصدقائك مع\nالأسماء المفضلة لديك ومظاهر بدلا من استخدام تلك العشوائية.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "صناديق لعنة تتحول لك في قنبلة موقوتة.\nالعلاج الوحيد هو الاستيلاء بسرعة على حزمة الصحية.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "على الرغم من مظهرها، وقدرات جميع الشخصيات متطابقة،\nحتى مجرد اختيار أيهما واحد كنت أقرب تشبه.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "لا تحصل على مبهرج جدا مع أن درع الطاقة؛ لا يزال بإمكانك الحصول على نفسك ألقيت قبالة الهاوية.", + "Don't run all the time. Really. You will fall off cliffs.": "لا تعمل في كل وقت. هل حقا. سوف تسقط المنحدرات.", + "Don't spin for too long; you'll become dizzy and fall.": "لا تدور لفترة طويلة جدا. عليك أن تصبح بالدوار والسقوط.", + "Hold any button to run. (Trigger buttons work well if you have them)": "عقد أي زر لتشغيل. (أزرار الزناد تعمل بشكل جيد إذا كان لديك لهم)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "اضغط باستمرار على أي زر لتشغيله. ستحصل على الأماكن بشكل أسرع\nولكن لن تتحول بشكل جيد للغاية، لذلك احترس من المنحدرات.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "القنابل الثلجية ليست قوية جدا، لكنها تجمد\nأيا كان ضربهم، مما يجعلهم عرضة للتلف.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "إذا كان شخص ما يختار لك، لكمة لهم وأنها سوف ترك.\nهذا يعمل في الحياة الحقيقية أيضا.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "إذا كنت قصيرا على وحدات التحكم، فثبت تطبيق '${REMOTE_APP_NAME}'\nعلى الأجهزة المحمولة الخاصة بك لاستخدامها كأجهزة تحكم.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "إذا كنت تحصل على قنبلة لزجة عالقة لك، والقفز حولها وتدور في الدوائر. يمكنك\nهز القنبلة قبالة، أو إذا كان أي شيء آخر لحظاتك الأخيرة ستكون مسلية.", + "If you kill an enemy in one hit you get double points for it.": "إذا كنت قتل عدو في ضربة واحدة تحصل على نقاط ضعف لذلك.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "إذا كنت تلتقط لعنة، الأمل الوحيد للبقاء على قيد الحياة هو\nالعثور على السلطة الصحية حتى في ثوان القليلة المقبلة.", + "If you stay in one place, you're toast. Run and dodge to survive..": "إذا كنت البقاء في مكان واحد، وكنت نخب. تشغيل ودودج من أجل البقاء ..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "إذا كنت قد حصلت على الكثير من اللاعبين القادمة والذهاب، بدوره على 'لصناعة السيارات في ركلة الخمول لاعبين'\nتحت إعدادات في حالة نسي أحد لمغادرة اللعبة.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "إذا كان جهازك يحصل دافئ جدا أو كنت ترغب في الحفاظ على طاقة البطارية،\nرفض \"المرئيات\" أو \"قرار\" في إعدادات-> الرسومات", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "إذا كان إطار الإطارات متقطع، فجرب رفض الدقة\nأو المرئيات في إعدادات الرسومات اللعبة.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "في القبض على العلم العلم الخاص بك يجب أن يكون في قاعدة الخاص بك ليسجل، إذا كان الآخر\nفريق على وشك أن يسجل، وسرقة علمهم يمكن أن يكون وسيلة جيدة لوقفها.", + "In hockey, you'll maintain more speed if you turn gradually.": "في الهوكي، عليك الحفاظ على مزيد من السرعة إذا قمت بتشغيل تدريجيا.", + "It's easier to win with a friend or two helping.": "فإنه من الأسهل للفوز مع صديق أو اثنين مساعدة.", + "Jump just as you're throwing to get bombs up to the highest levels.": "القفز تماما كما كنت رمي ​​للحصول على القنابل تصل إلى أعلى المستويات.", + "Land-mines are a good way to stop speedy enemies.": "إن الألغام البرية وسيلة جيدة لوقف الأعداء العاجلين.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "العديد من الأشياء يمكن التقاطها وألقيت، بما في ذلك لاعبين آخرين. القذف\nأعدائك قبالة المنحدرات يمكن أن تكون استراتيجية فعالة وعاطفيا الوفاء.", + "No, you can't get up on the ledge. You have to throw bombs.": "لا، لا يمكنك الحصول على ما يصل على الحافة. لديك لرمي القنابل.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "يمكن للاعبين الانضمام وترك في منتصف معظم المباريات،\nويمكنك أيضا سد وفصل وحدات التحكم على الطاير.", + "Practice using your momentum to throw bombs more accurately.": "ممارسة استخدام الخاص بك لرمي القنابل بشكل أكثر دقة.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "اللكمات تفعل المزيد من الضرر أسرع القبضات الخاصة بك تتحرك،\nوذلك في محاولة تشغيل، والقفز، والغزل مثل مجنون.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "تشغيل ذهابا وإيابا قبل رمي قنبلة\nإلى \"الاصابة\" عليه ورميها أبعد.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "إخراج مجموعة من الأعداء من قبل\nوإطلاق قنبلة بالقرب من مربع تي ان تي.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "الرأس هو المنطقة الأكثر ضعفا، لذلك قنبلة لزجة\nإلى نوجين يعني عادة لعبة أكثر.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "هذا المستوى لا تنتهي أبدا، ولكن على درجة عالية هنا\nسوف كسب لك الاحترام الأبدية في جميع أنحاء العالم.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "وتستند قوة رمي على الاتجاه الذي عقد.\nلإرم شيء برفق أمامك، لا تحمل أي اتجاه.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "تعبت من الموسيقى التصويرية؟ استبدالها بنفسك!\nانظر إعدادات-> الصوت-> الموسيقى التصويرية", + "Try 'Cooking off' bombs for a second or two before throwing them.": "محاولة 'الطبخ قبالة' القنابل لمدة ثانية أو اثنين قبل رمي لهم.", + "Try tricking enemies into killing eachother or running off cliffs.": "محاولة خداع الأعداء في قتل بعضهم البعض أو الجري المنحدرات.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "استخدم زر البيك اب للاستيلاء على العلم <${PICKUP}", + "Whip back and forth to get more distance on your throws..": "سوط ذهابا وإيابا للحصول على مزيد من المسافة على رميات الخاص بك", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "يمكنك 'تهدف' اللكمات الخاصة بك عن طريق الغزل اليسار أو اليمين.\nوهذا مفيد لضرب الأشرار من حواف أو التهديف في الهوكي.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "يمكنك الحكم عندما قنبلة سوف تنفجر على أساس\nلون الشرر من فتيله: أصفر .. أورانج..ريد .. بوم.", + "You can throw bombs higher if you jump just before throwing.": "يمكنك رمي القنابل أعلى إذا كنت القفز قبل رمي.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "كنت تأخذ الضرر عندما كنت اجتز رأسك على الأشياء،\nوذلك في محاولة لا اجتز رأسك على الأشياء.", + "Your punches do much more damage if you are running or spinning.": "لكماتك تؤثر بضرر اكبر اذا كنت تجري او تدور" + } + }, + "trophiesRequiredText": "جوائز ${NUMBER} هذا يحتاج على الاقل", + "trophiesText": "الجوائز", + "trophiesThisSeasonText": "جوائز هذا الموسم", + "tutorial": { + "cpuBenchmarkText": "(CPU تشغيل البرنامج التعليمي بالسرعة المثيرة للسخرية (رئيسيا لتجريب سرعة", + "phrase01Text": "!مرحبا", + "phrase02Text": "!${APP_NAME} أهلا بك في", + "phrase03Text": "إليك بعض النصائح للتحكم بشخصيتك", + "phrase04Text": "درست فرقة القنبلة كثيرا من الاشياء فيزيائيا ${APP_NAME}**", + "phrase05Text": "...,على سبيل المثال, عندما تلكم", + "phrase06Text": "الضرر مبني على سرعة لكماتك", + "phrase07Text": "${NAME} كما ترى, نحن لا نتحرك لهذا بالكاد نصيب", + "phrase08Text": "الآن قم بالقفز والدوران لكسب زخم اكثر", + "phrase09Text": "!آه هذا أفضل", + "phrase10Text": "الجري يساعد ايضا", + "phrase11Text": "استمر في الضغط على اي زر للجري", + "phrase12Text": "للحصول على لكمة رائعة اضافية, جرب الجري و الدوران", + "phrase13Text": "${NAME} أوبس؛ اعتذر بشأن هذا يا يا صاح", + "phrase14Text": "${NAME} يمكنك امساك ورمي الاشياء مثل الاعلام .. أو", + "phrase15Text": "أخيرا, هناك القنابل", + "phrase16Text": "رمي القنابل يحتاج للتدريب", + "phrase17Text": "اوو! لم تكن رمية جيدة", + "phrase18Text": "التحرك يساعدك على الرمي ابعد", + "phrase19Text": "القفز يساعدك على الرمي اعلى", + "phrase20Text": "فجر\"قنابلك لمدى حتى أبعد من ذلك\"", + "phrase21Text": "توقيت القنبلة يمكن أن يكون مخادعا", + "phrase22Text": "!بووم", + "phrase23Text": "جرب امساك القنبلة ليذوب الفتيل لثانية او اثنتين", + "phrase24Text": "مرحى! لقد تم شواؤه بشكل رائع", + "phrase25Text": "حسنا, هذا كل ما في الامر", + "phrase26Text": "!والآن؛ اذهب ونل منهم يا نمر", + "phrase27Text": "إن تذكرت تدريبك.. فستعود حيا", + "phrase28Text": "...حسنا, ربما...", + "phrase29Text": "!حظا سعيدا", + "randomName1Text": "فريد", + "randomName2Text": "هاني", + "randomName3Text": "باسل", + "randomName4Text": "كيفن", + "randomName5Text": "جوني", + "skipConfirmText": "حقا تريد تخطي البرنامج التعليمي؟ إلمس أو اضغط بإستمرار", + "skipVoteCountText": "تخطي اصوات ${COUNT}/${TOTAL}", + "skippingText": "جاري تخطي البرنامج التعليمي....", + "toSkipPressAnythingText": "(إلمس او إضغظ اي شئ لتخطي البرنامج التعليمي)" + }, + "twoKillText": "!قتل مزدوج", + "unavailableText": "غير متوفر", + "unconfiguredControllerDetectedText": ":تم الكشف على يد تحكم غير مهيئة", + "unlockThisInTheStoreText": "هذا يجب ان يفتح في المتجر", + "unlockThisProfilesText": "لإنشاء أكثر من ${NUM} من الملفات الشخصية، تحتاج إلى:", + "unlockThisText": ":لفتح هذا, تحتاج ل", + "unsupportedHardwareText": "عفوا, هذه المعدات غير مدعومة في هذه النسخة من اللعبة", + "upFirstText": ":يصل اولا", + "upNextText": ":${COUNT}التالي في اللعبة", + "updatingAccountText": "تحديث الحساب الخاص بك....", + "upgradeText": "احصل على ترقية", + "upgradeToPlayText": "في متجر اللعبة لتلعب هذا \"${PRO}\" اشتري", + "useDefaultText": "استخدام الإفتراضي", + "usesExternalControllerText": "هذه اللعبة تستخدم يد تحكم خارجية للإدخال", + "usingItunesText": "...استخدام تطبيق الموسيقى للموسيقى التصويرية", + "validatingTestBuildText": "التحقق من صحة البناء", + "victoryText": "!النصر", + "voteDelayText": "ثانية ${NUMBER} لا يمكنك التصويت ثانية حتى", + "voteInProgressText": "التصويت بالفعل في التقدم", + "votedAlreadyText": "لقد صوت بالفعل", + "votesNeededText": "صوت مطلوب ${NUMBER}", + "vsText": "ضد", + "waitingForHostText": "(للإستمرار ${HOST} انتظار)", + "waitingForPlayersText": "...انتظار اللاعيبين للإنضمام", + "waitingInLineText": "الانتظار في السطر (الطرف ممتلئ) ...", + "watchAVideoText": "شاهد فيديو", + "watchAnAdText": "شاهد اعلان", + "watchWindow": { + "deleteConfirmText": "\"${REPLAY}\"? حذف", + "deleteReplayButtonText": "حذف\nالإعادة", + "myReplaysText": "إعاداتي", + "noReplaySelectedErrorText": "لايوجد إعادة تم اختيارها", + "playbackSpeedText": "سرعة الاعادة: ${SPEED}", + "renameReplayButtonText": "إعادة تسمبة \n الإعادة", + "renameReplayText": ":الى \"${REPLAY}\" اعادة تسمية", + "renameText": "إعادة تسمية", + "replayDeleteErrorText": "خطا في حذف الإعادة", + "replayNameText": "اسم الإعادة", + "replayRenameErrorAlreadyExistsText": "اسم الإعادة موجود بالفعل", + "replayRenameErrorInvalidName": "لايمكن اعادة تسمية الإعادة; الاسم غير صالح", + "replayRenameErrorText": "خطا في اعادة تسمية الإعادة", + "sharedReplaysText": "الإعادة المشاركة", + "titleText": "شاهد", + "watchReplayButtonText": "شاهد\nالإعادة" + }, + "waveText": "موجة", + "wellSureText": "!حسنا طبعا", + "wiimoteLicenseWindow": { + "titleText": "DarwinRemoteحقوق التأليف والنشر ل" + }, + "wiimoteListenWindow": { + "listeningText": "الاستماع ل وييموتس ...", + "pressText": "رقم 1 و 2 معا Wiimote اضغط زري", + "pressText2": "الاحمر في الخلف بدلا من ذلك'sync' فيه,اضغط زر Motion Plus الاجهزة الجديدة مع خاصية Wiimotes في اجهزة" + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote حق نشر", + "listenText": "استمع", + "macInstructionsText": "الخاص بك مطفئا والبلوتوث مفعلا في الماك الخاص بك Wii تأكد ان\nقد لا يفعل من اول مرة Wiimote support 'Listen' ثم اضغظ \nقد يكون عليك التجريب عدة مرات قبل ان تحصل على اتصال\n\n\nيجب على البلوتوث ان يتعامل الى مايصل الى 7 اجهزة متصلة\nرغم ان المسافة قد تختلف\n\nالاصلية Wiimotes اللعبة تدعم اجهزة\nوالجهاز الافتراضي,Nunchuks,\nيعمل ايضا Wii Remote Plus جهاز \nولكن ليس مع المرفقات", + "thanksText": "DarwiinRemote شكرا لفريق\nلجعل هذا ممكنا", + "titleText": "wiimote تثبيت" + }, + "winsPlayerText": "${NAME} !يفوز", + "winsTeamText": "${NAME} !يفوز فريق", + "winsText": "${NAME} !يفوز", + "worldScoresUnavailableText": "(النتيجة العالمية غير متوفرة (اتصل بالانترنت", + "worldsBestScoresText": "افضل نتيجة للعالم", + "worldsBestTimesText": "افضل اوقات العالم", + "xbox360ControllersWindow": { + "getDriverText": "احصل على تعريف", + "macInstructions2Text": "لاستخدام وحدات تحكم لاسلكيا، سوف تحتاج أيضا المتلقي ذلك\nيأتي مع \"تحكم اكس بوكس 360 اللاسلكية ويندوز\".\nواحد المتلقي يسمح لك لربط ما يصل إلى 4 وحدات تحكم.\n\nهام: لن تعمل أجهزة الاستقبال التابعة لجهة خارجية مع برنامج التشغيل هذا؛\nتأكد من جهاز الاستقبال يقول 'مايكروسوفت' على ذلك، وليس 'الاكس بوكس 360'.\nمايكروسوفت لم تعد تبيع هذه بشكل منفصل، لذلك سوف تحتاج إلى الحصول عليها\nواحد المجمعة مع وحدة تحكم أو البحث إيباي آخر.\n\nإذا وجدت هذا مفيد، يرجى النظر في التبرع ل\nمطور برامج في موقعه.", + "macInstructionsText": "لاستخدام وحدات تحكم الاكس بوكس 360، ستحتاج إلى تثبيت\nبرنامج تشغيل ماك متوفر على الرابط أدناه.\nوهو يعمل مع كل من وحدات تحكم السلكية واللاسلكية.", + "ouyaInstructionsText": "سلكية مع اللعبة Xbox 360 لإستخدام يد تحكم \nببساطة وصلها بمنفذ يو إس بي للجهاز. يمكنك استخدام محور يو إس بي \nلتوصيل عدة اجهزة\n\n\"Xbox 360 wireless Controller for Windows\"لتوصيل يد تحكم لاسلكية تحتاج لمتسقبل لاسلكي خاص\n, متوفرة في حزمة\nاو يمكنك شراءها منفردة.كل مستقبل يمكنك \nمن خلاله توصيل 4 ايدي تحكم لاسلكية", + "titleText": "استخدام يد تحكم Xbox 360 مع ${APP_NAME}:" + }, + "yesAllowText": "!نعم,اسمح", + "yourBestScoresText": "أفضل نقاطك", + "yourBestTimesText": "أفضل أوقاتك" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/belarussian.json b/dist/ba_data/data/languages/belarussian.json new file mode 100644 index 0000000..77b7b2b --- /dev/null +++ b/dist/ba_data/data/languages/belarussian.json @@ -0,0 +1,1840 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Імя акаўнта не можа ўтрымліваць эмоджы і іншыя спецыяльныя сымбалі", + "accountsText": "Акаўнты", + "achievementProgressText": "Дасягненні: ${COUNT} з ${TOTAL}", + "campaignProgressText": "Прагрэс кампаніі (Цяжка): ${PROGRESS}", + "changeOncePerSeason": "Вы можаце змяніць толькі адзін раз за сезон.", + "changeOncePerSeasonError": "Трэба пачакаць наступнага сезона, каб зноў змяніць гэта (${NUM} days)", + "customName": "Зрабіць імя", + "linkAccountsEnterCodeText": "Увесцi Код", + "linkAccountsGenerateCodeText": "Стварыць Код", + "linkAccountsInfoText": "(сінхранізацыя гульні паміж рознымі платформамі)", + "linkAccountsInstructionsNewText": "Для таго, каб звязаць два акаўнта, згенерыруйце код на першым\nі ўведзіце гэты код на другім. Прагрэс з\nдругога акаўнта будзе размяркоўвацца паміж абодвума.\n(Прагрэс з першага акаўнта будзе страчаны)\n\nВы можаце падключыць да ${COUNT} акаўнтаў.\n\nУВАГА: звязвайце толькі тыя акаўнты, якімі вы валодаеце;\nКалі вы звязваеце іх з акаўнтамі сяброў, вы не будзеце\nмець магчымасць гуляць онлайн ў той жа самы час.", + "linkAccountsInstructionsText": "Каб звязаць 2 акаўнта, стварыце код на першым\nз іх і увядзіце яго ў другім.\nПрагрэс і пакупкі будуць сінранізаваны.\nВы можаце звязаць да ${COUNT} акаўнтаў.\n\nАсцярожна! Гэта нельга адмяніць!", + "linkAccountsText": "Звязаць Акаўнты", + "linkedAccountsText": "Злучаныя Акаўнты:", + "nameChangeConfirm": "Змяніць імя акаўнта на ${NAME} ?", + "resetProgressConfirmNoAchievementsText": "Гэта скіне ўвесь ваш кааператыўны прагрэс\nды лакальныя лепшыя вынікі (акрамя білетаў).\nГэты працэс незваротны. Вы ўпэўнены?", + "resetProgressConfirmText": "Гэта скіне ўвесь ваш кааператыўны\nпрагрэс, дасягненні ды лакальныя вынікі\n(акрамя білетаў). Гэты працэс незваротны.\nВы ўпэўнены?", + "resetProgressText": "Скінуць прагрэс", + "setAccountName": "Усталяваць імя акаўнта", + "setAccountNameDesc": "Выберыце імя для адлюстравання для вашага ўліковага запісу.\nВы можаце выкарыстоўваць імя ў адным з звязаных\nуліковых запісаў альбо стварыць унікальнае прыстасаванае імя.", + "signInInfoText": "Увайдзіце, каб збіраць квіткі, удзельнічаць у спаборніцтвах \nі сінхранізіраваць прагрэс паміж рознымі прыладамі.", + "signInText": "Увайсці", + "signInWithDeviceInfoText": "(толькі аўтаматычны акаўнт даступны для гэтай прылады)", + "signInWithDeviceText": "Увайсці з акаўнта прылады", + "signInWithGameCircleText": "Увайсці з дапамогаю Game Circle", + "signInWithGooglePlayText": "Увайсці з дапамогаю Google Play", + "signInWithTestAccountInfoText": "(акаўнт, які ўжо існуе; спачатку увайдзіце з прылады)", + "signInWithTestAccountText": "Увайсці з тэст-акаўнта", + "signOutText": "Выйсці", + "signingInText": "Уваход...", + "signingOutText": "Выхад...", + "testAccountWarningCardboardText": "Увага: вы ўваходзіце з \"тэст\"-акаунта.\nГэтыя дадзеныя будуць заменены акаунтамі Google, як толькі \nяны стануць падтрымлівацца ў cardboard прыладах.\n\nЗараз вам прыйдзецца зарабляць усе білеты толькі на гэтай прыладзе.\n(негледзячы на гэта, вы атрымаеце Pro-версію BombSquade бясплатна)", + "testAccountWarningOculusText": "Увага: вы ўваходзіце з \"тэст\"-акаунта.\nЁн будзе заменены акаунтамі Oculus пазней у гэтым годзе, \nякія будуць прапаноўваць куплю білетаў і іншыя магчымасці.\n\nЗараз вам прыйдзецца зарабляць усе білеты толькі на гэтай прыладзе.\n(негледзячы на гэта, вы атрымаеце Pro-версію BombSquade бясплатна)", + "testAccountWarningText": "Увага: вы ўваходзіце з \"тэст\"-акаунта.\nГэты акаунт захаваны толькі на гэтай прыладзе і\nможа часам выдаляцца. (таму калі ласка не марнуйце\nмнога часу, збіраючы білеты)\n\nКаб выкарыстоўваць сапраўдны акаунт (Game-Center, Google+\nі іншыя) запусціце платную версію гульні. Гэта таксама\nдазволіць вам захоўваць свой прагрэс ў воблацы і\nгуляць з ім на розных прыладах. ", + "ticketsText": "Квіткі: ${COUNT}", + "titleText": "Акаўнт", + "unlinkAccountsInstructionsText": "Выберыце ўліковы запіс, каб спыніць сувязь", + "unlinkAccountsText": "Адлучэнне акаунтау", + "viaAccount": "(праз акаунт ${NAME})", + "youAreSignedInAsText": "Вы ўвайшлі як:" + }, + "achievementChallengesText": "Дасягненні", + "achievementText": "Дасягненне", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Забейце 3 злодзеяў з дапамогаю TNT", + "descriptionComplete": "З дапамогаю TNT забіты 3 злодзея", + "descriptionFull": "Забейце 3 злодзеяў з дапамогаю TNT на ўзроўні ${LEVEL}", + "descriptionFullComplete": "На ўзроўні ${LEVEL} з дапамогаю TNT забіты 3 злодзея", + "name": "Зараз падарвецца!" + }, + "Boxer": { + "description": "Перамажыце, не выкарыстоўвываючы бомбы", + "descriptionComplete": "Перамаглі, не выкарыстоўвываючы бомбы", + "descriptionFull": "Прайдзіце ${LEVEL}, не выкарыстоўвываючы бомбы", + "descriptionFullComplete": "Прайшлі ${LEVEL}, не выкарыстоўвываючы бомбы", + "name": "Баксёр" + }, + "Dual Wielding": { + "descriptionFull": "Падлучыце кантролера", + "descriptionFullComplete": "Падлучаны 2 кантролера", + "name": "Падваенне Зброі" + }, + "Flawless Victory": { + "description": "Перамажыце, не атрымліваючы пашкоджанні", + "descriptionComplete": "Перамаглі, не атрымліваючы пашкоджанні", + "descriptionFull": "Перамажыце ${LEVEL}, не атрымліваючы пашкоджанні", + "descriptionFullComplete": "Перамаглі ${LEVEL}, не атрымліваючы пашкоджанні", + "name": "Бездакорная перамога" + }, + "Free Loader": { + "descriptionFull": "Пачніце гульню з 2 і больш гульцамі", + "descriptionFullComplete": "Пачата гульня з 2 і больш гульцамі", + "name": "Свабодны Гулец" + }, + "Gold Miner": { + "description": "Забейце 6 злодзеяў з дапамогаю мін", + "descriptionComplete": "З дапамогаю мін забіты 6 злодзеяў", + "descriptionFull": "Забейце 6 злодзеяў з дапамогаю мін на узроўні ${LEVEL}", + "descriptionFullComplete": "На узроўні ${LEVEL} з дапамогаю мін забіты 6 злодзеяў", + "name": "Залаты мінёр" + }, + "Got the Moves": { + "description": "Перамажыце, не выкарыстоўваючы ўдары ці бомбы", + "descriptionComplete": "Перамаглі, не выкарыстоўваючы ўдары ці бомбы", + "descriptionFull": "Перамажыце на ўзроўні ${LEVEL}, не выкарыстоўваючы ўдары ці бомбы", + "descriptionFullComplete": "Перамаглі ўзровень ${LEVEL}, не выкарыстоўваючы ўдары ці бомбы", + "name": "Дакладныя Дзеянні" + }, + "In Control": { + "descriptionFull": "Падлучыце кантролер", + "descriptionFullComplete": "Падлучаны кантролер.", + "name": "Усё пад Кантролем" + }, + "Last Stand God": { + "description": "Набярыце 1000 ачкоў", + "descriptionComplete": "Набралі 1000 ачкоў", + "descriptionFull": "Набярыце 1000 ачкоў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Набралі 1000 ачкоў на ўзроўні ${LEVEL}", + "name": "Бог узроўня ${LEVEL}" + }, + "Last Stand Master": { + "description": "Набярыце 250 ачкоў", + "descriptionComplete": "Набралі 250 ачкоў", + "descriptionFull": "Набярыце 250 ачкоў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Набралі 250 ачкоў на ўзроўні ${LEVEL}", + "name": "Майстар узроўня ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Набярыце 500 ачкоў", + "descriptionComplete": "Набралі 500 ачкоў", + "descriptionFull": "Набярыце 500 ачкоў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Набралі 500 ачкоў на ўзроўні ${LEVEL}", + "name": "Чараўнік узроўня ${LEVEL}" + }, + "Mine Games": { + "description": "Забейце 3 злодзеяў мінамі", + "descriptionComplete": "Забілі 3 злодзеяў мінамі", + "descriptionFull": "Забейце 3 злодзеяў мінамі на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Забілі 3 злодзеяў мінамі на ўзроўні ${LEVEL}", + "name": "Гульні З Мінамі" + }, + "Off You Go Then": { + "description": "Скіньце 3 злодзеяў з мапы", + "descriptionComplete": "Скінулі 3 злодзеяў з мапы", + "descriptionFull": "Скіньце 3 злодзеяў з мапы на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Скінулі 3 злодзеяў з мапы на ўзроўні ${LEVEL}", + "name": "Давай Адсюль" + }, + "Onslaught God": { + "description": "Набярыце 5000 ачкоў", + "descriptionComplete": "Набралі 5000 ачкоў", + "descriptionFull": "Набярыце 5000 ачкоў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Набралі 5000 ачкоў на ўзроўні ${LEVEL}", + "name": "Бог узроўня ${LEVEL}" + }, + "Onslaught Master": { + "description": "Набярыце 500 ачкоў", + "descriptionComplete": "Набралі 500 ачкоў", + "descriptionFull": "Набярыце 500 ачкоў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Набралі 500 ачкоў на ўзроўні ${LEVEL}", + "name": "Майстар узроўня ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Перамажыце ўсе хвалі", + "descriptionComplete": "Перамаглі ўсе хвалі", + "descriptionFull": "Перамажыце ўсе хвалі на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Перамаглі ўсе хвалі на ўзроўні ${LEVEL}", + "name": "Перамога на ўзроўні ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Набярыце 1000 ачкоў", + "descriptionComplete": "Набралі 1000 ачкоў", + "descriptionFull": "Набярыце 1000 ачкоў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Набралі 1000 ачкоў на ўзроўні ${LEVEL}", + "name": "Чараўнік узроўня ${LEVEL}" + }, + "Precision Bombing": { + "description": "Перамажыце без узмацняльнікаў", + "descriptionComplete": "Перамаглі без узмацняльнікаў", + "descriptionFull": "Перамажыце ўзровень ${LEVEL} без узмацняльнікаў", + "descriptionFullComplete": "Перамаглі ўзровень ${LEVEL} без узмацняльнікаў", + "name": "Прыцэльнае Бомбакіданне" + }, + "Pro Boxer": { + "description": "Перамажыце без бомб", + "descriptionComplete": "Перамаглі без бомб", + "descriptionFull": "Перамажыце ўзровень ${LEVEL} без бомб", + "descriptionFullComplete": "Перамаглі ўзровень ${LEVEL} без бомб", + "name": "Баксёр Профі" + }, + "Pro Football Shutout": { + "description": "Перамажыце, не даўшы злодзеям набраць ачкі", + "descriptionComplete": "Перамаглі, не даўшы злодзеям набраць ачкі", + "descriptionFull": "Перамажыце на ўзроўні ${LEVEL}, не даўшы злодзеям набраць ачкі", + "descriptionFullComplete": "Перамаглі на ўзроўні ${LEVEL}, не даўшы злодзеям набраць ачкі", + "name": "Усухую на ўзроўні ${LEVEL}" + }, + "Pro Football Victory": { + "description": "Перамажыце гульню", + "descriptionComplete": "Перамаглі гульню", + "descriptionFull": "Перамажыце гульню на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Перамаглі гульню на ўзроўні ${LEVEL}", + "name": "Перамога на ўзроўні ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Перамажыце ўсе хвалі", + "descriptionComplete": "Перамаглі ўсе хвалі", + "descriptionFull": "Перамажыце ўсе хвалі на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Перамаглі ўсе хвалі на ўзроўні ${LEVEL}", + "name": "Перамога на ўзроўні ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Перамажыце ўсе хвалі", + "descriptionComplete": "Перамаглі ўсе хвалі", + "descriptionFull": "Перамажыце ўсе хвалі на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Перамаглі ўсе хвалі на ўзроўні ${LEVEL}", + "name": "Перамога на узроўні ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Перамажыце, не даўшы злодзеям набраць ачкі", + "descriptionComplete": "Перамаглі, не даўшы злодзеям набраць ачкі", + "descriptionFull": "Перамажыце на ўзроўні ${LEVEL}, не даўшы злодзеям набраць ачкі", + "descriptionFullComplete": "Перамаглі на ўзроўні ${LEVEL}, не даўшы злодзеям набраць ачкі", + "name": "Усухую на узроўні ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Перамажыце матч", + "descriptionComplete": "Перамаглі матч", + "descriptionFull": "Перамажыце матч на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Перамаглі матч на ўзроўні ${LEVEL}", + "name": "Перамога на ўзроўні ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Перамажыце ўсе хвалі", + "descriptionComplete": "Перамаглі ўсе хвалі", + "descriptionFull": "Перамажыце ўсе хвалі на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Перамажыце ўсе хвалі на ўзроўні ${LEVEL}", + "name": "Перамога на ўзроўні ${LEVEL}" + }, + "Runaround God": { + "description": "Набярыце 2000 ачкоў", + "descriptionComplete": "Набралі 2000 ачкоў", + "descriptionFull": "Набярыце 2000 ачкоў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Набралі 2000 ачкоў на ўзроўні ${LEVEL}", + "name": "Бог узроўня ${LEVEL}" + }, + "Runaround Master": { + "description": "Набярыце 500 ачкоў", + "descriptionComplete": "Набралі 500 ачкоў", + "descriptionFull": "Набярыце 500 ачкоў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Набралі 500 ачкоў на ўзроўні ${LEVEL}", + "name": "Майстар узроўня ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Набярыце 1000 ачкоў", + "descriptionComplete": "Набралі 1000 ачкоў", + "descriptionFull": "Набярыце 1000 ачкоў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Набралі 1000 ачкоў на ўзроўні ${LEVEL}", + "name": "Чараўнік узроўня ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Падзяліцеся гульнёй з сябрам", + "descriptionFullComplete": "Падзяліліся гульнёй з сябрам", + "name": "Клопат аб Сябры" + }, + "Stayin' Alive": { + "description": "Перамажыце, не памірая", + "descriptionComplete": "Перамаглі, не памірая", + "descriptionFull": "Перамажыце на ўзроўні ${LEVEL}, не памірая", + "descriptionFullComplete": "Перамаглі на ўзроўні ${LEVEL}, не памірая", + "name": "Застацца Жывым" + }, + "Super Mega Punch": { + "description": "Забейце адным ударам", + "descriptionComplete": "Забілі адным ударам", + "descriptionFull": "Забейце адным ударам на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Забілі адным ударам на ўзроўні ${LEVEL}", + "name": "Супер Мега Ўдар" + }, + "Super Punch": { + "description": "Зніміце палову здароўя адным ударам", + "descriptionComplete": "Знялі палову здароўя адным ударам", + "descriptionFull": "Зніміце палову здароўя адным ударам на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Знялі палову здароўя адным ударам на ўзроўні ${LEVEL}", + "name": "Супер Удар" + }, + "TNT Terror": { + "description": "Забейце 6 злодзеяў з дапамогаю TNT", + "descriptionComplete": "Забілі 6 злодзеяў з дапамогаю TNT", + "descriptionFull": "Забейце 6 злодзеяў з дапамогаю TNT на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Забілі 6 злодзеяў з дапамогаю TNT на ўзроўні ${LEVEL}", + "name": "TNT Вар'яцтва" + }, + "Team Player": { + "descriptionFull": "Пачніце гульню з 4 і больш гульцамі", + "descriptionFullComplete": "Пачалі гульню з 4 і больш гульцамі", + "name": "Камандны Гулец" + }, + "The Great Wall": { + "description": "Спыніце ўсіх злодзеяў", + "descriptionComplete": "Спынілі ўсіх злодзеяў", + "descriptionFull": "Спыніце ўсіх злодзеяў на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Спынілі ўсіх злодзеяў на ўзроўні ${LEVEL}", + "name": "Магутная Сцяна" + }, + "The Wall": { + "description": "Спыніць кожнага злодзея", + "descriptionComplete": "Спынілі кожнага злодзея", + "descriptionFull": "Спыніць кожнага злодзея на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Спыніць кожнага злодзея на ўзроўні ${LEVEL}", + "name": "Сцяна" + }, + "Uber Football Shutout": { + "description": "Перамажыце, не даўшы злодзеям набраць ачкі", + "descriptionComplete": "Перамаглі, не даўшы злодзеям набраць ачкі", + "descriptionFull": "Перамажыце на ўзроўні ${LEVEL}, не даўшы злодзеям набраць ачкі", + "descriptionFullComplete": "Перамаглі на ўзроўні ${LEVEL}, не даўшы злодзеям набраць ачкі", + "name": "Усухую на ўзроўні ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Перамажыце гульню", + "descriptionComplete": "Перамог гульню", + "descriptionFull": "Перамажыце гульню на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Перамаглі гульню на ўзроўні ${LEVEL}", + "name": "Перамога на ўзроўні ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Перамажыце ўсе хвалі", + "descriptionComplete": "Перамаглі ўсе хвалі", + "descriptionFull": "Перамажыце ўсе хвалі на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Перамаглі ўсе хвалі на ўзроўні ${LEVEL}", + "name": "Перамога на ўзроўні ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Перамажыце ўсе хвалі", + "descriptionComplete": "Перамаглі ўсе хвалі", + "descriptionFull": "Перамажыце ўсе хвалі на ўзроўні ${LEVEL}", + "descriptionFullComplete": "Перамаглі ўсе хвалі на ўзроўні ${LEVEL}", + "name": "Перамога на ўзроўні ${LEVEL}" + } + }, + "achievementsRemainingText": "Дасягенні, Якія Засталіся", + "achievementsText": "Дасягненні", + "achievementsUnavailableForOldSeasonsText": "Прабачце, спецыфіка дасягненняў недаступна для старога сезона.", + "addGameWindow": { + "getMoreGamesText": "Атрымаць больш гульняў...", + "titleText": "Дадаць гульню" + }, + "allowText": "Дазволіць", + "alreadySignedInText": "Ваш уліковы запіс увайшоў з іншай прылады;\nкалі ласка, пераключыце ўліковыя запісы альбо зачыніце гульню на вашым\nіншыя прылады і паспрабуйце яшчэ раз", + "apiVersionErrorText": "Нельга загрузіць модуль ${NAME}; ён прапануецца для api-версіі ${VERSION_USED}; мы карыстаемся ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Аўтаматычна\" працуе толькі калі ўстаўлены навушнікі)", + "headRelativeVRAudioText": "H-R VR Аўдыя", + "musicVolumeText": "Гучнасць Музыкі", + "soundVolumeText": "Гучнасць", + "soundtrackButtonText": "Саўндтрэкі", + "soundtrackDescriptionText": "(дадайце сваю музыку, каб яна гучала падчас гульняў)", + "titleText": "Аўдыя" + }, + "autoText": "Аўтаматычна", + "backText": "Вярнуцца", + "banThisPlayerText": "Забараніць гэтага гульца", + "bestOfFinalText": "Фінал: Лепшыя з ${COUNT}", + "bestOfSeriesText": "Лепшы за ${COUNT} серыяў:", + "bestRankText": "Ваш найлепшы - #${RANK}", + "bestRatingText": "Ваш найлепшы рэйтынг - ${RATING}", + "bombBoldText": "БОМБА", + "bombText": "Бомба", + "boostText": "Павышэнне", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} наладзіцца сам.", + "buttonText": "Кнопка", + "canWeDebugText": "Ці жадаеце вы аўтаматычна паведамляць аб багах\nі аварыях распрацоўніку?\n\nГэтыя паведамленні не ўтрымліваюць асабістую інфармацыю\nі дапамагаюць палепшыць гульню.", + "cancelText": "Зачыніць", + "cantConfigureDeviceText": "Прабачце, ${DEVICE} не падтрымліваецца.", + "challengeEndedText": "Спаборніцтва скончылася.", + "chatMuteText": "Адключэнне гуку ў чаце", + "chatMutedText": "Чат адключон", + "chatUnMuteText": "Уключыць гук у чаце", + "choosingPlayerText": "<выбар гульца>", + "completeThisLevelToProceedText": "Вы павінны прайсці гэты\nўзровень, каб працягнуць!", + "completionBonusText": "Бонус за праходжанне", + "configControllersWindow": { + "configureControllersText": "Налады кантролераў", + "configureKeyboard2Text": "Налады клавіятуры 2-ога гульца", + "configureKeyboardText": "Налады клавіятуры", + "configureMobileText": "Мабільныя Прылады Ў Якасці Кантролераў", + "configureTouchText": "Налады Экрана", + "ps3Text": "Кантролеры PS3", + "titleText": "Кантролеры", + "wiimotesText": "Wiimotes", + "xbox360Text": "Кантролеры Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Падтрымка кантролераў залежыць ад прылады і версіі Android", + "pressAnyButtonText": "Націсніце любую кнопку на кантролеры,\n які вы жадаеце наладзіць...", + "titleText": "Налады Кантролераў" + }, + "configGamepadWindow": { + "advancedText": "Дадатковыя", + "advancedTitleText": "Дадатковыя Налады Кантролераў", + "analogStickDeadZoneDescriptionText": "(павялічце, калі персанаж працягвае рухацца пасля таго, як вы адпусцілі стык)", + "analogStickDeadZoneText": "Мёртвая Зона Аналагавага Сціка", + "appliesToAllText": "(адносіцца да ўсіх кантролераў гэтага тыпу)", + "autoRecalibrateDescriptionText": "(уключыце гэта, калі персанаж не рухаецца на поўнай хуткасці)", + "autoRecalibrateText": "Аўта-Перакабліроўка Аналагавых Стыкаў", + "axisText": "Вось", + "clearText": "Ачысціць", + "dpadText": "D-Pad", + "extraStartButtonText": "Дадатковая кнопка \"Старт\"", + "ifNothingHappensTryAnalogText": "Калі нічога не адбываецца, паспрабуйце прызначыць аналагавы стык.", + "ifNothingHappensTryDpadText": "Калі нічога не адбываецца, паспрабуйце прызначыць D-Pad.", + "ignoreCompletelyDescriptionText": "(Прасачыце, каб гэты кантролер не ўплываў на гульню ці меню)", + "ignoreCompletelyText": "Ігнараваць Цалкам", + "ignoredButton1Text": "Непатрэбная Кнопка 1", + "ignoredButton2Text": "Непатрэбная Кнопка 2", + "ignoredButton3Text": "Непатрэбная Кнопка 3", + "ignoredButton4Text": "Ігнаруецца Кнопка 4", + "ignoredButtonDescriptionText": "(карыстайцеся гэтым, каб не дазволіць кнопкам 'home' ці 'sync' удзейнічаць на гульню)", + "pressAnyAnalogTriggerText": "Націсніце на любы аналагавы трыгер...", + "pressAnyButtonOrDpadText": "Націсніце любую кнопку ці D-Pad...", + "pressAnyButtonText": "Націсніце любую кнопку...", + "pressLeftRightText": "Націсніце Ўправа ці Ўлева...", + "pressUpDownText": "Націсніце Ўверх ці Ўніз...", + "runButton1Text": "Кнопка для Бегу 1", + "runButton2Text": "Кнопка для Бегу 2", + "runTrigger1Text": "Трыгер для Бегу 1", + "runTrigger2Text": "Трыгер для Бегу 2", + "runTriggerDescriptionText": "(аналагавыя трыгеры дазволяць бегаць з рознымі хуткасцямі)", + "secondHalfText": "Выкарыстоўвайце гэта для наладжвання\n2-ой часткі двайнога кантролера, які\nпадаецца, як адзін кантролер.", + "secondaryEnableText": "Уключыць", + "secondaryText": "Другі Кантролер", + "startButtonActivatesDefaultDescriptionText": "(выключыце гэта, калі ваша кнопка \"старт\" больш падобна на \"меню\")", + "startButtonActivatesDefaultText": "Кнопка Старт Актывізуе Стандартны Віджэт", + "titleText": "Налады Кантролера", + "twoInOneSetupText": "Налады Двайнога Кантролера", + "uiOnlyDescriptionText": "( прадухіліць гэты кантролер ад далучаючыся гульню )", + "uiOnlyText": "Ліміт на выкарыстання меню", + "unassignedButtonsRunText": "Усе Неразмеркаваныя Кнопкі - для Бегу", + "unsetText": "<не ўсталявана>", + "vrReorientButtonText": "Кнопка рэарыентавання ВР" + }, + "configKeyboardWindow": { + "configuringText": "Наладжванне ${DEVICE}", + "keyboard2NoteText": "Амаль усе клавіятуры могуць перадаць толькі некалькі \nнаціскаў; таму будзе лепш, калі гульцы будуць мець \nкожны сваю клавіятуру. Але звярніце ўвагу на тое, што\nўсё роўна гульцы павінны будуць карыстацца рознымі \nкнопкамі для кіравання персанажам." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Памеры Кнопак Дзеянняў", + "actionsText": "Дзеянні", + "buttonsText": "Кнопкі", + "dragControlsText": "< каб перанесці элементы кіравання, перацягніце іх >", + "joystickText": "Джойсцік", + "movementControlScaleText": "Памеры Кнопак Руху", + "movementText": "Рух", + "resetText": "Скінуць", + "swipeControlsHiddenText": "Схаваць Іконкі Змахвання", + "swipeInfoText": "Да кантролераў, працуючых са змахваннямі, неабходна \nпрывыкнуць, але з імі будзе лягчэй гуляць.", + "swipeText": "змахванне", + "titleText": "Налады Экрана" + }, + "configureItNowText": "Наладзіць гэта зараз?", + "configureText": "Налады", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "Для лепшых вынікаў вам спатрэбіцца добрая WiFi сетка. Вы можаце\nпалепшыць хуткасць WiFi, калі выключыце вашыя іншыя WiFi прылады,\nбудзеце гуляць каля WiFi-роўтэра ці калі вы падлучыцеся непасрэдна\nда інтэрнэт-сеткі.", + "explanationText": "Для ўжывання смартфона ці планшэта ў якасці кантролера\nўсталюйце прыкладанне \"${REMOTE_APP_NAME}\". Любая колькасць прылад \nможа далучыцца да гульні ${APP_NAME} праз Wi-Fi бясплатна!", + "forAndroidText": "для Android:", + "forIOSText": "для iOS:", + "getItForText": "Атрымайце ${REMOTE_APP_NAME} для iOS у Apple App Store\nці для Android у Google Play Store ці Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Выкарыстоўванне Мабільных Прылад як Кантролераў" + }, + "continuePurchaseText": "Працягнуць за ${PRICE}?", + "continueText": "Далей", + "controlsText": "Кнопкі", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Гэта не адносіцца да абсалютных рэкордаў.", + "activenessInfoText": "Гэты множнік павышаецца калі вы гуляеце\nі паніжаецца калі вы не гуляеце.", + "activityText": "Актыўнасць", + "campaignText": "Кампанія", + "challengesInfoText": "Зарабляйце прызы для завяршэння міні-гульняў.\n\nПрызы і ўзроўні складанасці павялічваюцца\nкожны раз, калі спаборніцтва завяршаецца, і\nзмяншаюцца, калі заканчваецца ці абнуляецца час.", + "challengesText": "Спаборніцтвы", + "currentBestText": "Зараз лепшы", + "customText": "Іншае", + "entryFeeText": "Удзельніцтва", + "forfeitConfirmText": "Прапусціць гэтае спаборніцтва?", + "forfeitNotAllowedYetText": "Пакуль нельга скончыць гэтае спаборніцтва.", + "forfeitText": "Прапусціць", + "multipliersText": "Множнікі", + "nextChallengeText": "Наступнае Спаборніцтва", + "nextPlayText": "Наступная Гульня", + "ofTotalTimeText": "з ${TOTAL}", + "playNowText": "Гуляць Зараз", + "pointsText": "Ачкі", + "powerRankingFinishedSeasonUnrankedText": "(сезон скончан не ў лізе)", + "powerRankingNotInTopText": "(не ў ${NUMBER} лепшых)", + "powerRankingPointsEqualsText": "= ${NUMBER} ачкоў", + "powerRankingPointsMultText": "(x ${NUMBER} ачкоў)", + "powerRankingPointsText": "${NUMBER} ачкоў", + "powerRankingPointsToRankedText": "(${CURRENT} of ${REMAINING} ачкоў)", + "powerRankingText": "Узровень Гульца", + "prizesText": "Прызы", + "proMultInfoText": "Гульцы з версіяй ${PRO}\nатрымліваюць ${PERCENT}% множнік тут.", + "seeMoreText": "Больш...", + "skipWaitText": "Прапусціць Чаканне", + "timeRemainingText": "Засталося Часу", + "toRankedText": "Атрымана", + "totalText": "за ўсё", + "tournamentInfoText": "Спаборнічайце з іншымі \nгульцамі вашай лігі.\n\nКалі турнір заканчваецца, гульцы з\nлепшымі вынікамі атрымліваюць прызы.", + "welcome1Text": "Рады бачыць вас у лізе ${LEAGUE}. Вы можаце палепшыць\nваш узровень лігі, калі будзеце зарабляць рэйтынг,\nвыконваць дасягненні і атрымліваць трафеі.", + "welcome2Text": "Вы таксама можаце зарабляць білеты многімі з такіх жа\nзаняткаў. Білеты дазваляюць адкрываць новых персанажаў, \nмапы, міні-гульні, удзельнічаць у турнірах і іншае.", + "yourPowerRankingText": "Ваш Узровень:" + }, + "copyOfText": "Копія ${NAME}", + "createEditPlayerText": "<Стварыць/Змяніць Гульца>", + "createText": "Стварыць", + "creditsWindow": { + "additionalAudioArtIdeasText": "Дадатковыя Саўндтрэкі, Малюнкі і Ідэі - ${NAME}", + "additionalMusicFromText": "Дадатковыя саўндтрэкі з ${NAME}", + "allMyFamilyText": "Усім маім сябрам і сям'і, якія дапамагалі мне тэсціраваць гульню", + "codingGraphicsAudioText": "Праграміраванне, Графіка і Аўдыя - ${NAME}", + "languageTranslationsText": "Пераклады на іншыя мовы:", + "legalText": "Юрыдычная інфармацыя:", + "publicDomainMusicViaText": "Агульнадаступная музыка праз ${NAME}", + "softwareBasedOnText": "Гэтае праграмнае забеспячэнне заснована на працы ${NAME}", + "songCreditText": "${TITLE} выконвае ${PERFORMER}\nКампазітар - ${COMPOSER}, Аранжыроўка - ${ARRANGER}, Выданне - ${PUBLISHER},\nПрадастаўлена - ${SOURCE}", + "soundAndMusicText": "Гук і Музыка:", + "soundsText": "Гукі (${SOURCE}):", + "specialThanksText": "Асаблівая Падзяка:", + "thanksEspeciallyToText": "Асобная падзяка - ${NAME}", + "titleText": "Падзякі ад ${APP_NAME}", + "whoeverInventedCoffeeText": "Тым, хто вынайшаў каву" + }, + "currentStandingText": "Вашае бягучае месца - #${RANK}", + "customizeText": "Наладзіць...", + "deathsTallyText": "${COUNT} смерцяў", + "deathsText": "Смерці", + "debugText": "Адладка", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Рэкамендуецца ўсталяваць найвышэйшы ўзровень графікі (гл. Налады -> Графіка -> Тэкстуры).", + "runCPUBenchmarkText": "Запусціць Тэст Прадукцыйнасці CPU", + "runGPUBenchmarkText": "Запусціць Тэст Прадукцыйнасці GPU", + "runMediaReloadBenchmarkText": "Запусціць Тэст Прадукцыйнасці Медыя", + "runStressTestText": "Запусціць тэст-нагрузку", + "stressTestPlayerCountText": "Колькасць Гульцоў", + "stressTestPlaylistDescriptionText": "Плэйліст Тэста-Нагрузкі", + "stressTestPlaylistNameText": "Імя Плэйліста", + "stressTestPlaylistTypeText": "Тып Плэйліста", + "stressTestRoundDurationText": "Працягласць раўнда", + "stressTestTitleText": "Тэст-Нагрузка", + "titleText": "Тэсты Прадукцыйнасці і Нагрузкі", + "totalReloadTimeText": "Час загрузкі медыя: ${TIME} (падрабязнасці ў лозе)" + }, + "defaultGameListNameText": "Стандартны ${PLAYMODE} Плэйліст", + "defaultNewGameListNameText": "Мой \"${PLAYMODE}\" Плэйліст", + "deleteText": "Выдаліць", + "demoText": "Дэманстрацыя", + "denyText": "Адхіліць", + "desktopResText": "Дазвол Экрана", + "difficultyEasyText": "Лёгка", + "difficultyHardOnlyText": "Толькі на Складаным Узроўні", + "difficultyHardText": "Цяжка", + "difficultyHardUnlockOnlyText": "Гэты ўзровень можа быць адкрыт толькі на складаным\nузроўні. Вы гэтага не зрабілі!", + "directBrowserToURLText": "Калі ласка, накіруйце вэб-браўзер па наступным адрасе:", + "disableRemoteAppConnectionsText": "Адключыць злучэнні з аддаленым дадаткам", + "disableXInputDescriptionText": "Дазваляе больш за 4 кантролераў, але можа таксама не працаваць.", + "disableXInputText": "Адключыць XInput", + "doneText": "Зроблена", + "drawText": "Нічыя", + "duplicateText": "Дублікат", + "editGameListWindow": { + "addGameText": "Дадаць\nГульню", + "cantOverwriteDefaultText": "Нельга перазапісаць стандартны плэйліст!", + "cantSaveAlreadyExistsText": "Плэйліст з такім імем ужо існуе!", + "cantSaveEmptyListText": "Нельга захаваць пусты плэйліст!", + "editGameText": "Змяніць\nГульню", + "listNameText": "Назва плэйліста", + "nameText": "Назва", + "removeGameText": "Выдаліць\nГульню", + "saveText": "Захаваць Спіс", + "titleText": "Рэдактар плэйліста" + }, + "editProfileWindow": { + "accountProfileInfoText": "Гэта профіль, які заснаваны\nна вашым акаўнце.\n\n${ICONS}\n\nСтварайце іншыя профілі, калі вы жадаеце \nкарыстацца рознымі імёнамі і аватарамі.", + "accountProfileText": "(профіль акаўнта)", + "availableText": "Імя \"${NAME}\" даступна.", + "changesNotAffectText": "Увага: змены не паўплываюць на профілі, якія ўжо ў гульні", + "characterText": "персанаж", + "checkingAvailabilityText": "Праверка даступнасці \"${NAME}\"...", + "colorText": "колер", + "getMoreCharactersText": "Атрымаць больш герояў...", + "getMoreIconsText": "Атрымаць больш аватараў...", + "globalProfileInfoText": "Кожны глабальны профіль мае асабістае імя.\nЁн таксама ўтрымлівае свой аватар.", + "globalProfileText": "(глабальны профіль)", + "highlightText": "адценне", + "iconText": "аватар", + "localProfileInfoText": "Лакальныя профілі не маюць аватараў, а іх імёны не абавязкова\nўнікальныя. Палепшыце профіль да глабальнага, каб\nзахаваць сваё імя і выбраць аватар.", + "localProfileText": "(лакальны профіль)", + "nameDescriptionText": "Імя Гульца", + "nameText": "Імя", + "randomText": "выпадкова", + "titleEditText": "Рэдагаваць Профіль", + "titleNewText": "Новы Профіль", + "unavailableText": "Імя \"${NAME}\" недаступна; паспрабуйце іншае.", + "upgradeProfileInfoText": "Гэта захавае вашае імя гульца і дазволіць\nвыбраць аватар для профіля.", + "upgradeToGlobalProfileText": "Палепшыць да Глабальнага Профіля" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Вы не можаце выдаліць стандартны саўндтрэк.", + "cantEditDefaultText": "Нельга змяніць стандартны саўндтрэк. Прадублюйце яго ці стварыце новы.", + "cantOverwriteDefaultText": "Нельга перазапісаць стандартны саўндтрэк.", + "cantSaveAlreadyExistsText": "Саўндтрэк з такім імем ужо існуе!", + "defaultGameMusicText": "<стандартная музыка гульні>", + "defaultSoundtrackNameText": "Стандартны Саўндтрэк", + "deleteConfirmText": "Выдаліць Саўндтрэк:\n\n'${NAME}'?", + "deleteText": "Выдаліць\nСаўндтрэк", + "duplicateText": "Прадубляваць\nСаўндтрэк", + "editSoundtrackText": "Рэдактар Саўндтрэкаў", + "editText": "Рэдагаваць\nСаўндтрэк", + "fetchingITunesText": "Атрымліванне iTunes плэйлістаў...", + "musicVolumeZeroWarning": "Увага: гучнасць музыкі ўсталявана на 0", + "nameText": "Імя", + "newSoundtrackNameText": "Мой Саўндтрэк ${COUNT}", + "newSoundtrackText": "Новы Саўндтрэк:", + "newText": "Новы\nСаўндтрэк", + "selectAPlaylistText": "Выберыце Плэйліст", + "selectASourceText": "Крыніца Музыкі", + "testText": "тэст", + "titleText": "Саўндтрэкі", + "useDefaultGameMusicText": "Стандартная Музыка Гульні", + "useITunesPlaylistText": "Плэйліст iTunes", + "useMusicFileText": "Музычны Файл (mp3 і г.д.)", + "useMusicFolderText": "Тэчка з Музыкай" + }, + "editText": "Рэдагаваць", + "endText": "Канец", + "enjoyText": "Поспехаў!", + "epicDescriptionFilterText": "${DESCRIPTION} у эпічным рэжыме.", + "epicNameFilterText": "${NAME} у Эпічным Рэжыме", + "errorAccessDeniedText": "доступ забаронены", + "errorOutOfDiskSpaceText": "не хапае месца на дыске", + "errorText": "Памылка", + "errorUnknownText": "Невядомая памылка", + "exitGameText": "Зачыніць ${APP_NAME}?", + "exportSuccessText": "'${NAME}' экспартуецца.", + "externalStorageText": "Знешняя памяць", + "failText": "Правал", + "fatalErrorText": "Ой; штосьці зламалася.\nКалі ласка, пераўсталюйце гульню ці\nнапішыце на ${EMAIL} для дапамогі.", + "fileSelectorWindow": { + "titleFileFolderText": "Выберыце Файл ці Тэчку", + "titleFileText": "Выберыце Файл", + "titleFolderText": "Выберыце Тэчку", + "useThisFolderButtonText": "Карыстацца Гэтай Тэчкай" + }, + "filterText": "Фільтр", + "finalScoreText": "Канчатковы Вынік", + "finalScoresText": "Канчатковыя Вынікі", + "finalTimeText": "Канчатковы Час", + "finishingInstallText": "Канец усталёўкі... адзін момант...", + "fireTVRemoteWarningText": "* Для больш зручнай гульні карыстайцеся\nстандартнымі кантролерамі, ці ўсталюйце \nпрыкладанне '${REMOTE_APP_NAME}'\nна ваш тэлефон ці планшэт.", + "firstToFinalText": "Фінал да ${COUNT} Ачкоў", + "firstToSeriesText": "Серыя да ${COUNT} Ачкоў", + "fiveKillText": "ПЯЦЬ ЗАБОЙСТВ!!!", + "flawlessWaveText": "Бездакорная хваля!", + "fourKillText": "ЧАТЫРЫ ЗАБОЙСТВЫ!!!", + "friendScoresUnavailableText": "Вынікі сяброў недаступныя.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Лідэры ${COUNT} Гульні", + "gameListWindow": { + "cantDeleteDefaultText": "Вы не можаце выдаліць стандартны плэйліст.", + "cantEditDefaultText": "Нельга рэдагаваць стандартны плэйліст! Прадублюйце яго ці стварыце новы.", + "cantShareDefaultText": "Вы не можаце падзяліцца спісам прайгравання па змаўчанні.", + "deleteConfirmText": "Выдаліць \"${LIST}\"?", + "deleteText": "Выдаліць\nПлэйліст", + "duplicateText": "Прадубляваць\nПлэйліст", + "editText": "Рэдагаваць\nПлэйліст", + "newText": "Новы\nПлэйліст", + "showTutorialText": "Паказаць Туторыял", + "shuffleGameOrderText": "Выпадковы Парадак Гульняў", + "titleText": "Наладзіць ${TYPE} Плэйлісты" + }, + "gameSettingsWindow": { + "addGameText": "Дадаць Гульню" + }, + "gamesToText": "${WINCOUNT} гульняў супраць ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Кожная прылада ў лоббі можа мець больш за \nаднаго гульца, калі ў вас дастаткова кантролераў.", + "aboutDescriptionText": "Выкарыстоўвайце гэтыя раздзелы, каб стварыць лоббі.\n\nЛоббі дазваляюць вам праходзіць гульні і турніры\nз вашымі сябрамі, з некалькіх прылад адначасова.\n\nВыкарыстоўвайце кнопку ${PARTY}, каб зайсці ў чат\nі размаўляць з сябрамі (або націсніце кнопку \n${BUTTON} у меню кантролера)", + "aboutText": "Аб функцыі", + "addressFetchErrorText": "<памылка пры атрымліванні адрасоў>", + "appInviteInfoText": "Запрасіце сяброў у гульню, і яны атрымаюць\n${COUNT} бясплатных квіткоў. Вы таксама атрымаеце\n${YOU_COUNT} за кожнага сябра.", + "appInviteMessageText": "${NAME} даслаў вам ${COUNT} квіткоў у ${APP_NAME}", + "appInviteSendACodeText": "Даслаць Код", + "appInviteTitleText": "Запрашэнне ў ${APP_NAME}", + "bluetoothAndroidSupportText": "(працуе з любой Android-прыладай, якая падтрымлівае Bluetooth)", + "bluetoothDescriptionText": "Стварыць/далучыцца да гульні праз Bluetooth:", + "bluetoothHostText": "Стварыць", + "bluetoothJoinText": "Далучыцца", + "bluetoothText": "Bluetooth", + "checkingText": "праверка...", + "dedicatedServerInfoText": "Для дасягнення найлепшых вынікаў наладзьце спецыяльны сервер. Гл. Bombsquadgame.com/server, каб даведацца, як.", + "disconnectClientsText": "Гэта адлучыць ${COUNT} гульцоў з вашага\nлоббі. Вы ўпэўнены?", + "earnTicketsForRecommendingAmountText": "Сябры атрымаюць ${COUNT} квіткоў, калі яны паспрабуюць гульню\n(вы таксама атрымаеце ${YOU_COUNT} квіткоў за кожнага сябра)", + "earnTicketsForRecommendingText": "Падзяліцеся гульнёй, \nкаб атрымаць квіткі.", + "emailItText": "Паслаць", + "friendHasSentPromoCodeText": "${COUNT} квіткоў ${APP_NAME} ад ${NAME}", + "friendPromoCodeAwardText": "Вы атрымаеце ${COUNT} квіткоў кожны раз, калі ён будзе выкарыстаны.", + "friendPromoCodeExpireText": "Код дзейнічае ${EXPIRE_HOURS} гадзін(ы) і працуе толькі для новых гульцоў.", + "friendPromoCodeInfoText": "Ён можа быць абменены на ${COUNT} квіткоў.\n\nЗайдзіце ў \"Налады->Дадатковыя->Увесці прома-код\", каб скарыстацца ім.\nНаведайце bombsquadgame.com, каб зладаваць гульню на любую платформу, якая падтрымліваецца.\nГэты код мінае праз ${EXPIRE_HOURS} гадзін(ы)(а) і ён дзейнічае толькі для новых гульцоў.", + "friendPromoCodeInstructionsText": "Каб выкарыстоўваць яго, адкрыйце ${APP_NAME} і перайдзіце ў раздзел \"Налады-> Дадатковыя-> Увесці код\".\nГлядзіце bombsquadgame.com для спасылкі на загрузку ўсіх падтрымліваемых платформаў.", + "friendPromoCodeRedeemLongText": "Ён можа быць абменены на ${COUNT} квіткоў максімум ${MAX_USES} гульцамі.", + "friendPromoCodeRedeemShortText": "Ён можа быць абменены на ${COUNT} квіткоў у гульні.", + "friendPromoCodeWhereToEnterText": "(у раздзеле \"Налады->Дадатковыя->Увядзіце код\")", + "getFriendInviteCodeText": "Атрымаць Код для Сяброў", + "googlePlayDescriptionText": "Запрасіце гульцоў з Google Play у вашае лоббі.", + "googlePlayInviteText": "Запрасіць", + "googlePlayReInviteText": "У вашым лоббі знаходзяцца ${COUNT} гульцоў з Google Play,\nякія будуць адключаны, калі вы створыце новае лоббі.\nУключыце іх у вашае запрашэнне, каб вярнуць іх назад.", + "googlePlaySeeInvitesText": "Паглядець запрашэнні", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / версія Google Play)", + "hostPublicPartyDescriptionText": "Прымае грамадскую вечарыну:", + "inDevelopmentWarningText": "Увага:\n\nГульня па сетцы - новая функцыя, якая зараз \nразвіваецца. На сённяшні дзень рэкамендуецца, \nкаб усе гульцы знаходзіліся ў адной WiFi сетцы.", + "internetText": "Інтэрнэт", + "inviteAFriendText": "У сяброў няма гульні? Запрасіце іх паспрабаваць,\nі яны атрымаюць ${COUNT} дадатковых квіткоў.", + "inviteFriendsText": "Запрасіць Сяброў", + "joinPublicPartyDescriptionText": "Далучайцеся да грамадскай вечарыны:", + "localNetworkDescriptionText": "Далучыцеся да лоббі ў вашай сетцы:", + "localNetworkText": "Лакальная сетка", + "makePartyPrivateText": "Зрабіць Маё Лоббі Прыватным", + "makePartyPublicText": "Зрабіце маю партыю публічнай", + "manualAddressText": "Адрас", + "manualConnectText": "Далучыцца", + "manualDescriptionText": "Далучыцеся да лоббі па адрасе:", + "manualJoinableFromInternetText": "Да вас можна далучыцца праз інтэрнэт?:", + "manualJoinableNoWithAsteriskText": "Не*", + "manualJoinableYesText": "Так", + "manualRouterForwardingText": "*каб выправіць гэта, паспрабуйце накіраваць ваш UDP-порт ${PORT} для вашага лакальнага адрасу", + "manualText": "Ручны", + "manualYourAddressFromInternetText": "Ваш адрас з інтэрнэту:", + "manualYourLocalAddressText": "Ваш лакальны адрас:", + "noConnectionText": "<няма злучэння>", + "otherVersionsText": "(іншыя версіі)", + "partyInviteAcceptText": "Згадзіцца", + "partyInviteDeclineText": "Адмовіцца", + "partyInviteGooglePlayExtraText": "(зайдзіце ў укладку \"Google Play\" у раздзеле \"Сабраць\")", + "partyInviteIgnoreText": "Ігнараваць", + "partyInviteText": "${NAME} запрасіў\nвас у сваё лоббі!", + "partyNameText": "Назва Лоббі", + "partySizeText": "Размер Лоббі", + "partyStatusCheckingText": "Правяраем статус...", + "partyStatusJoinableText": "Зараз твае Лоббі дасягаемае праз інтэрнэт", + "partyStatusNoConnectionText": "Не зпалучаецца дасягнуцца да серверу", + "partyStatusNotJoinableText": "Твае Лоббі не дасягаемае праз інтэрнэт", + "partyStatusNotPublicText": "Твае Лоббі не публічнае", + "pingText": "Пінг", + "portText": "Порт", + "requestingAPromoCodeText": "Запыт кода...", + "sendDirectInvitesText": "Даслаць Запрашэнні", + "sendThisToAFriendText": "Адпраўце гэты код вашаму сябру:", + "shareThisCodeWithFriendsText": "Падзяліцца кодам з сябрамі:", + "showMyAddressText": "Паказаць мой адрас", + "titleText": "Сабраць", + "wifiDirectDescriptionBottomText": "Калі ўсе прылады падтрымліваюць 'Wi-Fi Direct', яны могуць карыстацца ім, каб падключыцца\nадзін да другога. Калі ўсе прылады падключаны, вы можаце ствараць лоббі, карыстаючыся\nўкладкай \"Лакальная сетка\" так жа, як і з звычайнай WiFi сеткай.\n\nДля лепшых вынікаў хост Wi-Fi Direct павінен таксама быць хостам гульні ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct можа выкарыстоўвацца для злучэння Android прылад непасрэдна,\nбез WiFi сеткі. Гэта працуе лепш на Android 4.2 ці навей.\n\nКаб cкарыстацца гэтым, адчыніце налады і знайдзіце 'Wi-Fi Direct'.", + "wifiDirectOpenWiFiSettingsText": "Адкрыць налады WiFi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(працуе паміж усімі платформамі)", + "worksWithGooglePlayDevicesText": "(працуе з прыладамі, на якіх усталявана android-версія гульні)", + "youHaveBeenSentAPromoCodeText": "Вам адправілі прома-код ${APP_NAME}:" + }, + "getTicketsWindow": { + "freeText": "БЯСПЛАТНА!", + "freeTicketsText": "Бясплатныя Квіткі", + "inProgressText": "Транзакцыя выконваецца; калі ласка пачакайце хвіліну.", + "purchasesRestoredText": "Пакупкі адноўлены.", + "receivedTicketsText": "Атрымана ${COUNT} квіткоў!", + "restorePurchasesText": "Аднавіць пакупкі", + "ticketDoublerText": "Падваіцель Квіткоў", + "ticketPack1Text": "Малы Пакет Квіткоў", + "ticketPack2Text": "Сярэдні Пакет Квіткоў", + "ticketPack3Text": "Вялікі Пакет Квіткоў", + "ticketPack4Text": "Вельмі Вялікі Пакет Квіткоў", + "ticketPack5Text": "Гіганцкі Пакет Квіткоў", + "ticketPack6Text": "Максімальны Пакет Квіткоў", + "ticketsFromASponsorText": "Атрымаць ${COUNT} квіткоў\nад спонсара", + "ticketsText": "${COUNT} Квіткоў", + "titleText": "Атрымаць Квіткі", + "unavailableLinkAccountText": "Прабачце, пакупкі недаступныя на гэтай платформе.\nВы можаце злучыць гэты акаўнт з іншым акаўнтам на\nіншайплатформе і рабіць пакупкі на ім.", + "unavailableTemporarilyText": "Недаступна; калі ласка, паспрабуйце пазней.", + "unavailableText": "Прабачце, гэта недаступна.", + "versionTooOldText": "Прабачце, ваша версія гульні занадта старая; абнавіце яе.", + "youHaveShortText": "вы маеце ${COUNT}", + "youHaveText": "вы маеце ${COUNT} квіткоў" + }, + "googleMultiplayerDiscontinuedText": "Прабачце, мультігульны сервіс Гугл не даступны у гэты час.\nЯ клапачуся над гэтым з усёй скорасцю.\n\nДа таго часу, калі ласка паспрабуйце другое падключэнне", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Заўсёды", + "fullScreenCmdText": "Поўнаэкранны (Сmd-F)", + "fullScreenCtrlText": "Поўнаэкранны (Ctrl-F)", + "gammaText": "Гама", + "highText": "Высокае", + "higherText": "Найвышэйшае", + "lowText": "Нізкае", + "mediumText": "Сярэдняе", + "neverText": "Ніколі", + "resolutionText": "Дазвол", + "showFPSText": "Паказваць FPS", + "texturesText": "Тэкстуры", + "titleText": "Графіка", + "tvBorderText": "TV мяжа", + "verticalSyncText": "Вертыкальная Сінхранізацыя", + "visualsText": "Эфекты" + }, + "helpWindow": { + "bombInfoText": "- Бомба - \nМацней за ўдары, але можа нанесці\nшкоду і вам самім. Для лепшых\nвынікаў кідайце ў ворага, пакуль\nне згарэў кнот.", + "canHelpText": "${APP_NAME} можа дапамагчы.", + "controllersInfoText": "Вы можаце гуляць у ${APP_NAME} з сябрамі праз сетку або, калі\nвы маеце дастаткова кантролераў, на адной прыладзе.\n${APP_NAME} падтрымлівае мноства кантролераў - нават тэлефон \n(для гэтага спатрэбіцца прыкладанне '${REMOTE_APP_NAME}').\nГл. Налады -> Кантролеры для атрымання дадатковай інфармацыі.", + "controllersText": "Кантролеры", + "controlsSubtitleText": "Ваш персанаж ${APP_NAME} валодае некалькімі базавымі прыёмамі:", + "controlsText": "Прыёмы", + "devicesInfoText": "Вы можаце гуляць гуляць у ${APP_NAME} па сетцы, таму\nдаставайце вашыя дадатковыя тэлефоны, планшэты, камп'ютары і\nгуляйце на ніх. Можна нават падключаць звычайную версію гульні\nда VR версіі, каб дазволіць іншым людзям назіраць за\nпадзеямі ў гульні.", + "devicesText": "Прылады", + "friendsGoodText": "Вельмі добра іх мець. У ${APP_NAME} весялей гуляць з некалькімі\nгульцамі, ён падтрымлівае да 8 гульцоў адначасова.", + "friendsText": "Сябры", + "jumpInfoText": "- Прыгаць -\nПрыгайце, каб пераадольваць\nперашкоды, кідаць рэчы вышэй\nці проста павесяліцца.", + "orPunchingSomethingText": "Ці ўдарыць, скінуць з абрыва і падарваць бомбай-ліпучкай па дарозе ўніз.", + "pickUpInfoText": "- Падняць -\nХапайце сцягі, ворагаў ці штосьці\nіншае, што не прыкручана да зямлі.\nНажміце яшчэ раз, каб кінуць.", + "powerupBombDescriptionText": "Дазваляе вам кінуць тры бомбы\nза адзін раз замест адной.", + "powerupBombNameText": "Патрайняльнік Бомб", + "powerupCurseDescriptionText": "Вы, напэўна, хаціце пазбегнуць гэтага.\n ...ці не?", + "powerupCurseNameText": "Смерць", + "powerupHealthDescriptionText": "Ніколі не здагадайцеся.\nВяртае поўнае здароўе.", + "powerupHealthNameText": "Аптэчка", + "powerupIceBombsDescriptionText": "Слабейшая за іншыя бомбы,\nале замарожвае вашых ворагаў\nі наносіць няшмат страт.", + "powerupIceBombsNameText": "Лядовая Бомба", + "powerupImpactBombsDescriptionText": "Нямнога слабейшая за звычайную,\nале падрываецца пры ўдары.", + "powerupImpactBombsNameText": "Ударная Бомба", + "powerupLandMinesDescriptionText": "Выдаюцца па 3 штукі.\nКарысныя для абароны базы\nці спынення хуткіх ворагаў.", + "powerupLandMinesNameText": "Міны", + "powerupPunchDescriptionText": "Робяць вашыя ўдары мацней,\nхутчэй, і ўвогуле лепш.", + "powerupPunchNameText": "Баксёрскія Пальчаткі", + "powerupShieldDescriptionText": "Бяруць усе страты на сябе,\nтаму вы застаецеся здаровымі.", + "powerupShieldNameText": "Энэргетычны Шчыт", + "powerupStickyBombsDescriptionText": "Прыліпае да ўсяго, да чаго \nдакранецца. І пачынаецца...", + "powerupStickyBombsNameText": "Бомба-Ліпучка", + "powerupsSubtitleText": "Канечне, ніякая гульня немагчыма без узмацняльнікаў:", + "powerupsText": "Узмацняльнікі", + "punchInfoText": "- Удар -\nУдары наносяць тым больш страт,\nчем хутчэй вы рухаецеся і \nкруціцеся як вар'ят.", + "runInfoText": "- Бяжаць -\nНацісніце ЛЮБУЮ кнопку, каб бяжаць. Трыгеры ці плечавыя кнопкі падыходзяць для гэтага.\nБег дапамагае рухацца хутчэй, але манёўры становяцца больш складанымі.", + "someDaysText": "Часам вам проста хочацца нешта ўдарыць. Ці падарваць.", + "titleText": "Дапамога па ${APP_NAME}", + "toGetTheMostText": "Каб атрымаць максімум ад гэтай гульні, вам спатрэбяцца:", + "welcomeText": "Прывітанне ў ${APP_NAME}!" + }, + "holdAnyButtonText": "<націсніце любую кнопку>", + "holdAnyKeyText": "<націсніце любую кнопку>", + "hostIsNavigatingMenusText": "- ${HOST} знаходзіцца ў меню навігацыі -", + "importPlaylistCodeInstructionsText": "Выкарыстоўвайце наступны код, каб імпартаваць гэты спіс прайгравання ў іншае месца:", + "importPlaylistSuccessText": "Імпартыраван ${TYPE} плэйліст '${NAME}'", + "importText": "імпарт", + "importingText": "Імпартырую...", + "inGameClippedNameText": "У гульне будзе\n\"${NAME}\"", + "installDiskSpaceErrorText": "ПАМЫЛКА: Немагчыма закончыць усталёўку.\nХутчэй за ўсё, у вас на прыладзе закончылася \nмесца. Вызваліце нямнога і паспрабуйце яшчэ раз.", + "internal": { + "arrowsToExitListText": "націсніце ${LEFT} ці ${RIGHT}, каб закрыць спіс", + "buttonText": "кнопка", + "cantKickHostError": "Вы ня можаце выкінуць хост.", + "chatBlockedText": "${NAME} заглушаны на ${TIME} сякунд.", + "connectedToGameText": "падключэнне да '${NAME}'", + "connectedToPartyText": "Далучыўся да лоббі ${NAME}!", + "connectingToPartyText": "Падключэнне...", + "connectionFailedHostAlreadyInPartyText": "Падключэнне не ўдалося; хост у іншым лоббі.", + "connectionFailedPartyFullText": "Не атрымалася далучыцца; пакой заплонен.", + "connectionFailedText": "Падключэнне не ўдалося.", + "connectionFailedVersionMismatchText": "Далучэнне немагчыма; хост карыстаецца іншай версіяй гульні.\nПеракагайцеся, што вы абодва абноўлены і паспрабуйце яшчэ раз.", + "connectionRejectedText": "Далучэнне адхілена.", + "controllerConnectedText": "${CONTROLLER} падключаны.", + "controllerDetectedText": "Знойдзены кантролер.", + "controllerDisconnectedText": "${CONTROLLER} адключаны.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} адключаны. Калі ласка, паспрабуйце яшчэ раз.", + "controllerForMenusOnlyText": "Гэты кантролер не можа быць выкарыстаны , каб гуляць ; толькі для навігацыі па меню.", + "controllerReconnectedText": "${CONTROLLER} перападключаны.", + "controllersConnectedText": "Падключана ${COUNT} кантролера(ў).", + "controllersDetectedText": "Знойдзена ${COUNT} кантролера(ў).", + "controllersDisconnectedText": "Адключана ${COUNT} кантролер(а)(аў).", + "corruptFileText": "Знойдзены пашкоджаныя файлы. Паспрабуйце пераўсталяваць ці звярніцеся на ${EMAIL}.", + "errorPlayingMusicText": "Памылка прайгравання музыкі: ${MUSIC}", + "errorResettingAchievementsText": "Немагчыма скінуць online-вынікі; паспрабуйце пазней.", + "hasMenuControlText": "${NAME} зараз кіруе ў меню.", + "incompatibleNewerVersionHostText": "Хост запушчаны на больш новай версіі гульні.\nАбнавіце гульню да апошняй версіі і паспрабуйце яшчэ раз.", + "incompatibleVersionHostText": "Хост працуе з іншай версіяй гульні. Пераканайцеся,\nшто вы абодва абноўлены і паспрабуйце яшчэ раз.", + "incompatibleVersionPlayerText": "${NAME} працуе з іншай версіяй гульні. Пераканайцеся,\nшто вы абодва абноўлены і паспрабуйце яшчэ раз.", + "invalidAddressErrorText": "Памылка: няправільны адрас.", + "invalidNameErrorText": "Памылка: няправільнае імя.", + "invalidPortErrorText": "Памылка: няправільны порт", + "invitationSentText": "Запрашэнне адпраўлена.", + "invitationsSentText": "${COUNT} запрашэння(ў) адпраўлена.", + "joinedPartyInstructionsText": "Хто-та далучыўся да вашага лоббі.\nЗайдзіце ў \"Гуляць\", каб пачаць гульню.", + "keyboardText": "Клавіятура", + "kickIdlePlayersKickedText": "${NAME} выкінуты за прастой.", + "kickIdlePlayersWarning1Text": "${NAME} будзе выкінуты праз ${COUNT} секунд з-за прастою.", + "kickIdlePlayersWarning2Text": "(вы можаце выключыць гэта ў Налады -> Дадатковыя)", + "leftGameText": "'${NAME}' выйшаў.", + "leftPartyText": "Выйшаў з лоббі ${NAME}.", + "noMusicFilesInFolderText": "У тэчцы няма ніводнага музычнага файла.", + "playerJoinedPartyText": "${NAME} далучыўся да лоббі!", + "playerLeftPartyText": "${NAME} выйшаў з лоббі.", + "rejectingInviteAlreadyInPartyText": "Запрашэнне адхілена (ужо ў лоббі).", + "serverRestartingText": "Сервер перазапускаецца. Калі ласка, зноў далучыцеся ...", + "serverShuttingDownText": "Сервер выключаецца ...", + "signInErrorText": "Памылка пры ўваходзе.", + "signInNoConnectionText": "Немагчыма ўвайсці (няма інтэрнэт-злучэння?)", + "telnetAccessDeniedText": "ПАМЫЛКА: карыстальнік не даў доступ Telnet.", + "timeOutText": "(засталося ${TIME} секунд(ы))", + "touchScreenJoinWarningText": "Вы зайшлі з сэнсарным экранам.\nКалі гэта была памылка, націсніце 'Меню -> Пакінуць Гульню'.", + "touchScreenText": "Сэнсарны Экран", + "unableToResolveHostText": "Памылка: немагчыма знайсцi хост.", + "unavailableNoConnectionText": "Зараз гэта недаступна (няма інтэрнэт-злучэння?)", + "vrOrientationResetCardboardText": "Скарыстайцеся гэтым, каб скінуць VR арыентацыю.\nКаб гуляць, вам спатрэбіцца знешні кантролер.", + "vrOrientationResetText": "Скідванне арыентацыі VR.", + "willTimeOutText": "(час скончыцца пры прастоі)" + }, + "jumpBoldText": "ПРЫГАЙЦЕ!", + "jumpText": "Прыгайце", + "keepText": "Захаваць", + "keepTheseSettingsText": "Захаваць гэтыя налады?", + "keyboardChangeInstructionsText": "Двойчы націсніце прабел, каб змяніць клавіятуру.", + "keyboardNoOthersAvailableText": "Іншых клавіятур няма.", + "keyboardSwitchText": "Пераключэнне клавіятуры на \"${NAME}\".", + "kickOccurredText": "${NAME} быў выкiнут.", + "kickQuestionText": "Выгнаць ${NAME}?", + "kickText": "Выгнаць", + "kickVoteCantKickAdminsText": "Мадэратараў нельга выкiдываць.", + "kickVoteCantKickSelfText": "вы ня можаце выкінуць самога сябе.", + "kickVoteFailedNotEnoughVotersText": "Не хапае гульцоў для галасавання.", + "kickVoteFailedText": "Галасаванне не адбылося.", + "kickVoteStartedText": "Было пачата галасаванне супраць ${NAME}.", + "kickVoteText": "Галасаваць за штоб выгнаць", + "kickVotingDisabledText": "Галасавання адключаны.", + "kickWithChatText": "Тып ${YES} у чаце так і ${NO} для няма.", + "killsTallyText": "${COUNT} забойств", + "killsText": "Забойствы", + "kioskWindow": { + "easyText": "Лёгка", + "epicModeText": "Эпічны Рэжым", + "fullMenuText": "Поўнае Меню", + "hardText": "Цяжка", + "mediumText": "Сярэдне", + "singlePlayerExamplesText": "Адзін гулец / Кааператыўныя гульні", + "versusExamplesText": "Адзін супраць аднаго" + }, + "languageSetText": "Мова зараз: ${LANGUAGE}.", + "lapNumberText": "Круг ${CURRENT}/${TOTAL}", + "lastGamesText": "(апошнія ${COUNT} гульні(яў))", + "leaderboardsText": "Лідары", + "league": { + "allTimeText": "За ўвесь час", + "currentSeasonText": "Гэты сезон (${NUMBER})", + "leagueFullText": "${NAME} Ліга", + "leagueRankText": "Месца ў Лізе", + "leagueText": "Ліга", + "rankInLeagueText": "#${RANK}, ${NAME} Ліга${SUFFIX}", + "seasonEndedDaysAgoText": "Сезон скончыўся ${NUMBER} дзён таму.", + "seasonEndsDaysText": "Сезон скончыцца праз ${NUMBER} дзён.", + "seasonEndsHoursText": "Сезон скончыцца праз ${NUMBER} гадзін.", + "seasonEndsMinutesText": "Сезон скончыцца праз ${NUMBER} мінут.", + "seasonText": "Сезон ${NUMBER}", + "tournamentLeagueText": "Вы павінны дасягнуць ${NAME} лігі, каб увайсці ў гэты турнір.", + "trophyCountsResetText": "Трафейныя ачкі знікнуць у наступным сезоне." + }, + "levelBestScoresText": "Лепшыя вынікі на ўзроўні ${LEVEL}", + "levelBestTimesText": "Лепшы час на ўзроўні ${LEVEL}", + "levelFastestTimesText": "Самы хуткі на уроўні ${LEVEL}", + "levelHighestScoresText": "Найлепшыя вынікі на ўзроўні ${LEVEL}", + "levelIsLockedText": "${LEVEL} узровень недаступны.", + "levelMustBeCompletedFirstText": "${LEVEL} узровень павінен быць скончаны спачатку.", + "levelText": "Узровень ${NUMBER}", + "levelUnlockedText": "Узровень Адкрыт!", + "livesBonusText": "Бонус", + "loadingText": "Ладаванне...", + "loadingTryAgainText": "Загрузка; паспрабуйце яшчэ раз праз хвіліну ...", + "macControllerSubsystemBothText": "Абодва (не рэкамендуецца)", + "macControllerSubsystemClassicText": "класічны", + "macControllerSubsystemDescriptionText": "(паспрабуйце змяніць гэта, калі вашы кантролеры не працуюць)", + "macControllerSubsystemMFiNoteText": "Выяўлены кантролер, зроблены для iOS / Mac;\nВы можаце ўключыць іх у Наладах -> Кантролеры", + "macControllerSubsystemMFiText": "Зроблена для iOS / Mac", + "macControllerSubsystemTitleText": "Падтрымка кантролера", + "mainMenu": { + "creditsText": "Падзякі", + "demoMenuText": "Дэма-Меню", + "endGameText": "Скончыць Гульню", + "exitGameText": "Зачыніць Гульню", + "exitToMenuText": "Выйсці ў меню?", + "howToPlayText": "Як Гуляць", + "justPlayerText": "(Толькі ${NAME})", + "leaveGameText": "Пакінуць Гульню", + "leavePartyConfirmText": "Сапраўды пакінуць лоббі?", + "leavePartyText": "Пакінуць Лоббі", + "quitText": "Выйсці", + "resumeText": "Працягнуць", + "settingsText": "Налады" + }, + "makeItSoText": "Так", + "mapSelectGetMoreMapsText": "Атрымаць больш мапаў...", + "mapSelectText": "Выбраць...", + "mapSelectTitleText": "${GAME} Мапы", + "mapText": "Мапа", + "maxConnectionsText": "Максімальная колькасць падключэнняў.", + "maxPartySizeText": "Максімальны памер групы.", + "maxPlayersText": "Максімальная колькасць гульцоў.", + "modeArcadeText": "Аркадны рэжым.", + "modeClassicText": "Класічны рэжым.", + "modeDemoText": "Дэмо рэжым", + "mostValuablePlayerText": "Самы Карысны Гулец", + "mostViolatedPlayerText": "Самы Збіты Гулец", + "mostViolentPlayerText": "Самы Жорсткі Гулец", + "moveText": "Рухацца", + "multiKillText": "${COUNT}-ЗАБІТА!!!", + "multiPlayerCountText": "${COUNT} гульцоў", + "mustInviteFriendsText": "Вы можаце запрасіць сяброў у\nраздзеле \"${GATHER}\" ці прыкласці\nкантролеры, каб гуляць у мультыплэеры.", + "nameBetrayedText": "${NAME} здрадзіў ${VICTIM}.", + "nameDiedText": "${NAME} загінуў.", + "nameKilledText": "${NAME} забіў ${VICTIM}.", + "nameNotEmptyText": "Імя не можа быць пустым!", + "nameScoresText": "${NAME} вядзе!", + "nameSuicideKidFriendlyText": "${NAME} выпадкова загінуў.", + "nameSuicideText": "${NAME} скончыў жыццё самагубствам.", + "nameText": "Імя", + "nativeText": "Родны", + "newPersonalBestText": "Новы асабісты рэкорд!", + "newTestBuildAvailableText": "Новая тэставая зборка даступна! (${VERSION} build ${BUILD}).\nАтрымайце яе на ${ADDRESS}", + "newText": "Новы", + "newVersionAvailableText": "Новая версія ${APP_NAME} даступна! (${VERSION})", + "nextAchievementsText": "Наступныя дасягненні:", + "nextLevelText": "Наступны Ўзровень", + "noAchievementsRemainingText": "- не", + "noContinuesText": "(без працягу)", + "noExternalStorageErrorText": "Знешняя памяць не знойдзена", + "noGameCircleText": "Памылка: вы не ўвайшлі ў GameCircle", + "noProfilesErrorText": "У вас няма ніводнага профіля, таму вас будуць называць '${NAME}'.\nЗайдзіце ў \"Налады -> Профілі\", каб стварыць уласны профіль.", + "noScoresYetText": "Вынікаў пакуль няма.", + "noThanksText": "Не, дзякуй", + "noTournamentsInTestBuildText": "УВАГА: Ацэнкі турніраў з гэтай тэставай зборкі будуць ігнаравацца.", + "noValidMapsErrorText": "Не знойдзена мап для гэтага тыпу гульні.", + "notEnoughPlayersRemainingText": "Засталося вельмі мала гульцоў; пачніце новую гульню.", + "notEnoughPlayersText": "Вам патрэбна не менш за ${COUNT} гульцоў, каб пачаць!", + "notNowText": "Не Зараз", + "notSignedInErrorText": "Вы павінны ўвайсці, каб выканаць гэта.", + "notSignedInGooglePlayErrorText": "Вы павінны ўвайсці з Google Play, каб выканаць гэта.", + "notSignedInText": "не ўвайшлі", + "nothingIsSelectedErrorText": "Нічога не выбрана!", + "numberText": "№${NUMBER}", + "offText": "Выключана", + "okText": "Так", + "onText": "Уключана", + "onslaughtRespawnText": "${PLAYER} з'явіцца ў ${WAVE} хвалі", + "orText": "${A} ці ${B}", + "otherText": "Іншае...", + "outOfText": "(#${RANK} з ${ALL})", + "ownFlagAtYourBaseWarning": "Каб зарабіць ачкі, ваш сцяг\nпавінен быць на вашай базе!", + "packageModsEnabledErrorText": "Гульня па сетцы не дазваляе выкарыстоўваць моды (глядзі \"Налады -> Дадатковыя\")", + "partyWindow": { + "chatMessageText": "Паведамленне ў Чат", + "emptyText": "Вашае лоббі пустое", + "hostText": "(хост)", + "sendText": "Адправіць", + "titleText": "Вашае Лоббі" + }, + "pausedByHostText": "(спынена хостам)", + "perfectWaveText": "Бездакорная Хваля!", + "pickUpText": "Падняць", + "playModes": { + "coopText": "Кааператыўны", + "freeForAllText": "Адзіночны", + "multiTeamText": "Мультыкамандны", + "singlePlayerCoopText": "Адзін Гулец / Група", + "teamsText": "Камандны" + }, + "playText": "Гуляць", + "playWindow": { + "oneToFourPlayersText": "1-4 гульца", + "titleText": "Гуляць", + "twoToEightPlayersText": "2-8 гульцоў" + }, + "playerCountAbbreviatedText": "${COUNT} гулец", + "playerDelayedJoinText": "${PLAYER} увойдзе ў пачатаку наступнага раўнда.", + "playerInfoText": "Інфармацыя аб Гульцы", + "playerLeftText": "${PLAYER} пакінуў гульню.", + "playerLimitReachedText": "Ліміт гульцоў (${COUNT}) дасягнуты; далучэнне немагчыма.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Вы не можаце выдаліць профіль вашага акаўнта.", + "deleteButtonText": "Выдаліць\nПрофіль", + "deleteConfirmText": "Выдаліць '${PROFILE}'?", + "editButtonText": "Рэдагаваць\nПрофіль", + "explanationText": "(стварэнне розных імёнаў і выглядаў для гэтага акаўнта)", + "newButtonText": "Новы\nПрофіль", + "titleText": "Профілі Гульцоў" + }, + "playerText": "Гулец", + "playlistNoValidGamesErrorText": "У гэтым плэйлісце няма адкрытых гульняў.", + "playlistNotFoundText": "плэйліст не знойдзены", + "playlistsText": "Плэйлісты", + "pleaseRateText": "Калі вам падабаецца ${APP_NAME}, калі ласка, знайдзіце\nчас, каб ацаніць яго ці напісаць водгук. Гэта забя-\nспечвае сувязь і дапамагае развіццю гульні.\n\nДзякуй!\n-Эрык", + "pleaseWaitText": "Калі ласка пачакай...", + "pluginsDetectedText": "Выяўлены новыя ўбудовы. Уключыце / наладзьце іх у наладах.", + "pluginsText": "Убудовы", + "practiceText": "Практыка", + "pressAnyButtonPlayAgainText": "Націсніце любую кнопку, каб перазапусціць...", + "pressAnyButtonText": "Націсніце любую кнопку, каб працягнуць...", + "pressAnyButtonToJoinText": "націсніце любую кнопку, каб далучыцца...", + "pressAnyKeyButtonPlayAgainText": "Націсніце любую кнопку, каб перазапусціць...", + "pressAnyKeyButtonText": "Націсніце любую кнопку, каб працягнуць...", + "pressAnyKeyText": "Націсніце любую кнопку...", + "pressJumpToFlyText": "** Націскайце прыгаць некалькі разоў, каб лятаць **", + "pressPunchToJoinText": "націсніце УДАРЫЦЬ, каб далучыцца", + "pressToOverrideCharacterText": "націсніце ${BUTTONS}, каб пераназначыць свайго персанажа", + "pressToSelectProfileText": "націсніце ${BUTTONS}, каб выбраць гульца", + "pressToSelectTeamText": "націсніце ${BUTTONS}, каб выбраць каманду", + "promoCodeWindow": { + "codeText": "Код", + "codeTextDescription": "Прома-Код", + "enterText": "Адправіць" + }, + "promoSubmitErrorText": "Памылка пры адпраўцы кода; праверце падключэнне да Інтэрнэту", + "ps3ControllersWindow": { + "macInstructionsText": "Адключыце харчаванне на задняй панэлі PS3, пераканайцеся, што Bluetooth\nуключаны на вашым кампутары, а затым падключыце кантролер да Mac\nз дапамогаю кабеля USB для сінхранізацыі. Зараз можна выкарыстоўваць\nкнопку кантролера 'Home' каб падключыць яго да Mac у правадным (USB)\nабо бесправадным (Bluetooth) рэжыме.\n\nНа некаторых Mac пры сінхранізацыі можа спатрэбіцца код доступу.\nУ гэтым выпадку звярніцеся да наступнай інструкцыі або да Google.\n\n\n\n\nКантралёры PS3, звязаныя па бесправадной сетцы, павінны з'явіцца\nу спісе прылад у \"Налады сістэмы -> Bluetooth\". Магчыма, вам прыйдзецца\nвыдаліць іх з гэтага спісу, калі вы хочаце зноў выкарыстоўваць іх з PS3.\n\nТаксама заўсёды адключайце іх ад Bluetooth, калі ён не выкарыстоўваецца,\nінакш будуць садзіцца батарэйкі.\n\nBluetooth павінен апрацоўваць да 7 падлучаных прылад,\nале у вас можа атрымацца па-іншаму.", + "ouyaInstructionsText": "Каб выкарыстоўваць кантролер PS3 з OUYA, проста падключыце яго адзін раз\nз дапамогай кабеля USB для сінхранізацыі. Гэта можа адключыць іншыя\nкантролеры, тады трэба перазагрузіць OUYA і адлучыць кабель USB.\n\nПасля гэтага можна выкарыстоўваць кнопку 'Home' для бесправаднога\nпадключэння. Пасля гульні націсніце і ўтрымлівайце кнопку 'Home' на працягу\n10 секунд каб выключыць кантролер, у адваротным выпадку ён можа\nзастацца уключаным і разрадзіць батарэйкі.", + "pairingTutorialText": "спарванне туторыяла", + "titleText": "Выкарыстоўванне кантролераў PS3 з ${APP_NAME}:" + }, + "punchBoldText": "УДАРЫЦЬ", + "punchText": "Ударыць", + "purchaseForText": "Набыць за ${PRICE}", + "purchaseGameText": "Набыць Гульню", + "purchasingText": "Набыццё...", + "quitGameText": "Зачыніць ${APP_NAME}?", + "quittingIn5SecondsText": "Выхад праз 5 секунд...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Выпадкова", + "rankText": "Узровень", + "ratingText": "Рэйтынг", + "reachWave2Text": "Дасягніце 2-ой хвалі, каб атрымаць узровень.", + "readyText": "гатовы", + "recentText": "Нядаўнія", + "remoteAppInfoShortText": "Гуляць ў ${APP_NAME} значна цікавей з сям'ёй ці сябрамі.\nПадлучыце некалькі кантролераў ці ўсталюйце прыкладанне\n${REMOTE_APP_NAME} на тэлефон ці планшэт, каб выкарыстоўваць\nіх у якасці кантролераў.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Месцазнаходжанне Кнопак", + "button_size": "Памер Кнопак", + "cant_resolve_host": "Хост не знойдзены.", + "capturing": "Чаканне...", + "connected": "Злучана.", + "description": "Выкарыстоўвайце ваш тэлефон ці планшэт у якасці кантролераў.\nАдначасова можна падлучыць да 8 прылад.", + "disconnected": "Адключаны серверам.", + "dpad_fixed": "фіксаваны", + "dpad_floating": "незафіксаваны", + "dpad_position": "Месцазнаходжанне D-Pad", + "dpad_size": "Памер D-Pad", + "dpad_type": "Тып D-Pad", + "enter_an_address": "Увесці Адрас", + "game_full": "Гульня не адказвае.", + "game_shut_down": "Гульня адключана.", + "hardware_buttons": "Сістэмныя Кнопкі", + "join_by_address": "Падлучыцца па Адрасе...", + "lag": "Лаг: ${SECONDS} сек", + "reset": "Аднавіць Налады", + "run1": "Бег 1", + "run2": "Бег 2", + "searching": "Пошук гульняў BombSquad...", + "searching_caption": "Націсніце на імя гульні, каб падлучыцца.\nПераканайцеся, што вы падлучаны да адной WiFi сеткі.", + "start": "Пачаць", + "version_mismatch": "Старая версія.\nПераканайцеся, што вы маеце апошнія версіі\nBombSquad і BombSquad Remote." + }, + "removeInGameAdsText": "Купіце \"${PRO}\" у магазіне, каб выдаліць рэкламу.", + "renameText": "Перайменаваць", + "replayEndText": "Закончыць Запіс", + "replayNameDefaultText": "Запіс Апошняй Гульні", + "replayReadErrorText": "Памылка пры чытанні запіса.", + "replayRenameWarningText": "Пераймянуйце \"${REPLAY}\" пасля гульні, калі вы жадаеце захаваць яго; інакш ён выдаліцца.", + "replayVersionErrorText": "Прабачце, запіс быў зроблены ў старай версіі\nгульні і не можа адлюстравацца.", + "replayWatchText": "Глядзець Запіс", + "replayWriteErrorText": "Памылка пры запісе відэазапіса.", + "replaysText": "Запісы", + "reportPlayerExplanationText": "Напішыце на гэты email, каб паскардзіцца на махлярства ці іншыя дрэнныя паводзіны.\nКалі ласка, апішыце ніжэй:", + "reportThisPlayerCheatingText": "Махлярства", + "reportThisPlayerLanguageText": "Нецэнзурныя Выказванні", + "reportThisPlayerReasonText": "На што вы жадаеце паскардзіцца?", + "reportThisPlayerText": "Паскардзіцца", + "requestingText": "Запыт...", + "restartText": "Перазапусціць", + "retryText": "Яшчэ Раз", + "revertText": "Аднавіць", + "runText": "Бяжаць", + "saveText": "Захаваць", + "scanScriptsErrorText": "Памылкі пры сканаванні сцэнарыяў; падрабязнасці глядзіце ў логах.", + "scoreChallengesText": "Іншыя Вынікі", + "scoreListUnavailableText": "Вынікі недаступны.", + "scoreText": "Ачкі", + "scoreUnits": { + "millisecondsText": "Мілісекунды", + "pointsText": "Ачкі", + "secondsText": "Секунды" + }, + "scoreWasText": "(быў ${COUNT})", + "selectText": "Выбраць", + "seriesWinLine1PlayerText": "ПЕРАМАГАЕ Ў", + "seriesWinLine1TeamText": "ПЕРАМАГАЮЦЬ У", + "seriesWinLine1Text": "ПЕРАМАГАЕ Ў", + "seriesWinLine2Text": "СЕРЫІ!", + "settingsWindow": { + "accountText": "Акаўнт", + "advancedText": "Дадатковыя", + "audioText": "Аўдыя", + "controllersText": "Кантролеры", + "graphicsText": "Графіка", + "playerProfilesMovedText": "Заўвага: Профілі гульцоў былі перамешчаны ў \"Акаўнт\" у галоўным меню.", + "playerProfilesText": "Профілі Гульцоў", + "titleText": "Налады" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(простая, зручная для кантролера клавіятура для рэдагавання тэксту)", + "alwaysUseInternalKeyboardText": "Заўсёды карыстацца ўбудаванай клавіятурай", + "benchmarksText": "Тэст Прадукцыйнасці і Тэст-Нагрузка", + "disableCameraGyroscopeMotionText": "Адключыць рух гіраскопа камеры", + "disableCameraShakeText": "Адключыць устрэсванне камеры", + "disableThisNotice": "(вы можаце адключыць гэта апавяшчэнне ў дадатковых наладах)", + "enablePackageModsDescriptionText": "(уключае моды, але адключае гульню па сетцы)", + "enablePackageModsText": "Уключыць Лакальныя Пакеты Модаў", + "enterPromoCodeText": "Увесці код", + "forTestingText": "Гэтыя значэнні выкарыстоўваюцца толькі для тэстаў і будуць згублены пры закрыцці гульні.", + "helpTranslateText": "Пераклад ${APP_NAME} з англійскай мовы - намаганне супольнасці\nпадтрымкі. Калі вы жадаеце выправіць пераклад,\nпрайдзіце па спасылцы ніжэй. Дзякуй!", + "kickIdlePlayersText": "Выкідваць гульцоў, якія не дзейнічаюць", + "kidFriendlyModeText": "Дзіцячы Рэжым (менш гвалту і г.д.)", + "languageText": "Мова", + "moddingGuideText": "Кіраўніцтва па Модынгу", + "mustRestartText": "Вы павінны перазагрузіць гульню, каб прымяніць новыя налады.", + "netTestingText": "Тэсціраванне Сеткі", + "resetText": "Скінуць", + "showBombTrajectoriesText": "Паказваць Траекторыi Бомб", + "showPlayerNamesText": "Паказваць Імёны Гульцоў", + "showUserModsText": "Паказаць Тэчку З Модамі", + "titleText": "Дадаткова", + "translationEditorButtonText": "Рэдактар Перакладаў ${APP_NAME}", + "translationFetchErrorText": "статус перакладу недаступны", + "translationFetchingStatusText": "праверка статуса перакладу...", + "translationInformMe": "Паведаміце мне, калі мая мова мае патрэбу ў абнаўленнях", + "translationNoUpdateNeededText": "гэтая мова абноўлена; ура!", + "translationUpdateNeededText": "** гэтая мова патрабуе абнаўлення!! **", + "vrTestingText": "VR Тэстіраванне" + }, + "shareText": "Падзяліцца", + "sharingText": "Абагульванне...", + "showText": "Паказаць", + "signInForPromoCodeText": "Вы павінны ўвайсці, каб карыстацца кодамі.", + "signInWithGameCenterText": "Каб карыстацца акаўнтам Game Centerб\nувайдзіце з дапамогаю прыкладання Game Center.", + "singleGamePlaylistNameText": "Толькі ${GAME}", + "singlePlayerCountText": "1 гулец", + "soloNameFilterText": "Сола ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Выбар Героя", + "Chosen One": "Абраны", + "Epic": "Эпічны Рэжым", + "Epic Race": "Эпічная Гонка", + "FlagCatcher": "Захоп Сцяга", + "Flying": "Шчаслівыя Думкі", + "Football": "Футбол", + "ForwardMarch": "Напад", + "GrandRomp": "Заваяванне", + "Hockey": "Хакей", + "Keep Away": "Утрыманне", + "Marching": "Манёўр", + "Menu": "Галоўнае Меню", + "Onslaught": "Атака", + "Race": "Гонка", + "Scary": "Кароль Гары", + "Scores": "Табліца Вынікаў", + "Survival": "Ліквідацыя", + "ToTheDeath": "Смяротная Бойка", + "Victory": "Табліца Канчатковых Вынікаў" + }, + "spaceKeyText": "прабел", + "statsText": "Статыстыка", + "storagePermissionAccessText": "Для гэтага неабходны доступ да сховішча", + "store": { + "alreadyOwnText": "У вас ужо ёсць ${NAME}!", + "bombSquadProDescriptionText": "• Дублюе ўсе білеты, якія вы атрымліваеце\n• Выдаляе ўсю рэкламу з гульні\n• Утрымлівае ${COUNT} бонусных білетаў\n• +${PERCENT}% да вашага бонуса Лігі\n• Адкрывае кааператыўныя гульні\n '${INF_ONSLAUGHT}' і '${INF_RUNAROUND}' ", + "bombSquadProFeaturesText": "Асаблівасці:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Выдаляе гульнявую рэкламу і экраны\n• Адкрывае дадатковыя налады гульні\n• Таксама ўключае:", + "buyText": "Набыць", + "charactersText": "Персанажы", + "comingSoonText": "Хутка...", + "extrasText": "Дадаткова", + "freeBombSquadProText": "BombSquad зараз бясплатны, але калі вы набылі яго раней, зараз вы атрымліваеце\nBombSquad Pro і ${COUNT} білетаў у якасці падзякі.\nАтрымлівайце асалоду ад новых магчымасцей і вялікі дзякуй за вашу падтрымку!\n-Эрык", + "gameUpgradesText": "Паляпшэнне Гульні", + "holidaySpecialText": "Святочная Акцыя", + "howToSwitchCharactersText": "(зайдзіце ў \"${SETTINGS} -> ${PLAYER_PROFILES}\", каб наладзіць профілі)", + "howToUseIconsText": "(стварыце глабальны профіль (у раздзеле \"Акаўнт\"), каб скарыстацца гэтым)", + "howToUseMapsText": "(карыстайцеся гэтымі мапамі ў вашых гульнях)", + "iconsText": "Аватары", + "loadErrorText": "Немагчыма загрузіць старонку.\nПраверце інтэрнэт-злучэнне.", + "loadingText": "ладаванне", + "mapsText": "Мапы", + "miniGamesText": "Міні-Гульні", + "oneTimeOnlyText": "(толькі адзін раз)", + "purchaseAlreadyInProgressText": "Набыццё гэтага аб'екта ўжо выконваецца.", + "purchaseConfirmText": "Набыць ${ITEM}?", + "purchaseNotValidError": "Набыццё не адбылося.\nКалі гэта памылка, напішыце на ${EMAIL}.", + "purchaseText": "Набыць", + "saleBundleText": "Распродаж Камплектаў!", + "saleExclaimText": "Акцыя!", + "salePercentText": "(${PERCENT}% зніжка)", + "saleText": "АКЦЫЯ", + "searchText": "Шукаць", + "teamsFreeForAllGamesText": "Камандныя Гульні / Кожны Сам За Сябе", + "totalWorthText": "*** будет стоить вам ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Абнавіць?", + "winterSpecialText": "Зімняя Акцыя", + "youOwnThisText": "- вы маеце гэта -" + }, + "storeDescriptionText": "Гульнявое Вар'яцтва з 8 Гульцамі!\n\nПадрывайце вашых сяброў (ці камп'ютар) у турніры выбуховых міні-гульняў, такіх як Захоп Сцяга ці Эпічная Бойка Запаволенага Дзеяння!\n\nЗ простым кіраваннем і пашыранай падтрымкай кантролераў 8 чалавек могуць далучыцца да гульні, можна нават выкарыстоўваць мабільныя прылады як кантралёры праз бясплатнае прыкладанне 'BombSquad Remote'!\n\nУ атаку!\n\nГл. www.froemling.net/BombSquad для дадатковай інфармацыі.", + "storeDescriptions": { + "blowUpYourFriendsText": "Падарвіце вашых сяброў.", + "competeInMiniGamesText": "Спаборнічайце ў міні-гульнях ад гонак да палётаў.", + "customize2Text": "Наладжвайце персанажаў, міні-гульні і нават саўндтрэкі.", + "customizeText": "Наладзіць персанажаў і стварыць сваі плэйлісты з міні-гульнямі.", + "sportsMoreFunText": "Спорт у шмат разоў весялей з выбухамі!", + "teamUpAgainstComputerText": "Каманды супраць камп'ютара." + }, + "storeText": "Крама", + "submitText": "Адправіць", + "submittingPromoCodeText": "Адпраўка кода...", + "teamNamesColorText": "Назвы / колеры каманд ...", + "telnetAccessGrantedText": "Доступ Telnet уключаны.", + "telnetAccessText": "Знойдзены доступ Telnet, дазволіць?", + "testBuildErrorText": "Гэтая версія састарэла; калі ласка, праверце абнаўленні.", + "testBuildText": "Тэставая Зборка", + "testBuildValidateErrorText": "Не атрымліваецца праверыць тэставую зборку (няма інтэрнэту?)", + "testBuildValidatedText": "Тэставая Зборка Праверана; Поспехаў!", + "thankYouText": "Дзякуй за вашу падтрымку! Прыемнай гульні!!", + "threeKillText": "ТРЫ ЗАБОЙСТВЫ!!", + "timeBonusText": "Бонус Часу", + "timeElapsedText": "Прайшло Часу", + "timeExpiredText": "Час Скончыўся", + "timeSuffixDaysText": "${COUNT} дзён", + "timeSuffixHoursText": "${COUNT} гадзін", + "timeSuffixMinutesText": "${COUNT} мінут", + "timeSuffixSecondsText": "${COUNT} секунд", + "tipText": "Парада", + "titleText": "BombSquad", + "titleVRText": "${ARG1}", + "topFriendsText": "Топ Сяброў", + "tournamentCheckingStateText": "Праверка статусу турніра; калі ласка, пачакайце...", + "tournamentEndedText": "Гэты турнір скончыўся. Хутка пачнецца новы.", + "tournamentEntryText": "Уваход у Турнір", + "tournamentResultsRecentText": "Вынікі Нядаўніх Турніраў", + "tournamentStandingsText": "Месцы ў Турніры", + "tournamentText": "Турнір", + "tournamentTimeExpiredText": "Час Турніра Скончыўся", + "tournamentsText": "Турніры", + "translations": { + "characterNames": { + "Agent Johnson": "Агент 007", + "B-9000": "Ка-9000", + "Bernard": "Бернард", + "Bones": "Боўнс", + "Butch": "Буч", + "Easter Bunny": "Вялікдзеньскі Трусік", + "Flopsy": "Флопсі", + "Frosty": "Фросці", + "Gretel": "Грытэль", + "Grumbledorf": "Гэндальф", + "Jack Morgan": "Джэк Морган", + "Kronk": "Кронк", + "Lee": "Лі", + "Lucky": "Шчасліўчык", + "Mel": "Мэл", + "Middle-Man": "Сярэдні", + "Minimus": "Мінімус", + "Pascal": "Паскаль", + "Pixel": "Піксел", + "Sammy Slam": "Сэммі Слэм", + "Santa Claus": "Святы Мікалай", + "Snake Shadow": "Цень Змяі", + "Spaz": "Спаз", + "Taobao Mascot": "Таабаа Маскат", + "Todd": "Кевін", + "Todd McBurton": "Тод МакБартан", + "Xara": "Ксара", + "Zoe": "Зоя", + "Zola": "Зола" + }, + "coopLevelNames": { + "${GAME} Training": "Падрыхтоўка да гульні ${GAME}", + "Infinite ${GAME}": "Бясконцая ${GAME}", + "Infinite Onslaught": "Бясконцая Атака", + "Infinite Runaround": "Бясконцы Манеўр", + "Onslaught Training": "Атака: Трэніроўка", + "Pro ${GAME}": "${GAME} Профі", + "Pro Football": "Футбол Профі", + "Pro Onslaught": "Атака Профі", + "Pro Runaround": "Манёўр Профі", + "Rookie ${GAME}": "${GAME} Лёгкі", + "Rookie Football": "Футбол Лёгкі", + "Rookie Onslaught": "Атака Лёгкая", + "The Last Stand": "Апошняя Мяжа", + "Uber ${GAME}": "Убер ${GAME}", + "Uber Football": "Убер Футбол", + "Uber Onslaught": "Убер Атака", + "Uber Runaround": "Убер Манёўр" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Каб перамагчы, стань абраным на некаторы час.\nКаб стаць абраным, забей мінулага абранага.", + "Bomb as many targets as you can.": "Падарвіце як мага больш мішэняў.", + "Carry the flag for ${ARG1} seconds.": "Трымайце флаг у руках ${ARG1} секундаў.", + "Carry the flag for a set length of time.": "Трымайце флаг у руках на працягу некаторага часу.", + "Crush ${ARG1} of your enemies.": "Разбейце ${ARG1} ворага(ў).", + "Defeat all enemies.": "Перамажыце ўсех ворагаў.", + "Dodge the falling bombs.": "Ухіліцеся ад падаючых бомб.", + "Final glorious epic slow motion battle to the death.": "Апошняя эпічная смяротная бойка запаволенага дзеяння.", + "Gather eggs!": "Збярыце яйкі!", + "Get the flag to the enemy end zone.": "Перанясіце сцяг у зону абароны ворага.", + "How fast can you defeat the ninjas?": "Як хутка вы можаце перамагчы ніндзя?", + "Kill a set number of enemies to win.": "Забейце некаторую колькасць ворагаў, каб перамагчы.", + "Last one standing wins.": "Апошні жывы перамагае.", + "Last remaining alive wins.": "Перамагае апошні жывы.", + "Last team standing wins.": "Апошняя жывая каманда перамагае.", + "Prevent enemies from reaching the exit.": "Не дайце ворагам прайсці да выхада.", + "Reach the enemy flag to score.": "Дасягніце варожага сцяга, каб зарабіць ачкі.", + "Return the enemy flag to score.": "Перанясіце варожы сцяг на базу, каб зарабіць ачкі.", + "Run ${ARG1} laps.": "Прабяжыце ${ARG1} кругоў.", + "Run ${ARG1} laps. Your entire team has to finish.": "Прабяжыце ${ARG1} кругоў. Прыйсці на фініш павінна ўся каманда.", + "Run 1 lap.": "Прабяжыце 1 круг.", + "Run 1 lap. Your entire team has to finish.": "Прабяжыце 1 круг. Прыйсці на фініш павінна ўся каманда.", + "Run real fast!": "Бяжыце вельмі хутка!", + "Score ${ARG1} goals.": "Забейце ${ARG1} галоў.", + "Score ${ARG1} touchdowns.": "Зрабіце ${ARG1} тачдаўнаў.", + "Score a goal.": "Забейце гол.", + "Score a touchdown.": "Зрабіце тачдаўн.", + "Score some goals.": "Забейце некалькі галоў.", + "Secure all ${ARG1} flags.": "Захапіце ўсе ${ARG1} сцягоў.", + "Secure all flags on the map to win.": "Захапіце ўсе сцягі на мапе, каб перамагчы.", + "Secure the flag for ${ARG1} seconds.": "Захапіце сцяг на ${ARG1} секунд.", + "Secure the flag for a set length of time.": "Захапіце сцяг на пэўны час.", + "Steal the enemy flag ${ARG1} times.": "Скрадзіце варожы сцяг ${ARG1} разоў.", + "Steal the enemy flag.": "Скрадзіце варожы сцяг.", + "There can be only one.": "Можа быць толькі адзін.", + "Touch the enemy flag ${ARG1} times.": "Дакраніцеся да варожага сцяга ${ARG1} разоў.", + "Touch the enemy flag.": "Дакраніцеся да варожага сцяга.", + "carry the flag for ${ARG1} seconds": "трымайце флаг у руках ${ARG1} секундаў", + "kill ${ARG1} enemies": "забейце ${ARG1} ворагаў", + "last one standing wins": "апошні жывы перамагае", + "last team standing wins": "апошняя жывая каманда перамагае", + "return ${ARG1} flags": "перанясіце ${ARG1} сцягаў на базу", + "return 1 flag": "перанясіце сцяг на базу", + "run ${ARG1} laps": "прабяжыце ${ARG1} кругоў", + "run 1 lap": "прабяжыце 1 круг", + "score ${ARG1} goals": "забейце ${ARG1} галоў", + "score ${ARG1} touchdowns": "зрабіце ${ARG1} тачдаўнаў", + "score a goal": "забейце гол", + "score a touchdown": "зрабіце тачдаўн", + "secure all ${ARG1} flags": "захапіце ўсе ${ARG1} сцягоў", + "secure the flag for ${ARG1} seconds": "захапіце сцяг на ${ARG1} секунд", + "touch ${ARG1} flags": "дакраніцеся да ${ARG1} сцягаў", + "touch 1 flag": "дакраніцеся да сцяга" + }, + "gameNames": { + "Assault": "Напад", + "Capture the Flag": "Захоп Сцяга", + "Chosen One": "Абраны", + "Conquest": "Заваёва", + "Death Match": "Смяротная Бойка", + "Easter Egg Hunt": "Вялікдзеньскае Паляванне па Яйкі", + "Elimination": "Ліквідацыя", + "Football": "Футбол", + "Hockey": "Хакей", + "Keep Away": "Не падыходзіць!", + "King of the Hill": "Кароль Гары", + "Meteor Shower": "Метыярытны Дождж", + "Ninja Fight": "Бойка З Ніндзя", + "Onslaught": "Атака", + "Race": "Гонка", + "Runaround": "Абыход", + "Target Practice": "Стральба Па Мішэнях", + "The Last Stand": "Апошняя Мяжа" + }, + "inputDeviceNames": { + "Keyboard": "Клавіятура", + "Keyboard P2": "Клавіятура - Гулец 2" + }, + "languages": { + "Arabic": "Арабскi", + "Belarussian": "Беларуская", + "Chinese": "Кітайская спрошчаная", + "ChineseTraditional": "Кітайская традыцыйная", + "Croatian": "Харвацкая", + "Czech": "Чэшская", + "Danish": "Дацкая", + "Dutch": "Нямецкая", + "English": "Англійская", + "Esperanto": "Эсперанта", + "Finnish": "Фінская", + "French": "Французская", + "German": "Нямецкая", + "Gibberish": "Gibberish", + "Greek": "Грэчаскі", + "Hindi": "Хіндзі", + "Hungarian": "Венгерская", + "Indonesian": "Інданезійская", + "Italian": "Італьянская", + "Japanese": "Японская", + "Korean": "Карэйская", + "Persian": "Фарсі", + "Polish": "Польская", + "Portuguese": "Партугальская", + "Romanian": "Румынская", + "Russian": "Руская", + "Serbian": "Сербская", + "Slovak": "Славацкая", + "Spanish": "Гішпанская", + "Swedish": "Шведская", + "Turkish": "Турэцкі", + "Ukrainian": "Украінскі", + "Venetian": "Венецыянскі", + "Vietnamese": "В'етнамскі" + }, + "leagueNames": { + "Bronze": "Бронзавая", + "Diamond": "Алмазная", + "Gold": "Залатая", + "Silver": "Сярэбраная" + }, + "mapsNames": { + "Big G": "Вялікая G", + "Bridgit": "Брыджыт", + "Courtyard": "Двор", + "Crag Castle": "Замак на Скале", + "Doom Shroom": "Смяротны Грыб", + "Football Stadium": "Футбольны Стадыён", + "Happy Thoughts": "Воблачныя Думкі", + "Hockey Stadium": "Хакейны Стадыён", + "Lake Frigid": "Ледзяная Роўнядзь", + "Monkey Face": "Твар Малпы", + "Rampage": "Буянства", + "Roundabout": "Карусель", + "Step Right Up": "Лесвіца", + "The Pad": "Кілімок", + "Tip Top": "Ціп-Топ", + "Tower D": "Вежа D", + "Zigzag": "Зігзаг" + }, + "playlistNames": { + "Just Epic": "Толькі Эпічна", + "Just Sports": "Толькі Спорт" + }, + "scoreNames": { + "Flags": "Сцягі", + "Goals": "Галы", + "Score": "Ачкі", + "Survived": "Выжыванні", + "Time": "Час", + "Time Held": "Час Гульні" + }, + "serverResponses": { + "A code has already been used on this account.": "Прома-код ужо выкарыстоўваўся на гэтым акаўнце.", + "A reward has already been given for that address.": "За гэты адрас ужо ўручана ўзнагарода.", + "Account linking successful!": "Злучэнне акаўнтаў выканана!", + "Account unlinking successful!": "Ўліковы запіс паспяхова адключаны!", + "Accounts are already linked.": "Акаўнты ўжо злучаны.", + "An error has occurred; (${ERROR})": "Адбылася памылка; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Адбылася памылка; калі ласка, звяжыцеся са службай падтрымкі. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Узнікла памылка; звяжыцеся з support@froemling.net.", + "An error has occurred; please try again later.": "Адбылася памылка; калі ласка паспрабуйце зноў пазней.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Вы ўпэўнены, што жадаеце звязаць гэтыя акаўнты?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nГэта нельга будзе адмяніць!", + "BombSquad Pro unlocked!": "BombSquad Pro адкрыты!", + "Can't link 2 accounts of this type.": "Немагчыма звязаць 2 акаўнты гэтага тыпа.", + "Can't link 2 diamond league accounts.": "Нельга звязаць 2 акаўнты алмазнай лігі.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Злучэнне немагчыма; дасягнута максімальная колькасць злучэнняў (${COUNT}).", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Заўважана махлярства; ачкі і прызы забаронены на ${COUNT} дзён.", + "Could not establish a secure connection.": "Немагчыма стварыць бяспечнае злучэнне.", + "Daily maximum reached.": "Штодзённы максімум дасягнуты.", + "Entering tournament...": "Уваход у турнір...", + "Invalid code.": "Няправільны код.", + "Invalid payment; purchase canceled.": "Несапраўдная аплата; купля адменена.", + "Invalid promo code.": "Няправільны прома-код.", + "Invalid purchase.": "Памылка пры набыцці.", + "Invalid tournament entry; score will be ignored.": "Няправільны турнір; вынік не будзе залічаны.", + "Item unlocked!": "Элемент разблакаваны!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "ЗВЯЗАННЕ АДМЕНАВАНА. ${ACCOUNT} змяшчае\nважныя дадзеныя, якія ЎСЕ БУДУЦЬ СТРАЧЭНЫ.\nВы можаце зрабіць спасылку ў адваротным парадку, калі хочаце\n(і замест гэтага страціць дадзеныя ГЭТАГА ўліковага запісу)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Прывязаць уліковы запіс ${ACCOUNT} к гэтаму улiковаму запiсу?\nУсе існуючыя дадзеныя на ${ACCOUNT} будуць страчаны.\nГэта не можа быць адменена. Вы ўпэўнены?", + "Max number of playlists reached.": "Максімальная колькасць плэйлістаў дасягнута.", + "Max number of profiles reached.": "Максімальная колькасць профіляў дасягнута.", + "Maximum friend code rewards reached.": "Дасягнута максімальная ўзнагарода за код сябра.", + "Message is too long.": "Паведамленне занадта доўгае.", + "Profile \"${NAME}\" upgraded successfully.": "Профіль \"${NAME}\" палепшаны паспяхова.", + "Profile could not be upgraded.": "Профіль нельга палепшыць.", + "Purchase successful!": "Аб'ект набыты паспяхова!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Атрымана ${COUNT} квіткоў за ўваход.\nПрыходзьце заўтра, каб атрымаць яшчэ ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Функцыянальнасць сервера больш не падтрымліваецца ў гэтай версіі гульні;\nАбнавіце да новай версіі.", + "Sorry, there are no uses remaining on this code.": "Прабачце, код ужо выкарыстаны максімальную колькасць разоў.", + "Sorry, this code has already been used.": "Прабачце, гэты код ужо выкарыстоўваўся.", + "Sorry, this code has expired.": "На жаль, срок дзеяння гэтага кода ўжо скончыўся.", + "Sorry, this code only works for new accounts.": "Прабачце, гэты код працуе толькі на новых акаўнтах.", + "Temporarily unavailable; please try again later.": "Часова недаступны; калі ласка паспрабуйце зноў пазней.", + "The tournament ended before you finished.": "Турнір скончыўся перад тым, як вы закончылі.", + "This account cannot be unlinked for ${NUM} days.": "Немагчыма адлучыць гэты ўліковы запіс на працягу ${NUM} дзён.", + "This code cannot be used on the account that created it.": "Кодам нельга скарыстацца на акаўнце, які стварыў яго.", + "This requires version ${VERSION} or newer.": "Неабходна версія ${VERSION} гульні ці навей.", + "Tournaments disabled due to rooted device.": "Турніры адключаны з-за рутiраванай прылады.", + "Tournaments require ${VERSION} or newer": "Для турніраў патрабуецца ${VERSION} або больш позняя версія", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Адключыць ${ACCOUNT} ад гэтага ўліковага запісу?\nУсе дадзеныя на ${ACCOUNT} будуць скіданы.\n(за выключэннем дасягненняў у некаторых выпадках)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "УВАГА: на ваш уліковы запіс паступілі скаргі на ўзлом.\nБудуць забаронены ўліковыя запісы, якія будуць прызнаныя хакерскімі. Калі ласка, гуляйце сумленна.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Ці жадаеце вы аб'яднаць ваш акаўнт на прыладзе з гэтым?\n\nВаш акаўнт на прыладзе - ${ACCOUNT1}\nГэты акаўнт - ${ACCOUNT2}\n\nГэта дазволіць вам сінхранізіраваць прагрэс.\nАсцярожна - гэта нельга адмяніць!", + "You already own this!": "Вы ўжо маеце гэта!", + "You can join in ${COUNT} seconds.": "Вы можаце далучыцца праз ${COUNT} секунд.", + "You don't have enough tickets for this!": "У вас не хапае квіткоў!", + "You don't own that.": "Вы не валодаеце гэтым.", + "You got ${COUNT} tickets!": "Вы атрымалі ${COUNT} квіткоў!", + "You got a ${ITEM}!": "Вы атрымалі ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Вас павысілі і перавялі ў іншую лігу; віншуем!", + "You must update to a newer version of the app to do this.": "Вы павінны абнавіць гульню, каб зрабіць гэта.", + "You must update to the newest version of the game to do this.": "Для гэтага неабходна абнавіць да новай версіі гульні.", + "You must wait a few seconds before entering a new code.": "Пачакайце некалькі секунд, перад тым, як уводзіць новы код.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Ваш узровень у апошнім турніры: #${RANK}. Дзякуй за гульню!", + "Your account was rejected. Are you signed in?": "Ваш уліковы запіс быў адхілены. Вы ўвайшлі ў сістэму?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Ваша версія гульні была мадыфікавана.\nКалі ласка, адмяніце ўсе змены і паспрабуйце яшчэ раз.", + "Your friend code was used by ${ACCOUNT}": "${ACCOUNT} выкарыстаў ваш сяброўскі код" + }, + "settingNames": { + "1 Minute": "1 Мінута", + "1 Second": "1 Секунда", + "10 Minutes": "10 Мінут", + "2 Minutes": "2 мінуты", + "2 Seconds": "2 Секунды", + "20 Minutes": "20 Мінут", + "4 Seconds": "4 Секунды", + "5 Minutes": "5 Мінут", + "8 Seconds": "8 секунд", + "Allow Negative Scores": "Дазволiць Адмоўныя Вынікі", + "Balance Total Lives": "Размяркоўваць Здароўе, Якое Засталося", + "Bomb Spawning": "Стварэнне бомб", + "Chosen One Gets Gloves": "Абраны Атрымлівае Пальчаткі", + "Chosen One Gets Shield": "Абраны Атрымлівае Шчыт", + "Chosen One Time": "Час Абранага", + "Enable Impact Bombs": "Уключыць Ударныя Бомбы", + "Enable Triple Bombs": "Уключыць Патрайняльнік Бомб", + "Entire Team Must Finish": "Уся каманда павінна дайсці да фінішу", + "Epic Mode": "Эпічны Рэжым", + "Flag Idle Return Time": "Час Вяртання Кінутага Сцяга", + "Flag Touch Return Time": "Час Захопу Сцяга", + "Hold Time": "Час Утрымлівання", + "Kills to Win Per Player": "Забойствы - Перамагаючаму Гульцу", + "Laps": "Кругі", + "Lives Per Player": "Здароўя На Гульца", + "Long": "Доўга", + "Longer": "Даўжэй", + "Mine Spawning": "Мінаванне", + "No Mines": "Без Мін", + "None": "Не", + "Normal": "Нармальна", + "Pro Mode": "Складаны рэжым", + "Respawn Times": "Час да Ўваскрашэння", + "Score to Win": "Ачкоў для Перамогі", + "Short": "Коратка", + "Shorter": "Карачэй", + "Solo Mode": "Сола-Рэжым", + "Target Count": "Мішэняў Адначасова", + "Time Limit": "Ліміт Часу" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} дыскваліфікаваны, таму што ${PLAYER} выйшаў", + "Killing ${NAME} for skipping part of the track!": "Ліквідацыя ${NAME} за скарачэнне трасы!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Папярэджанне для ${NAME}: турба / спам-кнопкі выб'е вас з ладу." + }, + "teamNames": { + "Bad Guys": "Злодзеі", + "Blue": "Блакітныя", + "Good Guys": "Героі", + "Red": "Чырвоныя" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Своечасовая паслядоўнасць \"Бег-Скачок-Кручэнне-Удар\" можа забіць\nадным ударам і даць вам пажыццёвую павагу вашых сяброў.", + "Always remember to floss.": "Не забывайце карыстацца зубной ніткай.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Стварайце профілі для сябе і вашых сяброў з асабістымі імёнамі і\nзнешнім выглядам замест таго, каб карыстацца выпадковымі.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Скрыні Смерці хутка ператвараюць вас у бомбу.\nАдзіны спосаб пазбавіцца ад гэтага - схапіць аптэчку.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Незалежна ад знешняга выгляду, здольнасці ўсіх персанажаў \nаднолькавыя, таму выбіраеце тых, на каго вы больш падобныя.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Не будзьце занадта дзёрзкім с гэтым энэргетычным шчытом; вас усё яшчэ можна скінуць з абрыва.", + "Don't run all the time. Really. You will fall off cliffs.": "Не бегай увесь час. Сур'ёзна. Звалішся з абрыва.", + "Don't spin for too long; you'll become dizzy and fall.": "Не круціцеся занадта доўга; у вас закружыцца галава і вы ўпадзе.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Націсніце любую кнопку, каб бяжаць. (трыгеры вельмі зручныя для гэтага)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Націсніце любую кнопку, каб бяжаць. Канечне, вы будзеце \nрухацца хутчэй. Але не забывайце пра абрывы!", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ледзяныя бомбы не вельмі моцныя, але яны спыняюць усіх\nнавокал, робячы іх безабароненымі і далікатнымі.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Калі хтосьці вас схапіў, бейце яго, і ён адпусціць.\nГэта таксама працуе ў рэальным жыцці.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Калі ў вас няма кантролераў, усталюйце прыкладанне ${REMOTE_APP_NAME} на \nвашу мабільную прыладу, каб карыстацца ёю ў якасці кантролера.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Калі вам не хапае кантролераў, усталюйце прыкладанне 'BombSquad Remote'\nна iOS або Android прылады, каб карыстацца імі, як кантролерамі.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Калі вы злавілі бомбу-ліпучку, прыгайце і бегайце. Магчыма, вам пашанцуе, і вы \nскінеце яе. У адваротным выпадку вы хоць павесяліце сяброў.", + "If you kill an enemy in one hit you get double points for it.": "Калі вы заб'еце ворага адным ударам, вам дадуць у два разы больш ачкоў.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Калі вы падхапілі смяротны бонус, адзіная надзея - знайсці\nаптэчку ў бліжэйшыя некалькі секундаў.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Не стой на месцы - падсмажышся! Бягі і старайся выжыць.", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Калі ў вашым лоббі гуляе многа людзей, уключыце \"Аўтаматычнае выкідванне пры\nпрастоі\" у наладах. Магчыма, нехта з гульцоў забыў выйсці.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Калi ваша прылада награваецца цi вы жадаеце захаваць запад батарэi,\nпаменьшыце \"Вiзуальныя эфекты\" цi \"Разрашэнне\" ý Налады->Графiка.", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Калi карцiнка перарывiстая, паспрабуйце паменьшыць разрэшэнне\nцi графiку ý наладах графiкi ý гульне.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "У Захопе Сцяга ваш сцяг павiнен быць на вашай базе, каб захапiць чужы, калi чужая \nкаманда амаль-што захапiла ваш сцяг, добрым вырашэннем iх спынення будзе схапiць iх сцяг.", + "In hockey, you'll maintain more speed if you turn gradually.": "У хакее магчыма падтрымлiваць большую хуткасць, калi паварачваць паступова.", + "It's easier to win with a friend or two helping.": "Лягчэй перамагчы з адным цi дзвума сябрамi.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Скокнi адразу парад кiдком бомбы,каб закiнуць яе як мага вышэй.", + "Land-mines are a good way to stop speedy enemies.": "Мiны - добры спосаб спынiць хуткiх ворагаý.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Многа рэчаý магчыма ýзняць i кiнуць, уключна другiх гулбцоý. Шпурлянне \nвашых ворагаý з абрыва можа зрабiцца эфектыýнай i падбадзёрлiвай стратэгiяй.", + "No, you can't get up on the ledge. You have to throw bombs.": "Не, у вас не атрымаецца ýзлезцi на выступ. Вам патрэбна кiдаць бомбы.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Гульцы могуць далучацца i зыходзiць ý сярэдзiне многiх гульняý, \nтаксама вы можаце падключаць i адключаць кантролеры прам на ляту.", + "Practice using your momentum to throw bombs more accurately.": "Папрактыкуйцеся карыстацца патрэбным момантам для кiдання бомб больш акуратна.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Удары робяць тым больш урону, чым хутчэй вы бяжыце перад iмi,\nТак што паспрабуйце бегчы, скакаць i круцiцца як вар'ят.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Прабяжыце назад i павернiцеся перад кiдком \nбомбы для таго, каб махнуць ёй i кiнуць далей.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Забейце некалькi злодзеяý, кiнуýшы \nбомбу недалёка ад TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Галава - самае ўразлівае месца, так што ліпучая бомба\nпа галаве, як правіла, значыць капут.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Гэты ўзровень бясконцы, але высокія ачкі тут\nзаробяць вам адвечную павагу ва ўсім свеце.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Моц кідку залежыць ад накірунку, які націснут. Каб акуратна \nкінуць што-небудзь прама перад сабой, не націскакйце ні ў якім накірунку.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Надакучыў саундтрэк? Змяніце яго на свой!\nГлядзіце Налады->Аўдыя->Саундтрэкі", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Паспрабуйце \"Падцяпліць\" бомбы секунду ці дзве перад тым, як кінуць іх.", + "Try tricking enemies into killing eachother or running off cliffs.": "Паспрабуйце падмануць ворагаў, каб яны забілі адзін аднаго ці саскочылі з абрыва.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Карыстайцеся кнопкай выбару (трохвугольнік), каб схапіць сцяг < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Узмахніце туды-сюды, каб закінуць далей", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Вы можаце накіроўваць удары, круцячыся ў правы ці ў левы бок.\nГэта карысна для спіхвання злодзеяў з краю або для галоў у хакее", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Час выбуху бомбы магчыма адгадаць па колеры іскраў ад кнота:\nжоўты..аранжавы..чырвоны..БАБАХ.", + "You can throw bombs higher if you jump just before throwing.": "Бомбу магчыма кінуць вышэй, калі падскочыць адразу перад кідком", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Вы атрымліваеце страты, калі б'ецеся галавой,\nтак што беражыце галаву", + "Your punches do much more damage if you are running or spinning.": "Вашы ўдары робяцца мацней, калі вы бяжыце ці кружыцеся" + } + }, + "trophiesRequiredText": "Для гэтага патрэбны мінімум ${NUMBER} трафеяў.", + "trophiesText": "Трафеі", + "trophiesThisSeasonText": "Трафеі за Гэты Сезон", + "tutorial": { + "cpuBenchmarkText": "Прагон туторыяла з немагчымай хуткасцю (нагружае працэсар)", + "phrase01Text": "Прывітанне!", + "phrase02Text": "Прывітанне ў ${APP_NAME}!", + "phrase03Text": "Вось некалькі парад для кантролю вашага героя:", + "phrase04Text": "Мноства рэчаў у ${APP_NAME} заснавана на фізіцы.", + "phrase05Text": "Напрыклад, калі вы ўдарыце,..", + "phrase06Text": "..страты будуць залежыць ад вашай хуткасці.", + "phrase07Text": "Бачылі? Мы не рухаліся, таму ледзь ударылі ${NAME}.", + "phrase08Text": "Цяпер давайце прыгаць, каб рухацца яшчэ хутчэй.", + "phrase09Text": "О, так лепш.", + "phrase10Text": "Бег таксама дапамагае.", + "phrase11Text": "Націсніце ЛЮБУЮ кнопку, каб бяжаць.", + "phrase12Text": "Каб нанесці вельмі вялікія страты, спрабуйце бяжаць І прыгаць.", + "phrase13Text": "Упс; ${NAME}, прабач за гэта.", + "phrase14Text": "Вы можаце падымаць і кідаць рэчы, напрыклад сцягі.. ці ${NAME}.", + "phrase15Text": "Нарэшце, гэта бомбы.", + "phrase16Text": "Кіданне бомб патрабуе практыкі.", + "phrase17Text": "Оў! Не вельмі добры кідок.", + "phrase18Text": "Рух дапамагае вам кідаць далей.", + "phrase19Text": "Скачкі дапамагаюць вам кідаць вышэй.", + "phrase20Text": "\"Закручаныя\" бомбы ляцяць яшчэ далей.", + "phrase21Text": "\"Падцяпліць\" бомбу даволі складана.", + "phrase22Text": "Міма.", + "phrase23Text": "Паспрабуйце \"падцяпліць\" бомбу секунду ці дзве.", + "phrase24Text": "Ура! Добра падцяплілі!", + "phrase25Text": "Гэта, мабыць, усё.", + "phrase26Text": "Наперад, на міны!", + "phrase27Text": "Памятайце сваю трэніроўку, і вы вернецеся жывым!", + "phrase28Text": "...магчыма...", + "phrase29Text": "Поспехаў!", + "randomName1Text": "Фрэд", + "randomName2Text": "Гары", + "randomName3Text": "Біл", + "randomName4Text": "Чак", + "randomName5Text": "Філ", + "skipConfirmText": "Сапраўды прапусціць туторыял? Націсніце, каб пацвердзіць.", + "skipVoteCountText": "${COUNT}/${TOTAL} галасоў за пропуск", + "skippingText": "пропуск туторыяла...", + "toSkipPressAnythingText": "(націсніце, каб прапусціць туторыял)" + }, + "twoKillText": "ДВА ЗАБОЙСТВЫ!", + "unavailableText": "недаступна", + "unconfiguredControllerDetectedText": "Невядомы кантролер знойдзены:", + "unlockThisInTheStoreText": "Гэта павінна быць адкрыта ў магазіне.", + "unlockThisProfilesText": "Каб стварыць больш за ${NUM} профіляў, вам трэба:", + "unlockThisText": "Каб адкрыць гэта, вам патрэбна:", + "unsupportedHardwareText": "Прабачце, ваша прылада не падтрымлівае гэтую версію гульні.", + "upFirstText": "Спачатку:", + "upNextText": "Далей у гульні ${COUNT}:", + "updatingAccountText": "Абнаўленне вашага акаўнта...", + "upgradeText": "Палепшыць", + "upgradeToPlayText": "Адкрыйце \"${PRO}\" у магазіне, каб гуляць у гэта.", + "useDefaultText": "Вярнуць Стандартныя", + "usesExternalControllerText": "Гэта гульня можа выкарыстоўваць знешні кантролер для кіравання.", + "usingItunesText": "Выкарыстанне музычнага прыкладання для саўндтрэка ...", + "usingItunesTurnRepeatAndShuffleOnText": "Калі ласка, праверце, што ператасаванне і паўтор усяго ў iTunes ўключаны. ", + "validatingTestBuildText": "Праверка Тэставай Зборкі...", + "victoryText": "Перамога!", + "voteDelayText": "Вы не можаце пачаць яшчэ адно галасаванне на працягу ${NUMBER} секунд", + "voteInProgressText": "Галасаванне ўжо ідзе.", + "votedAlreadyText": "Вы ўжо прагаласавалі", + "votesNeededText": "Патрэбна ${NUMBER} галасоў", + "vsText": "супраць", + "waitingForHostText": "(чакаем ${HOST}, каб працягнуць)", + "waitingForPlayersText": "чакаем гульцоў...", + "waitingInLineText": "Чакаем у чарзе (вечарынка поўная)...", + "watchAVideoText": "Глядзіце відэа", + "watchAnAdText": "Глядзець Рэкламу", + "watchWindow": { + "deleteConfirmText": "Выдаліць \"${REPLAY}\"?", + "deleteReplayButtonText": "Выдаліць\nЗапіс", + "myReplaysText": "Мае Відэазапісы", + "noReplaySelectedErrorText": "Запіс Не Выбраны", + "playbackSpeedText": "Хуткасць прайгравання: ${SPEED}", + "renameReplayButtonText": "Перайменаваць\nЗапіс", + "renameReplayText": "Перайменаваць \"${REPLAY}\" на:", + "renameText": "Перайменаваць", + "replayDeleteErrorText": "Памылка пры выдаленні.", + "replayNameText": "Імя Запіса", + "replayRenameErrorAlreadyExistsText": "Запіс с такім імем ужо існуе.", + "replayRenameErrorInvalidName": "Нельга перайменаваць запіс; немагчымае імя.", + "replayRenameErrorText": "Памылка пры перайменаванні запіса.", + "sharedReplaysText": "Агульныя Запісы", + "titleText": "Глядзець", + "watchReplayButtonText": "Глядзець\nЗапіс" + }, + "waveText": "Хваля", + "wellSureText": "Выдатна!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Слуханне Wiimotes...", + "pressText": "Адначасова націсніце кнопкі 1 і 2.", + "pressText2": "На новых Wiimotes з убудаваным Motion Plus, націсніце чырвоную кнопку 'sync'." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "listenText": "Слухаць", + "macInstructionsText": "Пераканайцеся, што ваш Wii выключаны, а на вашым камп'ютары\nўключаны Bluetooth, а затым націсніце «Слухаць». Падтрымка \nWiimote можа быць трохі няправільная, так што вам, магчыма, прыйдзецца \nпаспрабаваць некалькі разоў, перш чым вы атрымаеце злучэнне.\n\nBluetooth павінны апрацоўваць да 7 падлучаных прылад,\nале нэта можа вар'іравацца.\n\nBombSquad падтрымлівае арыгінальныя Wiimotes, Nunchuks,\nі класічны кантролер.\nНовая Wii Remote Plus ў цяперашні час працуе добра,\nале не з ўкладаннямі.", + "thanksText": "Дзякуй камандзе DarwiinRemote\nза то, што зрабілі гэта магчымым.", + "titleText": "Налады Wiimote" + }, + "winsPlayerText": "${NAME} Перамагае!", + "winsTeamText": "${NAME} Перамагаюць!", + "winsText": "${NAME} Перамагае!", + "worldScoresUnavailableText": "Сусветныя вынікі недаступны.", + "worldsBestScoresText": "Лепшыя Сусветныя Вынікі", + "worldsBestTimesText": "Лепшы Сусветны Час", + "xbox360ControllersWindow": { + "getDriverText": "Атрымаць Драйвер", + "macInstructions2Text": "Каб выкарыстоўваць бесправадныя кантролеры, вам таксама патрэбен \nпрыёмнік, які пастаўляецца з «Xbox 360 Wireless Controller для Windows\".\nАдзін прыёмнік дазваляе падключыць да 4 кантролераў.\n\nВажна: 3-ія прыёмнікі не будуць працаваць з гэтым драйверам;\nпераканайцеся, што на вашым прыёмніке напісана \"Microsoft\", а не \n«XBOX 360». Microsoft больш не прадае іх асобна, так што вам\nтрэба набыць камплект, альбо шукаць кантролеры на Ebay.\n\nКалі вы знойдзеце гэта карысным, калі ласка, разгледзіце ахвяраванне\nраспрацоўшчыку драйвераў на ягоным сайце.", + "macInstructionsText": "Для выкарыстання кантролераў Xbox 360, вы павінны будзеце ўсталяваць\nдрайвер для Mac, даступны па спасылцы ніжэй.\nГэта працуе як з праваднымі, так і з бесправаднымі кантролерамі.", + "ouyaInstructionsText": "Для выкарыстання правадных кантролераў Xbox 360 з BombSquad, проста\nпадключыце іх да USB-порту вашай прылады. Вы можаце выкарыстоўваць канцэнтратар\nUSB для падлучэння некалькіх кантролераў.\n\nДля выкарыстання бесправадных кантролераў вам патрэбен бесправадной прыёмнік,\nякі даступны як частка пакета \"Бесправадной кантролер Xbox 360 для Windows\"\nабо прадаецца асобна. Кожны прыёмнік падключаецца да порта USB і\nдазваляе падключыць да 4 бесправадных кантролераў.", + "titleText": "Выкарыстоўванне кантролераў Xbox 360 з ${APP_NAME}:" + }, + "yesAllowText": "Так, Дазволіць!", + "yourBestScoresText": "Вашыя Лепшыя Вынікі", + "yourBestTimesText": "Ваш Лепшы Час" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/chinese.json b/dist/ba_data/data/languages/chinese.json new file mode 100644 index 0000000..b9bf744 --- /dev/null +++ b/dist/ba_data/data/languages/chinese.json @@ -0,0 +1,1869 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "账户名称不能包含Emoji表情符号或其他特殊符号!", + "accountProfileText": "(账户资料)", + "accountsText": "账户", + "achievementProgressText": "完成了${TOTAL}个成就中的${COUNT}个", + "campaignProgressText": "战役进程 [困难] :${PROGRESS}", + "changeOncePerSeason": "在每个赛季中你只能更改它一次。", + "changeOncePerSeasonError": "你需要等到下个赛季才能对它再次更改 (还有${NUM}天)", + "customName": "玩家姓名", + "linkAccountsEnterCodeText": "输入代码", + "linkAccountsGenerateCodeText": "生成代码", + "linkAccountsInfoText": "(在不同的平台上同步游戏进程)", + "linkAccountsInstructionsNewText": "要关联两个帐户,首先点“生成代码”\n,在第二设备点“输入代码”输入。\n两个帐户数据将被两者共享。\n\n您最多可以关联${COUNT}个帐户。\n(包括自己的账户)\n\n最好只关联自己的用户,\n避免对方在30天之后\n取消你的关联,造成损失。", + "linkAccountsInstructionsText": "若要关联两个账户,在其中一个账户内\n生成一个代码,用以在另一个账户内输入。\n游戏进程和物品将会被合并。\n您最多可以关联${COUNT}个账户\n\n重要:只能关联您自己的帐户!\n如果您跟您的朋友关联帐户\n您将无法在同一时间玩\n\n另外:此操作目前不能撤销,所以要小心!", + "linkAccountsText": "关联账户", + "linkedAccountsText": "已关联的账户:", + "nameChangeConfirm": "更改账户名称为${NAME}?", + "notLoggedInText": "未登录", + "resetProgressConfirmNoAchievementsText": "这样会重置单机模式的进程和\n本地的高分记录(并不包括你的点券),\n而且不能恢复,确定要这样做吗?", + "resetProgressConfirmText": "这样会重置单机模式的进程,\n成就和本地的高分记录\n(并不包括你的点券),而且不能\n恢复,确定要这样做吗?", + "resetProgressText": "重置游戏进程", + "setAccountName": "设置账户名称", + "setAccountNameDesc": "选择要为您的帐户显示的名称。\n您可以从链接的帐户选择\n或创建唯一的自定义名称。", + "signInInfoText": "登陆以获取点券, 在线竞赛,\n并在不同设备上同步游戏进程。", + "signInText": "登陆", + "signInWithDeviceInfoText": "(仅适用于此设备的一个自动账户)", + "signInWithDeviceText": "用设备账户来登陆", + "signInWithGameCircleText": "使用 Game Circle 登入", + "signInWithGooglePlayText": "用 Google Play 来登陆", + "signInWithTestAccountInfoText": "使用设备上的其他网络帐号登录;不建议选择该项", + "signInWithTestAccountText": "用测试账户来登陆", + "signOutText": "登出", + "signingInText": "登录中...", + "signingOutText": "登出中...", + "testAccountWarningOculusText": "警告:你正在使用“测试”账户登录。\n此账户会在今年晚些时候被Oculus账户代替,\n并将会提供票券购买功能和其他的功能。\n\n现在你只能在游戏中赢取票券。\n(然而,你可以免费升级到BombSquad的专业版本)", + "testAccountWarningText": "警告:你正在使用“测试”账户登录。\n这个账户是与现在这一台设备绑定的,\n并且可能会被周期性地重置数据。(所以\n请不要花太长的时间来解锁或收集物品)\n\n运行本游戏的正式版来使用一个\"真正的\"账户\n(Game-Center或Google Plus账户等等)。\n这样做能将你的数据存储在云端并且在不同的\n设备间共享。\n", + "ticketsText": "点券:${COUNT}", + "titleText": "账号", + "unlinkAccountsInstructionsText": "选择要取消关联的帐户", + "unlinkAccountsText": "取消连结帐户", + "viaAccount": "(不可用名称 ${NAME})", + "youAreLoggedInAsText": "您已登录为", + "youAreSignedInAsText": "你已登录为:" + }, + "achievementChallengesText": "成就挑战", + "achievementText": "成就", + "achievements": { + "Boom Goes the Dynamite": { + "description": "用 TNT 炸死三个坏蛋", + "descriptionComplete": "用 TNT 炸死了三个坏蛋", + "descriptionFull": "在${LEVEL}中用TNT炸死三个坏蛋", + "descriptionFullComplete": "在${LEVEL}中用 TNT 炸死了三个坏蛋", + "name": "发威吧!TNT!" + }, + "Boxer": { + "description": "不使用任何炸弹获得胜利", + "descriptionComplete": "没有使用任何炸弹就获胜了", + "descriptionFull": "在${LEVEL}中不用炸弹获胜", + "descriptionFullComplete": "在${LEVEL}中没用炸弹就获胜了", + "name": "拳王" + }, + "Dual Wielding": { + "descriptionFull": "连接两个控制手柄(硬件或应用)(耳机按钮可达到链接效果)", + "descriptionFullComplete": "已经连接两个控制手柄(硬件或应用)", + "name": "成双成对" + }, + "Flawless Victory": { + "description": "毫发无损地获胜", + "descriptionComplete": "毫发无损地获胜了", + "descriptionFull": "在${LEVEL}中毫发无损地获胜", + "descriptionFullComplete": "在${LEVEL}中毫发无损地获胜了", + "name": "完美获胜" + }, + "Free Loader": { + "descriptionFull": "开始一个“混战模式”游戏(2+玩家)", + "descriptionFullComplete": "已经开始一个“混战模式”游戏(2+玩家)", + "name": "揩油的人" + }, + "Gold Miner": { + "description": "用地雷杀死6个坏蛋", + "descriptionComplete": "用地雷杀死了6个坏蛋", + "descriptionFull": "在${LEVEL}中用地雷杀死6个坏蛋", + "descriptionFullComplete": "在${LEVEL}中用地雷杀死了6个坏蛋", + "name": "地雷专家" + }, + "Got the Moves": { + "description": "不用炸弹或拳头攻击就获胜", + "descriptionComplete": "没有用炸弹或拳头攻击就获胜了", + "descriptionFull": "在${LEVEL}中不用炸弹或拳头攻击就获胜", + "descriptionFullComplete": "在${LEVEL}中没有用炸弹或拳头攻击就获胜了", + "name": "跑位是关键" + }, + "In Control": { + "descriptionFull": "连接一个控制手柄(硬件或应用)", + "descriptionFullComplete": "已经连接一个控制手柄(硬件或应用)", + "name": "掌控之中" + }, + "Last Stand God": { + "description": "得1000分", + "descriptionComplete": "得了1000分", + "descriptionFull": "在${LEVEL}中获得1000分", + "descriptionFullComplete": "在${LEVEL}中获得了1000分", + "name": "${LEVEL}之神" + }, + "Last Stand Master": { + "description": "得250分", + "descriptionComplete": "得了250分", + "descriptionFull": "在${LEVEL}中获得250分", + "descriptionFullComplete": "在${LEVEL}中获得了250分", + "name": "${LEVEL}的大师" + }, + "Last Stand Wizard": { + "description": "得了500分", + "descriptionComplete": "得了500分", + "descriptionFull": "在${LEVEL}中获得500分", + "descriptionFullComplete": "在${LEVEL}中获得了500分", + "name": "${LEVEL}的行家" + }, + "Mine Games": { + "description": "用地雷炸死3个坏蛋", + "descriptionComplete": "用地雷炸死了3个坏蛋", + "descriptionFull": "在${LEVEL}中用地雷炸死3个坏蛋", + "descriptionFullComplete": "在${LEVEL}中用地雷炸死了3个坏蛋", + "name": "地雷战" + }, + "Off You Go Then": { + "description": "把3个坏蛋扔出地图", + "descriptionComplete": "把3个坏蛋扔出地图", + "descriptionFull": "在${LEVEL}中把3个坏蛋扔出地图", + "descriptionFullComplete": "在${LEVEL}中把3个坏蛋扔出地图", + "name": "现在到你了" + }, + "Onslaught God": { + "description": "得5000分", + "descriptionComplete": "得了5000分", + "descriptionFull": "在${LEVEL}中获得5000分", + "descriptionFullComplete": "在${LEVEL}中获得了5000分", + "name": "${LEVEL}之神" + }, + "Onslaught Master": { + "description": "得500分", + "descriptionComplete": "得了500分", + "descriptionFull": "在${LEVEL}中得500分", + "descriptionFullComplete": "在${LEVEL} 中得到500分", + "name": "${LEVEL} 专家" + }, + "Onslaught Training Victory": { + "description": "打败所有敌人", + "descriptionComplete": "打败了所有敌人", + "descriptionFull": "在${LEVEL} 上打败所有敌人", + "descriptionFullComplete": "在${LEVEL} 上打败了所有敌人", + "name": "${LEVEL} 胜利" + }, + "Onslaught Wizard": { + "description": "得1000分", + "descriptionComplete": "得了1000分", + "descriptionFull": "在${LEVEL} 中得1000分", + "descriptionFullComplete": "在${LEVEL} 中得到了1000分", + "name": "${LEVEL} 之圣" + }, + "Precision Bombing": { + "description": "一个工具箱都不捡就取得胜利", + "descriptionComplete": "一个工具箱都不捡就取得了胜利", + "descriptionFull": "在${LEVEL} 中一个工具箱都不捡就取得胜利", + "descriptionFullComplete": "在${LEVEL} 中一个工具箱都不捡就取得了胜利", + "name": "精确爆炸" + }, + "Pro Boxer": { + "description": "不用任何炸弹就取胜", + "descriptionComplete": "不用任何炸弹就取胜了", + "descriptionFull": "在${LEVEL} 中不用炸弹就取胜", + "descriptionFullComplete": "在${LEVEL} 中不用炸弹就取胜了", + "name": "专业拳击手" + }, + "Pro Football Shutout": { + "description": "完爆坏人队(不让坏人队得分)", + "descriptionComplete": "完爆了坏人队(不让坏人队得分)", + "descriptionFull": "在${LEVEL} 中完爆坏人队(不让坏人队得分)", + "descriptionFullComplete": "在${LEVEL} 中完爆了坏人队(不让坏人队得分)", + "name": "无懈可击的${LEVEL}" + }, + "Pro Football Victory": { + "description": "赢得比赛", + "descriptionComplete": "赢得了比赛", + "descriptionFull": "赢得${LEVEL}", + "descriptionFullComplete": "赢得了${LEVEL}", + "name": "${LEVEL} 获胜" + }, + "Pro Onslaught Victory": { + "description": "打败所有敌人", + "descriptionComplete": "打败了所有敌人", + "descriptionFull": "在${LEVEL} 中打败所有坏蛋", + "descriptionFullComplete": "在${LEVEL} 中打败了所有坏蛋", + "name": "${LEVEL} 获胜" + }, + "Pro Runaround Victory": { + "description": "打败所有敌人", + "descriptionComplete": "打败了所有敌人", + "descriptionFull": "在${LEVEL} 中打败所有坏蛋", + "descriptionFullComplete": "在${LEVEL} 中打败了所有坏蛋", + "name": "${LEVEL} 取胜" + }, + "Rookie Football Shutout": { + "description": "完爆坏人队(不让坏人队得分)", + "descriptionComplete": "完爆了坏人队", + "descriptionFull": "在${LEVEL} 中完爆坏人队", + "descriptionFullComplete": "在${LEVEL} 中完爆了坏人队", + "name": "${LEVEL} 完胜" + }, + "Rookie Football Victory": { + "description": "赢得比赛", + "descriptionComplete": "赢得了比赛", + "descriptionFull": "赢得${LEVEL}", + "descriptionFullComplete": "赢得了${LEVEL}", + "name": "${LEVEL} 获胜" + }, + "Rookie Onslaught Victory": { + "description": "打败所有敌人", + "descriptionComplete": "打败了所有敌人", + "descriptionFull": "在${LEVEL} 中打败所有坏蛋", + "descriptionFullComplete": "在${LEVEL} 中打败了所有坏蛋", + "name": "${LEVEL} 获胜" + }, + "Runaround God": { + "description": "得2000分", + "descriptionComplete": "得了2000分", + "descriptionFull": "在${LEVEL} 中得到2000分", + "descriptionFullComplete": "在${LEVEL} 中得到了2000分", + "name": "${LEVEL} 之神" + }, + "Runaround Master": { + "description": "得500分", + "descriptionComplete": "得了500分", + "descriptionFull": "在${LEVEL} 中得到500分", + "descriptionFullComplete": "在${LEVEL} 中得到了500分", + "name": "${LEVEL} 大师" + }, + "Runaround Wizard": { + "description": "得1000分", + "descriptionComplete": "得了1000分", + "descriptionFull": "在${LEVEL} 得到1000分", + "descriptionFullComplete": "在${LEVEL} 中得到了1000分", + "name": "${LEVEL} 之圣" + }, + "Sharing is Caring": { + "descriptionFull": "成功和一个朋友分享游戏", + "descriptionFullComplete": "已经成功和一个朋友分享游戏", + "name": "独享不如分享" + }, + "Stayin' Alive": { + "description": "一直活着赢得比赛", + "descriptionComplete": "一直活着赢得了比赛", + "descriptionFull": "在${LEVEL} 中不死赢得比赛", + "descriptionFullComplete": "在${LEVEL} 中不死赢得了比赛", + "name": "不死之身" + }, + "Super Mega Punch": { + "description": "一拳造成100%的伤害", + "descriptionComplete": "一拳造成了100%的伤害", + "descriptionFull": "一拳造成了100%的伤害", + "descriptionFullComplete": "在${LEVEL}中一拳造成了100%的伤害", + "name": "无敌大拳头" + }, + "Super Punch": { + "description": "一拳造成50%的伤害", + "descriptionComplete": "一拳造成了50%的伤害", + "descriptionFull": "在${LEVEL}中一拳造成了50%的伤害", + "descriptionFullComplete": "在${LEVEL}中一拳造成50%的伤害", + "name": "大拳头" + }, + "TNT Terror": { + "description": "用TNT炸死6个坏蛋", + "descriptionComplete": "用TNT炸死了6个坏蛋", + "descriptionFull": "在${LEVEL}中用TNT炸死6个坏蛋", + "descriptionFullComplete": "在${LEVEL}中用TNT炸死了6个坏蛋", + "name": "恐怖TNT" + }, + "Team Player": { + "descriptionFull": "开始一个“团队”游戏(4+玩家)", + "descriptionFullComplete": "已经开始一个“团队”游戏(4+玩家)", + "name": "团队玩家" + }, + "The Great Wall": { + "description": "阻止所有坏蛋通过", + "descriptionComplete": "阻止了所有坏蛋", + "descriptionFull": "在${LEVEL}中阻止所有坏蛋通过", + "descriptionFullComplete": "在${LEVEL}中阻止了所有坏蛋", + "name": "铜墙铁壁" + }, + "The Wall": { + "description": "阻止所有坏蛋通过", + "descriptionComplete": "阻止了所有的坏蛋", + "descriptionFull": "在${LEVEL}中阻止所有坏蛋通过", + "descriptionFullComplete": "在${LEVEL}中阻止了所有的坏蛋", + "name": "铜墙铁壁" + }, + "Uber Football Shutout": { + "description": "不失分赢得比赛", + "descriptionComplete": "不失分赢得了比赛", + "descriptionFull": "在${LEVEL} 中不失分赢得比赛", + "descriptionFullComplete": "在${LEVEL} 中不失分赢得了比赛", + "name": "${LEVEL} 完胜" + }, + "Uber Football Victory": { + "description": "赢得比赛", + "descriptionComplete": "赢得比赛", + "descriptionFull": "在${LEVEL}中获胜", + "descriptionFullComplete": "在${LEVEL}中获胜", + "name": "${LEVEL} 获胜" + }, + "Uber Onslaught Victory": { + "description": "防御所有坏蛋", + "descriptionComplete": "防御了所有坏蛋", + "descriptionFull": "在 ${LEVEL} 中防御所有坏蛋", + "descriptionFullComplete": "在 ${LEVEL} 中防御了所有坏蛋", + "name": "${LEVEL} 获胜" + }, + "Uber Runaround Victory": { + "description": "打败所有敌人", + "descriptionComplete": "打败了所有敌人", + "descriptionFull": "在 ${LEVEL} 中打败所有敌人", + "descriptionFullComplete": "在 ${LEVEL} 中打败了所有敌人", + "name": "${LEVEL} 获胜" + } + }, + "achievementsRemainingText": "未完成成就:", + "achievementsText": "成就", + "achievementsUnavailableForOldSeasonsText": "抱歉,往届的成就细节不可用。", + "addGameWindow": { + "getMoreGamesText": "获取更多游戏模式…", + "titleText": "添加比赛" + }, + "allowText": "允许", + "alreadySignedInText": "您的账号已在其他设备登录;\n请切换账号或者退出已登录的设备,\n然后再试一次", + "apiVersionErrorText": "无法加载模组${NAME};它的API版本为 ${VERSION_USED},我们需要 ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(“自动”仅在插入耳机时有效)", + "headRelativeVRAudioText": "头部相对VR音频", + "musicVolumeText": "音乐音量", + "soundVolumeText": "音效音量", + "soundtrackButtonText": "自定义背景音乐", + "soundtrackDescriptionText": "导入自定义背景音乐", + "titleText": "声音" + }, + "autoText": "自动", + "backText": "返回", + "banThisPlayerText": "禁掉该玩家", + "bestOfFinalText": "${COUNT}局决胜制最后得分", + "bestOfSeriesText": "${COUNT}决胜制:", + "bestRankText": "您的最佳是 #${RANK}", + "bestRatingText": "您的最高评价是 ${RATING}", + "bombBoldText": "炸弹", + "bombText": "炸弹", + "boostText": "加速", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME}在该应用程序自身中设置。", + "buttonText": "按钮", + "canWeDebugText": "您是否希望炸弹小分队向开发人员自动报告\n错误、崩溃和基本使用信息?\n\n数据中不包含任何个人信息,可有助于\n保持游戏零错误平稳运行。", + "cancelText": "取消", + "cantConfigureDeviceText": "抱歉,${DEVICE}不可配置。", + "challengeEndedText": "此挑战已经结束。", + "chatMuteText": "屏蔽消息", + "chatMutedText": "聊天静音", + "chatUnMuteText": "取消屏蔽消息", + "choosingPlayerText": "<选择玩家>", + "completeThisLevelToProceedText": "你需要先完成这一关", + "completionBonusText": "完成奖励", + "configControllersWindow": { + "configureControllersText": "手柄调试", + "configureKeyboard2Text": "设置键盘 P2", + "configureKeyboardText": "设置键盘", + "configureMobileText": "用移动设备作为控制器", + "configureTouchText": "触摸屏配置", + "ps3Text": "PS3手柄", + "titleText": "手柄", + "wiimotesText": "Wiimote", + "xbox360Text": "Xbox360手柄" + }, + "configGamepadSelectWindow": { + "androidNoteText": "注意:手柄支持取决于设备和安卓版本。", + "pressAnyButtonText": "按手柄上的任意按钮\n 您想要设置...", + "titleText": "手柄调试" + }, + "configGamepadWindow": { + "advancedText": "高级", + "advancedTitleText": "高级控制器设置", + "analogStickDeadZoneDescriptionText": "(释放摇杆角色出现“漂移”情况时调高)", + "analogStickDeadZoneText": "模拟摇杆盲区", + "appliesToAllText": "(适用于所有该类型手柄)", + "autoRecalibrateDescriptionText": "(角色未全速移动时有效)", + "autoRecalibrateText": "自动校准模拟摇杆", + "axisText": "轴", + "clearText": "清除", + "dpadText": "方向键", + "extraStartButtonText": "额外启动按钮", + "ifNothingHappensTryAnalogText": "如无反应,请尝试分配至模拟摇杆。", + "ifNothingHappensTryDpadText": "如无反应,请尝试分配至方向键。", + "ignoreCompletelyDescriptionText": "(避免这个控制器影响游戏或菜单)", + "ignoreCompletelyText": "已完全忽略", + "ignoredButton1Text": "已忽略 按键1", + "ignoredButton2Text": "已忽略 按键2", + "ignoredButton3Text": "已忽略 按键3", + "ignoredButton4Text": "已忽略 按键4", + "ignoredButtonDescriptionText": "(用于防止“主页”或“同步”按钮影响用户界面)", + "pressAnyAnalogTriggerText": "按任意模拟扳机...", + "pressAnyButtonOrDpadText": "按任意键或方向键...", + "pressAnyButtonText": "按任意键", + "pressLeftRightText": "按左或右", + "pressUpDownText": "按上或下", + "runButton1Text": "跑 按键1", + "runButton2Text": "跑 按键2", + "runTrigger1Text": "跑 扳机1", + "runTrigger2Text": "跑 扳机2", + "runTriggerDescriptionText": "(模拟扳机可实现变速运行)", + "secondHalfText": "用于设置显示为单一手柄的\n二合一手柄设备的\n第二部分。", + "secondaryEnableText": "启用", + "secondaryText": "从属手柄", + "startButtonActivatesDefaultDescriptionText": "(如果启动按钮更倾向为“菜单”按钮,则关闭此功能)", + "startButtonActivatesDefaultText": "启动按钮激活默认部件", + "titleText": "手柄设置", + "twoInOneSetupText": "二合一手柄设置", + "uiOnlyDescriptionText": "阻止这个控制器加入游戏", + "uiOnlyText": "限制菜单应用", + "unassignedButtonsRunText": "全部未分配按钮 跑", + "unsetText": "<未设置>", + "vrReorientButtonText": "虚拟按钮调整" + }, + "configKeyboardWindow": { + "configuringText": "配置${DEVICE}", + "keyboard2NoteText": "注:大多数键盘一次只能同时按几个按键,\n所以如果玩家另接一个单独的键盘,\n则两个键盘可帮助他们更好地玩游戏。\n请注意,即使在这种情况下,您也仍需\n为两个玩家分配独特的按键。" + }, + "configTouchscreenWindow": { + "actionControlScaleText": "动作控制量表", + "actionsText": "动作", + "buttonsText": "按钮", + "dragControlsText": "<拖动手柄重新定位他们>", + "joystickText": "游戏摇杆", + "movementControlScaleText": "移动控制量表", + "movementText": "移动", + "resetText": "重置", + "swipeControlsHiddenText": "隐藏滑动图标", + "swipeInfoText": "“滑动”式手柄是需要花点时间来适应的,但\n能不看手柄上的控制键玩游戏会更轻松。", + "swipeText": "滑动", + "titleText": "触摸屏配置" + }, + "configureItNowText": "立即配置?", + "configureText": "配置", + "connectMobileDevicesWindow": { + "amazonText": "亚马逊Appstore", + "appStoreText": "App Store", + "bestResultsText": "零延迟无线网可助你取得最佳成绩。要想降低无线网延迟,\n你可以关闭其他无线设备,\n或者在无线路由器附近玩,\n再或者将游戏主机连接以太网。", + "explanationText": "如果想把智能手机或平板设备作为控制器,请安装\n“${REMOTE_APP_NAME}”。通过无线网络,最多可\n以支持8个设备同时连接。最酷的是,这完全免费!", + "forAndroidText": "安卓", + "forIOSText": "苹果iOS", + "getItForText": "iOS 用户可从 ${REMOTE_APP_NAME} 取得炸弹小分队遥控器\nAndroid 使用者可以通过 Google Play 商店或者亚马逊应用商店下载", + "googlePlayText": "Google Play", + "titleText": "把移动设备用作为游戏手柄" + }, + "continuePurchaseText": "花 ${PRICE}继续?", + "continueText": "继续", + "controlsText": "控制键", + "coopSelectWindow": { + "activenessAllTimeInfoText": "不提供所有时间的排名。", + "activenessInfoText": "倍数随玩游戏天数增加,\n随未玩游戏天数减少。", + "activityText": "活动", + "campaignText": "比赛", + "challengesInfoText": "获取完成迷你游戏的奖励。\n\n每完成一项挑战\n奖励和难度都会增加,\n每挑战失败或放弃挑战一次,奖励和难度都会降低。", + "challengesText": "挑战", + "currentBestText": "当前最高分", + "customText": "自定义", + "entryFeeText": "参赛", + "forfeitConfirmText": "要放弃挑战吗?", + "forfeitNotAllowedYetText": "尚不能放弃此挑战。", + "forfeitText": "放弃", + "multipliersText": "倍数", + "nextChallengeText": "下一挑战", + "nextPlayText": "下一次游戏", + "ofTotalTimeText": "${TOTAL}", + "playNowText": "立即开玩", + "pointsText": "分", + "powerRankingFinishedSeasonUnrankedText": "(已结束赛季,未排名)", + "powerRankingNotInTopText": "(${NUMBER}名以外)", + "powerRankingPointsEqualsText": "${NUMBER}分", + "powerRankingPointsMultText": "(x ${NUMBER} 分)", + "powerRankingPointsText": "${NUMBER} 分", + "powerRankingPointsToRankedText": "(${CURRENT}分 已获得 共${REMAINING}分)", + "powerRankingText": "能力排位", + "prizesText": "奖励", + "proMultInfoText": "${PRO}升级的玩家\n获得${PERCENT}%的得分提速。", + "seeMoreText": "更多……", + "skipWaitText": "跳过等待", + "timeRemainingText": "剩余时间", + "toRankedText": "排名", + "totalText": "总计", + "tournamentInfoText": "与联赛中的其他玩家\n争夺高分。\n\n锦标赛时间结束时\n高分玩家将赢得奖励。", + "welcome1Text": "欢迎来到${LEAGUE}。你可以通过\n在锦标赛中赚取星级、达成成就及\n赢得冠军来提升你的联赛排名。", + "welcome2Text": "你还可参加很多相同活动来赢取点券。\n点券可用于解锁新的角色、地图和\n迷你游戏,或进入锦标赛,或更多用途", + "yourPowerRankingText": "你的能力排位:" + }, + "copyOfText": "${NAME} 复制", + "createEditPlayerText": "<创建/编辑玩家>", + "createText": "创建", + "creditsWindow": { + "additionalAudioArtIdeasText": "添补音频、初期原图和创意:${NAME}", + "additionalMusicFromText": "添补背景音乐:${NAME}", + "allMyFamilyText": "帮助进行游戏测试的我的所有好友和家人", + "codingGraphicsAudioText": "编码、图像和音频:${NAME}", + "languageTranslationsText": "语言翻译:", + "legalText": "法律:", + "publicDomainMusicViaText": "版权公有音乐:${NAME}", + "softwareBasedOnText": "该软件部分基于${NAME}的工作", + "songCreditText": "${TITLE}演唱:${PERFORMER}\n作曲:${COMPOSER},改编:${ARRANGER},发行:${PUBLISHER},\n由${SOURCE}提供", + "soundAndMusicText": "音乐和音效", + "soundsText": "音乐(${SOURCE}):", + "specialThanksText": "特别感谢", + "thanksEspeciallyToText": "尤其感谢 ${NAME}", + "titleText": "${APP_NAME}制作团队", + "whoeverInventedCoffeeText": "发明咖啡的人" + }, + "currentStandingText": "您目前的排名是#${RANK}", + "customizeText": "自定义...", + "deathsTallyText": "${COUNT}次死亡", + "deathsText": "死亡", + "debugText": "调试", + "debugWindow": { + "reloadBenchmarkBestResultsText": "注意:建议您在调试阶段将设置->画面->纹理设置为“高”。", + "runCPUBenchmarkText": "运行CPU基准程序", + "runGPUBenchmarkText": "运行GPU基准程序", + "runMediaReloadBenchmarkText": "运行媒体重载基准程序", + "runStressTestText": "运行压力测试", + "stressTestPlayerCountText": "玩家计数", + "stressTestPlaylistDescriptionText": "压力测试列表", + "stressTestPlaylistNameText": "列表名称", + "stressTestPlaylistTypeText": "列表类型", + "stressTestRoundDurationText": "回合时长", + "stressTestTitleText": "压力测试", + "titleText": "基准程序和压力测试", + "totalReloadTimeText": "总计重载时间:${TIME}(详见日志)" + }, + "defaultGameListNameText": "默认${PLAYMODE}列表", + "defaultNewGameListNameText": "我的${PLAYMODE}列表", + "deleteText": "删除", + "demoText": "演示", + "denyText": "拒绝", + "desktopResText": "桌面分辨率", + "difficultyEasyText": "简单", + "difficultyHardOnlyText": "仅限困难模式", + "difficultyHardText": "困难", + "difficultyHardUnlockOnlyText": "该关卡只可解锁在困难模式下解锁。\n你认为自己拥有夺冠的实力吗!?!?", + "directBrowserToURLText": "请打开网页浏览器访问以下URL:", + "disableRemoteAppConnectionsText": "解除手柄应用链接", + "disableXInputDescriptionText": "允许使用4个以上的控制器但可能不会正常工作", + "disableXInputText": "禁用XInput", + "doneText": "完成", + "drawText": "平局", + "duplicateText": "复制", + "editGameListWindow": { + "addGameText": "添加\n比赛", + "cantOverwriteDefaultText": "不可以覆盖默认的比赛列表!", + "cantSaveAlreadyExistsText": "已存在与此名字相同的比赛列表!", + "cantSaveEmptyListText": "不可以保存空白的比赛列表!", + "editGameText": "编辑\n比赛", + "gameListText": "比赛列表", + "listNameText": "比赛列表名称", + "nameText": "名称", + "removeGameText": "删除\n比赛", + "saveText": "保存", + "titleText": "列表编辑器" + }, + "editProfileWindow": { + "accountProfileInfoText": "此特殊玩家档案,\n可能含有其中一个图标:\n\n${ICONS}\n\n基于您的登录平台以显示\n相关图标。", + "accountProfileText": "(账户资料)", + "availableText": "\"${NAME}\"可用。", + "changesNotAffectText": "注意:更改不会影响已经存在的比赛角色", + "characterText": "角色", + "checkingAvailabilityText": "正在检查\"${NAME}\"是否可用…", + "colorText": "颜色", + "getMoreCharactersText": "获取更多角色...", + "getMoreIconsText": "获取更多图标...", + "globalProfileInfoText": "全球玩家档案可使玩家拥有全世界唯一\n的名称。档案也包括自定义图标。", + "globalProfileText": "(全球档案)", + "highlightText": "副颜色", + "iconText": "图标", + "localProfileInfoText": "本地玩家档案不含图标,且玩家名称不具唯一性。\n升级至全球档案,以拥有唯一的游戏名称,并添加自定义图标。\n#警告:切勿使用随机到的名称,被使用的可能性极大。", + "localProfileText": "(本地档案)", + "nameDescriptionText": "角色名称", + "nameText": "名称", + "randomText": "随机", + "titleEditText": "编辑档案", + "titleNewText": "新建档案", + "unavailableText": "\"${NAME}\"不可用;请尝试另一名称。", + "upgradeProfileInfoText": "这可在全球范围内保存您的玩家名称,\n并可让您为自己的名称分配一个自定义图标。", + "upgradeToGlobalProfileText": "升级至全球档案" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "不能删除默认的背景音乐。", + "cantEditDefaultText": "不能编辑默认的背景音乐。请备份或是新建一个背景音乐列表。", + "cantOverwriteDefaultText": "不可以覆盖默认的背景音乐", + "cantSaveAlreadyExistsText": "这个名字的背景音乐已经存在!", + "defaultGameMusicText": "<默认游戏音乐>", + "defaultSoundtrackNameText": "默认自定义背景音乐", + "deleteConfirmText": "删除自定义背景音乐:\n\n'${NAME}'?", + "deleteText": "删除自定义\n背景音乐", + "duplicateText": "备份自定义\n背景音乐", + "editSoundtrackText": "自定义背景音乐编辑器", + "editText": "编辑自定义\n背景音乐", + "fetchingITunesText": "正在获得音乐应用播放列表", + "musicVolumeZeroWarning": "注意:现在音量为0", + "nameText": "名称", + "newSoundtrackNameText": "我的背景音乐${COUNT}", + "newSoundtrackText": "新的背景音乐:", + "newText": "创建背\n景音乐", + "selectAPlaylistText": "选择一个列表", + "selectASourceText": "音乐来源", + "testText": "测试", + "titleText": "自定义背景音乐", + "useDefaultGameMusicText": "默认游戏音乐", + "useITunesPlaylistText": "音乐应用播放列表", + "useMusicFileText": "音乐文件(mp3等)", + "useMusicFolderText": "音乐文件夹" + }, + "editText": "修改", + "endText": "结束", + "enjoyText": "尽情享用吧!", + "epicDescriptionFilterText": "史诗级慢动作 ${DESCRIPTION}。", + "epicNameFilterText": "史诗级${NAME}", + "errorAccessDeniedText": "访问被拒绝", + "errorOutOfDiskSpaceText": "磁盘空间不足", + "errorText": "错误", + "errorUnknownText": "未知错误", + "exitGameText": "退出${APP_NAME}?", + "exportSuccessText": "退出'${NAME}'", + "externalStorageText": "外部存储器", + "failText": "失败", + "fatalErrorText": "哎呀,有些档案遗失或损坏了。\n请尝试重新安装游戏,或者\n联系 ${EMAIL} 寻求帮助", + "fileSelectorWindow": { + "titleFileFolderText": "选择一个文件或文件夹", + "titleFileText": "选择一个文件", + "titleFolderText": "选择一个文件夹", + "useThisFolderButtonText": "使用此文件夹" + }, + "filterText": "过滤器", + "finalScoreText": "最后得分", + "finalScoresText": "最后得分", + "finalTimeText": "最终时间", + "finishingInstallText": "安装即将完成...", + "fireTVRemoteWarningText": "* 为获得更好的游戏体验,请使用\n游戏手柄或在您的手机和\n平板电脑上安装\n'${REMOTE_APP_NAME}'。", + "firstToFinalText": "${COUNT}局自由混战赛最后得分", + "firstToSeriesText": "${COUNT}局自由混战赛", + "fiveKillText": "五连杀!!!", + "flawlessWaveText": "完美的一波!", + "fourKillText": "四连杀!!!", + "friendScoresUnavailableText": "无法查看好友的得分。", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "最高得分玩家数:${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "您不能删除默认的比赛列表!", + "cantEditDefaultText": "不能编辑默认的比赛列表!复制或者新建一个。", + "cantShareDefaultText": "你不能分享默认列表", + "deleteConfirmText": "是否删除“${LIST}”?", + "deleteText": "删除比\n赛列表", + "duplicateText": "复制比\n赛列表", + "editText": "编辑比\n赛列表", + "gameListText": "比赛列表", + "newText": "新建比\n赛列表", + "showTutorialText": "显示教程", + "shuffleGameOrderText": "随机比赛模式", + "titleText": "自定义${TYPE}列表" + }, + "gameSettingsWindow": { + "addGameText": "添加比赛" + }, + "gamesToText": "${WINCOUNT}比${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "请记住:如果你有足够多的游戏手柄,\n派对中的任意设备可允许多个玩家同时游戏。", + "aboutDescriptionText": "使用这些选项卡来组织一场派对。\n\n通过派对,你可以和好友在不同的设备上\n一起玩游戏和比赛。\n\n使用手柄右上角的${PARTY}按钮\n来发起派对聊天和互动。\n(在菜单中时按${BUTTON})", + "aboutText": "关于", + "addressFetchErrorText": "<获取地址出错>", + "appInviteInfoText": "邀请好友一起玩BombSquad,新玩家可获得 ${COUNT}免费点券。\n每成功邀请到一位好友,您可获得${YOU_COUNT}点券。", + "appInviteMessageText": "${NAME} 在${APP_NAME}中送给了您${COUNT} 点券", + "appInviteSendACodeText": "向他们发送一个代码", + "appInviteTitleText": "${APP_NAME} App 邀请码", + "bluetoothAndroidSupportText": "(适用于任何支持蓝牙功能的安卓设备)", + "bluetoothDescriptionText": "通过蓝牙来创建或加入比赛", + "bluetoothHostText": "通过蓝牙来创建游戏", + "bluetoothJoinText": "通过蓝牙来加入游戏", + "bluetoothText": "蓝牙", + "checkingText": "检查中...", + "copyCodeConfirmText": "代码已复制到粘贴板。", + "copyCodeText": "复制代码", + "dedicatedServerInfoText": "建立一个专用服务器来获得最佳效果,详情见 bombsquadgame.com/server 哦!", + "disconnectClientsText": "这将使派对中的${COUNT}位玩家\n断开连接。是否确定?", + "earnTicketsForRecommendingAmountText": "如果您的朋友们玩了这款游戏,他们将会收到 ${COUNT} 点券\n(每个畅游的朋友将让你收到 ${YOU_COUNT}点券)", + "earnTicketsForRecommendingText": "分享游戏来\n获取免费点券...", + "emailItText": "通过电子邮件发送", + "favoritesSaveText": "另存为收藏", + "favoritesText": "收藏", + "freeCloudServerAvailableMinutesText": "下一次免费云服务器将在 ${MINUTES} 分钟后可用。", + "freeCloudServerAvailableNowText": "当前免费云服务器可用!", + "freeCloudServerNotAvailableText": "无免费云服务器可用。", + "friendHasSentPromoCodeText": "从 ${NAME} 中获取 ${COUNT} 张 ${APP_NAME} 点券", + "friendPromoCodeAwardText": "每使用一次,你可收到${COUNT}张点券。", + "friendPromoCodeExpireText": "代码将在 ${EXPIRE_HOURS} 小时后失效,代码仅适用于新玩家。", + "friendPromoCodeInstructionsText": "要使用代码,可打开 ${APP_NAME},按\"设置->高级->输入促销代码\"操作。\n所有支持平台的下载链接见 bombsquadgame.com。", + "friendPromoCodeRedeemLongText": "达到${MAX_USES}人后,就不可获得${COUNT}免费点券。(防止玩家用来作弊)", + "friendPromoCodeRedeemShortText": "可在游戏中兑换${COUNT}免费点券。", + "friendPromoCodeWhereToEnterText": "(在\"设置->高级->输入促销代码\"中)", + "getFriendInviteCodeText": "获取好友邀请码", + "googlePlayDescriptionText": "邀请Google Play玩家来加入你的派对", + "googlePlayInviteText": "邀请", + "googlePlayReInviteText": "如果你发出一个新邀请,\n则你的派对中的${COUNT}位Google Play玩家将断开连接。\n在发出的新邀请中再次邀请他们,让他们重回派对。", + "googlePlaySeeInvitesText": "查看邀请", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(仅针对 Google Play 设备)", + "hostPublicPartyDescriptionText": "创建一个公开派对", + "hostingUnavailableText": "主机不可用", + "inDevelopmentWarningText": "注意:\n\n联网模式是一项新的并且还在开发特性,\n目前强烈建议所有玩家在同一个\n无线局域网下游戏。", + "internetText": "在线游戏", + "inviteAFriendText": "好友还未加入该游戏?邀请他们\n一起玩,新玩家可获得${COUNT}张免费点券。", + "inviteFriendsText": "邀请朋友", + "joinPublicPartyDescriptionText": "加入一个公开派对", + "localNetworkDescriptionText": "加入一个附近的派对(通过局域网,蓝牙,etc等)", + "localNetworkText": "本地网络", + "makePartyPrivateText": "将我的派对变成私人派对", + "makePartyPublicText": "将我的派对变成公开派对", + "manualAddressText": "地址", + "manualConnectText": "连接", + "manualDescriptionText": "加入派对,地址:", + "manualJoinSectionText": "使用地址加入", + "manualJoinableFromInternetText": "是否可从互联网连接?:", + "manualJoinableNoWithAsteriskText": "否*", + "manualJoinableYesText": "是", + "manualRouterForwardingText": "* 若要解决此问题,请尝试将您的路由器设置为将UDP端口${PORT}转发到你的本地地址(地址一般是192.168.*.1)", + "manualText": "手动", + "manualYourAddressFromInternetText": "互联网地址:", + "manualYourLocalAddressText": "本地地址:", + "nearbyText": "附近", + "noConnectionText": "<无连接>", + "otherVersionsText": "(其他版本)", + "partyCodeText": "派对代码", + "partyInviteAcceptText": "接受", + "partyInviteDeclineText": "拒绝", + "partyInviteGooglePlayExtraText": "(查看'Gather'窗口中的'Google Play'选项卡)", + "partyInviteIgnoreText": "忽略", + "partyInviteText": "${NAME}已邀请\n你加入他们的派对!", + "partyNameText": "派对名称", + "partyServerRunningText": "您的派对服务器正在运行。", + "partySizeText": "派对规模大小", + "partyStatusCheckingText": "检查中", + "partyStatusJoinableText": "你的派对现在可以从互联网上加入了", + "partyStatusNoConnectionText": "无法连接到服务器", + "partyStatusNotJoinableText": "你的派对不能从互联网上加入", + "partyStatusNotPublicText": "你的派对是私人的", + "pingText": "延迟", + "portText": "端口", + "privatePartyCloudDescriptionText": "私有方在专用的云服务器上运行;无需路由器配置", + "privatePartyHostText": "创建一个私人派对", + "privatePartyJoinText": "加入一个公开派对", + "privateText": "私人", + "publicHostRouterConfigText": "这可能需要配置路由器进行端口转发,为了更简单的选择,请使用私人派对", + "publicText": "公共", + "requestingAPromoCodeText": "正在请求代码…", + "sendDirectInvitesText": "直接邀请", + "shareThisCodeWithFriendsText": "与好友分享此代码:", + "showMyAddressText": "显示我的地址", + "startHostingPaidText": "创建需花费 ${COST}", + "startHostingText": "创建", + "startStopHostingMinutesText": "你可以在接下来的${MINUTES}分钟内启用会停止一个免费的主机", + "stopHostingText": "停止主机", + "titleText": "多人游戏", + "wifiDirectDescriptionBottomText": "如果所有设备都设有 Wi-Fi Direct 面板,那他们应该可以使用通过它来找到彼此,\n然后相互连接。一旦所有设备都相互连接上了,你就可以通过“本地网络”选项卡\n在此组织派对,常规的无线局域网也是一样。\n\n如要取得最佳效果,Wi-Fi Direct 创建者也应是${APP_NAME} 派对的创建者", + "wifiDirectDescriptionTopText": "无需无线网络即可直接\n通过Wi-Fi Direct连接安卓设备。对于安装了Android 4.2或更高版本操作系统的设备效果更好。\n\n要使用该功能,可打开无线网络连接设置,然后在菜单中寻找'Wi-Fi Direct'。", + "wifiDirectOpenWiFiSettingsText": "打开无线网络连接设置", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(所有平台之间运作)", + "worksWithGooglePlayDevicesText": "(适用于运行Google Play(安卓)版游戏的设备)", + "youHaveBeenSentAPromoCodeText": "您已送出一个 ${APP_NAME} 促销代码:" + }, + "getTicketsWindow": { + "freeText": "免费!", + "freeTicketsText": "免费点券", + "inProgressText": "一个交易正在进行;请稍后再试。", + "purchasesRestoredText": "购买恢复。", + "receivedTicketsText": "获得${COUNT}点券!", + "restorePurchasesText": "恢复购买", + "ticketDoublerText": "点券倍增器", + "ticketPack1Text": "小型点券包", + "ticketPack2Text": "中等点券包", + "ticketPack3Text": "大型点券包", + "ticketPack4Text": "巨型点券包", + "ticketPack5Text": "猛犸象点券包", + "ticketPack6Text": "终极点券包", + "ticketsFromASponsorText": "从赞助商处\n获得${COUNT}点券", + "ticketsText": "${COUNT}点券", + "titleText": "获得点券", + "unavailableLinkAccountText": "对不起,该平台上不可进行购买。\n您可将此帐户链接到另一个\n平台,以进行购买。", + "unavailableTemporarilyText": "该选项当前不可用;请稍后再试。", + "unavailableText": "对不起,该选项不可用。", + "versionTooOldText": "对不起,这个版本的游戏太旧了;请更新到新版本。", + "youHaveShortText": "您拥有${COUNT}", + "youHaveText": "你拥有${COUNT}点券" + }, + "googleMultiplayerDiscontinuedText": "抱歉,Google的多人游戏服务已不可用。\n我将尽快更换新的替代服务。\n在此之前,请尝试其他连接方法。\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "总是", + "fullScreenCmdText": "全屏显示(Cmd+F)", + "fullScreenCtrlText": "全屏(Ctrl-F)", + "gammaText": "Gamma", + "highText": "高", + "higherText": "极高", + "lowText": "低", + "mediumText": "中", + "neverText": "关", + "resolutionText": "分辨率", + "showFPSText": "显示FPS", + "texturesText": "材质质量", + "titleText": "图像质量", + "tvBorderText": "电视边缘", + "verticalSyncText": "垂直同步", + "visualsText": "视觉" + }, + "helpWindow": { + "bombInfoText": "炸弹\n比拳头伤害高,但也能把自己送上西天。\n给你个建议:等引线快烧完的时候\n再把炸弹扔向敌人。", + "canHelpText": "${APP_NAME}可以帮助。", + "controllersInfoText": "你可以和好友在同一网络下玩${APP_NAME},或者\n如果你有足够多的手柄,那也可以在同一个设备上游戏。\n${APP_NAME}支持各种选择;你甚至可以通过免费的'${REMOTE_APP_NAME}'\n用手机作为游戏手柄。\n更多信息,请参见设置->手柄。", + "controllersText": "手柄", + "controlsSubtitleText": "你的友好的${APP_NAME}角色具有几个基本动作:", + "controlsText": "控制键", + "devicesInfoText": "VR版${APP_NAME}可与\n普通版本联网游戏,所以掏出你所有的手机、平板电脑\n和电脑,大玩一场吧。你甚至还可以将\n普通版本的游戏连接至VR版,\n让游戏外的人也能看你玩。", + "devicesText": "设备", + "friendsGoodText": "这对你而言都是好事。和三五好友一起玩${APP_NAME}最有趣了,\n最多可以支持8个玩家一起玩,进入", + "friendsText": "好友", + "jumpInfoText": "跳跃\n跳跃可以跳过较窄的缝隙,\n或是把炸弹扔的更远,\n或是表达你难以掩盖的喜悦之情。", + "orPunchingSomethingText": "或用拳猛击敌人,将它砸下悬崖,然后在它下落的途中用粘性炸弹炸掉它。", + "pickUpInfoText": "拾起\n你可以拾起旗子,敌人,\n还有所有没固定在地上的东西,\n然后,再扔出去吧。", + "powerupBombDescriptionText": "连续扔出\n三枚炸弹。", + "powerupBombNameText": "三连炸弹", + "powerupCurseDescriptionText": "你可能想要避开这些。\n…或者你想试试看?", + "powerupCurseNameText": "诅咒", + "powerupHealthDescriptionText": "让你完全恢复生命值。\n你永远都猜不到。", + "powerupHealthNameText": "中等生命值包", + "powerupIceBombsDescriptionText": "威力比普通炸弹小,\n但能将你的敌人冻住,\n让它们变得特别脆弱。", + "powerupIceBombsNameText": "冰冻弹", + "powerupImpactBombsDescriptionText": "威力比普通炸弹稍弱,\n但碰到外物后就会爆炸。", + "powerupImpactBombsNameText": "触感弹", + "powerupLandMinesDescriptionText": "大特价,一包3个。\n居家旅行,防守阵地的不二选择,\n还可以阻止那些跑的飞快的敌人。", + "powerupLandMinesNameText": "地雷", + "powerupPunchDescriptionText": "让您变成专业拳击手\n更高!更快!更强!", + "powerupPunchNameText": "拳击手套", + "powerupShieldDescriptionText": "能吸收一些伤害,\n关键时刻可能会有用。", + "powerupShieldNameText": "能量护盾", + "powerupStickyBombsDescriptionText": "黏在任何碰到的东西上,\n然后就等着看烟花吧。", + "powerupStickyBombsNameText": "粘性炸弹", + "powerupsSubtitleText": "当然,没有提升器的游戏很难通关:", + "powerupsText": "提升器", + "punchInfoText": "拳击\n跑得越快,拳击的伤害越高。\n所以请成为飞奔的拳击手吧!", + "runInfoText": "冲刺\n按任意键冲刺,如果你用手柄操作将会容易许多。\n冲刺跑的虽快,但会造成转向困难。且冲且珍惜。", + "someDaysText": "有些时候你只是想挥拳猛击某些东西,或把什么东西给炸飞。", + "titleText": "${APP_NAME}帮助", + "toGetTheMostText": "要想最痛快地玩这个游戏,你需要:", + "welcomeText": "欢迎来到${APP_NAME}!" + }, + "holdAnyButtonText": "<按住任意按钮>", + "holdAnyKeyText": "<按住任何键>", + "hostIsNavigatingMenusText": "- ${HOST}像大老板一样观察主界面 -", + "importPlaylistCodeInstructionsText": "用代码来导入列表", + "importPlaylistSuccessText": "成功导入${TYPE}列表 '${NAME}'", + "importText": "导入", + "importingText": "导入中", + "inGameClippedNameText": "名字会是\n\"${NAME}\"", + "installDiskSpaceErrorText": "错误:无法完成安装。\n您的设备可能是空间不足。\n腾出一些空间,然后重试。", + "internal": { + "arrowsToExitListText": "按${LEFT}或${RIGHT}退出列表", + "buttonText": "按钮", + "cantKickHostError": "你不能踢房主啊喂!", + "chatBlockedText": "玩家 ${NAME} 被禁言 ${TIME} 秒.", + "connectedToGameText": "加入 '${NAME}'", + "connectedToPartyText": "加入${NAME}的派对!", + "connectingToPartyText": "正在连接...", + "connectionFailedHostAlreadyInPartyText": "连接失败;创建者正在另一派对中。", + "connectionFailedPartyFullText": "连接出错:房间满员了…", + "connectionFailedText": "连接失败。", + "connectionFailedVersionMismatchText": "连接失败;创建者正在运行不同版本的游戏。\n请确保你们都安装了最新版本,然后再试一次。", + "connectionRejectedText": "连接被拒绝。", + "controllerConnectedText": "${CONTROLLER}已连接。", + "controllerDetectedText": "检测到1个手柄。", + "controllerDisconnectedText": "${CONTROLLER}断开连接。", + "controllerDisconnectedTryAgainText": "${CONTROLLER}断开连接。请尝试重新连接。", + "controllerForMenusOnlyText": "此控制器无法在游戏中使用", + "controllerReconnectedText": "${CONTROLLER}重新连接。", + "controllersConnectedText": "已连接${COUNT}个手柄。", + "controllersDetectedText": "检测到${COUNT}个手柄。", + "controllersDisconnectedText": "${COUNT}个手柄断开连接。", + "corruptFileText": "检测到已损坏的文件。请尝试重新安装,或发送电子邮件至${EMAIL}", + "errorPlayingMusicText": "播放音乐错误:${MUSIC}", + "errorResettingAchievementsText": "无法重置在线成就;请稍后再试。", + "hasMenuControlText": "${NAME}目前拥有菜单的控制权限。", + "incompatibleNewerVersionHostText": "房主运行着最新版本游戏。\n请更新您的游戏版本然后重试。", + "incompatibleVersionHostText": "创建者正在运行不同版本的游戏。\n请确保你们都安装了最新版本,然后再试一次。", + "incompatibleVersionPlayerText": "${NAME}正在运行不同版本的游戏。\n请确保你们都安装了最新版本,然后再试一次。", + "invalidAddressErrorText": "错误:无效的地址。", + "invalidNameErrorText": "错误:无效的名字", + "invalidPortErrorText": "错误:无效端口", + "invitationSentText": "已发出邀请。", + "invitationsSentText": "已发出${COUNT}个邀请。", + "joinedPartyInstructionsText": "有人加入了你的派对\n去“开始战斗”中开始一场游戏吧", + "keyboardText": "键盘", + "kickIdlePlayersKickedText": "${NAME}空闲,将其踢出。", + "kickIdlePlayersWarning1Text": "如果${NAME}仍然空闲,则将在${COUNT}秒后被踢出。", + "kickIdlePlayersWarning2Text": "(您可以在设置 ->高级中将其关闭)", + "leftGameText": "离开 '${NAME}'.", + "leftPartyText": "离开${NAME}的游戏", + "noMusicFilesInFolderText": "文件夹内没有音乐文件。", + "playerJoinedPartyText": "${NAME}加入了游戏!", + "playerLeftPartyText": "${NAME}离开了游戏。", + "rejectingInviteAlreadyInPartyText": "拒绝邀请(已经在派对中)。", + "serverRestartingText": "服务器重启下下,请各位重新加入哈,,", + "serverShuttingDownText": "服务器正在关机…", + "signInErrorText": "登陆出错啦~", + "signInNoConnectionText": "哎呀,无法登陆。(网络连接有故障?)", + "telnetAccessDeniedText": "错误:用户未得到telnet访问授权。", + "timeOutText": "(将在${TIME}秒内超出时限)", + "touchScreenJoinWarningText": "您已以触摸屏方式加入。\n如果这是一个错误,点击“菜单->离开游戏菜单”。", + "touchScreenText": "触摸屏", + "unableToResolveHostText": "错误:房主有问题", + "unavailableNoConnectionText": "哎呀,这个用不了呢(网络连接故障?)", + "vrOrientationResetCardboardText": "重置VR定位。\n您需使用外部手柄来玩该游戏。", + "vrOrientationResetText": "VR定位重置。", + "willTimeOutText": "(若空闲则会超出时限)" + }, + "jumpBoldText": "跳", + "jumpText": "跳", + "keepText": "举起", + "keepTheseSettingsText": "要保留您的新设置吗?", + "keyboardChangeInstructionsText": "双击空格以更改控制器", + "keyboardNoOthersAvailableText": "无其他可用的控制器", + "keyboardSwitchText": "切换控制器为\"${NAME}\"", + "kickOccurredText": "踢出 ${NAME}", + "kickQuestionText": "你们说要不要踢 ${NAME}? 呢…", + "kickText": "踢出", + "kickVoteCantKickAdminsText": "无法踢出管理员.", + "kickVoteCantKickSelfText": "您不能踢出您自己.", + "kickVoteFailedNotEnoughVotersText": "没有足够玩家投票", + "kickVoteFailedText": "踢出玩家投票未成功", + "kickVoteStartedText": "踢出${NAME}的投票已被发起", + "kickVoteText": "投票踢出玩家", + "kickVotingDisabledText": "投票踢出已被禁用.", + "kickWithChatText": "在聊天框中输入 ${YES} 来同意,输入 ${NO} 来拒绝(输入2来弃权)", + "killsTallyText": "${COUNT}次击杀", + "killsText": "击杀数", + "kioskWindow": { + "easyText": "简单", + "epicModeText": "史诗模式", + "fullMenuText": "完整菜单", + "hardText": "困难", + "mediumText": "中等", + "singlePlayerExamplesText": "单人游戏/合作模式样例", + "versusExamplesText": "对战模式样例" + }, + "languageSetText": "现在的语言是 \"${LANGUAGE}\"。", + "lapNumberText": "圈数:${CURRENT}/${TOTAL}", + "lastGamesText": "(最后${COUNT}局比赛)", + "leaderboardsText": "排行榜", + "league": { + "allTimeText": "所有时间", + "currentSeasonText": "当前赛季(${NUMBER})", + "leagueFullText": "${NAME}联赛", + "leagueRankText": "联赛排名", + "leagueText": "联赛", + "rankInLeagueText": "#${RANK}、${NAME}联赛${SUFFIX}", + "seasonEndedDaysAgoText": "赛季已于${NUMBER}天前结束。", + "seasonEndsDaysText": "赛季将于${NUMBER}天后结束。", + "seasonEndsHoursText": "赛季将于${NUMBER}小时后结束。", + "seasonEndsMinutesText": "赛季将于${NUMBER}分钟后结束。", + "seasonText": "第${NUMBER}赛季", + "tournamentLeagueText": "你一定要到${NAME}联赛参加这项赛事。", + "trophyCountsResetText": "奖杯计数将在下个赛季重置。" + }, + "levelBestScoresText": "在 ${LEVEL}中的最佳成绩", + "levelBestTimesText": "在 ${LEVEL}中的最佳时间", + "levelFastestTimesText": "${LEVEL}最短时间", + "levelHighestScoresText": "${LEVEL}最高得分", + "levelIsLockedText": "${LEVEL}处于锁定状态。", + "levelMustBeCompletedFirstText": "必须先完成${LEVEL}。", + "levelText": "${NUMBER}关卡", + "levelUnlockedText": "关卡解锁!", + "livesBonusText": "生命奖励", + "loadingText": "载入中", + "loadingTryAgainText": "加载中请稍后再试", + "macControllerSubsystemBothText": "均可(不推荐)", + "macControllerSubsystemClassicText": "经典", + "macControllerSubsystemDescriptionText": "(如果你的手柄无法工作,请尝试更改此项)", + "macControllerSubsystemMFiNoteText": "已检测到 Made-for-iOS/Mac 手柄;\n你需要在 设置->手柄 中启用该设备", + "macControllerSubsystemMFiText": "Made-for-iOS/Mac", + "macControllerSubsystemTitleText": "手柄支持", + "mainMenu": { + "creditsText": "制作团队", + "demoMenuText": "演示菜单", + "endGameText": "结束", + "exitGameText": "退出", + "exitToMenuText": "退出到菜单", + "howToPlayText": "帮助", + "justPlayerText": "(仅${NAME})", + "leaveGameText": "离开游戏", + "leavePartyConfirmText": "确实要离开吗?", + "leavePartyText": "离开派对", + "quitText": "离开游戏", + "resumeText": "回到游戏", + "settingsText": "设置" + }, + "makeItSoText": "应用", + "mapSelectGetMoreMapsText": "获取更多地图…", + "mapSelectText": "选择…", + "mapSelectTitleText": "${GAME}地图", + "mapText": "地图", + "maxConnectionsText": "最大连接数", + "maxPartySizeText": "最大派对规模", + "maxPlayersText": "最多人数", + "modeArcadeText": "街机模式", + "modeClassicText": "经典模式", + "modeDemoText": "演示模式", + "mostValuablePlayerText": "最有价值的玩家", + "mostViolatedPlayerText": "最遭受暴力的玩家", + "mostViolentPlayerText": "最暴力的玩家", + "moveText": "移动", + "multiKillText": "${COUNT}连杀!!", + "multiPlayerCountText": "${COUNT}名玩家", + "mustInviteFriendsText": "注意:你必须在“${GATHER}”面板中邀请好友,\n或连接多个\n手柄,和好友一起游戏。", + "nameBetrayedText": "${NAME}背叛了${VICTIM}", + "nameDiedText": "${NAME}挂了。", + "nameKilledText": "${NAME}把${VICTIM}杀了", + "nameNotEmptyText": "名字不能为空", + "nameScoresText": "${NAME}得分咯!", + "nameSuicideKidFriendlyText": "${NAME}意外挂了。", + "nameSuicideText": "${NAME}自杀了。", + "nameText": "名称", + "nativeText": "本机", + "newPersonalBestText": "新个人记录!", + "newTestBuildAvailableText": "更新的测试版可供下载了!(${VERSION}生成${BUILD})。\n到${ADDRESS}获取", + "newText": "新建", + "newVersionAvailableText": "更新版本的 ${APP_NAME} 可供下载了! 版本号(${VERSION})", + "nextAchievementsText": "下一个成就:", + "nextLevelText": "下一关", + "noAchievementsRemainingText": "- '无'", + "noContinuesText": "(无可继续)", + "noExternalStorageErrorText": "该设备上未发现外部存储器", + "noGameCircleText": "错误:未登入GameCircle", + "noProfilesErrorText": "您没有玩家档案,所以还得忍受“${NAME}”这个名字。\n进入设置->玩家档案,为自己创建档案。", + "noScoresYetText": "还未有得分记录。", + "noThanksText": "不,谢谢", + "noTournamentsInTestBuildText": "温馨提示:测试版的锦标赛分数不能计入锦标赛哦!", + "noValidMapsErrorText": "该比赛类型中未发现有效地图。", + "notEnoughPlayersRemainingText": "剩余玩家不足,退出并开始新游戏。", + "notEnoughPlayersText": "你需要至少${COUNT}名玩家来开始这场比赛!", + "notNowText": "不是现在", + "notSignedInErrorText": "您必须登录到您的帐户。", + "notSignedInGooglePlayErrorText": "您必须通过Google Play登录。", + "notSignedInText": "(未登录)", + "nothingIsSelectedErrorText": "未选择任何内容!", + "numberText": "#${NUMBER}", + "offText": "关", + "okText": "好的", + "onText": "开", + "oneMomentText": "请稍候...", + "onslaughtRespawnText": "${PLAYER}将于第${WAVE}波后复活", + "orText": "${A}或${B}", + "otherText": "其他。。。", + "outOfText": "(在${ALL}名玩家中位列#${RANK})", + "ownFlagAtYourBaseWarning": "你的旗帜必须在\n你自己的基地上才能得分!", + "packageModsEnabledErrorText": "启用本地程序包修改时,联网对战模式不可用(参见设置->高级)", + "partyWindow": { + "chatMessageText": "聊天消息", + "emptyText": "你的派对为空", + "hostText": "(创建者)", + "sendText": "发送", + "titleText": "你的派对" + }, + "pausedByHostText": "(创建者已暂停)", + "perfectWaveText": "完美的一波!", + "pickUpText": "捡起", + "playModes": { + "coopText": "合作", + "freeForAllText": "混战模式", + "multiTeamText": "多团队", + "singlePlayerCoopText": "单人游戏/合作模式", + "teamsText": "团队对抗" + }, + "playText": "开始战斗", + "playWindow": { + "coopText": "合作模式", + "freeForAllText": "大乱斗", + "oneToFourPlayersText": "适合1~4名玩家", + "teamsText": "团队对抗", + "titleText": "开始战斗", + "twoToEightPlayersText": "适合2~8名玩家" + }, + "playerCountAbbreviatedText": "${COUNT}名玩家", + "playerDelayedJoinText": "${PLAYER}将在下一回合开始时进入。", + "playerInfoText": "玩家资料", + "playerLeftText": "${PLAYER}离开了游戏。", + "playerLimitReachedText": "已达到${COUNT}名玩家上限;其他玩家不允许加入。", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "您无法删除您的帐户资料。", + "deleteButtonText": "删除\n档案", + "deleteConfirmText": "删除'${PROFILE}'?", + "editButtonText": "编辑\n档案", + "explanationText": "(为这个账号定制玩家名称和外观)", + "newButtonText": "创建\n档案", + "titleText": "玩家档案" + }, + "playerText": "玩家", + "playlistNoValidGamesErrorText": "此列表未包含有效的已解锁游戏。", + "playlistNotFoundText": "找不到列表", + "playlistText": "列表", + "playlistsText": "列表", + "pleaseRateText": "如果你喜欢 ${APP_NAME},请考虑花一点时间\n来评价一下它或为它写一篇评论。这将为我们提供\n有用的反馈建议,为游戏的未来开发给予支持。\n\n感谢您!\n-eric", + "pleaseWaitText": "请稍等...", + "pluginsDetectedText": "新的插件已经加载.请在设置中启用它们.", + "pluginsText": "插件", + "practiceText": "练习", + "pressAnyButtonPlayAgainText": "按任意按钮再玩一次...", + "pressAnyButtonText": "按任意按钮继续...", + "pressAnyButtonToJoinText": "按任意按钮加入...", + "pressAnyKeyButtonPlayAgainText": "按任意键/按钮再玩一次...", + "pressAnyKeyButtonText": "按任意键/按钮继续......", + "pressAnyKeyText": "按任意键...", + "pressJumpToFlyText": "** 按连续跳跃以腾空 **", + "pressPunchToJoinText": "按下“出拳”来加入", + "pressToOverrideCharacterText": "按${BUTTONS}更换您的角色", + "pressToSelectProfileText": "按${BUTTONS}选择一个玩家", + "pressToSelectTeamText": "按下${BUTTONS}来选择一支队伍", + "promoCodeWindow": { + "codeText": "代码", + "codeTextDescription": "促销代码", + "enterText": "输入" + }, + "promoSubmitErrorText": "提交代码时出错; 检查您的互联网连接", + "ps3ControllersWindow": { + "macInstructionsText": "关闭PS3背面的电源开关,确保\n您的Mac电脑上启用了蓝牙,然后通过USB连接线将您的手柄连接到\n您的Mac电脑上使其配对。之后,您\n就可以使用该手柄上的主页按钮以有线(USB)或无线(蓝牙)模式\n将其连接到您的Mac电脑上。\n\n在一些Mac电脑上配对时可能会提示您输入密码。\n在此情况下,请参阅一下教程或搜索谷歌寻求帮助。\n\n\n\n\n无线连接的PS3手柄应该出现在\n系统偏好设置->蓝牙中的设备列表中。当您想要再次用你的PS3使用它们时,\n您可能需要从该列表中移除它们。\n\n另外,请确保它们在未使用状态下时与蓝牙断开连接,\n以免其电池持续消耗。\n\n蓝牙最多可处理7个连接设备,\n虽然您的里程可能会有所不同。", + "ouyaInstructionsText": "若要通过OUYA使用PS3手柄,仅需使用USB连接线\n将其连接配对。这样做可能会使您的其他手柄断开连接,因此\n您应该重新启动您的OUYA,然后拔下USB连接线。\n\n然后,你应该能够使用手柄的主页按钮\n以无线模式将其连接。结束游戏后,按住主页按钮\n10秒钟,以关闭手柄;否则,手柄将持续处于启动状态\n并消耗电池。", + "pairingTutorialText": "配对教程视频", + "titleText": "使用 PS3 手柄玩 ${APP_NAME}:" + }, + "punchBoldText": "拳击", + "punchText": "拳击", + "purchaseForText": "购买花费${PRICE}", + "purchaseGameText": "购买游戏", + "purchasingText": "正在购买…", + "quitGameText": "退出${APP_NAME}?", + "quittingIn5SecondsText": "在5秒后退出...", + "randomPlayerNamesText": "Deva最萌, 企鹅王, 企鹅骑士团成员, 王♂の传人, 挨揍使我快乐, 正义之雷, 炸弹超人, 天下谁能敌手, 坑死队友不偿命, ChineseBomber, 一拳超人, 比尔, 二营长の意大利炮, 雷王, 野渡无人舟自横, 马克斯, 雪糕, 炸鸡翅, 手柄玩家18子, 寻找宝藏的海盗, 炸弹投手, 炸弹不是糖果, 我是对面的, Xxx_至高无上之炸弹王_xxX,万有引力,鸟语花香,狗年大吉,小狗狗,大狗子,二狗子,三狗子,四狗子,五狗子,灵虹膜", + "randomText": "随机", + "rankText": "排行", + "ratingText": "排名", + "reachWave2Text": "进入第2波才可排名。", + "readyText": "准备", + "recentText": "最近", + "remoteAppInfoShortText": "与家人或者朋友们一起玩${APP_NAME}是非常有趣的!\n您可以连接一个或多个硬件控制器\n或者在手机、平板上安装${REMOTE_APP_NAME}APP程序\n把他们当做控制器使用。", + "remote_app": { + "app_name": "炸弹小分队手柄", + "app_name_short": "炸弹小分队手柄", + "button_position": "按钮位置", + "button_size": "按钮尺寸", + "cant_resolve_host": "无法解析主机。", + "capturing": "捕捉中…", + "connected": "已连接。", + "description": "使用手机或平板电脑作为炸弹小分队游戏手柄。\n一台电视或平板电脑上可同时连接8台设备,体验史诗级多人模式的疯狂游戏。", + "disconnected": "服务器断开连接", + "dpad_fixed": "固定", + "dpad_floating": "浮动", + "dpad_position": "方向键位置", + "dpad_size": "方向键尺寸", + "dpad_type": "方向键类型", + "enter_an_address": "输入地址", + "game_full": "游戏连接已满或不接受更多连接。", + "game_shut_down": "游戏关闭。", + "hardware_buttons": "硬件按钮", + "join_by_address": "通过地址…加入", + "lag": "延迟:${SECONDS}秒", + "reset": "恢复默认值", + "run1": "运行 1", + "run2": "运行 2", + "searching": "正在搜索炸弹小分队游戏…", + "searching_caption": "点击游戏名,进入游戏。\n确保和游戏处于相同的wifi网络下。", + "start": "开始", + "version_mismatch": "版本不匹配。\n确定 炸弹小分队 和 BombSquad Remote\n 为最新版本后,重新尝试。" + }, + "removeInGameAdsText": "在商店中解锁\"${PRO}\",以删除游戏中的广告。", + "renameText": "重命名", + "replayEndText": "结束回放", + "replayNameDefaultText": "终场游戏回放", + "replayReadErrorText": "读取回放文件时出错。", + "replayRenameWarningText": "如果想保存回放文件,则以游戏来命名\"${REPLAY}\";否则文件将被覆盖。", + "replayVersionErrorText": "抱歉,该回放由不同版本的游戏制成,\n不能使用。", + "replayWatchText": "观看回放", + "replayWriteErrorText": "写入回放文件时出错。", + "replaysText": "回放", + "reportPlayerExplanationText": "利用此电子邮箱举报作弊、 不当言语或其他不良行为。\n请描述如下信息:", + "reportThisPlayerCheatingText": "作弊", + "reportThisPlayerLanguageText": "不当言语", + "reportThisPlayerReasonText": "举报内容是?", + "reportThisPlayerText": "举报该玩家", + "requestingText": "正在请求...", + "restartText": "重新启动", + "retryText": "请重试", + "revertText": "还原", + "runText": "运行", + "saveText": "保存", + "scanScriptsErrorText": "扫描脚本存在一个或多个错误;查看错误日志来了解详情。", + "scoreChallengesText": "得分挑战", + "scoreListUnavailableText": "得分列表不可用。", + "scoreText": "得分", + "scoreUnits": { + "millisecondsText": "毫秒", + "pointsText": "分", + "secondsText": "秒" + }, + "scoreWasText": "(是${COUNT})", + "selectText": "选择", + "seriesWinLine1PlayerText": "赢得", + "seriesWinLine1TeamText": "赢得", + "seriesWinLine1Text": "赢得", + "seriesWinLine2Text": "系列!", + "settingsWindow": { + "accountText": "账户", + "advancedText": "高级", + "audioText": "音效", + "controllersText": "控制器", + "graphicsText": "图像", + "playerProfilesMovedText": "注意:玩家档案已移至主菜单的「账号」窗口下", + "playerProfilesText": "玩家档案", + "titleText": "设置" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(一款简单、控制方便的用于文本编辑的屏幕键盘)", + "alwaysUseInternalKeyboardText": "始终使用内置键盘", + "benchmarksText": "基准与压力测试", + "disableCameraGyroscopeMotionText": "禁用相机陀螺仪运动", + "disableCameraShakeText": "禁用相机抖动", + "disableThisNotice": "(可在高级设置中关闭此通知)", + "enablePackageModsDescriptionText": "(启用额外的修改性能,但是禁用网络播放)", + "enablePackageModsText": "启用本地程序包修改", + "enterPromoCodeText": "输入促销代码", + "forTestingText": "注意:这些数值仅用于测试,并会在应用程序退出时丢失。", + "helpTranslateText": "${APP_NAME}的非英语翻译是社区\n共同努力的成果。如果您希望参与翻译或对其提出更正,\n请点击以下链接。先行感谢!", + "kickIdlePlayersText": "踢掉空闲玩家", + "kidFriendlyModeText": "儿童友好模式(低暴力等)", + "languageText": "语言", + "moddingGuideText": "修改指南", + "mustRestartText": "您必须重启游戏来使之生效", + "netTestingText": "网络测试", + "resetText": "恢复默认值", + "showBombTrajectoriesText": "显示炸弹轨迹", + "showPlayerNamesText": "显示玩家名字", + "showUserModsText": "显示修改文件夹", + "titleText": "高级", + "translationEditorButtonText": "${APP_NAME}翻译编辑器", + "translationFetchErrorText": "翻译状态不可用", + "translationFetchingStatusText": "检查翻译进度…", + "translationInformMe": "所选语言可更新时请通知我!", + "translationNoUpdateNeededText": "当前语言是最新的;呜呼!", + "translationUpdateNeededText": "**当前语言需要更新!! **", + "vrTestingText": "VR测试" + }, + "shareText": "分享", + "sharingText": "分享", + "showText": "显示", + "signInForPromoCodeText": "您必须登录到一个帐户, 代码才能生效", + "signInWithGameCenterText": "使用游戏中心\n应用程序登录。", + "singleGamePlaylistNameText": "仅${GAME}", + "singlePlayerCountText": "一个玩家", + "soloNameFilterText": "单挑模式 ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "角色选择", + "Chosen One": "选定模式", + "Epic": "史诗模式游戏", + "Epic Race": "史诗级竞赛", + "FlagCatcher": "夺旗战", + "Flying": "快乐山区", + "Football": "运旗战", + "ForwardMarch": "突袭战", + "GrandRomp": "征服战", + "Hockey": "冰球战", + "Keep Away": "抓旗战", + "Marching": "塔防战", + "Menu": "主菜单", + "Onslaught": "冲锋战", + "Race": "竞赛", + "Scary": "山丘之王", + "Scores": "得分屏幕", + "Survival": "消除战", + "ToTheDeath": "死亡竞赛", + "Victory": "最终得分屏幕" + }, + "spaceKeyText": "空格", + "statsText": "统计", + "storagePermissionAccessText": "需要存储权限", + "store": { + "alreadyOwnText": "您已拥有${NAME}!", + "bombSquadProDescriptionText": "• 在游戏中赚取双倍点券\n• 移除游戏广告\n• 包括${COUNT}奖励点券\n• +${PERCENT}%联赛得分奖励\n• 解锁'${INF_ONSLAUGHT}'和\n '${INF_RUNAROUND}'合作关卡", + "bombSquadProFeaturesText": "功能:", + "bombSquadProNameText": "${APP_NAME}专业版", + "bombSquadProNewDescriptionText": "• 移除游戏内广告和烦人页面\n• 解锁更多的游戏设置\n• 另外还包括:", + "buyText": "购买", + "charactersText": "人物", + "comingSoonText": "敬请期待……", + "extrasText": "额外部分", + "freeBombSquadProText": "BombSquad现在是免费的,由于最初您是通过购买所得,您将获得\nBombSquad专业版升级和${COUNT}点券,以表感谢。\n尽享全新功能,同时感谢您的支持!\n-Eric", + "gameUpgradesText": "游戏升级", + "holidaySpecialText": "假期特献", + "howToSwitchCharactersText": "(进入\"${SETTINGS} -> ${PLAYER_PROFILES}\"指定和自定义人物)", + "howToUseIconsText": "(升级全球档案以使用图标)", + "howToUseMapsText": "(在团队/混战游戏中使用这些地图)", + "iconsText": "图标", + "loadErrorText": "无法加载页面。\n请检查您的网络连接。", + "loadingText": "加载中", + "mapsText": "地图", + "miniGamesText": "迷你游戏", + "oneTimeOnlyText": "(仅一次)", + "purchaseAlreadyInProgressText": "该物品的购买已在进行中。", + "purchaseConfirmText": "购买${ITEM}?", + "purchaseNotValidError": "购买无效。\n如果这是一个错误,请联系${EMAIL}。", + "purchaseText": "购买", + "saleBundleText": "捆绑销售!", + "saleExclaimText": "出售!", + "salePercentText": "(${PERCENT}%折扣)", + "saleText": "特卖", + "searchText": "搜索", + "teamsFreeForAllGamesText": "团队/混战游戏", + "totalWorthText": "*** 价值${TOTAL_WORTH}! ***", + "upgradeQuestionText": "更新吗?", + "winterSpecialText": "冬季特献", + "youOwnThisText": "- 您已拥有 -" + }, + "storeDescriptionText": "8人派对游戏尽显疯狂!\n\n在爆炸类迷你游戏中炸飞您的好友(或电脑),如夺旗战、冰球战及史诗级慢动作死亡竞赛!\n\n简单的控制和广泛的手柄支持可轻松允许多达8人参与游戏;您甚至可以通过免费的“BombSquad Remote”应用将您的移动设备作为手柄使用!\n\n投射炸弹!\n\n更多信息,请登录www.froemling.net/bombsquad。", + "storeDescriptions": { + "blowUpYourFriendsText": "炸飞你的好友。", + "competeInMiniGamesText": "在从竞速到飞行的迷你游戏中一较高下。", + "customize2Text": "自定义角色、迷你游戏,甚至是背景音乐。", + "customizeText": "自定义角色并创建自己的迷你游戏播放列表。", + "sportsMoreFunText": "加入炸药后运动变得更加有趣。", + "teamUpAgainstComputerText": "组队对抗电脑程序。" + }, + "storeText": "商店", + "submitText": "提交", + "submittingPromoCodeText": "正在提交代码...", + "teamNamesColorText": "团队名称/颜色。。。", + "telnetAccessGrantedText": "Telnet访问已启用。", + "telnetAccessText": "检测到Telnet访问;是否允许?", + "testBuildErrorText": "该测试版已失效;请检查是否存在新版本。", + "testBuildText": "测试版", + "testBuildValidateErrorText": "无法验证测试版。(无网络连接?)", + "testBuildValidatedText": "测试版已通过验证;尽请享用!", + "thankYouText": "感谢您的支持!尽情享受游戏!!", + "threeKillText": "三杀!!", + "timeBonusText": "时间奖励", + "timeElapsedText": "时间耗尽", + "timeExpiredText": "时间结束", + "timeSuffixDaysText": "${COUNT}天", + "timeSuffixHoursText": "${COUNT}时", + "timeSuffixMinutesText": "${COUNT}分", + "timeSuffixSecondsText": "${COUNT}秒", + "tipText": "提示", + "titleText": "炸弹小分队", + "titleVRText": "炸弹小分队 VR", + "topFriendsText": "最佳好友", + "tournamentCheckingStateText": "检查锦标赛状态;请稍候……", + "tournamentEndedText": "本次锦标赛已经结束。一场新的锦标赛即将开始。", + "tournamentEntryText": "锦标赛入口", + "tournamentResultsRecentText": "最近锦标赛结果", + "tournamentStandingsText": "锦标赛积分榜", + "tournamentText": "锦标赛", + "tournamentTimeExpiredText": "锦标赛时间结束", + "tournamentsText": "锦标赛", + "translations": { + "characterNames": { + "Agent Johnson": "约翰逊特工", + "B-9000": "B-9000", + "Bernard": "伯纳德", + "Bones": "骷髅", + "Butch": "牛仔邦奇", + "Easter Bunny": "复活兔", + "Flopsy": "萌兔耷拉", + "Frosty": "冰冰", + "Gretel": "歌者格蕾特", + "Grumbledorf": "男巫道傅", + "Jack Morgan": "杰克摩根", + "Kronk": "克罗克", + "Lee": "李", + "Lucky": "好运者", + "Mel": "梅尔", + "Middle-Man": "平衡之尊", + "Minimus": "迷你姆斯", + "Pascal": "巴斯卡", + "Pixel": "精灵", + "Sammy Slam": "萨米斯拉姆", + "Santa Claus": "圣诞老人", + "Snake Shadow": "蛇影", + "Spaz": "斯巴子", + "Taobao Mascot": "淘公仔", + "Todd McBurton": "托德马克波顿", + "Zoe": "佐伊", + "Zola": "刺杀者佐拉" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME}训练", + "Infinite ${GAME}": "无限${GAME}", + "Infinite Onslaught": "无限冲锋战", + "Infinite Runaround": "无限塔防战", + "Onslaught Training": "冲锋训练", + "Pro ${GAME}": "专业版${GAME}", + "Pro Football": "专业足球战", + "Pro Onslaught": "专业冲锋战", + "Pro Runaround": "专业塔防战", + "Rookie ${GAME}": "新手版${GAME}", + "Rookie Football": "新手橄榄球赛", + "Rookie Onslaught": "新手冲锋战", + "The Last Stand": "最终杀敌战", + "Uber ${GAME}": "高级${GAME}", + "Uber Football": "高级橄榄球赛", + "Uber Onslaught": "高级冲锋战", + "Uber Runaround": "高级塔防战" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "在一定时间内取代选定目标进而获得胜利。\n杀死选定目标并取而代之。", + "Bomb as many targets as you can.": "尽可能多地炸毁目标。", + "Carry the flag for ${ARG1} seconds.": "扛旗${ARG1}秒。", + "Carry the flag for a set length of time.": "在设定时长内扛旗。", + "Crush ${ARG1} of your enemies.": "粉碎${ARG1}敌人。", + "Defeat all enemies.": "打败所有的敌人。", + "Dodge the falling bombs.": "躲避下落的炸弹。", + "Final glorious epic slow motion battle to the death.": "在最后的荣耀史诗级慢动作大战中战斗至死。", + "Gather eggs!": "收集蛋吧!", + "Get the flag to the enemy end zone.": "扛旗进入敌人达阵区。", + "How fast can you defeat the ninjas?": "你能多快地打败忍者?", + "Kill a set number of enemies to win.": "杀死一定数量的敌人来获得胜利。", + "Last one standing wins.": "最终杀敌者获胜。", + "Last remaining alive wins.": "最终幸存者获胜。", + "Last team standing wins.": "最终杀敌团队获胜。", + "Prevent enemies from reaching the exit.": "阻止敌人到达出口。", + "Reach the enemy flag to score.": "抵达敌人的旗帜来得分。", + "Return the enemy flag to score.": "交回敌人的旗帜来得分。", + "Run ${ARG1} laps.": "跑${ARG1}圈。", + "Run ${ARG1} laps. Your entire team has to finish.": "跑${ARG1}圈。你的整个团队必须来完成。", + "Run 1 lap.": "跑1圈。", + "Run 1 lap. Your entire team has to finish.": "跑1圈。你的整个团队必须来完成。", + "Run real fast!": "快速奔跑!", + "Score ${ARG1} goals.": "${ARG1}进球得分。", + "Score ${ARG1} touchdowns.": "${ARG1}触地得分。", + "Score a goal.": "一次进球得分。", + "Score a touchdown.": "一次触地得分。", + "Score some goals.": "多次进球得分。", + "Secure all ${ARG1} flags.": "固定所有的${ARG1}旗帜。", + "Secure all flags on the map to win.": "固定地图上的所有旗帜来获得胜利。", + "Secure the flag for ${ARG1} seconds.": "固定旗帜${ARG1}秒。", + "Secure the flag for a set length of time.": "在设定时长内固定旗帜。", + "Steal the enemy flag ${ARG1} times.": "窃取敌人的旗帜${ARG1}次。", + "Steal the enemy flag.": "窃取敌人的旗帜。", + "There can be only one.": "无敌模式", + "Touch the enemy flag ${ARG1} times.": "触碰敌人的旗帜${ARG1}次。", + "Touch the enemy flag.": "触碰敌人的旗帜。", + "carry the flag for ${ARG1} seconds": "扛旗${ARG1}秒", + "kill ${ARG1} enemies": "杀死${ARG1}敌人", + "last one standing wins": "最终杀敌者获胜", + "last team standing wins": "最终杀敌团队获胜", + "return ${ARG1} flags": "交回${ARG1}旗帜", + "return 1 flag": "交回1面旗帜", + "run ${ARG1} laps": "跑${ARG1}圈", + "run 1 lap": "跑1圈", + "score ${ARG1} goals": "${ARG1}进球得分", + "score ${ARG1} touchdowns": "${ARG1}触地得分", + "score a goal": "一次进球得分", + "score a touchdown": "一次触地得分", + "secure all ${ARG1} flags": "固定所有的${ARG1}旗帜", + "secure the flag for ${ARG1} seconds": "固定旗帜${ARG1}秒", + "touch ${ARG1} flags": "触碰${ARG1}旗帜", + "touch 1 flag": "触碰1面旗帜" + }, + "gameNames": { + "Assault": "突袭战", + "Capture the Flag": "夺旗战", + "Chosen One": "选定模式", + "Conquest": "征服战", + "Death Match": "死亡竞赛", + "Easter Egg Hunt": "猎蛋复活者", + "Elimination": "消除战", + "Football": "运旗战", + "Hockey": "冰球战", + "Keep Away": "抓旗战", + "King of the Hill": "山丘之王", + "Meteor Shower": "流星战", + "Ninja Fight": "忍者大战", + "Onslaught": "冲锋战", + "Race": "竞速赛", + "Runaround": "塔防战", + "Target Practice": "目标训练", + "The Last Stand": "最终杀敌战" + }, + "inputDeviceNames": { + "Keyboard": "键盘", + "Keyboard P2": "键盘P2" + }, + "languages": { + "Arabic": "阿拉伯语", + "Belarussian": "白俄罗斯语", + "Chinese": "简体中文", + "ChineseTraditional": "繁体中文", + "Croatian": "克罗地亚语", + "Czech": "捷克语", + "Danish": "丹麦语", + "Dutch": "荷兰语", + "English": "英语", + "Esperanto": "世界语", + "Finnish": "芬兰语", + "French": "法语", + "German": "德语", + "Gibberish": "用于测试", + "Greek": "希腊语", + "Hindi": "印度语", + "Hungarian": "匈牙利语", + "Indonesian": "印尼语", + "Italian": "意大利语", + "Japanese": "日本语", + "Korean": "朝鲜语", + "Persian": "波斯文", + "Polish": "波兰语", + "Portuguese": "葡萄牙语", + "Romanian": "罗马尼亚语", + "Russian": "俄语", + "Serbian": "塞尔维亚语", + "Slovak": "斯洛伐克语", + "Spanish": "西班牙语", + "Swedish": "瑞典语", + "Turkish": "土耳其语", + "Ukrainian": "乌克兰语", + "Venetian": "威尼斯语", + "Vietnamese": "越南语" + }, + "leagueNames": { + "Bronze": "铜牌联赛", + "Diamond": "钻石联赛", + "Gold": "金牌联赛", + "Silver": "银牌联赛" + }, + "mapsNames": { + "Big G": "大G地图", + "Bridgit": "小桥地图", + "Courtyard": "庭院地图", + "Crag Castle": "岩城地图", + "Doom Shroom": "末日蘑菇地图", + "Football Stadium": "足球场地图", + "Happy Thoughts": "快乐想法", + "Hockey Stadium": "曲棍球场地图", + "Lake Frigid": "寒湖地图", + "Monkey Face": "猴面地图", + "Rampage": "狂暴地图", + "Roundabout": "塔防地图", + "Step Right Up": "攻击地图", + "The Pad": "平板地图", + "Tip Top": "顶点地图", + "Tower D": "塔防地图", + "Zigzag": "蜿蜒地图" + }, + "playlistNames": { + "Just Epic": "仅限史诗级", + "Just Sports": "仅限运动类" + }, + "scoreNames": { + "Flags": "旗帜", + "Goals": "进球", + "Score": "得分", + "Survived": "幸存", + "Time": "时间", + "Time Held": "保持时间" + }, + "serverResponses": { + "A code has already been used on this account.": "该账户已使用代码。", + "A reward has already been given for that address.": "您已经领取过该奖励了", + "Account linking successful!": "账号连接成功!", + "Account unlinking successful!": "取消关联账户成功!", + "Accounts are already linked.": "账号已经连接。", + "An error has occurred; (${ERROR})": "出现了一个错误; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "出现了一个错误,请联系客服获取支持.(${ERROR})", + "An error has occurred; please contact support@froemling.net.": "发生了一个错误,请联系 support@froemling.net。", + "An error has occurred; please try again later.": "发生了一个错误, 请稍候再试", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "确定要链接这些账户?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\n此操作不可撤销!", + "BombSquad Pro unlocked!": "炸弹小分队专业版已解锁!", + "Can't link 2 accounts of this type.": "无法连接2个这种账号。", + "Can't link 2 diamond league accounts.": "无法连接两个钻石联赛账号。", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "无法连接,会超过上限 ${COUNT} 个账号。", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "发现作弊行为;得分及奖励在${COUNT}天内暂停。", + "Could not establish a secure connection.": "无法建立安全连接。", + "Daily maximum reached.": "已达今日上限。", + "Entering tournament...": "进入锦标赛……", + "Invalid code.": "代码无效。", + "Invalid payment; purchase canceled.": "不可用的付款方式:交易取消", + "Invalid promo code.": "促销代码无效。", + "Invalid purchase.": "购买无效。", + "Invalid tournament entry; score will be ignored.": "错误的联赛资料,分数会被忽略。", + "Item unlocked!": "项目已解除锁定!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "连接账号行为取消。${ACCOUNT} 含有\n重要数据可能会丢失。\n如果你想要的话,你可以反向链接账号。\n(那样就会丢失这个账号的数据)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "将帐户${ACCOUNT}关联到此帐户吗?\n${ACCOUNT}将共享数据。\n此操作不能撤销。", + "Max number of playlists reached.": "已达到最大列表数目。", + "Max number of profiles reached.": "已达到最大档案数目。", + "Maximum friend code rewards reached.": "邀请码奖励达到上限", + "Message is too long.": "消息太长.", + "Profile \"${NAME}\" upgraded successfully.": "${NAME}档案升级成功。", + "Profile could not be upgraded.": "档案不可升级。", + "Purchase successful!": "购买成功!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "登录领取${COUNT}点券。\n明日再来领取${TOMORROW_COUNT}。", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "此版本的游戏不再支持服务器功能;\n请更新到较新版本。", + "Sorry, there are no uses remaining on this code.": "对不起,此代码已经无法继续使用了。", + "Sorry, this code has already been used.": "对不起,此代码已被使用。", + "Sorry, this code has expired.": "对不起,此代码已失效。", + "Sorry, this code only works for new accounts.": "对不起,此代码仅适用于新账户。", + "Temporarily unavailable; please try again later.": "目前暂不可用;请稍候再试!", + "The tournament ended before you finished.": "本次锦标赛在你完成之前结束。", + "This account cannot be unlinked for ${NUM} days.": "此帐户无法在${NUM}天内取消关联。", + "This code cannot be used on the account that created it.": "此代码不可在创建其的账户上使用。", + "This is currently unavailable; please try again later.": "当前不可用:请稍后再试", + "This requires version ${VERSION} or newer.": "这需要版本${VERSION}或更高版本。", + "Tournaments disabled due to rooted device.": "该设备已禁用比赛。", + "Tournaments require ${VERSION} or newer": "比赛需要${VERSION}或更高版本", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "从此帐户取消 ${ACCOUNT} 的连结?\n${ACCOUNT}上的所有数据将被重置。\n(在某些情况下除成就外)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "警告:针对您的帐户发出黑客投诉。\n被盗用的帐户将被禁止。请公平竞技。", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "是否将您的设备帐户链接到此?\n\n您的设备账户为${ACCOUNT1}\n此帐户为${ACCOUNT2}\n\n您可保存现有进度。\n警告: 此操作不可撤消!", + "You already own this!": "你已拥有了!", + "You can join in ${COUNT} seconds.": "你在${COUNT} 秒后可以加入", + "You don't have enough tickets for this!": "你的点券不足!", + "You don't own that.": "你尚未拥有", + "You got ${COUNT} tickets!": "你获得了${COUNT}点券!", + "You got a ${ITEM}!": "你获得了一个${ITEM}!", + "You have been promoted to a new league; congratulations!": "你已被升级至一个全新联赛;恭喜!", + "You must update to a newer version of the app to do this.": "你必須先更新遊戲才能這麼做。", + "You must update to the newest version of the game to do this.": "你必须更新到最新版来做到这一点。", + "You must wait a few seconds before entering a new code.": "你必须在输入新代码前稍等几秒。", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "你在上一场锦标赛中排名#${RANK}。多谢玩赏本游戏!", + "Your account was rejected. Are you signed in?": "您的账号被拒绝。您是否已登录?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "你的游戏副本已被更改。\n请恢复任何更改并重试。", + "Your friend code was used by ${ACCOUNT}": "${ACCOUNT}已使用您的好友代码" + }, + "settingNames": { + "1 Minute": "1分钟", + "1 Second": "1秒钟", + "10 Minutes": "10分钟", + "2 Minutes": "2分钟", + "2 Seconds": "2秒钟", + "20 Minutes": "20分钟", + "4 Seconds": "4秒钟", + "5 Minutes": "5分钟", + "8 Seconds": "8秒钟", + "Allow Negative Scores": "允許負分", + "Balance Total Lives": "平衡总生命", + "Bomb Spawning": "生成炸弹", + "Chosen One Gets Gloves": "选定目标获取手套", + "Chosen One Gets Shield": "选定目标获取盾牌", + "Chosen One Time": "选定模式时间", + "Enable Impact Bombs": "启用冲击炸弹", + "Enable Triple Bombs": "启用三连炸弹", + "Entire Team Must Finish": "整个队伍必须一起通过", + "Epic Mode": "史诗模式", + "Flag Idle Return Time": "旗帜闲置返回时间", + "Flag Touch Return Time": "旗帜触碰返回时间", + "Hold Time": "保持时间", + "Kills to Win Per Player": "每一玩家取胜击杀数", + "Laps": "圈数", + "Lives Per Player": "每一玩家生命", + "Long": "长", + "Longer": "更长", + "Mine Spawning": "地雷增生", + "No Mines": "无地雷", + "None": "无", + "Normal": "正常", + "Pro Mode": "专业模式", + "Respawn Times": "重生時長", + "Score to Win": "得分取胜", + "Short": "短", + "Shorter": "更短", + "Solo Mode": "单人模式", + "Target Count": "目标计数", + "Time Limit": "时限" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM}被踢,因为 ${PLAYER}离开了", + "Killing ${NAME} for skipping part of the track!": "杀死跳过部分赛道的${NAME}!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "警告 ${NAME}: 超幅 / 散播按钮 将使你被踢出。" + }, + "teamNames": { + "Bad Guys": "坏人队", + "Blue": "蓝队", + "Good Guys": "好人队", + "Red": "红队" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "一记完美、及时的“跑跳旋转拳”可一次性击杀敌人,并\n助你一生享有好友的尊重。", + "Always remember to floss.": "地面上的辅助线可能会有用。", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "使用首选名称和外观,而非采用随机形式\n来为自己和好友创建玩家档案。", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "诅咒之盒把你变成了一个定时炸弹。\n唯一的解决方法是迅速抢占一个生命值包。", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "尽管长相不同,所有人物的技能是相同的,\n所以只需随意挑选一个与你最相似的。", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "不要因为拥有能量盾牌而狂妄自大;你仍然可能使自己坠入悬崖。", + "Don't run all the time. Really. You will fall off cliffs.": "不要总是奔跑。真的。你可能会坠入悬崖。", + "Don't spin for too long; you'll become dizzy and fall.": "不要旋转得太久,不然你会眩晕并摔倒。", + "Hold any button to run. (Trigger buttons work well if you have them)": "按住任意按钮来奔跑。(如果你有的话,扳机按钮将会很有用)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "按住任意按钮来奔跑。你将会更快地抵达一些地方,\n但是转弯效果并不好,所以当心悬崖。", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "寒冰炸弹并非很厉害,但它们能够冻结\n任何被击中者,使他们极易粉碎。", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "如果有人将你提起,出拳攻击他们,他们便会放手。\n这在现实生活中同样有效。", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "如果您缺控制器,在手機上安裝「${REMOTE_APP_NAME}」\n並用手機當控制器。", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "如果你没有手柄,请在你的iOS或Android设备上安装\n“BombSquad Remote”应用程序,并将它们作为手柄使用。", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "如果一个黏黏弹将你困住,你应该四处跳动并转圈。你可能\n将炸弹抖落,或如果没有其他办法,你最后的时刻将是有趣的。", + "If you kill an enemy in one hit you get double points for it.": "如果你一击杀死一个敌人,你将获得双倍积分。", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "如果你捡到一个诅咒之盒,你唯一的生存希望是\n在接下来的几秒内找到一个生命值提升器。", + "If you stay in one place, you're toast. Run and dodge to survive..": "如果你停留在一个地方,你就完了。为了生存而奔跑和躲避……", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "如果众多玩家进进出出,在设置下打开“自动踢出闲置玩家”,以防\n任何玩家忘记离开游戏。", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "如果你的设备过热,或者你想要节省电池电量,\n则在设置->图形中调低“视觉效果”或“分辨率”", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "如果你的帧速率不稳定,请尝试在游戏的\n图形设置中调低分辨率或视觉效果。", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "在夺旗战中,你的旗帜必须位于你的基地才能得分,如果对方\n团队即将得分,窃取他们的旗帜是一个不错的阻止方法。", + "In hockey, you'll maintain more speed if you turn gradually.": "在冰球战中,逐渐转向将使你保持更快的速度。", + "It's easier to win with a friend or two helping.": "在拥有一名好友或两个帮扶的情况下更易获胜。", + "Jump just as you're throwing to get bombs up to the highest levels.": "就像你试图将炸弹扔到最高点那样跳起来。", + "Land-mines are a good way to stop speedy enemies.": "地雷是阻止高速敌人的一个很好的方式。", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "很多东西都可以捡起来并投掷,包括其他玩家。将你的\n敌人抛下悬崖可能是一个有效的且情感上可获得满足的策略。", + "No, you can't get up on the ledge. You have to throw bombs.": "不,你不能在岩脊上起身。你必须要投掷炸弹。", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "玩家可在大多数游戏中途加入或离开,\n同时,你也可以在百忙中插上或拔出手柄。", + "Practice using your momentum to throw bombs more accurately.": "练习借助你的力量更准确地投掷炸弹。", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "拳头跑得越快,拳击的伤害越高,\n所以请成为飞奔的拳击手吧。", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "在投掷炸弹之前来回跑动,\n以“鞭打”炸弹,并将其投掷更远。", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "在TNT炸药箱附近引爆\n一个炸弹来消灭一群敌人。", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "头部是最脆弱的区域,所以一个黏黏弹\n接触头部通常便意味着游戏结束。", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "这一关卡永远不会结束,但是更高的得分将\n助你赢得全世界永恒的尊重。", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "投掷力量取决于你所保持的方向。\n如要向前方轻轻投掷某物,不要保持在任何方向。", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "更换背景音乐?更换成你自己音乐吧!\n参见设置->音频->背景音乐", + "Try 'Cooking off' bombs for a second or two before throwing them.": "尝试在投掷之前将炸弹“爆燃”一秒或两秒。", + "Try tricking enemies into killing eachother or running off cliffs.": "试图诱使敌人互相厮杀或坠入悬崖。", + "Use the pick-up button to grab the flag < ${PICKUP} >": "使用拾取按钮来抢夺旗帜< ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "来回鞭打以投掷更远距离……", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "你可以通过左转或右转“瞄准”出拳。\n这有利于将坏人击倒出边界或在冰球战中得分。", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "你可以根据导火线火花的颜色判断炸弹什么时候爆炸:\n黄色……橙色……红色……嘭。", + "You can throw bombs higher if you jump just before throwing.": "如果投弹前跳起,你将投掷更远。", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "当你用头部重击物体时将受到伤害,\n所以尽量不要用头部重击物体。", + "Your punches do much more damage if you are running or spinning.": "如果你奔跑或旋转,拳击的伤害将更高。" + } + }, + "trophiesRequiredText": "你必须要有至少 ${NUMBER} 个奖杯", + "trophiesText": "奖杯", + "trophiesThisSeasonText": "本赛季奖杯", + "tutorial": { + "cpuBenchmarkText": "以惊人的速度运行教程(主要用于测试CPU速度)", + "phrase01Text": "嗨,您好!", + "phrase02Text": "欢迎来到${APP_NAME}!", + "phrase03Text": "以下是用于控制你的角色的一些技巧:", + "phrase04Text": "${APP_NAME}的很多方面是以物理为基础的。", + "phrase05Text": "例如,当你出拳时,", + "phrase06Text": "伤害程度取决于你拳头的速度。", + "phrase07Text": "看到没?如果我们不动,这样几乎不会伤害${NAME}。", + "phrase08Text": "现在,让我们跳跃并旋转起来,以获得更快的速度。", + "phrase09Text": "啊,这样好多了。", + "phrase10Text": "奔跑也会发挥作用。", + "phrase11Text": "按住任意按钮来奔跑。", + "phrase12Text": "如要获得超赞的出拳,请尝试持续奔跑并旋转。", + "phrase13Text": "哎呦;关于${NAME}十分抱歉。", + "phrase14Text": "你可以捡起并投掷物体,如旗帜或${NAME}。", + "phrase15Text": "最后,还有炸弹。", + "phrase16Text": "投掷炸弹需要练习。", + "phrase17Text": "哎哟!这一记投掷并不漂亮。", + "phrase18Text": "移动有助你投掷得更远。", + "phrase19Text": "跳跃有助你投掷得更高。", + "phrase20Text": "“鞭打”你的炸弹以抛出更远的距离。", + "phrase21Text": "定时你的炸弹可能会非常棘手。", + "phrase22Text": "我靠。", + "phrase23Text": "尝试将导火线“爆燃”一秒或两秒。", + "phrase24Text": "万岁!爆燃超赞。", + "phrase25Text": "好吧,已经很不错了。", + "phrase26Text": "去完成你的任务吧,老铁!", + "phrase27Text": "记住你的训练,你会活着回来的!", + "phrase28Text": "......好吧,也许...", + "phrase29Text": "祝好运!", + "randomName1Text": "弗雷德", + "randomName2Text": "哈里", + "randomName3Text": "比尔", + "randomName4Text": "查克", + "randomName5Text": "菲尔", + "skipConfirmText": "确定跳过教程?点击或按下按钮以确认。", + "skipVoteCountText": "${COUNT}/${TOTAL}跳过投票", + "skippingText": "跳过教程……", + "toSkipPressAnythingText": "(点击或按下任何按钮以跳过教程)" + }, + "twoKillText": "双杀!", + "unavailableText": "不可用", + "unconfiguredControllerDetectedText": "检测到未配置的手柄:", + "unlockThisInTheStoreText": "这必须在商店中解锁。", + "unlockThisProfilesText": "如需创建超过 ${NUM} 个玩家档案,你需要", + "unlockThisText": "你需要这些来解锁", + "unsupportedHardwareText": "抱歉,此版本的游戏不支持该硬件。", + "upFirstText": "进入第一局:", + "upNextText": "进入比赛${COUNT}第二局:", + "updatingAccountText": "更新您的帐户……", + "upgradeText": "升级", + "upgradeToPlayText": "在游戏商店中解锁\"${PRO}\",以体验该游戏。", + "useDefaultText": "使用默认值", + "usesExternalControllerText": "该游戏使用外部手柄进行输入。", + "usingItunesText": "使用音乐应用设置背景音乐……", + "usingItunesTurnRepeatAndShuffleOnText": "请确认iTunes中随机播放已开启且重复全部歌曲。", + "validatingTestBuildText": "测试版验证中……", + "victoryText": "胜利!", + "voteDelayText": "${NUMBER} 秒内你不能发起另一个投票", + "voteInProgressText": "已经有一个投票在进行中了", + "votedAlreadyText": "你已经投过票啦!", + "votesNeededText": "通过需要 ${NUMBER} 个投票", + "vsText": "vs.", + "waitingForHostText": "(等待${HOST}以继续)", + "waitingForPlayersText": "等待玩家的加入……", + "waitingInLineText": "正在排队等候(人满为患)...", + "watchAVideoText": "看一个小广告视频", + "watchAnAdText": "观看广告", + "watchWindow": { + "deleteConfirmText": "删除\"${REPLAY}\"?", + "deleteReplayButtonText": "删除\n回放", + "myReplaysText": "我的回放", + "noReplaySelectedErrorText": "未选择回放", + "playbackSpeedText": "回放速度: ${SPEED}", + "renameReplayButtonText": "重命名\n录像", + "renameReplayText": "重命名\"${REPLAY}\"至:", + "renameText": "重命名", + "replayDeleteErrorText": "删除回放错误。", + "replayNameText": "回放名称", + "replayRenameErrorAlreadyExistsText": "该名称的回放已经存在。", + "replayRenameErrorInvalidName": "无法重命名回放;名称无效。", + "replayRenameErrorText": "重命名回放错误。", + "sharedReplaysText": "共享回放", + "titleText": "观看回放", + "watchReplayButtonText": "观看\n回放" + }, + "waveText": "波", + "wellSureText": "确定!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote版权所有" + }, + "wiimoteListenWindow": { + "listeningText": "监听Wiimotes……", + "pressText": "同时按下Wiimote按钮1和2。", + "pressText2": "在较新的安装有Motion Plus的Wiimotes上,可按下背部的红色“同步”按钮作为替代。" + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote版权所有", + "listenText": "监听", + "macInstructionsText": "确保您Mac上的Wii已关闭且蓝牙已启用\n,然后按下“监听”。Wiimote支持可能出现稍许不稳定,\n所以在完成连接之前\n您可能需要尝试多次。\n\n蓝牙需能够处理多达7台连接设备,\n但您的里程可能会有所不同。\n\nBombSquad支持原装Wiimotes、Nunchuks\n及经典手柄。\n较新版本的Wii Remote Plus现也已可使用,\n但与附件不兼容", + "thanksText": "感谢DarwiinRemote团队的努力,\n这一切已成为可能。", + "titleText": "Wiimote设置" + }, + "winsPlayerText": "${NAME}获胜!", + "winsTeamText": "${NAME}获胜!", + "winsText": "${NAME}获胜!", + "worldScoresUnavailableText": "全球得分不可用。", + "worldsBestScoresText": "全球最高得分", + "worldsBestTimesText": "全球最佳时间", + "xbox360ControllersWindow": { + "getDriverText": "获取驱动程序", + "macInstructions2Text": "如要使用无线手柄,您还需要\n“用于Windows的无线Xbox360手柄”自带的接收器。\n一个接收器可让您连接多达4个手柄。\n\n重要提示:第三方接收器与此驱动程序不兼容;\n请确保您的接收器标示有'Microsoft',而非'XBOX 360'。\nMicrosoft不再单独出售该设备,所以您需要获得\n手柄附带的接收器或在eBay搜索其他设备。\n\n如果这对您有帮助,请考虑在驱动程序开发人员\n网站进行捐赠。", + "macInstructionsText": "如要使用Xbox 360手柄,您需要安装\n通过以下链接所获得的Mac驱动程序。\n该驱动程序与有线和无线手柄二者兼容。", + "ouyaInstructionsText": "如要在BombSquad中使用有线Xbox 360手柄,只需\n将其插入您设备的USB端口。您可以使用USB集线器\n以连接多个手柄。\n\n如要使用无线手柄,您需要一台无线接收器,\n其可作为“用于Windows的无线Xbox360手柄”\n安装包的一部分或单独出售。每一接收器插入一个USB端口,\n允许您连接多达4台无线手柄。", + "ouyaInstructionsTextScale": 0.8, + "titleText": "在${APP_NAME}中使用Xbox 360手柄:" + }, + "yesAllowText": "是的,允许!", + "yourBestScoresText": "你的最高得分", + "yourBestTimesText": "你的最佳时刻" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/chinesetraditional.json b/dist/ba_data/data/languages/chinesetraditional.json new file mode 100644 index 0000000..3c92a58 --- /dev/null +++ b/dist/ba_data/data/languages/chinesetraditional.json @@ -0,0 +1,1844 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "帳號名不可包含表情符號或其他特殊字符", + "accountsText": "帳號", + "achievementProgressText": "已完成${TOTAL}個成就中的${COUNT}個", + "campaignProgressText": "戰役進度[困難]: ${PROGRESS}", + "changeOncePerSeason": "每一賽季只有一次更改機會", + "changeOncePerSeasonError": "您必須等${NUM}天到下個賽季改變此選項", + "customName": "自定義名稱", + "linkAccountsEnterCodeText": "輸入代碼", + "linkAccountsGenerateCodeText": "生成代碼", + "linkAccountsInfoText": "(在不同的平台上共享遊戲進度)", + "linkAccountsInstructionsNewText": "要連結兩個帳號,在第一個帳號里點擊“生成代碼”按鈕。\n在第二個帳號裡輸入那個代碼。\n兩個帳號數據將被共享。\n(第一個帳號的數據將會消失)\n\n您總共可連結${COUNT}個帳號。\n\n重要訊息:你只能連結你自己的帳號;\n如果你連結了朋友的帳號, \n你將無法同時線上遊玩。", + "linkAccountsText": "連結帳號", + "linkedAccountsText": "已連結的帳號:", + "nameChangeConfirm": "更改帳號名為${NAME}?", + "resetProgressConfirmNoAchievementsText": "這會重設你的合作模式進度和\n本地高分紀錄 (但不包含你的票卷)。\n這是不可回復的操作,你確定嗎?", + "resetProgressConfirmText": "這會重設你的合作模式進度,\n成就和本地高分\n(但不包括你的票卷)。這是不可\n回復的操作。你確定嗎?", + "resetProgressText": "重置遊戲進程", + "setAccountName": "設置帳號名", + "setAccountNameDesc": "選擇你要為你帳號使用的遊戲內名稱。\n你直接使用你其中一個已連結的賬戶的名稱或\n創造一個獨特的自定義名稱。", + "signInInfoText": "登入得以收集票卷,完成線上\n和與其他裝置共享進度", + "signInText": "登入", + "signInWithDeviceInfoText": "一個這裝置現有的自動帳號", + "signInWithDeviceText": "使用設備賬戶登入", + "signInWithGameCircleText": "使用Game Circle登入", + "signInWithGooglePlayText": "用play商店登入", + "signInWithTestAccountInfoText": "(舊有的帳號登入方式;使用後來的創設的帳號)", + "signInWithTestAccountText": "用測試帳號登入", + "signOutText": "登出", + "signingInText": "登入中…", + "signingOutText": "登出中…", + "ticketsText": "擁有票券: ${COUNT}", + "titleText": "帳號", + "unlinkAccountsInstructionsText": "選擇一個要解除連結的帳號", + "unlinkAccountsText": "解除連結帳號", + "viaAccount": "(透過帳號 ${NAME})", + "youAreSignedInAsText": "您以這帳號登入:" + }, + "achievementChallengesText": "成就挑戰", + "achievementText": "成就", + "achievements": { + "Boom Goes the Dynamite": { + "description": "用TNT殺死了三個壞蛋", + "descriptionComplete": "用TNT殺死了三個壞蛋", + "descriptionFull": "在${LEVEL}中用TNT殺3個壞蛋", + "descriptionFullComplete": "在 ${LEVEL}中殺死了三個壞蛋", + "name": "炸彈爆炸了" + }, + "Boxer": { + "description": "沒用任何炸彈的情況下獲勝", + "descriptionComplete": "在沒用任何炸彈的情況下獲勝了", + "descriptionFull": "在沒用任何炸彈的情況下完成 ${LEVEL}", + "descriptionFullComplete": "在沒用任何炸彈的情況下完成了 ${LEVEL}", + "name": "拳擊手" + }, + "Dual Wielding": { + "descriptionFull": "鏈接兩個控制器(硬體或手機應用程式)", + "descriptionFullComplete": "兩個控制器已連接(硬體或應用程式)", + "name": "成雙成對" + }, + "Flawless Victory": { + "description": "毫髮無傷", + "descriptionComplete": "毫髮無傷地贏了", + "descriptionFull": "毫髮無傷地在 ${LEVEL}獲勝", + "descriptionFullComplete": "毫髮無傷地在 ${LEVEL}獲勝了", + "name": "完美獲勝" + }, + "Free Loader": { + "descriptionFull": "與兩位以上的玩家開始一場自由死鬥遊戲", + "descriptionFullComplete": "已與兩位以上的玩家開始了一場自由死鬥遊戲", + "name": "自由的戰鬥民族" + }, + "Gold Miner": { + "description": "用地雷殺死六個壞蛋", + "descriptionComplete": "已用地雷殺死了六個壞蛋", + "descriptionFull": "在 ${LEVEL}用地雷殺死六個壞蛋", + "descriptionFullComplete": "在 ${LEVEL}用地雷殺死了六個壞蛋", + "name": "淘金礦工" + }, + "Got the Moves": { + "description": "在不揮拳或使用炸彈的情況下獲勝", + "descriptionComplete": "在不揮拳或使用炸彈的情況下獲勝了", + "descriptionFull": "不揮拳或使用炸彈的情況下在 ${LEVEL}獲勝", + "descriptionFullComplete": "不揮拳或使用炸彈的情況下在 ${LEVEL}獲勝了", + "name": "蛇皮走位" + }, + "In Control": { + "descriptionFull": "連結一個控制器 (硬體或應用程式)", + "descriptionFullComplete": "已連結一個控制器 (硬體或應用程式)", + "name": "一切都在掌控之中" + }, + "Last Stand God": { + "description": "得到1000分", + "descriptionComplete": "已得到1000分", + "descriptionFull": "在 ${LEVEL}得到1000分", + "descriptionFullComplete": "已在 ${LEVEL}得到了1000分", + "name": "${LEVEL} 之神" + }, + "Last Stand Master": { + "description": "贏得250分", + "descriptionComplete": "已贏得了250分", + "descriptionFull": "在 ${LEVEL}贏得250分", + "descriptionFullComplete": "在 ${LEVEL}贏得了250分", + "name": "${LEVEL}大師" + }, + "Last Stand Wizard": { + "description": "贏得500分", + "descriptionComplete": "已贏得了500分", + "descriptionFull": "在 ${LEVEL}贏得500分", + "descriptionFullComplete": "在 ${LEVEL}贏得了500分", + "name": "${LEVEL}巫師" + }, + "Mine Games": { + "description": "用地雷殺死三個壞蛋", + "descriptionComplete": "已用地雷殺死了三個壞蛋", + "descriptionFull": "在 ${LEVEL}用地雷殺死三個壞蛋", + "descriptionFullComplete": "在 ${LEVEL}用地雷殺死了三個壞蛋", + "name": "地雷遊戲" + }, + "Off You Go Then": { + "description": "把三個壞蛋丟出地圖外", + "descriptionComplete": "把三個壞蛋丟出地圖外了", + "descriptionFull": "在 ${LEVEL}把三個壞蛋丟出地圖外", + "descriptionFullComplete": "在 ${LEVEL}把三個壞蛋丟出地圖外了", + "name": "滾出去!!!" + }, + "Onslaught God": { + "description": "贏得5000分", + "descriptionComplete": "贏得了5000分", + "descriptionFull": "在 ${LEVEL}贏得5000分", + "descriptionFullComplete": "在 ${LEVEL}贏得了5000分", + "name": "${LEVEL}衝鋒神" + }, + "Onslaught Master": { + "description": "得分500分", + "descriptionComplete": "獲得500分", + "descriptionFull": "在${LEVEL}裡獲得500分", + "descriptionFullComplete": "在${LEVEL}裡獲得500分", + "name": "${LEVEL}專家" + }, + "Onslaught Training Victory": { + "description": "擊敗所有敵人", + "descriptionComplete": "擊敗所有敵人", + "descriptionFull": "擊敗${LEVEL}中的所有敵人", + "descriptionFullComplete": "擊敗了${LEVEL}中的所有敵人", + "name": "${LEVEL}獲勝" + }, + "Onslaught Wizard": { + "description": "獲得1000分", + "descriptionComplete": "獲得1000分", + "descriptionFull": "在${LEVEL}裡獲得1000分", + "descriptionFullComplete": "在${LEVEL}裡獲得了1000分", + "name": "${LEVEL}精英" + }, + "Precision Bombing": { + "description": "不使用任何道具獲勝", + "descriptionComplete": "不使用任何道具就獲勝了", + "descriptionFull": "在${LEVEL}裡不使用任何道具獲勝", + "descriptionFullComplete": "在${LEVEL}裡不使用任何道具就", + "name": "精準爆破" + }, + "Pro Boxer": { + "description": "不使用任何炸彈獲勝", + "descriptionComplete": "不使用任何炸彈就獲勝了", + "descriptionFull": "在${LEVEL}裡不使用任何炸彈獲勝", + "descriptionFullComplete": "在${LEVEL}裡不使用任何炸彈就獲勝了", + "name": "職業拳擊手" + }, + "Pro Football Shutout": { + "description": "在敵人不得分的情況下獲勝", + "descriptionComplete": "在敵人不得分的情況下就獲勝了", + "descriptionFull": "在${LEVEL}裡不然敵人得分就獲勝了", + "descriptionFullComplete": "在${LEVEL}裡不然敵人得分就獲勝了", + "name": "無懈可擊的${LEVEL}" + }, + "Pro Football Victory": { + "description": "贏得比賽", + "descriptionComplete": "贏得了比賽", + "descriptionFull": "在${LEVEL}裡獲勝", + "descriptionFullComplete": "在${LEVEL}裡獲勝了", + "name": "${LEVEL}獲勝" + }, + "Pro Onslaught Victory": { + "description": "擊敗所有敵人", + "descriptionComplete": "擊敗了所有敵人", + "descriptionFull": "擊敗${LEVEL}裡的所有敵人", + "descriptionFullComplete": "擊敗了${LEVEL}裡的所有敵人", + "name": "${LEVEL}獲勝" + }, + "Pro Runaround Victory": { + "description": "擊敗所有敵人", + "descriptionComplete": "擊敗了所有敵人", + "descriptionFull": "擊敗${LEVEL}裡的所有敵人", + "descriptionFullComplete": "擊敗了${LEVEL}裡的所有敵人", + "name": "${LEVEL}h獲勝" + }, + "Rookie Football Shutout": { + "description": "不讓敵人得分的情況下獲勝", + "descriptionComplete": "不讓敵人得分的情況下就獲勝了", + "descriptionFull": "不讓敵人得分的情況下獲得${LEVEL}的勝利", + "descriptionFullComplete": "不讓敵人得分的情況下就在${LEVEL}獲勝", + "name": "無懈可擊的${LEVEL}" + }, + "Rookie Football Victory": { + "description": "獲得比賽的勝利", + "descriptionComplete": "獲得了比賽的勝利", + "descriptionFull": "在${LEVEL}裡獲勝", + "descriptionFullComplete": "在${LEVEL}裡獲勝了", + "name": "${LEVEL}獲勝" + }, + "Rookie Onslaught Victory": { + "description": "擊敗所有敵人", + "descriptionComplete": "擊敗了所有敵人", + "descriptionFull": "擊敗${LEVEL}裡的所有敵人", + "descriptionFullComplete": "擊敗了${LEVEL}裡的所有敵人", + "name": "${LEVEL}獲勝" + }, + "Runaround God": { + "description": "獲得2000分", + "descriptionComplete": "獲得了2000分", + "descriptionFull": "在${LEVEL}裡獲得2000分", + "descriptionFullComplete": "在${LEVEL}裡獲得了2000分", + "name": "${LEVEL}大師" + }, + "Runaround Master": { + "description": "獲得500分", + "descriptionComplete": "獲得了500分", + "descriptionFull": "在${LEVEL}裡獲得500分", + "descriptionFullComplete": "在${LEVEL}裡獲得了500分", + "name": "${LEVEL}專家" + }, + "Runaround Wizard": { + "description": "獲得1000分", + "descriptionComplete": "獲得了1000分", + "descriptionFull": "在${LEVEL}裡獲得1000分", + "descriptionFullComplete": "在${LEVEL}裡獲得了1000分", + "name": "${LEVEL}精英" + }, + "Sharing is Caring": { + "descriptionFull": "成功給朋友推薦遊戲", + "descriptionFullComplete": "成功給朋友推薦了遊戲", + "name": "分享的快樂" + }, + "Stayin' Alive": { + "description": "不死亡獲勝", + "descriptionComplete": "不死亡就獲勝了", + "descriptionFull": "在${LEVEL}裡不死亡獲勝", + "descriptionFullComplete": "在${LEVEL}裡不死亡就獲勝了", + "name": "我……還活著" + }, + "Super Mega Punch": { + "description": "一拳造成100%的傷害", + "descriptionComplete": "一拳造成了100%的傷害", + "descriptionFull": "在${LEVEL}裡一拳造成100%的傷害", + "descriptionFullComplete": "在${LEVEL}裡一拳造成了100%的傷害", + "name": "鑽石之拳" + }, + "Super Punch": { + "description": "一拳造成50%的傷害", + "descriptionComplete": "一拳造成了50%的傷害", + "descriptionFull": "在${LEVEL}裡一拳造成了50%的傷害", + "descriptionFullComplete": "在${LEVEL}裡一拳造成了50%的傷害", + "name": "黃金之拳" + }, + "TNT Terror": { + "description": "用TNT殺死六個敵人", + "descriptionComplete": "用TNT殺死了六個敵人", + "descriptionFull": "在${LEVEL}裡用TNT殺死六個敵人", + "descriptionFullComplete": "在${LEVEL}裡用TNT殺死了六個敵人", + "name": "爆破鬼才" + }, + "Team Player": { + "descriptionFull": "開始一個四人以上的比賽", + "descriptionFullComplete": "開始了一個四人以上的比賽", + "name": "團隊遊戲" + }, + "The Great Wall": { + "description": "阻止所有敵人", + "descriptionComplete": "阻止了所有敵人", + "descriptionFull": "在${LEVEL}裡阻止了所有敵人", + "descriptionFullComplete": "在${LEVEL}裡阻止了所有敵人", + "name": "城牆" + }, + "The Wall": { + "description": "阻止所有敵人", + "descriptionComplete": "阻止了所有敵人", + "descriptionFull": "在${LEVEL}裡阻止了所有敵人", + "descriptionFullComplete": "在${LEVEL}裡阻止了所有敵人", + "name": "高牆" + }, + "Uber Football Shutout": { + "description": "不讓敵人得分並獲勝", + "descriptionComplete": "不讓敵人得分就獲勝了", + "descriptionFull": "在${LEVEL}裡不讓敵人得分就是獲勝了", + "descriptionFullComplete": "在${LEVEL}裡不讓敵人得分就獲勝了", + "name": "無懈可擊的${LEVEL}" + }, + "Uber Football Victory": { + "description": "贏得比賽", + "descriptionComplete": "贏得比賽", + "descriptionFull": "在${LEVEL}裡獲勝", + "descriptionFullComplete": "在${LEVEL}裡獲得勝利", + "name": "${LEVEL}獲勝" + }, + "Uber Onslaught Victory": { + "description": "擊敗所有敵人", + "descriptionComplete": "擊敗了所有敵人", + "descriptionFull": "擊敗了${LEVEL}裡的所有敵人", + "descriptionFullComplete": "擊敗了${LEVEL}裡的所有敵人", + "name": "${LEVEL}勝利" + }, + "Uber Runaround Victory": { + "description": "擊敗所有敵人", + "descriptionComplete": "擊敗了所有敵人", + "descriptionFull": "擊敗了${LEVEL}裡的所有敵人", + "descriptionFullComplete": "擊敗了${LEVEL}裡的所有敵人", + "name": "${LEVEL}勝利" + } + }, + "achievementsRemainingText": "未完成的成就", + "achievementsText": "成就", + "achievementsUnavailableForOldSeasonsText": "抱歉,往屆成就細節不可用", + "addGameWindow": { + "getMoreGamesText": "獲取更多比賽模式", + "titleText": "新增比賽" + }, + "allowText": "允許", + "alreadySignedInText": "你的賬號已在其他設備上登錄\n請退出其他設備的登錄\n然後重試", + "apiVersionErrorText": "無法加載模塊${NAME},它的API版本為${VERSION_USED},我們需要${VERSION_REQUIRED}。", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(“自動”僅在插入耳機時有效)", + "headRelativeVRAudioText": "頭戴式VR音頻", + "musicVolumeText": "音樂音量", + "soundVolumeText": "音效音量", + "soundtrackButtonText": "自定義音軌", + "soundtrackDescriptionText": "(導入自定義的音樂)", + "titleText": "音樂音量" + }, + "autoText": "自動", + "backText": "返回", + "banThisPlayerText": "禁止這個玩家", + "bestOfFinalText": "${COUNT}的最後一局", + "bestOfSeriesText": "${COUNT}決勝制", + "bestRankText": "你的最佳是#${RANK}", + "bestRatingText": "你的最高評價是${RATING}", + "bombBoldText": "炸彈", + "bombText": "炸彈", + "boostText": "加速", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME}是在应用本身中配置的。", + "buttonText": "按鈕", + "canWeDebugText": "你想要炸彈小分隊自動報告\n錯誤信息給開發人員嗎?\n\n信息中不包含個人信息\n可用於遊戲更加穩定", + "cancelText": "取消", + "cantConfigureDeviceText": "抱歉,${DEVICE}不可配置", + "challengeEndedText": "此比賽挑戰已結束", + "chatMuteText": "屏蔽消息", + "chatMutedText": "屏蔽聊天信息", + "chatUnMuteText": "取消屏蔽消息", + "choosingPlayerText": "<選擇玩家>", + "completeThisLevelToProceedText": "你需要先完成\n這一關", + "completionBonusText": "完成獎勵", + "configControllersWindow": { + "configureControllersText": "設置遊戲手柄", + "configureKeyboard2Text": "設置本地第二鍵盤", + "configureKeyboardText": "設置鍵盤", + "configureMobileText": "用移動電話作控制器", + "configureTouchText": "設置觸屏", + "ps3Text": "PS3 控制器設置", + "titleText": "手柄", + "wiimotesText": "Wiimotes", + "xbox360Text": "Xbox 360 手柄" + }, + "configGamepadSelectWindow": { + "androidNoteText": "注意:手柄支持取決於你的Android版本", + "pressAnyButtonText": "點擊手柄任意按鈕\n以繼續設置...", + "titleText": "手柄測試" + }, + "configGamepadWindow": { + "advancedText": "高級設置", + "advancedTitleText": "高級控制器設置", + "analogStickDeadZoneDescriptionText": "(釋放搖桿時角色出現“漂移”時調高)", + "analogStickDeadZoneText": "模擬搖桿盲區", + "appliesToAllText": "(適用於所有該類型手柄)", + "autoRecalibrateDescriptionText": "(角色未全速移動時有效)", + "autoRecalibrateText": "自動校準模擬搖桿", + "axisText": "搖桿軸", + "clearText": "清除", + "dpadText": "方向鍵", + "extraStartButtonText": "額外啟動按鈕", + "ifNothingHappensTryAnalogText": "如果無法使用,請手動設置模擬搖桿", + "ifNothingHappensTryDpadText": "如果無法使用,請手動設置方向鍵", + "ignoreCompletelyDescriptionText": "(避免這個控制器影響遊戲或菜單)", + "ignoreCompletelyText": "已完全忽略", + "ignoredButton1Text": "已忽略 按鍵1", + "ignoredButton2Text": "已忽略 按鍵2", + "ignoredButton3Text": "已忽略 按鍵3", + "ignoredButton4Text": "已屏蔽 按鍵4", + "ignoredButtonDescriptionText": "(用於防止“主頁”或“同步”按鈕影響用戶界面)", + "pressAnyAnalogTriggerText": "按任意模擬板機", + "pressAnyButtonOrDpadText": "按任意鍵或方向鍵...", + "pressAnyButtonText": "按任意鍵...", + "pressLeftRightText": "按左或右...", + "pressUpDownText": "按上或下...", + "runButton1Text": "跑 按鍵1", + "runButton2Text": "跑 按鍵2", + "runTrigger1Text": "跑 扳機1", + "runTrigger2Text": "跑 扳機2", + "runTriggerDescriptionText": "(模擬板機可實現變速運行)", + "secondHalfText": "用於設置顯示為單一手柄的\n二合一手柄設備的\n第二部分", + "secondaryEnableText": "啟用", + "secondaryText": "從屬手柄", + "startButtonActivatesDefaultDescriptionText": "(如果啟動按鈕更傾向為“菜單按鈕”,則關閉此功能)", + "startButtonActivatesDefaultText": "啟動按鈕激活默認部件", + "titleText": "手柄設置", + "twoInOneSetupText": "二合一手柄設置", + "uiOnlyDescriptionText": "(組織這個控制器加入遊戲)", + "uiOnlyText": "限制菜單應用", + "unassignedButtonsRunText": "未給任何按鈕分配“跑”", + "unsetText": "<未設置>", + "vrReorientButtonText": "虛擬按鈕調整" + }, + "configKeyboardWindow": { + "configuringText": "配置 ${DEVICE}", + "keyboard2NoteText": "注意:很多鍵盤只能一次同時按下幾個按鍵\n所以如果玩家連接另一個鍵盤\n則兩個鍵盤可以獲得更好的遊戲體驗\n請注意:即使在這種情況下,您也仍需\n為兩個玩家分配獨特的按鍵" + }, + "configTouchscreenWindow": { + "actionControlScaleText": "動作搖桿大小", + "actionsText": "動作", + "buttonsText": "按鈕", + "dragControlsText": "<拖動按鍵以改變它們的位置>", + "joystickText": "遊戲搖桿", + "movementControlScaleText": "移動遙感大小", + "movementText": "移動", + "resetText": "重置", + "swipeControlsHiddenText": "隱藏滑動圖標", + "swipeInfoText": "“滑動”式控制是需要花時間來適應的\n但更嫻熟的使用可能會讓你在遊戲內表現的更好", + "swipeText": "滑動", + "titleText": "觸屏操作設置" + }, + "configureItNowText": "立即配置?", + "configureText": "配置", + "connectMobileDevicesWindow": { + "amazonText": "亞馬遜應用商店", + "appStoreText": "應用商店", + "bestResultsText": "更好的網絡環境可幫助你在遊戲內表現更好,你可以關閉\n其他無線設備來降低網絡延遲\n或者盡量在路由器附近\n再或者將遊戲主機使用有線網絡", + "explanationText": "如果想用智能手機或平板電腦作為控制器,請在手機或平板電腦上安裝\n\"${REMOTE_APP_NAME}\"軟件。${APP_NAME}可以在局域網環境下\n同時支持八個設備的鏈接,這些完全免費", + "forAndroidText": "對於Android:", + "forIOSText": "對於iOS:", + "getItForText": "iOS用戶可從${REMOTE_APP_NAME}獲取Bombsquad控制器\nAndroid用戶可從Google Play商店或Amazon應用商店下載", + "googlePlayText": "Google Play商店", + "titleText": "使用移動裝置作為控制器:" + }, + "continuePurchaseText": "花費${PRICE}繼續?", + "continueText": "繼續", + "controlsText": "控制按鍵", + "coopSelectWindow": { + "activenessAllTimeInfoText": "不提供所有時間的排名", + "activenessInfoText": "倍數隨著遊戲天數增加\n隨著離線天數減少", + "activityText": "活動", + "campaignText": "比賽", + "challengesInfoText": "獲得完成迷你遊戲的獎勵\n\n每完成一項挑戰\n獎勵和難度就會隨之增加\n每挑戰失敗或放棄一次,獎勵和難度就會隨之下降", + "challengesText": "挑戰", + "currentBestText": "當前最高分", + "customText": "自定義", + "entryFeeText": "參賽", + "forfeitConfirmText": "要放棄此挑戰嗎?", + "forfeitNotAllowedYetText": "目前不能放棄此挑戰", + "forfeitText": "放棄", + "multipliersText": "倍數", + "nextChallengeText": "下一次挑戰", + "nextPlayText": "下一次遊戲", + "ofTotalTimeText": "在 ${TOTAL}", + "playNowText": "現在開始", + "pointsText": "分數", + "powerRankingFinishedSeasonUnrankedText": "(以結束賽季,無排名)", + "powerRankingNotInTopText": "(在 ${NUMBER}名次以外)", + "powerRankingPointsEqualsText": "= ${NUMBER} 分", + "powerRankingPointsMultText": "(x ${NUMBER} 分)", + "powerRankingPointsText": "${NUMBER} 分", + "powerRankingPointsToRankedText": "(共${CURRENT}分 已獲得${REMAINING}分)", + "powerRankingText": "能力排位", + "prizesText": "獎勵", + "proMultInfoText": "擁有 ${PRO} 的玩家\n可獲得 ${PERCENT}% 的得分加成", + "seeMoreText": "更多", + "skipWaitText": "跳過等待", + "timeRemainingText": "剩餘時間", + "toRankedText": "排名", + "totalText": "總計", + "tournamentInfoText": "與聯賽中的玩家\n爭奪分數\n\n錦標賽時間結束時\n最高分玩家將會獲得獎勵", + "welcome1Text": "歡迎來到${LEAGUE}。你可以通過\n在錦標賽中獲得獎杯、完成成就及\n獲得冠軍來提升你的聯賽排名", + "welcome2Text": "你還可以參加很多相同活動來贏取點券\n點券可以用於解鎖新角色、地圖、圖標\n和迷你遊戲,或購買錦標賽門票", + "yourPowerRankingText": "你的能力排名:" + }, + "copyOfText": "${NAME} 拷貝", + "createEditPlayerText": "<創建/編輯玩家>", + "createText": "創建", + "creditsWindow": { + "additionalAudioArtIdeasText": "音頻補充、初期原稿和創意: ${NAME}", + "additionalMusicFromText": "補充背景音樂: ${NAME}", + "allMyFamilyText": "幫助進行遊戲測試的朋友和家人", + "codingGraphicsAudioText": "代碼,貼圖和音頻: ${NAME}", + "languageTranslationsText": "翻譯:", + "legalText": "法律", + "publicDomainMusicViaText": "版權共有音樂: ${NAME}", + "softwareBasedOnText": "該軟件基於 ${NAME}的工作", + "songCreditText": "${TITLE} 演唱: ${PERFORMER}\n作曲: ${COMPOSER},改編: ${ARRANGER},發行: ${PUBLISHER}\n由 ${SOURCE}提供", + "soundAndMusicText": "音樂&音效:", + "soundsText": "音樂 (${SOURCE}):", + "specialThanksText": "特別鳴謝:", + "thanksEspeciallyToText": "尤其感謝 ${NAME}", + "titleText": "${APP_NAME} 製作團隊", + "whoeverInventedCoffeeText": "製作咖啡的人" + }, + "currentStandingText": "您目前的排名是 #${RANK}", + "customizeText": "自定義…", + "deathsTallyText": "${COUNT}次死亡", + "deathsText": "死亡", + "debugText": "測試", + "debugWindow": { + "reloadBenchmarkBestResultsText": "注意:建議您在調試階段將設置—>畫面—>紋理設置為“高”", + "runCPUBenchmarkText": "CPU壓力測試", + "runGPUBenchmarkText": "CPU壓力測試", + "runMediaReloadBenchmarkText": "運行媒體重載基準程序", + "runStressTestText": "運行壓力測試", + "stressTestPlayerCountText": "模擬玩家數量", + "stressTestPlaylistDescriptionText": "壓力測試列表", + "stressTestPlaylistNameText": "列表名稱", + "stressTestPlaylistTypeText": "列表類型", + "stressTestRoundDurationText": "回合時長", + "stressTestTitleText": "壓力測試", + "titleText": "基準程序和壓力測試", + "totalReloadTimeText": "總計裝載時間: ${TIME} (詳見日誌)" + }, + "defaultGameListNameText": "默認${PLAYMODE}列表", + "defaultNewGameListNameText": "我的${PLAYMODE}列表", + "deleteText": "刪除", + "demoText": "演示", + "denyText": "拒絕", + "desktopResText": "桌面分辨率", + "difficultyEasyText": "簡單", + "difficultyHardOnlyText": "僅困難模式", + "difficultyHardText": "困難", + "difficultyHardUnlockOnlyText": "該關卡僅在困難模式解鎖.\n你相信你有奪冠的能力嗎!?!?!", + "directBrowserToURLText": "請打開瀏覽器訪問以下URL:", + "disableRemoteAppConnectionsText": "取消Remote應用連接", + "disableXInputDescriptionText": "允許使用四個以上的控制器,但可能不會正常工作", + "disableXInputText": "禁用XInput", + "doneText": "完成", + "drawText": "平局", + "duplicateText": "複製", + "editGameListWindow": { + "addGameText": "添加\n比賽", + "cantOverwriteDefaultText": "不可以覆蓋默認的遊戲列表", + "cantSaveAlreadyExistsText": "已存在一個同名稱的遊戲列表", + "cantSaveEmptyListText": "不可以保存空白的遊戲列表", + "editGameText": "編輯\n比賽", + "listNameText": "遊戲列表名稱", + "nameText": "名稱", + "removeGameText": "刪除\n比賽", + "saveText": "保存列表", + "titleText": "遊戲列表編輯器" + }, + "editProfileWindow": { + "accountProfileInfoText": "此特殊玩家檔案\n可能含有其中一個圖標\n\n${ICONS}\n\n基於你的登錄平台以顯示\n相關圖標", + "accountProfileText": "(賬戶資料)", + "availableText": "\"${NAME}\" 可用", + "characterText": "腳色", + "checkingAvailabilityText": "正在檢查\"${NAME}\"是否可用", + "colorText": "顏色", + "getMoreCharactersText": "獲得更多角色...", + "getMoreIconsText": "獲得更多圖標...", + "globalProfileInfoText": "全球玩家檔案名稱不會重複\n並且可以自定義圖標", + "globalProfileText": "(全球檔案)", + "highlightText": "副顏色", + "iconText": "圖標", + "localProfileInfoText": "本地玩家檔案不含圖標,且玩家名稱不具備唯一性\n升級至全球檔案,以擁有唯一的遊戲名稱,並且自定義圖標\n#警告:切勿使用隨機名稱,被使用的可能性很大", + "localProfileText": "(本地檔案)", + "nameDescriptionText": "玩家名稱", + "nameText": "名稱", + "randomText": "隨機", + "titleEditText": "編輯檔案", + "titleNewText": "新建檔案", + "unavailableText": "\"${NAME}\" 不可用,請使用其他名稱", + "upgradeProfileInfoText": "這可以在全球範圍內保存您的萬家名稱\n並且你可以自定義一個圖標", + "upgradeToGlobalProfileText": "升級至全球檔案" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "不能刪除默認的背景音與", + "cantEditDefaultText": "不能編輯默認的背景音樂。請備份或創建一個新的背景音樂列表", + "cantOverwriteDefaultText": "不可以覆蓋默認的背景音樂", + "cantSaveAlreadyExistsText": "這個名字的背景音樂已經存在", + "defaultGameMusicText": "<默認背景音樂>", + "defaultSoundtrackNameText": "默認自定義背景音樂", + "deleteConfirmText": "刪除自定義背景音樂\n\n“${NAME}”?", + "deleteText": "刪除\n自定義背景音樂", + "duplicateText": "備份\n自定義背景音樂", + "editSoundtrackText": "自定義背景音樂編輯器", + "editText": "編輯\n自定義背景音樂", + "fetchingITunesText": "正在獲取音樂軟件播放列表...", + "musicVolumeZeroWarning": "注意:目前音量為0", + "nameText": "名稱", + "newSoundtrackNameText": "我的背景音樂${COUNT}", + "newSoundtrackText": "新的背景音樂", + "newText": "創建\n背景音樂", + "selectAPlaylistText": "選擇一個音樂列表", + "selectASourceText": "音樂來源", + "testText": "測試", + "titleText": "自定義背景音樂", + "useDefaultGameMusicText": "默認遊戲音樂", + "useITunesPlaylistText": "音樂軟件播放列表", + "useMusicFileText": "音樂文件(MP3等...)", + "useMusicFolderText": "音樂文件夾" + }, + "editText": "修改", + "endText": "結束", + "enjoyText": "盡情享用吧", + "epicDescriptionFilterText": "史詩級慢動作${DESCRIPTION}", + "epicNameFilterText": "史詩級${NAME}", + "errorAccessDeniedText": "訪問被拒絕", + "errorOutOfDiskSpaceText": "磁盤空間不足", + "errorText": "錯誤", + "errorUnknownText": "未知錯誤", + "exitGameText": "退出${APP_NAME}?", + "exportSuccessText": "退出'${NAME}'", + "externalStorageText": "外部存儲器", + "failText": "失敗", + "fatalErrorText": "有一些遊戲檔案遺失或被損壞\n請嘗試重新安裝退遊戲\n或者聯繫${EMAIL}尋求幫助", + "fileSelectorWindow": { + "titleFileFolderText": "選擇一個文件或文件夾", + "titleFileText": "選擇一個文件", + "titleFolderText": "選擇一個文件夾", + "useThisFolderButtonText": "使用此文件夾" + }, + "filterText": "过滤器", + "finalScoreText": "最後得分", + "finalScoresText": "最後得分", + "finalTimeText": "最終時間", + "finishingInstallText": "安裝即將完成...", + "fireTVRemoteWarningText": "*為獲得更好的遊戲體驗\n請使用遊戲手柄\n或者在你的手機或平板電腦安裝\n'${REMOTE_APP_NAME}'", + "firstToFinalText": "${COUNT} 局自由混戰賽最後得分", + "firstToSeriesText": "${COUNT}局的自由混戰賽", + "fiveKillText": "五連殺!!!!", + "flawlessWaveText": "無傷通過!", + "fourKillText": "四連殺!!!", + "friendScoresUnavailableText": "無法獲取好友的得分", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "最高得分玩家數${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "你不能刪除默認的遊戲列表", + "cantEditDefaultText": "不能編輯默認的遊戲列表!請複製或新建一個", + "cantShareDefaultText": "你不能分享默認列表", + "deleteConfirmText": "是否刪除\"${LIST}\"?", + "deleteText": "刪除\n遊戲列表", + "duplicateText": "複製\n比賽列表", + "editText": "編輯\n比賽列表", + "newText": "新建\n比賽列表", + "showTutorialText": "顯示新手教程", + "shuffleGameOrderText": "隨機比賽模式", + "titleText": "自定義${TYPE}列表" + }, + "gameSettingsWindow": { + "addGameText": "添加比賽" + }, + "gamesToText": "${WINCOUNT}比${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "請注意:如果你有足夠多的遊戲手柄\n派對中的任意設備可以允許多個玩家同時遊戲", + "aboutDescriptionText": "使用這些選項卡來組織一場派對\n\n通過派對,你可以和好友在不同的設備上\n一起玩遊戲和比賽\n\n使用手柄右上角的${PARTY}按鈕\n來發起派對聊天和互動\n(在菜單時按${BUTTON})", + "aboutText": "關於", + "addressFetchErrorText": "<獲取地址錯誤>", + "appInviteMessageText": "${NAME} 在 ${APP_NAME} 送給了您 ${COUNT} 點券", + "appInviteSendACodeText": "向他們發送一個代碼", + "appInviteTitleText": "${APP_NAME} 邀請碼", + "bluetoothAndroidSupportText": "(適用於任何支持藍牙功能的Android設備)", + "bluetoothDescriptionText": "使用藍牙創建遊戲或加入遊戲", + "bluetoothHostText": "使用藍牙創建遊戲", + "bluetoothJoinText": "通過藍牙加入遊戲", + "bluetoothText": "藍牙", + "checkingText": "檢查中...", + "copyCodeConfirmText": "代碼已複製進剪切板", + "copyCodeText": "複製此代碼", + "dedicatedServerInfoText": "建立一個伺服器來獲取最佳效果,詳情見bombsquadgame.com/server", + "disconnectClientsText": "這將使派對中的${COUNT}位玩家斷開連接\n確定這麼做嗎?", + "earnTicketsForRecommendingAmountText": "如果您的朋友們玩了這款遊戲,它們將會受到${COUNT}點券\n(每個遊玩的朋友會使你獲取${YOU_COUNT}點券)", + "earnTicketsForRecommendingText": "分享遊戲來\n獲取免費點券...", + "emailItText": "通過電子郵件發送", + "favoritesSaveText": "另存為收藏", + "favoritesText": "收藏", + "freeCloudServerAvailableMinutesText": "你需要${MINUTES}分鐘後才可獲取一個免費的伺服器", + "freeCloudServerAvailableNowText": "免費伺服器可用", + "freeCloudServerNotAvailableText": "非免費伺服器可用", + "friendHasSentPromoCodeText": "從${NAME}中獲取到${COUNT}個${APP_NAME}點券", + "friendPromoCodeAwardText": "每使用一次,你就會收到${COUNT}張點券", + "friendPromoCodeExpireText": "此代碼將在${EXPIRE_HOURS}小時後失效,該代碼只對新玩家有效", + "friendPromoCodeInstructionsText": "要使用此代碼,可打開${APP_NAME}。通過“設置->高級設置->輸入促銷代碼”操作\n所有支持平台的下載鏈接可見bombsquadgame.com", + "friendPromoCodeRedeemLongText": "達到${MAX_USES}人後就不能獲得${COUNT}免費點券(防止玩家作弊)", + "friendPromoCodeRedeemShortText": "可在遊戲中兌換${COUNT}免費點券", + "friendPromoCodeWhereToEnterText": "(在“設置->高級設置->輸入促銷代碼”中)", + "getFriendInviteCodeText": "獲取好友邀請碼", + "googlePlayDescriptionText": "通過Google Play邀請玩家進入你的派對", + "googlePlayInviteText": "邀請", + "googlePlayReInviteText": "如果你發送一個邀請\n則你的派對中的${COUNT}位Google Play玩家將斷開連接\n在發出新邀請中再次邀請他們,讓他們回到派對", + "googlePlaySeeInvitesText": "查看邀請", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android/Google Play 設備)", + "hostPublicPartyDescriptionText": "創建一個公開派對", + "hostingUnavailableText": "主機不可用", + "inDevelopmentWarningText": "注意:\n\n聯網模式是一項新的並且還在開發階段\n目前強烈建議所有玩家在\n局域網環境下進行遊戲", + "internetText": "在線遊戲", + "inviteAFriendText": "好友還未加入遊戲?邀請他們加入遊戲\n新玩家還可以獲得${COUNT}免費點券", + "inviteFriendsText": "邀請朋友", + "joinPublicPartyDescriptionText": "加入一個公開派對", + "localNetworkDescriptionText": "加入一個附近的派對(局域網,藍牙,etc等)", + "localNetworkText": "本地局域網", + "makePartyPrivateText": "將我的派對變成私人派對", + "makePartyPublicText": "將我的派對變成公開排隊", + "manualAddressText": "地址", + "manualConnectText": "連接", + "manualDescriptionText": "加入派對,地址:", + "manualJoinSectionText": "使用地址加入", + "manualJoinableFromInternetText": "能否從互聯網連接:", + "manualJoinableNoWithAsteriskText": "否*", + "manualJoinableYesText": "能", + "manualRouterForwardingText": "*若要解決此問題,請嘗試將你的路由器設置為將UDP端口 ${PORT}轉發到你的本地地址", + "manualText": "手動", + "manualYourAddressFromInternetText": "互聯網地址:", + "manualYourLocalAddressText": "本地地址:", + "nearbyText": "附近", + "noConnectionText": "<無連接>", + "otherVersionsText": "(其他版本)", + "partyCodeText": "派對代碼", + "partyInviteAcceptText": "接受", + "partyInviteDeclineText": "拒絕", + "partyInviteGooglePlayExtraText": "(查看“多人遊戲”窗口的“Google Play”選項卡)", + "partyInviteIgnoreText": "忽略", + "partyInviteText": "${NAME} 已邀請\n你可以加入他們的派對", + "partyNameText": "排隊名稱", + "partyServerRunningText": "你的派對伺服器正在運行", + "partySizeText": "派對人數", + "partyStatusCheckingText": "檢查中...", + "partyStatusJoinableText": "你的派對現在可以從公開派對列表中加入了", + "partyStatusNoConnectionText": "無法連接到伺服器", + "partyStatusNotJoinableText": "你的派對無法從公開派對列表加入", + "partyStatusNotPublicText": "你的派對是私人派對", + "pingText": "網絡延遲", + "portText": "端口", + "privatePartyCloudDescriptionText": "私有方在專用的伺服器上運行,無需進行路由器配置", + "privatePartyHostText": "舉辦私人派對", + "privatePartyJoinText": "加入一個私人派對", + "privateText": "私人", + "publicHostRouterConfigText": "這可能需要配置路由器進行端口轉發,為了更簡單的選擇,請舉辦一個私人派對", + "publicText": "公開", + "requestingAPromoCodeText": "正在請求代碼...", + "sendDirectInvitesText": "直接邀請", + "shareThisCodeWithFriendsText": "給好友分享此代碼", + "showMyAddressText": "顯示我的地址", + "startHostingPaidText": "使用${COST}去創建一個派對", + "startHostingText": "開始", + "startStopHostingMinutesText": "你的下一個免費服務器將在${MINUTES}後可被開始或停止", + "stopHostingText": "停止", + "titleText": "多人遊戲", + "wifiDirectDescriptionBottomText": "如果所有設備都沒有'Wi-Fi Direct'界面,那他們應該可以通過它找到彼此\n然後互相連接。一旦所有的設備都互相連接上了,你就可以通過“本地網絡”選項卡\n在此組織派對,常規的局域網也是一樣\n\n如果要獲取最佳的效果,Wi-Fi Direct創建者也應是${APP_NAME}派對的創建者", + "wifiDirectDescriptionTopText": "無需打開網絡連接即可直接\n通過Wi-Fi Direct連接安卓設備。對Android4.2及以上的系統版本效果更好\n\n要使用該功能。可打開網絡設置,然後在菜單中尋找'Wi-Fi Direct'", + "wifiDirectOpenWiFiSettingsText": "打開無線網絡設置", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(所有平台之間運作)", + "worksWithGooglePlayDevicesText": "(適用於運行Google Play(Android)版遊戲的設備)", + "youHaveBeenSentAPromoCodeText": "您已送出一個${APP_NAME}促銷代碼:" + }, + "getTicketsWindow": { + "freeText": "免費!", + "freeTicketsText": "免費點券", + "inProgressText": "一個交易正在進行:請稍後再試", + "purchasesRestoredText": "購買恢復", + "receivedTicketsText": "獲得${COUNT}點券", + "restorePurchasesText": "恢復購買", + "ticketPack1Text": "小型點券包", + "ticketPack2Text": "中型點券包", + "ticketPack3Text": "大型點券包", + "ticketPack4Text": "局型點券包", + "ticketPack5Text": "巨巨巨巨巨巨巨巨巨巨型點券包", + "ticketPack6Text": "終極點券包", + "ticketsFromASponsorText": "從贊助商\n獲取${COUNT}點券", + "ticketsText": "${COUNT} 點券", + "titleText": "獲得點券", + "unavailableLinkAccountText": "對不起,該平台不可進行購買\n您可以將賬戶鏈接到另一個\n平台,以進行購買", + "unavailableTemporarilyText": "該選項當前不可用;請稍後再試", + "unavailableText": "對不起,該選項目前不可用", + "versionTooOldText": "對不起,你的遊戲版本太舊了;請更新到最新版本", + "youHaveShortText": "你擁有 ${COUNT}", + "youHaveText": "你擁有 ${COUNT}點券" + }, + "googleMultiplayerDiscontinuedText": "抱歉,Google的多人遊戲服務不再可用。\n我將盡快更換新的替代服務。\n在此之前,請嘗試其他連接方法。\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "總是", + "fullScreenCmdText": "全屏顯示Cmd-F", + "fullScreenCtrlText": "全屏顯示(Ctrl-F)", + "gammaText": "Gamma", + "highText": "高", + "higherText": "最高", + "lowText": "低", + "mediumText": "中", + "neverText": "關", + "resolutionText": "分辨率", + "showFPSText": "顯示幀數", + "texturesText": "材質質量", + "titleText": "貼圖質量", + "tvBorderText": "UI微縮進", + "verticalSyncText": "垂直同步", + "visualsText": "視覺" + }, + "helpWindow": { + "bombInfoText": "—炸彈—\n比拳頭傷害高,但也能對自己造成傷害\n給你個建議:\n在引線快要燒完時\n把炸彈扔向敵人", + "canHelpText": "${APP_NAME}可以給你幫助", + "controllersInfoText": "你可以正在局域網環境下與其他玩家遊玩${APP_NAME} ,或者\n你有足夠多的遊戲手柄,那樣也可以在同一個設備下游戲\n${APP_NAME}支持各種選擇;你甚至可以通過免費的 '${REMOTE_APP_NAME}' \n用手機或平板電腦作為遊戲手柄\n更多信息,請參考\"設置—>控制器\"", + "controllersText": "手柄", + "controlsSubtitleText": "你的好友的${APP_NAME}角色具有幾個基本動作", + "controlsText": "控制鍵", + "devicesInfoText": "VR版${APP_NAME}可與\n普通版本進行聯網遊戲,所以掏出你的所有手機、平板電腦\n和電腦,盡情暢玩吧。你甚至可以將\n普通版本的遊戲連機至VR版\n讓遊戲外的人也能看到遊戲畫面", + "devicesText": "設備", + "friendsGoodText": "這對於你而言是個好事,與三五好友一起玩${APP_NAME}最有趣了\n最對支持8個玩家一起玩", + "friendsText": "好友", + "jumpInfoText": "-跳躍-\n跳躍可以讓你跳過比較高的障礙物\n或者是讓你把炸彈扔的更遠\n又或者用來表達你的喜悅", + "orPunchingSomethingText": "或用券猛擊敵人,將他們打下懸崖,然後在下落的途中用粘性炸彈炸掉它", + "pickUpInfoText": "-撿起-\n你可以撿起旗子,敵人\n還有所有沒固定在地上的東西\n然後再扔出去吧", + "powerupBombDescriptionText": "連續扔出\n三連炸彈", + "powerupBombNameText": "三連炸彈", + "powerupCurseDescriptionText": "你可能想要避開那些\n...或者你想試試看?", + "powerupCurseNameText": "詛咒", + "powerupHealthDescriptionText": "讓你恢復所有生命值\n你永遠都想不到", + "powerupHealthNameText": "生命恢復", + "powerupIceBombsDescriptionText": "威力比普通炸彈小\n但能把你的敵人凍住\n並且變得十分脆弱", + "powerupIceBombsNameText": "冰凍炸彈", + "powerupImpactBombsDescriptionText": "威力比普通炸彈低\n但是碰到其他物品之後直接爆炸", + "powerupImpactBombsNameText": "觸感炸彈", + "powerupLandMinesDescriptionText": "大特價,買一贈二\n居家旅行,防守陣地的不二選擇\n還可以阻止那些跑的飛快的人", + "powerupLandMinesNameText": "地雷", + "powerupPunchDescriptionText": "沒有拳套的我唯唯諾諾\n擁有拳套的我重拳出擊", + "powerupPunchNameText": "拳擊手套", + "powerupShieldDescriptionText": "無可阻擋!\n堅不可摧!", + "powerupShieldNameText": "能量護盾", + "powerupStickyBombsDescriptionText": "可以黏在任何物體上\n(聽說它和TNT是死對頭)", + "powerupStickyBombsNameText": "粘性炸彈", + "powerupsSubtitleText": "當然,沒有道具的遊戲會變得很難通關", + "powerupsText": "道具", + "punchInfoText": "-拳擊-\n跑得越快,造成的傷害越高\n你可以嘗試一下“衝刺旋轉跳拳”\n當然,要注意防守", + "runInfoText": "-衝刺-\n按住任意鍵+移動=衝刺\n衝刺雖然跑得快,但會對轉彎造成困難", + "someDaysText": "有些時候你只是想揮拳猛擊默寫東西,或把什麼東西炸飛", + "titleText": "${APP_NAME}幫助", + "toGetTheMostText": "想要獲得更好的遊戲體驗,你需要:", + "welcomeText": "歡迎來到 ${APP_NAME}!" + }, + "holdAnyButtonText": "<按住任意按鈕>", + "holdAnyKeyText": "<按住任意鍵>", + "hostIsNavigatingMenusText": "-${HOST} 正在觀察主頁面-", + "importPlaylistCodeInstructionsText": "用代碼來導入列表", + "importPlaylistSuccessText": "成功導入${TYPE} 遊戲列表'${NAME}'", + "importText": "導入", + "importingText": "導入中...", + "inGameClippedNameText": "名字會是\n\"${NAME}\"", + "installDiskSpaceErrorText": "錯誤:無法完成安裝\n你的設備磁盤空間不足\n請釋放一些空間後重試", + "internal": { + "arrowsToExitListText": "按${LEFT} 或 ${RIGHT} 退出列表", + "buttonText": "按鈕", + "cantKickHostError": "你無法踢出創建者", + "chatBlockedText": "玩家${NAME} 被禁言 ${TIME} 秒", + "connectedToGameText": "加入 '${NAME}'", + "connectedToPartyText": "加入${NAME}的派對", + "connectingToPartyText": "正在連接...", + "connectionFailedHostAlreadyInPartyText": "連接失敗:創建者正在另一派對中", + "connectionFailedPartyFullText": "連接錯誤:房間滿員了", + "connectionFailedText": "連接失敗", + "connectionFailedVersionMismatchText": "連接失敗:創建者使用了更高的遊戲版本\n請確保你的遊戲版本與創建者一致再繼續", + "connectionRejectedText": "連接被拒絕", + "controllerConnectedText": "${CONTROLLER} 已連接", + "controllerDetectedText": "檢測到1個手柄", + "controllerDisconnectedText": "${CONTROLLER} 斷開連接", + "controllerDisconnectedTryAgainText": "${CONTROLLER} 斷開連接。請嘗試重新連接", + "controllerForMenusOnlyText": "此控制器無法在遊戲中使用", + "controllerReconnectedText": "${CONTROLLER} 重新連接", + "controllersConnectedText": "已連接${COUNT} 個手柄", + "controllersDetectedText": "檢測到${COUNT} 個手柄", + "controllersDisconnectedText": "${COUNT} 個手柄斷開連接", + "corruptFileText": "檢測到已損壞文件。請嘗試重新安裝,或發送電子郵件到${EMAIL}尋求幫助", + "errorPlayingMusicText": "無法播放音樂: ${MUSIC}", + "errorResettingAchievementsText": "無法重置在線成就;請稍後再試", + "hasMenuControlText": "${NAME} 目前擁有控制菜單的權限", + "incompatibleNewerVersionHostText": "創建者的遊戲版本更高\n請更新您的遊戲版本後重試", + "incompatibleVersionHostText": "創建者的遊戲版本與你的不一致\n請確保你們的遊戲版本相同後重試", + "incompatibleVersionPlayerText": "${NAME} 正在運行不同的遊戲版本\n請確保你們的遊戲版本一致後重試", + "invalidAddressErrorText": "錯誤:無效的地址", + "invalidNameErrorText": "錯誤:無效的名字", + "invalidPortErrorText": "錯誤:無效的端口", + "invitationSentText": "已發出邀請", + "invitationsSentText": "已發出${COUNT} 個邀請", + "joinedPartyInstructionsText": "有人加入了你的派對\n去“開始戰鬥”中開始一場對戰吧", + "keyboardText": "鍵盤", + "kickIdlePlayersKickedText": "${NAME}掛機時間過長,將其踢出", + "kickIdlePlayersWarning1Text": "如果${NAME} 繼續掛機,則將會在${COUNT} 後將其踢出", + "kickIdlePlayersWarning2Text": "(你可以在設置->高級設置中將其關閉)", + "leftGameText": "離開'${NAME}'.", + "leftPartyText": "離開${NAME}的派對", + "noMusicFilesInFolderText": "文件夾內沒有音頻文件", + "playerJoinedPartyText": "${NAME} 加入了派對", + "playerLeftPartyText": "${NAME} 離開了派對", + "rejectingInviteAlreadyInPartyText": "拒絕邀請(已經在派對中)", + "serverRestartingText": "伺服器正在重啟,請稍後加入", + "serverShuttingDownText": "伺服器正在關閉...", + "signInErrorText": "登錄出錯.", + "signInNoConnectionText": "網絡連接失敗,無法登錄", + "telnetAccessDeniedText": "錯誤:用戶未得到遠程登錄連接授權", + "timeOutText": "(將在${TIME} 秒後超出時限)", + "touchScreenJoinWarningText": "你已以觸摸屏的方式加入\n如果這是一個錯誤,請手動退出遊戲", + "touchScreenText": "觸摸屏", + "unableToResolveHostText": "錯誤:創建者網絡環境有問題", + "unavailableNoConnectionText": "網絡連接故障", + "vrOrientationResetCardboardText": "重置VR定位\n您需要用外部手柄來進行遊戲", + "vrOrientationResetText": "VR定位重置", + "willTimeOutText": "(若掛機則會超出時限)" + }, + "jumpBoldText": "跳", + "jumpText": "跳", + "keepText": "舉起", + "keepTheseSettingsText": "保存這些設置嗎", + "keyboardChangeInstructionsText": "雙擊空格以更改控制器", + "keyboardNoOthersAvailableText": "無其他可用的控制器", + "keyboardSwitchText": "切換控制器為\"${NAME}\"", + "kickOccurredText": "${NAME} 被踢出", + "kickQuestionText": "是否要踢出玩家${NAME}?", + "kickText": "踢出", + "kickVoteCantKickAdminsText": "服務器管理員無法被踢出派對", + "kickVoteCantKickSelfText": "你不可以踢出你自己", + "kickVoteFailedNotEnoughVotersText": "沒有足夠的玩家參與投票", + "kickVoteFailedText": "投票踢出玩家未成功", + "kickVoteStartedText": "踢出玩家${NAME}的投票已發起", + "kickVoteText": "投票踢出玩家", + "kickVotingDisabledText": "投票踢人已被禁用", + "kickWithChatText": "在聊天框內輸入${YES} 以同意,輸入${NO} 以否定", + "killsTallyText": "${COUNT} 次擊殺", + "killsText": "擊殺數", + "kioskWindow": { + "easyText": "簡單", + "epicModeText": "史詩模式", + "fullMenuText": "完整菜單", + "hardText": "困難", + "mediumText": "中級", + "singlePlayerExamplesText": "單人模式/合作模式樣例", + "versusExamplesText": "對戰模式樣例" + }, + "languageSetText": "現在的語言是\"${LANGUAGE}\".", + "lapNumberText": "圈數:${CURRENT}/${TOTAL}", + "lastGamesText": "(最後${COUNT}局比賽)", + "leaderboardsText": "排行榜", + "league": { + "allTimeText": "所有時間", + "currentSeasonText": "當前賽季(${NUMBER})", + "leagueFullText": "${NAME}聯賽", + "leagueRankText": "聯賽排名", + "leagueText": "聯賽", + "rankInLeagueText": "#${RANK}, ${NAME} 聯賽${SUFFIX}", + "seasonEndedDaysAgoText": "賽季已於${NUMBER}天前結束", + "seasonEndsDaysText": "賽季將在${NUMBER}天後結束", + "seasonEndsHoursText": "賽季將於${NUMBER}小時後結束", + "seasonEndsMinutesText": "賽季將於${NUMBER}分鐘後結束", + "seasonText": "第${NUMBER}賽季", + "tournamentLeagueText": "你一定要到${NAME}聯賽後才能參加此賽事", + "trophyCountsResetText": "獎杯計數將於下個賽季重置" + }, + "levelBestScoresText": "在${LEVEL}中的最高成績", + "levelBestTimesText": "在${LEVEL}中的最佳時間", + "levelIsLockedText": "${LEVEL}正在鎖定狀態", + "levelMustBeCompletedFirstText": "你必須先完成${LEVEL}", + "levelText": "${NUMBER}關卡", + "levelUnlockedText": "關卡解鎖", + "livesBonusText": "生命獎勵", + "loadingText": "正在加載中", + "loadingTryAgainText": "加載中...請稍後再試", + "macControllerSubsystemBothText": "均可(不推薦)", + "macControllerSubsystemClassicText": "經典", + "macControllerSubsystemDescriptionText": "(在你手柄無法使用時請嘗試更改此選項)", + "macControllerSubsystemMFiNoteText": "已檢測到Made-for-iOS/Mac手柄\n你需要在設置->控制器中啟用此設備", + "macControllerSubsystemMFiText": "Made-for-iOS/Mac", + "macControllerSubsystemTitleText": "手柄支持", + "mainMenu": { + "creditsText": "製作團隊", + "demoMenuText": "演示菜單", + "endGameText": "結束遊戲", + "exitGameText": "退出遊戲", + "exitToMenuText": "退出到菜單?", + "howToPlayText": "幫助", + "justPlayerText": "(勁${NAME})", + "leaveGameText": "離開遊戲", + "leavePartyConfirmText": "確定要離開派對嗎?", + "leavePartyText": "離開派對", + "quitText": "離開遊戲", + "resumeText": "回到遊戲", + "settingsText": "設置" + }, + "makeItSoText": "應用", + "mapSelectGetMoreMapsText": "獲取更多地圖...", + "mapSelectText": "選擇...", + "mapSelectTitleText": "${GAME}地圖", + "mapText": "地圖", + "maxConnectionsText": "最大連接數", + "maxPartySizeText": "最大派對規模", + "maxPlayersText": "最多人數", + "modeArcadeText": "街機模式", + "modeClassicText": "經典模式", + "modeDemoText": "演示模式", + "mostValuablePlayerText": "最有價值的玩家", + "mostViolatedPlayerText": "最遭受暴力的玩家", + "mostViolentPlayerText": "最暴力的玩家", + "moveText": "移動", + "multiKillText": "${COUNT}連殺!!!", + "multiPlayerCountText": "${COUNT}名玩家", + "mustInviteFriendsText": "注意:你必須在“${GATHER}”面板中邀請好友\n或連接多個手柄\n和好友一起遊戲", + "nameBetrayedText": "${NAME} 擊殺了隊友${VICTIM}.", + "nameDiedText": "${NAME}死了", + "nameKilledText": "${NAME}擊殺${VICTIM}.", + "nameNotEmptyText": "名字不能為空", + "nameScoresText": "${NAME} 得分", + "nameSuicideKidFriendlyText": "${NAME}意外逝世.", + "nameSuicideText": "${NAME}自殺了.", + "nameText": "名稱", + "nativeText": "本機", + "newPersonalBestText": "新個人最佳!", + "newTestBuildAvailableText": "更新的測試版可供下載了! (${VERSION} 升級至 ${BUILD}).\n到${ADDRESS}獲取測試版", + "newText": "新建", + "newVersionAvailableText": "更新版本的${APP_NAME} 可供下載了 版本號:${VERSION}", + "nextAchievementsText": "未完成的成就:", + "nextLevelText": "下一關", + "noAchievementsRemainingText": "- 無", + "noContinuesText": "(無可繼續)", + "noExternalStorageErrorText": "該設備未發現外部存儲器", + "noGameCircleText": "錯誤:未登錄GameCircle", + "noScoresYetText": "沒有得分記錄", + "noThanksText": "不,謝謝", + "noTournamentsInTestBuildText": "注意:此測試版本的比賽分數將會被作廢", + "noValidMapsErrorText": "沒有發現該比賽類型的有效地圖", + "notEnoughPlayersRemainingText": "剩餘玩家不足,退出並開始新遊戲", + "notEnoughPlayersText": "至少需要${COUNT}名玩家來開始遊戲", + "notNowText": "不是現在", + "notSignedInErrorText": "你必須登入來進行該操作.", + "notSignedInGooglePlayErrorText": "您必須通過Google Play登錄", + "notSignedInText": "(未登錄)", + "nothingIsSelectedErrorText": "未選擇任何內容", + "numberText": "#${NUMBER}", + "offText": "關", + "okText": "好的", + "onText": "開", + "oneMomentText": "請等待", + "onslaughtRespawnText": "${PLAYER}將於${WAVE}波復活", + "orText": "${A} 或 ${B}", + "otherText": "其他", + "outOfText": "(在${ALL}名玩家中位列#${RANK})", + "ownFlagAtYourBaseWarning": "你的旗幟必須在自己\n的基地上才能得分", + "packageModsEnabledErrorText": "啟用本地程序包修改時,聯網對戰不可用(參見設置->高級設置)", + "partyWindow": { + "chatMessageText": "聊天消息", + "emptyText": "你的派對為空", + "hostText": "(創建者)", + "sendText": "發送", + "titleText": "你的派對" + }, + "pausedByHostText": "(創建者已暫停)", + "perfectWaveText": "完美的一波", + "pickUpText": "撿起1", + "playModes": { + "coopText": "合作模式", + "freeForAllText": "混戰模式", + "multiTeamText": "多團隊", + "singlePlayerCoopText": "單人遊戲/合作模式", + "teamsText": "團隊模式" + }, + "playText": "開始戰鬥", + "playWindow": { + "oneToFourPlayersText": "一到四個玩家", + "titleText": "開始戰鬥", + "twoToEightPlayersText": "適合2-8名玩家" + }, + "playerCountAbbreviatedText": "${COUNT}名玩家", + "playerDelayedJoinText": "${PLAYER}將在下一回合進入遊戲", + "playerInfoText": "玩家資料", + "playerLeftText": "${PLAYER}離開了遊戲", + "playerLimitReachedText": "已達到${COUNT}名玩家上線;其他玩家不可加入", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "您無法刪除您的玩家資料", + "deleteButtonText": "刪除\n檔案", + "deleteConfirmText": "刪除'${PROFILE}'?", + "editButtonText": "編輯\n檔案", + "explanationText": "(為這個賬號定義玩家名稱和外觀)", + "newButtonText": "新建\n檔案", + "titleText": "玩家檔案" + }, + "playerText": "玩家", + "playlistNoValidGamesErrorText": "此列表未包含有效的已解鎖遊戲", + "playlistNotFoundText": "找不到遊戲列表", + "playlistText": "遊玩列表", + "playlistsText": "遊戲列表", + "pleaseRateText": "如果你喜歡${APP_NAME},請考慮花一些時間\n為Bombsquad寫一篇評論。這將為我們\n提供一些有用的反饋建議,為遊戲的未來開發給予支持\n\nThanks!\n-Eric", + "pleaseWaitText": "請稍等....", + "pluginsDetectedText": "檢測到新插件。 在設置中啟用/配置它們。", + "pluginsText": "外掛程式", + "practiceText": "練習", + "pressAnyButtonPlayAgainText": "按任意鍵再玩一次...", + "pressAnyButtonText": "按任意鍵以繼續...", + "pressAnyButtonToJoinText": "按任意鍵加入...", + "pressAnyKeyButtonPlayAgainText": "按任意鍵再玩一次...", + "pressAnyKeyButtonText": "按任意鍵繼續...", + "pressAnyKeyText": "按任意鍵...", + "pressJumpToFlyText": "**按跳躍鍵以飛行**", + "pressPunchToJoinText": "按出拳加入", + "pressToOverrideCharacterText": "按 ${BUTTONS} 更換你的角色", + "pressToSelectProfileText": "按 ${BUTTONS} 選擇一個玩家", + "pressToSelectTeamText": "按${BUTTONS} 選擇一個隊伍", + "promoCodeWindow": { + "codeText": "代碼", + "enterText": "輸入" + }, + "promoSubmitErrorText": "提交代碼時錯誤:檢查你的網絡連接", + "ps3ControllersWindow": { + "macInstructionsText": "關閉PS3背面的電源開關,確保\n您的MAC電腦上啟用了藍牙,然後通過USB連接將您的手柄連接到\n您的MAC電腦上時期配對。之後,您\n就可以使用該手柄上的主頁按鈕以有線(USB)或無線(藍牙)模式\n將其連接到你的電腦上\n\n在一些MAC電腦上配對時可能會提示你輸入密碼\n在此情況下,請參閱一下教程或搜索Google尋求幫助\n\n\n\n\n無線連接的PS3手柄應該出現在\n系統偏好設置->藍牙中的設備列表中。當你想再次使用你的PS3時\n您可以從該列表中移除它們\n\n另外,請確保它們在未使用狀態下時與藍牙斷開連接\n以免其電池持續消耗\n\n藍牙最多可以處理七個連接設備\n雖然您的里程可能會有所不同", + "ouyaInstructionsText": "若要通過OUYA使用PS3手柄,僅需使用USB連接線\n將其連接配對。這樣做可能會使您的其他手柄斷開連接,因此\n您應該重新啟動您的OUYA,然後拔下USB連接線\n\n然後,你應該能夠使用手柄的主頁按鈕\n以無線模式將其連接。結束遊戲後,按住主頁按鈕\n10秒鐘,以關閉手柄,否則,手柄將持續處於啟動狀態\n並消耗電池", + "pairingTutorialText": "配對教學視頻", + "titleText": "用PS3手柄玩 ${APP_NAME}:" + }, + "punchBoldText": "拳擊", + "punchText": "拳擊", + "purchaseForText": "購買花費 ${PRICE}", + "purchaseGameText": "購買遊戲", + "purchasingText": "正在購買...", + "quitGameText": "退出 ${APP_NAME}?", + "quittingIn5SecondsText": "將於5秒後退出...", + "randomPlayerNamesText": "Reol,爆破鬼才,ZACK,炸蛋小分隊,隊友摧毀者,炸彈吞噬者,TNT的朋友,冰凍使者,田所浩二,拳擊高手,創世神,炸彈人,機械狂人,大發明家,吹噓海盜,末日預言者", + "randomText": "隨機", + "rankText": "排行", + "ratingText": "排名", + "reachWave2Text": "進入第二波才可參與排名", + "readyText": "準備", + "recentText": "最近", + "remoteAppInfoShortText": "與家人或者朋友們一起玩${APP_NAME}是非常有趣的!\n您可以連接一個或多個控制器\n或者在手機、平板上安裝${REMOTE_APP_NAME}APP程序\n把他們當作控制器使用", + "remote_app": { + "app_name": "BombSquad手柄", + "app_name_short": "BS手柄", + "button_position": "按鈕位置", + "button_size": "按鈕尺寸", + "cant_resolve_host": "無法解析主機...", + "capturing": "捕捉中...", + "connected": "已連接", + "description": "使用手機或平板電腦作為Bombsquad的遊戲手柄\n一台電視或平板電腦上可以同時連接8台設備,體驗史詩級多人模式的瘋狂遊戲", + "disconnected": "伺服器斷開鏈接", + "dpad_fixed": "固定", + "dpad_floating": "浮動", + "dpad_position": "方向鍵位置", + "dpad_size": "方向盤大小", + "dpad_type": "方向鍵類型", + "enter_an_address": "輸入地址", + "game_full": "遊戲連接已滿或不接收更多的連接", + "game_shut_down": "遊戲關閉", + "hardware_buttons": "硬件按鈕", + "join_by_address": "通過地址加入...", + "lag": "延遲:${SECONDS}秒", + "reset": "恢復默認值", + "run1": "運行1", + "run2": "運行2", + "searching": "正在搜索Bombsquad遊戲", + "searching_caption": "點擊遊戲名,進入遊戲\n確保和遊戲處於同一個局域網環境下", + "start": "開始", + "version_mismatch": "版本不匹配\n確保Bombsquad與Bombsquad手柄都是\n最新版本後重試" + }, + "removeInGameAdsText": "在商店中解鎖\"${PRO}\",以刪除遊戲中的廣告", + "renameText": "重命名", + "replayEndText": "結束回放", + "replayNameDefaultText": "終場遊戲回放", + "replayReadErrorText": "讀取回放文件時出錯", + "replayRenameWarningText": "如果想保存回放文件,則以遊戲來命名\"${REPLAY}\",否則溫江將會被覆蓋", + "replayVersionErrorText": "抱歉,該回放由於遊戲版本不同\n無法播放", + "replayWatchText": "播放回放", + "replayWriteErrorText": "寫入遊戲回放時出錯", + "replaysText": "回放", + "reportPlayerExplanationText": "利用此電子郵件來舉報作弊、不當言論等其他遊戲不良行為\n請描述如下信息:", + "reportThisPlayerCheatingText": "作弊", + "reportThisPlayerLanguageText": "不當言行", + "reportThisPlayerReasonText": "舉報該玩家的原因是?", + "reportThisPlayerText": "舉報此玩家", + "requestingText": "正在請求", + "restartText": "重新啟動", + "retryText": "請重試", + "revertText": "還原", + "runText": "運行", + "saveText": "保存", + "scanScriptsErrorText": "掃描腳本時存在錯誤,查看錯誤日誌來了解詳情", + "scoreChallengesText": "得分挑戰", + "scoreListUnavailableText": "得分列表不可用", + "scoreText": "得分", + "scoreUnits": { + "millisecondsText": "毫秒", + "pointsText": "分", + "secondsText": "秒" + }, + "scoreWasText": "(是 ${COUNT})", + "selectText": "選擇", + "seriesWinLine1PlayerText": "贏得", + "seriesWinLine1TeamText": "贏得", + "seriesWinLine1Text": "贏得", + "seriesWinLine2Text": "本局!", + "settingsWindow": { + "accountText": "賬戶", + "advancedText": "高級設置", + "audioText": "音效", + "controllersText": "控制器", + "graphicsText": "圖像", + "playerProfilesMovedText": "注意:玩家檔案已移至主菜單的“賬戶”窗口下", + "titleText": "設置" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(注意:遊戲內置輸入法只能讓輸入英文字符和部分符號)", + "alwaysUseInternalKeyboardText": "使用遊戲內置輸入法", + "benchmarksText": "基準/壓力測試", + "disableCameraGyroscopeMotionText": "禁用陀螺儀畫面抖動", + "disableCameraShakeText": "禁止畫面抖動", + "disableThisNotice": "(可在高級設置中關閉此通知)", + "enablePackageModsDescriptionText": "(啟用額外的修改性能,但是禁止網絡遊戲)", + "enablePackageModsText": "啟用本地程序包修改", + "enterPromoCodeText": "輸入促銷代碼", + "forTestingText": "注意:這些數值僅用於測試,並會在退出遊戲後重置", + "helpTranslateText": "${APP_NAME}的非英語翻譯是其他玩家\n共同努力的成果,如果你希望參與遊戲文本翻譯或修正\n請點擊以下連接。感謝大家對遊戲翻譯提出的貢獻", + "kickIdlePlayersText": "自動踢出掛機玩家", + "kidFriendlyModeText": "兒童模式(低暴力等)", + "languageText": "語言", + "moddingGuideText": "修改指南", + "mustRestartText": "你必須重啟遊戲才能生效", + "netTestingText": "網絡連接測試", + "resetText": "恢復默認", + "showBombTrajectoriesText": "顯示炸彈軌跡", + "showPlayerNamesText": "顯示玩家名稱", + "showUserModsText": "顯示MOD安裝文件夾", + "titleText": "高級設置", + "translationEditorButtonText": "${APP_NAME}翻譯編輯", + "translationFetchErrorText": "翻譯狀態不可用", + "translationFetchingStatusText": "檢查翻譯進度...", + "translationInformMe": "我的語言翻譯可更新時通知我", + "translationNoUpdateNeededText": "當前語言翻譯文本是最新的", + "translationUpdateNeededText": "**當前語言翻譯文本需要更新**", + "vrTestingText": "VR 調試" + }, + "shareText": "分享", + "sharingText": "分享中...", + "showText": "顯示", + "signInForPromoCodeText": "你必須登錄才能使用促銷代碼", + "signInWithGameCenterText": "使用Game Center\n來登錄", + "singleGamePlaylistNameText": "僅 ${GAME}", + "singlePlayerCountText": "1 玩家", + "soloNameFilterText": "單挑模式 ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "角色選擇", + "Chosen One": "選定模式", + "Epic": "史詩模式遊戲", + "Epic Race": "史詩級競賽", + "FlagCatcher": "奪旗戰", + "Flying": "飛行之地", + "Football": "運旗站", + "ForwardMarch": "突襲戰", + "GrandRomp": "佔領戰", + "Hockey": "冰球戰", + "Keep Away": "舉旗戰", + "Marching": "塔防戰", + "Menu": "主菜單", + "Onslaught": "衝鋒戰", + "Race": "競賽", + "Scary": "據點佔領", + "Scores": "得分結算屏幕", + "Survival": "消除戰", + "ToTheDeath": "死亡競賽", + "Victory": "最終結算屏幕" + }, + "spaceKeyText": "空格", + "statsText": "統計", + "storagePermissionAccessText": "需要存儲權限", + "store": { + "alreadyOwnText": "您已擁有${NAME}!", + "bombSquadProNameText": "${APP_NAME} PRO", + "bombSquadProNewDescriptionText": "•移除遊戲中廣告和煩人的界面\n•解鎖更多遊戲設置\n•以及:", + "buyText": "購買", + "charactersText": "人物", + "comingSoonText": "即將來臨...", + "extrasText": "額外部分", + "freeBombSquadProText": "BombSquad現在是免費的,由於最初您是通過購買獲得,您將獲得\nBombSquad Pro和${COUNT}點券,以表感謝\n盡享全新功能,感謝您對Bombsquad的支持\n-Eric", + "holidaySpecialText": "假日特售", + "howToSwitchCharactersText": "(進入${SETTINGS} -> ${PLAYER_PROFILES}自定義人物)", + "howToUseIconsText": "(升級全球檔案以使用圖標)", + "howToUseMapsText": "(在團隊/混戰遊戲中使用這些地圖)", + "iconsText": "圖標", + "loadErrorText": "無法加載頁面\n請檢查網絡連接", + "loadingText": "加載中", + "mapsText": "地圖", + "miniGamesText": "迷你遊戲", + "oneTimeOnlyText": "(僅一次)", + "purchaseAlreadyInProgressText": "該物品的購買正在進行中", + "purchaseConfirmText": "購買${ITEM}?", + "purchaseNotValidError": "購買失敗\n如果這是一個錯誤,請聯繫${EMAIL}", + "purchaseText": "購買", + "saleBundleText": "捆綁售賣", + "saleExclaimText": "出售", + "salePercentText": "(-${PERCENT}%)", + "saleText": "特惠", + "searchText": "搜索", + "teamsFreeForAllGamesText": "團隊/混戰遊戲", + "totalWorthText": "*** ${TOTAL_WORTH} 值 ***", + "upgradeQuestionText": "更新?", + "winterSpecialText": "冬季特售", + "youOwnThisText": "-您已擁有-" + }, + "storeDescriptionText": "8人派对游戏尽显疯狂!\n\n在爆炸类迷你游戏中炸飞您的好友(或电脑),如夺旗战、冰球战及史诗级慢动作死亡竞赛!\n\n简单的控制和广泛的手柄支持可轻松允许多达8人参与游戏;您甚至可以通过免费的“BombSquad Remote”应用将您的移动设备作为手柄使用!\n\n投射炸弹!\n\n更多信息,请登录www.froemling.net/bombsquad。", + "storeDescriptions": { + "blowUpYourFriendsText": "炸飛你的朋友", + "competeInMiniGamesText": "在從競速到飛行的迷你遊戲中一決高下", + "customize2Text": "自定義角色、迷你遊戲甚至是背景音樂", + "customizeText": "自定義角色並創建自己的迷你遊戲列表", + "sportsMoreFunText": "加入炸藥後遊戲變得更加有趣", + "teamUpAgainstComputerText": "組隊對抗人機" + }, + "storeText": "商店", + "submitText": "提交", + "submittingPromoCodeText": "正在提交代碼...", + "teamNamesColorText": "團隊名稱/顏色...", + "telnetAccessGrantedText": "Telnet訪問以啟用", + "telnetAccessText": "檢測到Telnet訪問,是否允許?", + "testBuildErrorText": "該測試版已失效;請檢查是否存在新版本", + "testBuildText": "測試版", + "testBuildValidateErrorText": "無法驗證測試版(網絡連接出錯)", + "testBuildValidatedText": "測試版已通過驗證,盡情享用", + "thankYouText": "感謝你的支持!盡情享受遊戲!", + "threeKillText": "三殺!!", + "timeBonusText": "時間獎勵", + "timeElapsedText": "時間耗盡", + "timeExpiredText": "時間結束", + "timeSuffixDaysText": "${COUNT}天", + "timeSuffixHoursText": "${COUNT}時", + "timeSuffixMinutesText": "${COUNT}分", + "timeSuffixSecondsText": "${COUNT}秒", + "tipText": "提示", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "最佳好友", + "tournamentCheckingStateText": "檢查錦標賽狀態中,請稍後...", + "tournamentEndedText": "此錦標賽已結束。新的錦標賽已開始", + "tournamentEntryText": "錦標賽入口", + "tournamentResultsRecentText": "最近錦標賽結果", + "tournamentStandingsText": "錦標賽積分榜", + "tournamentText": "錦標賽", + "tournamentTimeExpiredText": "錦標賽時間結束", + "tournamentsText": "錦標賽", + "translations": { + "characterNames": { + "Agent Johnson": "約翰遜特工", + "B-9000": "B-9000", + "Bernard": "伯納德", + "Bones": "骷髏", + "Butch": "牛仔邦奇", + "Easter Bunny": "復活節兔子", + "Flopsy": "Flopsy", + "Frosty": "冬日雪人", + "Gretel": "歌者格雷特", + "Grumbledorf": "男巫道博", + "Jack Morgan": "海盜傑克", + "Kronk": "肌肉男克羅克", + "Lee": "李", + "Lucky": "幸運兒", + "Mel": "大廚梅爾", + "Middle-Man": "平衡者", + "Minimus": "小姆斯", + "Pascal": "企鵝巴斯卡", + "Pixel": "精靈莉莉絲", + "Sammy Slam": "薩米斯拉姆", + "Santa Claus": "聖誕老人", + "Snake Shadow": "忍者蛇影", + "Spaz": "鋼盔斯巴子", + "Taobao Mascot": "淘公仔", + "Todd McBurton": "托德馬克波頓", + "Zoe": "遊俠佐伊", + "Zola": "刺殺者佐拉" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME}訓練", + "Infinite ${GAME}": "無限${GAME}", + "Infinite Onslaught": "無限衝鋒戰", + "Infinite Runaround": "無限塔防戰", + "Onslaught Training": "衝鋒訓練", + "Pro ${GAME}": "${GAME}Pro", + "Pro Football": "橄欖球戰Pro", + "Pro Onslaught": "衝鋒戰Pro", + "Pro Runaround": "塔防戰Pro", + "Rookie ${GAME}": "新手${GAME}", + "Rookie Football": "新手橄欖球戰", + "Rookie Onslaught": "新手衝鋒戰", + "The Last Stand": "最終殺敵戰", + "Uber ${GAME}": "高級${GAME}", + "Uber Football": "高級橄欖球戰", + "Uber Onslaught": "高級衝鋒戰", + "Uber Runaround": "高級塔防戰" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "在一定時間內取代選定目標進而取得勝利\n殺死選定目標並取而代之", + "Bomb as many targets as you can.": "盡可能多的摧毀炸毀", + "Carry the flag for ${ARG1} seconds.": "扛旗 ${ARG1} 秒", + "Carry the flag for a set length of time.": "在設定時長內扛旗", + "Crush ${ARG1} of your enemies.": "殺死 ${ARG1} 敵人", + "Defeat all enemies.": "打敗所有敵人", + "Dodge the falling bombs.": "躲避所有炸彈", + "Final glorious epic slow motion battle to the death.": "在最終殺敵中盡量得分吧", + "Gather eggs!": "收集蛋", + "Get the flag to the enemy end zone.": "舉旗進入敵方陣地", + "How fast can you defeat the ninjas?": "你能多快的打敗所有忍者", + "Kill a set number of enemies to win.": "殺死一定數量的敵人來獲勝", + "Last one standing wins.": "最終殺敵者獲勝", + "Last remaining alive wins.": "最終倖存者獲勝", + "Last team standing wins.": "最終殺敵團隊獲勝", + "Prevent enemies from reaching the exit.": "阻止敵人到達出口", + "Reach the enemy flag to score.": "抵達敵方旗幟來得分", + "Return the enemy flag to score.": "交回敵人旗幟來得分", + "Run ${ARG1} laps.": "跑${ARG1}圈", + "Run ${ARG1} laps. Your entire team has to finish.": "跑${ARG1} 圈。你的整個團隊必須來完成", + "Run 1 lap.": "跑1圈", + "Run 1 lap. Your entire team has to finish.": "跑1圈。你的整個團隊都需完成", + "Run real fast!": "快速奔跑", + "Score ${ARG1} goals.": "${ARG1}進球得分", + "Score ${ARG1} touchdowns.": "${ARG1}觸地得分", + "Score a goal.": "一次進球得分", + "Score a touchdown.": "一次觸地得分", + "Score some goals.": "多次觸地得分", + "Secure all ${ARG1} flags.": "固定所有的${ARG1}旗幟", + "Secure all flags on the map to win.": "固定地圖上的所有旗幟來得分", + "Secure the flag for ${ARG1} seconds.": "固定旗幟${ARG1}秒", + "Secure the flag for a set length of time.": "在設定時長內固定旗幟", + "Steal the enemy flag ${ARG1} times.": "竊取的敵人旗幟${ARG1}次", + "Steal the enemy flag.": "竊取敵人旗幟", + "There can be only one.": "誰會是唯一呢?", + "Touch the enemy flag ${ARG1} times.": "觸碰敵人旗幟${ARG1}次", + "Touch the enemy flag.": "觸碰敵人旗幟", + "carry the flag for ${ARG1} seconds": "舉旗${ARG1} 秒", + "kill ${ARG1} enemies": "殺死${ARG1}敵人", + "last one standing wins": "最終殺敵者獲勝", + "last team standing wins": "最終殺敵團隊獲勝", + "return ${ARG1} flags": "交回${ARG1}旗幟", + "return 1 flag": "交回1面旗幟", + "run ${ARG1} laps": "跑${ARG1}圈", + "run 1 lap": "跑1圈", + "score ${ARG1} goals": "${ARG1}進球得分", + "score ${ARG1} touchdowns": "${ARG1}觸地得分", + "score a goal": "一次進球得分", + "score a touchdown": "一次觸地得分", + "secure all ${ARG1} flags": "固定所有的${ARG1}旗幟", + "secure the flag for ${ARG1} seconds": "固定旗幟${ARG1}秒", + "touch ${ARG1} flags": "觸碰${ARG1}旗幟", + "touch 1 flag": "觸碰一次旗幟" + }, + "gameNames": { + "Assault": "突襲戰", + "Capture the Flag": "奪旗戰", + "Chosen One": "選定模式", + "Conquest": "佔領戰", + "Death Match": "死亡競賽", + "Easter Egg Hunt": "復活蛋獵人", + "Elimination": "消除戰", + "Football": "橄欖球戰", + "Hockey": "冰球戰", + "Keep Away": "舉起戰", + "King of the Hill": "據點佔領", + "Meteor Shower": "炸彈流星戰", + "Ninja Fight": "忍者大戰", + "Onslaught": "衝鋒戰", + "Race": "競速賽", + "Runaround": "塔防戰", + "Target Practice": "精準度訓練", + "The Last Stand": "最終殺敵戰" + }, + "inputDeviceNames": { + "Keyboard": "鍵盤", + "Keyboard P2": "鍵盤P2" + }, + "languages": { + "Arabic": "阿拉伯語", + "Belarussian": "白俄羅斯語", + "Chinese": "簡體中文", + "ChineseTraditional": "繁體中文", + "Croatian": "克羅地亞語", + "Czech": "捷克語", + "Danish": "丹麥語", + "Dutch": "荷蘭語", + "English": "英語", + "Esperanto": "世界語", + "Finnish": "芬蘭語", + "French": "法語", + "German": "德語", + "Gibberish": "用於測試", + "Greek": "希臘語", + "Hindi": "印度語", + "Hungarian": "匈牙利語", + "Indonesian": "印尼語", + "Italian": "意大利語", + "Japanese": "日語", + "Korean": "朝鮮語", + "Persian": "波斯文", + "Polish": "波蘭語", + "Portuguese": "葡萄牙語", + "Romanian": "羅馬尼亞語", + "Russian": "俄羅斯語", + "Serbian": "塞爾維亞語", + "Slovak": "斯洛伐克語", + "Spanish": "西班牙語", + "Swedish": "瑞典語", + "Turkish": "土耳其語", + "Ukrainian": "烏克蘭語", + "Venetian": "威尼斯語", + "Vietnamese": "越南語" + }, + "leagueNames": { + "Bronze": "銅牌", + "Diamond": "鑽石", + "Gold": "金牌", + "Silver": "銀牌" + }, + "mapsNames": { + "Big G": "大G", + "Bridgit": "小橋", + "Courtyard": "庭院", + "Crag Castle": "岩城", + "Doom Shroom": "蘑菇雲", + "Football Stadium": "橄欖球場", + "Happy Thoughts": "飛行地帶", + "Hockey Stadium": "冰球場", + "Lake Frigid": "寒湖", + "Monkey Face": "猴面", + "Rampage": "狂暴", + "Roundabout": "塔防", + "Step Right Up": "攻擊", + "The Pad": "平板", + "Tip Top": "頂點", + "Tower D": "塔防", + "Zigzag": "蜿蜒" + }, + "playlistNames": { + "Just Epic": "僅限史詩級", + "Just Sports": "僅限運動類" + }, + "scoreNames": { + "Flags": "旗幟", + "Goals": "進球", + "Score": "得分", + "Survived": "倖存", + "Time": "時間", + "Time Held": "保持時間" + }, + "serverResponses": { + "A code has already been used on this account.": "該賬戶已使用過促銷代碼", + "A reward has already been given for that address.": "您已經領取過該獎勵了", + "Account linking successful!": "賬號關聯成功", + "Account unlinking successful!": "取消關聯成功", + "Accounts are already linked.": "賬號已經連接", + "An error has occurred; (${ERROR})": "出現了一個錯誤(${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "出現了一個錯誤,請聯繫客服解決(${ERROR})", + "An error has occurred; please contact support@froemling.net.": "發生了一個錯誤,請聯繫support@froemling.net", + "An error has occurred; please try again later.": "發生了一個錯誤,請稍後再試", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "確定要連接這些賬戶?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\n此操作可在30天后取消", + "BombSquad Pro unlocked!": "BombSquad Pro已解鎖", + "Can't link 2 accounts of this type.": "無法連接這兩個賬號", + "Can't link 2 diamond league accounts.": "無法連接兩個鑽石聯賽的賬號", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "無法連接,已超過關聯上線${COUNT}個賬號", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "發現賬號作弊行為;得分及獎勵在${COUNT}天內暫停", + "Could not establish a secure connection.": "無法建立安全連接", + "Daily maximum reached.": "已達今日上限", + "Entering tournament...": "進入錦標賽...", + "Invalid code.": "代碼無效", + "Invalid payment; purchase canceled.": "不可用的付款方式:交易取消", + "Invalid promo code.": "促銷代碼無效", + "Invalid purchase.": "購買無效", + "Invalid tournament entry; score will be ignored.": "錯誤的聯賽資料,分數會被忽略", + "Item unlocked!": "項目已解除綁定", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "連接賬號行為取消,${ACCOUNT}含有\n的重要數據可能會丟失\n如果想要關聯的話,你可以反向鏈接賬號\n(最好用自己的賬號進行關聯)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "確定將${ACCOUNT}關聯到此賬戶嗎\n${ACCOUNT}將共享數據\n可在30天后取消", + "Max number of playlists reached.": "已達到最大列表數", + "Max number of profiles reached.": "已達到最大檔案數", + "Maximum friend code rewards reached.": "邀請碼獎勵達到上限", + "Message is too long.": "消息過長", + "Profile \"${NAME}\" upgraded successfully.": "\"${NAME}\"檔案升級成功", + "Profile could not be upgraded.": "檔案不可升級", + "Purchase successful!": "購買成功", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "登錄簽到獎勵${COUNT} 點券\n明天簽到可獲得${TOMORROW_COUNT}點券", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "此版本不在支持伺服器功能\n請升級到最新版本", + "Sorry, there are no uses remaining on this code.": "對不起,此代碼已經無法繼續使用了", + "Sorry, this code has already been used.": "對不起,此代碼已被使用", + "Sorry, this code has expired.": "對不起,此代碼已失效", + "Sorry, this code only works for new accounts.": "對不起,此代碼僅適用於新用戶", + "Temporarily unavailable; please try again later.": "目前暫不可用,請稍後再試", + "The tournament ended before you finished.": "本次錦標賽將於在你完成之前結束了", + "This account cannot be unlinked for ${NUM} days.": "此賬戶在${NUM}天內無法取消關聯", + "This code cannot be used on the account that created it.": "此代碼無法在創建的其他賬戶上使用", + "This is currently unavailable; please try again later.": "當前不可用,請稍後再試", + "This requires version ${VERSION} or newer.": "這需要${VERSION}或更高的版本", + "Tournaments disabled due to rooted device.": "此設備已禁賽", + "Tournaments require ${VERSION} or newer": "比賽需要${VERSION}或更高的版本", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "從此賬戶上取消${ACCOUNT}的關聯?\n${ACCOUNT}上的數據將會被重置\n(在某些情況下成就除外)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "警告:針對你的賬戶發出黑客控訴\n被盜用的賬戶將被禁止。請公平競技", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "是否經你的賬戶連接到此?\n\n你的賬戶為${ACCOUNT1}\n此賬戶為${ACCOUNT2}\n\n你可以保存現有進度\n警告:此操作可在30天后取消", + "You already own this!": "你已擁有這個", + "You can join in ${COUNT} seconds.": "你可以在${COUNT}秒後加入", + "You don't have enough tickets for this!": "你的點券不足", + "You don't own that.": "你尚未擁有", + "You got ${COUNT} tickets!": "你獲得了${COUNT}點券", + "You got a ${ITEM}!": "你獲得了一個${ITEM}!", + "You have been promoted to a new league; congratulations!": "你已經被升級至一個全新的聯賽:恭喜", + "You must update to a newer version of the app to do this.": "你必須先更新遊戲才能這麼做", + "You must update to the newest version of the game to do this.": "你必須更新到最新版本才可以執行此操作", + "You must wait a few seconds before entering a new code.": "你必須要在輸入新代碼前等待一會", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "你在上一場錦標賽中排名#${RANK},繼續加油", + "Your account was rejected. Are you signed in?": "你的髒號被拒絕,您是否已登陸?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "您的遊戲副本已更改\n請恢復任何更改並重試", + "Your friend code was used by ${ACCOUNT}": "${ACCOUNT}已使用您的促銷代碼" + }, + "settingNames": { + "1 Minute": "1分鐘", + "1 Second": "1秒", + "10 Minutes": "10分鐘", + "2 Minutes": "2分鐘", + "2 Seconds": "2秒", + "20 Minutes": "20分鐘", + "4 Seconds": "4秒", + "5 Minutes": "5分鐘", + "8 Seconds": "8秒", + "Allow Negative Scores": "允許負分", + "Balance Total Lives": "平衡總生命", + "Bomb Spawning": "生成炸彈", + "Chosen One Gets Gloves": "選定目標獲得拳擊手套", + "Chosen One Gets Shield": "選定目標獲得能量護盾", + "Chosen One Time": "選定模式時間", + "Enable Impact Bombs": "啟用觸感炸彈", + "Enable Triple Bombs": "啟用三連炸彈", + "Entire Team Must Finish": "需要所有隊伍成員一起通過", + "Epic Mode": "史詩模式", + "Flag Idle Return Time": "旗幟閒置重置時間", + "Flag Touch Return Time": "旗幟觸碰重置時間", + "Hold Time": "保持時間", + "Kills to Win Per Player": "玩家獲勝擊殺數", + "Laps": "圈數", + "Lives Per Player": "每位玩家的初始生命數量", + "Long": "長", + "Longer": "更長", + "Mine Spawning": "地雷生成", + "No Mines": "無地雷", + "None": "無", + "Normal": "正常", + "Pro Mode": "Pro模式", + "Respawn Times": "復活時長", + "Score to Win": "獲勝分數", + "Short": "短", + "Shorter": "更短", + "Solo Mode": "單人模式", + "Target Count": "目標計數", + "Time Limit": "時限" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} 被踢,因為${PLAYER}離開遊戲", + "Killing ${NAME} for skipping part of the track!": "殺死跳過部分賽道的${NAME}", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "警告:檢測到${NAME}操作異常 將使其被踢出" + }, + "teamNames": { + "Bad Guys": "敵方", + "Blue": "藍隊", + "Good Guys": "我方", + "Red": "紅隊" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "一记完美、及时的“跑跳旋转拳”可一次性击杀敌人,并\n助你一生享有好友的尊重。", + "Always remember to floss.": "地面上的辅助线可能会有用。", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "使用首选名称和外观,而非采用随机形式\n来为自己和好友创建玩家档案。", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "诅咒之盒把你变成了一个定时炸弹。\n唯一的解决方法是迅速抢占一个生命值包。", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "尽管长相不同,所有人物的技能是相同的,\n所以只需随意挑选一个与你最相似的。", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "不要因为拥有能量盾牌而狂妄自大;你仍然可能使自己坠入悬崖。", + "Don't run all the time. Really. You will fall off cliffs.": "不要总是奔跑。真的。你可能会坠入悬崖。", + "Don't spin for too long; you'll become dizzy and fall.": "不要旋转得太久,不然你会眩晕并摔倒。", + "Hold any button to run. (Trigger buttons work well if you have them)": "按住任意按钮来奔跑。(如果你有的话,扳机按钮将会很有用)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "按住任意按钮来奔跑。你将会更快地抵达一些地方,\n但是转弯效果并不好,所以当心悬崖。", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "寒冰炸弹并非很厉害,但它们能够冻结\n任何被击中者,使他们极易粉碎。", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "如果有人将你提起,出拳攻击他们,他们便会放手。\n这在现实生活中同样有效。", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "如果您缺控制器,在手機上安裝「${REMOTE_APP_NAME}」\n並用手機當控制器。", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "如果一个黏黏弹将你困住,你应该四处跳动并转圈。你可能\n将炸弹抖落,或如果没有其他办法,你最后的时刻将是有趣的。", + "If you kill an enemy in one hit you get double points for it.": "如果你一击杀死一个敌人,你将获得双倍积分。", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "如果你捡到一个诅咒之盒,你唯一的生存希望是\n在接下来的几秒内找到一个生命值提升器。", + "If you stay in one place, you're toast. Run and dodge to survive..": "如果你停留在一个地方,你就完了。为了生存而奔跑和躲避……", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "如果众多玩家进进出出,在设置下打开“自动踢出闲置玩家”,以防\n任何玩家忘记离开游戏。", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "如果你的设备过热,或者你想要节省电池电量,\n则在设置->图形中调低“视觉效果”或“分辨率”", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "如果你的帧速率不稳定,请尝试在游戏的\n图形设置中调低分辨率或视觉效果。", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "在夺旗战中,你的旗帜必须位于你的基地才能得分,如果对方\n团队即将得分,窃取他们的旗帜是一个不错的阻止方法。", + "In hockey, you'll maintain more speed if you turn gradually.": "在冰球战中,逐渐转向将使你保持更快的速度。", + "It's easier to win with a friend or two helping.": "在拥有一名好友或两个帮扶的情况下更易获胜", + "Jump just as you're throwing to get bombs up to the highest levels.": "就像你试图将炸弹扔到最高点那样跳起来。", + "Land-mines are a good way to stop speedy enemies.": "地雷是阻止高速敌人的一个很好的方式。", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "很多东西都可以捡起来并投掷,包括其他玩家。将你的\n敌人抛下悬崖可能是一个有效的且情感上可获得满足的策略。", + "No, you can't get up on the ledge. You have to throw bombs.": "不,你不能在岩脊上起身。你必须要投掷炸弹", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "玩家可在大多数游戏中途加入或离开,\n同时,你也可以在百忙中插上或拔出手柄。", + "Practice using your momentum to throw bombs more accurately.": "练习借助你的力量更准确地投掷炸弹。", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "拳头跑得越快,拳击的伤害越高,\n所以请成为飞奔的拳击手吧。", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "在投掷炸弹之前来回跑动,\n以“鞭打”炸弹,并将其投掷更远。", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "在TNT炸药箱附近引爆\n一个炸弹来消灭一群敌人。", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "头部是最脆弱的区域,所以一个黏黏弹\n接触头部通常便意味着游戏结束。", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "这一关卡永远不会结束,但是更高的得分将\n助你赢得全世界永恒的尊重。", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "投掷力量取决于你所保持的方向。\n如要向前方轻轻投掷某物,不要保持在任何方向。", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "更换背景音乐?更换成你自己音乐吧!\n参见设置->音频->背景音乐", + "Try 'Cooking off' bombs for a second or two before throwing them.": "尝试在投掷之前将炸弹“爆燃”一秒或两秒。", + "Try tricking enemies into killing eachother or running off cliffs.": "试图诱使敌人互相厮杀或坠入悬崖。", + "Use the pick-up button to grab the flag < ${PICKUP} >": "使用拾取按钮来抢夺旗帜< ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "来回鞭打以投掷更远距离……", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "你可以通过左转或右转“瞄准”出拳。\n这有利于将坏人击倒出边界或在冰球战中得分。", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "你可以根据导火线火花的颜色判断炸弹什么时候爆炸:\n黄色……橙色……红色……嘭。", + "You can throw bombs higher if you jump just before throwing.": "如果投弹前跳起,你将投掷更远。", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "当你用头部重击物体时将受到伤害,\n所以尽量不要用头部重击物体。", + "Your punches do much more damage if you are running or spinning.": "如果你奔跑或旋转,拳击的伤害将更高。" + } + }, + "trophiesRequiredText": "你至少要擁有${NUMBER}個獎杯", + "trophiesText": "獎杯", + "trophiesThisSeasonText": "本賽季獎杯", + "tutorial": { + "cpuBenchmarkText": "以驚人的速度來運行遊戲教程(用於測試CPU上限)", + "phrase01Text": "嘿,夥計", + "phrase02Text": "歡迎來到${APP_NAME}!", + "phrase03Text": "以下適用於此遊戲的一些技巧", + "phrase04Text": "${APP_NAME}在很多情況下還是很物理的", + "phrase05Text": "比如,當你出拳時", + "phrase06Text": "拳頭的傷害取決於你它的速度", + "phrase07Text": "看到了嗎,如果我們不動,這樣幾乎不會造成傷害${NAME}", + "phrase08Text": "現在,讓我們跳躍並旋轉起來,以獲得更快的速度", + "phrase09Text": "啊,這樣就好多了", + "phrase10Text": "奔跑也會發揮作用", + "phrase11Text": "按住任意按鈕來奔跑", + "phrase12Text": "如果要完成一個很棒的出拳,請持續奔跑和旋轉", + "phrase13Text": "humm 關於${NAME}我很抱歉", + "phrase14Text": "你可以撿起投擲物品,如炸彈、旗幟或${NAME}", + "phrase15Text": "最後還有炸彈", + "phrase16Text": "投擲炸彈需要練習", + "phrase17Text": "ahh這一次貌似並不漂亮", + "phrase18Text": "移動有助於你投擲的更遠", + "phrase19Text": "跳躍有助於你投擲的更高", + "phrase20Text": "甩炸彈會讓你把炸彈投擲到更遠的位置", + "phrase21Text": "把握好時間可能會十分困難", + "phrase22Text": "¿", + "phrase23Text": "嘗試將導火線控制的更加精準", + "phrase24Text": "OHHHHH爆炸就是藝術", + "phrase25Text": "OK已經很不錯了", + "phrase26Text": "現在去完成你的任務吧,兄弟", + "phrase27Text": "記住你的訓練,你會活著回來的", + "phrase28Text": "也許...是吧", + "phrase29Text": "祝你好運", + "randomName1Text": "Fred", + "randomName2Text": "Harry", + "randomName3Text": "Bill", + "randomName4Text": "Chuck", + "randomName5Text": "Phil", + "skipConfirmText": "確定要跳過教程?再次按下按鈕來確定", + "skipVoteCountText": "${COUNT}/${TOTAL} 跳過投票", + "skippingText": "跳過教程...", + "toSkipPressAnythingText": "(按下任意鍵來跳過教程)" + }, + "twoKillText": "雙殺!", + "unavailableText": "不可用", + "unconfiguredControllerDetectedText": "檢測到未配置的手柄", + "unlockThisInTheStoreText": "這必須從商店解鎖", + "unlockThisProfilesText": "如需創建超過${NUM}個檔案,你需要", + "unlockThisText": "你需要這些來解鎖", + "unsupportedHardwareText": "抱歉,此版本的遊戲不支持該硬件", + "upFirstText": "進入第一局", + "upNextText": "進入比賽${COUNT}第二局", + "updatingAccountText": "更新你的賬戶", + "upgradeText": "升級", + "upgradeToPlayText": "你必須在商店裡解鎖${PRO}以體驗此內容", + "useDefaultText": "使用默認值", + "usesExternalControllerText": "該遊戲使用外部手柄進行接入", + "usingItunesText": "使用音樂軟件設置背景音樂...", + "validatingTestBuildText": "測試版驗證中", + "victoryText": "勝利!", + "voteDelayText": "你不能在${NUMBER}內發起一個新的投票", + "voteInProgressText": "已經有一個投票正在進行中了", + "votedAlreadyText": "你已經參與過投票了", + "votesNeededText": "通過需要${NUMBER}個投票", + "vsText": "vs.", + "waitingForHostText": "(等待${HOST}繼續)", + "waitingForPlayersText": "等待玩家加入", + "waitingInLineText": "正在派對等候(派對爆滿)", + "watchAVideoText": "觀看一個廣告視頻", + "watchAnAdText": "觀看廣告", + "watchWindow": { + "deleteConfirmText": "刪除\"${REPLAY}\"?", + "deleteReplayButtonText": "刪除\n回放", + "myReplaysText": "我的回放", + "noReplaySelectedErrorText": "未選擇回放", + "playbackSpeedText": "回放速度: ${SPEED}", + "renameReplayButtonText": "重命名\n回放", + "renameReplayText": "重命名\"${REPLAY}\"至:", + "renameText": "重命名", + "replayDeleteErrorText": "刪除回放錯誤", + "replayNameText": "錄像名稱", + "replayRenameErrorAlreadyExistsText": "該名稱的回放已經存在", + "replayRenameErrorInvalidName": "無法重命名回放:名稱無效", + "replayRenameErrorText": "重命名回放錄像時出現錯誤。", + "sharedReplaysText": "已分享錄像", + "titleText": "回放錄像", + "watchReplayButtonText": "觀看\n重放" + }, + "waveText": "波", + "wellSureText": "確定!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote版權擁有©" + }, + "wiimoteListenWindow": { + "listeningText": "監聽Wiimotes...", + "pressText": "同時按下Wiimote的按鈕1和按鈕2", + "pressText2": "再較新的安裝有Motion Plus的Wiimotes上,可按下背部紅色的'sync'按鈕作為替代" + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote 版權所有", + "listenText": "監聽", + "macInstructionsText": "確保您Mac上的Wil已關閉且藍牙功能已啟用\n然後按下監聽,Wiimote支持可能出現不穩定\n所以在完成連接之前\n您肯呢個需要嘗試多次\n\n藍牙需能夠處理多達7台連接設備\n但您的里程可能會有所不同\n\nBombsquad支持原裝Wiimotes, Nunchuks\n及經典手柄\n較新版本的Wii Remote Plus現也可以使用\n但與附件不兼容", + "thanksText": "感謝DarwiinRemote團隊的努力\n這一切已成為可能", + "titleText": "Wiimote設置" + }, + "winsPlayerText": "${NAME}贏了!", + "winsTeamText": "${NAME}贏了!", + "winsText": "${NAME}贏了!", + "worldScoresUnavailableText": "無法讀入全球分數", + "worldsBestScoresText": "全球最佳分數", + "worldsBestTimesText": "全球最佳時間", + "xbox360ControllersWindow": { + "getDriverText": "獲取驅動程式", + "macInstructions2Text": "如果想使用無線手柄,您還需要\n“用於Windows的無線Xbox360手柄的無線接收器”\n一個接收器可讓你連接多達4個手柄\n\n重要提示:第三方接收器與此驅動程序不兼容\n確保您的接收器標示有“Microsoft”而非“Xbox360”\nMicrosoft不再單獨出售該產品,所以您需要獲得\n手柄附帶的接收器或在eBoy搜索其他設備\n\n如果這對你有幫助,請考慮在驅動程序開發人員\n網站進行捐贈", + "macInstructionsText": "若需使用Xbox360控制器,你需要安裝\n下方超鏈接中的Mac 專用驅動。\n這個驅動同時支持無線與有限連接方式。", + "ouyaInstructionsText": "如果要在Bombsquad中使用有線Xbox360手柄,只需\n將其插入USB接口。您可以使用USB集線器\n以連接多個手柄\n\n如果要使用無線手柄,您需要一台無線接收器\n其可以作為“用於Windows的無線Xbox360手柄”\n安裝包的一部分或單獨出售。每一個接收器插入一個USB接口\n允許你連接多達4個手柄", + "titleText": "通過${APP_NAME}使用Xbox 360 控制器:" + }, + "yesAllowText": "是的,允許!", + "yourBestScoresText": "最佳分數", + "yourBestTimesText": "最佳時間" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/croatian.json b/dist/ba_data/data/languages/croatian.json new file mode 100644 index 0000000..53efc5f --- /dev/null +++ b/dist/ba_data/data/languages/croatian.json @@ -0,0 +1,1838 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Ime računa nemože satržavati emotikone ili ostale posebne znakove", + "accountProfileText": "(korisnički račun)", + "accountsText": "Profili", + "achievementProgressText": "Postignuća: ${COUNT} od ${TOTAL}", + "campaignProgressText": "Napredak u kampanji [Teško]: ${PROGRESS}", + "changeOncePerSeason": "Ovo možeš promjeniti samo jednom po sezoni.", + "changeOncePerSeasonError": "Moraš pričekati sljedeču sezonu da promjeniš ovo(${NUM} days)", + "customName": "Prilagođeno ime", + "linkAccountsEnterCodeText": "Unesi kod", + "linkAccountsGenerateCodeText": "Stvori kod", + "linkAccountsInfoText": "(podijeli napredak na svim platformama)", + "linkAccountsInstructionsNewText": "Za povezivanje dva računa, generiraj kod sa prvog\ni unesi taj kod na drugi. Podaci s drugog računa\nće biti podjeljeni između oba.\n(Podaci s prvog računa će biti izgubljeni)\n\nMožeš povezati do ${COUNT} računa.\n\nVAŽNO: povezuj samo vlastite račune;\nAko povežeš prijateljev račun onda nećete moći\nzajedno igrati u isto vrijeme.", + "linkAccountsInstructionsText": "Da povežeš dva profila, stvori kod na \njednomod njih i unesi ga na drugom.\nNapredak i sve kupljeno bit će kombinirano.\nMožeš povezati najviše ${COUNT} profila.", + "linkAccountsText": "Poveži profile", + "linkedAccountsText": "Povezani profili:", + "nameChangeConfirm": "Promjeni svoje ime u ${NAME}?", + "resetProgressConfirmNoAchievementsText": "Ovo će poništiti tvoj napredak u timskom modu i\ntvoje najbolje rezultate (ali ne i tvoje kupone).\nNemaš mogućnost povratka. Jesi li siguran?", + "resetProgressConfirmText": "Ovo će poništiti tvoj napredak u timskom modu,\npostignuća, i vaše najbolje rezultate\n(ali ne i tvoje kupone). Nemaš mogućnost\npovratka. Jesi li siguran?", + "resetProgressText": "Počni ponovno", + "setAccountName": "Postavi ime računa", + "setAccountNameDesc": "Odaberi koje ime da se prikaže za tvoj račun.\nMožeš koristiti ime jednog od tvojih povezanih računa\nili kreirati vlastito prilagođeno ime.", + "signInInfoText": "Prijavi se da možeš dobivati kupone, natjecati\nse online, i dijeliti napredak između uređaja.", + "signInText": "Prijavi se", + "signInWithDeviceInfoText": "(automatski profil dostupan samo na ovom uređaju)", + "signInWithDeviceText": "Prijavi se sa profilom uređaja", + "signInWithGameCircleText": "Prijavi se sa Game Circle", + "signInWithGooglePlayText": "Prijavi sa sa Google Play", + "signInWithTestAccountInfoText": "(stari tip profila; ubuduće koristi profile uređaja)", + "signInWithTestAccountText": "Prijavi se s testnim profilom", + "signOutText": "Odjavi se", + "signingInText": "Prijava...", + "signingOutText": "Odjava...", + "testAccountWarningCardboardText": "Upozorenje: prijavljuješ se s \"testnim\" računom.\nOn će biti zamijenjen Google računom kada\nGoogle računi postanu podržani u Cardboard aplikacijama.\n\nZasad ćeš morati sakupiti sve kupone u igri.\n(u svakom slučaju ćeš dobiti BombSquad Pro nadogradnju besplatno)", + "testAccountWarningOculusText": "Upozorenje: prijavljuješ se s \"testnim\" računom. \nOn će biti zamijenjen s Oculus računom kasnije ove\ngodine koji će omogućiti kupovanje kuponima i ostale značajke. \n\nZasad ćeš morati sakupiti sve kupone u igri. \n(u svakom slučaju ćeš dobiti BombSquad Pro nadogradnju besplatno)", + "testAccountWarningText": "Upozorenje: prijavljuješ se \"testnim\" računom. \nOvaj račun je vezan za ovaj uređaj i\nmože se povremeno izbrisati. (zato te molim da \nne trošiš previše vremena skupljajući/otključavajući stvari za njega)\n\nPokreni verziju igre iz trgovine da možeš koristiti \"pravi\" \nračun (Game-Center, Google Plus, itd.) Ovo ti također \nomogućuje da sačuvaš svoj napredak u oblaku i dijeliš\nga s drugim uređajima. ", + "ticketsText": "Kuponi: ${COUNT}", + "titleText": "Račun", + "unlinkAccountsInstructionsText": "Odaberi račun za prekid veze.", + "unlinkAccountsText": "Prekini vezu računa", + "viaAccount": "(Preko ${NAME} računa)", + "youAreSignedInAsText": "Prijavljeni ste kao:" + }, + "achievementChallengesText": "Izazovi za postignuća", + "achievementText": "Postignuće", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Ubij 3 neprijatelja TNT-om", + "descriptionComplete": "Ubio si 3 neprijatelja TNT-om", + "descriptionFull": "Ubij 3 neprijatelja TNT-om na ${LEVEL}", + "descriptionFullComplete": "Ubio si 3 neprijatelja TNT-om na ${LEVEL}", + "name": "3, 2, 1, BUM!!!" + }, + "Boxer": { + "description": "Pobijedi bez korištenja bombi", + "descriptionComplete": "Pobijedio si bez korištenja bombi", + "descriptionFull": "Dovrši ${LEVEL} bez korištenja bombi", + "descriptionFullComplete": "Dovršio si ${LEVEL} bez korištenja bombi", + "name": "Boksač" + }, + "Dual Wielding": { + "descriptionFull": "Konektuj 2 kontrolera (hardver ili aplikacija)", + "descriptionFullComplete": "Konektovana 2 kontrolera (hardver ili aplikacija)", + "name": "Dupla Kontrola" + }, + "Flawless Victory": { + "description": "Pobijedi bez primljenog udarca", + "descriptionComplete": "Pobijedio si bez primljenog udarca", + "descriptionFull": "Pobijedi na ${LEVEL} bez primljenog udarca", + "descriptionFullComplete": "Pobijedio si na ${LEVEL} bez primljenog udarca", + "name": "Pobjeda bez greške" + }, + "Free Loader": { + "descriptionFull": "Pokreni Svako-Za-Svakog igru sa 2+ igrača", + "descriptionFullComplete": "Pokrenuta Svako-Za-Svakog igra sa 2+ igrača", + "name": "Vuk Samotnjak" + }, + "Gold Miner": { + "description": "Ubij 6 neprijatelja minama", + "descriptionComplete": "Ubio si 6 neprijatelja minama", + "descriptionFull": "Ubij 6 neprijatelja minama na ${LEVEL}", + "descriptionFullComplete": "Ubio si 6 neprijatelja minama na ${LEVEL}", + "name": "Eli-mina-tor" + }, + "Got the Moves": { + "description": "Pobijedi bez korištenja bombi i udaraca", + "descriptionComplete": "Pobijedio si bez korištenja bombi i udaraca", + "descriptionFull": "Pobijedi na ${LEVEL} bez korištenja bombi i udaraca", + "descriptionFullComplete": "Pobijedio si na ${LEVEL} bez korištenja bombi i udaraca", + "name": "Imaš ritma" + }, + "In Control": { + "descriptionFull": "Konektuj kontroler. (hardver ili aplikacija)", + "descriptionFullComplete": "Konektovan kontroler. (hardver ili aplikacija)", + "name": "U Kontroli" + }, + "Last Stand God": { + "description": "Osvoji 1000 bodova", + "descriptionComplete": "Osvojio si 1000 bodova", + "descriptionFull": "Osvoji 1000 bodova na ${LEVEL}", + "descriptionFullComplete": "Osvojio si 1000 bodova na ${LEVEL}", + "name": "Bog (${LEVEL})" + }, + "Last Stand Master": { + "description": "Osvoji 250 bodova", + "descriptionComplete": "Osvojio si 250 bodova", + "descriptionFull": "Osvoji 250 bodova na ${LEVEL}", + "descriptionFullComplete": "Osvojio si 250 bodova na ${LEVEL}", + "name": "Majstor (${LEVEL})" + }, + "Last Stand Wizard": { + "description": "Osvoji 500 bodova", + "descriptionComplete": "Osvojio si 500 bodova", + "descriptionFull": "Osvoji 500 bodova na ${LEVEL}", + "descriptionFullComplete": "Osvojio si 500 bodova na ${LEVEL}", + "name": "Čarobnjak (${LEVEL})" + }, + "Mine Games": { + "description": "Ubij 3 neprijatelja minama", + "descriptionComplete": "Ubio si 3 neprijatelja minama", + "descriptionFull": "Ubij 3 neprijatelja minama na ${LEVEL}", + "descriptionFullComplete": "Ubio si 3 neprijatelja minama na ${LEVEL}", + "name": "Igra s minama" + }, + "Off You Go Then": { + "description": "Baci 3 neprijatelja preko ruba mape", + "descriptionComplete": "Bacio si 3 neprijatelja preko ruba mape", + "descriptionFull": "Baci 3 neprijatelja preko ruba mape na ${LEVEL}", + "descriptionFullComplete": "Bacio si 3 neprijatelja preko ruba mape na ${LEVEL}", + "name": "Do (ne)viđenja" + }, + "Onslaught God": { + "description": "Osvoji 5000 bodova", + "descriptionComplete": "Osvojio si 5000 bodova", + "descriptionFull": "Osvoji 5000 bodova na ${LEVEL}", + "descriptionFullComplete": "Osvojio si 5000 bodova na ${LEVEL}", + "name": "Bog (${LEVEL})" + }, + "Onslaught Master": { + "description": "Osvoji 500 bodova", + "descriptionComplete": "Osvojio si 500 bodova", + "descriptionFull": "Osvoji 500 bodova na ${LEVEL}", + "descriptionFullComplete": "Osvojio si 500 bodova na ${LEVEL}", + "name": "Majstor (${LEVEL})" + }, + "Onslaught Training Victory": { + "description": "Pobijedi sve nalete", + "descriptionComplete": "Pobijedio si sve nalete", + "descriptionFull": "Pobijedi sve nalete na ${LEVEL}", + "descriptionFullComplete": "Pobijedio si sve nalete na ${LEVEL}", + "name": "Pobjeda na ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Osvoji 1000 bodova", + "descriptionComplete": "Osvojio si 1000 bodova", + "descriptionFull": "Osvoji 1000 bodova na ${LEVEL}", + "descriptionFullComplete": "Osvojio si 1000 bodova na ${LEVEL}", + "name": "Čarobnjak (${LEVEL})" + }, + "Precision Bombing": { + "description": "Pobijedi bez ikakvih bonusa", + "descriptionComplete": "Pobijedio si bez ikakvih bonusa", + "descriptionFull": "Pobijedi na ${LEVEL} bez ikakvih bonusa", + "descriptionFullComplete": "Pobijedio si na ${LEVEL} bez ikakvih bonusa", + "name": "Precizno bombardiranje" + }, + "Pro Boxer": { + "description": "Pobijedi bez korištenja bombi", + "descriptionComplete": "Pobijedio si bez korištenja bombi", + "descriptionFull": "Pobijedi na ${LEVEL} bez korištenja bombi", + "descriptionFullComplete": "Pobijedio si na ${LEVEL} bez korištenja bombi", + "name": "Profesionalni boksač" + }, + "Pro Football Shutout": { + "description": "Pobijedi ne dopuštajući lošim dečkima da zabiju gol", + "descriptionComplete": "Pobijedio si ne dopuštajući lošim dečkima da zabiju gol", + "descriptionFull": "Pobijedi na ${LEVEL} ne dopuštajući lošim dečkima da zabiju gol", + "descriptionFullComplete": "Pobijedio si na ${LEVEL} ne dopuštajući lošim dečkima da zabiju gol", + "name": "Prazna mreža (${LEVEL})" + }, + "Pro Football Victory": { + "description": "Pobijedi", + "descriptionComplete": "Pobijedio si", + "descriptionFull": "Pobijedi na ${LEVEL}", + "descriptionFullComplete": "Pobijedio si na ${LEVEL}", + "name": "Pobjeda na ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Pobijedi sve nalete", + "descriptionComplete": "Pobijedio si sve nalete", + "descriptionFull": "Pobijedi sve nalete na ${LEVEL}", + "descriptionFullComplete": "Pobijedio si sve nalete na ${LEVEL}", + "name": "Pobjeda na ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Pobijedi sve nalete", + "descriptionComplete": "Pobijedio si sve nalete", + "descriptionFull": "Pobijedi sve nalete na ${LEVEL}", + "descriptionFullComplete": "Pobijedio si sve nalete na ${LEVEL}", + "name": "Pobjeda na ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Pobijedi ne dopuštajući lošim dečkima da zabiju gol", + "descriptionComplete": "Pobijedio si ne dopuštajući lošim dečkima da zabiju gol", + "descriptionFull": "Pobijedi na ${LEVEL} ne dopuštajući lošim dečkima da zabiju gol", + "descriptionFullComplete": "Pobijedio si na ${LEVEL} ne dopuštajući lošim dečkima da zabiju gol", + "name": "Prazna mreža (${LEVEL})" + }, + "Rookie Football Victory": { + "description": "Pobijedi", + "descriptionComplete": "Pobijedio si", + "descriptionFull": "Pobijedi na ${LEVEL}", + "descriptionFullComplete": "Pobijedio si na ${LEVEL}", + "name": "Pobjeda na ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Pobijedi sve nalete", + "descriptionComplete": "Pobijedio si sve nalete", + "descriptionFull": "Pobijedi sve nalete na ${LEVEL}", + "descriptionFullComplete": "Pobijedio si sve nalete na ${LEVEL}", + "name": "Pobjeda na ${LEVEL}" + }, + "Runaround God": { + "description": "Osvoji 2000 bodova", + "descriptionComplete": "Osvojio si 2000 bodova", + "descriptionFull": "Osvoji 2000 bodova na ${LEVEL}", + "descriptionFullComplete": "Osvojio si 2000 bodova na ${LEVEL}", + "name": "Bog (${LEVEL})" + }, + "Runaround Master": { + "description": "Osvoji 500 bodova", + "descriptionComplete": "Osvojio si 500 bodova", + "descriptionFull": "Osvoji 500 bodova na ${LEVEL}", + "descriptionFullComplete": "Osvojio si 500 bodova na ${LEVEL}", + "name": "Majstor (${LEVEL})" + }, + "Runaround Wizard": { + "description": "Osvoji 1000 bodova", + "descriptionComplete": "Osvojio si 1000 bodova", + "descriptionFull": "Osvoji 1000 bodova na ${LEVEL}", + "descriptionFullComplete": "Osvojio si 1000 bodova na ${LEVEL}", + "name": "Čarobnjak (${LEVEL})" + }, + "Sharing is Caring": { + "descriptionFull": "Uspešno podeli igru sa drugom", + "descriptionFullComplete": "Uspešno podeljena igra sa drugom", + "name": "Deljenje na Čekanju" + }, + "Stayin' Alive": { + "description": "Pobijedi bez da ijednom umreš", + "descriptionComplete": "Pobijedio si bez da si ijednom umro", + "descriptionFull": "Pobijedi na ${LEVEL} bez da ijednom umreš", + "descriptionFullComplete": "Pobijedio si na ${LEVEL} bez da si ijednom umro", + "name": "Preživljavam" + }, + "Super Mega Punch": { + "description": "Nanesi 100% štete jednim udarcem", + "descriptionComplete": "Nanio si 100% štete jednim udarcem", + "descriptionFull": "Nanesi 100% štete jednim udarcem na ${LEVEL}", + "descriptionFullComplete": "Nanio si 100% štete jednim udarcem na ${LEVEL}", + "name": "Super mega udarac" + }, + "Super Punch": { + "description": "Nanesi 50% štete jednim udarcem", + "descriptionComplete": "Nanio si 50% štete jednim udarcem", + "descriptionFull": "Nanesi 50% štete jednim udarcem na ${LEVEL}", + "descriptionFullComplete": "Nanio si 50% štete jednim udarcem na ${LEVEL}", + "name": "Super Udarac" + }, + "TNT Terror": { + "description": "Ubij 6 neprijatelja TNT-om", + "descriptionComplete": "Ubio si 6 neprijatelja TNT-om", + "descriptionFull": "Ubij 6 neprijatelja TNT-om na ${LEVEL}", + "descriptionFullComplete": "Ubio si 6 neprijatelja TNT-om na ${LEVEL}", + "name": "TNT ubojica" + }, + "Team Player": { + "descriptionFull": "Pokreni igru Timova sa 4+ igrača", + "descriptionFullComplete": "Pokrenuta igra Timova sa 4+ igrača", + "name": "Timski Igrač" + }, + "The Great Wall": { + "description": "Zaustavi baš svakog neprijatelja", + "descriptionComplete": "Zaustavio si baš svakog neprijatelja", + "descriptionFull": "Zaustavi baš svakog neprijatelja na ${LEVEL}", + "descriptionFullComplete": "Zaustavio si baš svakog neprijatelja na ${LEVEL}", + "name": "Veliki zid" + }, + "The Wall": { + "description": "Zaustavi baš svakog neprijatelja", + "descriptionComplete": "Zaustavio si baš svakog neprijatelja", + "descriptionFull": "Zaustavi baš svakog neprijatelja na ${LEVEL}", + "descriptionFullComplete": "Zaustavio si baš svakog neprijatelja na ${LEVEL}", + "name": "Zid" + }, + "Uber Football Shutout": { + "description": "Pobijedi ne dopuštajući lošim dečkima da zabiju gol", + "descriptionComplete": "Pobijedio si ne dopuštajući lošim dečkima da zabiju gol", + "descriptionFull": "Pobijedi ${LEVEL} ne dopuštajući lošim dečkima da zabiju gol", + "descriptionFullComplete": "Pobijedio si na ${LEVEL} ne dopuštajući lošim dečkima da zabiju gol", + "name": "Prazna mreža (${LEVEL})" + }, + "Uber Football Victory": { + "description": "Pobijedi", + "descriptionComplete": "Pobijedio si", + "descriptionFull": "Pobijedi na ${LEVEL}", + "descriptionFullComplete": "Pobijedio si na ${LEVEL}", + "name": "Pobjeda na ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Pobijedi sve nalete", + "descriptionComplete": "Pobijedio si sve nalete", + "descriptionFull": "Pobijedi sve nalete na ${LEVEL}", + "descriptionFullComplete": "Pobijedio si sve nalete na ${LEVEL}", + "name": "Pobijedi na ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Pobijedi sve nalete", + "descriptionComplete": "Pobijedio si sve nalete", + "descriptionFull": "Pobijedi sve nalete na ${LEVEL}", + "descriptionFullComplete": "Pobijedio si sve nalete na ${LEVEL}", + "name": "Pobjeda na ${LEVEL}" + } + }, + "achievementsRemainingText": "Preostala Postignuća:", + "achievementsText": "Postignuća", + "achievementsUnavailableForOldSeasonsText": "Žao nam je,postignuća pojedinosti nisu dostupni za stare sezone.", + "addGameWindow": { + "getMoreGamesText": "Još igara...", + "titleText": "Dodaj igru" + }, + "allowText": "Dopusti", + "alreadySignedInText": "Tvoj akaunt je prijavljen sa drugog uredjaja;\nmolimo promenite akaunt ili zatvori igru na\ntvom drugom uredjaju i probaj opet.", + "apiVersionErrorText": "Nemoguće je učitati modul ${NAME}; napravljen je za ${VERSION_USED} verziju aplikacije; potrebna je verzija ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Automatski\" omogućuje ovo samo kad su slušalice priključene)", + "headRelativeVRAudioText": "VR relativni zvuk", + "musicVolumeText": "Glasnoća glazbe", + "soundVolumeText": "Glasnoća zvukova", + "soundtrackButtonText": "Glazbena lista", + "soundtrackDescriptionText": "(postavi svoju vlasitu glazbu da svira tijekom igara)", + "titleText": "Zvuk" + }, + "autoText": "Automatski", + "backText": "Nazad", + "banThisPlayerText": "Zabrani ovog igrača", + "bestOfFinalText": "Najbolji od ${COUNT} - konačni rezultat:", + "bestOfSeriesText": "Najbolji od ${COUNT} - rezultati:", + "bestOfUseFirstToInstead": 1, + "bestRankText": "Najbolji je bio: #${RANK}", + "bestRatingText": "Tvoja najbolja ocjena je ${RATING}", + "bombBoldText": "BOMBA", + "bombText": "Bomba", + "boostText": "Ubrzaj", + "bsRemoteConfigureInAppText": "${REMOTE_APLIKACIJA_IME} je podešeno u samoj aplikaciji.", + "buttonText": "tipka", + "canWeDebugText": "Bi li htio da BombSquad automatski prijavi\ngreške, rušenja, i osnovne informacije o korištenju programeru? \n\nOvi podaci ne sadrže osobne informacije i pomažu\nda igra teče glatko i bez grešaka.", + "cancelText": "Odustani", + "cantConfigureDeviceText": "Žao mi je, ${DEVICE} nije moguće podesiti.", + "challengeEndedText": "Ovaj izazov je završio.", + "chatMuteText": "Stišaj Chat", + "chatMutedText": "Chat Stišan", + "chatUnMuteText": "Upali Chat", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Moraš završiti ovu\nrazinu da nastaviš!", + "completionBonusText": "Završni bonus", + "configControllersWindow": { + "configureControllersText": "Podesi kontrolere", + "configureKeyboard2Text": "Podesi P2 tipkovnicu", + "configureKeyboardText": "Podesi tipkovnica", + "configureMobileText": "Mobilni uređaji kao kontroleri", + "configureTouchText": "Podesi Touchscreen", + "ps3Text": "PS3 kontroleri", + "titleText": "Kontroleri", + "wiimotesText": "Wiimote-ovi", + "xbox360Text": "Xbox 360 kontroleri" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Pažnja: podrška za kontrole ovisi o uređaju i verziji Androida.", + "pressAnyButtonText": "Na kontroleru pritisni bilo koju tipku\nkoju želiš podesiti...", + "titleText": "Podesi kontrolere" + }, + "configGamepadWindow": { + "advancedText": "Napredno", + "advancedTitleText": "Napredno podešavanje kontrolera", + "analogStickDeadZoneDescriptionText": "(povećaj ovo ako tvoj lik otklizi kad otpustiš palicu)", + "analogStickDeadZoneText": "Mrtva zona analogne palice", + "appliesToAllText": "(primjenjuje se na sve kontrolere ove vrste)", + "autoRecalibrateDescriptionText": "(uključi ovo ako se tvoj lik ne kreće punom brzinom)", + "autoRecalibrateText": "Automatski rekalibriraj anlognu palicu", + "axisText": "os", + "clearText": "ukloni", + "dpadText": "dpad", + "extraStartButtonText": "Dodatna Start tipka", + "ifNothingHappensTryAnalogText": "Ako se ništa ne događa, pokušaj tu kontrolu dodijeliti analognoj palici.", + "ifNothingHappensTryDpadText": "Ako se ništa ne događa, pokušaj tu kontrolu dodijeliti d-padu.", + "ignoreCompletelyDescriptionText": "(Spreči ovaj kontroler od uticanja igri i menijima)", + "ignoreCompletelyText": "Ignoriši kompletno", + "ignoredButton1Text": "Zanemarena tipka 1", + "ignoredButton2Text": "Zanemarena tipka 2", + "ignoredButton3Text": "Zanemarena tipka 3", + "ignoredButton4Text": "Ignorisan tipka 4", + "ignoredButtonDescriptionText": "(koristi ovo da spriječiš 'home' i 'sync' tipke da utječu na sučelje)", + "pressAnyAnalogTriggerText": "Pritisni bilo koji analogni okidač...", + "pressAnyButtonOrDpadText": "Pritisni bilo koju tipku ili dpad...", + "pressAnyButtonText": "Pririsni bilo koju tipku...", + "pressLeftRightText": "Pritisni lijevo ili desno...", + "pressUpDownText": "Pritisni gore ili dolje...", + "runButton1Text": "Tipka za trčanje 1", + "runButton2Text": "Tipka za trčanje 2", + "runTrigger1Text": "Okidač za trčanje 1", + "runTrigger2Text": "Okidač za trčanje 2", + "runTriggerDescriptionText": "(analogni okidači omogućuju ti da trčiš pri promjenjivim brzinama)", + "secondHalfText": "Koristi ovo da podesiš drugu polovicu\n2 kontrolera u 1 uređaja koji se\npokazuje kao jedan kontroler.", + "secondaryEnableText": "Omogući", + "secondaryText": "Sekundarni kontroler", + "startButtonActivatesDefaultDescriptionText": "(isključi ovo ako je tvoja start tipka sličnija 'menu' tipki)", + "startButtonActivatesDefaultText": "Start tipka aktvira zadani widget", + "titleText": "Podešavanja kontrolera", + "twoInOneSetupText": "Podešavanje 2 u 1 kontrolera", + "uiOnlyDescriptionText": "(Spreči ovaj kontroler od ulaska u igru)", + "uiOnlyText": "Limit korišćenja menija", + "unassignedButtonsRunText": "Sve nedodijeljene tipke su za trk", + "unsetText": "", + "vrReorientButtonText": "Orijentacija za VS" + }, + "configKeyboardWindow": { + "configuringText": "Podešavanje uređaja ${DEVICE}", + "keyboard2NoteText": "Pažnja: većina tipkovnica može registrirati samo nekoliko pritisaka tipki\nodjednom, pa bi igranje s drugim igračem na tipkovnici\nmoglo raditi bolje ako postoji odvojena tipkovnica da je koristi. \nNo, ipak ćeš morati dodijeliti jedinstvene kontrole za\noba igrača u tom slučaju." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Veličina akcijskih kontrola", + "actionsText": "Akcijske kontrole", + "buttonsText": "tipke", + "dragControlsText": "", + "joystickText": "igraća palica", + "movementControlScaleText": "Veličina kontrola kretanja", + "movementText": "Kretanje", + "resetText": "Poništi promjene", + "swipeControlsHiddenText": "Sakrij ikone povlačenja", + "swipeInfoText": "Treba malo vremena da se naviknete na kontrole 'Povlačenje' \nali je lakše igrati bez da gledaš u kontrole.", + "swipeText": "povlačenje", + "titleText": "Podesi ekran na dodir" + }, + "configureItNowText": "Podesi sada?", + "configureText": "Podesi", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "Za najbolje rezultate trebat ćeš wifi mrežu bez laga(zastoja). Možeš \nsmanjiti wifi lag tako da isključiš ostale bežične uređaje, igraš\nBlizu tvog wifi modema, i priključujući\ndomaćina igre direktno na mrežu putem entherneta.", + "explanationText": "Da možeš koristiti pametni telefon ili tablet kao bežični kontroler, \ninstaliraj \"${REMOTE_APP_NAME}\" aplikaciju na njega. Bilo koji broj uređaja\nse može povezati sa ${APP_NAME} igrom preko Wi-Fi-a", + "forAndroidText": "za Android:", + "forIOSText": "za iOS:", + "getItForText": "Get ${REMOTE_APP_NAME} za IOS na Apple APP storu\nili za Android na Google Plej storu ili Amazon App storu", + "googlePlayText": "Google Play", + "titleText": "Korištenje mobilnih uređaja kao kontrolera:" + }, + "continuePurchaseText": "Nastavi za ${PRICE}?", + "continueText": "Nastavi", + "controlsText": "Kontrole", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Ovo se ne primjenjuje na ukupni poredak.", + "activenessInfoText": "Ovaj se multiplikator povećava na dane kada\nigraš, a smanjuje na dane kada ne igraš.", + "activityText": "Aktivnost", + "campaignText": "Kampanja", + "challengesInfoText": "Zaradi nagrade završavanjem mini-igara.\n\nNagrade i težina levela se uvećava\nsvakog puta kada je izazov završen i \nsmanjuje se kada istekne ili je zabranjen.", + "challengesText": "Izazovi", + "currentBestText": "Trenutno Najbolji", + "customText": "Razno", + "entryFeeText": "Kotizacija", + "forfeitConfirmText": "Zaboravio si ovaj izazov?", + "forfeitNotAllowedYetText": "Ovaj izazov nemože još biti zaboravljen.", + "forfeitText": "Zaboraviti", + "multipliersText": "Multiplikatori", + "nextChallengeText": "Sledeći izazov", + "nextPlayText": "Sledeća igra", + "ofTotalTimeText": "od ${TOTAL}", + "playNowText": "Igraj sad", + "pointsText": "Bodovi", + "powerRankingFinishedSeasonUnrankedText": "(Završena sezona neobjavljena)", + "powerRankingNotInTopText": "(nisi u najboljih ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} bodova", + "powerRankingPointsMultText": "(x ${NUMBER} bodova)", + "powerRankingPointsText": "${NUMBER} bodova", + "powerRankingPointsToRankedText": "(${CURRENT} od ${REMAINING} bodova)", + "powerRankingText": "Rang igrača", + "prizesText": "Nagrade", + "proMultInfoText": "Igrači s ${PRO} nadogradnjom\novdje dobivaju još ${PERCENT}% bodova.", + "seeMoreText": "Više...", + "skipWaitText": "Prekočite Čekanje", + "timeRemainingText": "Preostalo Vrijeme", + "toRankedText": "Do pozicije na ljestvici", + "totalText": "ukupno", + "tournamentInfoText": "Završite za visoke rezultate sa\nostalim igračima u tvojoj ligi\n\nNagrade su dodeljene igračima sa \nnajvećim rezultatom kada se turnir završi.", + "welcome1Text": "Dobrodošao u ${LEAGUE}. Možeš napredovati u\nligi osvajajući dobre ocjene(zvjezdice), dovršavajući\npostignuća, i osvajajući trofeje u turnirima.", + "welcome2Text": "Možeš zarađivati kupone raznim aktivnostima.\nKupone možeš koristiti za otključavanje novih likova, mapa, i\nigara, ulaženje u turnire, i puno više.", + "yourPowerRankingText": "Tvoja pozicija" + }, + "copyOfText": "Kopija ${NAME}", + "createEditPlayerText": "", + "createText": "Stvori", + "creditsWindow": { + "additionalAudioArtIdeasText": "Dodatni Zvukovi, Rane Ilustracije, i Ideje by ${NAME}", + "additionalMusicFromText": "Dodatna glazba od ${NAME}", + "allMyFamilyText": "Svi moji prijatelji i obitelj koji su mi pomogli testirati", + "codingGraphicsAudioText": "Kodiranje, grafika i zvukovi by ${NAME}", + "languageTranslationsText": "Prijevodi:", + "legalText": "Legalno:", + "publicDomainMusicViaText": "Glazba u javnom vlasništvu preko ${NAME}", + "softwareBasedOnText": "Ovaj softver je dijelom baziran radu ${NAME} grupe", + "songCreditText": "${TITLE} izvodi ${PERFORMER}\nSkladao ${COMPOSER}, uredio ${ARRANGER} , objavio ${PUBLISHER},\nusluga ${SOURCE}", + "soundAndMusicText": "Zvuk i glazba:", + "soundsText": "Zvukovi (${SOURCE}):", + "specialThanksText": "Posebne Zahvale:", + "thanksEspeciallyToText": "Posebno hvala ${NAME}", + "titleText": "${APP_NAME} Krediti", + "whoeverInventedCoffeeText": "Tkogod je izumio kavu" + }, + "currentStandingText": "Tvoj trenutačni plasman je #${RANK}", + "customizeText": "Prilagodi...", + "deathsTallyText": "Umro si ${COUNT} puta", + "deathsText": "Smrti", + "debugText": "ukloni greške", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Preporučeno je da Postavke->Grafika->Teksture postavite na 'Visoko' dok testirate ovo.", + "runCPUBenchmarkText": "Pokreni test učinka CPU-a (procesora)", + "runGPUBenchmarkText": "Pokreni test učinka GPU-a (grafike)", + "runMediaReloadBenchmarkText": "Pokreni Media-Reload test učinka", + "runStressTestText": "Pokreni test opterećenja", + "stressTestPlayerCountText": "Broj igrača", + "stressTestPlaylistDescriptionText": "Lista igara testa opterećenja", + "stressTestPlaylistNameText": "Ime liste igara", + "stressTestPlaylistTypeText": "Vrsta liste igara", + "stressTestRoundDurationText": "Trajanje runde", + "stressTestTitleText": "Test opterećenja", + "titleText": "Testovi učinka i opterećenja", + "totalReloadTimeText": "Ukupno vrijeme ponovnog učitavanja: ${TIME} (pogledaj log za detalje)" + }, + "defaultGameListNameText": "Zadana ${PLAYMODE} lista igara", + "defaultNewGameListNameText": "Moja ${PLAYMODE} lista igara", + "deleteText": "Izbriši", + "demoText": "Demo", + "denyText": "Odbij", + "desktopResText": "Desktop rezolucija", + "difficultyEasyText": "Lagano", + "difficultyHardOnlyText": "Samo u teškom modu", + "difficultyHardText": "Teško", + "difficultyHardUnlockOnlyText": "Ovu razinu možeš otključati samo u teškom modu. \nMisliš li da možeš to!?!?!", + "directBrowserToURLText": "Molim posjetite sljedeći URL s web-preglednikom:", + "disableRemoteAppConnectionsText": "Isključi konekciju Remote-aplikacije", + "disableXInputDescriptionText": "Dozvoljava više od 4 kontrolera ali možda neće raditi dobro.", + "disableXInputText": "Isključi XInput", + "doneText": "Gotovo", + "drawText": "Neriješeno", + "duplicateText": "Dupliciraj", + "editGameListWindow": { + "addGameText": "Dodaj\nIgru", + "cantOverwriteDefaultText": "Nemoguće je mijenjati zadanu listu igara!", + "cantSaveAlreadyExistsText": "Lista igara s tim imenom već postoji!", + "cantSaveEmptyListText": "Nemoguće je spremiti praznu listu igara!", + "editGameText": "Uredi\nIgru", + "listNameText": "Ime liste igara", + "nameText": "Ime", + "removeGameText": "Ukloni\nIgru", + "saveText": "Spremi Listu", + "titleText": "Uređivaj liste igara" + }, + "editProfileWindow": { + "accountProfileInfoText": "Ovaj specijalni \nprofil ima ime i \nikonicu zasnovanu \nna tvom \nakauntu.\n\n${ICONS}", + "accountProfileText": "(profil akaunta)", + "availableText": "Ime \"${NAME}\" je dostupno.", + "changesNotAffectText": "Promjene neće utjecati na likove koji su već u igri. ", + "characterText": "lik", + "checkingAvailabilityText": "Proveravanje dostupnosti za \"${NAME}\"...", + "colorText": "boja", + "getMoreCharactersText": "Još likova...", + "getMoreIconsText": "Uzmi više ikonica...", + "globalProfileInfoText": "Profil svakog igraca na svijetu ima svoje ime. \nTakođer postoje i osobne ikone.", + "globalProfileText": "(globalni profil)", + "highlightText": "pozadina", + "iconText": "Ikonica", + "localProfileInfoText": "Lokalni profili igrača nemaju ikone i njihova imena možda\nnisu jedinstvena. Nadogradi na\nglobalni profil\nda rezerviraš jedinstveno ime i prilagođenu ikonu.", + "localProfileText": "(Lokalni profil)", + "nameDescriptionText": "Ime Igrača", + "nameText": "Ime", + "randomText": "nasumično", + "titleEditText": "Uredi Profil", + "titleNewText": "Novi Profil", + "unavailableText": "\"${NAME}\" nije dosgupan; probaj drugo ime", + "upgradeProfileInfoText": "Ovo ce zauzeti tvoje korisnicko ime\nI dozvoliti ti da koristis osobnu ikonu", + "upgradeToGlobalProfileText": "Nadogradi na Globalni Profil" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Ne možeš izbrisati zadanu glazbenu listu.", + "cantEditDefaultText": "Ne možeš uređivati zadanu glazbenu listu. Dupliciraj je ili napravi novu.", + "cantOverwriteDefaultText": "Nemoguće je presnimiti zadanu glazbenu listu", + "cantSaveAlreadyExistsText": "Glazbena lista s tim imenom već postoji!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Zadana glazbena lista", + "deleteConfirmText": "Izbriši glazbenu listu: \n\n'${NAME}'?", + "deleteText": "Izbriši\nglazbenu listu", + "duplicateText": "Dupliciraj\nglazbenu listu", + "editSoundtrackText": "Uređuj glazbene liste", + "editText": "Uredi\nglazbenu listu", + "fetchingITunesText": "dohvaćam glazbene liste...", + "musicVolumeZeroWarning": "Upozorenje: glasnoća glazbe je postavljena na 0", + "nameText": "Ime", + "newSoundtrackNameText": "Moja glazbena lista ${COUNT}", + "newSoundtrackText": "Nova glazbena lista:", + "newText": "Nova\nglazbena lista", + "selectAPlaylistText": "Izaberi listu igara", + "selectASourceText": "Izvor glazbe", + "testText": "test", + "titleText": "Glazbene liste", + "useDefaultGameMusicText": "Zadana Glazba", + "useITunesPlaylistText": "Glazbena lista", + "useMusicFileText": "Glazbena Datoteka (mp3, itd.)", + "useMusicFolderText": "Mapa s Glazbenim Datotekama" + }, + "editText": "Izmjeni", + "endText": "Kraj", + "enjoyText": "Uživaj", + "epicDescriptionFilterText": "${DESCRIPTION} U epskom usporenom filmu.", + "epicNameFilterText": "${NAME} (epski mod)", + "errorAccessDeniedText": "pristup zabranjen", + "errorOutOfDiskSpaceText": "nema prostora na disku", + "errorText": "Greška", + "errorUnknownText": "nepoznata greška", + "exitGameText": "Izlaz iz ${APP_NAME}?", + "exportSuccessText": "'${NAME}' je izvezen.", + "externalStorageText": "Vanjska Pohrana", + "failText": "Neuspjeh", + "fatalErrorText": "Uh oh; nešto nedostaje ili je pokvareno. \nMolim pokušaj ponovno instalirati aplikaciju ili\nkontaktiraj ${EMAIL} za pomoć.", + "fileSelectorWindow": { + "titleFileFolderText": "Izaberi Datoteku ili Mapu", + "titleFileText": "Izaberi Datoteku", + "titleFolderText": "Izaberi Mapu", + "useThisFolderButtonText": "Koristi Ovu Mapu" + }, + "filterText": "Pretraži", + "finalScoreText": "Konačni Rezultat", + "finalScoresText": "Konačni Rezultati", + "finalTimeText": "Konačno Vrijeme", + "finishingInstallText": "Dovršavam instalaciju; samo trenutak...", + "fireTVRemoteWarningText": "* Za bolje iskustvo, koristi\nGame Controller ili instaliraj\n'${REMOTE_APP_NAME}' aplikaciju na tvoje\ntelefone i tablete.", + "firstToFinalText": "Prvi do ${COUNT} - konačni rezultat", + "firstToSeriesText": "Prvi do ${COUNT} - rezultati", + "fiveKillText": "PETEROSTRUKO UBOJSTVO!!!", + "flawlessWaveText": "Nalet bez greške!", + "fourKillText": "ČETVEROSTRUKO UBOJSTVO!!!", + "friendScoresUnavailableText": "Rezultati prijatelja nedostupni.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Najbolji u igri ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "Ne možeš izbrisati zadanu listu igara.", + "cantEditDefaultText": "Ne možeš uređivati zadanu listu igara! Dupliciraj je ili napravi novu.", + "cantShareDefaultText": "Ne možeš dijeliti zadan popis pjesama.", + "deleteConfirmText": "Izbriši \"${LIST}\"?", + "deleteText": "Izbriši \nListu", + "duplicateText": "Dupliciraj\nListu", + "editText": "Uredi \nListu", + "newText": "Nova\nLista", + "showTutorialText": "Pokaži upute", + "shuffleGameOrderText": "Promiješaj red igara", + "titleText": "Prilagodi ${TYPE} liste igara" + }, + "gameSettingsWindow": { + "addGameText": "Dodaj Igru" + }, + "gamesToText": "${WINCOUNT} igre naprema ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Zapamti: bilo koji uređaj u partiji može imati više\nod jednog igrača ako imaš dovoljno kontrolera.", + "aboutDescriptionText": "Koristi ove kartice da sastaviš partiju. \n\nPartije ti omogućuju da igraš igre i turnire\ns tvojim prijateljima na različitim uređajima. \n\nKoristi ${PARTY} tipku u gornjem desnom kutu za\nrazgovor i komunikaciju s ostalima.\n(na kontroleru, pritsni ${BUTTON} dok si u izborniku)", + "aboutText": "O", + "addressFetchErrorText": "", + "appInviteMessageText": "${NAME} poslao ti je ${COUNT} ulaznica za ${APP_NAME}", + "appInviteSendACodeText": "Pošalji im kod", + "appInviteTitleText": "${APP_NAME} Pozovi u igru", + "bluetoothAndroidSupportText": "(radi s bilo kojim Android uređajem koji podržava Bluetooth)", + "bluetoothDescriptionText": "Ugosti/uključi se u partiju preko Bluetooth-a:", + "bluetoothHostText": "Ugosti preko Bluetooth-a", + "bluetoothJoinText": "Uključi se preko Bluetooth-a", + "bluetoothText": "Bluetooth", + "checkingText": "provjeravam...", + "dedicatedServerInfoText": "Za najbolje rezultate, postavi posvećen server. Vidi bombsquadgame.com/server da saznas kako.", + "disconnectClientsText": "Ovo će isključiti ${COUNT} igrača\niz partije. Jesi li siguran?", + "earnTicketsForRecommendingAmountText": "Prijatelji će dobiti ${COUNT} ulaznica ako probaju igru\n(ti ćeš dobiti ${YOU_COUNT} ulaznica za svakog koji proba)", + "earnTicketsForRecommendingText": "Podeli igri za besplatne karte\n...", + "emailItText": "Emajlaj", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} ulaznica od ${NAME}", + "friendPromoCodeAwardText": "Primit ćeš ${COUNT} ulaznica svaki put kada je kod iskorišten.", + "friendPromoCodeExpireText": "Ovaj kod će isteći za ${EXPIRE_HOURS} sati i radi samo za nove igrače.", + "friendPromoCodeInstructionsText": "Da ga iskoristiš, otvori ${APP_NAME} idi u \"Postavke->Napredno->Unesi kod\".\nVidi bombsquadgame.com za sve platforme na kojima možeš igrati.", + "friendPromoCodeRedeemLongText": "Može biti otkupljeno za ${COUNT} besplatnih ulaznica do ${MAX_USES} ljudi.", + "friendPromoCodeRedeemShortText": "Može biti otkupljeno za ${COUNT} ulaznica u igri.", + "friendPromoCodeWhereToEnterText": "(u \"Postavke->Napredno>Unesi Kod\")", + "getFriendInviteCodeText": "Dohvati kod za poziv prijatelja", + "googlePlayDescriptionText": "Pozovi Google Play igrače u tvoju partiju:", + "googlePlayInviteText": "Pozovi", + "googlePlayReInviteText": "${COUNT} Google Play igrača iz tvoje partije\nće biti isključeno iz nje ako započneš novi poziv. \nUključi ih u njega ako želiš da se vrate.", + "googlePlaySeeInvitesText": "Pogledaj pozive", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play verzija)", + "hostPublicPartyDescriptionText": "Izradi javnu partiju:", + "inDevelopmentWarningText": "Pažnja: \n\nMrežna igra je nova značajka koja je još u razvoju. \nZasad, preporučuje se da su svi\nigrači spojeni na istu Wi-Fi mrežu.", + "internetText": "Internet", + "inviteAFriendText": "Prijatelji nemaju igru? Pozovi ih da je\nprobaju i oni će dobiti ${COUNT} besplatnih ulaznica.", + "inviteFriendsText": "Pozovi Prijatelje", + "joinPublicPartyDescriptionText": "Pridruži se javnoj partiji:", + "localNetworkDescriptionText": "Uključi se u partiju na tvojoj mreži:", + "localNetworkText": "Lokalna mreža", + "makePartyPrivateText": "Postavi moju partiju privatnom", + "makePartyPublicText": "Postavi moju partiju javnom", + "manualAddressText": "Adresa", + "manualConnectText": "Spoji se", + "manualDescriptionText": "Uljuči se u partiju po adresi:", + "manualJoinableFromInternetText": "Mogu li se drugi priključiti tebi s Interneta?:", + "manualJoinableNoWithAsteriskText": "NE*", + "manualJoinableYesText": "DA", + "manualRouterForwardingText": "*da popraviš ovo, pokušaj podesiti tvoj modem da prosljeđuje UDP port ${PORT} za tvoju lokalnu adresu", + "manualText": "Ručno", + "manualYourAddressFromInternetText": "Tvoja adresa s Interneta:", + "manualYourLocalAddressText": "Tvoja lokalna adresa:", + "noConnectionText": "", + "otherVersionsText": "(druge verzije)", + "partyInviteAcceptText": "Prihvati", + "partyInviteDeclineText": "Odbij", + "partyInviteGooglePlayExtraText": "(pogledaj 'Google Play' karticu u prozoru 'Sakupi')", + "partyInviteIgnoreText": "Odbaci", + "partyInviteText": "${NAME} te pozvao\nda se priključiš njegovoj partiji!", + "partyNameText": "Naziv igre", + "partySizeText": "Veličina žurke", + "partyStatusCheckingText": "provjeravam status...", + "partyStatusJoinableText": "tvoja igra je spremna za povezivanje s interneta", + "partyStatusNoConnectionText": "ne mogu se spojiti na server", + "partyStatusNotJoinableText": "tvoja igra nije dostupna preko interneta", + "partyStatusNotPublicText": "tvoja igra nije javna", + "pingText": "ping", + "portText": "Port", + "requestingAPromoCodeText": "Dohvaćam kod...", + "sendDirectInvitesText": "Pošalji izravni poziv", + "sendThisToAFriendText": "Posalji ovaj kod prijatelju:", + "shareThisCodeWithFriendsText": "Podijeli ovaj kod s prijateljima:", + "showMyAddressText": "Prikaži moju IP adresu", + "titleText": "Okupljanje", + "wifiDirectDescriptionBottomText": "Ako svi uređaji imaju 'Wi-Fi Direct' opciju, trebali bi ga moći iskoristiti da pronađu jedan\ndrugoga i povežu se. Kad su svi uređaji povezani, možeš kreiratu igru\nkoristeći karticu 'Lokalna mreža', isto kao i kod obične Wi-Fi mreže. \n\nZa najbolje rezultate, domaćin Wi-Fi Directa također bi trebao biti domaćin ${APP_NAME} igre.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct možete koristiti da povežete Android uređaje direktno bez \npotrebe za Wi-Fi mrežom. Ovo najbolje radi na Android verziji 4.2 ili novijoj. \n\nZa korištenje, otvori Wi-Fi postavke i potraži 'Wi-Fi Direct' u izborniku.", + "wifiDirectOpenWiFiSettingsText": "Otvori Wi-Fi postavke", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(radi na svim platformama)", + "worksWithGooglePlayDevicesText": "(radi na uređajima s Google Play (Android) verzijom igre)", + "youHaveBeenSentAPromoCodeText": "Netko ti je poslao ${APP_NAME} promo kod:" + }, + "getTicketsWindow": { + "freeText": "BESPLATNO!", + "freeTicketsText": "Besplatni kuponi", + "inProgressText": "Transakcija je u tijeku; pokušaj ponovno za trenutak.", + "purchasesRestoredText": "Kupovine poništene.", + "receivedTicketsText": "Dobio si ${COUNT} kupona!", + "restorePurchasesText": "Poništi kupovine", + "ticketDoublerText": "Dupli kuponi", + "ticketPack1Text": "Mali paket kupona", + "ticketPack2Text": "Srednji paket kupona", + "ticketPack3Text": "Veliki paket kupona", + "ticketPack4Text": "Jumbo paket kupona", + "ticketPack5Text": "Mamutski paket kupona", + "ticketPack6Text": "Ultimativni paket kupona", + "ticketsFromASponsorText": "Dobij ${COUNT} kupona\nod sponzora", + "ticketsText": "${COUNT} kupona", + "titleText": "Zaradi kupone", + "unavailableLinkAccountText": "Nažalost, kupovina nije dostupna na ovoj platformi.\nKao zaobilazno rješenje, možete povezati račun s računom na \ndrugoj platformi i tamo nastaviti kupovinu.", + "unavailableTemporarilyText": "Ovo je trenutačno nedostupno; molim pokušajte kasnije.", + "unavailableText": "Žao mi je, ovo nije dostupno.", + "versionTooOldText": "Žao mi je, ova verzija igre je prestara; molim ažurirajte je na noviju verziju.", + "youHaveShortText": "imaš ${COUNT}", + "youHaveText": "imaš ${COUNT} kupona" + }, + "googleMultiplayerDiscontinuedText": "Oprostite, Google Play Games servis za partije u igrama više ne postoji.\nTrenutno radim na zamjeni što je brže moguće.\nTo tada, priključujte se u partije na druge načine.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Uvijek", + "fullScreenCmdText": "Cijeli ekran (Cmd-F)", + "fullScreenCtrlText": "Cijeli ekran (Ctrl-F)", + "gammaText": "Svjetlina(Gamma)", + "highText": "Visoko", + "higherText": "Više", + "lowText": "Nisko", + "mediumText": "Srednje", + "neverText": "Nikad", + "resolutionText": "Rezolucija", + "showFPSText": "Pokaži FPS", + "texturesText": "Teksture", + "titleText": "Grafika", + "tvBorderText": "TV okvir", + "verticalSyncText": "Vertical Sync (Vertikalna sinkronizacija)", + "visualsText": "Vizualno" + }, + "helpWindow": { + "bombInfoText": "- Bomba -\nJača od udaraca, ali može rezultirati \nsamoubojstvom. Za najbolje \nrezultate, baci je prema neprijatelju \nprije nego što fitilj potpuno izgori.", + "canHelpText": "${APP_NAME} može pomoći.", + "controllersInfoText": "Možeš igrati ${APP_NAME} s prijeteljima preko mreže, ili svi\nmožete igrati na istom uređaju ako imate dovoljno kontrolera. \n${APP_NAME} podržava razne kontrolere; čak možete koristiti telefone\nkao kontrolere pomoću besplatne '${REMOTE_APP_NAME}' aplikacije. \nZa više informacija pogledaj pod Postavke->Kontroleri.", + "controllersText": "Kontroleri", + "controlsSubtitleText": "Tvoj prijateljski ${APP_NAME} lik ima par osnovnih kretnji:", + "controlsText": "Kontrole", + "devicesInfoText": "VR verziju ${APP_NAME} možeš igrati preko mreže s\nobičnom verzijom, pa izvadi svoje dodatne telefone, tablete, \ni računala i igraj. Čak može biti korisno da\npovežeš običnu verziju igre s VR verzijom samo da\nomogućiš ostalima da gledaju akciju.", + "devicesText": "Uređaji", + "friendsGoodText": "Dobro ih je imati. ${APP_NAME} je najzabavniji s\nprijateljima i podržava do 8 odjednom, što nas dovodi do:", + "friendsText": "Prijatelji", + "jumpInfoText": "- Skok - \nSkači da preskočiš manje rupe, \nbacaš stvari više, i\nizraziš sreću.", + "orPunchingSomethingText": "Ili udariti nešto, baciti ga s litice i raznijeti ga na putu dolje ljepljivom bombom.", + "pickUpInfoText": "- Podigni -\nUhvati zastave, neprijatelje, ili bilo što\ndrugo što nije pričvršćeno za tlo. \nPritsni ponovno da baciš.", + "powerupBombDescriptionText": "Omogućuje ti da baciš tri bombe\nzaredom umjesto samo jedne.", + "powerupBombNameText": "Trostruke bombe", + "powerupCurseDescriptionText": "Vjerojatno želiš izbjegavati ovo. \n.... ili možda ne?", + "powerupCurseNameText": "Kletva", + "powerupHealthDescriptionText": "Potpuno te izliječi. \nNikad ne bi pogodio.", + "powerupHealthNameText": "Prva pomoć", + "powerupIceBombsDescriptionText": "Slabije od normalnih bombi\nali smrzavaju tvoje neprijatelje, \nostavljajući ih lomljivima.", + "powerupIceBombsNameText": "Ledene bombe", + "powerupImpactBombsDescriptionText": "Lagano slabije od običnih \nbombi, ali eksplodiraju pri udarcu.", + "powerupImpactBombsNameText": "Kontakt bombe", + "powerupLandMinesDescriptionText": "Dolaze u pakovanjima po 3;\nKorisne za obranu baze ili\nza zaustavljanje brzih neprijatelja.", + "powerupLandMinesNameText": "Mine", + "powerupPunchDescriptionText": "Čini tvoje udarce čvršćima, \nbržima, boljima, jačima.", + "powerupPunchNameText": "Boksačke rukavice", + "powerupShieldDescriptionText": "Prima udarce tako \nda ti ne moraš.", + "powerupShieldNameText": "Energetski štit", + "powerupStickyBombsDescriptionText": "Lijepe se za sve što udare. \nNastaje veselje.", + "powerupStickyBombsNameText": "Ljepljive bombe", + "powerupsSubtitleText": "Naravno, nijedna igra nije dovršena bez bonusa:", + "powerupsText": "Bonusi", + "punchInfoText": "- Udarac -\nUdarci nanose više štete\nšto se brže kreću tvoje šake, \npa trči i vrti se kao luđak.", + "runInfoText": "- Trk -\nDrži BILO KOJU tipku da potrčiš. Okidačke ili ramene (PS L,R 1,2) tipke također rade. \nDok trčiš, puno si brži ali je teže okretati se, pa pazi na zavoje i litice.", + "someDaysText": "Nekad samo želiš udariti nešto. Ili dignuti nešto u zrak.", + "titleText": "${APP_NAME} Pomoć", + "toGetTheMostText": "Da izvučeš najbolje iz ove igre, trebat ćeš:", + "welcomeText": "Dobrodošli u ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} prolazi kroz labirint izbornika -", + "importPlaylistCodeInstructionsText": "Iskoristi ovaj kod za uvoz ove playliste negdje drugdje:", + "importPlaylistSuccessText": "${TYPE} playlista '${NAME}' je uvezena.", + "importText": "Ubaci", + "importingText": "Ubacivanje...", + "inGameClippedNameText": "U igri bi će \n\"${NAME}\"", + "installDiskSpaceErrorText": "GREŠKA: Nemoguće je dovršiti instalaciju. \nMožda nema više prostora na tvom uređaju. \nOslobodi malo prostora i pokušaj ponovno.", + "internal": { + "arrowsToExitListText": "pritisni ${LEFT} ili ${RIGHT} da iziđeš s liste", + "buttonText": "tipka", + "cantKickHostError": "Nemožeš izbaciti domaćina.", + "chatBlockedText": "${NAME} je čet blokiran za ${TIME} sekundi.", + "connectedToGameText": "Ušao '${NAME}'", + "connectedToPartyText": "Priključio si se ${NAME}(o)voj partiji!", + "connectingToPartyText": "Povezujem se...", + "connectionFailedHostAlreadyInPartyText": "Greška u povezivanju; domaćin je u drugoj partiji.", + "connectionFailedPartyFullText": "Veza nije uspostavljena; žurka je puna.", + "connectionFailedText": "Greška u povezivanju.", + "connectionFailedVersionMismatchText": "Greška u povezivanju; domaćin ima drugu verziju igre. \nProvjeri da obojica imate aktualnu verziju i pokušaj ponovno.", + "connectionRejectedText": "Pokušaj povezivanja odbačen.", + "controllerConnectedText": "${CONTROLLER} priključen.", + "controllerDetectedText": "1 kontroler otkriven.", + "controllerDisconnectedText": "${CONTROLLER} isključen.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} isključen. Pokušaj ga ponovno priključiti.", + "controllerForMenusOnlyText": "S ovim kontrolerom se ne može igrati; može se samo kretati kroz menije.", + "controllerReconnectedText": "${CONTROLLER} se ponovno priključio.", + "controllersConnectedText": "${COUNT} kontrolera priključeno.", + "controllersDetectedText": "${COUNT} kontrolera otkriveno.", + "controllersDisconnectedText": "${COUNT} kontrolera isključeno.", + "corruptFileText": "Otkrivene neispravne datoteke. Pokušaj ponovno instalirati, ili pošalji email na adresu: ${EMAIL}", + "errorPlayingMusicText": "Greška u reprodukciji glazbe: ${MUSIC}", + "errorResettingAchievementsText": "Nije moguće poništiti mrežna postignuća; pokušaj ponovno kasnije.", + "hasMenuControlText": "${NAME} ima kontrolu menija.", + "incompatibleNewerVersionHostText": "Domaćin ima noviju verziju igre.\nAžuriraj igru na zadnju verziju i pokušaj ponovo.", + "incompatibleVersionHostText": "Domaćin ima drugu verziju igre. \nProvjeri da obojica imate aktualnu verziju igre i pokušaj ponovno.", + "incompatibleVersionPlayerText": "${NAME} ima drugu verziju igre. \nProvjeri da obojica imate aktualnu verziju igre i pokušaj ponovno.", + "invalidAddressErrorText": "Greška: neispravna adresa.", + "invalidNameErrorText": "Greška: krivo ime.", + "invalidPortErrorText": "Greška: krivi port.", + "invitationSentText": "Pozivnica poslana.", + "invitationsSentText": "${COUNT} pozivnica poslano.", + "joinedPartyInstructionsText": "Netko se priključio u tvoj tim. \nPritisni 'Igraj' da pokreneš igru.", + "keyboardText": "Tipkovnica", + "kickIdlePlayersKickedText": "Izbacujem ${NAME} zbog neaktivnosti.", + "kickIdlePlayersWarning1Text": "${NAME} će biti izbačen za ${COUNT} sekudi ako još bude neaktivan.", + "kickIdlePlayersWarning2Text": "(ovo možeš isključiti pod Postavke -> Napredno)", + "leftGameText": "Izašao '${NAME}'.", + "leftPartyText": "Napustio si ${NAME}(o)vu partiju.", + "noMusicFilesInFolderText": "Mapa ne sadrži glazbene datoteke.", + "playerJoinedPartyText": "${NAME} se uključio u partiju!", + "playerLeftPartyText": "${NAME} je napustio partiju.", + "rejectingInviteAlreadyInPartyText": "Odbijam poziv (već si u partiji).", + "serverRestartingText": "Server se ponovno pokreće. Pridružite se ponovno za trenutak...", + "serverShuttingDownText": "Server se isključuje...", + "signInErrorText": "Greška u prijavi.", + "signInNoConnectionText": "Nije moguće prijaviti se. (nema internetske veze?)", + "telnetAccessDeniedText": "GREŠKA: korisniku nije dopušten telnet pristup.", + "timeOutText": "(završava za ${TIME} sekundi)", + "touchScreenJoinWarningText": "Uključio si se s ekranom na dodir. \nAko si pogriješio, odaberi 'Izbornik->Napusti igru' na njemu.", + "touchScreenText": "Ekran na dodir", + "unableToResolveHostText": "Greška: ne mogu pronaći domaćina.", + "unavailableNoConnectionText": "Ovo je trenutno nedostupno (nema internetske veze?)", + "vrOrientationResetCardboardText": "Koristi ovo da bi vratio na početak orijenaciju VR-a.\nDa bi mogao igrati igru, trebat ćeš vanjski kontroler.", + "vrOrientationResetText": "VR orijentacija poništena.", + "willTimeOutText": "(završit će ako je neaktivan)" + }, + "jumpBoldText": "SKOK", + "jumpText": "Skok", + "keepText": "Zadrži", + "keepTheseSettingsText": "Zadrži ove postavke?", + "keyboardChangeInstructionsText": "Stisni te space tipku dva puta da zamijenite tipkovnice.", + "keyboardNoOthersAvailableText": "Nijedna tipkovnica dostupna.", + "keyboardSwitchText": "Minjanje tipkovnice do \"${NAME}\".", + "kickOccurredText": "${NAME} je izbačen.", + "kickQuestionText": "Izbaci ${NAME}?", + "kickText": "Izbaci", + "kickVoteCantKickAdminsText": "Administratori ne mogu biti izbačeni.", + "kickVoteCantKickSelfText": "Nemozeš izbaciti sebe.", + "kickVoteFailedNotEnoughVotersText": "Nema dovoljno igrača za glasanje.", + "kickVoteFailedText": "Glasanje za izbačaj nije uspelo.", + "kickVoteStartedText": "Glasanje za izbačaj ${NAME} je počelo.", + "kickVoteText": "Glasajte da izbacite", + "kickVotingDisabledText": "Glasanje za izbacivanje je isključeno.", + "kickWithChatText": "Napiši ${YES} u četu za da i ${NO} za ne.", + "killsTallyText": "${COUNT} ubojstava", + "killsText": "Ubojstva", + "kioskWindow": { + "easyText": "Lagano", + "epicModeText": "Epski mod", + "fullMenuText": "Cijeli izbornik", + "hardText": "Teško", + "mediumText": "Srednje", + "singlePlayerExamplesText": "Primjeri razina za jednog igrača / tim", + "versusExamplesText": "Primjeri igara u kojima se igrači bore jedni protiv drugih" + }, + "languageSetText": "Jezik je sad \"${LANGUAGE}\".", + "lapNumberText": "Krug ${CURRENT}/${TOTAL}", + "lastGamesText": "(zadnjih ${COUNT} igara)", + "leaderboardsText": "Najbolji rezultati", + "league": { + "allTimeText": "Ukupan poredak", + "currentSeasonText": "Trenutna sezona (${NUMBER})", + "leagueFullText": "${NAME} liga", + "leagueRankText": "Pozicija u ligi", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga${SUFFIX}", + "seasonEndedDaysAgoText": "Sezona je završila prije ${NUMBER} dana.", + "seasonEndsDaysText": "Sezona završava za ${NUMBER} dana.", + "seasonEndsHoursText": "Sezona završava za ${NUMBER} sati.", + "seasonEndsMinutesText": "Sezona završava za ${NUMBER} minuta.", + "seasonText": "Sezona ${NUMBER}", + "tournamentLeagueText": "Moraš se plasirati u ${NAME} ligu da možeš sudjelovati u ovom turniru.", + "trophyCountsResetText": "Broj trofeja će se poništiti iduće sezone." + }, + "levelBestScoresText": "Najbolji skor na ${LEVEL}", + "levelBestTimesText": "Najbolje vrijeme na ${LEVEL}", + "levelFastestTimesText": "Najbrže vrijeme na ${LEVEL}", + "levelHighestScoresText": "Najbolji rezultati na ${LEVEL}", + "levelIsLockedText": "Razina ${LEVEL} je zaključana.", + "levelMustBeCompletedFirstText": "Prvo moraš završiti razinu ${LEVEL}.", + "levelText": "Nivo ${NUMBER}", + "levelUnlockedText": "Razina otključana!", + "livesBonusText": "Preostali život", + "loadingText": "učitavam", + "loadingTryAgainText": "Učitavanje; probajte kasnije...", + "macControllerSubsystemBothText": "Oba (nije preporučeno)", + "macControllerSubsystemClassicText": "klasika", + "macControllerSubsystemDescriptionText": "(probaj to promijeniti ako tvoji kontroleri ne rade)", + "macControllerSubsystemMFiNoteText": "Made-for-iOS/Mac kontroler detektiran;\nMožda bi ih htio aktivirati u Postavke -> Kontroleri", + "macControllerSubsystemMFiText": "Made-for-iOS/Mac", + "macControllerSubsystemTitleText": "Podržavanje kontrolera", + "mainMenu": { + "creditsText": "Zasluge", + "demoMenuText": "Demo izbornik", + "endGameText": "Kraj igre", + "exitGameText": "Izlaz iz igre", + "exitToMenuText": "Povratak u izbornik?", + "howToPlayText": "Kako igrati", + "justPlayerText": "(Samo ${NAME})", + "leaveGameText": "Napusti igru", + "leavePartyConfirmText": "Stvarno napuštaš partiju?", + "leavePartyText": "Napusti partiju", + "quitText": "Izlaz", + "resumeText": "Nastavi", + "settingsText": "Postavke" + }, + "makeItSoText": "Neka bude tako", + "mapSelectGetMoreMapsText": "Još mapa...", + "mapSelectText": "Odaberi...", + "mapSelectTitleText": "Mape za ${GAME}", + "mapText": "Mapa", + "maxConnectionsText": "Maksimalna konekcija", + "maxPartySizeText": "Maksimum veličine žurke", + "maxPlayersText": "Maksimum igrača", + "modeArcadeText": "Arkadni Mod", + "modeClassicText": "Klasični Mod", + "modeDemoText": "Demo Mod", + "mostValuablePlayerText": "Najbolji igrač", + "mostViolatedPlayerText": "Najveća žrtva", + "mostViolentPlayerText": "Najnasilniji igrač", + "moveText": "Pomicanje", + "multiKillText": "${COUNT}-STRUKO UBOJSTVO!!!", + "multiPlayerCountText": "${COUNT} igrača", + "mustInviteFriendsText": "Pažnja: moraš pozvati prijatelje u\n\"${GATHER}\" panelu ili priključiti\nkontrolere da možeš igrati s drugima.", + "nameBetrayedText": "${NAME} je izdao ${VICTIM}.", + "nameDiedText": "${NAME} je umro.", + "nameKilledText": "${NAME} je ubio ${VICTIM}.", + "nameNotEmptyText": "Ime ne može biti prazno!", + "nameScoresText": "${NAME} osvaja bod!", + "nameSuicideKidFriendlyText": "${NAME} je slučajno umro.", + "nameSuicideText": "${NAME} je počinio samoubojstvo.", + "nameText": "Ime", + "nativeText": "Standardno", + "newPersonalBestText": "Novi osobni najbolji rezultat!", + "newTestBuildAvailableText": "", + "newText": "Novo", + "newVersionAvailableText": "Novija verzija ${APP_NAME} je dostupna! (${VERSION})", + "nextAchievementsText": "Sledeće dostignuće:", + "nextLevelText": "Iduća razina", + "noAchievementsRemainingText": "- nijedno", + "noContinuesText": "(bez nastavaka)", + "noExternalStorageErrorText": "Vanjska pohrana nije pronađena", + "noGameCircleText": "Greška: nisi prijavljen u GameCircle", + "noProfilesErrorText": "Nemaš profila igrača, pa ti je samo ime '${NAME}' na raspolaganju. \nPođi pod Postavke->Profili igrača da sebi napraviš profil. ", + "noScoresYetText": "Još nema rezultata.", + "noThanksText": "Ne hvala", + "noTournamentsInTestBuildText": "UPOZORENJE: Turnirski bodovi od ove test gradnje će biti ignorirani.", + "noValidMapsErrorText": "Nijedna ispravna mapa nije pronađena za ovu igru.", + "notEnoughPlayersRemainingText": "Nije ostalo dovoljno igrača; iziđi i započni novu igru.", + "notEnoughPlayersText": "Trebaš najmanje ${COUNT} igrača da započneš ovu igru!", + "notNowText": "Ne sad", + "notSignedInErrorText": "Moraš se prijaviti da uradiš ovo.", + "notSignedInGooglePlayErrorText": "Moraš se ulogovati preko Google pleja da uradiš ovo.", + "notSignedInText": "nisi prijavljen", + "nothingIsSelectedErrorText": "Ništa nije odabrano!", + "numberText": "#${NUMBER}", + "offText": "Isključeno", + "okText": "U redu", + "onText": "Uključeno", + "onslaughtRespawnText": "${PLAYER} će se ponovno pojaviti u naletu ${WAVE}", + "orText": "${A} ili ${B}", + "otherText": "Drugo...", + "outOfText": "(#${RANK} od ${ALL})", + "ownFlagAtYourBaseWarning": "Tvoja zastava mora biti\nu tvojoj bazi da bi mogao osvojiti bod!", + "packageModsEnabledErrorText": "Mrežna igra nije dozvoljena dok su modovi iz lokalne pohrane uključeni(pogledaj pod Postavke->Napredno)", + "partyWindow": { + "chatMessageText": "Poruka", + "emptyText": "Nitko se nije priključio tvojoj partiji", + "hostText": "(domaćin)", + "sendText": "Pošalji", + "titleText": "Tvoja partija" + }, + "pausedByHostText": "(pauzirao domaćin)", + "perfectWaveText": "Savršen nalet!", + "pickUpText": "Podigni", + "playModes": { + "coopText": "Timski mod", + "freeForAllText": "Svatko protiv svakoga", + "multiTeamText": "Više ekipa", + "singlePlayerCoopText": "Solo/timski mod", + "teamsText": "Ekipe" + }, + "playText": "Igraj", + "playWindow": { + "oneToFourPlayersText": "1-4 igrača", + "titleText": "Igraj", + "twoToEightPlayersText": "2-8 igrača" + }, + "playerCountAbbreviatedText": "${COUNT} igrač(a)", + "playerDelayedJoinText": "${PLAYER} će se pojaviti na početku sledeće igre.", + "playerInfoText": "Igracova informacija", + "playerLeftText": "${PLAYER} je napustio igru.", + "playerLimitReachedText": "Maksimalan broj igrača je ${COUNT}; nitko se više ne može uključiti.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Ne možeš izbrisati tvoj korisnički račun.", + "deleteButtonText": "Izbriši\nProfil", + "deleteConfirmText": "Izbriši '${PROFILE}'?", + "editButtonText": "Uredi\nProfil", + "explanationText": "(stvori vaša imena i izglede za igrače na ovom uređaju)", + "newButtonText": "Novi\nProfil", + "titleText": "Profili igrača" + }, + "playerText": "Igrač", + "playlistNoValidGamesErrorText": "Ova lista igara ne sadrži nijednu valjanu otključanu igru.", + "playlistNotFoundText": "lista igara nije pronađena", + "playlistsText": "Liste igara", + "pleaseRateText": "Ako ti se sviđa ${APP_NAME}, molim te razmisli o tome\nda ga ocijeniš ili napišeš recenziju.\nOvako šalješ korisnu povratnu informaciju koja pomaže u daljnjem razvoju.\n\nHvala!\n-eric", + "pleaseWaitText": "Molimo sačekajte...", + "pluginsDetectedText": "Novi dodatak(ci) detektiraju. Osposobi/konfiguriraj ih u postavkama.", + "pluginsText": "Dodatci", + "practiceText": "Vježba", + "pressAnyButtonPlayAgainText": "Pritisni bilo koju tipku za ponovnu igru...", + "pressAnyButtonText": "Pritisni bilo koju tipku za nastavak...", + "pressAnyButtonToJoinText": "pritisni bilo koju tipku da se uključiš...", + "pressAnyKeyButtonPlayAgainText": "Pritisni bilo koju kontrolu/tipku za ponovnu igru...", + "pressAnyKeyButtonText": "Pritisni bilo koju kontrolu/tipku za nastavak...", + "pressAnyKeyText": "Pritisni bilo koju kontrolu...", + "pressJumpToFlyText": "**Pritišći skok konstantno za let**", + "pressPunchToJoinText": "pritisni UDARAC da se uključiš...", + "pressToOverrideCharacterText": "pritisni ${BUTTONS} da promijeniš svog lika", + "pressToSelectProfileText": "pritisni ${BUTTONS} da izabereš svog igrača", + "pressToSelectTeamText": "pritisni ${BUTTONS} da izabereš svoju ekipu", + "promoCodeWindow": { + "codeText": "Kod", + "codeTextDescription": "Promotivni kod", + "enterText": "Unesi" + }, + "promoSubmitErrorText": "Greška u korištenju koda; provjerite internetsku vezu", + "ps3ControllersWindow": { + "macInstructionsText": "Isključi tvoj PS3 na njegovoj stražnjoj strani, provjeri da je\nBluetooth uključen na tvom Macu, pa onda priključi kontroler\nna tvom Macu USB kabelom da ih upariš. Nakon toga, možeš\nkoristiti home tipku kontrolera da ga priključiš sa svojim Macom\nu žičnom (USB) ili bežičnom (Bluetooth) modu.\n\nNa nekim Macovima mogli bi te zatražiti loziku prilikom uparivanja.\nAko se ovo dogodi, pogledaj sljedeće upute ili guglaj za pomoć.\n\n\n\n\nPS3 kontroleri koje priključiš bežično trebali bi se pokazati u\nuređaja pod System Preferences->Bluetooth. Možda ćeš ih trebati\nukloniti s te liste kad ih želiš ponovno koristiti s tvojim PS3.\n\nIsto tako provjeri da ih isključiš s Bluetooth kad ih\nne koristiš ili će se njihove baterije nastaviti prazniti.\n\nBluetooth bi trebao podržavati do 7 povezanih uređaja,\nmada bi tvoja daljina mogla varirati.", + "ouyaInstructionsText": "Za korištenje PS3 kontrolera s OUYA-om, jednostavno ga priključi USB kabelom\njednom da ga upariš. Ovo bi moglo isključiti ostale kontrolere, pa\nbi trebao ponovno pokrenuti OUYA-u i isključiti USB kabel.\n\nNakon toga bi trebao moći koristiti HOME tipku na kontroleru da\nga priključiš bežično. Kad si završio s igrom, drži HOME tipku na kontroleru\n10 sekundi da isključiš kontroler; u protivnom bi mogao ostati uključen\ni baterija bi mu se mogla istrošiti.", + "pairingTutorialText": "video s uputama za uparivanje", + "titleText": "Korištenje PS3 kontrolera s ${APP_NAME}:" + }, + "punchBoldText": "UDARAC", + "punchText": "Udarac", + "purchaseForText": "Kupi za ${PRICE}", + "purchaseGameText": "Kupi igru", + "purchasingText": "Kupujem...", + "quitGameText": "izađi iz ${APP_NAME}?", + "quittingIn5SecondsText": "Izlazim za 5 sekundi...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Nasumično", + "rankText": "Pozicija", + "ratingText": "Ocjena", + "reachWave2Text": "Đođi do drugog naleta da se nađeš na ljestvici.", + "readyText": "spreman", + "recentText": "Nedavno", + "remoteAppInfoShortText": "${APP_NAME} najviše je zabavan kada se igra s obitelji i prijateljima.\nPovežite jedan ili više hardverskih kontrolera ili instalirajte\n${REMOTE_APP_NAME} aplikaciju na mobitele i tablete da ih koristite \nkao kontrolere.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Pozicija gumba", + "button_size": "Veličina gumba", + "cant_resolve_host": "Ne mogu pronaći hosta.", + "capturing": "Snimanje", + "connected": "Povezano.", + "description": "Koristi svoj mobitel ili tablet kao kontroler sa BombSquadom.\nDo 8 uređaja se može povezati od jednom za epsko ludilo za lokalno više igrača na TV-u ili tabletu.", + "disconnected": "Veza prekinuta od servera.", + "dpad_fixed": "zaljepljeno", + "dpad_floating": "lebdeće", + "dpad_position": "Pozicija D-Pada", + "dpad_size": "Veličina D-Pada", + "dpad_type": "Tip D-Pada", + "enter_an_address": "Ubaci Andresu", + "game_full": "Igra ima najviše mogućih igraća ili ne prihvaća veze.", + "game_shut_down": "Igra se ugasila.", + "hardware_buttons": "Gumbi hardvera", + "join_by_address": "Uključi se adresom...", + "lag": "Zaostaje: ${SECONDS} sekundi", + "reset": "Resetirano na standardno", + "run1": "Trči 1", + "run2": "Trči 2", + "searching": "Traženje BombSquad igri...", + "searching_caption": "Klikni na ime igre da joj se pridružiš.\nPobrinite se da ste na istoj wifi mreži kao i igra.", + "start": "Počni", + "version_mismatch": "Nejednake verzije.\nProvjerite da BombSquad i BombSquad Remote\nbudu ažurirani i pokušajte ponovo." + }, + "removeInGameAdsText": "Otključaj \"${PRO}\" u dućanu da ukloniš oglase.", + "renameText": "Preimenuj", + "replayEndText": "Kraj snimke", + "replayNameDefaultText": "Snimka zadnje igre", + "replayReadErrorText": "Greška u čitanju datoteke snimke.", + "replayRenameWarningText": "Preimenuj \"${REPLAY}\" poslije igre ako je želiš sačuvati; u protivnom će biti presnimljena.", + "replayVersionErrorText": "Žao mi je, ova snimka je napravljena u drugoj \nverziji igre i ne može se koristiti.", + "replayWatchText": "Gledaj snimku", + "replayWriteErrorText": "Greška u brisanju snimke.", + "replaysText": "Snimke", + "reportPlayerExplanationText": "Preko ovog emaila mi javite varanje, neprikladan jezik ili drugo loše ponašanje.\nMolim vas opišite ispod:", + "reportThisPlayerCheatingText": "Varanje", + "reportThisPlayerLanguageText": "Neprikladan jezik", + "reportThisPlayerReasonText": "Šta bi želeo da prijaviš?", + "reportThisPlayerText": "Reportaj ovog igraca", + "requestingText": "Zatražujem...", + "restartText": "Počni ponovno", + "retryText": "Pokušaj ponovno", + "revertText": "Poništi", + "runText": "Trk", + "saveText": "Spremi", + "scanScriptsErrorText": "Greška(e) skeniranja skripra; provjerite dnevnik za detalje.", + "scoreChallengesText": "Izazovi za rezultat", + "scoreListUnavailableText": "Lista s rezultatima nedostupna.", + "scoreText": "Rezultat", + "scoreUnits": { + "millisecondsText": "Milisekundi", + "pointsText": "Bodova", + "secondsText": "Sekundi" + }, + "scoreWasText": "(bio je ${COUNT})", + "selectText": "Odaberi", + "seriesWinLine1PlayerText": "POBJEĐUJE", + "seriesWinLine1TeamText": "POBJEĐUJU", + "seriesWinLine1Text": "POBJEĐUJE", + "seriesWinLine2Text": "U SERIJI!", + "settingsWindow": { + "accountText": "Račun", + "advancedText": "Napredno", + "audioText": "Zvuk", + "controllersText": "Kontroleri", + "graphicsText": "Grafika", + "playerProfilesMovedText": "Napomena: Profili igrača su pomaknuti na sekciju Račun u meniju.", + "playerProfilesText": "Profili Igrača", + "titleText": "Postavke" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(jednostavna tipkovnica za uređivanje teksta laka za upravljanje kontrolerom)", + "alwaysUseInternalKeyboardText": "Uvijek koristi ugrađenu tipkovnicu", + "benchmarksText": "Testovi učinka i testovi opterećenja", + "disableCameraGyroscopeMotionText": "Onesposobi Žiroskopski Pokret Kamere", + "disableCameraShakeText": "Onesposobi Potres Kamere", + "disableThisNotice": "(možeš isključiti ovu obavijest u naprednim postavkama)", + "enablePackageModsDescriptionText": "(omogućava dodatne modding mogućnosti ali onemogućava mrežnu igru)", + "enablePackageModsText": "Omogući modove iz lokalne pohrane", + "enterPromoCodeText": "Unesi kod", + "forTestingText": "Pažnja: ove vrijednosti su samo za testiranje i bit će izgubljene kad iziđete iz aplikacije.", + "helpTranslateText": "Prijevodi ${APP_NAME} na druge jezike su trud\nzajednice. Ako bi želio doprinijeti ili ispraviti\nprijevod, slijedi poveznicu ispod. Hvala unaprijed!", + "kickIdlePlayersText": "Izbaci neaktivne igrače", + "kidFriendlyModeText": "Mod za djecu (smanjeno nasilje, itd.)", + "languageText": "Jezik", + "moddingGuideText": "Vodič za modding", + "mustRestartText": "Moraš ponovno pokrenuti igru da ovo stupi na snagu.", + "netTestingText": "Testiranje mreže", + "resetText": "Poništi", + "showBombTrajectoriesText": "Pokaži putanju bombe", + "showPlayerNamesText": "Pokaži imena igrača", + "showUserModsText": "Pokaži mapu za modove", + "titleText": "Napredno", + "translationEditorButtonText": "Uredi prijevode ${APP_NAME}", + "translationFetchErrorText": "status prijevoda nedostupan", + "translationFetchingStatusText": "provjeravam status prijevoda...", + "translationInformMe": "Obavijesti me kad moj jezik treba ažurirat", + "translationNoUpdateNeededText": "trenutni jezik je ažuran; juuhuu!", + "translationUpdateNeededText": "** trenutni jezik treba ažurirati!! **", + "vrTestingText": "VR testiranje" + }, + "shareText": "Podeli", + "sharingText": "Deljenje...", + "showText": "Pokaži", + "signInForPromoCodeText": "Moraš se prijaviti na račun da bi kodovi utjecali.", + "signInWithGameCenterText": "Da upotrijebiš Game Center račun,\nprijavi se u Game Center aplikaciji.", + "singleGamePlaylistNameText": "Samo ${GAME}", + "singlePlayerCountText": "1 igrač", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Odabir lika", + "Chosen One": "Odabrani", + "Epic": "Igre u epskom modu", + "Epic Race": "Epska utrka", + "FlagCatcher": "Otmi zastavu", + "Flying": "Sretne misli", + "Football": "Američki nogomet", + "ForwardMarch": "Juriš", + "GrandRomp": "Osvajači", + "Hockey": "Hokej", + "Keep Away": "Drži se podalje", + "Marching": "Obrana", + "Menu": "Glavni izbornik", + "Onslaught": "Invazija", + "Race": "Utrka", + "Scary": "Čuvar zastave", + "Scores": "Ekran s rezultatima", + "Survival": "Eliminacija", + "ToTheDeath": "Bitka do smrti", + "Victory": "Ekran sa završnim rezultatima" + }, + "spaceKeyText": "razmaknica", + "statsText": "Statistika", + "storagePermissionAccessText": "Ovo treba pristup datotekama", + "store": { + "alreadyOwnText": "Već imaš ${NAME}!", + "bombSquadProDescriptionText": "Udvostručuje kupone zarađene u igri\n\nUklanja oglase\n\nUključuje ${COUNT} dodatnih kupona\n\n+${PERCENT}% više bodova za poziciju u ligi\n\nOtključava ${INF_ONSLAUGHT}\ni ${INF_RUNAROUND} razine u timskom modu", + "bombSquadProFeaturesText": "Značajke: ", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Uklanja reklame u igri i nag zaslona\n• Otključava više postavki u igri\n• Također uključuje:", + "buyText": "Kupi", + "charactersText": "Likovi", + "comingSoonText": "Ubrzo...", + "extrasText": "Extra", + "freeBombSquadProText": "BombSquad je sada besplatan, ali budući da si ga kupio prije toga\nprimaš BombSquad Pro nadogradnju i ${COUNT} kupona kao zahvalu. \nUživaj u novim značajkama, i hvala ti na podršci! \n-Eric", + "gameUpgradesText": "Nadogradnje igre", + "holidaySpecialText": "Blagdanski specijal", + "howToSwitchCharactersText": "(idi pod \" ${SETTINGS} -> ${PLAYER_PROFILES}\" da dodijeliš i prilagodiš likove)", + "howToUseIconsText": "(napravi globalni profil igrača (u sekciji račun) da ih koristiš)", + "howToUseMapsText": "(koristi te mape u tvojim ekipnim/svatko protiv svakoga listama igara)", + "iconsText": "Ikoni", + "loadErrorText": "Nemoguće je učitati stranicu. \nProvjeri svoju internetsku vezu.", + "loadingText": "učitavam", + "mapsText": "Mape", + "miniGamesText": "Igre", + "oneTimeOnlyText": "(samo jednom)", + "purchaseAlreadyInProgressText": "Kupovina ovog artikla je već u tijeku.", + "purchaseConfirmText": "Kupi ${ITEM}?", + "purchaseNotValidError": "Kupovina nije valjana. \nKontaktiraj ${EMAIL} ako je ovo greška.", + "purchaseText": "Kupi", + "saleBundleText": "Rasprodaja paketa!", + "saleExclaimText": "Popust!", + "salePercentText": "(${PERCENT}% popusta)", + "saleText": "POPUST", + "searchText": "Traži", + "teamsFreeForAllGamesText": "Ekipe / Svatko protiv svakoga igre", + "totalWorthText": "*** Vrijednost ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Ažuriraj?", + "winterSpecialText": "Zimski specijal", + "youOwnThisText": "- ovo već imaš -" + }, + "storeDescriptionText": "Ludilo za 8 igrača!\n\nRaznesi svoje prijatelje (ili računalo) u turniru eksplozivnih igara kao Otmi zastavu, Hokej i Epska bitka do smrti u usporenom filmu!\n\nSvima je lako uskočiti u akciju uz jednostavne kontrole i podršku za vanjske kontrolere; čak možeš koristiti tvoje mobilne uređaje kao kontrolere putem besplatne 'BombSquad Remote' aplikacije!\n\nPazi, bomba!\n\nZa više informacija, posjeti www.froemling.net/bombsquad.", + "storeDescriptions": { + "blowUpYourFriendsText": "Digni u zrak tvoje prijatelje.", + "competeInMiniGamesText": "Natječi se u igrama od utrkivanja do letenja.", + "customize2Text": "Prilagodi likove, igre, pa čak i glazbu.", + "customizeText": "Prilagodi likove i stvori tvoje vlastite liste igara.", + "sportsMoreFunText": "Sportovi su zabavniji s eksplozivima.", + "teamUpAgainstComputerText": "Udružite se protiv kompjutera." + }, + "storeText": "Trgovina", + "submitText": "Podnesi", + "submittingPromoCodeText": "Podnošenje koda...", + "teamNamesColorText": "Imena/Boja ekipa...", + "telnetAccessGrantedText": "Telnet pristup dozvoljen.", + "telnetAccessText": "Telnet pristup otkriven; dozvoli?", + "testBuildErrorText": "Ova testna vezija više nije aktivna; molim instaliraj novu.", + "testBuildText": "Testna verzija", + "testBuildValidateErrorText": "Nemoguće je potvrditi testnu verziju. (nema internetske veze?)", + "testBuildValidatedText": "Testna verzija potvrđena; uživaj!", + "thankYouText": "Hvala ti na podršci! Uživaj u igri!!", + "threeKillText": "TROSTRUKO UBOJSTVO!!!", + "timeBonusText": "Vremenski bonus", + "timeElapsedText": "Vremena prošlo.", + "timeExpiredText": "Vrijeme isteklo", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Savjet", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Prijatelji s najboljim rezultatima", + "tournamentCheckingStateText": "Provjeravam stanje turnira; molim pričekaj...", + "tournamentEndedText": "Ovaj turnir je završio. Novi će započeti ubrzo.", + "tournamentEntryText": "Kotizacija", + "tournamentResultsRecentText": "Nedavni Rezultati turnira", + "tournamentStandingsText": "Poredak", + "tournamentText": "Turnir", + "tournamentTimeExpiredText": "Vrijeme turnira je isteklo", + "tournamentsText": "Turniri", + "translations": { + "characterNames": { + "Agent Johnson": "Agent Johnson", + "B-9000": "B-9000", + "Bernard": "Medo Brundo", + "Bones": "Bones", + "Butch": "Butch", + "Easter Bunny": "Uskršnji Zeko", + "Flopsy": "Flopsy", + "Frosty": "Mrzli", + "Gretel": "Gretel", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Srećko", + "Mel": "Mel", + "Middle-Man": "Sredina-Man", + "Minimus": "Minimus", + "Pascal": "Paskal", + "Pixel": "Piksel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Djed Mraz", + "Snake Shadow": "Snake Shadow", + "Spaz": "Spaz", + "Taobao Mascot": "Taobao Maskota", + "Todd McBurton": "Todd McBurton", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} (Trening)", + "Infinite ${GAME}": "${GAME} (Beskonačno)", + "Infinite Onslaught": "Beskonačna invazija", + "Infinite Runaround": "Beskonačna obrana", + "Onslaught Training": "Invazija (Trening)", + "Pro ${GAME}": "${GAME} (Pro)", + "Pro Football": "Američki nogomet (Pro)", + "Pro Onslaught": "Invazija (Pro)", + "Pro Runaround": "Obrana (Pro)", + "Rookie ${GAME}": "${GAME} (Početnik)", + "Rookie Football": "Američki nogomet (Početnik)", + "Rookie Onslaught": "Invazija (Početnik)", + "The Last Stand": "Posljednja bitka", + "Uber ${GAME}": "${GAME} (Uber)", + "Uber Football": "Američki nogomet (Uber)", + "Uber Onslaught": "Invazija (Uber)", + "Uber Runaround": "Obrana (Uber)" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Budi odabrani u određenom vremenskom periodu da pobijediš. \nUbij odabranog da postaneš odabran.", + "Bomb as many targets as you can.": "Pogodi što više meta.", + "Carry the flag for ${ARG1} seconds.": "Nosi zastavu ${ARG1} sekundi.", + "Carry the flag for a set length of time.": "Nosi zastavu u određenom vremenskom periodu.", + "Crush ${ARG1} of your enemies.": "Ubij ${ARG1} neprijatelja.", + "Defeat all enemies.": "Pobijedi sve neprijatelje.", + "Dodge the falling bombs.": "Izbjegavaj bombe koje padaju.", + "Final glorious epic slow motion battle to the death.": "Završna slavna epska bitka do smrti u usporenom filmu.", + "Gather eggs!": "Skupi jaja!", + "Get the flag to the enemy end zone.": "Odnesi zastavu do krajnje neprijateljske zone.", + "How fast can you defeat the ninjas?": "Koliko brzo možeš pobijediti nindže?", + "Kill a set number of enemies to win.": "Ubij određeni broj neprijatelja da pobijediš.", + "Last one standing wins.": "Posljednji preživjeli pobjeđuje.", + "Last remaining alive wins.": "Posljednji preživjeli pobjeđuje.", + "Last team standing wins.": "Posljednji preživjeli tim pobjeđuje.", + "Prevent enemies from reaching the exit.": "Spriječi neprijatelje da dođu do izlaza.", + "Reach the enemy flag to score.": "Dođi do neprijateljske zastave da osvojiš bod.", + "Return the enemy flag to score.": "Otmi neprijateljsku zastavu i donesi je u svoju bazu da osvojiš bod.", + "Run ${ARG1} laps.": "Pretrči zadani broj krugova. (${ARG1})", + "Run ${ARG1} laps. Your entire team has to finish.": "Pretrči zadani broj krugova (${ARG1}). Cijela tvoja ekipa mora završiti.", + "Run 1 lap.": "Pretrči 1 krug.", + "Run 1 lap. Your entire team has to finish.": "Pretrči 1 krug. Cijela tvoja ekipa mora završiti.", + "Run real fast!": "Trči stvarno brzo!", + "Score ${ARG1} goals.": "Zabij zadani broj golova. (${ARG1})", + "Score ${ARG1} touchdowns.": "Zabij zadani broj golova. (${ARG1})", + "Score a goal.": "Zabij gol.", + "Score a touchdown.": "Zabij gol.", + "Score some goals.": "Zabij nekoliko golova.", + "Secure all ${ARG1} flags.": "Osiguraj sve ${ARG1} zastave.", + "Secure all flags on the map to win.": "Osiguraj sve zastave na mapi da podijediš.", + "Secure the flag for ${ARG1} seconds.": "Osiguraj zastavu ${ARG1} sekundi.", + "Secure the flag for a set length of time.": "Osiguraj zastavu u određenom vremenskom periodu.", + "Steal the enemy flag ${ARG1} times.": "Otmi neprijateljsku zastavu ${ARG1} puta.", + "Steal the enemy flag.": "Otmi neprijateljsku zastavu.", + "There can be only one.": "Na tronu može biti samo jedan.", + "Touch the enemy flag ${ARG1} times.": "Dotakni neprijateljsku zastavu zadani broj puta. (${ARG1})", + "Touch the enemy flag.": "Dotakni neprijateljsku zastavu.", + "carry the flag for ${ARG1} seconds": "nosi zastavu ${ARG1} sekundi", + "kill ${ARG1} enemies": "ubij ${ARG1} neprijatelja", + "last one standing wins": "posljednji preživjeli pobjeđuje", + "last team standing wins": "posljednji preživjeli tim pobjeđuje", + "return ${ARG1} flags": "otmi zadani broj zastava (${ARG1})", + "return 1 flag": "otmi 1 zastavu", + "run ${ARG1} laps": "pretrči zadani broj krugova (${ARG1})", + "run 1 lap": "pretrči 1 krug", + "score ${ARG1} goals": "zabij zadani broj golova (${ARG1})", + "score ${ARG1} touchdowns": "zabij zadani broj golova (${ARG1})", + "score a goal": "zabij gol", + "score a touchdown": "zabij gol", + "secure all ${ARG1} flags": "osiguraj sve ${ARG1} zastave", + "secure the flag for ${ARG1} seconds": "osiguraj zastavu ${ARG1} sekundi", + "touch ${ARG1} flags": "dotakni zadani broj zastava (${ARG1})", + "touch 1 flag": "dotakni 1 zastavu" + }, + "gameNames": { + "Assault": "Juriš", + "Capture the Flag": "Otmi zastavu", + "Chosen One": "Odabrani", + "Conquest": "Osvajači", + "Death Match": "Bitka do smrti", + "Easter Egg Hunt": "Lov na Uskršnja Jaja", + "Elimination": "Eliminacija", + "Football": "Američki nogomet", + "Hockey": "Hokej", + "Keep Away": "Drži se podalje", + "King of the Hill": "Čuvar zastave", + "Meteor Shower": "Kiša meteora", + "Ninja Fight": "Borba s nindžama", + "Onslaught": "Invazija", + "Race": "Utrka", + "Runaround": "Obrana", + "Target Practice": "Vježba gađanja", + "The Last Stand": "Posljednja bitka" + }, + "inputDeviceNames": { + "Keyboard": "Tipkovnica", + "Keyboard P2": "Tipkovnica P2" + }, + "languages": { + "Arabic": "Arapski", + "Belarussian": "Bjeloruski", + "Chinese": "Pojednostavljeni Kineski", + "ChineseTraditional": "Tradicionalni Kineski", + "Croatian": "Hrvatski", + "Czech": "Češki", + "Danish": "Danski", + "Dutch": "Nizozemski", + "English": "Engleski", + "Esperanto": "Esperanto", + "Finnish": "Finski", + "French": "Francuski", + "German": "Njemački", + "Gibberish": "Frfljanje", + "Greek": "Grčki", + "Hindi": "Hindski", + "Hungarian": "Mađarski", + "Indonesian": "Indonezijski", + "Italian": "Talijanski", + "Japanese": "Japanski", + "Korean": "Korejski", + "Persian": "Perzijski", + "Polish": "Poljski", + "Portuguese": "Portugalski", + "Romanian": "Rumunjski", + "Russian": "Ruski", + "Serbian": "Srpski", + "Slovak": "Slovački", + "Spanish": "Španjolski", + "Swedish": "Švedski", + "Turkish": "Turski", + "Ukrainian": "Ukrajinski", + "Venetian": "Venecijanski", + "Vietnamese": "Vijetnamski" + }, + "leagueNames": { + "Bronze": "Brončana", + "Diamond": "Dijamantna", + "Gold": "Zlatna", + "Silver": "Srebrna" + }, + "mapsNames": { + "Big G": "Veliki G", + "Bridgit": "Bridgit", + "Courtyard": "Dvorište", + "Crag Castle": "Crag Castle", + "Doom Shroom": "Gljiva smrti", + "Football Stadium": "Nogometni stadion", + "Happy Thoughts": "Vesele misli", + "Hockey Stadium": "Stadion za hokej", + "Lake Frigid": "Jezero Frigid", + "Monkey Face": "Majmunovo lice", + "Rampage": "Rampage", + "Roundabout": "Roundabout", + "Step Right Up": "Step Right Up", + "The Pad": "The Pad", + "Tip Top": "Tip Top", + "Tower D": "Toranj D", + "Zigzag": "Cik-cak" + }, + "playlistNames": { + "Just Epic": "Samo epske", + "Just Sports": "Samo sportovi" + }, + "scoreNames": { + "Flags": "Zastave", + "Goals": "Golovi", + "Score": "Rezultat", + "Survived": "Preživio", + "Time": "Vrijeme", + "Time Held": "Vrijeme držanja" + }, + "serverResponses": { + "A code has already been used on this account.": "Ovaj kod je vec koriscen na ovom akauntu", + "A reward has already been given for that address.": "Nagrada je već dobivena na ovoj adresi.", + "Account linking successful!": "Povezivanje računa uspješno!", + "Account unlinking successful!": "Odspajanje računa uspješno!", + "Accounts are already linked.": "Računi su već povezani.", + "An error has occurred; (${ERROR})": "Greška se pojavila; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Greška se pojavila; molimo vas pozovite pomoć. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Greška se pojavila; molim vas pozovite support@froemling.net.", + "An error has occurred; please try again later.": "Greška se pojavila, molim vas pokušajte kasnije.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Jeste li sigurni da želite spojiti ove račune?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nTo se ne može zaustaviti!", + "BombSquad Pro unlocked!": "BombSquad Pro otključan!", + "Can't link 2 accounts of this type.": "Nije moguće spojiti 2 računa tog tipa.", + "Can't link 2 diamond league accounts.": "Nije moguće spojiti 2 računa dijamantne lige.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Nije moguće spojiti; prešlo bi maksimum od ${COUNT} spojenih računa.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Varanje detektirano; rekordi i nagrade suspendirani na ${COUNT} dana.", + "Could not establish a secure connection.": "Ne moguće uspostaviti sigurnu vezu.", + "Daily maximum reached.": "Dnevni maksimu predzen", + "Entering tournament...": "Ulazim u turnir...", + "Invalid code.": "Nevažeći kod.", + "Invalid payment; purchase canceled.": "Neispravno plaćanje; kupovina prekinuta.", + "Invalid promo code.": "Neispravan promotivni kod.", + "Invalid purchase.": "Greška u kupnji.", + "Invalid tournament entry; score will be ignored.": "Nevaljan ulazak u turnir; rekord će se ignorirati.", + "Item unlocked!": "Otključao si stvar!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "SPAJANJE ZAUSTAVLJENO. ${ACCOUNT} sadržava\nznačajne podatke koje bi ste SVE NJIH IZGUBILI.\nMožete se povezati drugim redosljedom ako hoćete\n(i izgubiti sve informacije OVOG računa)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Spoji račun ${ACCOUNT} na ovaj račun?\nSve postojeće informacije o ${ACCOUNT} će biti izgubljene.\nTo se ne može zaustaviti. Jesi li siguran?", + "Max number of playlists reached.": "Max broj dosegao popise pjesama.", + "Max number of profiles reached.": "Max broj dosegao profila.", + "Maximum friend code rewards reached.": "Maksimalne nagrade od prijateljskih kodova dostignute.", + "Message is too long.": "Poruka je prevelika.", + "Profile \"${NAME}\" upgraded successfully.": "Profil \"${NAME}\" uspjesno upgrajdovan", + "Profile could not be upgraded.": "Profil se ne može ažurirati.", + "Purchase successful!": "Kupovina uspješna!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Primio si ${COUNT} kupona za prijavu. \nPonovno se prijavi sutra da dobiješ ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Funkcija poslužitelja više nije podržana u ovoj verziji igre;\nAžurirajte na noviju verziju.", + "Sorry, there are no uses remaining on this code.": "Izvini, ovaj kod se vise nemoze koristit.", + "Sorry, this code has already been used.": "Izvini, ovaj kod je već iskorišćen.", + "Sorry, this code has expired.": "Izvini, ovaj kod je istekao.", + "Sorry, this code only works for new accounts.": "Izvini, ovaj kod radi samo za nove igrače.", + "Temporarily unavailable; please try again later.": "Privremeno nedostupno; molimo vas pokušajte kasnije.", + "The tournament ended before you finished.": "Turnir je šio prije nego što si ti završio.", + "This account cannot be unlinked for ${NUM} days.": "Ovaj se račun ne može odvezati ${NUM} dana.", + "This code cannot be used on the account that created it.": "Ovaj kod se nemoze koristiti na akauntu koji je stvorio.", + "This requires version ${VERSION} or newer.": "Ovo zahtijeva verziju ${VERSION} ili noviju.", + "Tournaments disabled due to rooted device.": "Turniri nedostupni zbog rootanog uređaja.", + "Tournaments require ${VERSION} or newer": "Turniri koriste ${VERSION} ili noviju", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Odveži ${ACCOUNT} s ovog računa?\nSve informacije sa ${ACCOUNT} će se resetirati.\n(osim postignuća u nekim slučajima)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "UPOZORENJE: pritužbe hakiranja izdane su na vaš račun.\nRačuni koji su pronađeni hakirajući će biti zabranjeni za korištenje. Igrajte pošteno.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Da li bi ste htjeli spojiti račun uređaja na ovaj?\n\nRačun uređaja je ${ACCOUNT1}\nOvaj račun je ${ACCOUNT2}\n\nOvo će dozvoliti da nastaviš gdje si stao.\nUpozorenje: ovo se ne može poništiti!", + "You already own this!": "Ovo već imaš!", + "You can join in ${COUNT} seconds.": "Možeš se pridružiti za ${COUNT} sekundi.", + "You don't have enough tickets for this!": "Nemaš dovoljno kupona za ovo!", + "You don't own that.": "Ne poseduješ to.", + "You got ${COUNT} tickets!": "Dobio si ${COUNT} kupona!", + "You got a ${ITEM}!": "Dobio si ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Prešao si u novu ligu; čestitke!", + "You must update to a newer version of the app to do this.": "Moraš ažurirati ovu aplikaciju na noviju verziju da uradiš ovo.", + "You must update to the newest version of the game to do this.": "Morate ažurirati igru na najnoviju verziju da biste ovo mogli.", + "You must wait a few seconds before entering a new code.": "Moraš pričekati nekoliko sekundi prije unošenja novog koda.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Zauzeo si #${RANK} mjesto u zadnjem turniru. Hvala na sudjelovanju!", + "Your account was rejected. Are you signed in?": "Vaš je račun odbijen. Jeste li prijavljeni?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Tvoja kopija igre je izmijenjena. \nMolim poništi sve promjene i pokušaj ponovno.", + "Your friend code was used by ${ACCOUNT}": "Tvoj kod druga je iskorišćen od strane ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Minuta", + "1 Second": "1 Sekunda", + "10 Minutes": "10 Minuta", + "2 Minutes": "2 Minute", + "2 Seconds": "2 Sekunde", + "20 Minutes": "20 Minuta", + "4 Seconds": "4 Sekunde", + "5 Minutes": "5 Minuta", + "8 Seconds": "8 Sekundi", + "Allow Negative Scores": "Dozvoli Negativne Rezultate", + "Balance Total Lives": "Balansiraj ukupne živote", + "Bomb Spawning": "Bomba Nastaje", + "Chosen One Gets Gloves": "Odabrani dobija rukavice", + "Chosen One Gets Shield": "Odabrani dobija štit", + "Chosen One Time": "Vrijeme koje moraš biti Odabrani", + "Enable Impact Bombs": "Uključi kontakt-bombe", + "Enable Triple Bombs": "Uključi trostruke bombe", + "Entire Team Must Finish": "Ceo Tim Mora Završiti", + "Epic Mode": "Epski mod", + "Flag Idle Return Time": "Vrijeme povratka zastave dok je u mirovanju", + "Flag Touch Return Time": "Vrijeme povratka zastave nakon dodira", + "Hold Time": "Vrijeme držanja", + "Kills to Win Per Player": "Ubojstava za pobjedu po igraču", + "Laps": "Krugovi", + "Lives Per Player": "Života po igraču", + "Long": "Dugo", + "Longer": "Duže", + "Mine Spawning": "Stvaranje mina", + "No Mines": "Nema mina", + "None": "Ništa", + "Normal": "Normalno", + "Pro Mode": "Pro mod", + "Respawn Times": "Vrijeme oživljavanja", + "Score to Win": "Bodova za pobjedu", + "Short": "Kratko", + "Shorter": "Kraće", + "Solo Mode": "Solo mod", + "Target Count": "Broj meta", + "Time Limit": "Vremensko ograničenje" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} je diskvalifikovan zbog ${PLAYER} izasao", + "Killing ${NAME} for skipping part of the track!": "Ubijam ${NAME} zbog preskakanja dijela trake!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Upozorenje za ${NAME}: turbo / neprekidno pritiskanje tipki vas izbaci." + }, + "teamNames": { + "Bad Guys": "Loši dečki", + "Blue": "Plavi", + "Good Guys": "Dobri dečki", + "Red": "Crveni" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Pravovremeni trčeći-skačući-vrteći udarac može ubiti odjednom\ni zaraditi ti doživotno poštovanje tvojih prijatelja.", + "Always remember to floss.": "Uvijek očisti zube koncem.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Stvori profile igrača za sebe i svoje prijatelje s\ntebi dražim imenima i izgledima umjesto nasumičnih.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Kletva te pretvori u otkucavajuću tempiranu bombu. \nJedini lijek je da brzo zgrabiš prvu pomoć.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Unatoč njihovim izgledima, mogućnosti svih likova su iste, \npa samo izaberi onoga kojemu si najsličniji.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Nemoj se previše uobraziti s energetskim štitom; još te uvijek mogu baciti s litice.", + "Don't run all the time. Really. You will fall off cliffs.": "Nemoj trčati cijelo vrijeme. Stvarno. Past ćeš s litice.", + "Don't spin for too long; you'll become dizzy and fall.": "Nemoj se vrteti mnogo dugo; dobićeš vrtoglavicu i pasti.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Drži bilo koju tipku za trk. (Okidačke tipke isto rade ako ih imaš)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Drži bilo koju tipku za trk. Bit ćeš brži, \nali ne možeš dobro skretati, pa pazi na zavoje i litice.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ledene bombe nisu jako moćne, ali zamrzavaju\nsvakoga koga udare, ostavljajući ih lomljivima.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Ako te netko uhvati, udari ga i pustit će te. \nOvo radi i u stvarnom životu.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Ako nemate kontrolere, intalirajte\n'${REMOTE_APP_NAME}` app na vašem telefonu da bi ste ih koristili kao kontrolere.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Ako ti nedostaje kontrolera, instaliraj 'BombSquad Remote' aplikaciju\nna tvoje iOS i Android uređaje da ih možeš koristiti kao kontrolere. ", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Ako se ljepljiva bomba zalijepi na tebe, skači okolo i vrti se u krugovima. Mogao bi\notresti bombu sa sebe, a ako ne uspiješ bar će tvoji zadnji trenutci biti zabavni.", + "If you kill an enemy in one hit you get double points for it.": "Ako ubiješ neprijatelja u jednom udarcu, za to dobiješ duple bodove.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Ako pokupiš kletvu, jedina nada da da preživiš je da\npronađeš prvu pomoć u idućih par sekundi.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Ako stojiš na jednom mjestu, gotov si. Trči i izbjegavaj neprijatelje da preživiš..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Ako imaš puno igrača koji dolaze i odlaze, uključi 'automatski izbaci neaktivne igrače' \nu postavkama u slučaju da netko zaboravi napustiti igru.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Ako se tvoj uređaj previše ugrije ili bi htio sačuvati bateriju, \nsmanji \"Vizualno\" ili \"Rezolucija\" u Postavke->Grafika", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Ako je tvoj FPS nizak, pokušaj smanjiti rezoluciju\nili vizualna u grafičkim postavkama igre.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "U igri Otmi zastavu, tvoja vlastita zastava mora biti u tvojoj bazi da bi mogao osvojiti bod, \npa ako je druga ekipa blizu osvajanja boda, ukradi njihovu zastavu da ih zaustaviš.", + "In hockey, you'll maintain more speed if you turn gradually.": "U hokeju, dobit ćeš više brzine ako se okrećeš postepeno.", + "It's easier to win with a friend or two helping.": "Lakše je pobijediti ako ti pomaže jedan ili dva prijatelja.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Skači dok bacaš bombe da ih baciš na najviše razine.", + "Land-mines are a good way to stop speedy enemies.": "Mine su dobar način da zaustaviš brze neprijatelje.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Puno stvari možeš podignuti i baciti, uključujući ostale igrače. Bacanje \ntvojih neprijatelja s litice može biti učinkovita i emocionalno zadovoljavajuća strategija.", + "No, you can't get up on the ledge. You have to throw bombs.": "Ne, ne možeš se popeti gore. Moraš bacati bombe.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Igrači mogu ući u i napustiti većinu igara dok su u tijeku, \na isto tako možeš uključiti i isključiti kontrolere.", + "Practice using your momentum to throw bombs more accurately.": "Vježbaj koristeći zalet da bacaš bombe preciznije.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Udarci nanose više štete što se brže kreću tvoje šake, \npa pokušaj trčati, skakati i vrtjeti se kao lud.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Trči naprijed-nazad prije nego što\nbaciš bombu da je baciš dalje.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Ubij grupu neprijatelja\nbacajući bombu blizu TNT-a.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Glava je najosjetljivije područje, pa ljepljiva bomba\nu glavu obično znači kraj igre.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Ova razina nikad ne završava, ali najbolji rezultat ovdje\nzaradit će ti vječno poštovanje kroz cijeli svijet.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Jačina izbačaja temelji se na pravcu kojeg držiš. \nDa baciš nešto nježno ispred sebe, nemoj držati niti jedan pravac.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Dosadila ti je glazbena lista? Zamijeni je s vlastitom! \nPogledaj pod Postavke->Zvuk->Glazbena lista", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Pokušaj pričekati sekundu ili dvije prije bacanja bombe da fitilj malo izgori.", + "Try tricking enemies into killing eachother or running off cliffs.": "Pokuša prevariti neprijatelje tako da ubiju jedan drugoga ili padnu s litice.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Koristi 'podigni' tipku da uhvatiš zastavu < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Trči naprijed-nazad da bacaš dalje..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Možeš 'naciljati' tvoje udarce vrteći se lijevo ili desno. \nOvo je korisno za odbacivanje neprijatelja s ruba ili zabijanje golova u hokeju.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Možeš prosuditi kad će bomba eksplodirati na temelju boje \niskri s fitilja: žuta.. narančasta.. crvena.. BUM.", + "You can throw bombs higher if you jump just before throwing.": "Možeš bacati bombe više ako skočiš malo prije nego što baciš.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Trpiš štetu kad udariš glavom o nešto, \npa pokušaj ne udarati glavom o nešto.", + "Your punches do much more damage if you are running or spinning.": "Tvoji udarci nanose više štete ako trčiš ili se vrtiš." + } + }, + "trophiesRequiredText": "Za ovo je potrebno minimum ${NUMBER} trofeja.", + "trophiesText": "Trofeja", + "trophiesThisSeasonText": "Trofeji ove sezone", + "tutorial": { + "cpuBenchmarkText": "Vrtim upute na ludo-smiješnoj brzini (prvenstveno testira brzinu procesora)", + "phrase01Text": "Pozdrav!", + "phrase02Text": "Dobrodošao u ${APP_NAME}!", + "phrase03Text": "Evo nekoliko savjeta za kontroliranje tvog lika:", + "phrase04Text": "Puno stvari u ${APP_NAME}-u je temeljeno na FIZICI.", + "phrase05Text": "Na primjer, kad udaraš,..", + "phrase06Text": "..šteta koju ćeš nanijeti temelji se na brzini tvojih šaka.", + "phrase07Text": "Vidiš? Nismo se micali, pa je to jedva ozlijedilo ${NAME}a.", + "phrase08Text": "Sad ćemo skočiti i vrtjeti se da postignemo veću brzinu.", + "phrase09Text": "Ah, to je bolje.", + "phrase10Text": "Trčanje također pomaže.", + "phrase11Text": "Pritisni i drži pritisnutom BILO KOJU tipku da potrčiš.", + "phrase12Text": "Trči i okreći se za ekstra-fenomenalne udarce.", + "phrase13Text": "Uuups, oprosti zbog toga ${NAME}.", + "phrase14Text": "Možeš podići i baciti stvari kao što su zastave... ili ${NAME}.", + "phrase15Text": "Naposljetku, bombe.", + "phrase16Text": "Za precizno bacanje bombi treba vježbe.", + "phrase17Text": "Au! Ovo nije bilo baš dobro bacanje.", + "phrase18Text": "Kretanje ti pomaže da bacaš dalje.", + "phrase19Text": "Skakanje ti pomaže da bacaš više.", + "phrase20Text": "Zavrti se dok bacaš bombe da bacaš još više.", + "phrase21Text": "Tempiranje tvojih bombi može biti teško.", + "phrase22Text": "K vrapcu.", + "phrase23Text": "Pokušaj pričekati sekundu ili dvije da fitilj malo izgori.", + "phrase24Text": "Hura! Lijepo izgoreno.", + "phrase25Text": "Pa, to je više-manje sve.", + "phrase26Text": "Sad ih uhvati, tigre!", + "phrase27Text": "Sjeti se treninga, i VRATIT ćeš se živ!", + "phrase28Text": "... pa, valjda...", + "phrase29Text": "Sretno!", + "randomName1Text": "Fred", + "randomName2Text": "Harry", + "randomName3Text": "Bill", + "randomName4Text": "Chuck", + "randomName5Text": "Phil", + "skipConfirmText": "Stvarno ćeš preskočiti upute? Pritisni da potvrdiš.", + "skipVoteCountText": "${COUNT}/${TOTAL} glasova za preskakanje", + "skippingText": "preskačem upute...", + "toSkipPressAnythingText": "(pritisni bilo što da preskočiš upute)" + }, + "twoKillText": "DVOSTRUKO UBOJSTVO!", + "unavailableText": "nedostupno", + "unconfiguredControllerDetectedText": "Nepodešen kontroler otkriven:", + "unlockThisInTheStoreText": "Ovo moraš otključati u dućanu.", + "unlockThisProfilesText": "Da napraviš više od ${NUM} profila, treba ti:", + "unlockThisText": "Da oslobodite ovo, trebate", + "unsupportedHardwareText": "Žao mi je, ova verzija igre ne podržava ovaj hardver.", + "upFirstText": "Prva igra:", + "upNextText": "Igra ${COUNT}:", + "updatingAccountText": "Ažuriram tvoj račun...", + "upgradeText": "Upgrajd", + "upgradeToPlayText": "Otključaj \"${PRO}\" u dućanu da možeš igrati ovo.", + "useDefaultText": "Koristi zadano", + "usesExternalControllerText": "Ova igra koristi vanjski kontroler za kretanje.", + "usingItunesText": "Koristim glazbenu aplikaciju za glazbenu listu...", + "usingItunesTurnRepeatAndShuffleOnText": "Molim provjeri da je opcija shuffle uključena, a opcija repeat postavljena na SVE na iTunes. ", + "validatingTestBuildText": "Potvrđujem testnu verziju...", + "victoryText": "Pobjeda!", + "voteDelayText": "Nemožeš započeti još jedno glasanje ${NUMBER} sekundi", + "voteInProgressText": "Glasanje je već u toku", + "votedAlreadyText": "Već si glasao", + "votesNeededText": "${NUMBER} glasova potrebno", + "vsText": "protiv", + "waitingForHostText": "(čekam na ${HOST} za nastavak)", + "waitingForPlayersText": "čekam na igrače da se priključe...", + "waitingInLineText": "Čekanje u redu (žurka je puna)...", + "watchAVideoText": "Pogledaj video", + "watchAnAdText": "Pogledaj oglas", + "watchWindow": { + "deleteConfirmText": "Izbriši \"${REPLAY}\"?", + "deleteReplayButtonText": "Izbriši\nsnimku", + "myReplaysText": "Moje snimke", + "noReplaySelectedErrorText": "Niti jedna snimka nije odabrana", + "playbackSpeedText": "Brzina reprodukcije: ${SPEED}", + "renameReplayButtonText": "Preimenuj \nsnimku", + "renameReplayText": "Novo ime snimke \"${REPLAY}\":", + "renameText": "Preimenuj", + "replayDeleteErrorText": "Greška u brisanju snimke.", + "replayNameText": "Ime snimke", + "replayRenameErrorAlreadyExistsText": "Snimka s tim imenom već postoji.", + "replayRenameErrorInvalidName": "Nemoguće preimenovati snimku; nevažeće ime.", + "replayRenameErrorText": "Greška u preimenovanju snimke.", + "sharedReplaysText": "Podijeljene snimke", + "titleText": "Gledaj", + "watchReplayButtonText": "Pogledaj\nsnimku" + }, + "waveText": "Nalet", + "wellSureText": "Sigurno!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Tražim Wiimote-ove...", + "pressText": "Pritisni Wiimote tipke 1 i 2 istovremeno.", + "pressText2": "Na novijim Wiimote-ovima s ugrađenim Motion Plus-om, umjesto njih pritisni crvenu 'sync' tipku na poleđini." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "listenText": "Traži", + "macInstructionsText": "Provjeri da je tvoj Wii isključen, a Bluetooth uključen\nna tvome Macu, pa pritisni 'Traži'. Podrška za Wiimote-ove\nmože biti malo nestabilna, pa ćeš možda morati pokušati nekoliko puta\nprije nego što uspostaviš vezu.\n\nBluetooth bi trebao podržati do 7 povezanih uređaja,\nmada bi tvoja udaljenost mogla varirati.\n\nBombSquad podržava originalne Wiimote-ove, Nunchuk-ove,\ni Classic Controller.\nNoviji Wii Remote Plus sada radi,\nali ne s dodacima.", + "thanksText": "Hvala DarwiinRemote timu\nšto je ovo omogućio.", + "titleText": "Podešavanje Wiimote-ova" + }, + "winsPlayerText": "${NAME} pobjeđuje!", + "winsTeamText": "${NAME} pobjeđuju!", + "winsText": "${NAME} pobjeđuje!", + "worldScoresUnavailableText": "Svjetski rezultati nedostupni.", + "worldsBestScoresText": "Najbolji svjetski rezultati", + "worldsBestTimesText": "Najbolja svjetska prolazna vremena", + "xbox360ControllersWindow": { + "getDriverText": "Nabavi driver", + "macInstructions2Text": "Da možeš koristiti kontrolere bežično, trebat ćeš i reciever koji\ndolazi u 'Xbox 360 Wireless Controller for Windows' paketu.\nJedan reciever omogućuje ti da povežeš do 4 kontrolera.\n\nVažno: recieveri trećih strana neće raditi s ovim driverom; \nprovjeri da na tvom recieveru piše 'Microsoft', a ne 'XBOX 360'.\nMicrosoft ih više ne prodaje odvojeno, pa ćeš morati nabaviti\njednoga u paketu s kontrolerom ili potraži na ebayu.\n\nAko ti je ovo bilo korisno, molim te razmisli o donaciji\nprogrameru drivera na njegovoj stranici.", + "macInstructionsText": "Da možeš koristiti Xbox 360 kontrolere, morat ćeš instalirati\nMac driver dostupan na poveznici ispod. \nRadi i sa žičnim i s bežičnim kontrolerima.", + "ouyaInstructionsText": "Za korištenje žičnih Xbox 360 kontrolera s BombSquadom, jednostavno\nih uključi u USB ulaz tvog uređaja. Možeš koristiti USB hub\nda priključiš više kontrolera.\n\nZa korištenje bežičnih kontrolera trebat ćeš bežični reciever, \nkoji je dostupan kao dio \"Xbox 360 wireless Controller for Windows\" \npaketa ili u slobodnoj prodaji. Svaki se reciever priključuje u USB ulaz i\nomogućuje ti da povežeš do 4 bežična kontrolera.", + "titleText": "Korišćenje Xbox 360 kontrolera sa $(APP_NAME)" + }, + "yesAllowText": "Da, dopusti!", + "yourBestScoresText": "Tvoji najbolji rezultati", + "yourBestTimesText": "Tvoja najbolja prolazna vremena" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/czech.json b/dist/ba_data/data/languages/czech.json new file mode 100644 index 0000000..aa35848 --- /dev/null +++ b/dist/ba_data/data/languages/czech.json @@ -0,0 +1,1869 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Jména účtů nemůžou obsahovat smajlíky ani jiné speciální znaky", + "accountProfileText": "(Aktuální profil)", + "accountsText": "Účty", + "achievementProgressText": "Achievementy: ${COUNT}/${TOTAL}", + "campaignProgressText": "Postup Kampaně [Těžká]: ${PROGRESS}", + "changeOncePerSeason": "Lze změnit pouze jednou za sezónu.", + "changeOncePerSeasonError": "Chcete-li toto změnit znovu, musíte počkat na další sezónu (${NUM} days)", + "customName": "Vlastní Jméno", + "linkAccountsEnterCodeText": "Vložit kód", + "linkAccountsGenerateCodeText": "Generovat kód", + "linkAccountsInfoText": "(sdílení postupu mezi různými zařízeními)", + "linkAccountsInstructionsNewText": "Pro spojení dvou účtů na jednom z nich\ngenerujte kód a na druhém tento kód zadejte. Data z \ndruhého účtu se pak budou s prvním sdílet\n(Data z prvního účtu budou smazána.)\n\nMůžete spojit až ${COUNT} účtů.\n\nDŮLEŽITÉ: Spojujte jen účty které vlastníte, \nnikoliv s přáteli. Pokud tak učiníte, nebudete\nmoci hrát ve stejný čas!", + "linkAccountsInstructionsText": "Pokud chcete propojit účty, na jednom z nich generujte kód\na na druhém tento kód zadejte.\nPostup a inventář bude zkombinován.\nMůžete propojit až ${COUNT} účtů.\n\nDŮLEŽITÉ: PROPOJUJTE POUZE ÚČTY KTERÉ JSOU VAŠE!\nPOKUD PROPOJÍTE ÚČTY S KAMARÁDEM, NEBUDETE MOCI\nHRÁT OBA VE STEJNÝ ČAS!\n\nDobře si to rozmyslete; Tuto akci nelze vrátit zpět!", + "linkAccountsText": "Propojit účty", + "linkedAccountsText": "Propojené účty:", + "nameChangeConfirm": "Přejete si změnit jméno účtu na ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Jste si jistí, že to chcete udělat?\nTímto NENÁVRATNĚ resetujete veškerý postup Co-op\na lokální nejlepší výsledky (ale kupóny vám zůstanou).", + "resetProgressConfirmText": "Jste si jistí, že to chcete udělat?\nTímto NENÁVRATNĚ resetujete veškerý postup Co-Op,\nachievementy a lokální nejlepší výsledky.\n(ale kupóny vám zůstanou)", + "resetProgressText": "Resetovat Postup", + "setAccountName": "Nastavit jméno účtu", + "setAccountNameDesc": "Vyberte si jméno pro váš účet.\nMůžete použít jedno ze svých již\npoužitých nebo si vytvořit nové.", + "signInInfoText": "Přihlašte se, abyste mohli sbírat kupóny, soupeřit online,\na sdílet postup mezi zařízeními.", + "signInText": "Přihlásit", + "signInWithDeviceInfoText": "(automaticky vytvořený účet dostupný pouze na tomto zařízení)", + "signInWithDeviceText": "Přihlásit se s účtem zařízení", + "signInWithGameCircleText": "Přihlásit se s ,,Game circle\"", + "signInWithGooglePlayText": "Přihlásit se přes Google Play", + "signInWithTestAccountInfoText": "(zastaralý typ účtu; použije raději účet zařízení)", + "signInWithTestAccountText": "Přihlásit se s testovacím účtem", + "signOutText": "Odhlásit se", + "signingInText": "Přihlašuji se...", + "signingOutText": "Odhlašuji se...", + "testAccountWarningCardboardText": "Varování: Přihlašujete se s \"testovacím\" účtem.\nTyto účty budou nahrazeny Google účty hned,\njakmile budou podporovány cardboard aplikacemi.\n\nProzatím si všechny kupony budete muset získat ve hře\n(avšak získáte Pro účet zdarma na vyzkoušení).", + "testAccountWarningOculusText": "Varování: Přihlašujete se pod \"testovacím\" účtem.\nTyto účty budou později v tomto roce nahrazeny Oculus účty,\nkteré nabídnou nákupy kupónů a ostatní vylepšení.\n\nProzatím musíte všechny kupóny získat ve hře\n(avšak získáte BombSquad Pro upgrade zdarma na vyzkoušení).", + "testAccountWarningText": "Upozornění: jste přihlášení k \"test\" účtu. \nTento účet je vázán na toto konkrétní zařízení a \nmůže dojít k pravidelnému resetování. (takže není důvod utrácet \nspoustu času k získávání a odemykání věci) \n\nPoužíváním klasické verze této hry je možné používat \"skutečný\" \núčet (Game-Center, Google Plus, atd.), \nkterý umožňuje ukládat svůj postup v cloudu \na sdílet tento postup mezi různými zařízeními.", + "ticketsText": "Kupóny: ${COUNT}", + "titleText": "Profil", + "unlinkAccountsInstructionsText": "Zvolte účet k odpojení", + "unlinkAccountsText": "Odpojit účty", + "viaAccount": "(přes účet ${NAME})", + "youAreLoggedInAsText": "Jste přihlášen jako:", + "youAreSignedInAsText": "Jste přihlášen jako:" + }, + "achievementChallengesText": "Ocenění Výzev", + "achievementText": "Úspěch", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Zabijte 3 padouchy pomocí TNT", + "descriptionComplete": "Zabili jste 3 padouchy pomocí TNT", + "descriptionFull": "Zabijte 3 padouchy pomocí TNT v ${LEVEL}", + "descriptionFullComplete": "Zabili jste 3 padouchy pomocí TNT v ${LEVEL}", + "name": "Dynamit dělá Bum" + }, + "Boxer": { + "description": "Vyhrajte bez použití bomb", + "descriptionComplete": "Vyhráli jste bez použití bomb", + "descriptionFull": "Dokončete ${LEVEL} bez použití bomb.", + "descriptionFullComplete": "Dokončili jste ${LEVEL} bez použití bomb.", + "name": "Boxer" + }, + "Dual Wielding": { + "descriptionFull": "Připojte alespoň 2 ovladače (Hardware nebo aplikaci)", + "descriptionFullComplete": "Připojeno alespoň 2 ovladače (hardware/aplikaci)", + "name": "Ovladačový maniak" + }, + "Flawless Victory": { + "description": "Vyhrajte aniž byste byly zasaženi", + "descriptionComplete": "Vyhráli jste aniž byste byly zasaženi", + "descriptionFull": "Vyhrajte ${LEVEL} aniž byste byli zasaženi", + "descriptionFullComplete": "Vyhráli jste ${LEVEL} aniž byste byli zasaženi", + "name": "Bezvadná Výhra" + }, + "Free Loader": { + "descriptionFull": "Začněte hru ,,všichni proti všem\" s alespoň 2 hráči", + "descriptionFullComplete": "Začali jste ,,všichni proti všem\" hru s více jak 2 hráči", + "name": "Všichni proti všem!!!" + }, + "Gold Miner": { + "description": "Zabijte 6 padouchů pomocí min", + "descriptionComplete": "Zabili jste 6 padouchů pomocí min", + "descriptionFull": "Zabijte 6 padouchů pomocí min v ${LEVEL}", + "descriptionFullComplete": "Zabili jste 6 padouchů pomocí min v ${LEVEL}", + "name": "Zlatokop" + }, + "Got the Moves": { + "description": "Vyhrajte bez použití pěstí a bomb", + "descriptionComplete": "Vyhráli jste bez použití pěstí a bomb", + "descriptionFull": "Vyhrajte ${LEVEL} bez mlácení nebo bomb", + "descriptionFullComplete": "Vyhráli jste ${LEVEL} bez mlácení nebo bomb", + "name": "Naučit se kroky" + }, + "In Control": { + "descriptionFull": "Připojte ovladač (hardware/aplikaci)", + "descriptionFullComplete": "Připojen ovladač (hardware/aplikaci)", + "name": "Ve spojení" + }, + "Last Stand God": { + "description": "Získejte 1000 bodů", + "descriptionComplete": "Získáno 1000 bodů", + "descriptionFull": "Získejte 1000 bodů v ${LEVEL}", + "descriptionFullComplete": "Získáno 1000 bodů v ${LEVEL}", + "name": "${LEVEL} Bůh" + }, + "Last Stand Master": { + "description": "Získejte 250 bodů", + "descriptionComplete": "Získáno 250 bodů", + "descriptionFull": "Získejte 250 bodů v ${LEVEL}", + "descriptionFullComplete": "Získáno 250 bodů v ${LEVEL}", + "name": "${LEVEL} Profík" + }, + "Last Stand Wizard": { + "description": "Získajte 500 bodů", + "descriptionComplete": "Získáno 500 bodů", + "descriptionFull": "Získejte 500 bodů v ${LEVEL}", + "descriptionFullComplete": "Získáno 500 bodů v ${LEVEL}", + "name": "${LEVEL} Kouzelník" + }, + "Mine Games": { + "description": "Zabij 3 padouchy pomocí min", + "descriptionComplete": "Zabili jste 3 padouchy pomocí min", + "descriptionFull": "Zabij 3 padouchy pomocí min v ${LEVEL}", + "descriptionFullComplete": "Zabili jste 3 padouchy pomocí min v ${LEVEL}", + "name": "Minové hry" + }, + "Off You Go Then": { + "description": "Vyhoď 3 padouchy pryč z mapy", + "descriptionComplete": "Vyhodili jste 3 padouchy pryč z mapy", + "descriptionFull": "Vyhoďte 3 padouchy pryč z mapy v ${LEVEL}", + "descriptionFullComplete": "Vyhodili jste 3 padouchy pryč z mapy v ${LEVEL}", + "name": "Je čas, aby jsi odešel" + }, + "Onslaught God": { + "description": "Získejte 5000 bodů", + "descriptionComplete": "Získáno 5000 bodů", + "descriptionFull": "Získejte 5000 bodů v ${LEVEL}", + "descriptionFullComplete": "Získáno 5000 bodů v ${LEVEL}", + "name": "${LEVEL} Bůh" + }, + "Onslaught Master": { + "description": "Získejte 500 bodů", + "descriptionComplete": "Získáno 500 bodů", + "descriptionFull": "Získejte 500 bodů v ${LEVEL}", + "descriptionFullComplete": "Získáno 500 bodů v ${LEVEL}", + "name": "${LEVEL} Profík" + }, + "Onslaught Training Victory": { + "description": "Dokončete všechny vlny", + "descriptionComplete": "Dokončeny všechny vlny", + "descriptionFull": "Dokončete všechny vlny v ${LEVEL}", + "descriptionFullComplete": "Dokončeny všechny vlny v ${LEVEL}", + "name": "${LEVEL} Vítězství" + }, + "Onslaught Wizard": { + "description": "Získejte 1000 bodů", + "descriptionComplete": "Získáno 5000 bodů", + "descriptionFull": "Získejte 1000 bodů v ${LEVEL}", + "descriptionFullComplete": "Získáno 1000 bodů v ${LEVEL}", + "name": "${LEVEL} Kouzelník" + }, + "Precision Bombing": { + "description": "Vyhrajte bez bonusů", + "descriptionComplete": "Vyhráli jste bez bonusů", + "descriptionFull": "Vyhrajte ${LEVEL} bez bonusů", + "descriptionFullComplete": "Vyhráli jste ${LEVEL} bez bonusů", + "name": "Přesné odpálení" + }, + "Pro Boxer": { + "description": "Vyhrajte bez bomb", + "descriptionComplete": "Vyhráli jste bez bomb", + "descriptionFull": "Dokončete ${LEVEL} bez bomb", + "descriptionFullComplete": "Dokončen ${LEVEL} bez bomb", + "name": "Profi Boxer" + }, + "Pro Football Shutout": { + "description": "Vyhrajte aniž by padouši skórovali", + "descriptionComplete": "Vyhráli jste aniž by padouši skórovali", + "descriptionFull": "Vyhrajte ${LEVEL} aniž by padouši skórovali", + "descriptionFullComplete": "Vyhráli jste ${LEVEL} aniž by padouši skórovali", + "name": "${LEVEL} Bezchybná výhra" + }, + "Pro Football Victory": { + "description": "Vyhrajte hru", + "descriptionComplete": "Vyhráli jste hru", + "descriptionFull": "Vyhrajte hru v ${LEVEL}", + "descriptionFullComplete": "Vyhráli jste hru v ${LEVEL}", + "name": "${LEVEL} Vítězství" + }, + "Pro Onslaught Victory": { + "description": "Porazte všechny vlny", + "descriptionComplete": "Všechny vlny poraženy", + "descriptionFull": "Porazte všechny vlny v ${LEVEL}", + "descriptionFullComplete": "Porazili jste všechny vlny v ${LEVEL}", + "name": "${LEVEL} Vítězství" + }, + "Pro Runaround Victory": { + "description": "Dokončete všechny vlny", + "descriptionComplete": "Dokončili jste všechny vlny", + "descriptionFull": "Dokončete všechny vlny v ${LEVEL}", + "descriptionFullComplete": "Dokončili jste všechny vlny v ${LEVEL}", + "name": "${LEVEL} Výhra" + }, + "Rookie Football Shutout": { + "description": "Vyhrajte aniž by padouši skórovali", + "descriptionComplete": "Vyhráli jste aniž by padouši skórovali", + "descriptionFull": "Vyhrajte ${LEVEL} aniž by padouši skórovali", + "descriptionFullComplete": "Vyhráli jste ${LEVEL} aniž by padouši skórovali", + "name": "${LEVEL} Bezchybná výhra" + }, + "Rookie Football Victory": { + "description": "Vyhrajte hru", + "descriptionComplete": "Vyhráli jste hru", + "descriptionFull": "Vyhrajte hru v ${LEVEL}", + "descriptionFullComplete": "Vyhráli jste hru v ${LEVEL}", + "name": "${LEVEL} Vítězství" + }, + "Rookie Onslaught Victory": { + "description": "Dokončete všechny vlny", + "descriptionComplete": "Dokončili jste všechny vlny", + "descriptionFull": "Dokončete všechny vlny v ${LEVEL}", + "descriptionFullComplete": "Dokončili jste všechny vlny v ${LEVEL}", + "name": "${LEVEL} Vítězství" + }, + "Runaround God": { + "description": "Získejte 2000 bodů", + "descriptionComplete": "Získáno 2000 bodů", + "descriptionFull": "Získejte 2000 bodů v ${LEVEL}", + "descriptionFullComplete": "Získáno 2000 bodů v ${LEVEL}", + "name": "${LEVEL} Bůh" + }, + "Runaround Master": { + "description": "Získejte 500 bodů", + "descriptionComplete": "Získáno 500 bodů", + "descriptionFull": "Získejte 500 bodů v ${LEVEL}", + "descriptionFullComplete": "Získáno 500 bodů v ${LEVEL}", + "name": "${LEVEL} Profík" + }, + "Runaround Wizard": { + "description": "Získejte 1000 bodů", + "descriptionComplete": "Získáno 1000 bodů", + "descriptionFull": "Získejte 1000 bodů v ${LEVEL}", + "descriptionFullComplete": "Získáno 1000 bodů v ${LEVEL}", + "name": "${LEVEL} Kouzelník" + }, + "Sharing is Caring": { + "descriptionFull": "Úspěšně sdílet hru s přítelem", + "descriptionFullComplete": "Úspěšně sdílena hra s přítelem", + "name": "Sdílení je laskavé" + }, + "Stayin' Alive": { + "description": "Vyhrajte bez smrti", + "descriptionComplete": "Vyhráli jste bez smrti", + "descriptionFull": "Vyhrajte ${LEVEL} bez smrti", + "descriptionFullComplete": "Vyhráli jste ${LEVEL} bez smrti", + "name": "Zůstaňte naživu" + }, + "Super Mega Punch": { + "description": "Způsobte 100% zranění jedním úderem", + "descriptionComplete": "Způsobili jste 100% zranění jedním úderem", + "descriptionFull": "Způsobte 100% zranění jedním úderem v ${LEVEL}", + "descriptionFullComplete": "Způsobili jste 100% zranění jedním úderem v ${LEVEL}", + "name": "Super Mega Úder" + }, + "Super Punch": { + "description": "Způsobte 50% zranění jedním úderem", + "descriptionComplete": "Způsobili jste 50% zranění jedním úderem", + "descriptionFull": "Způsobte 50% zranění jedním úderem v ${LEVEL}", + "descriptionFullComplete": "Způsobili jste 50% zranění jedním úderem v ${LEVEL}", + "name": "Super Úder" + }, + "TNT Terror": { + "description": "Zabijte 6 padouchů pomocí TNT", + "descriptionComplete": "Zabili jste 6 padouchů pomocí TNT", + "descriptionFull": "Zabijte 6 padouchů pomocí TNT v ${LEVEL}", + "descriptionFullComplete": "Zabili jste 6 padouchů pomocí TNT v ${LEVEL}", + "name": "TNT Terror" + }, + "Team Player": { + "descriptionFull": "Začněte týmovou hru s 4+ hráči", + "descriptionFullComplete": "Začali jste Týmovou hru s 4+ hráči", + "name": "Týmový hráč" + }, + "The Great Wall": { + "description": "Zastavte všechny padouchy", + "descriptionComplete": "Zastavili jste všechny padouchy", + "descriptionFull": "Zastavte všechny padouchy v ${LEVEL}", + "descriptionFullComplete": "Zastavili jste všechny padouchy v ${LEVEL}", + "name": "Velká zeď" + }, + "The Wall": { + "description": "Zastavte všechny padouchy", + "descriptionComplete": "Zastavili jste všechny padouchy", + "descriptionFull": "Zastavte všechny padouchy v ${LEVEL}", + "descriptionFullComplete": "Zastavili jste všechny padouchy v ${LEVEL}", + "name": "Zeď" + }, + "Uber Football Shutout": { + "description": "Vyhrajte bez toho, aby padouši skórovali", + "descriptionComplete": "Vyhráli jste bez toho, aby padouši skórovali", + "descriptionFull": "Vyhrajte ${LEVEL} bez toho, aby padouši skórovali", + "descriptionFullComplete": "Vyhráli jste ${LEVEL} bez toho, aby padouchové skórovali", + "name": "${LEVEL} Bezchybná výhra" + }, + "Uber Football Victory": { + "description": "Vyhrajte hru", + "descriptionComplete": "Vyhráli jste hru", + "descriptionFull": "Vyhrajte hru v ${LEVEL}", + "descriptionFullComplete": "Vyhráli jste hru v ${LEVEL}", + "name": "${LEVEL} Vítězství" + }, + "Uber Onslaught Victory": { + "description": "Poražte všechny vlny", + "descriptionComplete": "Porazili jste všechny vlny", + "descriptionFull": "Poražte všechny vlny v ${LEVEL}", + "descriptionFullComplete": "Porazili jste všechny vlny v ${LEVEL}", + "name": "${LEVEL} Vítězství" + }, + "Uber Runaround Victory": { + "description": "Dokončete všechny vlny", + "descriptionComplete": "Dokončili jste všechny vlny", + "descriptionFull": "Dokončete všechny vlny v ${LEVEL}", + "descriptionFullComplete": "Dokončili jste všechny vlny v ${LEVEL}", + "name": "${LEVEL} Vítězství" + } + }, + "achievementsRemainingText": "Achievementů Zbývá:", + "achievementsText": "Achievementy", + "achievementsUnavailableForOldSeasonsText": "Promiňte, ale detaily úspěchu nejsou pro starší sezóny zpřístupněny.", + "addGameWindow": { + "getMoreGamesText": "Získat Více Her...", + "titleText": "Přidat Hru" + }, + "allowText": "Povolit", + "alreadySignedInText": "Tento účet je používán v jiném zařízení;\npřepněte účet nebo v druhém zařízení hru zavřete, \npoté to zkuste znova.", + "apiVersionErrorText": "Nelze načíst modul ${NAME}; je vytvořen pro verzi api ${VERSION_USED}; je potřeba ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" zapněte pouze když jsou připojeny sluchátka)", + "headRelativeVRAudioText": "Head-Relative VR Audio", + "musicVolumeText": "Hlasitost Hudby", + "soundVolumeText": "Hlasitost Zvuků", + "soundtrackButtonText": "Soundtracky", + "soundtrackDescriptionText": "(přiřaďte vaši vlastní hudbu, která bude hrát při hraní)", + "titleText": "Zvuky" + }, + "autoText": "Automaticky", + "backText": "Zpět", + "banThisPlayerText": "Znemožnit hráči přístup do hry", + "bestOfFinalText": "Nejlepší z ${COUNT} Finální", + "bestOfSeriesText": "Nejlepší z ${COUNT} sérií", + "bestRankText": "Váš nejlepší je #${RANK}", + "bestRatingText": "Vaše nejlepší hodnocení je ${RATING}", + "betaErrorText": "Tato beta-verze již není aktivní; prosím podívejte se na novější verze", + "betaValidateErrorText": "Nelze schválit data. (není připojení k internetu?)", + "betaValidatedText": "Beta Schválená; Bavte se!", + "bombBoldText": "BOMBA", + "bombText": "Bomba", + "boostText": "Zrychlit", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} je nastaven v aplikaci sám.", + "buttonText": "tlačitko", + "canWeDebugText": "Chtěli byste, aby BombSquad automaticky hlásil \nchyby a základní info o používání, vývojáři?\n\nTato data neobsahují žádné osobní informace a napomáhají,\naby hra běžela hladce a bez chyb.", + "cancelText": "Zrušit", + "cantConfigureDeviceText": "Omlouváme se, ale ${DEVICE} není nastavitelné", + "challengeEndedText": "Tato výzva již byla ukončena.", + "chatMuteText": "Ztlumit Chat", + "chatMutedText": "Chat ztlumen", + "chatUnMuteText": "Obnovit chat", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Musíte dokončit\ntento level abyste mohli pokračovat!", + "completionBonusText": "Bonus za dokončení", + "configControllersWindow": { + "configureControllersText": "Nastavit Ovladače", + "configureKeyboard2Text": "Nastavit Klávesnici P2", + "configureKeyboardText": "Nastavit Klávesnici", + "configureMobileText": "Mobilní Zařízení jako Ovladače", + "configureTouchText": "Nastavit Dotykovou obrazovku", + "ps3Text": "PS3 Ovladače", + "titleText": "Ovladače", + "wiimotesText": "Wiimote ovladače", + "xbox360Text": "Xbox 360 Ovladače" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Poznámka: podpora ovladače závisí na zařízení a verzi Androidu", + "pressAnyButtonText": "Stiskněte libovolné tlačítko na ovladači,\n který chcete nastavovat...", + "titleText": "Nastavit Ovladače" + }, + "configGamepadWindow": { + "advancedText": "Pokročilé", + "advancedTitleText": "Pokročilé Nastavení Ovladače", + "analogStickDeadZoneDescriptionText": "(zapněte tohle, jestliže Vaše postava 'klouže' po uvolnění páčky)", + "analogStickDeadZoneText": "Mrtvá Zóna Analogové Páčky", + "appliesToAllText": "(nastaví na všechny ovladače tohoto typu)", + "autoRecalibrateDescriptionText": "(zapněte to, jestliže se Vaše postava nepohybuje plnou rychlostí)", + "autoRecalibrateText": "Automaticky Rekalibrovat Analogovou Páčku", + "axisText": "osa", + "clearText": "smazat", + "dpadText": "dpad", + "extraStartButtonText": "Startovací tlačítko (navíc)", + "ifNothingHappensTryAnalogText": "Jestliže se nic neděje, zkuste místo toho přiřadit analogovou páčku.", + "ifNothingHappensTryDpadText": "Jestliže se nic neděje, zkuste místo toho přiřadit d-pad.", + "ignoreCompletelyDescriptionText": "(zabránit tomuto ovladači ovlivnit buď hru nebo nabídku)", + "ignoreCompletelyText": "Ignorovat úplně", + "ignoredButton1Text": "Ignorované Tlačítko 1", + "ignoredButton2Text": "Ignorované Tlačítko 2", + "ignoredButton3Text": "Ignorované Tlačítko 3", + "ignoredButton4Text": "Ignorováno tlačítko 4", + "ignoredButtonDescriptionText": "(použijte tohle pro zabránění tlačítkům 'home' nebo 'synchronizace' ovlivnění UI)", + "pressAnyAnalogTriggerText": "Stiskněte libovolný analogový spínač...", + "pressAnyButtonOrDpadText": "Stistkněte libovolné tlačítko nebo dpad...", + "pressAnyButtonText": "Stiskněte libovolné tlačítko...", + "pressLeftRightText": "Stistkněte doleva nebo doprava...", + "pressUpDownText": "Stiskněte nahoru nebo dolů...", + "runButton1Text": "Tlačítko Běh 1", + "runButton2Text": "Tlačítko Běh 2", + "runTrigger1Text": "Přepnutí Běhu 1", + "runTrigger2Text": "Přepnutí Běhu 2", + "runTriggerDescriptionText": "(analogové spínače Vám dovolí běhat v různých rychlostech)", + "secondHalfText": "Použijte tohle k nastavení druhé poloviny\n2 ovladačů v 1 zařízení, které se zobrazuje\njako jedno zařízení", + "secondaryEnableText": "Zapnout", + "secondaryText": "Druhý Ovladač", + "startButtonActivatesDefaultDescriptionText": "(vypněte, jestliže Vaše start tlačítko je spíše 'menu' tlačítko)", + "startButtonActivatesDefaultText": "Start Tlačítko Aktivuje Základní Widget", + "titleText": "Nastavení Ovladače", + "twoInOneSetupText": "2-v-1 Ovladač Nastavení", + "uiOnlyDescriptionText": "(zabránit ovladači k připojení do hry)", + "uiOnlyText": "Omezeno k použití menu", + "unassignedButtonsRunText": "Všechna Nenastavená Tlačítka Běh", + "unsetText": "", + "vrReorientButtonText": "Reorientovat VR tlačítko" + }, + "configKeyboardWindow": { + "configuringText": "Nastavuji ${DEVICE}", + "keyboard2NoteText": "Poznámka: většina klávesnic může registrovat jen pár stisklých\nkláves najednou, takže pro druhého hráče na klávesnici může být\nvýhodnější, když má druhou, vlastní klávesnici, kterou bude používat.\nOvšem,i v tomto případě, je stejně potřeba nastavit \nkaždému jiné klávesy." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Velikost Ovládacího Prvku", + "actionsText": "Ovládací Prvky", + "buttonsText": "tlačítka", + "dragControlsText": "< posouvejte ovládacími prvky pro změnu jejich pozice >", + "joystickText": "joystick", + "movementControlScaleText": "Velikost Ovládacího Prvku pro Pohyb", + "movementText": "Pohyb", + "resetText": "Resetovat", + "swipeControlsHiddenText": "Skrýt Přejížděcí Ikony", + "swipeInfoText": "Na 'Přejížděcí' styl ovládání se trochu déle zvyká, ale\nje s ním jednodužší hrát bez koukání se na tlačítka.", + "swipeText": "přejíždění", + "titleText": "Nastavit Dotykovou Obrazovku" + }, + "configureItNowText": "Nastavit teď?", + "configureText": "Nastavit", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "Pro nejlepší výsledky budete potřebovat wifi síť bez lagů.\nLagy na wifi můžete snížit vypnutím některých bezdrátových zařízení,\nhraním blízko Vašeho routeru, a připojením hostitele hry přímo\ndo sítě přes ethernet.", + "explanationText": "Pro použití chytrého telefonu nebo tabletu jako bezdrátový ovladač,nainstalujte si aplikaci ${REMOTE_APP_NAME}.\nLibovolný počet zařízení může by připojeno do hry ${APP_NAME}\npřipojeno přes síť Wi-Fi , a je to zdarma !!", + "forAndroidText": "pro Android:", + "forIOSText": "pro iOS:", + "getItForText": "Získejte ${REMOTE_APP_NAME} pro I OS v App Store,\nnebo pro Android v Google play nebo v Amazon appstore", + "googlePlayText": "Google Play", + "titleText": "Použití Mobilních Zařízení jako Ovladačů" + }, + "continuePurchaseText": "Pokračovat za ${PRICE}?", + "continueText": "Pokračovat", + "controlsText": "Ovládání", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Nepočítá se do celkového žebříčku.", + "activenessInfoText": "Tento násobič se zvyšuje ve dnech kdy hrajete,\na snižuje ve dnech, kdy ne.", + "activityText": "Aktivita", + "campaignText": "Kampaň", + "challengesInfoText": "Vyhrajte ceny za dokončování mini-her.\n\nCeny a obtížnost úrovní se zvyšují vždy,\njakmile je výzva dokončena a snižují se vždy\njakmile výzva vyprší nebo je zahozena.", + "challengesText": "Výzvy", + "currentBestText": "Aktuální Nejepší", + "customText": "Speciální mapy", + "entryFeeText": "Vstupné", + "forfeitConfirmText": "Chcete zahodit tuto výzvu?", + "forfeitNotAllowedYetText": "Tato výzva ještě nemůže být zahozena.", + "forfeitText": "Zahodit", + "multipliersText": "Násobiče", + "nextChallengeText": "Další výzva", + "nextPlayText": "Další hra", + "ofTotalTimeText": "z ${TOTAL}", + "playNowText": "Hrát teď", + "pointsText": "Body", + "powerRankingFinishedSeasonUnrankedText": "(sezóna ukončena bez hodnocení)", + "powerRankingNotInTopText": "(nejste v top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER}bodů", + "powerRankingPointsMultText": "(x ${NUMBER})", + "powerRankingPointsText": "${NUMBER} bodů", + "powerRankingPointsToRankedText": "(${CURRENT} z ${REMAINING} bodů)", + "powerRankingText": "Hodnocení", + "prizesText": "Ceny", + "proMultInfoText": "Hráči s upgradem ${PRO}\nobdrží ${PERCENT}% bonus k bodům.", + "seeMoreText": "Zobrazit více...", + "skipWaitText": "Přeskočit čekání", + "timeRemainingText": "Zbývající Čas", + "toRankedText": "Pro hodnocení", + "totalText": "Celkem", + "tournamentInfoText": "Soutěžte s vysokým skóre\ns ostatními hráči ve Vaší lize.\n\nCeny jsou uělovány hráčům, kteří mají\nnejvíce bodů po ukončení turnaje.", + "welcome1Text": "Vítejte v ${LEAGUE}. Pozici v lize\nmůžete vylepšit pomocí hvězdných hodnocení,\nplněním achievementů a vyhráváním trofejí v turnajích.", + "welcome2Text": "Také můžete získávat kupóny z mnoha aktivit.\nMohou být použity pro odemykání nových postav, map,\nmini-her, pro vstup do turnajů, a dalších.", + "yourPowerRankingText": "Vaše pozice:" + }, + "copyOfText": "${NAME} Kopie", + "createEditPlayerText": "", + "createText": "Vytvořit", + "creditsWindow": { + "additionalAudioArtIdeasText": "Dodatečné Audio, První Artworky, a nápady od ${NAME}", + "additionalMusicFromText": "Dodatečná hudba od ${NAME}", + "allMyFamilyText": "Všem mým přátelům a rodině, kteří pomohli testovat hratelnost", + "codingGraphicsAudioText": "Kódování, Grafika, a Audio od ${NAME}", + "languageTranslationsText": "Překladatelé:", + "legalText": "Práva:", + "publicDomainMusicViaText": "Public-domain hudba přes ${NAME}", + "softwareBasedOnText": "Tento software je založen na části práce ${NAME}", + "songCreditText": "${TITLE} od ${PERFORMER}\nSložil ${COMPOSER}, Aranžér ${ARRANGER}, Publikoval ${PUBLISHER},\nS laskavým svolením ${SOURCE}", + "soundAndMusicText": "Zvuky & Hudba", + "soundsText": "Zvuky (${SOURCE}):", + "specialThanksText": "Zvláštní Poděkování:", + "thanksEspeciallyToText": "Děkuji zejména ${NAME}", + "titleText": "Tvůrci ${APP_NAME}", + "whoeverInventedCoffeeText": "Tomu, kdo vynalezl kávu" + }, + "currentStandingText": "Vaše momentální umístění je #${RANK}", + "customizeText": "Přizpůsobit...", + "deathsTallyText": "${COUNT} - počet smrtí", + "deathsText": "Smrti", + "debugText": "ladění", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Poznámka: je doporučené nastavit Nastavení->Grafika>Textury na \"Vysoká\" při tomto testování.", + "runCPUBenchmarkText": "Spustit CPU Benchmark", + "runGPUBenchmarkText": "Spustit GPU Benchmark", + "runMediaReloadBenchmarkText": "Spustit Media-Reload Benchmark", + "runStressTestText": "Spustit test výdrže", + "stressTestPlayerCountText": "Počet Hráčů", + "stressTestPlaylistDescriptionText": "Playlist Testu Výdrže", + "stressTestPlaylistNameText": "Název Playlistu", + "stressTestPlaylistTypeText": "Typ Playlistu", + "stressTestRoundDurationText": "Délka Trvání Kola", + "stressTestTitleText": "Test Výdrže", + "titleText": "Benchmarky & Testy Výdrže", + "totalReloadTimeText": "Celkový čas znovunačtení: ${TIME} (koukněte do logu pro detaily)" + }, + "defaultGameListNameText": "Výchozí ${PLAYMODE} Playlist", + "defaultNewGameListNameText": "Můj ${PLAYMODE} Playlist", + "deleteText": "Vymaž", + "demoText": "Demo", + "denyText": "Zakázat", + "desktopResText": "Rozlišení Plochy", + "difficultyEasyText": "Lehká", + "difficultyHardOnlyText": "Pouze Těžký Mód", + "difficultyHardText": "Těžká", + "difficultyHardUnlockOnlyText": "Tento level může být odemčen pouze v těžkém módu.\nMyslíte si snad, že na to máte?", + "directBrowserToURLText": "Naveďte prosím svůj webový prohlížeč na následující adresu:", + "disableRemoteAppConnectionsText": "Zablokovat přístup Ovladačům-z-aplikace", + "disableXInputDescriptionText": "Povolí více než 4 ovladače ale nemusí fungovat dobře.", + "disableXInputText": "Vypnout XInput", + "doneText": "Hotovo", + "drawText": "Remíza", + "duplicateText": "Duplikovat", + "editGameListWindow": { + "addGameText": "Přidat\nHru", + "cantOverwriteDefaultText": "Nemohu přepsat výchozí playlist!", + "cantSaveAlreadyExistsText": "Playlist s tímto jménem už existuje!", + "cantSaveEmptyListText": "Nemohu uložit prázdný playlist!", + "editGameText": "Upravit\nHru", + "listNameText": "Název Playlistu", + "nameText": "Jméno", + "removeGameText": "Odstranit\nHru", + "saveText": "Uložit seznam", + "titleText": "Editor Playlistů" + }, + "editProfileWindow": { + "accountProfileInfoText": "Toto je speciální profil se jménem\na ikonou založenou na Vašem účtě.\n\n${ICONS}\n\nVytvořte si vlastní profily pro použití\nrůzných jmen či vlastních ikon.", + "accountProfileText": "(profil účtu)", + "availableText": "Jméno \"${NAME}\" je dostupné.", + "changesNotAffectText": "Poznámka: změny neovlivní postavy, které jsou již ve hře", + "characterText": "postava", + "checkingAvailabilityText": "Kontrola dostupnosti jména \"${NAME}\"...", + "colorText": "barva", + "getMoreCharactersText": "Získat Více Postav...", + "getMoreIconsText": "Získat Více Ikon...", + "globalProfileInfoText": "U globálních herních profilů je garantováno, že Vaše\njméno bude na celém světě unikátní. Včetně vlastních ikon.", + "globalProfileText": "(globální profil)", + "highlightText": "zvýraznění", + "iconText": "ikona", + "localProfileInfoText": "Lokální herní profily nemají žádné ikony a u jejich jmen \nnelze garantovat jejich unikátnost. Přeměnou na globální profil\nsi zajistíte unikátní uživatelské jméno a možnost přidat vlatní ikonu.", + "localProfileText": "(local profile)", + "nameDescriptionText": "Přezdívka", + "nameText": "Jméno", + "randomText": "náhodně", + "titleEditText": "Upravit Profil", + "titleNewText": "Nový Profil", + "unavailableText": "jméno \"${NAME}\" není dostupné; zkuste jiné.", + "upgradeProfileInfoText": "Toto rezervuje Vaše uživatelské jméno celosvětově,\na povolí Vám k němu přiřadit vlastní ikonu.", + "upgradeToGlobalProfileText": "Přeměnit na Globální profil" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Nemůžete odstranit výchozí soundtrack.", + "cantEditDefaultText": "Nelze upravit výchozí soundtrack. Zduplikujte jej nebo vytvořte nový.", + "cantEditWhileConnectedOrInReplayText": "Nelze upravovat soundtracky, když jste připojení k partě nebo při záznamu.", + "cantOverwriteDefaultText": "Nelze přepsat výchozí soundtrack", + "cantSaveAlreadyExistsText": "Soundtrack s tímto jménem už existuje!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Výchozí Soundtrack", + "deleteConfirmText": "Odstranit Soundtrack:\n\n'${NAME}'?", + "deleteText": "Odstranit\nSoundtrack", + "duplicateText": "Duplikovat\nSoundtrack", + "editSoundtrackText": "Editor Soundtracků", + "editText": "Upravit\nSoundtrack", + "fetchingITunesText": "získávám Music App playlisty...", + "musicVolumeZeroWarning": "Varování: hlasitost hudby je nastavena na 0", + "nameText": "Název", + "newSoundtrackNameText": "Můj Soundtrack ${COUNT}", + "newSoundtrackText": "Nový Soundtrack:", + "newText": "Nový\nSoundtrack", + "selectAPlaylistText": "Vybrat Playlist", + "selectASourceText": "Zdroj Hudby", + "testText": "test", + "titleText": "Soundtracky", + "useDefaultGameMusicText": "Výchozí Herní Hudba", + "useITunesPlaylistText": "Music App Playlist", + "useMusicFileText": "Hudební Soubor (mp3, atd.)", + "useMusicFolderText": "Složka s Hudebními Soubory" + }, + "editText": "Upravit", + "endText": "Konec", + "enjoyText": "Užij si to!", + "epicDescriptionFilterText": "${DESCRIPTION} V epickém slow motionu", + "epicNameFilterText": "Epické ${NAME}", + "errorAccessDeniedText": "přístup zamítnut", + "errorOutOfDiskSpaceText": "není místo na disku", + "errorText": "Chyba", + "errorUnknownText": "neznámá chyba", + "exitGameText": "Ukončit ${APP_NAME} ???", + "exportSuccessText": "'${NAME}' úspěšně exportován.", + "externalStorageText": "Externí Úložiště", + "failText": "Fail", + "fatalErrorText": "Ajaj, něco chybí nebo se něco rozbilo.\nZkuste reinstalovat BombSquad\nnebo kontaktujte ${EMAIL} pro pomoc.", + "fileSelectorWindow": { + "titleFileFolderText": "Vyberte Soubor nebo Složku", + "titleFileText": "Vybrat Soubor", + "titleFolderText": "Vybrat Složku", + "useThisFolderButtonText": "Použít Tuto Složku" + }, + "filterText": "Filtr", + "finalScoreText": "Konečné Skóre", + "finalScoresText": "Konečné Výsledky", + "finalTimeText": "Konečný Čas", + "finishingInstallText": "Dokončuji instalaci; chvilí strpení...", + "fireTVRemoteWarningText": "* Pro lepší zkušenosti použijte \novladač nebo nainstalujte aplikaci \n${REMOTE_APP_NAME} na Váš\ntelefon nebo tablet.", + "firstToFinalText": "První-do-${COUNT} Finále", + "firstToSeriesText": "První-do-${COUNT} Série", + "fiveKillText": "PĚT ZABITÍ!!!", + "flawlessWaveText": "Dokonalá vlna!", + "fourKillText": "ČTYŘI ZABITÍ!!!", + "friendScoresUnavailableText": "Skóre přátel není dostupné.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Nejlepší hráči - ${COUNT}. hra", + "gameListWindow": { + "cantDeleteDefaultText": "Nemůžete odstranit výchozí playlist.", + "cantEditDefaultText": "Nelze upravit výchozí playlist! Duplikujte ho nebo vytvořte nový.", + "cantShareDefaultText": "Nemůžete sdílet výchozí playlist.", + "deleteConfirmText": "Odstranit \"${LIST}\" ?", + "deleteText": "Odstranit\nPlaylist", + "duplicateText": "Duplikovat\nPlaylist", + "editText": "Upravit\nPlaylist", + "newText": "Nový\nPlaylist", + "showTutorialText": "Zobrazit Tutorial", + "shuffleGameOrderText": "Náhodné seřazení her", + "titleText": "Přizpůsobit ${TYPE} Playlisty" + }, + "gameSettingsWindow": { + "addGameText": "Přidat Hru" + }, + "gamesToText": "${WINCOUNT} Her k ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Pamatujte: jakékoli zařízení v partě může mít více\nnež jednoho hráče, když máte dostatek ovladačů.", + "aboutDescriptionText": "Použijte tyto záložky pro vytvoření party.\n\nHra s partou vám umožní hrát turnaje\ns Vašimi přáteli mezi různými zařízeními.\n\nPoužijte ${PARTY} Tlačítko v pravo nahoře\npro chat a interakci s Vaší partou.\n(na ovladači stiskněte tlačítko ${BUTTON}, když jste v menu)", + "aboutText": "Použití", + "addressFetchErrorText": "", + "appInviteInfoText": "Pozvěte přátele ke hraní BombSquad a získají\n${COUNT} kupónů zdarma. Vy získáte ${YOU_COUNT} kupónů\nza každého kdo hru vyzkouší.", + "appInviteMessageText": "${NAME} Vám poslal ${COUNT} kupónů v ${APP_NAME}", + "appInviteSendACodeText": "Odeslat jim kód", + "appInviteTitleText": "Pozvat ke hraní ${APP_NAME}", + "bluetoothAndroidSupportText": "(funguje s jakýmkoli Android zařízením podporujícím Bluetooth)", + "bluetoothDescriptionText": "Hostovat/Připojit se k partě přes Bluetooth:", + "bluetoothHostText": "Hostovat přes Bluetooth", + "bluetoothJoinText": "Připojit přes Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "zjišťuji...", + "copyCodeConfirmText": "Kód zkopírován do schránky", + "copyCodeText": "Zkopírovat kód", + "dedicatedServerInfoText": "Pro dosažení nejlepších výsledků nastavte dedikovaný server. Viz bombsquadgame.com/server přečti si to.", + "disconnectClientsText": "Tímto se odpojí ${COUNT} hráč/ů\nve Vaší partě. Jste si jistí?", + "earnTicketsForRecommendingAmountText": "Přátelé získají ${COUNT} tiketů když zkusí tuto hru\n(a ty získáš ${YOU_COUNT} za každého, kdo to udělá)", + "earnTicketsForRecommendingText": "Sdílet hru\nza kupóny zdarma...", + "emailItText": "Odeslat emailem", + "favoritesSaveText": "Uložit jako oblíbený", + "favoritesText": "Oblíbené", + "freeCloudServerAvailableMinutesText": "Další bezplatný server bude dostupný za ${MINUTES} minut.", + "freeCloudServerAvailableNowText": "Bezplatný server je dostupný!", + "freeCloudServerNotAvailableText": "Žádný bezplatný server není dostupný.", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} kupónů od ${NAME}", + "friendPromoCodeAwardText": "Získáte ${COUNT} kupónů pokaždé, jakmile je použit.", + "friendPromoCodeExpireText": "Kód vyprší za ${EXPIRE_HOURS} hodin a je funkční pouze pro nové hráče.", + "friendPromoCodeInstructionsText": "Pro použití otevřete ${APP_NAME} a jděte do ,,Nastavení->Pokročilé->Vložit kód\"\nPodívejte se na bombsquadgame.com na odkazy k stažení na všechny podporované platformy.", + "friendPromoCodeRedeemLongText": "Může z něj být získáno ${COUNT} kupónů zdarma, až pro ${MAX_USES} lidí.", + "friendPromoCodeRedeemShortText": "Může být použit pro získání ${COUNT} kupónů do hry.", + "friendPromoCodeWhereToEnterText": "(v \"Nastavení->Pokročilé->Vložit Kód\")", + "getFriendInviteCodeText": "Získat kód pro pozvání přátel", + "googlePlayDescriptionText": "Pozvěte Google Play hráče do vaší Party:", + "googlePlayInviteText": "Pozvat", + "googlePlayReInviteText": "Máte v partě ${COUNT} Google Play hráčů.\nTi budou odpojeni, pokud odešlete novou pozvánku.\nPozvěte je také, aby se připojili zpět.", + "googlePlaySeeInvitesText": "Zobrazit Pozvánky", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google play verze)", + "hostPublicPartyDescriptionText": "Hostovat veřejnou party", + "hostingUnavailableText": "Hostování není dostupné", + "inDevelopmentWarningText": "Poznámka:\n\nSíťová hra je nová a stále se rozvíjející funkce.\nAktualně je vysoce doporučeno, aby všichni hráči\nbyli na stejné Wi-Fi síti.", + "internetText": "Internet", + "inviteAFriendText": "Že vaši přátelé ještě tuto hru nehrají? Pozvěte je,\naby ji vyzkoušeli, a získají ${COUNT} kupónů zdarma.", + "inviteFriendsText": "Pozvat přátele", + "joinPublicPartyDescriptionText": "Připojit se k veřejné skupině", + "localNetworkDescriptionText": "Připojit se k partě (LAN, Bluetooth, atd.)", + "localNetworkText": "Lokální síť", + "makePartyPrivateText": "Publikovat mojí Party", + "makePartyPublicText": "Publikovat mojí Party", + "manualAddressText": "Adresa", + "manualConnectText": "Připojit", + "manualDescriptionText": "Připojit se k partě pomocí adresy:", + "manualJoinSectionText": "Připojit k adrese", + "manualJoinableFromInternetText": "Jste připojitelní přes internet?:", + "manualJoinableNoWithAsteriskText": "NE*", + "manualJoinableYesText": "ANO", + "manualRouterForwardingText": "*aby to mohlo fungovat, zkuste nastavit forward UDP portu ${PORT} ve vašem routeru, na vaší lokální adresu", + "manualText": "Ručně", + "manualYourAddressFromInternetText": "Vaše adresa z internetu:", + "manualYourLocalAddressText": "Vaše lokální adresa:", + "nearbyText": "Blízké", + "noConnectionText": "<žádné připojení>", + "otherVersionsText": "(ostatní verze)", + "partyCodeText": "Kód party", + "partyInviteAcceptText": "Potvrdit", + "partyInviteDeclineText": "Zamítnout", + "partyInviteGooglePlayExtraText": "(koukněte se do 'Google Play' záložky v okně 'Klubovna')", + "partyInviteIgnoreText": "Ignorovat", + "partyInviteText": "${NAME} Vás pozval\nk připojení se k jeho partě!", + "partyNameText": "Název Party", + "partyServerRunningText": "Server vaší party běží", + "partySizeText": "velikost party", + "partyStatusCheckingText": "Zjištiji stav...", + "partyStatusJoinableText": "Nyní se k vaší Party může kdokoli přidat", + "partyStatusNoConnectionText": "Připojení k serveru selhalo", + "partyStatusNotJoinableText": "K vaší Party se nikdo nemůže přidat z internetu", + "partyStatusNotPublicText": "Vaše párty není veřejná", + "pingText": "ping", + "portText": "Port", + "privatePartyCloudDescriptionText": "Privátní parta běží na dedikovaných serverech; není potřeba nastavovat router.", + "privatePartyHostText": "Hostovat privátní partu", + "privatePartyJoinText": "Připojit se k privátní partě", + "privateText": "Privátní", + "publicHostRouterConfigText": "Může vyžadovat konfiguraci přesměrování portů na vašem routeru. Pro snadnější volbu, hostujte privátní partu.", + "publicText": "Veřejné", + "requestingAPromoCodeText": "Získávám kód...", + "sendDirectInvitesText": "Odeslat přímou pozvánku", + "shareThisCodeWithFriendsText": "Sdílejte tento kód s přáteli:", + "showMyAddressText": "Ukaž moji adresu", + "startHostingPaidText": "Hostovat ihned za ${COST}", + "startHostingText": "Hostovaní", + "startStopHostingMinutesText": "Můžete začít a ukončit bezplatné hostování na dalších ${MINUTES} minut.", + "stopHostingText": "Ukončit hostování", + "titleText": "Klubovna", + "wifiDirectDescriptionBottomText": "Jestližem mají všechna zařízení funkci 'Wi-Fi Direct', je možné ji použít k nalezení\na připojení k sobě navzájem. Jakmile jsou všichni jednou propojeni, můžou tvořit party\npomocí záložky 'Lokální síť', prostě tak, jako kdyby byli v normální Wi-Fi síti.\n\nPro nejlepší výsledky by měl být hostitel Wi-Fi Direct zároveň hostitel ${APP_NAME} party.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct může být použit pro propojení zařízení Android přímo,\nbez potřeby Wi-Fi sítě. Nejlépe to funguje na Android 4.2 a novějších.\n\nPro použítí, otevřete Wi-Fi nastavení, a podívejte se v menu po 'Wi-Fi Direct'.", + "wifiDirectOpenWiFiSettingsText": "Otevřít nastavení Wi-Fi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(funguje mezi všemi platformami)", + "worksWithGooglePlayDevicesText": "(funguje se zařízeními které mají Google Play (android) verzi hry)", + "youHaveBeenSentAPromoCodeText": "Byl vám odeslán ${APP_NAME} promo kód:" + }, + "getTicketsWindow": { + "freeText": "ZDARMA!", + "freeTicketsText": "Kupóny Zdarma!", + "inProgressText": "Probíhá transakce; Prosím zkuste to za chvíli.", + "purchasesRestoredText": "Transakce obnoveny", + "receivedTicketsText": "Obdrženo ${COUNT} kupónů!", + "restorePurchasesText": "Obnovit nákupy", + "ticketDoublerText": "Zdvojnásobovač Kupónů", + "ticketPack1Text": "Malý Balíček Kupónů", + "ticketPack2Text": "Střední Balíček Kupónů", + "ticketPack3Text": "Velký Balíček Kupónů", + "ticketPack4Text": "Sloní Balíček Kupónů", + "ticketPack5Text": "Mamutí Balíček Kupónů!", + "ticketPack6Text": "Ultimátní Balíček Kupónů", + "ticketsFromASponsorText": "Získat ${COUNT} kupónů\nod sponzora", + "ticketsText": "${COUNT} Kupónů", + "titleText": "Získat Kupóny", + "unavailableLinkAccountText": "Omlouváme se, ale nákupy nejsou na této platformě možné.\nJako řešení je, že můžete si tento účet propojit s jiným\nna jiné platformě a uskutečnit nákup tam.", + "unavailableTemporarilyText": "Momentálně nedostupné; Zkuste to prosím později", + "unavailableText": "Omlouváme se, ale není dostupné", + "versionTooOldText": "Omlouváme se, ale tato verze hry je moc stará; aktualizujte prosím na novější", + "youHaveShortText": "Máš ${COUNT}", + "youHaveText": "Máte ${COUNT} kupónů" + }, + "googleMultiplayerDiscontinuedText": "Litujeme, služba pro více hráčů Google již není k dispozici.\n Pracuji na výměně co nejrychleji.\n Do té doby zkuste jiný způsob připojení.\n -Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Vždy", + "fullScreenCmdText": "Celá obrazovka (Cmd-F)", + "fullScreenCtrlText": "Celá obrazovka (Ctrl-F)", + "gammaText": "Gamma", + "highText": "Velká", + "higherText": "Větší", + "lowText": "Malá", + "mediumText": "Střední", + "neverText": "Nikdy", + "resolutionText": "Rozlišení", + "showFPSText": "Zobrazit FPS", + "texturesText": "Textury", + "titleText": "Grafika", + "tvBorderText": "TV Rámeček", + "verticalSyncText": "Vertikální Synchronizace", + "visualsText": "Kvalita zobrazení" + }, + "helpWindow": { + "bombInfoText": "- Bomba -\nSilnější než pěsti, ale\nmůže dojít k sebe-zraněním.\nNejlepší bude, když hodíte bombu\nna nepřítele dříve, než vyprší její čas.", + "canHelpText": "${APP_NAME} může pomoci.", + "controllersInfoText": "Můžete hrát ${APP_NAME} s přáteli přes síť nebo můžete, pokud máte\ndostatek ovladačů, hrát všichni na stejném zařízení. ${APP_NAME}\njich podporuje nepřeberné množství. Navíc můžete použít svoje telefony\njako ovladače pomocí aplikace '${REMOTE_APP_NAME}', která\nje zdarma. Podívejte se do Nastavení->Ovladače pro více informací.", + "controllersText": "Ovladače", + "controlsSubtitleText": "Vaše přátelská ${APP_NAME} postava má pár základních schopností:", + "controlsText": "Ovládání", + "devicesInfoText": "VR verze ${APP_NAME} může být hrána přes síť s normální\nverzí. Tak vyndejte telefony, tablety a počítače co máte\nnavíc a zapněte svou hru. Propojení normální verze k VR\nverzi může být užitečné kvůli povolení pozorování akce\nlidem zvenčí.", + "devicesText": "Zařízení", + "friendsGoodText": "Je dobré je mít. ${APP_NAME} je největší zábava s více hráči\na podporuje jich až 8 najednou, což nás přivádí k:", + "friendsText": "Přátelé", + "jumpInfoText": "- Skok -\nSkákejte přes malé mezery,\nházejte věci výše nebo si skočte\njen tak z radosti.", + "orPunchingSomethingText": "Nebo do něčeho udeřit, hodit to z útesu nebo to odpálit lepivou bombou", + "pickUpInfoText": "- Zvedání -\nBrát vlajky, nepřátele, nebo cokoli\njiného nepřibitého k zemi.\nStiskněte znovu pro házení.", + "powerupBombDescriptionText": "Dovolí vám vyhodit tři bomby\nza sebou místo pouhé jedné.", + "powerupBombNameText": "Trojité-Bomby", + "powerupCurseDescriptionText": "Pravděpodobně se jim chcete vyhnout.\n ...nebo nechcete?", + "powerupCurseNameText": "Prokletí", + "powerupHealthDescriptionText": "Obnoví veškeré zdraví\nO tom se Vám ani nesnilo.", + "powerupHealthNameText": "Lékárnička", + "powerupIceBombsDescriptionText": "Slabší než normální bomby,\ntvé nepřátele však zmrazí\na učiní je křehkými.", + "powerupIceBombsNameText": "Ledové-Bomby", + "powerupImpactBombsDescriptionText": "Trochu slabší než normální bomby,\nale explodují při nárazu.", + "powerupImpactBombsNameText": "Nárazové-Bomby", + "powerupLandMinesDescriptionText": "Jsou v balíčku po 3;\nUžitečné pro základní obranu\nnebo zastavování rychlých nepřátel", + "powerupLandMinesNameText": "Pozemní-Miny", + "powerupPunchDescriptionText": "Udělá tvé pěsti tvrdšími,\nrychlejšími, lepšími, silnějšími.", + "powerupPunchNameText": "Boxovací-Rukavice", + "powerupShieldDescriptionText": "Absorbuje trochu zranění,\ntakže Vám se nic moc nestane.", + "powerupShieldNameText": "Energetický-Štít", + "powerupStickyBombsDescriptionText": "Přilepí se k čemukoli co trefí.\nZábava zaručena.", + "powerupStickyBombsNameText": "Lepící-Bomby", + "powerupsSubtitleText": "Samozřejmě, žádná hra není hotová bez bonusů:", + "powerupsText": "Bonusy", + "punchInfoText": "- Pěsti -\nPěsti zraní více,\nkdyž se rychle pohybují.\nTakže běhejte a točte se jako šílenci.", + "runInfoText": "- Sprint -\nDržením libovolného tlačítka sprintujte. Pokud máte ovladač tak fungují i ostatní tlačítka.\nSprintování Vás značně zrychlí ale ztíží vám pohyb, takže si dávejte pozor na okraje mapy.", + "someDaysText": "Některé dny prostě cítíte, že potřebujete něco praštit. Nebo něco vyhodit do vzduchu.", + "titleText": "${APP_NAME} Nápověda", + "toGetTheMostText": "Abyste si hru nejvíce užili, budete potřebovat:", + "welcomeText": "Vítejte v ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} obsluhuje menu like a boss -", + "importPlaylistCodeInstructionsText": "Použijte následující kó pro importování kdekoliv jinde:", + "importPlaylistSuccessText": "Úspěšně importován ${TYPE} playlist „'${NAME}'“", + "importText": "Importovat", + "importingText": "Probíhá importování...", + "inGameClippedNameText": "Ve hře bude vidět jako\n\"${NAME}\"", + "installDiskSpaceErrorText": "CHYBA: Není možné dokončit instalaci.\nMožná nemáte dostatek volného místa na Vašem zařízení.\nUvolněte nějaké místo, a zkuste to znovu.", + "internal": { + "arrowsToExitListText": "stiskněte ${LEFT} nebo ${RIGHT} pro opuštění seznamu", + "buttonText": "tlačítko", + "cantKickHostError": "Nemůžete kopat hostitele.", + "chatBlockedText": "${NAME} chat je blokován na ${TIME} sekund.", + "connectedToGameText": "Připojen '${NAME}'", + "connectedToPartyText": "Připojen k ${NAME} partě!", + "connectingToPartyText": "Připojuji...", + "connectionFailedHostAlreadyInPartyText": "Připojení selhalo; Hostitel je v jiné partě.", + "connectionFailedPartyFullText": "Připojení se nezdařilo; parta je plná.", + "connectionFailedText": "Připojení selhalo.", + "connectionFailedVersionMismatchText": "Připojení selhalo; Hostitel používá jinou verzi hry.\nUjistěte se, že máte oba aktualizovanou verzi, a zkuste to znovu.", + "connectionRejectedText": "Připojení odmítnuto.", + "controllerConnectedText": "${CONTROLLER} připojen.", + "controllerDetectedText": "1 nalezený ovladač.", + "controllerDisconnectedText": "${CONTROLLER} odpojen.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} odpojen. Prosím, zkuste ho připojit znovu.", + "controllerForMenusOnlyText": "Tento ovladač nemůže být použit pro hraní; pouze k ovládání v nabídce", + "controllerReconnectedText": "${CONTROLLER} znovu připojen.", + "controllersConnectedText": "${COUNT} - ovladačů připojeno.", + "controllersDetectedText": "${COUNT} - ovladačů nalezeno.", + "controllersDisconnectedText": "${COUNT} - ovladačů odpojeno.", + "corruptFileText": "Nalezen jeden nebo více poškozených souborů. Zkuste prosím reinstalaci, nebo napište email na ${EMAIL}", + "errorPlayingMusicText": "Chyba při přehrávání hudby: ${MUSIC}", + "errorResettingAchievementsText": "Nebylo možné resetovat online achievementy; zkuste to prosím znovu později", + "hasMenuControlText": "${NAME} ovládá menu.", + "incompatibleNewerVersionHostText": "Hostitel serveru má novější verzi než vy, \npro připojení hru aktualizujte.", + "incompatibleVersionHostText": "Hostitel používá jinou verzi hry.\nUjistěte se, že oba používáte aktualizovanou verzi, a zkuste to znovu", + "incompatibleVersionPlayerText": "${NAME} používá jinou verzi hry.\nUjistěte se, že oba použváte aktualizovanou verzi, a zkuste to znovu.", + "invalidAddressErrorText": "Chyba: Neplatná adresa.", + "invalidNameErrorText": "Chyba: Neplatné jméno.", + "invalidPortErrorText": "Chyba: neplatný port.", + "invitationSentText": "Pozvánka odeslána.", + "invitationsSentText": "${COUNT} - pozvánek odesláno.", + "joinedPartyInstructionsText": "Někdo se připojil do tvojí party. \nStiskni 'Hrát' pro start hry", + "keyboardText": "Klávesnice", + "kickIdlePlayersKickedText": "${NAME} vyhozen kvůli neaktivitě.", + "kickIdlePlayersWarning1Text": "${NAME} bude vyhozen do ${COUNT} sekund, pokud bude stále neaktivní.", + "kickIdlePlayersWarning2Text": "(můžete to vypnout v Nastavení -> Pokročilé)", + "leftGameText": "Odpojen '${NAME}'.", + "leftPartyText": "Opustili jste ${NAME} partu.", + "noMusicFilesInFolderText": "Složka neobsahuje žádnou hudbu.", + "playerJoinedPartyText": "${NAME} se připojil do party!", + "playerLeftPartyText": "${NAME} opustil partu.", + "rejectingInviteAlreadyInPartyText": "Odmítnutí pozvánky (už jste v partě).", + "serverRestartingText": "Server se restartuje. Vraťte se za chvíli...", + "serverShuttingDownText": "Server se vypíná...", + "signInErrorText": "Chyba přihlašování.", + "signInNoConnectionText": "Přihlášení selhalo. (žádné internetové připojení)", + "telnetAccessDeniedText": "CHYBA: uživatel nemá povolený přístup k telnetu.", + "timeOutText": "(vyprší za ${TIME} sekund)", + "touchScreenJoinWarningText": "Připojili jste se s dotykovou obrazovkou.\nJestli je to chyba, klepněte na 'Menu->Opustit hru'.", + "touchScreenText": "Dotyková Obrazovka", + "unableToResolveHostText": "Chyba: Nezdařilo se spojit s hostitelem (IP adresa možná neexistuje)", + "unavailableNoConnectionText": "Toto je momentálně nedostupné (bez internetového připojení?)", + "vrOrientationResetCardboardText": "Použitjte toto pro reset orientace VR.\nPro hraní hry budete potřebovat externí ovladač.", + "vrOrientationResetText": "VR resetování orientace.", + "willTimeOutText": "(vyprší čas, když je neaktivní)" + }, + "jumpBoldText": "SKOK", + "jumpText": "Skok", + "keepText": "Zachovat", + "keepTheseSettingsText": "Zachovat tato nastavení?", + "keyboardChangeInstructionsText": "Dvakrát stiskni mezerník pro změnu klávesnice.", + "keyboardNoOthersAvailableText": "Žádné další klávesnice nejsou dostupné.", + "keyboardSwitchText": "Změna klávesnice na \"${NAME}\".", + "kickOccurredText": "${NAME} byl vykopnut.", + "kickQuestionText": "Kopnout ${NAME}?", + "kickText": "kop", + "kickVoteCantKickAdminsText": "Admin nemůže být vyhozen.", + "kickVoteCantKickSelfText": "Nemůžeš vyhodit sám sebe.", + "kickVoteFailedNotEnoughVotersText": "Není dostatek hráčů pro hlasování.", + "kickVoteFailedText": "Kopací-hlasování se nezdařilo.", + "kickVoteStartedText": "Kopací hlasování bylo zahájeno pro ${NAME}.", + "kickVoteText": "Hlasovat pro Kopnutí", + "kickVotingDisabledText": "Hlasování pro vyhození je vypnuto.", + "kickWithChatText": "Typ ${YES} v chatu pro ano a ${NO} pro ne.", + "killsTallyText": "${COUNT} zabití", + "killsText": "Zabití", + "kioskWindow": { + "easyText": "Lehké", + "epicModeText": "Epický Mód", + "fullMenuText": "Celé Menu", + "hardText": "Těžké", + "mediumText": "Střední", + "singlePlayerExamplesText": "Sólo Hra / Co-op Ukázky", + "versusExamplesText": "Versus Ukázky" + }, + "languageSetText": "Jazyk je nastaven na \"${LANGUAGE}\".", + "lapNumberText": "Kolo ${CURRENT}/${TOTAL}", + "lastGamesText": "(posledních ${COUNT} her)", + "leaderboardsText": "Žebříčky", + "league": { + "allTimeText": "Celkově", + "currentSeasonText": "Tato Sezóna (${NUMBER})", + "leagueFullText": "${NAME} Liga", + "leagueRankText": "Liga - Umístění", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga${SUFFIX}", + "seasonEndedDaysAgoText": "Sezóna ukončena před ${NUMBER} dny.", + "seasonEndsDaysText": "Sezóna končí za ${NUMBER} dní.", + "seasonEndsHoursText": "Sezóna končí za ${NUMBER} hodin.", + "seasonEndsMinutesText": "Sezóna končí za ${NUMBER} minut.", + "seasonText": "Sezóna ${NUMBER}", + "tournamentLeagueText": "Musíte být v lize ${NAME}, abyste se mohli zúčastnit tohoto turnaje.", + "trophyCountsResetText": "Počet trofejí se vymaže příští sezónu." + }, + "levelBestScoresText": "Nejlepší skóre na ${LEVEL}", + "levelBestTimesText": "Nejlepší čas na ${LEVEL}", + "levelFastestTimesText": "Nejrychleji v ${LEVEL}", + "levelHighestScoresText": "Nejvyšší skóre v ${LEVEL}", + "levelIsLockedText": "${LEVEL} je uzamčen.", + "levelMustBeCompletedFirstText": "${LEVEL} musí být nejdříve dokončen.", + "levelText": "Úroveň ${NUMBER}", + "levelUnlockedText": "Level odemčen!", + "livesBonusText": "Bonus života", + "loadingText": "načítám", + "loadingTryAgainText": "Načítání; zkuste to znovu za chvíli...", + "macControllerSubsystemBothText": "Obojí (může způsobovat chyby)", + "macControllerSubsystemClassicText": "Klasický", + "macControllerSubsystemDescriptionText": "(zkus toto změnit pokud vám blbnou ovladače)", + "macControllerSubsystemMFiNoteText": "Ddetekován IOS/Mac ovladač;\nPokud jej chcete použít, povolte je v Nastvení > Ovladače", + "macControllerSubsystemMFiText": "iOS/Mac ovladače", + "macControllerSubsystemTitleText": "Podporované ovladače", + "mainMenu": { + "creditsText": "Tvůrci", + "demoMenuText": "Demo menu", + "endGameText": "Konec Hry", + "exitGameText": "Ukončit Hru", + "exitToMenuText": "Vrátit se do menu?", + "howToPlayText": "Jak hrát", + "justPlayerText": "(Jen ${NAME})", + "leaveGameText": "Opustit Hru", + "leavePartyConfirmText": "Opravdu opustit partu?", + "leavePartyText": "Opustit partu", + "quitText": "Konec", + "resumeText": "Pokračovat", + "settingsText": "Nastavení" + }, + "makeItSoText": "Potvrdit", + "mapSelectGetMoreMapsText": "Získat více map...", + "mapSelectText": "Vybrat...", + "mapSelectTitleText": "${GAME} Mapy", + "mapText": "Mapa", + "maxConnectionsText": "Max připojitelných hráčů", + "maxPartySizeText": "Maximální velikost party", + "maxPlayersText": "Max hráčů", + "modeArcadeText": "Arkádový mód", + "modeClassicText": "Klasický mód", + "modeDemoText": "Ukázkový mód", + "mostValuablePlayerText": "Nejcennější hráč", + "mostViolatedPlayerText": "Nejvíce obětovaný hráč", + "mostViolentPlayerText": "Nejvíce násilný hráč", + "moveText": "Pohyb", + "multiKillText": "${COUNT}-ZABITÍ!!!", + "multiPlayerCountText": "${COUNT} - počet hráčů", + "mustInviteFriendsText": "Poznámka: Musíte pozvat přátele\npomocí tlačítka \"${GATHER}\", nebo\npžipojit ovladače pro hraní s více hráči.", + "nameBetrayedText": "${NAME} zradil ${VICTIM}", + "nameDiedText": "${NAME} zemřel.", + "nameKilledText": "${NAME} zabil ${VICTIM}.", + "nameNotEmptyText": "Jméno nemůže být prázdné!", + "nameScoresText": "${NAME} Skóruje!", + "nameSuicideKidFriendlyText": "${NAME} zemřel nešťastnou náhodou.", + "nameSuicideText": "${NAME} spáchal sebevraždu.", + "nameText": "Jméno", + "nativeText": "Nativní", + "newPersonalBestText": "Nový osobní rekord!", + "newTestBuildAvailableText": "Novější testovací build je dostupný! (${VERSION} build ${BUILD}).\nZískejte ho na ${ADDRESS}", + "newText": "Nový", + "newVersionAvailableText": "Je dostupná novější verze ${APP_NAME}! (${VERSION})", + "nextAchievementsText": "Další ocenění:", + "nextLevelText": "Další Level", + "noAchievementsRemainingText": "- žádný", + "noContinuesText": "(bez pokračování)", + "noExternalStorageErrorText": "Žádné externí úložiště nebylo na tomto zařízení nalezeno", + "noGameCircleText": "Chyba: nejste přihlášeni do Game Circle", + "noProfilesErrorText": "Nemáte žádné herní profily, takže Vám bylo podstrčeno jméno '${NAME}'.\nJděte do Nastavení->Herní Profily pro vytvoření svého profilu.", + "noScoresYetText": "Zatím žádné výsledky.", + "noThanksText": "Ne, Děkuji", + "noTournamentsInTestBuildText": "VAROVÁNÍ: Skore z turnajů z tohoto účtu budou ignorována.", + "noValidMapsErrorText": "Nebyly nalezeny žádné platné mapy pro tento typ hry.", + "notEnoughPlayersRemainingText": "Nezbývá dostatek hráčů; ukončete a zapněte novou hru", + "notEnoughPlayersText": "Potřebujete nejméně ${COUNT} hráčů pro spuštění této hry!", + "notNowText": "Teď Ne", + "notSignedInErrorText": "Pro tuto akci se musíte přihlásit", + "notSignedInGooglePlayErrorText": "Pro tuto akci se musíte přihlásit přes Google Play", + "notSignedInText": "nepřihlášen", + "nothingIsSelectedErrorText": "Nic není vybráno!", + "numberText": "#${NUMBER}", + "offText": "Vyp", + "okText": "Ok", + "onText": "Zap", + "oneMomentText": "Chvilku strpení...", + "onslaughtRespawnText": "${PLAYER} se oživí ve vlně ${WAVE}", + "orText": "${A} nebo ${B}", + "otherText": "Ostatní...", + "outOfText": "(#${RANK} z ${ALL})", + "ownFlagAtYourBaseWarning": "Vaše vlajka musí být\nna vaší základně, abyste mohli skórovat!", + "packageModsEnabledErrorText": "Hra po síti není povolena pokud jsou zapnuty lokální-balíčky-modů (Koukněte se do Nastavení->Pokročilé)", + "partyWindow": { + "chatMessageText": "Zpráva do chatu", + "emptyText": "Vaše parta nemá žádného člena", + "hostText": "(hostitel)", + "sendText": "Odeslat", + "titleText": "Vaše Parta" + }, + "pausedByHostText": "(pozastaveno hostitelem)", + "perfectWaveText": "Perfektní Vlna!", + "pickUpText": "Zvednout", + "playModes": { + "coopText": "Co-op", + "freeForAllText": "Všichni-proti-Všem", + "multiTeamText": "Multi-Team", + "singlePlayerCoopText": "Pro jednoho hráče / Co-op", + "teamsText": "Týmy" + }, + "playText": "Hrát", + "playWindow": { + "oneToFourPlayersText": "1-4 hráči", + "titleText": "Hrát", + "twoToEightPlayersText": "2-8 hráčů" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerDelayedJoinText": "${PLAYER} vstoupí na začátku dalšího kola.", + "playerInfoText": "Info o hráči", + "playerLeftText": "${PLAYER} opustil hru.", + "playerLimitReachedText": "Limit hráčů (${COUNT}) byl dosažen; žádní ostatní připojovaní nejsou povoleni.", + "playerLimitReachedUnlockProText": "Upgradujte v obchodě na \"${PRO}\", abyste mohli hrát s více než ${COUNT} hráči.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Nemůžete odstranit Váš profil s účtem.", + "deleteButtonText": "Odstranit\nProfil", + "deleteConfirmText": "Odstranit '${PROFILE}'?", + "editButtonText": "Upravit\nProfil", + "explanationText": "(různé přezdívky a vzhledy pro hráče na tomto účtu)", + "newButtonText": "Nový\nProfil", + "titleText": "Herní Profily" + }, + "playerText": "Hráč", + "playlistNoValidGamesErrorText": "Tento playlist neobsahuje žádné platné odemčené hry.", + "playlistNotFoundText": "playlist nenalezen", + "playlistText": "Playlist", + "playlistsText": "Playlisty", + "pleaseRateText": "Jestliže Vás ${APP_NAME} baví, zvažte prosím, jestli si nechcete udělat\nchvilku na ohodnocení nebo napsání recenze. Poskytuje to užitečnou\nzpětnou vazbu a pomáhá podporovat budoucí vývoj.\n\nDíky!\n-eric", + "pleaseWaitText": "Prosím čekejte...", + "pluginsDetectedText": "Nové plugin(y) nalezeny. Zapněte/konfigurujte je v nastavení.", + "pluginsText": "Pluginy", + "practiceText": "Cvičení", + "pressAnyButtonPlayAgainText": "Stiskněte libovolné tlačítko pro opakování hry...", + "pressAnyButtonText": "Stisktněte libovolné tlačítko pro pokračování...", + "pressAnyButtonToJoinText": "stiskněte libovolné tlačítko pro připojení...", + "pressAnyKeyButtonPlayAgainText": "Stiskněte libovolnou klávesu/tlačítko pro opakování hry...", + "pressAnyKeyButtonText": "Stiskněte libovolnou klávesu/tlačítko pro pokračování...", + "pressAnyKeyText": "Stiskněte libovolnou klávesu...", + "pressJumpToFlyText": "** Stiskněte opakovaně skok pro létání **", + "pressPunchToJoinText": "stiskněte PRAŠTIT pro připojení...", + "pressToOverrideCharacterText": "stiskněte ${BUTTONS} pro nahrazení vaší postavy", + "pressToSelectProfileText": "Stiskněte ${BUTTONS} pro zvolení hráče", + "pressToSelectTeamText": "stiskněte ${BUTTONS} pro vybrání týmu", + "promoCodeWindow": { + "codeText": "Kód", + "codeTextDescription": "Promo Kód", + "enterText": "Vložit" + }, + "promoSubmitErrorText": "Chyba při odesílání kódu; zkontrolujte internetové připojení", + "ps3ControllersWindow": { + "macInstructionsText": "Vypněte na zadní straně své PS3, ujistěte se, že\nje na Vašem Mac zaplý Bluetooth, a poté připojte Váš ovladač\nk vašemu Mac přes USB kabel, abyste je mohli spárovat. Odteď\nmůžete použít na ovladači tlačítko home (domů) pro připojení k Vašemu\nMacu ať už v kabelovém (USB) nebo bezdrátovém (Bluetooth) módu.\n\nNa některých Mac zařízeních můžete být při párování vyzváni k zadání passcode.\nJestliže se tohle stane, podívejte se na následující tutorial, nebo zkuste hledat na google.\n\n\n\n\nOvladače PS3, připojené bezdrátově, by se měly zobrazit v seznamu v\nSystémová nastavení->Bluetooth. Možná je budete potřebovat odstranit, pokud \nje budete chtít znovu použít s Vaším PS3.\n\nTaké se ujistěte, že je odpojíte od Bluetooth když nejsou používané,\nprotože jinak se jejich baterie budou stále vybíjet.\n\nBluetooth by měl zvládnout až 7 připojených zařízení,\nale to se může lišit.", + "ouyaInstructionsText": "Pro použití ovladače s Vaším OUYA, ho prostě jednou připojte přes USB kabel\nkvůli spárování. Ovšem při tomto kroku se mohou odpojit ostatní ovladače,\ntakže byste měli restartovat Vaši OUYA a odpojit USB kabel.\n\nOdteď budete schopni použít tlačítko HOME na ovladači k jeho připojení\nbezdrátově. Poté co dohrajete, držte tlačítko HOME 10 sekund, aby se\novladač vypl; jinak může zůstat zaplý\na vybíjet baterie.", + "pairingTutorialText": "video tutorial o párování", + "titleText": "Používání PS3 Ovladače s ${APP_NAME}:" + }, + "punchBoldText": "PRAŠTIT", + "punchText": "Praštit", + "purchaseForText": "Koupit za ${PRICE}", + "purchaseGameText": "Koupit Hru", + "purchasingText": "Probíhá transakce...", + "quitGameText": "Ukončit ${APP_NAME}?", + "quittingIn5SecondsText": "Ukončuji za 5 sekund...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Náhodně", + "rankText": "Rank", + "ratingText": "Hodnocení", + "reachWave2Text": "Pro hodnocení se dostaňte do vlny 2.", + "readyText": "připraven", + "recentText": "Poslední", + "remoteAppInfoShortText": "${APP_NAME} je zábavnější hrát s rodinou & přáteli.\nPřipoj jedno nebo více hardwerových ovladačů nebo \nnainstaluj ${REMOTE_APP_NAME} na telefon nebo tablet \nk jejich použití jako ovladač.", + "remote_app": { + "app_name": "BombSquad Ovladač", + "app_name_short": "BSOvladač", + "button_position": "Pozice tlačítek", + "button_size": "Velikost tlačítek", + "cant_resolve_host": "Hostitel nebyl nalezen.", + "capturing": "Zachytávám...", + "connected": "Připojen.", + "description": "Použijte Váš telefon či tablet jako ovladač pro BombSquad.\nNajednou lze připojit až 8 zařízení k lokálnímu multiplayerovému šílenství na televizi či tabletu.", + "disconnected": "Odpojen serverem.", + "dpad_fixed": "statický", + "dpad_floating": "plovoucí", + "dpad_position": "Pozice D-Pad", + "dpad_size": "Velikost D-Pad", + "dpad_type": "Typ D-Pad", + "enter_an_address": "Zadejte adresu", + "game_full": "Hra je již zaplněna hráči nebo nepřijímá připojení.", + "game_shut_down": "Hra byla vypnuta.", + "hardware_buttons": "Hardwarová tlačítka", + "join_by_address": "Připojit se k adrese...", + "lag": "Prodleva: ${SECONDS} sekund", + "reset": "Navrátit základní", + "run1": "Běh 1", + "run2": "Běh 2", + "searching": "Probíhá hledání aktivních her BombSquad...", + "searching_caption": "Pro připojení se dotkněte jména hry.\nUjistěte se, že jste na stejné síti wifi.", + "start": "Start", + "version_mismatch": "Neshoda verzí.\nUjistěte se, že BombSquad a BombSquad Ovladač\njsou aktualizované na poslední verzi, a zkuste to znovu." + }, + "removeInGameAdsText": "Odemkněte \"${PRO}\" v obchodě pro odstranění reklam ve hře.", + "renameText": "Přejmenovat", + "replayEndText": "Ukončit Záznam", + "replayNameDefaultText": "Záznam Poslední Hry", + "replayReadErrorText": "Chyba při čtení souboru záznamu.", + "replayRenameWarningText": "Přejmenujte \"${REPLAY}\" po odehrané hře, pokud ho chcete zachovat; jinak bude přepsán", + "replayVersionErrorText": "Omlouváme se, ale tento záznam byl vytvořen\nv jiné verzi hry a nemůže být přehrán.", + "replayWatchText": "Podívat se na Záznam", + "replayWriteErrorText": "Chyba při zápisu souboru záznamu.", + "replaysText": "Záznamy", + "reportPlayerExplanationText": "Použijte tento email pro nahlášení podvádění, nevhodného vyjadřování či jiného špatného chování.\nProsím popište níže:", + "reportThisPlayerCheatingText": "Podvádění", + "reportThisPlayerLanguageText": "Nevhodné vyjadřování", + "reportThisPlayerReasonText": "Proč chcete tohoto hráče nahlásit?", + "reportThisPlayerText": "Nahlásit tohoto hráče", + "requestingText": "Vyžaduje se...", + "restartText": "Restartovat", + "retryText": "Zkusit znovu", + "revertText": "Navrátit", + "runText": "Běh", + "saveText": "Uložit", + "scanScriptsErrorText": "Chyba při skenování skriptů; shlédni záznam pro více informací.", + "scoreChallengesText": "Skóre Výzev", + "scoreListUnavailableText": "Seznam skóre nedostupný", + "scoreText": "Skóre", + "scoreUnits": { + "millisecondsText": "Milisekund", + "pointsText": "Bodů", + "secondsText": "Sekund" + }, + "scoreWasText": "(předchozí ${COUNT})", + "selectText": "Zvolit", + "seriesWinLine1PlayerText": "VYHRÁVÁ", + "seriesWinLine1TeamText": "VYHRÁVÁ", + "seriesWinLine1Text": "VYHRÁVÁ", + "seriesWinLine2Text": "SÉRII!", + "settingsWindow": { + "accountText": "Účet", + "advancedText": "Pokročilé", + "audioText": "Zvuk", + "controllersText": "Ovladače", + "graphicsText": "Grafika", + "playerProfilesMovedText": "Poznámka: profily hráče byly přesunuty do okna účtů v hlavním menu.", + "playerProfilesText": "Herní Profily", + "titleText": "Nastavení" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(jednoduchá, ovládáním přátelská, klávesnice na obrazovce pro úpravu textu)", + "alwaysUseInternalKeyboardText": "Vždy Použít Interní klávesnici", + "benchmarksText": "Benchmarky a Testy výdrže", + "disableCameraGyroscopeMotionText": "Vypnout gyroskopický pohyb kamery", + "disableCameraShakeText": "Vypnout otřes kamery", + "disableThisNotice": "(můžete si toto oznámení vypnout v pokročilých nastaveních)", + "enablePackageModsDescriptionText": "(zapíná extra modovací možnosti, ale vypíná hru po síti)", + "enablePackageModsText": "Zapnout Lokální Balíčky Módů", + "enterPromoCodeText": "Zadat kód", + "forTestingText": "Poznámka: Tyto hodnoty sou pouze pro test. Obnoví se po restartu.", + "helpTranslateText": "Jiné než anglické verze ${APP_NAME} jsou komunitně\npodporovanou záležitostí. Pokud byste chtěli přidat\nnebo opravit překlad, následujte odkaz níže. Předem děkujeme!", + "kickIdlePlayersText": "Vyhazovat Neaktivní Hráče", + "kidFriendlyModeText": "Dětský Mód (sníženo násilí, atd.)", + "languageText": "Jazyk", + "moddingGuideText": "Příručka módů", + "mustRestartText": "Musíte restartovat hru, aby se změny projevily.", + "netTestingText": "Testování Sítě", + "resetText": "Obnovit", + "showBombTrajectoriesText": "Ukázat trajektorii bomb", + "showPlayerNamesText": "Jména hráčů", + "showUserModsText": "Zobrazit Složku s Módy", + "titleText": "Pokročilé", + "translationEditorButtonText": "${APP_NAME} Editor Překladu", + "translationFetchErrorText": "stav překladu nedostupný", + "translationFetchingStatusText": "zjišťuji stav překladu...", + "translationInformMe": "Oznamte mi když bude můj jazyk potřebovat aktualizaci", + "translationNoUpdateNeededText": "tento jazyk je aktuální; woohoo!", + "translationUpdateNeededText": "** jazyk potřebuje aktualizovat!! **", + "vrTestingText": "VR Test" + }, + "shareText": "Sdílet", + "sharingText": "Sdílení...", + "showText": "Zobrazit", + "signInForPromoCodeText": "Musíte se přihlásit k účtu, aby mohly kódy fungovat.", + "signInWithGameCenterText": "Pro použití účtu Game Center se přihlaste\ns pomocí aplikace Game Center.", + "singleGamePlaylistNameText": "Jen ${GAME}", + "singlePlayerCountText": "1 Hráč", + "soloNameFilterText": "Sólo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Vybírání Postavy", + "Chosen One": "Vyvolený", + "Epic": "Hry v Epickém Módu", + "Epic Race": "Epický závod", + "FlagCatcher": "Seberte vlajku", + "Flying": "Veselé Myšlenky", + "Football": "Ragby", + "ForwardMarch": "Přepadení", + "GrandRomp": "Dobývání", + "Hockey": "Hokej", + "Keep Away": "Držte se Dál", + "Marching": "Obrana", + "Menu": "Hlavní menu", + "Onslaught": "Útok", + "Race": "Závod", + "Scary": "Král Kopce", + "Scores": "Obrazovka Skóre", + "Survival": "Zneškodnění", + "ToTheDeath": "Zápas Smrti", + "Victory": "Obrazovka Konečného Skóre" + }, + "spaceKeyText": "mezera", + "statsText": "Statistiky", + "storagePermissionAccessText": "Tohle vyžaduje přístup k úložišti", + "store": { + "alreadyOwnText": "Už vlastníš ${NAME}!", + "bombSquadProDescriptionText": "• Zdvojnásobuje kupóny získané za achievementy\n• Odstraňuje reklamy ve hře\n• Obsahuje ${COUNT} bonusových kupónů\n• +${PERCENT}% bonus ke skóre ligy\n• Odemyká '${INF_ONSLAUGHT}' a \n '${INF_RUNAROUND}' co-op mapy", + "bombSquadProFeaturesText": "Vylepšení:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Odstraní reklamy ve hře\n• Odemkne další herní nastavení\n• A k tomu:", + "buyText": "Koupit", + "charactersText": "Postavy", + "comingSoonText": "Již brzy...", + "extrasText": "Bonusy", + "freeBombSquadProText": "BombSquad je nyní zdarma, ale pokud jste si ho dříve zakoupili,\ndostáváte BombSquad Pro upgrade a ${COUNT} kupónů jako poděkování.\nUžijte si nové možnosti a děkujeme za vaši podporu!\n-Eric", + "gameUpgradesText": "Vylepšení hry", + "holidaySpecialText": "Vánoční speciál", + "howToSwitchCharactersText": "(jděte do \"${SETTINGS} -> ${PLAYER_PROFILES}\" k použití nebo úpravě postav)", + "howToUseIconsText": "(vytvořte si globální herní účet (v okně účtů) abyste je mohli použít)", + "howToUseMapsText": "(použijte tyto mapy ve vlastních týmových/všichni-proti-všem seznamech)", + "iconsText": "Ikony", + "loadErrorText": "Nelze načíst.\nZkontrolujte připojení.", + "loadingText": "Načítání...", + "mapsText": "Mapy", + "miniGamesText": "MiniHry", + "oneTimeOnlyText": "(pouze jednou)", + "purchaseAlreadyInProgressText": "Koupě této položky již probíhá.", + "purchaseConfirmText": "Zakoupit ${ITEM}?", + "purchaseNotValidError": "Nákup je neplatný.\nKontaktuje ${EMAIL} pokuď je to chyba.", + "purchaseText": "Zakoupit", + "saleBundleText": "Balíček ve slevě!", + "saleExclaimText": "Sleva!", + "salePercentText": "(o ${PERCENT}% levnější)", + "saleText": "VÝPRODEJ", + "searchText": "Hledat", + "teamsFreeForAllGamesText": "Týmy / Všichni-proti-Všem Hry", + "totalWorthText": "***JEDNOTLIVĚ STOJÍ ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Aktivovat?", + "winterSpecialText": "Zimní Speciál", + "youOwnThisText": "- již vlastníte -" + }, + "storeDescriptionText": "Šílená Hra s Osmi Hráči v Partě!\n\nVyhoďte do vzduchu své přátele (nebo počítač) v turnaji explozivních miniher jako je třeba Seber Vlajku, Bomber-Hokej, a Epický-Slow-Motion-Zápas-Smrti!\n\nJednoduché ovládání a široká podpora ovladačů tvoří jednoduchou vlastnost připojit až 8 lidí do akce; a také můžete použít své mobilní zařízení jako ovladače přes aplikaci 'BombSquad Remote', která je zdarma!\n\nOdhoďte Bomby\n\nPro více informací navštivte stránku www.froemling.net/bombsquad", + "storeDescriptions": { + "blowUpYourFriendsText": "Vyhoďte přátele do vzduchu.", + "competeInMiniGamesText": "Utkejte se v minihrách od závodění po létání.", + "customize2Text": "Přuzpůsobte si postavy, minihry, a dokonce i soundtrack.", + "customizeText": "Přizpůsobte si postavy a vytvořte si vlastní playlist miniher.", + "sportsMoreFunText": "Sporty jsou s bombami větší zábava.", + "teamUpAgainstComputerText": "Sjednoťte se proti počítači." + }, + "storeText": "Obchod", + "submitText": "Odeslat", + "submittingPromoCodeText": "Odesílám kód...", + "teamNamesColorText": "Týmové Jména/Barvy...", + "telnetAccessGrantedText": "Přístup k telnetu zapnut.", + "telnetAccessText": "Detekován přístup k telnetu; povolit?", + "testBuildErrorText": "Tento testovací build již není dlouho aktivní; podívejte se prosím po nové verzi.", + "testBuildText": "Testovací Build", + "testBuildValidateErrorText": "Nebylo možné ověřit testovací build. (žádné internetové připojení?)", + "testBuildValidatedText": "Testovací Build Ověřen; Užívejte!", + "thankYouText": "Děkujeme Vám za podporu! Užijte si hru!!", + "threeKillText": "TŘI ZABITÍ!!!", + "timeBonusText": "Časový Bonus", + "timeElapsedText": "Uplynulý Čas", + "timeExpiredText": "Čas Vypršel", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Tip", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Nejlepší Přátelé", + "tournamentCheckingStateText": "Zjišťuji stav turnaje; Počkejte prosím...", + "tournamentEndedText": "Tento turnaj již skončil. Brzy ale začne další.", + "tournamentEntryText": "Vstup do turnaje", + "tournamentResultsRecentText": "Vysledky predchozich turnaju", + "tournamentStandingsText": "Pořadí v turnaji", + "tournamentText": "Turnaj", + "tournamentTimeExpiredText": "Čas turnaje vypršel", + "tournamentsText": "Turnaje", + "translations": { + "characterNames": { + "Agent Johnson": "Agent Johnson", + "B-9000": "B-9000", + "Bernard": "Bernard", + "Bones": "Kosťa", + "Butch": "Butch", + "Easter Bunny": "Velikonoční králíček", + "Flopsy": "Flopsy", + "Frosty": "Frosty", + "Gretel": "Gretel", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Štístko", + "Mel": "Mel", + "Middle-Man": "Střední-Muž", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Santa Claus", + "Snake Shadow": "Hadí stín", + "Spaz": "Spaz", + "Taobao Mascot": "Taobao maskot", + "Todd McBurton": "Todd McBurton", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} - Trénink", + "Infinite ${GAME}": "${GAME} - Nekonečné", + "Infinite Onslaught": "Nekonečné Útočení", + "Infinite Runaround": "Nekonečná Obrana", + "Onslaught Training": "Útok - Trénink", + "Pro ${GAME}": "${GAME} - Profík", + "Pro Football": "Ragby - Profík", + "Pro Onslaught": "Útok - Profík", + "Pro Runaround": "Obrana - Profík", + "Rookie ${GAME}": "${GAME} - Nováček", + "Rookie Football": "Ragby - Nováček", + "Rookie Onslaught": "Útok - Nováček", + "The Last Stand": "Poslední vzdor", + "Uber ${GAME}": "${GAME} - Mega", + "Uber Football": "Ragby - Megatěžké", + "Uber Onslaught": "Útok - Megatěžké", + "Uber Runaround": "Obrana - Megatěžká" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Staňte se vyvoleným po dlouhý čas, abyste vyhráli.\nZabijte vyvoleného, abyste se jím stali.", + "Bomb as many targets as you can.": "Vybombardujte tolik cílů, kolik dokážete.", + "Carry the flag for ${ARG1} seconds.": "Držte vlajku po dobu ${ARG1} sekund.", + "Carry the flag for a set length of time.": "Držte vlajku po nastavený čas.", + "Crush ${ARG1} of your enemies.": "Rozdrťte ${ARG1} Vašich nepřátel.", + "Defeat all enemies.": "Porazte všechny nepřátele", + "Dodge the falling bombs.": "Vyhněte se padajícím bombám.", + "Final glorious epic slow motion battle to the death.": "Finální úžasný epický slow motion souboj na smrt.", + "Gather eggs!": "Sbírání vajec!", + "Get the flag to the enemy end zone.": "Dostaňte vlajku na nepřátelskou koncovou zónu.", + "How fast can you defeat the ninjas?": "Jak rychle dokážete porazit ninja bojovníky?", + "Kill a set number of enemies to win.": "Zabijte určený počet nepřátel, abyste vyhráli.", + "Last one standing wins.": "Poslední vzdorující vyhrává.", + "Last remaining alive wins.": "Poslední zbývající živý vyhrává.", + "Last team standing wins.": "Poslední vzdorující tým vyhrává.", + "Prevent enemies from reaching the exit.": "Zabraňte nepřátelům dostat se na konec.", + "Reach the enemy flag to score.": "Dostaňte nepřátelskou vlajku, abyste skórovali.", + "Return the enemy flag to score.": "Vraťe nepřátelskou vlajku pro skórování.", + "Run ${ARG1} laps.": "Uběhněte ${ARG1} kol/a.", + "Run ${ARG1} laps. Your entire team has to finish.": "Uběhněte ${ARG1} kol. Celý Váš tým musí dokončit.", + "Run 1 lap.": "Uběhněte 1 kolo.", + "Run 1 lap. Your entire team has to finish.": "Uběhněte 1 kolo. Celý váš tým musí dokončit.", + "Run real fast!": "Běžte opravdu rychle!", + "Score ${ARG1} goals.": "Dejte góly - ${ARG1}.", + "Score ${ARG1} touchdowns.": "Dejte touchdowny - ${ARG1}.", + "Score a goal.": "Dejte gól.", + "Score a touchdown.": "Dejte touchdown.", + "Score some goals.": "Dejte pár gólů.", + "Secure all ${ARG1} flags.": "Zajistěte všechny vlajky - ${ARG1}", + "Secure all flags on the map to win.": "Zajistěte všechny vlajky na mapě, abyste vyhráli.", + "Secure the flag for ${ARG1} seconds.": "Zajistěte vlajku na ${ARG1} sekund.", + "Secure the flag for a set length of time.": "Zajistěte vlajku po určený čas.", + "Steal the enemy flag ${ARG1} times.": "Ukradněte ${ARG1}x nepřátelskou vlajku.", + "Steal the enemy flag.": "Ukradněte nepřátelskou vlajku.", + "There can be only one.": "Tady může být jen jedna.", + "Touch the enemy flag ${ARG1} times.": "Dotkněte se ${ARG1}x nepřátelské vlajky.", + "Touch the enemy flag.": "Dotkněte se nepřátelské vlajky.", + "carry the flag for ${ARG1} seconds": "Držte vlajku po dobu ${ARG1} sekund", + "kill ${ARG1} enemies": "zabijte ${ARG1} nepřátel", + "last one standing wins": "poslední vzdorující vyhrává", + "last team standing wins": "poslední vzdorující tým vyhrává", + "return ${ARG1} flags": "vraťte ${ARG1} vlajek", + "return 1 flag": "vraťte 1 vlajku", + "run ${ARG1} laps": "uběhněte ${ARG1} kol/a", + "run 1 lap": "uběhněte 1 kolo", + "score ${ARG1} goals": "dejte góly - ${ARG1}", + "score ${ARG1} touchdowns": "dejte touchdowny - ${ARG1}", + "score a goal": "dejte gól", + "score a touchdown": "dejte touchdown", + "secure all ${ARG1} flags": "zajistěte všechny vlajky - ${ARG1}", + "secure the flag for ${ARG1} seconds": "zajistěte vlajku na ${ARG1} sekund", + "touch ${ARG1} flags": "dotkněte se ${ARG1} vlajek", + "touch 1 flag": "dotkněte se 1 vlajky" + }, + "gameNames": { + "Assault": "Přepadení", + "Capture the Flag": "Seberte Vlajku", + "Chosen One": "Vyvolený", + "Conquest": "Dobývání", + "Death Match": "Zápas Smrti", + "Easter Egg Hunt": "Lovení velikonočních vajíček", + "Elimination": "Zneškodnění", + "Football": "Ragby", + "Hockey": "Hokej", + "Keep Away": "Držte se Dál", + "King of the Hill": "Král Kopce", + "Meteor Shower": "Smršť Meteoritů", + "Ninja Fight": "Souboj Ninja bojovníků", + "Onslaught": "Útok", + "Race": "Závod", + "Runaround": "Obrana", + "Target Practice": "Trénování Cílů", + "The Last Stand": "Poslední vzdor" + }, + "inputDeviceNames": { + "Keyboard": "Klávesnice", + "Keyboard P2": "Klávesnice P2" + }, + "languages": { + "Arabic": "Arabsky", + "Belarussian": "Běloruština", + "Chinese": "Zjednodušená Čínština", + "ChineseTraditional": "Tradiční Čínština", + "Croatian": "Chorvatština", + "Czech": "Čeština", + "Danish": "Dánština", + "Dutch": "Holandština", + "English": "Angličtina", + "Esperanto": "Esperanto", + "Finnish": "Finština", + "French": "Francouzština", + "German": "Němčina", + "Gibberish": "Hatmatilka", + "Greek": "Řečtina", + "Hindi": "Hindština", + "Hungarian": "Maďarština", + "Indonesian": "Indonéština", + "Italian": "Italština", + "Japanese": "Japonština", + "Korean": "Korejština", + "Persian": "Perština", + "Polish": "Polština", + "Portuguese": "Portugalština", + "Romanian": "Rumunština", + "Russian": "Ruština", + "Serbian": "Srbština", + "Slovak": "Slovenština", + "Spanish": "Španělština", + "Swedish": "Švédština", + "Turkish": "Turečtina", + "Ukrainian": "Ukrajinština", + "Venetian": "Benátština", + "Vietnamese": "Vietnamština" + }, + "leagueNames": { + "Bronze": "Bronz", + "Diamond": "Diamant", + "Gold": "Zlato", + "Silver": "Stříbro" + }, + "mapsNames": { + "Big G": "Velké G", + "Bridgit": "Mostík", + "Courtyard": "Nádvoří", + "Crag Castle": "Skalní Hrad", + "Doom Shroom": "Houba Zkázy", + "Football Stadium": "Ragby Stadion", + "Happy Thoughts": "Veselé Myšlenky", + "Hockey Stadium": "Hokejový Stadion", + "Lake Frigid": "Jezero Zmrzlík", + "Monkey Face": "Opičí Tvář", + "Rampage": "Zuřivost", + "Roundabout": "Kruhový Objezd", + "Step Right Up": "Zmatené schody", + "The Pad": "Podložka", + "Tip Top": "Tip Ťop", + "Tower D": "Věž D", + "Zigzag": "Cikcak" + }, + "playlistNames": { + "Just Epic": "Jen Epické", + "Just Sports": "Jen Sporty" + }, + "scoreNames": { + "Flags": "Vlajky", + "Goals": "Góly", + "Score": "Skóre", + "Survived": "Přežil", + "Time": "Čas", + "Time Held": "Čas Držen" + }, + "serverResponses": { + "A code has already been used on this account.": "Kód již byl pro tento účet použit.", + "A reward has already been given for that address.": "Chyba: Tato adresa již odměnu dostala.", + "Account linking successful!": "Spojení účtu úspěšné!", + "Account unlinking successful!": "Účet úspěšně odpojen!", + "Accounts are already linked.": "Účty jsou již spojeny.", + "An error has occurred; (${ERROR})": "Nastala chyba; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Nastala chyba; kontaktujete prosím podporu. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Stala se chyba; prosím kontaktujte support@froemling.net.", + "An error has occurred; please try again later.": "Vyskytla se chyba; prosím zkuste to znova za chvilku", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Jste si jisti, že chcete propojit tyto účty?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nTuto akci nelze vrátit zpět!", + "BombSquad Pro unlocked!": "BombSquad Pro odemčen!", + "Can't link 2 accounts of this type.": "Nelze spojit 2 účty tohodle typu.", + "Can't link 2 diamond league accounts.": "Nelze spojit 2 účty diamantové ligy.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Nelze propojit; převýšily maximum ${COUNT} propojených účtů.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Detekováno podvádění; skóre a ceny pozastaveny na ${COUNT} dní", + "Could not establish a secure connection.": "Nelze navázat bezpečné připojení.", + "Daily maximum reached.": "Dosaženo denní maximum.", + "Entering tournament...": "Vstupujete do turnaje...", + "Invalid code.": "Neplatný kód.", + "Invalid payment; purchase canceled.": "Neplatná platební maetoda; platba zrušena.", + "Invalid promo code.": "Neplatný Promo kód.", + "Invalid purchase.": "Neplatná koupě.", + "Invalid tournament entry; score will be ignored.": "Neplatný turnaj; skóre bude ignorováno.", + "Item unlocked!": "Položka odemčena!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "Propojení zamítnuto. ${ACCOUNT} obsahuje\nznačné data, které by byly ZCELA ZTRACENY.\nPokud chcete, můžete připojit účet v opačném\npořadí (a ztratit tak data TOHOTO účtu)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Chcete spojit účet ${ACCOUNT} s tímto účtem?\nVšechna data na účtě ${ACCOUNT} budou smazána.\nTuto akci nelze navrátit zpět. Jste si jisti?", + "Max number of playlists reached.": "Dosazano maximalniho počtu výpisů", + "Max number of profiles reached.": "Dosaženo maximalního počtu profilů", + "Maximum friend code rewards reached.": "Kamarádova maximální výhra na kódu.", + "Message is too long.": "Zpráva je příliš dlouhá.", + "Profile \"${NAME}\" upgraded successfully.": "Profil \"${NAME}\" byl úspěšně přeměněn.", + "Profile could not be upgraded.": "Profil nelze přeměnit.", + "Purchase successful!": "Koupě se zdařila!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Získali jste ${COUNT} kupónů za přihlášení.\nVraťte se zítra, abyste jich obdrželi ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Server běží na novější verzi a tu vaši nepodporuje,\naktualizujte hru pro připojení.", + "Sorry, there are no uses remaining on this code.": "Omlouváme se, ale již zde nejsou žádné zbývající použití pro tento kód.", + "Sorry, this code has already been used.": "Omlouváme se, ale tento kód již byl použit.", + "Sorry, this code has expired.": "Omlouváme se, ale platnost tohoto kódu vypršela.", + "Sorry, this code only works for new accounts.": "Omlouváme se, ale tento kód je platný pouze pro nové účty.", + "Temporarily unavailable; please try again later.": "Dočasně nedostupné; zkuste to později.", + "The tournament ended before you finished.": "Turnaj skončil než jste ho dokončili.", + "This account cannot be unlinked for ${NUM} days.": "Tento účet nelze odpojit po dobu ${NUM} dnů.", + "This code cannot be used on the account that created it.": "Tento kód nemůže být použit na účtu, na kterém byl vytvořen.", + "This is currently unavailable; please try again later.": "Tato možnost je momentálně nedostupná; prosím zkuste to později.", + "This requires version ${VERSION} or newer.": "Toto vyžaduje verzi ${VERSION} nebo vyšší.", + "Tournaments disabled due to rooted device.": "Turnaje nedostupné na zařízeních s rootem.", + "Tournaments require ${VERSION} or newer": "Turnaje vyžadují ${VERSION} nebo novější", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Odpojit ${ACCOUNT} od tohoto účtu?\nVeškerá data na účtě ${ACCOUNT} se resetují.\n(výjimka jsou v některých případech úspěchy)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "VAROVÁNÍ: Byla proti vám vznešena obvinění z podvádění ve hře.\nÚčty o kterých se prokáže že používají ulehčující módy budou smazány, hrajte prosím férově.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Přejete si propojit Váš účet zařízení k tomuto?\n\nÚčet vašeho zařízení je ${ACCOUNT1}\nTento účet je ${ACCOUNT2}\n\nTato akce Vám umožní zachovat si stávající postup.\nVarování: Nelze vrátit zpět!", + "You already own this!": "Toto již vlastníte!", + "You can join in ${COUNT} seconds.": "Připojit se můžete až za ${COUNT} sek.", + "You don't have enough tickets for this!": "Na toto bohužel nemáte dostatek kupónů!", + "You don't own that.": "Nevlastníte tuto věc.", + "You got ${COUNT} tickets!": "Získal jste ${COUNT} kupónů!", + "You got a ${ITEM}!": "Získaly jste ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Byli jste povýšeni do další ligy; gratulujeme!", + "You must update to a newer version of the app to do this.": "Musíš updatovat na novější verzi aplikace k udělání tohodle.", + "You must update to the newest version of the game to do this.": "Vaše verze hry tuto akci nepodporuje, aktualizujte hru na nejnovější verzi.", + "You must wait a few seconds before entering a new code.": "Před zadáním nového kódu počkejte pár sekund.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "V posledním turnaji jste se umístil na #${RANK} pozici. Děkujeme!", + "Your account was rejected. Are you signed in?": "Váš účet byl odmítnut. Jste přihlášen?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Vaše kopie hry byla pozměněna.\nVraťte prosím všechny změny a zkuste to znovu.", + "Your friend code was used by ${ACCOUNT}": "Váš kód pro přátele byl použit hráčem ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Minuta", + "1 Second": "1 Sekunda", + "10 Minutes": "10 Minut", + "2 Minutes": "2 Minuty", + "2 Seconds": "2 Sekundy", + "20 Minutes": "20 Minut", + "4 Seconds": "4 Sekundy", + "5 Minutes": "5 Minut", + "8 Seconds": "8 Sekund", + "Allow Negative Scores": "Povolit negativní skóre", + "Balance Total Lives": "Rovnovážný Počet Životů", + "Bomb Spawning": "Objevení bomby", + "Chosen One Gets Gloves": "Vyvolený získal rukavice", + "Chosen One Gets Shield": "Vyvolený získal štít", + "Chosen One Time": "Čas Vyvoleného", + "Enable Impact Bombs": "Zapnout Nárazové Bomby", + "Enable Triple Bombs": "Zapnout Trojité Bomby", + "Entire Team Must Finish": "Celý tým musí skončit", + "Epic Mode": "Epický Mód", + "Flag Idle Return Time": "Čas Návratu Neaktivní Vlajky", + "Flag Touch Return Time": "Čas Návratu Dotknuté Vlajky", + "Hold Time": "Čas Držení", + "Kills to Win Per Player": "Počet Zabití pro vítězství", + "Laps": "Kola", + "Lives Per Player": "Životy na Hráče", + "Long": "Dlouhý", + "Longer": "Delší", + "Mine Spawning": "Přidávání Min", + "No Mines": "Bez Min", + "None": "Žádný", + "Normal": "Normální", + "Pro Mode": "Pro(fi) mód", + "Respawn Times": "Časy Znovuzrození", + "Score to Win": "Skóre pro Výhru", + "Short": "Krátký", + "Shorter": "Kratší", + "Solo Mode": "Sólo Mód", + "Target Count": "Počet cílů", + "Time Limit": "Časový Limit" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "Diskvalifikuji ${TEAM} tým protože ${PLAYER} opustil hru.", + "Killing ${NAME} for skipping part of the track!": "Zabíjím ${NAME} kvůli přeskočení části tratě!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Varování pro ${NAME}: Turbo / spamování tlačítek tě vyhodí" + }, + "teamNames": { + "Bad Guys": "Padouši", + "Blue": "Modrý", + "Good Guys": "Dobráci", + "Red": "Červený" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Perfektně načasovaný Běh-výskok-otočka-úder může zabít na jednu ránu\na zajistit vám doživotní respekt od vašich přátel.", + "Always remember to floss.": "Nikdy nezapomeňte na zubní nit.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Vytvořte herní profily pro Vás i Vaše přátele s Vašimi\npreferovanými jmény a vzhledy místo používání náhodně generovaných.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Prokletí vás změní v časovanou bombu.\nJediná protilátka je rychle sebrat lékárničku.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Kromě vzhledu mají všechny postavy stejné vlastnosti\ntakže si prostě vyberte toho, který se Vám nejvíc podobá.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Radši s tím štítem nebuďte moc namyšlení; pořád můžete vypadnout pryč z mapy.", + "Don't run all the time. Really. You will fall off cliffs.": "Nepobíhejte pořád. Vážně. Vypadnete z mapy.", + "Don't spin for too long; you'll become dizzy and fall.": "Netoč se moc dlouho, bude ti špatně.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Pro sprint držte jakékoliv tlačítko.(Pokuď máte ovladač, fungují i ostatní tlačítka)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Držte jakékoliv tlačítko pro sprint. Budete rychlejší.¨\nAle dávejte si pozor na okraje mapy.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ledové Bomby nejsou moc silné, ale zmrazí vše, \nčeho se dotknou, a zanechají je náchylné k rozbití.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Pokuď vás někdo chytne, udeřte ho a on vás pustí.\nTak to funguje i v reálném životě.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Pokud jste krátký na řadičích, nainstalujte '${REMOTE_APP_NAME}' app \nna svých mobilních zařízení k jejich použití jako regulátorů.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Pokuď nemáte ovladač, nainstalujte si aplikaci 'BombSquad Remote'\nna vaše druhé zařízení a používejte ho jako ovladač.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Když se k Vám přilepí Lepivá Bomba, skákejte jako diví a točte se v kruzích.\nMožná setřesete bombu, nebo, když už nic jiného, budou Vaše poslední chvilky zajímavé.", + "If you kill an enemy in one hit you get double points for it.": "Pokuď někoho zabijete na jednu ránu, dostanete dvojnásobek bodů.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Pokud seberete prokletí, vaší jedinou nadějí na přežití\nje, v následujících pár sekundách, najít lékárničku.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Pokuď budete stát na místě, bude z vás toust. Běhejte a uhýbejte abyste přežili..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Jestliže máte hodně hráčů, kteří přichází a odchází, zapněte 'automatické vyhazování neaktivních hráčů'\nv nastavení, pro případ, že někdo zapomene opustit hru.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Pokud se vaše zařízení zahřívá nebo byste chtěli šetřit baterii,\nsnižte kvalitu \"Detailů\" nebo \"Rozlišení\" v Nastavení->Grafika", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Pokud se vám začne sekat obraz, zkuste si snížit\n\"Rozlišení\" nebo \"Detaily\" v Nastavení->Grafika", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "V Seberte Vlajku, Vaše vlajka musí být na vaší základně, abyste mohli skórovat.\nJestliže chce druhý tým skórovat, krádež jejich vlajky může být dobrý způsob jejich zastavení.", + "In hockey, you'll maintain more speed if you turn gradually.": "V Hokeji získáte větší rychlost, když se budete točit pomalu.", + "It's easier to win with a friend or two helping.": "Je jednodušší vyhrát když vám pomáhají přátelé.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Když během házení bomby vyskočité, bomby doletí výš a dál.", + "Land-mines are a good way to stop speedy enemies.": "Miny jsou dobré na zastavení rychlých padouchů.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Spousty věcí může být sebráno a hozeno, včetně ostatních hráčů.\nVrhání Vašich nepřátel z útesu může být efektivní a emočně naplňující strategií.", + "No, you can't get up on the ledge. You have to throw bombs.": "Ne, nemůžete lézt po římsách. Musíte házet bomby.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Hráči se mohou připojit a odpojit i v průběhu hry\na také lze během hry připojovat a odpojovat ovladače.", + "Practice using your momentum to throw bombs more accurately.": "Vyzkoušejte si přesnější házení bomb za použití Vaší kinetické energie.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Čím rychleji se pohybujete, tím větší zranění Vaše pěsti vytvoří,\ntakže zkoušejte běhat, skákat, a točit se jako šílenec.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Běhejte tam a zpět před odhozením bomby,\nabyste se rozmáchli a odhodili ji dále.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Zabijte víc padouchů tím že\ndáte bombu hned vedle TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Hlava je nezranitelnější část, takže Lepivá Bomba\nna hlavě většinou končí koncem hry.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Tento level nikdy nezkončí, ale zdejší vysoké skóre\nnaučí hráče respektovat Vás po celém světě.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Síla hodu je založena na směru který držíte.\nPro jemné odhození něčeho přímo před Vás, nedržte žádný směr.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Unavuje vás SoundTrack? Nastavte si svůj vlastní!\nNastavení->Zvuk->Soundtrack", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Zkuste bomby chvilku držet před tím než je hodíte.", + "Try tricking enemies into killing eachother or running off cliffs.": "Obelstěte padouchy, aby se zabili navzájem nebo vypadli pryč z mapy.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Na sebrání vlajky použijte < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Švihněte sebou dozadu a dopředu abyste bombu dohodily dál..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Můžete 'mířit' svoje údery točením se doleva nebo doprava.\nJe to užitečné pro shazování padouchů z hran nebo skórování v hokeji.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Můžete usoudit kdy se bomba chystá vybouchnout podle\nbarvy jisker z její rozbušky: žlutá..oranžová..červená..BUM.", + "You can throw bombs higher if you jump just before throwing.": "Můžete bomby házet výš když vyskočíte těsně před hodem.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Když se o něco bouchnete hlavou.. můžete se zranit\ntak bych to být Vámi nezkoušel.", + "Your punches do much more damage if you are running or spinning.": "Pokuď se točíte nebo běžíte tak vaše údery budou silnější." + } + }, + "trophiesRequiredText": "To vyžaduje minimálně ${NUMBER} trofeje.", + "trophiesText": "- Trofeje", + "trophiesThisSeasonText": "Trofeje tuto sezónu", + "tutorial": { + "cpuBenchmarkText": "Spuštění tutorialu ve směšné rychlosti (testuje hlavně rychlost CPU)", + "phrase01Text": "Ahoj, ty tam!", + "phrase02Text": "Vítejte v ${APP_NAME}u!", + "phrase03Text": "Tady máte pár tipů jak ovládat postavu:", + "phrase04Text": "Většina věcí v ${APP_NAME}u je založena na fyzice.", + "phrase05Text": "Například, když někoho udeříte,...", + "phrase06Text": "..záleží na tom jak rychlá je vaše pěst.", + "phrase07Text": "Vidíte? Nehýbal jsem se, takže to ${NAME}e sotva bolelo.", + "phrase08Text": "Teď výskok a otočka pro větší rychlost.", + "phrase09Text": "Ha, to je lepší.", + "phrase10Text": "Běh taky pomáhá.", + "phrase11Text": "Pro běh držte JAKÉKOLIV tlačítko.", + "phrase12Text": "Pro super údery, zkuste běh a otočku.", + "phrase13Text": "Ježiš, to jsem nechtěl ${NAME}i.", + "phrase14Text": "Některé věci můžete chytit a hodit. Třeba vlajky.. nebo ${NAME}e.", + "phrase15Text": "A to nejlepší, jsou tu bomby.", + "phrase16Text": "Naučit se házet bomby zabere čas.", + "phrase17Text": "Jau! To nebyl moc dobrý hod.", + "phrase18Text": "Pohyb vám pomůže dohodit dál.", + "phrase19Text": "Skákání vám pomůže házet výš.", + "phrase20Text": "Pro ještě větší vzdálenost zkuste malou otočku.", + "phrase21Text": "Správné načasovaní je dost užitečné.", + "phrase22Text": "Krucipísek.", + "phrase23Text": "Zkuste si s hodem počkat vteřinku nebo dvě.", + "phrase24Text": "Hurá! Dokonale načasováno.", + "phrase25Text": "Nu, to bude asi všechno.", + "phrase26Text": "Teď běž, tygře!!!", + "phrase27Text": "Pamatuj na trénink, a určitě se vrátíš živý!", + "phrase28Text": "...teda, možná...", + "phrase29Text": "Přeji hodně štěstí!", + "randomName1Text": "Lukáš", + "randomName2Text": "Marek", + "randomName3Text": "Daniel", + "randomName4Text": "Matěj", + "randomName5Text": "Pepa", + "skipConfirmText": "Opravdu chcete přeskočit Tutorial? Klikněte pro potvrzení.", + "skipVoteCountText": "${COUNT}/${TOTAL} přeskočení", + "skippingText": "Přeskakuji Tutorial...", + "toSkipPressAnythingText": "(Stistkni cokoli pro přeskočení Tutorialu)" + }, + "twoKillText": "DVĚ ZABITÍ!!!", + "unavailableText": "Nedostupné", + "unconfiguredControllerDetectedText": "Zaznamenán nenakonfigurovaný ovladač:", + "unlockThisInTheStoreText": "Toto musí být nejdříve odemknuto v obchodě.", + "unlockThisProfilesText": "Pro vytvoření více než ${NUM} profilů, potřebuješ:", + "unlockThisText": "Pro odemknutí tohoto je zapotřebí:", + "unsupportedHardwareText": "Omlouváme se, ale tento hardware není porporován tímto buildem hry.", + "upFirstText": "První:", + "upNextText": "Další, ${COUNT}. hra bude:", + "updatingAccountText": "Aktualizuji Váš účet...", + "upgradeText": "Přeměnit", + "upgradeToPlayText": "Upgradujte na \"${PRO}\" v herním obchodě, abyste toto mohli hrát.", + "useDefaultText": "Použít Výchozí", + "usesExternalControllerText": "Tato hra používá jako vstup externí ovladač.", + "usingItunesText": "Používám Music App pro soundtrack", + "usingItunesTurnRepeatAndShuffleOnText": "Ujistěte se prosím, že je zaplý shuffle, a opakovat VŠE v iTunes,", + "validatingTestBuildText": "Ověřuji Testovací Build...", + "victoryText": "Vítězství!!!", + "voteDelayText": "Nelze spustit další hlas za ${NUMBER} sekund", + "voteInProgressText": "Hlasování již probíhá.", + "votedAlreadyText": "Již jste hlasoval", + "votesNeededText": "${NUMBER} hlasů potřebných", + "vsText": "vs.", + "waitingForHostText": "(čeká se na ${HOST})", + "waitingForPlayersText": "Čeká se na jiného hráče...", + "waitingInLineText": "Čekání ve frontě (parta plná)", + "watchAVideoText": "Zkoukni reklamu", + "watchAnAdText": "Zkouknout Reklamu", + "watchWindow": { + "deleteConfirmText": "Vymazat \"${REPLAY}\"?", + "deleteReplayButtonText": "Vymazat\nZáznam", + "myReplaysText": "Moje záznamy", + "noReplaySelectedErrorText": "Nevybrán žádný záznam", + "playbackSpeedText": "Rychlost přehrávání: ${SPEED}", + "renameReplayButtonText": "Přejmenovat\nZáznam", + "renameReplayText": "Přejmenovat \"${REPLAY}\" na:", + "renameText": "Přejmenovat", + "replayDeleteErrorText": "Chyba při mazání záznamu.", + "replayNameText": "Jméno Záznamu", + "replayRenameErrorAlreadyExistsText": "Záznam s tímto jménem již existuje.", + "replayRenameErrorInvalidName": "Nelze přejmenovat; chybné jméno.", + "replayRenameErrorText": "Chyba při přejmenování záznamu.", + "sharedReplaysText": "Sdílené Záznamy", + "titleText": "Záznamy", + "watchReplayButtonText": "Shlédnout\nZáznam" + }, + "waveText": "Vlna", + "wellSureText": "Nuže dobrá!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Hledám Wiimotes...", + "pressText": "Stiskněte současně Tlačítka Wiimote 1 a 2.", + "pressText2": "Na novějších Wiimote ovladačích s vestavěným Motion Plus stiskněte červené 'synchroznizační' tlačítko v zadní části." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "listenText": "Poslouchat", + "macInstructionsText": "Ujistěte se, že je Vaše Wii vyplé a Bluetooth je na vašem\nMac zařízení zaplý, a poté stistkněte 'Poslouchat'. Podpora\nWiimote může být trochu horší, a možná bude potřeba to párkrát\nzkusit, než se Vám podaří ho připojit.\n\nZařízení Bluetooth dokáže mít připojených až 7 zařízení,\nale to se může lišit\n\nBombSquad podporuje originální Wiimote, Nunchuk,\na klasický ovladač\nNovější Wii Remote Plus nyní funguje také,\nale bez přídavků.", + "thanksText": "Děkujeme týmu DarwiinRemote,\nže tohle uskutečnili.", + "titleText": "Nastavení Wiimote" + }, + "winsPlayerText": "${NAME} Vyhrál!", + "winsTeamText": "${NAME} Vyhrál!", + "winsText": "${NAME} Vyhrál!", + "worldScoresUnavailableText": "Světové skóre nepřístupné.", + "worldsBestScoresText": "Světové nejlepší skóre", + "worldsBestTimesText": "Světové nejlepší časy", + "xbox360ControllersWindow": { + "getDriverText": "Získat Driver", + "macInstructions2Text": "Pro bezdrátové použití ovladače také potřebujete příjmač, který\nje součástí 'Xbox 360 Wireless Controller for Windows'.\nJeden příjmač dovoluje připojit až 4 zařízení.\n\nDůležité: příjmače třetích stran s tímto driverem nebudou fungovat;\nujistěte se, že je na Vašem příjmači 'Microsoft' a ne 'XBOX 360'.\nMicrosoft už tyto příjmače neprodává samostatně, takže budete muset\njeden koupit s ovladačem dohromady, nebo hledat na ebay.\n\nJestliže je pro Vás toto užitečné, zvažte prosím přispění \ntvůrci driveru na jeho stránce.", + "macInstructionsText": "Pro použití ovladače Xbox 360, potřebujete nainstalovat\nMac Driver který naleznete na odkazu napsaném níže.\nFunguje s drátovou i s bezdrátovou verzí ovladače.", + "ouyaInstructionsText": "Pro použití kabelových Xbox 360 ovladačů pro BombSquad, je jednoduše\npřipojte do USB portu vašeho zařízení. Můžete použít USB hub\npro připojení více ovladačů.\n\nPro použití bezdrátových ovladačů budete potřebovat bezdrátový příjmač,\ndostupný jako součást \"Xbox 360 wirelles Controller for Windows\"\nbalíčku nebo prodávaného samostatně. Každý příjmač zapojíte do USB portu\na každý z nich Vám dovolí připojit až 4 bezdrátové ovladače.", + "titleText": "Hrát ${APP_NAME} s ovladačem Xbox 360:" + }, + "yesAllowText": "Ano, Povolit!", + "yourBestScoresText": "Vaše nejlepší skóre", + "yourBestTimesText": "Váš nejlepší čas" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/danish.json b/dist/ba_data/data/languages/danish.json new file mode 100644 index 0000000..0e70c12 --- /dev/null +++ b/dist/ba_data/data/languages/danish.json @@ -0,0 +1,1626 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Konto navne kan ikke indeholde emojis eller andre specielle bogstaver", + "accountProfileText": "(bruger profil)", + "accountsText": "Konti", + "achievementProgressText": "Achievements: ${COUNT} ud af ${TOTAL}", + "campaignProgressText": "Kampagneforløb ${PROGRESS}", + "changeOncePerSeason": "Du kan kun ændre dette en gang pr. sæson", + "changeOncePerSeasonError": "Du må vente indtil næste sæson for at ændre dette igen (${NUM} days)", + "customName": "Brugerdefineret Navn", + "linkAccountsEnterCodeText": "Indtast kode", + "linkAccountsGenerateCodeText": "Generér kode", + "linkAccountsInfoText": "(del forløb på tværs af platforme)", + "linkAccountsInstructionsNewText": "For at forbinde to kontoer, generer en kode på den første\nog skriv den kode på den anden. Dataen fra den anden konto\nvil så blive delt mellem dem begge.\n(Dataen fra den første konto vil blive tabt)\n\nDu kan forbinde op til ${COUNT} kontoer.\n\nVIGTIGT: forbind kun kontoer som du ejer;\nHvis du forbinder med din vens konto vil du ikke\nkunne spille online på samme tid som ham.", + "linkAccountsInstructionsText": "For at forbinde to konti, opret en kode på en\naf dem og indtast den kode på den anden.\nFremskridt og inventar vil blive kombineret.\nDu kan forbinde op til ${COUNT} konti.\n\nVIGTIGT: Kun forbinde konti som du ejer!\nHvis du forbinder konti med dine venner\nvil i ikke kunne spille samtidigt!\n\nPlus: dette kan ikke endu fortrydes, så vær forsigtig!", + "linkAccountsText": "Forbind konti", + "linkedAccountsText": "Forbundne konti:", + "nameChangeConfirm": "Ændre dit konto navn til ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Dette vil nulstille dit co-op forløb og dine\nlokale high-scores (men ikke dine tickets).\nDette kan ikke fortrydes. Er du sikker?", + "resetProgressConfirmText": "Dette vil nulstille dine co-op fremskridt,\nachievements og lokale highschores. \n(men ikke dine tickets). Dette kan ikke \ngøres om. Er du sikker?", + "resetProgressText": "Nulstil Process", + "setAccountName": "Indstil kontonavn", + "setAccountNameDesc": "Vælg navnet du vil vise til din konto.\nDu kan bruge navnet fra en af dine forbundne\nkontoer eller lave et unikt brugerdefineret navn.", + "signInInfoText": "Log in for at optjene tickets, konkurrere online \nog dele dit forløb på tværs af enheder.", + "signInText": "Log Ind", + "signInWithDeviceInfoText": "(en automatisk oprettet konto kun tilgængelig fra denne enhed)", + "signInWithDeviceText": "Log in med en enheds-konto.", + "signInWithGameCircleText": "Log in med Game Circle", + "signInWithGooglePlayText": "Log in med Google Play", + "signInWithTestAccountInfoText": "(ældre kontotype; brug enheds-konti fremover)", + "signInWithTestAccountText": "Log in med test konto", + "signOutText": "Log ud", + "signingInText": "Logger ind...", + "signingOutText": "Logger ud...", + "testAccountWarningOculusText": "Advarsel: Du logger ind med en \"test\" bruger. \nDette ville blive erstattet med \"rigtige\" brugere senere i dette \når, hvilket vil tilbyde ticket (billet) købsmuligheder og andre funktioner.\n\nindtilvidere skal du tjene alle dine tickets in-game.\n(Du får dog BombSquad Pro upgrade helt gratis!)", + "testAccountWarningText": "Advarsel: Du logger nu ind med en \"test\" bruger.\nDenne bruger er bunden med en bestemt enhed og\nkan risikere at blive nulstillet periodisk. (så lad vær\nmed at brug lang tid på at samle/oplåse ting til den)\n\nKør en detailversion af spillet for at bruge en \"rigtig\"\nbruger (Game-Center, Google Plus, osv.) Dette giver\ndig også mulighed for at gemme dine fremskridt i\nskyen og dele det mellem forskellige enheder.", + "ticketsText": "Tickets (biletter): ${COUNT}", + "titleText": "Din bruger", + "unlinkAccountsInstructionsText": "Vælg en konto at adskille", + "unlinkAccountsText": "Adskil kontoer", + "viaAccount": "(via konto ${NAME})", + "youAreLoggedInAsText": "Du er logget ind som: ", + "youAreSignedInAsText": "Du er logget ind som:" + }, + "achievementChallengesText": "Achievementudfordringer", + "achievementText": "Achievement", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Dræb 3 fjender med dynamit", + "descriptionComplete": "Du dræbte 3 fjender med dynamit", + "descriptionFull": "Dræb 3 fjender med dynamit i ${LEVEL}", + "descriptionFullComplete": "Du dræbte 3 fjender med dynamit i ${LEVEL}", + "name": "”Boom”, sagde dynamitten" + }, + "Boxer": { + "description": "Vind uden at bruge nogen bomber", + "descriptionComplete": "Du vandt uden at bruge nogen bomber", + "descriptionFull": "Gennemfør ${LEVEL} uden at bruge nogen bomber", + "descriptionFullComplete": "Du gennemførte ${LEVEL} uden at bruge nogen bomber", + "name": "Bokser" + }, + "Dual Wielding": { + "descriptionFull": "Forbind 2 kontrollere (hardware eller app)", + "descriptionFullComplete": "2 kontrollere forbundet (hardware eller app)", + "name": "Dobbelt-båren" + }, + "Flawless Victory": { + "description": "Vind uden at blive ramt", + "descriptionComplete": "Du vandt uden at blive ramt", + "descriptionFull": "Vind ${LEVEL} uden at blive ramt", + "descriptionFullComplete": "Du vandt ${LEVEL} uden at blive ramt", + "name": "Fejlfri sejr" + }, + "Free Loader": { + "descriptionFull": "Start et Free-For-All spil med 2+ spillere.", + "descriptionFullComplete": "Startede et Free-For-All spil med 2+ spillere", + "name": "Gratist" + }, + "Gold Miner": { + "description": "Dræb 6 fjender med miner", + "descriptionComplete": "Du dræbte 6 fjender med miner", + "descriptionFull": "Dræb 6 fjender med miner i ${LEVEL}", + "descriptionFullComplete": "Du dræbte 6 fjender med miner i ${LEVEL}", + "name": "Guldgraver" + }, + "Got the Moves": { + "description": "Vind uden at slå eller bruge bomber", + "descriptionComplete": "Du vandt uden at slå eller bruge bomber", + "descriptionFull": "Vind ${LEVEL} uden at slå eller bruge bomber", + "descriptionFullComplete": "Du vandt ${LEVEL} uden at slå eller bruge bomber", + "name": "Styr på trinene" + }, + "In Control": { + "descriptionFull": "Forbind en controller (hardware eller app)", + "descriptionFullComplete": "Forbind en controller. (hardware eller app)", + "name": "Under Kontrol" + }, + "Last Stand God": { + "description": "Scor 1000 point", + "descriptionComplete": "Du scorede 1000 point", + "descriptionFull": "Score 1000 point i ${LEVEL}", + "descriptionFullComplete": "Du scorede 1000 point i ${LEVEL}", + "name": "Gud i ${LEVEL}" + }, + "Last Stand Master": { + "description": "Scor 250 point", + "descriptionComplete": "Du scorede 250 point", + "descriptionFull": "Scor 250 point i ${LEVEL}", + "descriptionFullComplete": "Du scorede 250 point i ${LEVEL}", + "name": "Mester i ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Scor 500 point", + "descriptionComplete": "Du scorede 500 point", + "descriptionFull": "Scor 500 point i ${LEVEL}", + "descriptionFullComplete": "Du scorede 500 point i ${LEVEL}", + "name": "Troldmand i ${LEVEL}" + }, + "Mine Games": { + "description": "Dræb 3 fjender med miner", + "descriptionComplete": "Du dræbte 3 fjender med miner", + "descriptionFull": "Dræb 3 fjender med miner i ${LEVEL}", + "descriptionFullComplete": "Du dræbte 3 fjender med miner i ${LEVEL}", + "name": "Minespil" + }, + "Off You Go Then": { + "description": "Kast 3 fjender ud af banen", + "descriptionComplete": "Kastede 3 fjender ud af banen", + "descriptionFull": "Kast 3 fjender af banen i ${LEVEL}", + "descriptionFullComplete": "Du kastede 3 fjender af banen i ${LEVEL}", + "name": "Afsted med dig" + }, + "Onslaught God": { + "description": "Scor 5000 point", + "descriptionComplete": "Du scorede 5000 point", + "descriptionFull": "Scor 5000 point i ${LEVEL}", + "descriptionFullComplete": "Du scorede 5000 point i ${LEVEL}", + "name": "Gud i ${LEVEL}" + }, + "Onslaught Master": { + "description": "Scor 5000 point", + "descriptionComplete": "Du scorede 500 point", + "descriptionFull": "Scor 500 point i ${LEVEL}", + "descriptionFullComplete": "Du scorede 500 point i ${LEVEL}", + "name": "Mester i ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Besejr alle bølgerne", + "descriptionComplete": "Du besejrede alle bølgerne", + "descriptionFull": "Besejr alle bølgerne i ${LEVEL}", + "descriptionFullComplete": "Du besejrede alle bølgerne i ${LEVEL}", + "name": "Sejr i ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Scor 1000 point", + "descriptionComplete": "Du scorede 1000 point", + "descriptionFull": "Scor 1000 point i ${LEVEL}", + "descriptionFullComplete": "Du scorede 1000 point i ${LEVEL}", + "name": "Troldmand i ${LEVEL}" + }, + "Precision Bombing": { + "description": "Vind uden at bruge nogen powerups", + "descriptionComplete": "Du vandt uden at bruge nogen powerups", + "descriptionFull": "Vind ${LEVEL} uden at bruge nogen powerups", + "descriptionFullComplete": "Du vandt ${LEVEL} uden at bruge nogen powerups", + "name": "Præcisionsbombning" + }, + "Pro Boxer": { + "description": "Vind uden at bruge nogen bomber", + "descriptionComplete": "Du vandt uden at bruge nogen bomber", + "descriptionFull": "Gennemfør ${LEVEL} uden at bruge nogen bomber", + "descriptionFullComplete": "Du gennemførte ${LEVEL} uden at bruge nogen bomber", + "name": "Professionel bokser" + }, + "Pro Football Shutout": { + "description": "Vind uden at lade fjenderne score", + "descriptionComplete": "Du vandt uden at lade fjenderne score", + "descriptionFull": "Vind ${LEVEL} uden at lade fjenderne score", + "descriptionFullComplete": "Du vandt ${LEVEL} uden at lade fjenderne score", + "name": "Straffespark i ${LEVEL}" + }, + "Pro Football Victory": { + "description": "Vind kampen", + "descriptionComplete": "Du vandt kampen", + "descriptionFull": "Vind kampen i ${LEVEL}", + "descriptionFullComplete": "Du vandt kampen i ${LEVEL}", + "name": "Sejr i ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Besejr alle bølger", + "descriptionComplete": "Du besejrede alle bølger", + "descriptionFull": "Besejr alle bølger i ${LEVEL}", + "descriptionFullComplete": "Du besejrede alle bølger af ${LEVEL}", + "name": "Sejr i ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Gennemfør alle bølger", + "descriptionComplete": "Du gennemførte alle bølger", + "descriptionFull": "Gennemfør alle bølger i ${LEVEL}", + "descriptionFullComplete": "Du gennemførte alle bølger i ${LEVEL}", + "name": "Sejr i ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Vind uden at fjenderne scorer", + "descriptionComplete": "Du vandt uden at fjenderne scorede", + "descriptionFull": "Vind ${LEVEL} uden at fjenderne scorer", + "descriptionFullComplete": "Du vandt ${LEVEL} uden at fjenderne scorede", + "name": "Straffespark i ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Vind kampen", + "descriptionComplete": "Du vandt kampen", + "descriptionFull": "Vind kampen i ${LEVEL}", + "descriptionFullComplete": "Du vandt kampen i ${LEVEL}", + "name": "Sejr i ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Besejr alle bølger", + "descriptionComplete": "Du besejrede alle bølger", + "descriptionFull": "Besejr alle bølger i ${LEVEL}", + "descriptionFullComplete": "Du besejrede alle bølger i ${LEVEL}", + "name": "Sejr i ${LEVEL}" + }, + "Runaround God": { + "description": "Scor 2000 point", + "descriptionComplete": "Du scorede 2000 point", + "descriptionFull": "Scor 2000 point i ${LEVEL}", + "descriptionFullComplete": "Du scorede 2000 point i ${LEVEL}", + "name": "Gud i ${LEVEL}" + }, + "Runaround Master": { + "description": "Scor 500 point", + "descriptionComplete": "Du scorede 500 point", + "descriptionFull": "Scor 500 point i ${LEVEL}", + "descriptionFullComplete": "Du scorede 500 point i ${LEVEL}", + "name": "Mester i ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Scor 1000 point", + "descriptionComplete": "Du scorede 1000 point", + "descriptionFull": "Scor 1000 point i ${LEVEL}", + "descriptionFullComplete": "Du scorede 1000 point i ${LEVEL}", + "name": "Troldmand i ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Del spillet med en ven", + "descriptionFullComplete": "Har delt spillet med en ven", + "name": "Sharing is Caring" + }, + "Stayin' Alive": { + "description": "Vind uden at dø", + "descriptionComplete": "Du vandt uden at dø", + "descriptionFull": "Vind ${LEVEL} uden at dø", + "descriptionFullComplete": "Du vandt ${LEVEL} uden at dø", + "name": "Forbliv i live" + }, + "Super Mega Punch": { + "description": "Giv 100% skade med ét slag", + "descriptionComplete": "Du gav 100% skade med ét slag", + "descriptionFull": "Giv 100% skade med ét slag i ${LEVEL}", + "descriptionFullComplete": "Du gav 100% skade med ét slag i ${LEVEL}", + "name": "Super Mega Slag" + }, + "Super Punch": { + "description": "Giv 50% skade med ét slag", + "descriptionComplete": "Du gav 50% skade med ét slag", + "descriptionFull": "Giv 50% skade med ét slag i ${LEVEL}", + "descriptionFullComplete": "Du gav 50% skade med ét slag i ${LEVEL}", + "name": "Super slag" + }, + "TNT Terror": { + "description": "Dræb 6 fjender med TNT", + "descriptionComplete": "Du dræbte 6 fjender med TNT", + "descriptionFull": "Dræb 6 fjender med TNT i ${LEVEL}", + "descriptionFullComplete": "Du dræbte 6 fjender med TNT i ${LEVEL}", + "name": "TNT-terror" + }, + "Team Player": { + "descriptionFull": "Start et Hold spil med mere end 4 spillere", + "descriptionFullComplete": "Startede et Hold spil med mere end 4 spillere", + "name": "Hold Spiller" + }, + "The Great Wall": { + "description": "Stop hver eneste fjende", + "descriptionComplete": "Du stoppede hver eneste fjende", + "descriptionFull": "Stop hver eneste fjende i ${LEVEL}", + "descriptionFullComplete": "Du stoppede hver eneste fjende i ${LEVEL}", + "name": "Den Store Mur" + }, + "The Wall": { + "description": "Stop hver eneste fjende", + "descriptionComplete": "Du stoppede hver eneste fjende", + "descriptionFull": "Stop hver eneste fjende i ${LEVEL}", + "descriptionFullComplete": "Du stoppede hver eneste fjende i ${LEVEL}", + "name": "Muren" + }, + "Uber Football Shutout": { + "description": "Vind uden at fjenderne scorer", + "descriptionComplete": "Du vandt uden at fjenden scorede", + "descriptionFull": "Vind ${LEVEL} uden at fjenden scorer", + "descriptionFullComplete": "Du vandt ${LEVEL} uden at fjenden scorede", + "name": "Straffespark i ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Vind kampen", + "descriptionComplete": "Du vandt kampen", + "descriptionFull": "Vind kampen i ${LEVEL}", + "descriptionFullComplete": "Du vandt kampen i ${LEVEL}", + "name": "Sejr i ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Besejr alle bølger", + "descriptionComplete": "Du besejrede alle bølger", + "descriptionFull": "Besejr alle bølger i ${LEVEL}", + "descriptionFullComplete": "Du gennemførte alle bølger i ${LEVEL}", + "name": "Sejr i ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Gennemfør alle bølger", + "descriptionComplete": "Du gennemførte alle bølger", + "descriptionFull": "Gennemfør alle bølger i ${LEVEL}", + "descriptionFullComplete": "Du gennemførte alle bølger i ${LEVEL}", + "name": "Sejr i ${LEVEL}" + } + }, + "achievementsRemainingText": "Achievements tilbage:", + "achievementsText": "Achievements", + "achievementsUnavailableForOldSeasonsText": "Beklager, det er ikke muligt at se detaljer for tidligere sæsoner.", + "addGameWindow": { + "getMoreGamesText": "Få Flere Spil...", + "titleText": "Tilføj spil" + }, + "allowText": "Tillad", + "alreadySignedInText": "Din konto er logget ind fra en anden enhed;\nvær sød at skifte konto eller luk spillet ned på\ndine andre enheder og prøv igen", + "apiVersionErrorText": "Kan ikke indlæse ${NAME}; det er målrettet api-version ${VERSION_USED}; vi behøver ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(Sætter kun dette til automatisk når høretelefoner er sat i)", + "headRelativeVRAudioText": "Head-Relative VR Audio", + "musicVolumeText": "Musiklydstyrke", + "soundVolumeText": "Lydstyrke", + "soundtrackButtonText": "Lydspor", + "soundtrackDescriptionText": "(Afspil din egen musik, mens du spiller)", + "titleText": "Lyd" + }, + "autoText": "Auto", + "backText": "Tilbage", + "banThisPlayerText": "Ban denne spiller", + "bestOfFinalText": "Bedst ud af ${COUNT} finale", + "bestOfSeriesText": "Bedst ud af ${COUNT}:", + "bestRankText": "Dine bedste er #${RANK}", + "bestRatingText": "Din bedste vurdering er ${RATING}", + "betaErrorText": "Denne beta er ikke længere aktiv; tjek venligst om der er en ny version tilgængelig", + "betaValidateErrorText": "Ude af stand til til at validere beta. Har du tjekket din internetforbindelse?", + "betaValidatedText": "Beta valideret; Nyd den!", + "bombBoldText": "BOMB", + "bombText": "Bomb", + "boostText": "Boost", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} konfigureres i selve app'en.", + "buttonText": "Knap", + "canWeDebugText": "Er det okay at BombSquad automatisk rapporterer fejl, \ncrashes og generelt brug til udvikleren?\n\nDenne data indeholder ingen personlige informationer og hjælper med \nat få spillet til at kører flydende og uden fejl.", + "cancelText": "Annuller", + "cantConfigureDeviceText": "Undskyld, ${DEVICE} kan ikke konfigureres.", + "challengeEndedText": "Udfordringen er afsluttet.", + "chatMuteText": "Slå chat fra", + "chatMutedText": "Chat fra", + "chatUnMuteText": "Slå chat til", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Du bliver nødt til at gennemføre\ndette level for at fortsætte!", + "completionBonusText": "Gennemføringsbonus", + "configControllersWindow": { + "configureControllersText": "Konfigurer Controllers", + "configureGamepadsText": "Konfigurér Gamepads", + "configureKeyboard2Text": "Konfigurér Tastatur P2", + "configureKeyboardText": "Konfigurér Tastatur", + "configureMobileText": "Mobilenheder som Controllers", + "configureTouchText": "Konfigurér touchskærm", + "ps3Text": "PS3-controllere", + "titleText": "Controllere", + "wiimotesText": "Wiimotes", + "xbox360Text": "Xbox 360-controllere" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Bemærk: Controllersupport varierer fra enhed til enhed og Android-version.", + "pressAnyButtonText": "Tryk på en hvilkårlig knap på controller\nsom du gerne vil konfigurere.", + "titleText": "Konfigurér Controllers" + }, + "configGamepadWindow": { + "advancedText": "Avanceret", + "advancedTitleText": "Avanceret Controllersetup", + "analogStickDeadZoneDescriptionText": "(Skru dette op, hvis din spiller 'drifter' efter du har sluppet pinden)", + "analogStickDeadZoneText": "Den analoge pinds dødszone", + "appliesToAllText": "(gælder alle controllers af denne type)", + "autoRecalibrateDescriptionText": "(Aktivér dette, hvis din spiller ikke bevæger sig med fuld hastighed)", + "autoRecalibrateText": "Re-kalibrer automatisk den analoge pind", + "axisText": "akse", + "clearText": "Ryd", + "dpadText": "dpad", + "extraStartButtonText": "Ekstra startknap", + "ifNothingHappensTryAnalogText": "Hvis intet sker, så prøv det analoge stik i stedet.", + "ifNothingHappensTryDpadText": "Hvis intet sker, så prøv d-pad'en i stedet.", + "ignoreCompletelyDescriptionText": "(forhindrer denne kontroller i at påvirke spillet eller menuerne)", + "ignoreCompletelyText": "Ignorer Fuldstændigt", + "ignoredButton1Text": "Ignoreret knap 1", + "ignoredButton2Text": "Ignoreret knap 2", + "ignoredButton3Text": "Ignoreret knap 3", + "ignoredButton4Text": "Ignoreret Knap 4", + "ignoredButtonDescriptionText": "(Brug denne for at undgå 'hjem'- eller 'sync'-knapperne påvirker UI'en)", + "ignoredButtonText": "Ignorerede knapper", + "pressAnyAnalogTriggerText": "Tryk på hvilken som helst analog udløser...", + "pressAnyButtonOrDpadText": "Tryk på hvilken som helst knap eller dpad...", + "pressAnyButtonText": "Tryk på hvilken som helst knap...", + "pressLeftRightText": "Tryk venstre eller højre...", + "pressUpDownText": "Tryk op eller ned...", + "runButton1Text": "Løbeknap 1", + "runButton2Text": "Løbeknap 2", + "runTrigger1Text": "Løbeudløser 1", + "runTrigger2Text": "Løbeudløser 2", + "runTriggerDescriptionText": "(analoge udløserere lader dig løbe i forskellige hastigheder)", + "secondHalfText": "Brug denne til at konfigurere den anden\ndel af en 2-i-1-enhed, som\nviser sig som en enkelt gamepad.", + "secondaryEnableText": "Aktivér", + "secondaryText": "Sekundær Controller", + "startButtonActivatesDefaultDescriptionText": "(sluk denne, hvis din 'start'-knap fungerer mere som en 'menu'-knap)", + "startButtonActivatesDefaultText": "Startknap aktiverer standard widget", + "titleText": "Controllersetup", + "twoInOneSetupText": "2-i-1 controllersetup", + "uiOnlyDescriptionText": "(forhindre, at denne controller deltager i et spil)", + "uiOnlyText": "Begræns til Menu Brug", + "unassignedButtonsRunText": "Alle knapper, der ikke er i brug, bruges som løbeknap", + "unsetText": "", + "vrReorientButtonText": "VR Positionsnulstillings Knap" + }, + "configKeyboardWindow": { + "configuringText": "Konfigurerer ${DEVICE}", + "keyboard2NoteScale": 0.7, + "keyboard2NoteText": "Bemærk: De fleste tastaturer kan kun registrere nogle få tryk\nad gangen, så det er en god idé at have et ekstra\ntastatur. Bemærk, at du stadig skal registrere unikke\nknapfunktioner til de to spillere." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Action kontrol vægt", + "actionsText": "Handlinger", + "buttonsText": "knapper", + "dragControlsText": "< træk styringerne for at replacere dem >", + "joystickText": "joystick", + "movementControlScaleText": "Bevægelseskontrol vægt", + "movementText": "Bevægelse", + "resetText": "Nulstil", + "swipeControlsHiddenText": "Gem 'swipe' ikoner.", + "swipeInfoText": "'Swipe'-kontrol tager lidt tid at vænne sig til, men\ndet gør det lettere at spille uden at kigge på knapperne.", + "swipeText": "swipe", + "titleText": "Konfigurér touchskærm", + "touchControlsScaleText": "Skala ift. berøringsstyring" + }, + "configureItNowText": "Konfigurér den nu?", + "configureText": "Konfigurér", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsScale": 0.65, + "bestResultsText": "For at få de bedste resultater skal du bruge et lag-frit wifi-netværk.\nDu kan reducere wifi-lag ved at slukke for andre trådløse enheder,\nved at spille tæt ved din wifi-router og ved at forbinde spilværten\ndirekte til netværket via internet.", + "explanationScale": 0.8, + "explanationText": "For at bruge en smartphone eller tablet som en trådløs controller,\nskal du installere app'en \"${REMOTE_APP_NAME}\". Et utal af enheder\nkan forbinde til et ${APP_NAME} spil via WiFi, og det er helt gratis!", + "forAndroidText": "til Android:", + "forIOSText": "til iOS:", + "getItForText": "Få ${REMOTE_APP_NAME} til iOS i Apple App Store eller til\nAndroid på Google Play Store eller Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Mobile enheder som controllers:" + }, + "continuePurchaseText": "Fortsæt for ${PRICE}?", + "continueText": "Fortsæt", + "controlsText": "Styring", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Nem mindenki Joshua", + "activenessInfoText": "Denne multiplikator siger på dage hvor du\nspiller og falder på dage hvor du ikke spiller.", + "activityText": "Aktivitet", + "campaignText": "Kampagne", + "challengesInfoText": "Tjen præmier ved at gennemføre mini-games.\n\nPræmier og vanskelighedniveauer stiger \nhver gang en udfordring er gennemført og\nfalder når én udløber eller er annuleret.", + "challengesText": "Udfordringer", + "currentBestText": "Nuværende bedste", + "customText": "Brugerdefineret", + "entryFeeText": "Entré", + "forfeitConfirmText": "Opgiv denne udfordring?", + "forfeitNotAllowedYetText": "Denne udfordring kan ikke opgives endnu.", + "forfeitText": "Opgiv", + "multipliersText": "Multiplikatorer", + "nextChallengeText": "Næste udfordring", + "nextPlayText": "Næste spil", + "ofTotalTimeText": "ud af ${TOTAL}", + "playNowText": "Spil nu", + "pointsText": "Point", + "powerRankingFinishedSeasonUnrankedText": "(færdiggjorde sæson urangeret)", + "powerRankingNotInTopText": "(ikke i top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} point", + "powerRankingPointsMultText": "(x ${NUMBER} point)", + "powerRankingPointsText": "${NUMBER} point", + "powerRankingPointsToRankedText": "(${CURRENT} ud af ${REMAINING} point)", + "powerRankingText": "Power Rangering", + "prizesText": "Priser", + "proMultInfoText": "Spillere med en ${PRO} opgradering\nfår ${PERCENT}% point boost her", + "seeMoreText": "Flere...", + "skipWaitText": "Skip at vente", + "timeRemainingText": "Tid tilbage", + "titleText": "Co-op", + "toRankedText": "Til rangeret", + "totalText": "i alt", + "tournamentInfoText": "Konkurrer efter high scores med\nandre spillere i din liga.\n\nPræmier er tildelt til de top scorende\nspillere når turneringens tid er udløbet.", + "welcome1Text": "Velkommen til ${LEAGUE}. Du kan forbedre din\nliga rang ved at indtjene stjerne bedømmelser, klare\npræstationer og vinde trofæer i turneringer.", + "welcome2Text": "Du kan også vinde billetter fra mange af de samme aktiviteter.\nBilletter kan blive brugt til oplåsning af nye spilkarakterer, baner, og\nmini-games, at komme ind i turneringer, og meget mere.", + "yourPowerRankingText": "Din Power Rangering" + }, + "copyOfText": "${NAME} kopi", + "copyText": "Kopier", + "copyrightText": "© 2013 Eric Froemling", + "createAPlayerProfileText": "Opret en spillerprofil?", + "createEditPlayerText": "", + "createText": "Opret", + "creditsWindow": { + "additionalAudioArtIdeasText": "Ekstra lyd, tidlige illustrationer og idéer af ${NAME}", + "additionalMusicFromText": "Ekstra musik fra ${NAME}", + "allMyFamilyText": "Alle mine venner og familie, der hjalp med at spilteste", + "codingGraphicsAudioText": "Kodning, grafik og lyd af ${NAME}", + "languageTranslationsText": "Sprogoversættelse:", + "legalText": "Juridisk:", + "publicDomainMusicViaText": "Offentlig ejendomsmusik via ${NAME}", + "softwareBasedOnText": "Denne software er delvist baseret på arbejde udført af ${NAME}", + "songCreditText": "${TITLE} Sunget af ${PERFORMER}\nKomponeret af ${COMPOSER}, Arrangeret af ${ARRANGER}, udgivet af ${PUBLISHER},\nVenligst stillet til rådighed af ${SOURCE}", + "soundAndMusicText": "Lyd & Musik:", + "soundsText": "Lyde (${SOURCE}):", + "specialThanksText": "Specielt tak til:", + "thanksEspeciallyToText": "Specielt tak til ${NAME}", + "titleText": "${APP_NAME} Credits", + "whoeverInventedCoffeeText": "Den person, der opfandt kaffe" + }, + "currentStandingText": "Din nuværende placering er #${RANK}", + "customizeText": "Tilpas...", + "deathsTallyText": "Antal gange død: ${COUNT}", + "deathsText": "Antal gange død", + "debugText": "debug", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Obs: Det anbefales at du sætter Indstillinger->Grafik-> Teksturer til 'høj' når du tester dette.", + "runCPUBenchmarkText": "Kør CPU Benchmark", + "runGPUBenchmarkText": "Kør GPU Benchmark", + "runMediaReloadBenchmarkText": "Kør Media-Reload Benchmark", + "runStressTestText": "Kør stresstest", + "stressTestPlayerCountText": "Antal spillere", + "stressTestPlaylistDescriptionText": "Stresstest playliste", + "stressTestPlaylistNameText": "Navn på playliste", + "stressTestPlaylistTypeText": "Playlistens type", + "stressTestRoundDurationText": "Varighed af runde", + "stressTestTitleText": "Stresstest", + "titleText": "Benchmark- og stresstests", + "totalReloadTimeText": "Totale genindlæsningstid: ${TIME} (se log for detaljer)", + "unlockCoopText": "Åbn co-op levels" + }, + "defaultFreeForAllGameListNameText": "Standard alle mod alle-spil", + "defaultGameListNameText": "Standard ${PLAYMODE} spilleliste", + "defaultNewFreeForAllGameListNameText": "Mine alle mod alle-spil", + "defaultNewGameListNameText": "Min ${PLAYMODE} playliste", + "defaultNewTeamGameListNameText": "Mine holdspil", + "defaultTeamGameListNameText": "Standard holdspil", + "deleteText": "Slet", + "demoText": "Demo", + "denyText": "Afvis", + "desktopResText": "Computeropløsning", + "difficultyEasyText": "Nemt", + "difficultyHardOnlyText": "Svær niveautilstand udelukkende", + "difficultyHardText": "Svær", + "difficultyHardUnlockOnlyText": "Dette niveau kan kun blive oplåst på svært niveautilstand.\nTror du at du har hvad der skal til!?!?!?", + "directBrowserToURLText": "Henvis venligst en web-browser til den følgende URL:", + "disableRemoteAppConnectionsText": "Deaktiver Remote-App forbindelser", + "disableXInputDescriptionText": "Tillader mere end 4 kontrollere men måske virker det ikke", + "disableXInputText": "Deaktiver XInput", + "doneText": "Færdig", + "drawText": "Uafgjort", + "duplicateText": "Dupliker", + "editGameListWindow": { + "addGameText": "Tilføj spil", + "cantOverwriteDefaultText": "Kan ikke overskrive standard spilliste!", + "cantSaveAlreadyExistsText": "En spilleliste med det navn eksisterer allerede!", + "cantSaveEmptyListText": "En tom spilleliste kan ikke gemmes!", + "editGameText": "Rediger\nspil", + "gameListText": "Spilliste", + "listNameText": "Spillelistens navn", + "nameText": "Navn", + "removeGameText": "Fjern\nspil", + "saveText": "Gem liste", + "titleText": "Spilleliste editor" + }, + "editProfileWindow": { + "accountProfileInfoText": "Denne specielle profil har et navn\nog et ikon baseret på din konto\n\n${ICONS}\n\nLav brugdefinerede profiler for at bruge\nforskellige navne og brugerdefinerede ikoner.", + "accountProfileText": "(konto profil)", + "availableText": "Dette navn \"${NAME}\" er ledigt.", + "changesNotAffectText": "Bemærk: ændringer vil ikke have nogen indvirkning på spillere, som allerede er i spil.", + "characterText": "karakter", + "checkingAvailabilityText": "Checker muligheden for \"${NAME}\"...", + "colorText": "farve", + "getMoreCharactersText": "Få flere karakterer...", + "getMoreIconsText": "Få flere ikoner...", + "globalProfileInfoText": "Globale spillerprofiler er garanteret til at have unikke\nnavne verden over. Det samme gælder brugerdefinerede ikoner.", + "globalProfileText": "(global profil)", + "highlightText": "fremhæv", + "iconText": "ikon", + "localProfileInfoText": "Lokale spillerprofiler har ikke ikoner og deres navne er\nikke garanteret til at være unikke. Opgrader til en global profil\nfor at reservere dit unikke navn og tilføje et brugerdefineret ikon.", + "localProfileText": "(lokal profil)", + "nameDescriptionText": "Spillernavn", + "nameText": "Navn", + "randomText": "tilfældig", + "titleEditText": "Rediger profil", + "titleNewText": "Ny profil", + "unavailableText": "\"${NAME}\" er ikke ledigt; prøv et andet navn.", + "upgradeProfileInfoText": "Dette vil reservere dit spillernavn i hele verden\nog tillade dig at tildele et brugerdefineret ikon til det.", + "upgradeToGlobalProfileText": "Opgrader til Global Profil" + }, + "editProfilesAnyTimeText": "(du kan redigere profiler på hvilket som helst tidspunkt under 'indstillinger')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Du kan ikke slette standard lydspor.", + "cantEditDefaultText": "Kan ikke redigere standard lydspor. Kopier den eller lav en ny.", + "cantOverwriteDefaultText": "Kan ikke overskrive standard lydspor", + "cantSaveAlreadyExistsText": "Et lydspor med det navn eksisterer allerede!", + "copyText": "Kopier", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Standard lydspor", + "deleteConfirmText": "Slet Lydspor:\n\n'${NAME}'?", + "deleteText": "Slet\nLydspor", + "duplicateText": "Dupliker\nLydspor", + "editSoundtrackText": "Lydspor Editor", + "editText": "Rediger\nLydspor", + "fetchingITunesText": "Henter iTunes spilleliste...", + "musicVolumeZeroWarning": "Advarsel: Din lydstyrke i spillet er sat til 0", + "nameText": "Navn", + "newSoundtrackNameText": "Mit lydspor ${COUNT}", + "newSoundtrackText": "Nyt lydspor:", + "newText": "Nyt\nLydspor", + "selectAPlaylistText": "Vælg en spilleliste", + "selectASourceText": "Musikkilde", + "soundtrackText": "Lydspor", + "testText": "Hør", + "titleText": "Lydspor", + "useDefaultGameMusicText": "Standard spilmusik", + "useITunesPlaylistText": "iTunesplayliste", + "useMusicFileText": "Musikfil (mp3 osv)", + "useMusicFolderText": "Mappe med musikfiler" + }, + "editText": "Rediger", + "endText": "Afslut", + "enjoyText": "God fornøjelse!", + "epicDescriptionFilterText": "${DESCRIPTION} i imponerende slowmotion.", + "epicNameFilterText": "Imponerende ${NAME}", + "errorAccessDeniedText": "Adgang nægtet", + "errorOutOfDiskSpaceText": "ikke nok disk plads", + "errorText": "Fejl", + "errorUnknownText": "ukendt fejl", + "exitGameText": "Afslut ${APP_NAME}?", + "exportSuccessText": "'${NAME}' eksporteret.", + "externalStorageText": "Ekstern lagring", + "failText": "Du tabte", + "fatalErrorText": "Uh åh; noget mangler eller er i stykker.\nVær venlig og geninstaller appen eller\nkontakt ${EMAIL} for hjælp.", + "fileSelectorWindow": { + "titleFileFolderText": "Vælg en fil eller mappe", + "titleFileText": "Vælg en fil", + "titleFolderText": "Vælg en mappe", + "useThisFolderButtonText": "Brug denne mappe" + }, + "finalScoreText": "Endelig score", + "finalScoresText": "Endelige scorer", + "finalTimeText": "Endelig tid", + "finishingInstallText": "Afslutter installationen; et øjeblik...", + "fireTVRemoteWarningText": "* For at få den bedste oplevelse, brug \nspilkontrollere eller installer\n'${REMOTE_APP_NAME}' appen på din \ntelefon eller tablet.", + "firstToFinalText": "Først til ${COUNT} – Finale", + "firstToSeriesText": "Første til ${COUNT}", + "fiveKillText": "FEM DRAB!!!!", + "flawlessWaveText": "Fejlfri bølge!", + "fourKillText": "FIREDOBBELT DRAB!!!!", + "freeForAllText": "Alle mod alle", + "friendScoresUnavailableText": "Dine venners scorer er utilgængelige.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Stilling efter ${COUNT}. spil:", + "gameListWindow": { + "cantDeleteDefaultText": "Du kan ikke slette standard spilliste!", + "cantEditDefaultText": "Kan ikke redigere i standardspillisten! Dupliker den eller lav en ny.", + "cantShareDefaultText": "Du kan ikke dele den standard spilliste.", + "deleteConfirmText": "Slet \"${LIST}\"?", + "deleteText": "Slet\nSpilliste", + "duplicateText": "Dupliker\nSpilliste", + "editText": "Rediger\nSpilliste", + "gameListText": "Spilliste", + "newText": "Ny\nSpilliste", + "showTutorialText": "Vis tutorial", + "shuffleGameOrderText": "Bland rækkefølgen", + "titleText": "Tilpas ${TYPE} Spillister" + }, + "gameSettingsWindow": { + "addGameText": "Tilføj spil" + }, + "gamepadDetectedText": "1 gamepad registreret", + "gamepadsDetectedText": "${COUNT} gamepads registreret.", + "gamesToText": "${WINCOUNT} til ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Husk: alle enheder i en gruppe kan have mere\nend en spiller hvis du har nok kontrollere.", + "aboutDescriptionText": "Brug disse faner for at samle en gruppe.\n\nGrupper lader dig spille spil og turneringer\nmed dine venner gennem forskellige enheder.\n\nBrug ${PARTY} knappen øverst til højre for\nat chatte og interagere med din gruppe.\n(på en kontroller, pres ${BUTTON} når du er i menuen)", + "aboutText": "Om", + "addressFetchErrorText": "", + "appInviteMessageText": "${NAME} sendte dig ${COUNT} billetter i ${APP_NAME}", + "appInviteSendACodeText": "Send dem en kode", + "appInviteTitleText": "${APP_NAME} App Inviter", + "bluetoothAndroidSupportText": "(virker med alle android enheder, som supporter Bluetooth)", + "bluetoothDescriptionText": "Lav/deltag en gruppe over Bluetooth:", + "bluetoothHostText": "Lav en gruppe over Bluetooth", + "bluetoothJoinText": "Deltag over Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "kontrollerer...", + "dedicatedServerInfoText": "For de bedste resultater, lav en dedikeret server. Se bombsquadgame.com/server for at lære hvordan.", + "disconnectClientsText": "Dette vil koble ${COUNT} spiller(e) fra\ndin gruppe. Er du sikker?", + "earnTicketsForRecommendingAmountText": "Venner vil modtage ${COUNT} billetter hvis de prøver spillet\n(og du vil modtage ${YOU_COUNT} for hver, som gør)", + "earnTicketsForRecommendingText": "Del spiller\nfor gratis billetter...", + "emailItText": "Email det", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} billetter fra ${NAME}", + "friendPromoCodeAwardText": "Du vil modtage ${COUNT} billetter hver gang det bliver brugt.", + "friendPromoCodeExpireText": "Denne kode vil udløbe om ${EXPIRE_HOURS} timer og vil kun virke for nye spillere.", + "friendPromoCodeInstructionsText": "For at bruge det, åben ${APP_NAME} og gå ind i \"Indstillinger->Avanceret->Indtast Kode\".\nSe bombsquadgame.com for downloadlinks til alle understøttede platforme.", + "friendPromoCodeRedeemLongText": "Det kan blive indløst for ${COUNT} gratis billetter for op til ${MAX_USES} personer.", + "friendPromoCodeRedeemShortText": "Det kan blive indløst for ${COUNT} billetter i spillet.", + "friendPromoCodeWhereToEnterText": "(i \"Indstillinger->Avanceret->Indtast Kode\")", + "getFriendInviteCodeText": "Få invitationskode til venner", + "googlePlayDescriptionText": "Inviter Google Play spillere til din gruppe:", + "googlePlayInviteText": "Inviter", + "googlePlayReInviteText": "Der er ${COUNT} Google Play spiller(e) i din gruppe\nsom vil blive koblet fra hvis du starter en ny invitation.\nInkluder dem i den nye invitation for at få dem tilbage.", + "googlePlaySeeInvitesText": "Se inviterede", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play version)", + "hostPublicPartyDescriptionText": "Lav en offentlig gruppe:", + "inDevelopmentWarningText": "Note:\n\nNetværksspil er en ny og stadig udviklende funktion.\nFor nu, anbefales det stærkt, at alle\nspillere er på det samme WiFi-netværk.", + "internetText": "Internet", + "inviteAFriendText": "Venner har ikke spillet? Inviter de til at\nprøve det og så modtager de ${COUNT} gratis billetter.", + "inviteFriendsText": "Inviter venner", + "joinPublicPartyDescriptionText": "Deltag i en offentlig gruppe:", + "localNetworkDescriptionText": "Deltag i en gruppe på dit netværk:", + "localNetworkText": "Lokalt netværk", + "makePartyPrivateText": "Gør min gruppe privat", + "makePartyPublicText": "Gør min gruppe offentlig", + "manualAddressText": "Adresse", + "manualConnectText": "Opret forbindelse", + "manualDescriptionText": "Deltag i en gruppe på en adresse:", + "manualJoinableFromInternetText": "Kan du deltage via internettet?:", + "manualJoinableNoWithAsteriskText": "NEJ*", + "manualJoinableYesText": "JA", + "manualRouterForwardingText": "*For at løse dette, prøv at konfigurere din router til at videresende UDP-port ${PORT} til din lokale adresse", + "manualText": "Manual", + "manualYourAddressFromInternetText": "Din adresse fra internettet:", + "manualYourLocalAddressText": "Din lokale adresse:", + "noConnectionText": "", + "otherVersionsText": "(andre versioner)", + "partyInviteAcceptText": "Accepter", + "partyInviteDeclineText": "Afvis", + "partyInviteGooglePlayExtraText": "(Se 'Google Play' fanen i 'Saml' vinduet)", + "partyInviteIgnoreText": "Ignorer", + "partyInviteText": "${NAME} har inviteret\ndig til at deltage i deres gruppe!", + "partyNameText": "Gruppe navn", + "partySizeText": "gruppe størrelse", + "partyStatusCheckingText": "kontrollerer status...", + "partyStatusJoinableText": "din gruppe kan nu tilsluttes fra internettet", + "partyStatusNoConnectionText": "kunne ikke forbinde til serveren", + "partyStatusNotJoinableText": "din gruppe kan ikke tilsluttes fra internettet", + "partyStatusNotPublicText": "din gruppe er ikke offentlig", + "pingText": "ping", + "portText": "Port", + "requestingAPromoCodeText": "Anmoder om kode...", + "sendDirectInvitesText": "Send direkte invitationer", + "shareThisCodeWithFriendsText": "Del denne kode med dine venner:", + "showMyAddressText": "Vis min adresse", + "titleText": "Saml", + "wifiDirectDescriptionBottomText": "Hvis alle enheder har et 'WiFi Direkte' panel, skulle de være i stand til at bruge det til at finde\nog forbinde til hinanden. Når alle enheder er tilsluttet, kan du danne grupper\nher ved hjælp af fanen 'Lokalt netværk', præcis på samme måde som med et almindeligt WiFi-netværk.\n\nFor de bedste resultater skal WiFi Direct værten også være ${APP_NAME} gruppeværten.", + "wifiDirectDescriptionTopText": "WiFi Direkte kan bruges til at forbinde Android-enheder direkte uden\nbrug for et WiFi-netværk. Dette fungerer bedst på Android 4.2 eller nyere.\n\nFor at bruge det skal du åbne WiFi-indstillingerne og kigge efter 'WiFi Direkte' i menuen.", + "wifiDirectOpenWiFiSettingsText": "Åben WiFi indstillinger", + "wifiDirectText": "WiFi Direkte", + "worksBetweenAllPlatformsText": "(virker imellem alle platforme)", + "worksWithGooglePlayDevicesText": "(fungerer med enheder, der kører Google Play (Android) versionen af ​​spillet)", + "youHaveBeenSentAPromoCodeText": "Du har modtaget en ${APP_NAME} promoveringskode:" + }, + "getCoinsWindow": { + "coinDoublerText": "Coinfordobler", + "coinsText": "${COUNT} coins", + "freeCoinsText": "Gratis coins", + "restorePurchasesText": "Genskab køb", + "titleText": "Få coins" + }, + "getTicketsWindow": { + "freeText": "GRATIS!", + "freeTicketsText": "Gratis billetter", + "inProgressText": "En transaktion er igang; vær venlig at prøve igen om lidt.", + "purchasesRestoredText": "Køb gendannet.", + "receivedTicketsText": "Modtog ${COUNT} billetter!", + "restorePurchasesText": "Gendan køb", + "ticketPack1Text": "Lille billetpakke", + "ticketPack2Text": "Mellem billetpakke", + "ticketPack3Text": "Stor billetpakke", + "ticketPack4Text": "Jumbo billetpakke", + "ticketPack5Text": "Mammut billetpakke", + "ticketPack6Text": "Ultimativ billetpakke", + "ticketsFromASponsorText": "Få ${COUNT} billetter\nfra en sponsor", + "ticketsText": "${COUNT} billetter", + "titleText": "Få billetter", + "unavailableLinkAccountText": "Beklager, køb er ikke tilgængeligt på denne platform.\nSom en løsning kan du forbinde denne konto til en anden konto på\nen anden platform og foretage købene der.", + "unavailableTemporarilyText": "Dette er i øjeblikket ikke tilgængeligt; prøv igen senere.", + "unavailableText": "Beklager, dette er ikke muligt.", + "versionTooOldText": "Beklager, denne version af spillet er for gammelt; vær venlig at opdatere til en nyere.", + "youHaveShortText": "Du har ${COUNT}", + "youHaveText": "du har ${COUNT} billetter" + }, + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Altid", + "fullScreenCmdText": "Fuld skærm (Cmd-F)", + "fullScreenCtrlText": "Fuld skærm (Ctrl-F)", + "gammaText": "Gamma", + "highText": "Høj", + "higherText": "Højere", + "lowText": "Lav", + "mediumText": "Mellem", + "neverText": "Aldrig", + "resolutionText": "Opløsning", + "showFPSText": "Vis FPS", + "texturesText": "Teksturer", + "titleText": "Grafik", + "tvBorderText": "TV-grænse", + "verticalSyncText": "Vertikal synkronisering", + "visualsText": "Visuals" + }, + "helpWindow": { + "bombInfoText": "- Bombe -\nStærkere end slag men den kan\nresultere i stor skade på dig selv.\nFor at få det bedste resultat kast bomben\nmod fjenden, før lunten løber ud.", + "bombInfoTextScale": 0.6, + "canHelpText": "${APP_NAME} kan hjælpe.", + "controllersInfoText": "Du kan spille ${APP_NAME} med venner over et netværk eller I\nkan alle spille på den samme enhed, hvis du har nok kontrollere.\n${APP_NAME} understøtter en række af dem; du kan endda bruge telefoner\nsom kontrollere via den gratis '${REMOTE_APP_NAME}' app.\nSe Indstillinger->Kontrollere for mere information.", + "controllersInfoTextFantasia": "BombSquad understøtter de fleste USB- og Bluetooth-gamepads ligesom xBox 360-controllere\nDu kan også bruge iOS/Android-enheder som controllere via den gratis \"BombSquad Remote\" app.\nSe \"Controllere\" under \"Indstillinger\" for mere info.", + "controllersInfoTextMac": "En eller to spillere kan bruge tastaturet, men Bombsquad er bedst med gamepads. \nBombSquad kan bruge USB-gamepads, PS3-controllere, xBox 360-controllere,\nWiimotes og iOS/Android-enheder til at spille med. Forhåbentlig har du en af disse.\nSe 'Controllere' under 'Indstillinger' for mere info.", + "controllersInfoTextOuya": "Du kan bruge OUYA-controllere, PS3-controllere, xBox 360-controllere,\nog mange andre USB- og Bluetooth-gamepads med BombSquad.\nDu kan også bruge iOS/Android-enheder som controllere via den gratis\n\"BombSquad Remote\" app. Se 'Controllere' under 'Indstillinger' for mere info.", + "controllersText": "Controllere", + "controllersTextScale": 0.67, + "controlsSubtitleText": "Din venlige ${APP_NAME} karakter har nogle få grundlæggende handlinger:", + "controlsSubtitleTextScale": 0.7, + "controlsText": "Styring", + "controlsTextScale": 1.4, + "devicesInfoText": "VR-versionen af ${APP_NAME} kan spilles over netværket med\nden almindelige version, så pisk dine ekstra telefoner, tablets,\nog computere frem og få dit spil igang. Det kan endda være nyttigt at\ntilslutte en almindelig version af spillet til VR-versionen bare for at\ntillade folk udefra at se handlingen.", + "devicesText": "Enheder", + "friendsGoodText": "Disse er gode at have. ${APP_NAME} er sjovest at spille med flere spillere,\nog det understøtter op til 8 spillere ad gangen, hvilket leder os til:", + "friendsGoodTextScale": 0.62, + "friendsText": "Venner", + "friendsTextScale": 0.67, + "jumpInfoText": "- Hop -\nHop over små huller,\nfor at kaste ting højere, og \nfor at vise, når du er glad.", + "jumpInfoTextScale": 0.6, + "orPunchingSomethingExtraSpace": 0, + "orPunchingSomethingText": "Eller slå noget, kaste det ud over en kløft, og sprænge det i luften med en klister bombe.", + "pickUpInfoText": "- Saml op -\nLøft flag, fjender og alt andet,\nder ikke er fæstnet til jorden.\nTryk igen for at kaste.", + "pickUpInfoTextScale": 0.6, + "powerupBombDescriptionText": "Lader dig kaste tre bomber\ni streg i stedet for kun en", + "powerupBombNameText": "Tredobbeltbombe", + "powerupCurseDescriptionText": "Du vil nok gerne undgå dem her.\n … eller vil du?", + "powerupCurseNameText": "Forbandelse", + "powerupHealthDescriptionText": "Giver dig fuldt liv.\nDet havde du aldrig gættet.", + "powerupHealthNameText": "Medicin", + "powerupIceBombsDescriptionText": "Svagere end normale bomber\nmen efterlader dine fjender frosne\nog skøre.", + "powerupIceBombsNameText": "Isbomber", + "powerupImpactBombsDescriptionText": "En smule svagere end normale bomber\nmen eksploderer, når de rør noget.", + "powerupImpactBombsNameText": "Berøringsbomber", + "powerupLandMinesDescriptionText": "De kommer i pakker af 3; \nNyttige til forsvar eller \nfor at stoppe hurtige fjender.", + "powerupLandMinesNameText": "Miner", + "powerupPunchDescriptionText": "Får dig til at flå hårdere, hurtigere, bedre og stærkere.", + "powerupPunchNameText": "Boksehandsker", + "powerupShieldDescriptionText": "Tager noget af skaden så du ikke tager den.", + "powerupShieldNameText": "Energiskjold", + "powerupStickyBombsDescriptionText": "Klistrer til alt de rammer.\nMunterhed følger.", + "powerupStickyBombsNameText": "Klisterbombe", + "powerupsSubtitleText": "Selvfølgelig er der intet spil uden powerups:", + "powerupsSubtitleTextScale": 0.8, + "powerupsText": "Powerups", + "powerupsTextScale": 1.4, + "punchInfoText": "- Slag -\nSlag gør mere skade, jo\nhurtigere du løber, Så løb\nog slå som en gal.", + "punchInfoTextScale": 0.6, + "runInfoText": "- Spurt -\nHold hvilken som helst knap nede. Udløselsesknapper virker godt, hvis du har dem. Spurt gør så \ndu bevæger dig hurtigere, men det gør det også sværere at dreje, så hold øje med skrænter.", + "runInfoTextScale": 0.6, + "someDaysExtraSpace": 0, + "someDaysText": "Nogle dage har du bare lyst til at slå noget. Eller at sprænge noget i luften.", + "titleText": "${APP_NAME} hjælp", + "toGetTheMostText": "For at få mest ud af spillet skal du bruge:", + "toGetTheMostTextScale": 1.0, + "welcomeText": "Velkommen til ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} navigerer i menuer som en boss -", + "importPlaylistCodeInstructionsText": "Brug den følgende kode for at importere denne spilliste til andre steder:", + "importPlaylistSuccessText": "Importerede ${TYPE} spilliste '${NAME}'", + "importText": "Importer", + "importingText": "Importerer...", + "inGameClippedNameText": "i spillet bliver det\n\"${NAME}\"", + "installDiskSpaceErrorText": "FEJL: Kan ikke gennemføre installationen.\nDu er måske tom for plads på din enhed.\nRyd noget plads og prøv igen.", + "internal": { + "arrowsToExitListText": "Tryk på ${LEFT} eller ${RIGHT} for afslutte liste", + "buttonText": "knap", + "cantKickHostError": "Du kan ikke smide værten ud.", + "chatBlockedText": "${NAME} er chatblokeret i ${TIME} sekunder.", + "connectedToGameText": "'${NAME}' deltog", + "connectedToPartyText": "Deltog ${NAME}'s gruppe!", + "connectingToPartyText": "Tilslutter...", + "connectionFailedHostAlreadyInPartyText": "Tilslutning fejlede; værten er i en anden gruppe.", + "connectionFailedPartyFullText": "Tilslutningen fejlede; gruppen er fuld.", + "connectionFailedText": "Tilslutningen fejlede.", + "connectionFailedVersionMismatchText": "Tilslutningen fejlede; værten kører på en anden version af spillet.\nVære sikker på, at I begge er opdaterede og prøv igen.", + "connectionRejectedText": "Forbindelsen er afvist.", + "controllerConnectedText": "${CONTROLLER} forbundet.", + "controllerDetectedText": "1 kontroller opdaget.", + "controllerDisconnectedText": "${CONTROLLER} afbrudt.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} afbrudt. Prøv venligst igen.", + "controllerForMenusOnlyText": "Denne kontroller kan ikke bruges til at spille; kun til at navigere i menuer.", + "controllerReconnectedText": "${CONTROLLER} blev forbundet igen.", + "controllersConnectedText": "${COUNT} kontrollere tilsluttet.", + "controllersDetectedText": "${COUNT} kontrollere opdaget.", + "controllersDisconnectedText": "${COUNT} kontrollere koblet fra.", + "corruptFileText": "Korrupt(e) fil(er) opdaget. Prøv at geninstallere eller email ${EMAIL}", + "errorPlayingMusicText": "Fejl ved afspilningen af ${MUSIC}", + "errorResettingAchievementsText": "Ude af stand til at nulstille online achievements; prøv venligst igen senere.", + "hasMenuControlText": "${NAME} har menu kontrollen.", + "incompatibleNewerVersionHostText": "Værten kører en nyere version af spillet.\nOpdater til den seneste version og prøv igen.", + "incompatibleVersionHostText": "Værten kører en anden version af spillet.\nVær sikker på, at I begge er opdateret og prøv igen.", + "incompatibleVersionPlayerText": "${NAME} kører på en anden version af spillet.\nVære sikker på, at I begge er opdateret og prøv igen.", + "invalidAddressErrorText": "Fejl: ugyldig adresse.", + "invalidNameErrorText": "Fejl: ugyldigt navn", + "invalidPortErrorText": "Fejl ugyldig port.", + "invitationSentText": "Invitation sendt.", + "invitationsSentText": "${COUNT} invitationer sendt.", + "joinedPartyInstructionsText": "Nogen har tilsluttet sig din gruppe.\nGå til 'Spil' for at starte spillet.", + "keyboardText": "Tastatur", + "kickIdlePlayersKickedText": "${NAME} blev smidt ud på grund af inaktivitet.", + "kickIdlePlayersWarning1Text": "${NAME} vil blive smidt ud om ${COUNT} sekunder, hvis inaktiviteten fortsætter.", + "kickIdlePlayersWarning2Text": "(du kan slå dette fra under 'Indstillinger' -> 'Avanceret')", + "leftGameText": "'${NAME}' forlod.", + "leftPartyText": "Forlod ${NAME}'s gruppe.", + "noMusicFilesInFolderText": "Mappen indeholder ingen musikfiler.", + "playerJoinedPartyText": "${NAME} tilsluttede sig gruppen!", + "playerLeftPartyText": "${NAME} forlod gruppen.", + "rejectingInviteAlreadyInPartyText": "Afviser invitation (allerede i en gruppe).", + "serverRestartingText": "GENSTARTER. Vær venlig at tilslutte dig igen om lidt.", + "signInErrorText": "Fejl ved login.", + "signInNoConnectionText": "Kan ikke logge ind. (ingen internetforbindelse?)", + "teamNameText": "Hold ${NAME}", + "telnetAccessDeniedText": "FEJL: bruger har ikke givet telnetadgang.", + "timeOutText": "(timeouter om ${TIME} sekunder)", + "touchScreenJoinWarningText": "Du er forbundet med touchskærm.\nHvis det var en fejl, tryk 'Menu->Forlad spil' med det.", + "touchScreenText": "Touchskærm", + "trialText": "prøveversion", + "unableToResolveHostText": "Fejl: kunne ikke finde vært.", + "willTimeOutText": "(timeouter hvis inaktiv)" + }, + "jumpBoldText": "HOP", + "jumpText": "Hop", + "keepText": "Behold", + "keepTheseSettingsText": "Behold disse indstillinger?", + "killsTallyText": "${COUNT} drab", + "killsText": "Drab", + "languageSetText": "Sproget et nu sat til ${LANGUAGE}.\nBemærk: eksisterende UI-elementer bliver måske ikke påvirket.", + "lapNumberText": "Omgang ${CURRENT}/${TOTAL}", + "lastGamesText": "(sidste ${COUNT} spil)", + "leaderboardsText": "Ranglister", + "levelFastestTimesText": "Hurtigste tider på ${LEVEL}", + "levelHighestScoresText": "Højeste scorer på ${LEVEL}", + "levelIsLockedText": "${LEVEL} er låst.", + "levelMustBeCompletedFirstText": "${LEVEL} skal gennemføres først.", + "levelUnlockedText": "Level låst op!", + "livesBonusText": "Livbonus", + "loadingText": "Indlæser", + "mainMenu": { + "creditsText": "Credits", + "endGameText": "Afslut spil", + "exitGameText": "Afslut spil", + "exitToMenuText": "Afslut, og gå til menuen?", + "howToPlayText": "Hvordan spiller man?", + "leaveText": "Forlad", + "quitText": "Afslut", + "resumeText": "Genoptag", + "settingsText": "Indstillinger" + }, + "makeItSoText": "Lav det sådan", + "mapSelectText": "Vælg...", + "mapSelectTitleText": "${GAME}-baner", + "mapText": "Bane", + "mostValuablePlayerText": "Mest værdifulde Spiller", + "mostViolatedPlayerText": "Mest forvoldte Spiller", + "mostViolentPlayerText": "Mest voldelige Spiller", + "moveText": "Bevæg", + "multiKillText": "${COUNT}-DRAB!!!", + "multiPlayerCountText": "${COUNT} spillere", + "nameBetrayedText": "${NAME} forrådte ${VICTIM}.", + "nameDiedText": "${NAME} døde.", + "nameKilledText": "${NAME} dræbte ${VICTIM}.", + "nameNotEmptyText": "Navn kan ikke være tomt!", + "nameScoresText": "${NAME} scorede!", + "nameSuicideKidFriendlyText": "${NAME} døde ved et uheld.", + "nameSuicideText": "${NAME} begik selvmord.", + "newPersonalBestText": "Ny personlig rekord!", + "newTestBuildAvailableText": "En ny testversion er tilgængelig! (${VERSION} build ${BUILD}).\nFå den på ${ADDRESS}", + "newVersionAvailableText": "En nyere version af BombSquad er tilgængelig (${VERSION})", + "nextLevelText": "Næste level", + "noAchievementsRemainingText": "- ingen", + "noExternalStorageErrorText": "Ingen ekstern lagringsenhed fundet på denne enhed.", + "noGameCircleText": "Fejl: ikke logget ind i GameCircle", + "noJoinCoopMidwayText": "Man kan ikke tilslutte co-op spil midtvejs.", + "noProfilesErrorText": "Du har ingen spillerprofiler, så du kan kun bruge '${NAME}'.\nGå til 'Indstillinger' -> 'Spillerprofiler' for at lave dig selv en profil.", + "noThanksText": "Nej tak", + "noValidMapsErrorText": "Ingen gyldige baner fundet for denne spiltype.", + "notNowText": "Ikke nu", + "nothingIsSelectedErrorText": "Ingen valgt", + "offText": "Slukket", + "okText": "Ok", + "onText": "Tændt", + "onslaughtRespawnText": "${PLAYER} vil blive rejst fra de døde i bølge ${WAVE}", + "orText": "${A} eller ${B}", + "outOfText": "(#${RANK} ud af ${ALL})", + "ownFlagAtYourBaseWarning": "Dit eget flag skal være\ni din base for at score!", + "perfectWaveText": "Perfekt bølge!", + "pickUpBoldText": "SAML OP", + "pickUpText": "Saml op", + "playText": "Spil", + "playWindow": { + "coopText": "Co-op", + "freeForAllText": "Alle mod alle", + "oneToFourPlayersText": "1-4 spillere", + "teamsText": "Hold", + "titleText": "Spil", + "twoToEightPlayersText": "2-8 spillere" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerLeftText": "${PLAYER} forlod spillet.", + "playerLimitReachedText": "Spillergrænsen på ${COUNT} er nået; der må ikke tilslutte flere.", + "playerProfilesWindow": { + "deleteButtonText": "Slet", + "deleteConfirmText": "Slet '${PROFILE}'?", + "editButtonText": "Rediger", + "newButtonText": "Ny", + "titleText": "Spillerprofiler" + }, + "playerText": "Spiller", + "playlistNotFoundText": "Playliste ikke fundet", + "pleaseRateText": "Hvis du synes, BombSquad er sjovt, så vær venlig at vurder det eller skriv en anmeldelse.\nDette giver mig nyttig feedback og hjælper med at støtte\nfremtidig udvikling.\n\nTak!\n-Eric (Udvikler)", + "pressAnyButtonPlayAgainText": "Tryk på hvilken som helst knap for at spille igen...", + "pressAnyButtonText": "Tryk hvilken som helst knap for at fortsætte...", + "pressAnyButtonToJoinText": "Tryk på hvilken som helst knap for at deltage...", + "pressAnyKeyButtonPlayAgainText": "Tryk på hvilken som helst tast/knap for at spille igen...", + "pressAnyKeyButtonText": "Tryk på hvilken som helst tast/knap for at fortsætte..", + "pressAnyKeyText": "Tryk på hvilken som helst knap...", + "pressJumpToFlyText": "** Tryk gentagne gange på hop for at flyve **", + "pressPunchToJoinText": "SLÅ for at deltage...", + "pressToOverrideCharacterText": "tryk ${BUTTONS} for at overskrive bruger", + "pressToSelectProfileText": "tryk ${BUTTONS} for at vælge profil", + "pressToSelectTeamText": "tryk ${BUTTONS} for at vælge hold", + "profileInfoText": "Opret profiler for dig selv og dine venner for at\nredigere dine navne, brugere og farver.", + "promoCodeWindow": { + "codeText": "Kode", + "codeTextDescription": "Kampagnekode", + "enterText": "Enter" + }, + "promoSubmitErrorText": "Fejl ved indsendelse af kampagnekode; tjek din internetforbindelse", + "ps3ControllersWindow": { + "macInstructionsText": "Sluk for strømmen bag på din PS3, og sørg for at Bluetooth er\nslået til på din Mac. Forbind herefter din controller til din Mac\nvia et USB-kabel for at parre dem. De efterfølgende gange kan\ndu bruge controllerens home-knap til at forbinde din Mac med\nenten USB-kabel eller en trådløs Bluetooth-forbindelse.\n\nPå nogle Macs skal du muligvis bruge en kode for at parre dem.\nHvis dette sker, så følg denne guide eller Google efter hjælp.\n\n\n\n\nPS3-controllers, der er forbundet trådløst, burde kunne ses på enhedslisten i\n'Indstillinger' -> 'Bluetooth'. Du skal muligvis fjerne dem fra den liste,\nnår du vil bruge dem med din PS3 igen.\n\nSørg også for at frakoble dem fra Bluetooth, når du ikke bruger dem.\nHvis du ikke gør dette, vil dine batterier fortsat blive drænet.\n\nBluetooth burde kunne håndtere op til 7 forbundne enheder, men\nderes ydeevne kan variere.", + "macInstructionsTextScale": 0.74, + "ouyaInstructionsText": "For at bruge din PS3-controller med din OUYA skal du bare forbinde den med\net USB-kabel en enkelt gang for at parre dem. At gøre dette kan resultere i\nfrakobling af dine controllere, så du bør genstarte din OUYA og frakoble USB-kablet.\n\nFra nu af bør det at være muligt at bruge controllerens HOME-knap til at forbinde\ndem trådløst. Når du er færdig med at spille, hold HOME-knappen nede i 10 sekunder\nfor at slå controlleren fra. Hvis du ikke gør dette, kan den muligvis forblive tændt\nog spilde batteri.", + "ouyaInstructionsTextScale": 0.74, + "pairingTutorialText": "sammensætter vejledningsvideo", + "titleText": "PS3-controllere og BombSquad:" + }, + "publicBetaText": "OFFENTLIG BETA", + "punchBoldText": "SLÅ", + "punchText": "Slå", + "purchaseForText": "Køb for ${PRICE}", + "purchaseGameText": "Køb spil", + "purchasingText": "Køber...", + "quitGameText": "Afslut BombSquad?", + "quittingIn5SecondsText": "Afslutter om 5 sekunder...", + "randomText": "Tilfældig", + "ratingText": "Vurdering", + "reachWave2Text": "Nå bølge 2 for at få en vurdering.", + "readyText": "Klar!", + "remainingInTrialText": "tilbage af prøverversionen", + "restartText": "Genstart", + "revertText": "Gendan", + "runText": "Løb", + "saveText": "Gem", + "scoreChallengesText": "Score udfordringer", + "scoreListUnavailableText": "Scoreliste utilgængelig.", + "scoreText": "Score", + "scoreUnits": { + "millisecondsText": "Millisekunder", + "pointsText": "Point", + "secondsText": "Sekunder" + }, + "scoreWasText": "(var ${COUNT})", + "selectText": "Vælg", + "seriesWinLine1PlayerText": "VANDT", + "seriesWinLine1TeamText": "VANDT", + "seriesWinLine1Text": "VINDER", + "seriesWinLine2Text": "SERIEN!", + "settingsWindow": { + "accountText": "Bruger", + "advancedText": "Avanceret", + "audioText": "Lyd", + "controllersText": "Controllere", + "graphicsText": "Grafik", + "playerProfilesText": "Spillerprofiler", + "titleText": "Indstillinger" + }, + "settingsWindowAdvanced": { + "enterPromoCodeText": "Indtast kampagnekode", + "helpTranslateText": "BombSquads sprogoversættelser er lavet af BombSquads\ncommunity. Hvis du vil hjælpe eller rette en oversættelse,\nså følg linket nedenunder. På forhånd tak!", + "helpTranslateTextScale": 1.0, + "kickIdlePlayersText": "Smid inaktive spillere ud.", + "kidFriendlyModeText": "Børnevenlig tilstand (Mindre vold osv)", + "languageText": "Sprog", + "languageTextScale": 1.0, + "moddingGuideText": "Moddingguide", + "showPlayerNamesText": "Vis spillernavne", + "showUserModsText": "Vis mappen til mods", + "titleText": "Avanceret", + "translationEditorButtonText": "BombSquad oversættelsesside", + "translationNoUpdateNeededText": "Det nuværende sprog er ajour; woohoo!", + "translationUpdateNeededText": "** Det nuværende sprog mangler opdateringer!! **" + }, + "singlePlayerCountText": "1 spiller", + "soloNameFilterText": "Solo-${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Brugerudvælgelse", + "Chosen One": "Den udvalgte", + "Epic": "Slowmotionspil", + "Epic Race": "Slowmotionrace", + "FlagCatcher": "Find fanen", + "Flying": "Glade drømme", + "Football": "Fodbold", + "ForwardMarch": "Angreb", + "GrandRomp": "Erobring", + "Hockey": "Hockey", + "Keep Away": "Hold dig væk", + "Marching": "Løb", + "Menu": "Hovedmenu", + "Onslaught": "Stormangreb", + "Race": "Race", + "Scary": "Kongen af bakken", + "Scores": "Scoreskærm", + "Survival": "Udslettelse", + "ToTheDeath": "Kamp til døden", + "Victory": "Endelig scoreskærm" + }, + "spaceKeyText": "mellemrum", + "store": { + "alreadyOwnText": "Du har allerede ${NAME}!", + "bombSquadProDescriptionText": "Double billetter optjent in-game\n\nFjerner In-game reklamer\n\nInkluderer ${COUNT} bonus billetter\n\n+${PERCENT}% liga score bonus\n\nOplåser'${INF_ONSLAUGHT}' og\n '${INF_RUNAROUND}' co-op baner", + "bombSquadProFeaturesText": "indkludere:", + "bombSquadProNameText": "BombSquad Pro", + "buyText": "Køb", + "charactersText": "Spillere", + "comingSoonText": "Kommer snart...", + "extrasText": "Extra", + "freeBombSquadProText": "Bombsquad er nu gratis, men da du oprindeligt købte det, du er\nmodtagelse af Bombsquad Pro opgradering og ${COUNT} billetter som en tak.\nNyd de nye funktioner, og tak for din støtte!\n-Eric", + "gameUpgradesText": "spil opgradering", + "getCoinsText": "Få coins", + "holidaySpecialText": "Feriespecials", + "howToSwitchCharactersText": "(gå til \"${SETTINGS} -> ${PLAYER_PROFILES}\" for at tildele og tilpasse karatere)", + "loadErrorText": "Kan ikke indlæse side.\nTjek din internetforbindelse.", + "loadingText": "Indlæser", + "mapsText": "Baner", + "miniGamesText": "Minispil", + "purchaseAlreadyInProgressText": "et køb af denne ting er allerede i gang.", + "purchaseNotValidError": "kan ikke foretage køb\nKontakt ${EMAIL}, hvis dette er en fejl.", + "saleText": "SALE", + "searchText": "Søg", + "teamsFreeForAllGamesText": "Teams / Free-for-All Games", + "winterSpecialText": "Vinterspecials", + "youOwnThisText": "- Du ejer dette -" + }, + "storeDescriptionText": "Vanvittigt spil med op plads til op til 8 spillere!\n\nSpræng dine venner eller computeren i luften i en turnering af eksplosive mini-spil som f.eks. find fanen, bombe-hockey og kamp til døden i slowmotion.\n\nNem styring og omfattende controllersupport gør det nemt at få op til 8 spillere med i kampen! Du kan endda bruge din mobil som controller via den gratis 'BombSquad Remote'-app.\n\nDer skydes!\n\nTjek www.froemling.net/bomsquad ud for mere information.", + "storeDescriptions": { + "blowUpYourFriendsText": "Spræng dine venner i luften", + "competeInMiniGamesText": "Konkurrer i mini-spil lige fra racing til at flyve.", + "customize2Text": "Tilpas figurere, minispil og lydspor.", + "customizeText": "Tilpas figurere og lav din egen minispilliste.", + "sportsMoreFunText": "Sport er sjovere med sprængstof.", + "teamUpAgainstComputerText": "Kæmp imod computeren med dine venner." + }, + "storeText": "Butik", + "teamsText": "Hold", + "telnetAccessGrantedText": "Telnet-adgang aktiveret.", + "telnetAccessText": "Telnet-adgang registreret; tillad?", + "testBuildErrorText": "Denne test build er ikke længere aktiv; være venlig og se efter en ny version.", + "testBuildText": "Test Build", + "testBuildValidateErrorText": "Kan ikke validere dette test build. (ingen netforbindelse?)", + "testBuildValidatedText": "Test Build Validated; Nyd det!", + "thankYouText": "Tak for din støtte! Nyd spillet!!", + "threeKillText": "TREDOBBELT DRAB!!", + "timeBonusText": "Tidsbonus", + "timeElapsedText": "Tid brugt", + "timeExpiredText": "Tiden er udløbet", + "tipText": "Tip", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Top venner", + "tournamentCheckingStateText": "Checker turnerings status; vent venligst...", + "tournamentEndedText": "Denne turnerings er slut. En ny vil snart starte.", + "tournamentEntryText": "turnerings indgang", + "tournamentStandingsText": "Turnerings Stillinger", + "tournamentText": "Turnering", + "tournamentTimeExpiredText": "Turnerings tiden er udvidet", + "tournamentsText": "Turneringer", + "translations": { + "characterNames": { + "Bernard": "Bernard", + "Bones": "Bones", + "Santa Claus": "Julemand" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Uendelig\nOnslaught", + "Infinite\nRunaround": "Uendelig\nRunaround", + "Onslaught\nTraining": "Onslaught\nTræning", + "Pro\nFootball": "Pro\nFootballl", + "Pro\nOnslaught": "Pro\nOnslaught", + "Pro\nRunaround": "Pro\nRunaround", + "Rookie\nFootball": "Rookie\nFootball", + "Rookie\nOnslaught": "Rookie\nOnslaught", + "The\nLast Stand": "Sidste\nStående", + "Uber\nFootball": "Uber\nFootball", + "Uber\nOnslaught": "Uber\nOnslaught", + "Uber\nRunaround": "Uber\nRunaround" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Træning", + "Infinite ${GAME}": "uendelig ${GAME}", + "Infinite Onslaught": "Uendeligt stormangreb", + "Infinite Runaround": "Uendeligt løb", + "Onslaught": "Uendelig Onslaught", + "Onslaught Training": "Stormangrebstræning", + "Pro ${GAME}": "Pro ${GAME}", + "Pro Football": "Pro fodbold", + "Pro Onslaught": "Pro stormangreb", + "Pro Runaround": "Pro løb", + "Rookie ${GAME}": "Begynder ${GAME}", + "Rookie Football": "Nybegynderfodbold", + "Rookie Onslaught": "Nybegynderstormangreb", + "Runaround": "Uendelig Runaround", + "The Last Stand": "Sidst overlevende", + "Uber ${GAME}": "Supersvær ${GAME}", + "Uber Football": "Uber fodbold", + "Uber Onslaught": "Uber stormangreb", + "Uber Runaround": "Uber løb" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Vær den udvalgte i et forudbestemt stykke tid for at vinde.\nDræb den udvalgte for selv at blive den udvalgte.", + "Bomb as many targets as you can.": "Ram så mange mål som du kan!", + "Carry the flag for ${ARG1} seconds.": "Bær flaget i ${ARG1} sekunder.", + "Carry the flag for a set length of time.": "Bær flaget i et stykke tid.", + "Crush ${ARG1} of your enemies.": "Knus ${ARG1} af dine fjender.", + "Defeat all enemies.": "Besejr alle dine modstandere", + "Dodge the falling bombs.": "Undgå de faldne bomber", + "Final glorious epic slow motion battle to the death.": "Sidste gloriøse episke slowmotionkamp til døden.", + "Get the flag to the enemy end zone.": "Få flaget til modstanderenes målzone.", + "How fast can you defeat the ninjas?": "Hvor hurtigt kan du besejre ninjaerne?", + "Kill a set number of enemies to win.": "Dræb et forudbestemt antal af fjender for at vinde.", + "Last one standing wins.": "Sidste overlevende vinder.", + "Last remaining alive wins.": "Sidste overlevende vinder", + "Last team standing wins.": "Sidste overlevende hold vinder.", + "Prevent enemies from reaching the exit.": "Forhindr fjenderne i at nå udgangen.", + "Reach the enemy flag to score.": "Nå til modstanderenes flag for at score", + "Return the enemy flag to score.": "Returner modstanderens flag for at score", + "Run ${ARG1} laps.": "Løb ${ARG1} runder.", + "Run ${ARG1} laps. Your entire team has to finish.": "Løb ${ARG1} runder. Hele dit hold skal gennemføre.", + "Run 1 lap.": "Løb 1 runde.", + "Run 1 lap. Your entire team has to finish.": "Løb 1 runde. Hele dit hold skal gennemfører.", + "Run real fast!": "Løb rigtig hurtigt!", + "Score ${ARG1} goals.": "Scor ${ARG1} mål.", + "Score ${ARG1} touchdowns.": "Lav ${ARG1} touchdowns.", + "Score a goal": "Scor et mål", + "Score a goal.": "Scor et mål.", + "Score a touchdown.": "Lav en touchdown.", + "Score some goals.": "Scor nogle mål.", + "Secure all ${ARG1} flags.": "Få fat i alle ${ARG1} flag.", + "Secure all flags on the map to win.": "Få fat i alle flag på banen for at vinde.", + "Secure the flag for ${ARG1} seconds.": "Hold flaget i ${ARG1} sekunder.", + "Secure the flag for a set length of time.": "Hold alle flag i et bestemt stykke tid.", + "Steal the enemy flag ${ARG1} times.": "Stjæl fjendens flag ${ARG1} gange.", + "Steal the enemy flag.": "Stjæl fjendens flag.", + "There can be only one.": "Der kan kun være én.", + "Touch the enemy flag ${ARG1} times.": "Rør fjendens flag ${ARG1} gange.", + "Touch the enemy flag.": "Rør fjendens flag.", + "carry the flag for ${ARG1} seconds": "Bær flaget i ${ARG1} sekunder", + "kill ${ARG1} enemies": "Dræb ${ARG1} fjender", + "last one standing wins": "Sidste overlevende vinder", + "last team standing wins": "Sidste overlevende hold vinder", + "return ${ARG1} flags": "Returner ${ARG1} flag", + "return 1 flag": "Returner 1 flag", + "run ${ARG1} laps": "Løb ${ARG1} runder", + "run 1 lap": "Løb 1 runde", + "score ${ARG1} goals": "Scor ${ARG1} mål", + "score ${ARG1} touchdowns": "Lav ${ARG1} touchdowns", + "score a goal": "Scor et mål", + "score a touchdown": "Lav en touchdown", + "secure all ${ARG1} flags": "Få fat i alle ${ARG1} flag", + "secure the flag for ${ARG1} seconds": "Hold flaget i ${ARG1} sekunder", + "touch ${ARG1} flags": "Rør ${ARG1} flag", + "touch 1 flag": "Rør 1 flag" + }, + "gameNames": { + "Assault": "Angreb", + "Capture the Flag": "Find fanen", + "Chosen One": "Den udvalgte", + "Conquest": "Erobring", + "Death Match": "Kamp til døden", + "Elimination": "Udslettelse", + "Football": "Fodbold", + "Hockey": "Hockey", + "Keep Away": "Hold dig væk", + "King of the Hill": "Kongen af bakken", + "Meteor Shower": "Meteorregn", + "Ninja Fight": "Ninja Kamp", + "Onslaught": "Stormangreb", + "Race": "Race", + "Runaround": "Løb", + "Target Practice": "mål øvelse", + "The Last Stand": "Den sidste overlevende" + }, + "inputDeviceNames": { + "Keyboard": "Tastatur", + "Keyboard P2": "Tastatur P2" + }, + "languages": { + "Chinese": "Kinesisk", + "Croatian": "Kroatisk", + "Czech": "tjekkisk", + "Danish": "Dansk", + "Dutch": "Hollandsk", + "English": "Engelsk", + "Esperanto": "Esperanto", + "Finnish": "Finsk", + "French": "Fransk", + "German": "Tysk", + "Gibberish": "Volapykengelsk", + "Italian": "Italiensk", + "Japanese": "Japansk", + "Korean": "Koreansk", + "Polish": "Polsk", + "Portuguese": "Portugisisk", + "Russian": "Russisk", + "Spanish": "Spansk", + "Swedish": "Svensk" + }, + "leagueNames": { + "Bronze": "Bronze", + "Diamond": "Diamandt", + "Gold": "Guld", + "Silver": "sløv" + }, + "mapsNames": { + "Big G": "Store G", + "Bridgit": "Bridgit", + "Courtyard": "Gårdhaven", + "Crag Castle": "Slottet på klippeskrænten", + "Doom Shroom": "Dommedagssvampen", + "Football Stadium": "Fodboldstadionet", + "Happy Thoughts": "Glade drømme", + "Hockey Stadium": "Hockeystadionet", + "Lake Frigid": "søen Frigid", + "Monkey Face": "Abefjæs", + "Rampage": "Rampage", + "Roundabout": "Rundkørslen", + "Step Right Up": "Trappeslottet", + "The Pad": "Kassen", + "Tip Top": "Keglen", + "Tower D": "D-tårnet", + "Zigzag": "Zigzag" + }, + "playlistNames": { + "Just Epic": "Bare Epicisk", + "Just Sports": "Bare Sport" + }, + "promoCodeResponses": { + "invalid promo code": "Ugyldig kampagnekode" + }, + "scoreNames": { + "Flags": "Flag", + "Goals": "Mål", + "Score": "Score", + "Survived": "Overlevet", + "Time": "Tid", + "Time Held": "Tid holdt" + }, + "serverResponses": { + "BombSquad Pro unlocked!": "BombSquad Pro oplukket!", + "Entering tournament...": "Går ind i Turnering...", + "Invalid promo code.": "ugylid promo kode.", + "Invalid purchase.": "Fejl i købt.", + "Purchase successful!": "Købt gennemført!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Modtog ${COUNT} tickets fordi du logger ind.\nKom tilbage i morgen for at modtage ${TOMORROW_COUNT}.", + "The tournament ended before you finished.": "Turneringen sluttede, før du blev færdig.", + "This requires version ${VERSION} or newer.": "Dette kræver version ${VERSION} eller nyere.", + "You already own this!": "Du ejer allerede dette!", + "You don't have enough tickets for this!": "Du har ikke tickets nok til dette!", + "You got ${COUNT} tickets!": "Du har ${COUNT} tickets!", + "You got a ${ITEM}!": "Du fik en ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Du er blevet forfremmet til en ny liga; Tillykke!", + "You must wait a few seconds before entering a new code.": "Du skal vente et par sekunder, før du indtaster en ny kode.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Du rangeret #${RANK} i den sidste turnering. Tak fordi du spillede med!", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Din kopi af spillet er blevet modifiseret. \nVære venlig og ændre det tilbage til normal version og prøv igen." + }, + "settingNames": { + "1 Minute": "1 minut", + "1 Second": "1 sekund", + "10 Minutes": "10 minutter", + "2 Minutes": "2 minutter", + "2 Seconds": "2 sekunder", + "20 Minutes": "20 minutter", + "4 Seconds": "4 sekunder", + "5 Minutes": "5 minutter", + "8 Seconds": "8 sekunder", + "Balance Total Lives": "Balancér samlet antal liv", + "Chosen One Gets Gloves": "Den udvalgte får boksehandsker", + "Chosen One Gets Shield": "Den udvalgte får skjold", + "Chosen One Time": "Den udvalgte tid", + "Enable Impact Bombs": "Aktiver Impact Bomber", + "Enable Triple Bombs": "Aktiver triple Bomber", + "Epic Mode": "Epic mode", + "Flag Idle Return Time": "Flag – inaktiv tid før returnering", + "Flag Touch Return Time": "Flag – Berøringstid før returnering", + "Hold Time": "Holdetid", + "Kills to Win Per Player": "Drab for at vinde per spiller", + "Laps": "Omgange", + "Lives Per Player": "Liv per spiller", + "Long": "Lang", + "Longer": "Længere", + "Mine Spawning": "Minegenoplivning", + "No Mines": "Ingen miner", + "None": "Ingen", + "Normal": "Normal", + "Respawn Times": "Antal genoplivninger", + "Score to Win": "Score for at vinde", + "Short": "Kort", + "Shorter": "Kortere", + "Solo Mode": "Solotilstand", + "Target Count": "Mål Optælling", + "Time Limit": "Tidsbegrænsning" + }, + "statements": { + "Killing ${NAME} for skipping part of the track!": "dræbt ${NAME} for at springe en del af banen over!" + }, + "teamNames": { + "Bad Guys": "Fjender", + "Blue": "Blå", + "Good Guys": "Venner", + "Red": "Rød" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Et perfekt løbe-hoppe-dreje-slag kan dræbe på et enkelt slag og\ngiver dig livslang respekt fra dine venner.", + "Always remember to floss.": "Husk altid at bruge tandtråd.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Lav spillerprofiler til dig selv og dine venner med jeres foretrukne navne\nog udseender i stedet for at bruge tilfældige navne og udseender.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Forbandelseskasserne laver dig om til en tikkende bombe.\nDen eneste kur er hurtig at tage en livpakke.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Selvom de ser anderledes ud, er alle spillernes evner\nidentiske. Så vælg den, som du mest ligner.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Bliv ikke for overmodig, bare fordi du har et energiskjold; du kan stadig blive kastet ud over kanten.", + "Don't run all the time. Really. You will fall off cliffs.": "Lad vær' med at løbe hele tiden. Helt seriøst. Du ender med at falde ud over kanten.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Hold hvilken som helst knap nede for at løbe. (Udløselsesknapper virker godt, hvis du har sådan nogle)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Hold en vilkårlig knap nede for at løbe. Du bevæger dig hurtigere, men du drejer ikke så godt, så pas på kanter.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Isbomber er ikke særligt kraftfulde, men de fryser, dem de rammer. Hvilket gør den ramte nem at slå i stykker.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Slå til dem, der løfter dig op – så giver de slip med det samme.\nDette virker også i virkeligheden.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Hvis du har få controllere, så installer 'BombSquad Remote' app'en på\ndin iOS- eller Android-enhed. De kan nemlig bruges som controllere.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Hvis du er blevet ramt af en klistrebombe, så hop rundt og drej i cirkler. Du kan muligvis\nryste bomben af – hvis du ikke kan det, var din sidste tid i det mindste underholdende.", + "If you kill an enemy in one hit you get double points for it.": "Hvis du dræber en fjende med ét slag, får du dobbelt så mange point for det.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Hvis du samler en forbandelse op, er dit eneste håb for overlevelse\nen livspakke, som du skal samle op inden tiden udløber.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Hvis du altid står stille, bliver du nakket. Løb og undgå at blive slået for at overleve.", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Hvis der kommer og går mange spillere, så slå automatisk udsmidning af inaktive spillere til under\n'Indstillinger' i tilfælde af at nogle glemmer at forlade spillet.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Hvis enheden bliver for varmt eller du gerne vil spare på batteriet,\nskrue ned \"Visuals\" eller \"Løsning\" i Opsætning-> Grafik", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Hvis din FPS er dårlig, så prøv at skru ned for din opløsning\neller de visuelle effekter i spillets grafikindstillinger.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "I Find Fanen skal dit eget flag være i din base, for at du kan score. Hvis det andet\nhold er ved at score, kan du stjæle deres flag. Dette gør så de ikke kan score.", + "In hockey, you'll maintain more speed if you turn gradually.": "På hockeybanen kan du opretholde din fart, hvis du kun drejer en smule ad gangen.", + "It's easier to win with a friend or two helping.": "Det er lettere at vinde hvis en eller flere af dine venner hjælper.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Hop samtidig med at du kaster for at få bomberne længere og højere.", + "Land-mines are a good way to stop speedy enemies.": "Miner er en god måde at stoppe hurtige fjender på.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Mange ting kan samles op og kastes med – også andre spillere. Det, at kaste dine\nfjender ud over en kant, kan være en effektiv og følelsesmæssigt tilfredsstillende strategi.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nej, du kan ikke komme op på afsatsen. Du bliver nødt til at kaste bomber.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Spillere kan deltage og forlade i midten af de fleste spil,\nog du kan også tilslutte og frakoble kontrollere i farten.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Spillere kan tilslutte og forlade midt i de fleste spil\nog du kan også til- og frakoble gamepads imens.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Powerups er tidsbegrænsede i co-op spil.\nI hold og alle mod alle er de dine, indtil du dør.", + "Practice using your momentum to throw bombs more accurately.": "Øv dig i at bruge dit momentum til at kaste bombe mere præcist.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Slag gør mere skade, jo hurtigere dine knytnæver bevæger\nsig, så prøv at løbe, hoppe og dreje som en gal.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Løb frem og tilbage, før du kaster en bombe\nfor at lave en piskebevægelse og dermed kaste den længere.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Dræb en gruppe af fjender ved\nat udløse en bombe nær en TNT-box.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Hovedet er det mest sårbare område, så en klistrebombe\ntil hovedet betyder for det meste game-over for offeret.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Denne bane ender aldrig, men en høj score vil\ngive dig evig respekt over hele verden.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Kastekraft er baseret på den retningsknap, du holder nede. For at kaste\nnoget lige så stille foran dig, skal du ikke holde nogen retningsknap nede.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Træt af soundtracket? Udskift det med dine egne!\nSe indstiller-> lyd-> Soundtrack", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Prøv at lade lunten brænde lidt, før du kaster bomben.", + "Try tricking enemies into killing eachother or running off cliffs.": "Prøv at snyd dine fjender, så de dræber hinanden eller løber ud over klipper.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Brug opsamlingsknappen til at snuppe flaget < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Lav en piskebevægelse frem og tilbage for at kaste længere.", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Du kan 'sigte' dine slag ved at dreje til venstre eller højre. Dette er\ngodt til at slå fjender ud over kanterne eller til at score i hockey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Du kan se hvornår en bombe vil springe, hvis du kigger\npå farven af gnisterne fra lunten: Gul..orange..rød..BOOM.", + "You can throw bombs higher if you jump just before throwing.": "Du kan kaste bomber højere, hvis du hopper, lige før du kaster.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Du behøver ikke at redigere din profil for at ændre brugere: Bare tryk på\n'saml-op' knappen når du tilslutter et spil for at slette dit normale.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Du tager skade, hvis du støder dit hoved mod ting,\nså prøv at undgå det.", + "Your punches do much more damage if you are running or spinning.": "Dit slag kan gøre meget mere skade, hvis du løber og/eller drejer." + } + }, + "trialPeriodEndedText": "Din prøveperiode er slut. Vil du købe\nBombSquad og fortsætte med at spille?", + "trophiesText": "Trofæer", + "tutorial": { + "cpuBenchmarkText": "Løb tutorial ved latterlig-hastighed (primært tester CPU-hastighed)", + "phrase01Text": "Hej med dig!", + "phrase02Text": "Velkommen til BombSquad!", + "phrase03Text": "Here er der lige nogle tips til at styre din spiller:", + "phrase04Text": "Mange ting i BombSquad er baseret på fysik.", + "phrase05Text": "For eksempel når du slår...", + "phrase06Text": "..er skaden baseret på dine hænders fart.", + "phrase07Text": "Se? Fordi vi ikke bevægede os, gjorde det næsten ikke engang ondt på ${NAME}.", + "phrase08Text": "Lad os hoppe og dreje for at få mere fart på.", + "phrase09Text": "Ah, det var bedre.", + "phrase10Text": "Det hjælper også at løbe.", + "phrase11Text": "Hold en vilkårlig knap nede for at løbe.", + "phrase12Text": "For ekstra seje slag, prøv at løb OG drej på samme tid.", + "phrase13Text": "Ups, undskyld ${NAME}.", + "phrase14Text": "Du kan samle ting og kaste med dem. Du kan fx løfte flag... eller ${NAME}.", + "phrase15Text": "Og sidst, men ikke mindst, så er der bomber.", + "phrase16Text": "Det kræver øvelse at kaste med bomber.", + "phrase17Text": "Øv! Det var ikke et særlig godt kast.", + "phrase18Text": "Du kaster længere, når du bevæger dig.", + "phrase19Text": "Du kaster højere, når du hopper.", + "phrase20Text": "Lav en piskebevægelse frem og tilbage for at kaste endnu længere.", + "phrase21Text": "Det kan være svært at time dine bomber.", + "phrase22Text": "Øv! Det var ikke et særlig godt kast.", + "phrase23Text": "Prøv at lade lunten brænde et sekund eller to, før du kaster.", + "phrase24Text": "Wow! Godt gået!", + "phrase25Text": "Nå, men det var det, jeg havde.", + "phrase26Text": "Nu er det for alvor din tur!", + "phrase27Text": "Husk på din træning, og så kommer du helt sikkert tilbage i live.", + "phrase28Text": "…. eller måske ikke...", + "phrase29Text": "Held og lykke!", + "randomName1Text": "Freddy", + "randomName2Text": "Malik", + "randomName3Text": "Niels", + "randomName4Text": "Nikolaj", + "randomName5Text": "Emil", + "skipConfirmText": "vil du springe tutorialen over? Tryk på skærmen eller en knap for at bekræfte.", + "skipVoteCountText": "${COUNT}/${TOTAL} vil springe tutorialen over", + "skippingText": "Springer tutorialen over...", + "toSkipPressAnythingText": "(tryk på en vilkårlig knap for at springe tutorialen over)" + }, + "twoKillText": "DOBBEL DRAB!", + "unavailableText": "utilgængelig", + "unconfiguredControllerDetectedText": "Ukonfigureret controller registreret:", + "unlockThisInTheStoreText": "Det skal være låst op i butikken.", + "unsupportedHardwareText": "Beklager, denne hardware er ikke understøttet af dette build af spillet.", + "upFirstText": "Første spil:", + "upNextText": "Næste bane i spil nummer ${COUNT}:", + "updatingAccountText": "Opdatere din account...", + "upgradeToPlayText": "Lås op \"${PRO}\" i butikken for at spille dette.", + "useDefaultText": "Benyt standardindstillinger", + "usesExternalControllerText": "Dette spil bruger en ekstern controller til input.", + "usingItunesText": "Bruger iTunes til musik...", + "usingItunesTurnRepeatAndShuffleOnText": "Sørg venligst for, at shuffle er PÅ, og at gentag er sat til 'Alle' i iTunes.", + "validatingBetaText": "Validerer beta...", + "validatingTestBuildText": "Validere Test Build ...", + "victoryText": "Sejr!", + "vsText": "vs.", + "waitingForHostText": "(venter på ${HOST} for at fortsætte)", + "waitingForLocalPlayersText": "Venter på lokale spillere...", + "waitingForPlayersText": "venter spillerne til at deltage ...", + "watchAnAdText": "Se en reklame", + "watchWindow": { + "deleteConfirmText": "Slet \"${REPLAY}\"?", + "deleteReplayButtonText": "Slet\nGengivelse", + "myReplaysText": "Mine Gengivelse", + "noReplaySelectedErrorText": "Ingen gengivelse valgt", + "renameReplayButtonText": "Omdøb\nGengivelse", + "renameReplayText": "Omdøb \"${REPLAY}\" til:", + "renameText": "Omdøb", + "replayDeleteErrorText": "Fejl sletter gengivelse.", + "replayNameText": "Gengivelse Navn", + "replayRenameErrorAlreadyExistsText": "En gengivelse med det navn eksisterer allerede.", + "replayRenameErrorInvalidName": "Kan ikke omdøbe gengivelse; ugyldigt navn.", + "replayRenameErrorText": "Fejl i omdøbning af gengivelse.", + "sharedReplaysText": "Del Gengivelser", + "titleText": "Se", + "watchReplayButtonText": "Se\nGengivelse" + }, + "waveText": "Bølge", + "wellSureText": "Øh, ja da!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Lytter efter Wiimotes...", + "pressText": "Tryk på Wiimote knap 1 og 2 på samme tid.", + "pressText2": "På nyere Wiimotes med Motion Plus indbygget, tryk på den røde 'sync' knap på bagsiden i stedet.", + "pressText2Scale": 0.55, + "pressTextScale": 1.0 + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "copyrightTextScale": 0.6, + "listenText": "Lytter", + "macInstructionsText": "Sørg for at din Wii er slået fra og dit Bluetooth er slået\ntil på din Mac. Tryk derefter på 'Lyt' eller 'Listen'. Wiimote-\nsupport kan være lidt ustabilt, så du er muligvis nødt til\nat prøve et par gange, før du får forbindelse.\n\nBluetooth burde at kunne have op til 7 tilsluttede enheder,\nmen forbindelserne kan variere.\n\nBombSquad virker til de originale Wiimotes, Nunchuks\nog den klassiske controller.\nDen nyere Wii Remote Plus virker også,\nmen kun uden ekstraudstyr.", + "macInstructionsTextScale": 0.7, + "thanksText": "Tak til DarwiinRemote holdet for\nat gøre dette muligt.", + "thanksTextScale": 0.8, + "titleText": "Wiimote opsætning" + }, + "winsPlayerText": "${NAME} vandt!", + "winsTeamText": "${NAME} vandt!", + "winsText": "${NAME} vandt!", + "worldScoresUnavailableText": "Verdensscore utilgængelig.", + "worldsBestScoresText": "Verdens bedste scorer", + "worldsBestTimesText": "Verdens bedste tider", + "xbox360ControllersWindow": { + "getDriverText": "Driverhjemmeside", + "macInstructions2Text": "For at bruge controllere trådløst, skal du også bruge modtageren\nsom følger med 'Xbox 360 Wireless Controller for Windows'.\nEn modtager gør det muligt at tilslutte op til 4 controllers.\n\nVigtigt: 3-partsmodtagere virker ikke med denne driver;\nsørg for, at der står 'Microsoft' på den og ikke 'XBOX 360'.\nMicrosoft sælger ikke længere disse seperat, så du bliver nødt til\nat få den i en pakke sammen med en controller - Eller søg på eBay.\n\nHvis du mener dette er brugbart, må du meget gerne donere\ntil driverudvikleren på hans side.", + "macInstructions2TextScale": 0.76, + "macInstructionsText": "For at bruge Xbox 360-controllere skal du installere en\nMacdriver, der er tilgængelig i linket herunder.\nDet virker både med controllere med og uden ledninger.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "For at bruge Xbox 360-controllere til BombSquad, tilslut dem din enheds USB-port.\nDu kan bruge en USB-hub\ntil at forbinde flere controllere. \n\nFor at bruge trådløse controllere skal du bruge en trådløs modtager,\ntilgængelig som en del af \"Xbox 360 wireless Controller for Windows\"-\npakken eller solgt seperat. Hver modtager puttes ind i en USB port\nog tillader dig at forbinde op til 4 trådløse controllers.", + "ouyaInstructionsTextScale": 0.8, + "titleText": "Hvordan man bruger Xbox 360-controllere med BombSquad:" + }, + "yesAllowText": "Ja, tillad!", + "yourBestScoresText": "Dine bedste scorer", + "yourBestTimesText": "Dine bedste tider" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/dutch.json b/dist/ba_data/data/languages/dutch.json new file mode 100644 index 0000000..02ba231 --- /dev/null +++ b/dist/ba_data/data/languages/dutch.json @@ -0,0 +1,1922 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Account namen mogen geen emoji's of andere speciale tekens bevatten.", + "accountProfileText": "(account profiel)", + "accountsText": "Accounts", + "achievementProgressText": "Prestaties: ${COUNT} van de ${TOTAL}", + "campaignProgressText": "Campagne Voortgang [Moeilijk]: ${PROGRESS}", + "changeOncePerSeason": "U kunt dit maar een keer per seizoen wijzigen.", + "changeOncePerSeasonError": "U moet wachten tot het volgende seizoen om dit te veranderen (${NUM} dagen)", + "customName": "Aangepaste naam", + "deviceSpecificAccountText": "U gebruikt nu het apparaat-specifieke account: ${NAME}", + "linkAccountsEnterCodeText": "Voer Code In", + "linkAccountsGenerateCodeText": "Genereer Code", + "linkAccountsInfoText": "(deel voortgang over verschillende platformen)", + "linkAccountsInstructionsNewText": "Om 2 accounts te verbinden, genereer je een code met het \neerste account en vul je die in op de tweede. \nData van het tweede account zal worden gedeeld over beiden accounts. \n\n(Data van het eerste account zal verloren gaan.) \n\nJe kunt in totaal ${COUNT} accounts verbinden.\n\nBELANGRIJK: Verbind alleen accounts die van jou zijn; \nAls je verbind met een account van een vriend zul je niet meer samen online kunnen spelen.", + "linkAccountsInstructionsText": "Om twee accounts te koppelen, genereer je bij een\ndaar van een code die je invult bij de ander.\nVoortgang en inventaris worden dan samengevoegd.\nJe kan maximaal ${COUNT} accounts koppelen.\n\nBELANGRIJK: Koppel alleen accounts die jij bezit!\nAls je accounts van vrienden koppelt kan je niet \nmeer tegelijkertijd spelen!\n\nOok: Dit kan momenteel niet ongedaan gemaakt worden, dus pas op!", + "linkAccountsText": "Koppel Accounts", + "linkedAccountsText": "Verbonden Accounts:", + "nameChangeConfirm": "Verander je account naam naar ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Dit reset uw co-op campagne voortgang en\nlokale high-scores (maar niet uw tickets).\nDit kan niet ongedaan worden gemaakt. Weet u het zeker?", + "resetProgressConfirmText": "Dit reset uw co-op voortgang,\nprestaties en lokale high-scores\n(maar niet uw tickets). Dit kan niet\nongedaan worden gemaakt. Weet u het zeker?", + "resetProgressText": "Reset Voortgang", + "setAccountName": "Stel account naam in", + "setAccountNameDesc": "Selecteer de naam om weer te geven voor uw account.\nU kan de naam gebruiken van een van de verbonden accounts\nof maak een unieke naam aan.", + "signInInfoText": "Log in om tickets te verzamelen, online te concurreren,\nen om je voortgang te delen tussen apparaten.", + "signInText": "Log In", + "signInWithDeviceInfoText": "(een automatisch account is alleen beschikbaar op dit apparaat)", + "signInWithDeviceText": "Log in met apparaat account", + "signInWithGameCircleText": "Log in met Game Circle", + "signInWithGooglePlayText": "Log in met Google Play", + "signInWithTestAccountInfoText": "(oudere account type; gebruik device account gaat door)", + "signInWithTestAccountText": "Log in met test account", + "signOutText": "Log Uit", + "signingInText": "Inloggen...", + "signingOutText": "Uitloggen...", + "testAccountWarningCardboardText": "Let op: je logt in met een \"test\" account.\nDeze zullen worden vervangen door Google accounts \nzodra dit wordt ondersteund in Google Cardboard apps.\n\nVoorlopig moet je alle tickets in-game verdienen.\n(Je krijgt, echter, de BombSquad Pro opwaardering gratis)", + "testAccountWarningOculusText": "Waarschuwing: U bent ingelogd met een \"test\" account.\nDeze accounts zullen later dit jaar vervangen worden met Oculus accounts later dit\njaar. Met dat account kan je tickets kopen en andere features gebruiken.\n\nVoor nu moet je al je tickets in-game verdienen.\n(Je krijgt, echter, de BombSquad Pro opwaardering gratis)", + "testAccountWarningText": "Waarschuwing: u ben ingelogd met een \"test\" account.\nDit account is gebonden aan dit specifieke apparaat en\nkan periodiek gereset worden. (besteed dus niet te \nveel tijd met het verzamelen/vrijspelen van spullen er voor)\n\nGebruik een gekochte versie van het spel om een \"echt\"\naccount (Game-Center, Google Plus, etc.) te gebruiken. Hierdoor kan u ook\nuw voortgang bewaren in de cloud en het delen tussen verschillende apparaten.", + "ticketsText": "Tickets: ${COUNT}", + "titleText": "Profiel", + "unlinkAccountsInstructionsText": "Selecteer een account om los te koppelen.", + "unlinkAccountsText": "Koppel accounts los.", + "viaAccount": "(via account ${NAME})", + "youAreLoggedInAsText": "Je bent ingelogd als:", + "youAreSignedInAsText": "U bent ingelogd als:" + }, + "achievementChallengesText": "Prestatie Uitdagingen", + "achievementText": "Prestatie", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Dood 3 slechteriken met TNT", + "descriptionComplete": "Doodde 3 slechteriken met TNT", + "descriptionFull": "Dood 3 slechteriken met TNT op ${LEVEL}", + "descriptionFullComplete": "Doodde 3 slechteriken met TNT op ${LEVEL}", + "name": "Dynamite gaat Boom" + }, + "Boxer": { + "description": "win without useing any fucking bombs", + "descriptionComplete": "Gewonnen zonder het gebruiken van bommen", + "descriptionFull": "Haal ${LEVEL} zonder gebruik van bommen", + "descriptionFullComplete": "Voltooide ${LEVEL} zonder gebruik van bommen", + "name": "Bokser" + }, + "Dual Wielding": { + "descriptionFull": "Verbind 2 controllers (hardware of app)", + "descriptionFullComplete": "Verbind 2 controllers (hardware of app)", + "name": "Dubbel gewapend" + }, + "Flawless Victory": { + "description": "Win zonder geraakt te worden", + "descriptionComplete": "Gewonnen zonder geraakt te worden", + "descriptionFull": "Win ${LEVEL} zonder geraakt te worden", + "descriptionFullComplete": "Won ${LEVEL} zonder geraakt te worden", + "name": "Perfecte Overwinning" + }, + "Free Loader": { + "descriptionFull": "Start een Ieder-Voor-Zich spel met minimaal 2+ spelers", + "descriptionFullComplete": "Een Ieder-Voor-Zich spel gestart met minimaal 2+ spelers", + "name": "Gratis lader" + }, + "Gold Miner": { + "description": "Dood 6 slechteriken met landmijnen", + "descriptionComplete": "Doodde 6 slechteriken met landmijnen", + "descriptionFull": "Dood 6 slechteriken met landmijnen op ${LEVEL}", + "descriptionFullComplete": "Doodde 6 slechteriken met landmijnen op ${LEVEL}", + "name": "Goud Mijner" + }, + "Got the Moves": { + "description": "Win zonder te slaan of bommen te gooien", + "descriptionComplete": "Winnen zonder te slaan of bommen te gooien", + "descriptionFull": "Win ${LEVEL} zonder slagen of bommen", + "descriptionFullComplete": "Won ${LEVEL} zonder slagen of bommen", + "name": "Heb de Bewegingen" + }, + "In Control": { + "descriptionFull": "Verbind een controller (hardware of app)", + "descriptionFullComplete": "Een controller verbonden. (hardware of app)", + "name": "Onder controle" + }, + "Last Stand God": { + "description": "Score 1000 punten", + "descriptionComplete": "Scoorde 1000 punten", + "descriptionFull": "Score 1000 punten op ${LEVEL}", + "descriptionFullComplete": "Scoorde 1000 punten op ${LEVEL}", + "name": "${LEVEL} God" + }, + "Last Stand Master": { + "description": "Score 250 punten", + "descriptionComplete": "250 punten gescoord", + "descriptionFull": "Score 250 punten bij ${LEVEL}", + "descriptionFullComplete": "250 punten gescoord bij ${LEVEL}", + "name": "${LEVEL} Meester" + }, + "Last Stand Wizard": { + "description": "Score 500 punten", + "descriptionComplete": "scoorde 500 punten", + "descriptionFull": "Score 500 punten bij ${LEVEL}", + "descriptionFullComplete": "500 punten gescoord bij ${LEVEL}", + "name": "${LEVEL} Tovenaar" + }, + "Mine Games": { + "description": "Dood 3 slechteriken met landmijnen", + "descriptionComplete": "Doodde 3 slechteriken met landmijnen", + "descriptionFull": "Dood 3 slechteriken met landmijnen op ${LEVEL}", + "descriptionFullComplete": "3 slechteriken gedood met landmijnen op ${LEVEL}", + "name": "Landmijn Spellen" + }, + "Off You Go Then": { + "description": "Gooi 3 slechteriken van het speelveld", + "descriptionComplete": "Gooide 3 slechteriken van het speelveld", + "descriptionFull": "Gooi 3 slechteriken van het speelveld in ${LEVEL}", + "descriptionFullComplete": "3 slechteriken van het speelveld gegooid in ${LEVEL}", + "name": "Daar Ga Je Dan" + }, + "Onslaught God": { + "description": "Score 5000 punten", + "descriptionComplete": "Scoorde 5000 punten", + "descriptionFull": "Score 5000 punten in ${LEVEL}", + "descriptionFullComplete": "5000 punten gescoord in ${LEVEL}", + "name": "${LEVEL} God" + }, + "Onslaught Master": { + "description": "Score 500 punten", + "descriptionComplete": "Scoorde 500 punten", + "descriptionFull": "Score 500 punten in ${LEVEL}", + "descriptionFullComplete": "500 punten gescoord in ${LEVEL}", + "name": "${LEVEL} Meester" + }, + "Onslaught Training Victory": { + "description": "Versla alle golven", + "descriptionComplete": "Alle golven verslagen", + "descriptionFull": "versla alle golven in ${LEVEL}", + "descriptionFullComplete": "Alle golven in ${LEVEL} verslagen", + "name": "${LEVEL} Overwinning" + }, + "Onslaught Wizard": { + "description": "Score 1000 punten", + "descriptionComplete": "Scoorde 1000 punten", + "descriptionFull": "Score 1000 punten in ${LEVEL}", + "descriptionFullComplete": "1000 punten gescoord in ${LEVEL}", + "name": "${LEVEL} Tovenaar" + }, + "Precision Bombing": { + "description": "Win zonder powerups", + "descriptionComplete": "Gewonnen zonder powerups", + "descriptionFull": "Win ${LEVEL} zonder enige power-ups", + "descriptionFullComplete": "${LEVEL} gewonnen zonder enige power-ups", + "name": "Precisie Bombardementen" + }, + "Pro Boxer": { + "description": "Winnen zonder het gebruik van bommen", + "descriptionComplete": "Gewonnen zonder het gebruik van bommen", + "descriptionFull": "Voltooi ${LEVEL} zonder het gebruik van bommen", + "descriptionFullComplete": "${LEVEL} voltooid zonder het gebruik van bommen", + "name": "Pro Boxer" + }, + "Pro Football Shutout": { + "description": "Win zonder de slechteriken te laten scoren", + "descriptionComplete": "Gewonnen zonder de slechteriken te laten scoren", + "descriptionFull": "Win ${LEVEL} zonder de slechteriken te laten scoren", + "descriptionFullComplete": "${LEVEL} gewoon zonder de slechteriken te laten scoren", + "name": "${LEVEL} Buitengesloten" + }, + "Pro Football Victory": { + "description": "Win het wedstrijd", + "descriptionComplete": "Won de wedstrijd", + "descriptionFull": "Win de wedstrijd in ${LEVEL}", + "descriptionFullComplete": "Won de wedstrijd in ${LEVEL}", + "name": "${LEVEL} overwinning" + }, + "Pro Onslaught Victory": { + "description": "Versla alle golven", + "descriptionComplete": "Alle golven verlagen", + "descriptionFull": "Verslag alle golven in ${LEVEL}", + "descriptionFullComplete": "Alle golven van ${LEVEL} verslagen", + "name": "${LEVEL} Gewonnen" + }, + "Pro Runaround Victory": { + "description": "Voltooi alle golven", + "descriptionComplete": "Alle golven voltooid", + "descriptionFull": "Voltooi alle golven van ${LEVEL}", + "descriptionFullComplete": "Alle golven van ${LEVEL} voltooid", + "name": "${LEVEL} Overwinning" + }, + "Rookie Football Shutout": { + "description": "Winnen zonder dat de slechteriken scoren", + "descriptionComplete": "Gewonnen zonder dat de slechteriken scoorden", + "descriptionFull": "Win Rookie${LEVEL} zonder dat de slechteriken scoren", + "descriptionFullComplete": "${LEVEL} gewonnen zonder dat de slechteriken scoren", + "name": "${LEVEL} Buitengesloten" + }, + "Rookie Football Victory": { + "description": "Win het spel", + "descriptionComplete": "De wedstrijd gewonnen", + "descriptionFull": "Win de wedstrijd in ${LEVEL}", + "descriptionFullComplete": "De wedstrijd gewonnen in ${LEVEL}", + "name": "${LEVEL} Overwinning" + }, + "Rookie Onslaught Victory": { + "description": "Versla alle golven", + "descriptionComplete": "Alle golven verslagen", + "descriptionFull": "Verslag alle golven in ${LEVEL}", + "descriptionFullComplete": "Alle golven in ${LEVEL} verslagen", + "name": "${LEVEL} Overwinning" + }, + "Runaround God": { + "description": "Score 2000 punten", + "descriptionComplete": "Scoorde 2000 punten", + "descriptionFull": "Score 2000 punten in ${LEVEL}", + "descriptionFullComplete": "2000 punten gescoord in ${LEVEL}", + "name": "${LEVEL} God" + }, + "Runaround Master": { + "description": "Score 500 punten", + "descriptionComplete": "500 punten gescoord", + "descriptionFull": "Score 500 punten in ${LEVEL}", + "descriptionFullComplete": "500 punten gescoord in ${LEVEL}", + "name": "${LEVEL} Meester" + }, + "Runaround Wizard": { + "description": "Score 1000 punten", + "descriptionComplete": "1000 punten gescoord", + "descriptionFull": "Score 1000 punten in ${LEVEL}", + "descriptionFullComplete": "1000 punten gescoord in ${LEVEL}", + "name": "${LEVEL} Tovenaar" + }, + "Sharing is Caring": { + "descriptionFull": "Deel succesvol de game met een vriend", + "descriptionFullComplete": "Succesvol de game met een vriend gedeeld", + "name": "Delen is verzorgen" + }, + "Stayin' Alive": { + "description": "Win zonder dood te gaan", + "descriptionComplete": "Gewonnen zonder dood te gaan", + "descriptionFull": "Win ${LEVEL} zonder dood te gaan", + "descriptionFullComplete": "${LEVEL} gewonnen zonder dood te gaan", + "name": "Blijven Leven" + }, + "Super Mega Punch": { + "description": "Breng 100% schade toe met een slag", + "descriptionComplete": "100% schade toegebracht met een slag", + "descriptionFull": "Breng 100% schade toe met een slag in ${LEVEL}", + "descriptionFullComplete": "100% schade toegebracht met een slag in ${LEVEL}", + "name": "Super Mega Slag" + }, + "Super Punch": { + "description": "Breng 50% schade toe met een slag", + "descriptionComplete": "50% schade toegebracht met een slag", + "descriptionFull": "Breng 50% schade toe met een slag in ${LEVEL}", + "descriptionFullComplete": "50% schade toegebracht met een slag in ${LEVEL}", + "name": "Super Slag" + }, + "TNT Terror": { + "description": "Dood 6 slechteriken met TNT", + "descriptionComplete": "6 Slechteriken gedood met TNT", + "descriptionFull": "Dood 6 slechteriken met TNT in ${LEVEL}", + "descriptionFullComplete": "6 Slechteriken gedood met TNT in ${LEVEL}", + "name": "TNT Terreur" + }, + "Team Player": { + "descriptionFull": "Start een team spel met 4+ spelers", + "descriptionFullComplete": "Een spel gestart met 4+ spelers", + "name": "Team speler" + }, + "The Great Wall": { + "description": "Stop iedere slechterik", + "descriptionComplete": "Iedere slechterik gestopt", + "descriptionFull": "Stop iedere slechterik in ${LEVEL}", + "descriptionFullComplete": "Iedere slechterik gestopt in ${LEVEL}", + "name": "De Grote Muur" + }, + "The Wall": { + "description": "Stop iedere slechterik", + "descriptionComplete": "Iedere slechterik gestopt", + "descriptionFull": "Stop iedere slechterik in ${LEVEL}", + "descriptionFullComplete": "Iedere slechterik gestopt in ${LEVEL}", + "name": "De Muur" + }, + "Uber Football Shutout": { + "description": "Win zonder de slechteriken te laten scoren", + "descriptionComplete": "Gewonnen zonder de slechteriken te laten scoren", + "descriptionFull": "Win ${LEVEL} zonder de slechteriken te laten scoren", + "descriptionFullComplete": "${LEVEL} gewonnen zonder de slechteriken te laten scoren", + "name": "${LEVEL} Buitengesloten" + }, + "Uber Football Victory": { + "description": "Win de wedstrijd", + "descriptionComplete": "De wedstrijd gewonnen", + "descriptionFull": "Win de wedstrijd in ${LEVEL}", + "descriptionFullComplete": "De wedstrijd gewonnen in ${LEVEL}", + "name": "${LEVEL} Overwinning" + }, + "Uber Onslaught Victory": { + "description": "Versla alle golven", + "descriptionComplete": "Alle golven verslagen", + "descriptionFull": "Verslag alle golven in ${LEVEL}", + "descriptionFullComplete": "Alle golven verslagen in ${LEVEL}", + "name": "${LEVEL} Overwinning" + }, + "Uber Runaround Victory": { + "description": "Voltooi alle golven", + "descriptionComplete": "Alle golven voltooid", + "descriptionFull": "Voltooi alle golven in ${LEVEL}", + "descriptionFullComplete": "Alle golven voltooid in ${LEVEL}", + "name": "${LEVEL} Overwinning" + } + }, + "achievementsRemainingText": "Resterende Prestaties:", + "achievementsText": "Prestaties", + "achievementsUnavailableForOldSeasonsText": "Sorry, prestatie gegevens voor oude seizoenen zijn niet beschikbaar.", + "addGameWindow": { + "getMoreGamesText": "Verkrijg Meer Spellen...", + "titleText": "Voeg Game toe" + }, + "allowText": "Toestaan", + "alreadySignedInText": "Uw account is al ingelogd op een ander apparaat;\nVerander van account of sluit het spel op uw andere\napparaten en probeer het opnieuw.", + "apiVersionErrorText": "Kan module ${NAME} niet laden; deze gebruikt api-versie ${VERSION_USED}; benodigd is ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" activeert dit alleen als een koptelefoon is aangesloten)", + "headRelativeVRAudioText": "Hoofd-Relatieve VR Geluid", + "musicVolumeText": "Muziek Volume", + "soundVolumeText": "Geluid Volume", + "soundtrackButtonText": "Muzieklijsten", + "soundtrackDescriptionText": "(wijs uw eigen muziek toe om af te spelen tijdens het spelen)", + "titleText": "Geluid" + }, + "autoText": "Auto", + "backText": "Terug", + "banThisPlayerText": "Verband deze speler", + "bestOfFinalText": "Beste-van-${COUNT} Definitief", + "bestOfSeriesText": "Beste van ${COUNT} reeks:", + "bestRankText": "Uw beste is #${RANK}", + "bestRatingText": "Uw beste rating is ${RATING}", + "betaErrorText": "Deze beta is niet meer actief, controleer daarom voor een nieuwe versie.", + "betaValidateErrorText": "Niet in staat om beta te valideren. (geen net verbinding?)", + "betaValidatedText": "Beta Gevalideerd; Geniet er van!", + "bombBoldText": "BOM", + "bombText": "Bom", + "boostText": "Boost", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} is geconfigureerd in de app zelf.", + "buttonText": "knop", + "canWeDebugText": "Wilt u dat BombSquad automatisch bugs, crashes,\nen basisgebruik info naar de ontwikkelaar rapporteert?\n\nDeze gegevens bevatten geen persoonlijke informatie\nen helpen om het spel soepel en bug-vrij te houden.", + "cancelText": "Annuleer", + "cantConfigureDeviceText": "Sorry, $ {DEVICE} niet configureerbaar.", + "challengeEndedText": "Deze uitdaging is beëindigd.", + "chatMuteText": "Chat negeren", + "chatMutedText": "berichten gedempt", + "chatUnMuteText": "maak de chat ongedaan", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "U moet dit level voltooien\nom door te gaan!", + "completionBonusText": "Voltooiing Bonus", + "configControllersWindow": { + "configureControllersText": "Configureer Controllers", + "configureGamepadsText": "Gamepads configureren", + "configureKeyboard2Text": "Configureer toetsenbord P2", + "configureKeyboardText": "Configureer Toetsenbord", + "configureMobileText": "Mobiele Apparaten als Controllers", + "configureTouchText": "Configureer Touchscreen", + "ps3Text": "PS3 Controllers", + "titleText": "Controllers", + "wiimotesText": "Wiimotes", + "xbox360Text": "Xbox 360 Controllers" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Opmerking: controller ondersteuning varieert per apparaat en de Android-versie.", + "pressAnyButtonText": "Druk op een knop van de controller\n  die u wilt configureren...", + "titleText": "Configureer Controllers" + }, + "configGamepadWindow": { + "advancedText": "Geavanceerd", + "advancedTitleText": "Geavanceerde Cotroller Configuratie", + "analogStickDeadZoneDescriptionText": "(zet deze aan als uw personage 'zweeft' als u de stick loslaat)", + "analogStickDeadZoneText": "Analoge Stick Dead Zone", + "appliesToAllText": "(geldt voor alle controllers van dit type)", + "autoRecalibrateDescriptionText": "(sta dit toe als uw personage niet beweegt op volle snelheid)", + "autoRecalibrateText": "Auto-Herkalibreer Analoge Stick", + "axisText": "as", + "clearText": "wissen", + "dpadText": "dpad", + "extraStartButtonText": "Extra Start Knop", + "ifNothingHappensTryAnalogText": "Als er niets gebeurt, probeer het dan in plaats daar van toe te toewijzen aan de analoge stick.", + "ifNothingHappensTryDpadText": "Als er niets gebeurt, probeer het dan in plaats daar van toe te wijzen aan de d-pad.", + "ignoreCompletelyDescriptionText": "(voorkom dat deze controller effect heeft op het spel of de menu's)", + "ignoreCompletelyText": "Negeer Volledig", + "ignoredButton1Text": "Genegeerde Knop 1", + "ignoredButton2Text": "Genegeerde Knop 2", + "ignoredButton3Text": "Genegeerde Knop 3", + "ignoredButton4Text": "Knop 4 genegeerd", + "ignoredButtonDescriptionText": "(gebruik deze om te voorkomen dat de 'home' of 'sync' invloed hebben op de UI)", + "ignoredButtonText": "Genegeerde Knop", + "pressAnyAnalogTriggerText": "Druk op een analoge trekker...", + "pressAnyButtonOrDpadText": "Druk op een knop of dpad...", + "pressAnyButtonText": "Druk een knop...", + "pressLeftRightText": "Druk links of rechts...", + "pressUpDownText": "Druk omhoog of omlaag...", + "runButton1Text": "Ren Knop 1", + "runButton2Text": "Ren Knop 2", + "runTrigger1Text": "Ren Activering 1", + "runTrigger2Text": "Ren Activering 2", + "runTriggerDescriptionText": "(analoge triggers laten u op variabele snelheid rennen)", + "secondHalfText": "Gebruik dit om de tweede helft te configureren\nvan een 2-controller-in-1 apparaat dat\nzich toont als een enkele controller.", + "secondaryEnableText": "Toestaan", + "secondaryText": "Secundaire Controller", + "startButtonActivatesDefaultDescriptionText": "(zet dit uit als uw start knop eerder een 'menu' knop is)", + "startButtonActivatesDefaultText": "Start Knop Activeert Standaard Widget", + "titleText": "Controller Instellingen", + "twoInOneSetupText": "2-in-1 Controller Instellingen", + "uiOnlyDescriptionText": "(voorkom dat deze controller meedoet in een spel)", + "uiOnlyText": "Limiteer tot menu gebruik", + "unassignedButtonsRunText": "Alle niet-toegewezen knoppen Ren", + "unsetText": "", + "vrReorientButtonText": "VR Heroriënteren Knop" + }, + "configKeyboardWindow": { + "configuringText": "Configuratie ${DEVICE}", + "keyboard2NoteText": "Opmerking: de meeste toetsenborden kunnen maar enkele\ntoetsaanslagen tegelijk registreren, dus het hebben van een\ntweede toetsenbord kan beter werken als er een apart toetsenbord\nis verbonden voor hen te gebruiken. Let op dat u nog steeds\ntoetsen moet toewijzen aan de tweede speler, zelfs in dit geval." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Actie Besturing Schaal", + "actionsText": "Acties", + "buttonsText": "knoppen", + "dragControlsText": "< sleep controls slepen om ze te herpositioneren >", + "joystickText": "stuurknuppel", + "movementControlScaleText": "Bewegings Besturing Schaal", + "movementText": "Beweging", + "resetText": "Herstel", + "swipeControlsHiddenText": "Verberg Swipe Iconen", + "swipeInfoText": "De 'veeg' stijl controls is even een beetje wennen, maar maakt\nhet makkelijker om te spelen zonder te kijken naar de besturing.", + "swipeText": "veeg", + "titleText": "Configureer Touchscreen", + "touchControlsScaleText": "Touch Besturing Schalen" + }, + "configureItNowText": "Nu configureren?", + "configureText": "Configureren", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "Voor het beste resultaat heb u een vertraging-vrij wifi netwerk nodig. u\nkunt de wifi lag verminderen door het uitschakelen van andere\ndraadloze apparaten, door dicht bij uw wifi-router te spelen en door\nde spel host rechtstreeks aan te sluiten op het netwerk via ethernet.", + "explanationText": "Om een smartphone of tablet te gebruiken als een draadloze controller,\ninstalleer dan de \"${REMOTE_APP_NAME}\" app er op. Iedere hoeveelheid apparaten\nkan verbinding maken met een ${APP_NAME} spel via WiFi, en het is gratis!", + "forAndroidText": "voor Android:", + "forIOSText": "voor iOS:", + "getItForText": "Krijg de ${REMOTE_APP_NAME} voor iOS in de Apple App Store\nof voor Android in de Google Play Store of de Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Gebruik Mobiele Apparaten als Controllers:" + }, + "continuePurchaseText": "Voortzetten voor ${PRICE}?", + "continueText": "Voortzetten", + "controlsText": "Besturing", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Dit heeft geen invloed op de all-time klassement.", + "activenessInfoText": "Deze verdubbelaar stijgt op dagen dat je\nspeelt en zakt op dagen dat je niet speelt.", + "activityText": "Activiteit", + "campaignText": "Campagne", + "challengesInfoText": "Verdien prijzen door minigames te voltooien.\n\nPrijzen en moeilijkheidsgraden verhogen\nelke keer een uitdaging is gehaald en\nhet verlaagd als er een verloopt of is opgegeven.", + "challengesText": "Uitdagingen", + "currentBestText": "Huidige Beste", + "customText": "Aangepast", + "entryFeeText": "Inschrijving", + "forfeitConfirmText": "Deze uitdaging opgeven?", + "forfeitNotAllowedYetText": "Deze uitdaging kan nog niet opgegeven worden.", + "forfeitText": "Opgeven", + "multipliersText": "Vermenigvuldigers", + "nextChallengeText": "Volgende Uitdaging", + "nextPlayText": "Volgende spel", + "ofTotalTimeText": "van ${TOTAL}", + "playNowText": "Speel Nu", + "pointsText": "Punten", + "powerRankingFinishedSeasonUnrankedText": "(Afgerond seizoen geen rangschikking)", + "powerRankingNotInTopText": "(niet in de top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} ptn", + "powerRankingPointsMultText": "(x ${NUMBER} ptn)", + "powerRankingPointsText": "${NUMBER} ptn", + "powerRankingPointsToRankedText": "(${CURRENT} van de ${REMAINING} ptn)", + "powerRankingText": "Macht Klassement", + "prizesText": "Prijzen", + "proMultInfoText": "Spelers met de ${PRO} opwaardering\nkrijgen een ${PERCENT}% punten boost hier.", + "seeMoreText": "Meer...", + "skipWaitText": "Sla wachten over", + "timeRemainingText": "Resterende Tijd", + "titleText": "Coöperatief", + "toRankedText": "Naar Rankschikking", + "totalText": "totaal", + "tournamentInfoText": "Strijd voor de highscores tegen andere spelers in jou competitie. \n\nPrijzen zijn bekroond aan de hoogst scorende spelers waneer de toernooi tijd voorbij is.", + "welcome1Text": "Welkom bij ${LEAGUE}. U kan uw competitie rang\nverbeteren door sterren te verdienen,\nprestaties te voltooien en met het winnen van trofeeën in toernooien.", + "welcome2Text": "U kunt ook tickets verdienen van veel van dezelfde activiteiten.\nTickets kunnen gebruikt worden om mee te doen aan toernooien,\nvoor het vrijspelen van nieuwe karakters, speelvelden, mini-spellen en meer.", + "yourPowerRankingText": "Uw Macht Klassement:" + }, + "copyOfText": "${NAME} Kopie", + "copyText": "Kopiëren", + "copyrightText": "© 2013 Eric Froemling", + "createAPlayerProfileText": "Speler profiel aanmaken?", + "createEditPlayerText": "", + "createText": "Maak", + "creditsWindow": { + "additionalAudioArtIdeasText": "Additionele Audio, Eerdere Illustraties, en Ideeën door ${NAME}", + "additionalMusicFromText": "Additionele muziek van ${NAME}", + "allMyFamilyText": "All mijn vrienden en familie die hebben geholpen met testen", + "codingGraphicsAudioText": "Programmeren, Illustraties, en Audio door ${NAME}", + "languageTranslationsText": "Vertalingen:", + "legalText": "Legaliteiten:", + "publicDomainMusicViaText": "Rechtenvrije muziek via ${NAME}", + "softwareBasedOnText": "Deze software is deels gebaseerd op het werk van ${NAME}", + "songCreditText": "${TITLE} Uitgevoerd door ${PERFORMER}\nGecomposeerd door ${COMPOSER}, Gearrangeerd door ${ARRANGER},\nGepubliceerd oor ${PUBLISHER}, Met dank aan ${SOURCE}", + "soundAndMusicText": "Geluid & Muziek:", + "soundsText": "Geluiden (${SOURCE}):", + "specialThanksText": "Speciale Dank:", + "thanksEspeciallyToText": "Vooral mijn dank aan ${NAME}", + "titleText": "${APP_NAME} Credits", + "whoeverInventedCoffeeText": "Wie dan ook koffie heeft uitgevonden" + }, + "currentStandingText": "Je huidige rang is #${RANK}", + "customizeText": "Aanpassen...", + "deathsTallyText": "${COUNT} sterfgevallen", + "deathsText": "Sterfgevallen", + "debugText": "foutopsporing", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Opmerking: het wordt aanbevolen dat u Instellingen->Grafisch->Texturen op 'Hoog' zet als u dit test.", + "runCPUBenchmarkText": "Voer CPU Maatstaaf Uit", + "runGPUBenchmarkText": "Voer GPU Maatstaaf Uit", + "runMediaReloadBenchmarkText": "Voer Media-Herlaad Maatstaaf Uit", + "runStressTestText": "Voer stress test uit", + "stressTestPlayerCountText": "Spelers Aantal", + "stressTestPlaylistDescriptionText": "Stress Test Speellijst", + "stressTestPlaylistNameText": "Speellijst Naam", + "stressTestPlaylistTypeText": "Speellijst Type", + "stressTestRoundDurationText": "Ronde Duratie", + "stressTestTitleText": "Stress test", + "titleText": "Maatstaven & Stress Tests", + "totalReloadTimeText": "Totale herlaad tijd: ${TIME} (zie log voor details)", + "unlockCoopText": "Ontgrendel coöperatieve levels" + }, + "defaultFreeForAllGameListNameText": "Standaard Ieder-voor-Zich Spellen", + "defaultGameListNameText": "Standaard ${PLAYMODE} Speellijst", + "defaultNewFreeForAllGameListNameText": "Mijn Ieder-voor-Zich Spellen", + "defaultNewGameListNameText": "Mijn ${PLAYMODE} Speellijsten", + "defaultNewTeamGameListNameText": "Mijn Team Spellen", + "defaultTeamGameListNameText": "Standaard Team Spellen", + "deleteText": "Verwijder", + "demoText": "demonstratie", + "denyText": "Weigeren", + "desktopResText": "Bureaublad Resolutie", + "difficultyEasyText": "Makkelijk", + "difficultyHardOnlyText": "Alleen moeilijk", + "difficultyHardText": "Moeilijk", + "difficultyHardUnlockOnlyText": "Dit level kan alleen in moeilijke modus vrij gespeeld worden.\nDurf je het aan!?!?!", + "directBrowserToURLText": "Gebruik een web browser om het volgende adres te bezoeken:", + "disableRemoteAppConnectionsText": "Schakel Afstandsbediening-App connecties uit", + "disableXInputDescriptionText": "Staat meer dan 4 controllers toe, maar werkt misschien niet helemaal goed.", + "disableXInputText": "Schakel XInput uit", + "doneText": "Klaar", + "drawText": "Gelijk", + "duplicateText": "Kopieer", + "editGameListWindow": { + "addGameText": "Voeg\nSpel\nToe", + "cantOverwriteDefaultText": "Kan de standaard speellijst niet overschrijven!", + "cantSaveAlreadyExistsText": "Er bestaat al een speellijst met die naam!", + "cantSaveEmptyListText": "Kan een lege speellijst niet opslaan!", + "editGameText": "Spel\nBewerken", + "gameListText": "Spellen Lijst", + "listNameText": "Speellijst Naam", + "nameText": "Naam", + "removeGameText": "Verwijder\nSpel", + "saveText": "Lijst Opslaan", + "titleText": "Speellijst Bewerker" + }, + "editProfileWindow": { + "accountProfileInfoText": "Dit speciale profiel heeft een naam en een pictogram gabaseerd op je profiel. \n\n${ICONS}\n\nCreëer aangepaste profielen als je andere namen en pictogrammen wilt gebruiken.", + "accountProfileText": "(gebruiker profiel)", + "availableText": "De naam \"${NAME}\" is beschikbaar.", + "changesNotAffectText": "Let op: aanpassingen hebben geen invloed op karakters die al in het spel zijn", + "characterText": "karakter", + "checkingAvailabilityText": "Controleren van beschikbaarheid voor \"${NAME}\"...", + "colorText": "kleur", + "getMoreCharactersText": "Verkrijg Meer Karakters...", + "getMoreIconsText": "Krijg Meer Pictogrammen...", + "globalProfileInfoText": "Globale speler profielen hebben gegarandeerd een unieke\nnaam wereldwijd. Ze hebben ook aangepaste pictogrammen.", + "globalProfileText": "(globaal profiel)", + "highlightText": "highlight", + "iconText": "pictogram", + "localProfileInfoText": "Lokale speler profielen hebben geen pictogrammen en hun namen zijn\nniet gegarandeerd uniek. Waardeer op naar een globaal profiel om een unieke\nnaam te krijgen en om een aangepast pictogram toe te voegen.", + "localProfileText": "(lokaal profiel)", + "nameDescriptionText": "Speler Naam", + "nameText": "Naam", + "randomText": "willekeurig", + "titleEditText": "Profiel Aanpassen", + "titleNewText": "Nieuw Profiel", + "unavailableText": "\"${NAME}\" is niet beschikbaar; probeer een andere naam.", + "upgradeProfileInfoText": "Hier door krijg je een wereld wijde speler naam\nen u kunt een aangepast pictogram toewijzen.", + "upgradeToGlobalProfileText": "Waardeer op naar Globaal Profiel" + }, + "editProfilesAnyTimeText": "(Je kan profielen altijd aanpassen in 'instellingen')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "U kunt de standaard muzieklijst niet verwijderen.", + "cantEditDefaultText": "Kan standaard muzieklijst niet bewerken. Dupliceer het of maak een nieuwe.", + "cantEditWhileConnectedOrInReplayText": "Kan muzieklijsten niet aanpassen als je verbonden bent met een partij of in een herhaling.", + "cantOverwriteDefaultText": "Kan standaard muzieklijst niet overschrijven", + "cantSaveAlreadyExistsText": "Er bestaat al een muzieklijst met die naam!", + "copyText": "Kopiëren", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Standaard Muzieklijst", + "deleteConfirmText": "Verwijder Muzieklijst:\n\n'${NAME}'?", + "deleteText": "Verwijder\nMuzieklijst", + "duplicateText": "Dupliceer\nMuzieklijst", + "editSoundtrackText": "Muzieklijst Bewerker", + "editText": "Bewerk Muzieklijst", + "fetchingITunesText": "afspeellijsten van Muziek-app ophalen ...", + "musicVolumeZeroWarning": "Waarschuwing: muziek volume is ingesteld op 0", + "nameText": "Naam", + "newSoundtrackNameText": "Mijn Muzieklijst ${COUNT}", + "newSoundtrackText": "Nieuwe Muzieklijst:", + "newText": "Nieuwe\nMuzieklijst", + "selectAPlaylistText": "Selecteer Een Afspeellijst", + "selectASourceText": "Muziek Bron", + "soundtrackText": "SoundTrack", + "testText": "test", + "titleText": "Muzieklijsten", + "useDefaultGameMusicText": "Standaard Spel Muziek", + "useITunesPlaylistText": "Muziek app afspeellijst", + "useMusicFileText": "Muziek Bestand (mp3, etc)", + "useMusicFolderText": "Map met Muziek Bestanden" + }, + "editText": "Bewerk", + "endText": "Einde", + "enjoyText": "Geniet!", + "epicDescriptionFilterText": "${DESCRIPTION} In epische slow motion.", + "epicNameFilterText": "Epische ${NAME}", + "errorAccessDeniedText": "toegang geweigerd", + "errorOutOfDiskSpaceText": "schuifruimte vol", + "errorText": "Fout", + "errorUnknownText": "onbekende fout", + "exitGameText": "${APP_NAME} Verlaten?", + "exportSuccessText": "'${NAME}' geëxporteerd.", + "externalStorageText": "Externe Opslag", + "failText": "Faal", + "fatalErrorText": "Uh oh; er mist iets of is kapot.\nInstalleer de app opnieuw, of\nvraag ${EMAIL} voor hulp.", + "fileSelectorWindow": { + "titleFileFolderText": "Selecteer een Map of Bestand", + "titleFileText": "Selecteer een Bestand", + "titleFolderText": "Selecteer een Map", + "useThisFolderButtonText": "Gebruik Deze Map" + }, + "finalScoreText": "Uiteindelijke Score", + "finalScoresText": "Uiteindelijke Scores", + "finalTimeText": "Uiteindelijke Tijd", + "finishingInstallText": "Installatie afwerken; een moment...", + "fireTVRemoteWarningText": "* Voor een betere ervaring:\ngebruik een Spel Controller of installeer de\n'${REMOTE_APP_NAME}' app op uw telefoon\nof tablet.", + "firstToFinalText": "Eerste-tot-${COUNT} Finale", + "firstToSeriesText": "Eerste-tot-${COUNT} Serie", + "fiveKillText": "VIJF DODEN!", + "flawlessWaveText": "Foutloze Golf!", + "fourKillText": "QUAD DOOD!!!", + "freeForAllText": "Ieder-voor-Zich", + "friendScoresUnavailableText": "Scores van vrienden onbeschikbaar.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Spel ${COUNT} Leiders", + "gameListWindow": { + "cantDeleteDefaultText": "U kunt de standaard speellijst niet verwijderen!", + "cantEditDefaultText": "Kan de standaard speellijst niet bewerken! Dupliceren het of maak een nieuwe.", + "cantShareDefaultText": "Je kan de standaard afspeellijst niet delen.", + "deleteConfirmText": "Verwijder \"${LIST}\"?", + "deleteText": "Verwijder\nSpeellijst", + "duplicateText": "Dupliceer\nSpeellijst", + "editText": "Bewerk\nSpeellijst", + "gameListText": "Spellen Lijst", + "newText": "Nieuwe\nSpeellijst", + "showTutorialText": "Uitleg Weergeven", + "shuffleGameOrderText": "Willekeurige Spel Volgorde", + "titleText": "Pas ${TYPE} Speellijst aan" + }, + "gameSettingsWindow": { + "addGameText": "Voeg Spel Toe" + }, + "gamepadDetectedText": "1 controler gedetecteerd", + "gamepadsDetectedText": "${COUNT} controllers gedetecteerd.", + "gamesToText": "${WINCOUNT} spellen tegen ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Onthoud: elk apparaat in een partij kan meer\ndan een speler hebben als u genoeg controllers hebt.", + "aboutDescriptionText": "Gebruik deze tabbladen om een partij samen te stellen.\n\nMet partijen kan u spellen en toernooien spelen\nmet uw vrienden op een ander apparaat.\n\nGebruik de ${PARTY} knop rechtsbovenaan om\nte chatten met uw partij.\n(bij een controller , druk ${BUTTON} als u in een menu bent)", + "aboutText": "Over", + "addressFetchErrorText": "", + "appInviteInfoText": "Nodig vrienden uit om BombSquad te proberen en ze\nkrijgen ${COUNT} gratis tickets. U verdient\n${YOU_COUNT} voor iedere vriend die dat doet.", + "appInviteMessageText": "${NAME} heeft je ${COUNT} tickets gegeven in ${APP_NAME}", + "appInviteSendACodeText": "Zendt Ze Een Code", + "appInviteTitleText": "${APP_NAME} app uitnodiging", + "bluetoothAndroidSupportText": "(werkt met elk Android apparaat met Bluetooth)", + "bluetoothDescriptionText": "Host/deelnemen aan een partij via Bluetooth:", + "bluetoothHostText": "Host via Bluetooth", + "bluetoothJoinText": "Neem deel via Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "controleren...", + "dedicatedServerInfoText": "Voor het beste resultaat, zet dan een dedicated server op. Zie bombsquadgame.com/server om te leren hoe.", + "disconnectClientsText": "Hierdoor verbreekt de verbinding met ${COUNT} spelers(s)\nvan uw partij. Weet u het zeker?", + "earnTicketsForRecommendingAmountText": "Vrienden ontvangen ${COUNT} tickets als ze het spel proberen\n(en jij ontvangt ${YOU_COUNT} voor elke vriend die dit doet)", + "earnTicketsForRecommendingText": "Deel de game \nVoor gratis tickets...", + "emailItText": "Mail het", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} tickets van ${NAME}", + "friendPromoCodeAwardText": "Je krijgt ${COUNT} tickets elke keer het is gebruikt.", + "friendPromoCodeExpireText": "Deze code zal vervallen in ${EXPIRE_HOURS} uren en werkt alleen met nieuwe spelers.", + "friendPromoCodeInstructionsText": "Om het te gebruiken, open je ${APP_NAME} en ga je naar 'Instellingen-> Geavanceerd-> Voer code in'.\nZie bombsquadgame.com voor downloadlinks voor alle ondersteunde platforms.", + "friendPromoCodeRedeemLongText": "Het kan ingewisseld worden voor ${COUNT} gratis tickets met tot en met ${MAX_USES} mensen.", + "friendPromoCodeRedeemShortText": "Het kan ingewisseld worden voor ${COUNT} in het spel", + "friendPromoCodeWhereToEnterText": "(in \"Instellingen-> Geavanceerd-> Voer code in\")", + "getFriendInviteCodeText": "Krijg een uitnodigingscode", + "googlePlayDescriptionText": "Nodig Google Play spelers uit voor uw partij:", + "googlePlayInviteText": "Uitnodigen", + "googlePlayReInviteText": "Er zijn ${COUNT} Google Play speler(s) in uw partij\nwaarbij de verbinding verbreek als je een nieuwe uitnodiging start.\nVoeg ze bij de nieuwe uitnodiging toe om ze er bij te houden.", + "googlePlaySeeInvitesText": "Zie Uitnodigingen", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play versie)", + "hostPublicPartyDescriptionText": "Accommodeer een Publieke Partij:", + "inDevelopmentWarningText": "Let op:\n\nNetwerk spel is een nieuwe functie in ontwikkeling.\nVoor nu wordt het streng aangeraden dat alle\nspelers op het zelfde Wi-Fi netwerk zijn.", + "internetText": "Internet", + "inviteAFriendText": "Hebben je vrienden dit spel niet?\nNodig ze uit om het uit te proberen en ze krijgen ${COUNT} gratis tickets.", + "inviteFriendsText": "Nodig vrienden uit", + "joinPublicPartyDescriptionText": "Meedoen aan een Publieke Partij.", + "localNetworkDescriptionText": "Neem deel aan een partij in uw netwerk:", + "localNetworkText": "Lokaal Netwerk", + "makePartyPrivateText": "Maak mijn Partij Privé", + "makePartyPublicText": "Maak Mijn Partij Publiek", + "manualAddressText": "Adres", + "manualConnectText": "Verbind", + "manualDescriptionText": "Neem deel aan een partij via adres", + "manualJoinableFromInternetText": "Kunnen mensen deelnemen via het internet?:", + "manualJoinableNoWithAsteriskText": "NEE*", + "manualJoinableYesText": "JA", + "manualRouterForwardingText": "*om dit op te lossen, probeer uw router te configureren om UDP port ${PORT} door te sturen naar uw lokale adres", + "manualText": "Handmatig", + "manualYourAddressFromInternetText": "Uw adres via het internet:", + "manualYourLocalAddressText": "Uw lokale adres:", + "noConnectionText": "", + "otherVersionsText": "(andere versies)", + "partyInviteAcceptText": "Accepteren", + "partyInviteDeclineText": "Weigeren", + "partyInviteGooglePlayExtraText": "(bekijk het 'Google Play' tabblad in het 'Verzamel' venster)", + "partyInviteIgnoreText": "Negeren", + "partyInviteText": "${NAME} heeft je uitgenodigd\nom bij zijn partij te voegen!", + "partyNameText": "Partij Naam", + "partySizeText": "partij grootte", + "partyStatusCheckingText": "status controleren...", + "partyStatusJoinableText": "uw partij is nu toegankelijk via het internet", + "partyStatusNoConnectionText": "kan geen verbinding maken met server", + "partyStatusNotJoinableText": "uw partij is niet toegankelijk via het internet", + "partyStatusNotPublicText": "uw partij is niet publiek", + "pingText": "ping", + "portText": "Port", + "requestingAPromoCodeText": "Code aanvragen...", + "sendDirectInvitesText": "Verzend directe uitnodigingen", + "shareThisCodeWithFriendsText": "Deel deze code met vrienden:", + "showMyAddressText": "Toon Mijn Adres", + "startAdvertisingText": "Begin Adverteren", + "stopAdvertisingText": "Stop Adverteren", + "titleText": "Verzamel", + "wifiDirectDescriptionBottomText": "Als alle apparaten een 'Wi-Fi Direct' paneel hebben, dan zou het mogelijk moeten zijn\nom ze te vinden en verbinden met elkaar. Als alle apparaten verbonden zijn, kan u hier\npartijen vormen via het 'Lokaal Netwerk' tab, net als bij een normaal Wi-Fi netwerk.\n\nVoor het beste resultaat zou de Wi-Fi Direct host ook de host moeten zijn van de ${APP_NAME} partij.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct kan gebruikt worden om directe verbinding te maken tussen Android apparaten zonder\ngebruik te maken van een Wi-Fi netwerk. Dit werkt het best bij Android 4.2 of nieuwer.\n\nOm het te gebruiken, open de Wi-Fi instellingen en zoek voor 'Wi-Fi Direct' in het menu.", + "wifiDirectOpenWiFiSettingsText": "Open Wi-Fi Instellingen", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(Werkt tussen alle platformen)", + "worksWithGooglePlayDevicesText": "(werkt met apparaten de de Google Play (android) versie van het spel gebruiken.", + "youHaveBeenSentAPromoCodeText": "Je hebt een ${APP_NAME} promo code gekregen:" + }, + "getCoinsWindow": { + "coinDoublerText": "Munten Verdubbelaar", + "coinsText": "${COUNT} Munten", + "freeCoinsText": "Gratis Munten", + "restorePurchasesText": "Aanschaf Herstellen", + "titleText": "Munten Aanschaffen" + }, + "getTicketsWindow": { + "freeText": "GRATIS!", + "freeTicketsText": "Gratis Tickets", + "inProgressText": "Er wordt al een transactie verwerkt; probeer het later opnieuw.", + "purchasesRestoredText": "Aankopen hersteld.", + "receivedTicketsText": "${COUNT} tickets ontvangen!", + "restorePurchasesText": "Herstel Aanschafte Items", + "ticketDoublerText": "Ticket Verdubbelaar", + "ticketPack1Text": "Klein Ticket Pakket", + "ticketPack2Text": "Medium Ticket Pakket", + "ticketPack3Text": "Olifanten Ticket Pakket", + "ticketPack4Text": "Jumbo Ticket Pakket", + "ticketPack5Text": "Mammoeten Ticket Pakket", + "ticketPack6Text": "Ultieme Ticket Pakket", + "ticketsFromASponsorText": "Krijg ${COUNT} tickets\nvan een sponsor", + "ticketsText": "${COUNT} Tickets", + "titleText": "Tickets Verkrijgen", + "unavailableLinkAccountText": "Sorry, aankopen zijn niet beschikbaar op dit platform.\nAls een omweg, kan je dit account verbinden met een account\nop een ander platform en daar de aankoop doen.", + "unavailableTemporarilyText": "Dit is niet beschikbaar op dit moment; probeer het laten opnieuw.", + "unavailableText": "Sorry, dit is niet beschikbaar.", + "versionTooOldText": "Sorry, deze versie van het spel is te oud; update naar een nieuwere versie.", + "youHaveShortText": "Je hebt ${COUNT}", + "youHaveText": "U heeft ${COUNT} tickets" + }, + "googleMultiplayerDiscontinuedText": "Sorry, de Google Play multi-player functie is nu even niet beschikbaar. \nIk werk hard aan een vervanger. \nProbeer nu alsjeblieft een andere connectie mogelijkheid.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Altijd", + "fullScreenCmdText": "Volledig scherm (Cmd-F)", + "fullScreenCtrlText": "Volledig scherm (Cmd-F)", + "gammaText": "Gamma", + "highText": "Hoog", + "higherText": "Hoger", + "lowText": "Laag", + "mediumText": "Gemiddeld", + "neverText": "Nooit", + "resolutionText": "Resolutie", + "showFPSText": "Toon FPS", + "texturesText": "Texturen", + "titleText": "Grafisch", + "tvBorderText": "TV Grens", + "verticalSyncText": "Verticale Sync", + "visualsText": "Visuele" + }, + "helpWindow": { + "bombInfoText": "- Bom -\nSterker dan slagen, maar kan\nresulteren in ernstige zelfverwonding.\nVoor het beste resultaat, gooi richting\nvijand voordat lont op raakt.", + "canHelpText": "${APP_NAME} kan helpen.", + "controllersInfoText": "U kan ${APP_NAME} spelen met vrienden via een netwerk, of u\nkan het allemaal op hetzelfde apparaat spelen als u genoeg controllers hebt.\n${APP_NAME} ondersteunt er een verscheidenheid van; u kunt zelfs uw telefoons\nals controller gebruiken met de gratis '${REMOTE_APP_NAME}' app.\nZie Instellingen->Controllers voor meer info.", + "controllersInfoTextFantasia": "Een speler kan de afstandbediening gebruiken als controller,\nmaar gamepads zijn sterk aanbevolen. Je kunt ook mobiele-apparaten\ngebruiken als controllers via de gratis 'Bombsquad Remote' app.\nZie 'Controllers' onder 'Instellingen' voor meer info.", + "controllersInfoTextMac": "Een of twee spelers kunnen gebruik maken van het toetsenbord, maar Bombsquad is het beste met gamepads.\nBombsquad kan gebruik maken van USB gamepads, PS3 controllers, Xbox 360 controllers, gebruik\nWiimotes en iOS / Android-apparaten om de karakters te besturen. Hopelijk heb je\neen aantal van deze bij de hand. Zie 'Controllers' onder 'Instellingen' voor meer info.", + "controllersInfoTextOuya": "U kunt OUYA controllers, PS3 controllers, Xbox 360 controllers, \nen tal van andere USB-en Bluetooth-gamepads gebruiken met Bombsquad.\nU kunt ook iOS-en Android-apparaten gebruiken als controllers via de gratis\n'Bombsquad Remote' app. Zie 'Controllers' onder 'Instellingen' voor meer info.", + "controllersText": "Controllers", + "controlsSubtitleText": "Uw vriendelijke ${APP_NAME} personage heeft een aantal basis acties:", + "controlsText": "Besturing", + "devicesInfoText": "De VR versie van ${APP_NAME} kan gespeeld worden via het netwerk met\nde reguliere versie, dus grijp uw extra telefoons, tablets\nen computers en begin met spelen. Het kan ook nuttig zijn om\nmet een reguliere versie van het spel verbinding te maken met de VR versie\nzodat mensen van buitenaf de actie mee kunnen kijken.", + "devicesText": "Apparaten", + "friendsGoodText": "Deze zijn goed om te hebben. ${APP_NAME} is het leukste met\nmeerdere spelers en kan tot en met 8 spelers tegelijk ondersteunen, dit brengt ons naar:", + "friendsText": "Vrienden", + "jumpInfoText": "- Springen -\nSpring over kleine openingen,\nom dingen hoger te gooien, en om\ngevoelens van vreugde uit te drukken.", + "orPunchingSomethingText": "Of iets slaan, van een klif gooien, en op de weg naar beneden opblazen met een kleverige bom.", + "pickUpInfoText": "- Op Pakken -\nPak vlaggen, vijanden, of iets\nanders niet vastgeschroefd aan de\ngrond. Druk opnieuw om te gooien.", + "powerupBombDescriptionText": "Laat u drie bommen opzwepen\nachter elkaar in plaats van maar een.", + "powerupBombNameText": "Drievoudige-Bommen", + "powerupCurseDescriptionText": "Waarschijnlijk wilt u deze te vermijden.\n  ...of toch niet?", + "powerupCurseNameText": "Vloek", + "powerupHealthDescriptionText": "Herstelt u naar volledige gezondheid.\nU zou het nooit geraden hebben.", + "powerupHealthNameText": "Med-Pack", + "powerupIceBombsDescriptionText": "Zwakker dan normale bommen\nmaar bevriest u vijanden en\nmaakt ze zeer breekbaar.", + "powerupIceBombsNameText": "IJs-bommen", + "powerupImpactBombsDescriptionText": "Iets zwakker dan gewone\nbommen, maar ze ontploffen bij inslag.", + "powerupImpactBombsNameText": "Trigger-Bommen", + "powerupLandMinesDescriptionText": "Deze komen in verpakkingen van 3;\nNuttig voor basis defensie of\nbij het stoppen snelle vijanden.", + "powerupLandMinesNameText": "Land-Mijnen", + "powerupPunchDescriptionText": "Maakt u slagen harder,\nsneller, beter en sterker.", + "powerupPunchNameText": "Box-Handschoenen", + "powerupShieldDescriptionText": "Absorbeert een beetje schade\nzodat jij dat niet hoeft te doen.", + "powerupShieldNameText": "Energie-Schild", + "powerupStickyBombsDescriptionText": "Plakt aan alles wat ze raken.\nHilariteit volgt.", + "powerupStickyBombsNameText": "Plak-Bommen", + "powerupsSubtitleText": "Natuurlijk, geen spel is compleet zonder power-ups:", + "powerupsText": "power-ups", + "punchInfoText": "- Slaan -\nSlaan doet meer schade hoe\nsneller uw vuisten bewegen, dus\nrennen en draaien als een dolle.", + "runInfoText": "- Ren -\nHoud een knop om te rennen. Triggers of schouderknoppen werken goed als u ze hebt. Rennen krijgt\nu sneller op andere plaatsen, maar maakt het moeilijk om te draaien, dus kijk uit voor kliffen.", + "someDaysText": "Sommige dagen heeft u gewoon het gevoel dat u iets moet slaan. Of om iets op te blazen.", + "titleText": "${APP_NAME} Help", + "toGetTheMostText": "Om het meeste uit het spel te halen, heeft u dit nodig:", + "welcomeText": "Welkom bij ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} is aan het navigeren als een baas -", + "importPlaylistCodeInstructionsText": "Gebruik de volgende code om deze afspeellijst ergens anders the importeren:", + "importPlaylistSuccessText": "${TYPE} afspeellijst '${NAME}' geïmporteerd", + "importText": "Importeer", + "importingText": "Aan het importeren...", + "inGameClippedNameText": "zal in-game worden\n\"${NAME}\"", + "installDiskSpaceErrorText": "FOUT: Niet in staat om de installatie te voltooien.\nHet kan zijn dat er gaan ruimte meer is op uw apparaat.\nMaak wat vrij en probeer opnieuw.", + "internal": { + "arrowsToExitListText": "druk ${LEFT} of ${RIGHT} om de lijst te verlaten", + "buttonText": "knop", + "cantKickHostError": "Je kan de host niet eruit schoppen.", + "chatBlockedText": "${NAME} is chat-geblokkeerd voor ${TIME} seconden.", + "connectedToGameText": "Verbonden met '${NAME}'", + "connectedToPartyText": "Deelgenomen aan ${NAME}'s partij!", + "connectingToPartyText": "Verbinden...", + "connectionFailedHostAlreadyInPartyText": "Verbinding mislukt; host is in een andere partij.", + "connectionFailedPartyFullText": "Connectie mislukt; de partij is vol.", + "connectionFailedText": "Verbinding mislukt.", + "connectionFailedVersionMismatchText": "Verbinding mislukt; host gebruikt een andere versie van het spel.\nZorg er voor dat u allebei up-to-date bent en probeer het opnieuw.", + "connectionRejectedText": "Verbinding afgekeurd.", + "controllerConnectedText": "${CONTROLLER} verbonden.", + "controllerDetectedText": "1 controller gedetecteerd", + "controllerDisconnectedText": "${CONTROLLER} verbinding verbroken.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} verbinding verbroken. Probeer opnieuw verbinding te maken alstublieft.", + "controllerForMenusOnlyText": "Deze controller kan niet gebruikt worden op te spelen; alleen voor het navigeren van menu's.", + "controllerReconnectedText": "${CONTROLLER} opnieuw aangesloten.", + "controllersConnectedText": "${COUNT} controllers verbonden.", + "controllersDetectedText": "${COUNT} controllers gedetecteerd.", + "controllersDisconnectedText": "Verbinding ${COUNT} controllers verbroken.", + "corruptFileText": "Verknoeide bestand(en) gedetecteerd. Probeer het opnieuw te installeren, of email ${EMAIL}", + "errorPlayingMusicText": "Fout tijdens het afspelen van: ${MUSIC}", + "errorResettingAchievementsText": "Resetten van uw online prestaties is mislukt; probeer het later nog eens.", + "hasMenuControlText": "${NAME} bestuurd nu het menu.", + "incompatibleNewerVersionHostText": "Host gebruikt een nieuwere versie van de game.\nUpdate naar de laatste versie en probeer opnieuw.", + "incompatibleVersionHostText": "Host gebruikt een andere versie van het spel; kan niet verbinden.\nZorg er voor dat u allebei up-to-date bent en probeer het opnieuw.", + "incompatibleVersionPlayerText": "${NAME} gebruikt een andere versie van het spel en kan niet verbinden.\nZorg er voor dat u allebei up-to-date bent en probeer het opnieuw.", + "invalidAddressErrorText": "Fout: Ongeldig adres.", + "invalidNameErrorText": "Fout: ongeldige naam.", + "invalidPortErrorText": "Fout: ongeldige port.", + "invitationSentText": "Uitnodiging verzonden.", + "invitationsSentText": "${COUNT} uitnodigingen verzonden.", + "joinedPartyInstructionsText": "Iemand doet mee in uw partij.\nGa naar 'Spelen' om een spel te starten.", + "keyboardText": "Toetsenbord", + "kickIdlePlayersKickedText": "${NAME} staat te lang stil en is verwijderd.", + "kickIdlePlayersWarning1Text": "${NAME} staat te lang stil en wordt over ${COUNT} seconden verwijderd.", + "kickIdlePlayersWarning2Text": "(u kunt dit uitschakelen in Instellingen > Geavanceerd)", + "leftGameText": "Je hebt '${NAME}' verlaten.", + "leftPartyText": "${NAME}'s partij verlaten.", + "noMusicFilesInFolderText": "De map bevat geen muziek bestanden.", + "playerJoinedPartyText": "${NAME} neemt deel aan de partij!", + "playerLeftPartyText": "${NAME} verliet de partij.", + "rejectingInviteAlreadyInPartyText": "Uitnodiging verworpen (al in een partij).", + "serverRestartingText": "De server start opnieuw op. Probeer het alsjeblieft nog een keer...", + "serverShuttingDownText": "De server is aan het sluiten...", + "signInErrorText": "Inlog fout", + "signInNoConnectionText": "Niet in staat om in te loggen. (geen internet verbinding?)", + "teamNameText": "Team ${NAME}", + "telnetAccessDeniedText": "FOUT: gebruiker heeft geen telnet toegang verleend.", + "timeOutText": "(tijd op in ${TIME} seconden)", + "touchScreenJoinWarningText": "U doet mee met een aanraakscherm.\nAls dit een vergissing was, druk er mee op 'Menu->Verlaat Spel'.", + "touchScreenText": "TouchScreen", + "trialText": "proefperiode", + "unableToResolveHostText": "Error: kan de host niet resolven.", + "unavailableNoConnectionText": "Dit is momenteel niet beschikbaar (geen internet verbinding?)", + "vrOrientationResetCardboardText": "Hiermee reset je de VR oriëntatie.\nOm het spel te spelen heb je een externe controller nodig.", + "vrOrientationResetText": "VR oriëntatie reset.", + "willTimeOutText": "(totdat u inactief bent)" + }, + "jumpBoldText": "SPRING", + "jumpText": "Spring", + "keepText": "houd", + "keepTheseSettingsText": "Deze instellingen behouden?", + "kickOccurredText": "${NAME} is eruit geschopt.", + "kickQuestionText": "${NAME} er uit schoppen?", + "kickText": "Er uit schoppen", + "kickVoteCantKickAdminsText": "Beheerders kunnen niet worden gekickt.", + "kickVoteCantKickSelfText": "Je kan jezelf niet kicken.", + "kickVoteFailedNotEnoughVotersText": "Te weinig spelers om te stemmen.", + "kickVoteFailedText": "Stemming om er uit te schoppen mislukt.", + "kickVoteStartedText": "Een stemming is begonnen om ${NAME} eruit te schoppen.", + "kickVoteText": "Tem op er uit te Schoppen", + "kickVotingDisabledText": "Kick stemmen is uitgeschakeld.", + "kickWithChatText": "Type ${YES} in de chat voor ja en ${NO} voor nee.", + "killsTallyText": "${COUNT} doden", + "killsText": "Doden", + "kioskWindow": { + "easyText": "Makkelijk", + "epicModeText": "Epische Modus", + "fullMenuText": "Volledig Menu", + "hardText": "Moeilijk", + "mediumText": "Medium", + "singlePlayerExamplesText": "Enkele Speler / Co-op Voorbeelden", + "versusExamplesText": "Tegen Elkaar Voorbeelden" + }, + "languageSetText": "De taal is nu \"${LANGUAGE}\".", + "lapNumberText": "Ronde ${CURRENT}/${TOTAL}", + "lastGamesText": "(laatste ${COUNT} spellen)", + "leaderboardsText": "Klassementen", + "league": { + "allTimeText": "Aller Tijde", + "currentSeasonText": "Huidig Seizoen (${NUMBER})", + "leagueFullText": "${NAME} Competitie", + "leagueRankText": "Competitie Rang", + "leagueText": "Competitie", + "rankInLeagueText": "#${RANK}, ${NAME} Tournooi${SUFFIX}", + "seasonEndedDaysAgoText": "Seizoen eindigde ${NUMBER} dagen geleden.", + "seasonEndsDaysText": "Seizoen eindigt in ${NUMBER} dagen.", + "seasonEndsHoursText": "Seizoen eindigt in ${NUMBER} uur.", + "seasonEndsMinutesText": "Seizoen eindigt in ${NUMBER} minuten.", + "seasonText": "Seizoen ${NUMBER}", + "tournamentLeagueText": "Haal de ${NAME} competitie om aan dit toernooi mee te doen.", + "trophyCountsResetText": "Trofee telling reset in het volgende seizoen." + }, + "levelBestScoresText": "Beste scores voor ${LEVEL}", + "levelBestTimesText": "Beste tijden voor ${LEVEL}", + "levelFastestTimesText": "Snelste tijden in ${LEVEL}", + "levelHighestScoresText": "Hoogste score in ${LEVEL}", + "levelIsLockedText": "${LEVEL} is vergrendeld.", + "levelMustBeCompletedFirstText": "${LEVEL} moet eerst voltooid worden.", + "levelText": "Level ${NUMBER}", + "levelUnlockedText": "Level Ontgrendeld!", + "livesBonusText": "Levens Bonus", + "loadingText": "laden", + "loadingTryAgainText": "Aan het laden; probeer later opnieuw...", + "macControllerSubsystemBothText": "Beide (niet aanbevolen)", + "macControllerSubsystemClassicText": "Klassiek", + "macControllerSubsystemDescriptionText": "(probeer dit te veranderen als je controllers niet werken)", + "macControllerSubsystemMFiNoteText": "Gemaakt-voor-iOS/Mac controller gedetecteerd;\nJe wilt misschien deze instellingen activeren in Instellingen -> Controllers", + "macControllerSubsystemMFiText": "Gemaakt-voor-iOS/Mac", + "macControllerSubsystemTitleText": "Controller Support", + "mainMenu": { + "creditsText": "Credits", + "demoMenuText": "Voorbeeld Menu", + "endGameText": "Beëindig Spel", + "exitGameText": "Verlaat Spel", + "exitToMenuText": "Verlaat naar menu?", + "howToPlayText": "Hoe te Spelen", + "justPlayerText": "(Alleen ${NAME})", + "leaveGameText": "Verlaat Spel", + "leavePartyConfirmText": "Echt de partij verlaten?", + "leavePartyText": "Verlaat Partij", + "leaveText": "Verlaten", + "quitText": "Verlaten", + "resumeText": "Hervatten", + "settingsText": "Instellingen" + }, + "makeItSoText": "Pas Toe", + "mapSelectGetMoreMapsText": "Verkrijg Meer Speelvelden...", + "mapSelectText": "Selecteer...", + "mapSelectTitleText": "${GAME} Gebieden", + "mapText": "Gebied", + "maxConnectionsText": "Max Connecties", + "maxPartySizeText": "Max Partij Grootte", + "maxPlayersText": "Max Spelers", + "mostValuablePlayerText": "Meest Waardevolle Speler", + "mostViolatedPlayerText": "Meest Geschonden Speler", + "mostViolentPlayerText": "Meest Gewelddadige Speler", + "moveText": "Bewegen", + "multiKillText": "${COUNT}-DOOD!!!", + "multiPlayerCountText": "${COUNT} spelers", + "mustInviteFriendsText": "Let op: U moet vrienden uitnodigen in\nhet \"${GATHER}\" paneel of verbind meer\ncontrollers om multiplayer te spelen.", + "nameBetrayedText": "${NAME} verraadde ${VICTIM}.", + "nameDiedText": "${NAME} ging dood.", + "nameKilledText": "${NAME} doodde ${VICTIM}.", + "nameNotEmptyText": "Naam kan niet leeg zijn!", + "nameScoresText": "${NAME} Scoort!", + "nameSuicideKidFriendlyText": "${NAME} had een ongelukje.", + "nameSuicideText": "${NAME} pleegde zelfmoord.", + "nameText": "Naam", + "nativeText": "van apparaat", + "newPersonalBestText": "Nieuw persoonlijk record!", + "newTestBuildAvailableText": "Er is een nieuwere testversie beschikbaar! (${VERSION} build ${BUILD}).\nDownload hier ${ADDRESS}", + "newText": "Nieuw", + "newVersionAvailableText": "Er is een nieuwere versie van ${APP_NAME} beschikbaar! (${VERSION})", + "nextAchievementsText": "Volgende Prestatie:", + "nextLevelText": "Volgend Level", + "noAchievementsRemainingText": "- geen", + "noContinuesText": "(geen voortzettingen)", + "noExternalStorageErrorText": "Geen externe opslag gevonden op dit apparaat", + "noGameCircleText": "Fout: niet aangemeld bij GameCircle", + "noJoinCoopMidwayText": "Je kan niet halverwegen inspringen bij coöperatieve spellen.", + "noProfilesErrorText": "U heeeft geen speler profielen, dus zit u vast aan '${NAME}'.\nGa naar Instellingen->Speler Profielen om een profiel te maken voor uzelf.", + "noScoresYetText": "Nog geen scores.", + "noThanksText": "Nee Bedankt", + "noTournamentsInTestBuildText": "PAS OP: De punten van deze test tournament worden niet meegerekend.", + "noValidMapsErrorText": "Geen geldige gebieden gevonden voor dit speltype.", + "notEnoughPlayersRemainingText": "Niet genoeg spelers over; stop en start een nieuw spel.", + "notEnoughPlayersText": "U heeft minstens ${COUNT} spelers nodig om dit spel te starten!", + "notNowText": "Niet Nu", + "notSignedInErrorText": "Je moet inloggen om dit te doen.", + "notSignedInGooglePlayErrorText": "Je moet ingelogd zijn met Google Play om dit te doen.", + "notSignedInText": "niet ingelogd", + "nothingIsSelectedErrorText": "Er is niks geselecteerd!", + "numberText": "#${NUMBER}", + "offText": "Uit", + "okText": "Ok", + "onText": "Aan", + "onslaughtRespawnText": "${PLAYER} zal respawnen in golf ${WAVE}", + "orText": "${A} of ${B}", + "otherText": "Andere ...", + "outOfText": "(#${RANK} van de ${ALL})", + "ownFlagAtYourBaseWarning": "Uw eigen vlag moet in\nuw basis zijn om te scoren!", + "packageModsEnabledErrorText": "Netwerk-spel is niet toegestaan als lokale-pakket-aanpassingen zijn ingeschakeld (zie Instellingen->Geavanceerd)", + "partyWindow": { + "chatMessageText": "Chat Bericht", + "emptyText": "Uw partij is leeg", + "hostText": "(host)", + "sendText": "Verzenden", + "titleText": "Uw Partij" + }, + "pausedByHostText": "(Gepauzeerd door host)", + "perfectWaveText": "Perfecte Golf!", + "pickUpBoldText": "PAK OP", + "pickUpText": "Pak Op", + "playModes": { + "coopText": "Coöperatief", + "freeForAllText": "Ieder-voor-zich", + "multiTeamText": "Meerdere Teams", + "singlePlayerCoopText": "Single player / Co-op", + "teamsText": "Teams" + }, + "playText": "Speel", + "playWindow": { + "coopText": "Coöperatief", + "freeForAllText": "Ieder-voor-Zich", + "oneToFourPlayersText": "1-4 spelers", + "teamsText": "Teams", + "titleText": "Speel", + "twoToEightPlayersText": "2-8 spelers" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerDelayedJoinText": "${PLAYER} zal aan het begin van de volgende ronde mee spelen.", + "playerInfoText": "Speler Info", + "playerLeftText": "${PLAYER} heeft het spel verlaten.", + "playerLimitReachedText": "Het speler limiet van ${COUNT} is bereikt; geen toetreders toegestaan​​.", + "playerLimitReachedUnlockProText": "Upgrade naar \"${PRO}\" in de winkel om te spelen met meer dan ${COUNT} spelers.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "U kunt uw account profiel niet verwijderen.", + "deleteButtonText": "Profiel\nVerwijderen", + "deleteConfirmText": "Verwijder '${PROFILE}'?", + "editButtonText": "Profiel\nBewerken", + "explanationText": "(Creëer aangepaste namen & uiterlijken voor dit account)", + "newButtonText": "Nieuw\nProfiel", + "titleText": "Speler Profielen" + }, + "playerText": "Speler", + "playlistNoValidGamesErrorText": "Deze speellijst bevat geen geldige vrijgespeelde spellen.", + "playlistNotFoundText": "speellijst niet gevonden", + "playlistsText": "Speellijsten", + "pleaseRateText": "Als u geniet van ${APP_NAME}, zou u dan een moment willen\nnemen om een waardering te geven of recensie te schrijven.\nDit geeft ons nuttige feedback voor toekomstige ontwikkelingen.\n\nBedankt!\n-eric", + "pleaseWaitText": "Even geduld...", + "pluginsDetectedText": "Nieuw contact gedecteerd. Schakel in/configureer in de instellingen.", + "pluginsText": "Contacten", + "practiceText": "Oefenen", + "pressAnyButtonPlayAgainText": "Druk een knop om opnieuw te spelen...", + "pressAnyButtonText": "Druk een knop om verder te gaan...", + "pressAnyButtonToJoinText": "druk een knop om mee te doen...", + "pressAnyKeyButtonPlayAgainText": "Druk een toets/knop om opnieuw te spelen...", + "pressAnyKeyButtonText": "Druk een toets/knop om verder te gaan...", + "pressAnyKeyText": "Druk een toets...", + "pressJumpToFlyText": "** Druk spring herhalend om te vliegen **", + "pressPunchToJoinText": "druk SLAAN om mee te doen...", + "pressToOverrideCharacterText": "druk op ${BUTTONS} om uw karakter te overschrijven", + "pressToSelectProfileText": "druk op ${BUTTONS} om een speler te selecteren", + "pressToSelectTeamText": "druk op ${BUTTONS} om een team te selecteren", + "profileInfoText": "Maak profielen voor jezelf en je vrienden\nom uw namen, karakteres en kleuren aan te passen.", + "promoCodeWindow": { + "codeText": "Code", + "codeTextDescription": "Promo Code", + "enterText": "Invoeren" + }, + "promoSubmitErrorText": "Fout bij het verzenden van code; Controleer je internetverbinding", + "ps3ControllersWindow": { + "macInstructionsText": "Schakel de stroom aan de achterkant van uw PS3, zorg ervoor\nBluetooth is ingeschakeld op uw Mac, verbind dan uw controller\naan uw Mac via een USB-kabel om de twee te koppelen. Van nu af aan, kunt\nu de home-knop van de controller gebruiken om het aan te sluiten op uw Mac\nin beide bedraad (USB) of draadloos (Bluetooth).\n\nOp sommige Macs kunt u worden gevraagd om een wachtwoord bij het koppelen.\nAls dit gebeurt, zie de volgende uitleg of google voor hulp.\n\n\n\n\nPS3 controllers die draadloos verbonden zijn worden weergegeven in de apparaten\nlijst in Systeemvoorkeuren-> Bluetooth. Het kan nodig zijn om ze te verwijderen\nuit die lijst wanneer u ze weer wilt gebruiken met uw PS3.\n\nZorg er ook voor om ze los te koppelen van Bluetooth als u deze niet\ngebruik of de accu's zal blijven leeg lopen.\n\nBluetooth zou 7 aangesloten apparaten aan moeten kunnen,\nal kan de snelheid variëren.", + "ouyaInstructionsText": "Om een PS3 controller met uw OUYA gebruiken, sluit u deze simpelweg aan een USB-kabel\nom deze te koppelen. Door dit te doen kunt u andere controllers los te koppelen, dan\nkunt u de OUYA opnieuw opstarten en de USB-kabel ontkoppel.\n\nVan nu af aan zou het mogelijk moeten zijn om de HOME-knop van de controller te gebruiken\nom hem draadloos te verbinden. Wanneer u klaar bent met spelen, houdt u de knop HOME\ngedurende 10 seconden ingedrukt om de controller uit te schakelen, anders kan het aan blijven\nen verspilt het batterijen.", + "pairingTutorialText": "koppeling instructievideo", + "titleText": "PS3-controllers gebruiken met ${APP_NAME}:" + }, + "publicBetaText": "PUBLIEKE BETA", + "punchBoldText": "SLAAN", + "punchText": "Slaan", + "purchaseForText": "Koop voor ${PRICE}", + "purchaseGameText": "Koop Spel", + "purchasingText": "Aanschaffen...", + "quitGameText": "${APP_NAME} Verlaten?", + "quittingIn5SecondsText": "Sluiten in 5 seconden...", + "randomPlayerNamesText": "DEFAULT_NAMES, Lucas, Finn, Thijs, Sam, Lars, Emma, Mila, Lieke, Lotte, Saar", + "randomText": "Willekeurig", + "rankText": "Rang", + "ratingText": "Waardering", + "reachWave2Text": "Bereik golf 2 om te kwalificeren.", + "readyText": "klaar", + "recentText": "Recent", + "remainingInTrialText": "nog in proefperiode", + "remoteAppInfoShortText": "${APP_NAME} is het leuks met familie en vrienden.\nVerbind één of meer controllers of installeer de\n${REMOTE_APP_NAME} app om je telefoon of tablet\nte gebruiken als controllers.", + "remote_app": { + "app_name": "BombSquad Afstandsbediening", + "app_name_short": "BSAfstandsbediening", + "button_position": "Knop Positie", + "button_size": "Knop Grootte", + "cant_resolve_host": "Kan host niet oplossen.", + "capturing": "Vastleggen...", + "connected": "Verbonden.", + "description": "Gebruik uw telefoon of tablet als controller bij BombSquad.\nTot 8 apparaten kunnen tegelijk verbinding maken voor een epische lokale multiplayer gekkenhuis op een enkele TV of tablet.", + "disconnected": "Verbinding verbroken door server.", + "dpad_fixed": "vast", + "dpad_floating": "zwevend", + "dpad_position": "D-Pad Positie", + "dpad_size": "D-Pad Grootte", + "dpad_type": "D-Pad Type", + "enter_an_address": "Voer een Adres in", + "game_full": "Dit spel is vol of accepteert geen nieuwe verbindingen.", + "game_shut_down": "Het spel is afgesloten.", + "hardware_buttons": "Hardware Knoppen", + "join_by_address": "Meedoen met Adres...", + "lag": "Lag: ${SECONDS} seconden", + "reset": "Herstel naar standaard", + "run1": "Rennen 1", + "run2": "Rennen 2", + "searching": "Zoeken naar BombSquad spellen...", + "searching_caption": "Tik op de naam van het spel om mee te doen.\nZorg er voor dat u op het zelfde wifi netwerk bent als het spel.", + "start": "Begin", + "version_mismatch": "Versie ongelijk.\nZorg er voor dat BombSquad en BombSquad Afstandsbediening\nde nieuwste versie zijn en probeer het opnieuw." + }, + "removeInGameAdsText": "Koop \"${PRO}\" in de winkel om de in-game advertenties te verwijderen.", + "renameText": "Hernoemen", + "replayEndText": "Stop Herhaling", + "replayNameDefaultText": "Herhaling Laatste Spel", + "replayReadErrorText": "Fout bij inlezen herhaling bestand.", + "replayRenameWarningText": "Hernoem \"${REPLAY}\" na een spel om deze te behouden; anders wordt het overschreven.", + "replayVersionErrorText": "Sorry, deze herhaling is opgenomen in een vorige\nversie en kan niet worden afgespeeld", + "replayWatchText": "Bekijk Herhaling", + "replayWriteErrorText": "Fout tijdens wegschrijven herhaling.", + "replaysText": "Herhaling", + "reportPlayerExplanationText": "Gebruik deze email voor het rapporteren van valsspelen, ongepast taalgebruik, of ander slecht gedrag.\nBeschrijf hieronder:", + "reportThisPlayerCheatingText": "Valsspelen", + "reportThisPlayerLanguageText": "Ongepast Taalgebruik", + "reportThisPlayerReasonText": "Wat zou u willen melden?", + "reportThisPlayerText": "Geef deze speler aan", + "requestingText": "Verkrijgen...", + "restartText": "Herstarten", + "retryText": "Probeer opnieuw", + "revertText": "Omkeren", + "runBoldText": "REN", + "runText": "Ren", + "saveText": "Opslaan", + "scanScriptsErrorText": "Fout(en) bij inlezen scripts; zie log voor details.", + "scoreChallengesText": "Score Uitdagingen", + "scoreListUnavailableText": "Score lijst niet beschikbaar.", + "scoreText": "Score", + "scoreUnits": { + "millisecondsText": "Milliseconden", + "pointsText": "Punten", + "secondsText": "Seconden" + }, + "scoreWasText": "(was ${COUNT})", + "selectText": "Selecteren", + "seriesWinLine1PlayerText": "WINT DE", + "seriesWinLine1TeamText": "WINT DE", + "seriesWinLine1Text": "WINT DE", + "seriesWinLine2Text": "SERIE!", + "settingsWindow": { + "accountText": "Profiel", + "advancedText": "Geavanceerd", + "audioText": "Geluid", + "controllersText": "Controllers", + "graphicsText": "Grafisch", + "playerProfilesMovedText": "Opgelet: Speler profielen zijn verhuist naar het Account venster in het hoofd menu.", + "playerProfilesText": "Speler Profielen", + "titleText": "Instellingen" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(een simpele, controller-vriendelijke toetsenbord op het scherm voor tekstbewerking)", + "alwaysUseInternalKeyboardText": "Gebruik altijd Interne Toetsenbord", + "benchmarksText": "Maatstaven en Stress-Testen", + "disableCameraGyroscopeMotionText": "Schakel de Camera Gyroscoop beweging uit", + "disableCameraShakeText": "Schakel schuddende camera uit", + "disableThisNotice": "(U kunt deze melding uitschakelen in de geavanceerde instellingen)", + "enablePackageModsDescriptionText": "(Geeft extra aanpassingsmogelijkheden maar deactiveert net-spel)", + "enablePackageModsText": "Schakel Lokale Pakket Aanpassingen in", + "enterPromoCodeText": "Voer code in", + "forTestingText": "Let op: deze waarden zijn alleen voor testen en zullen verloren gaan als de app gesloten wordt.", + "helpTranslateText": "${APP_NAME}'s niet-Engelse vertalingen is een gezamenlijke\ninspanning van de gemeenschap. Als u daar aan wilt bijdragen of een vertaling\nwilt corrigeren, klik dan op de link hieronder. Bij voorbaat dank!", + "helpTranslateTextScale": 1.0, + "kickIdlePlayersText": "Verstoot Afwezige Spelers", + "kidFriendlyModeText": "Kind Vriendelijke Modus (minder geweld, etc.)", + "languageText": "Taal", + "languageTextScale": 1.0, + "moddingGuideText": "Aanpasgids", + "mustRestartText": "U moet het spel herstarten voordat dit effect heeft.", + "netTestingText": "Netwerk Testen", + "resetText": "Reset", + "showBombTrajectoriesText": "Baan van de bom weergeven", + "showPlayerNamesText": "Toon Namen Spelers", + "showUserModsText": "Toon Aanpassingen Map", + "titleText": "Geavanceerd", + "translationEditorButtonText": "${APP_NAME} Vertalings Bewerker", + "translationFetchErrorText": "vertaalstatus niet beschikbaar", + "translationFetchingStatusText": "vertaalstatus controleren...", + "translationInformMe": "Meld het wanneer mijn taal updates nodig heeft.", + "translationNoUpdateNeededText": "De huidige taal is up to date; woohoo!", + "translationUpdateNeededText": "** de huidige taal heeft updates nodig!! **", + "vrTestingText": "VR Testen" + }, + "shareText": "Deel", + "sharingText": "Aan het delen...", + "showText": "Toon", + "signInForPromoCodeText": "U moet inloggen op een account om de codes van kracht te laten worden.", + "signInWithGameCenterText": "Om een Game Center account te gebruiken,\nlogt u in bij de Game Center app.", + "singleGamePlaylistNameText": "Alleen ${GAME}", + "singlePlayerCountText": "1 speler", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Karakter selectie", + "Chosen One": "De Uitverkorene", + "Epic": "Epische Modus Spellen", + "Epic Race": "Epische Race", + "FlagCatcher": "Verover de Flag", + "Flying": "Vrolijke Gedachten", + "Football": "Rugby", + "ForwardMarch": "Bestorming", + "GrandRomp": "Verovering", + "Hockey": "Hockey", + "Keep Away": "Hou Weg", + "Marching": "Omlopen", + "Menu": "Hoofd Menu", + "Onslaught": "Veldslag", + "Race": "Race", + "Scary": "Koning van de Heuvel", + "Scores": "Score Scherm", + "Survival": "Eliminatie", + "ToTheDeath": "Dood Spel", + "Victory": "Uiteindelijke Score Scherm" + }, + "spaceKeyText": "spatie", + "statsText": "stats", + "storagePermissionAccessText": "Dit vereist toegang tot opslag", + "store": { + "alreadyOwnText": "U bent al de eigenaar van ${NAME}!", + "bombSquadProDescriptionText": "• Verdubbelt prestatie ticket beloningen\n• Verwijdert reclame in het spel\n• Bevat ${COUNT} bonus tickets\n• +${PERCENT}% competitie score bonus\n• Speelt '${INF_ONSLAUGHT}' en\n '${INF_RUNAROUND}' co-op levels vrij", + "bombSquadProFeaturesText": "Functies:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Verwijdert reclames en nare schermpjes binnen het spel\n• Ontgrendeld meer game instellingen\n• Ook inclusief:", + "buyText": "Koop", + "charactersText": "Karakters", + "comingSoonText": "Binnenkort...", + "extrasText": "Extras", + "freeBombSquadProText": "BombSquad is nu gratis, maar sinds u het spel origineel gekocht heeft\nkrijgt u de BombSquad Pro opwaardering en ${COUNT} tickets als dank.\nVeel plezier met de nieuwe functies, en bedankt voor uw ondersteuning!\n-Eric", + "gameUpgradesText": "Spel Opwaarderingen", + "getCoinsText": "Munten Aanschaffen", + "holidaySpecialText": "Speciale aanbieding", + "howToSwitchCharactersText": "(ga naar \"${SETTINGS} -> ${PLAYER_PROFILES}\" om karakters toe te wijzen & aan te passen)", + "howToUseIconsText": "(Creëer globale speler profielen (in het account venster) om deze te gebruiken)", + "howToUseMapsText": "(gebruik deze speelvelden in je eigen team/ieder-voor-zich speellijsten)", + "iconsText": "Pictogrammen", + "loadErrorText": "Kon de pagina niet laden.\nControleer uw internet verbinding.", + "loadingText": "laden", + "mapsText": "Speelvelden", + "miniGamesText": "MiniSpellen", + "oneTimeOnlyText": "(eenmalige aanbieding)", + "purchaseAlreadyInProgressText": "Een aankoop dat dit artikel wordt al verwerkt.", + "purchaseConfirmText": "Koop ${ITEM}?", + "purchaseNotValidError": "Aankoop niet geldig.\nContacteer ${EMAIL} als dit een fout is.", + "purchaseText": "Koop", + "saleBundleText": "Bundel Uitverkoop!", + "saleExclaimText": "Aanbieding!", + "salePercentText": "(${PERCENT}% korting)", + "saleText": "KORTING", + "searchText": "Zoeken", + "teamsFreeForAllGamesText": "Teams / Ieder-voor-Zich Spellen", + "totalWorthText": "*** Ter waarde van ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Opwaarderen?", + "winterSpecialText": "Winter Aanbieding", + "youOwnThisText": "- u bezit dit -" + }, + "storeDescriptionText": "8 Speler Feest Spel Krankzinnigheid!\n\nBlaas uw vrienden op (of de computer) in een toernooi van explosieve mini-spellen zoals Verover-de-Vlag, Bommen-Hockey, en Epische-Slow-Motion-Dood-Spel!\n\nSimpele besturing en uitgebreide controller ondersteuning maakt het makkelijk om tot met 8 spelers in actie te zijn; u kunt zelfs uw iPhones gebruiken als controllers met de gratis 'BombSquad Remote' app!\n\nBombardeer er op los!\n\nKijk op www.froemling.net/Bombsquad voor meer info.", + "storeDescriptions": { + "blowUpYourFriendsText": "Blaas uw vrienden op.", + "competeInMiniGamesText": "Concurreer in mini-spellen van racen tot vliegen.", + "customize2Text": "Pas uw karakter aan, mini-spellen, en zelfs uw eigen muziek.", + "customizeText": "Pas karakters aan en maak uw eigen mini-spellen lijst.", + "sportsMoreFunText": "Sporten zijn leuker met explosieven.", + "teamUpAgainstComputerText": "Vorm een team tegen de computer." + }, + "storeText": "Winkel", + "submitText": "Voorleggen", + "submittingPromoCodeText": "Code verzenden ...", + "teamNamesColorText": "Teamnamen / kleuren ...", + "teamsText": "Teams", + "telnetAccessGrantedText": "Telnet toegang ingeschakeld.", + "telnetAccessText": "Telnet toegang gedetecteerd; toestaan?", + "testBuildErrorText": "Deze test versie is niet meer actief; gelieve controleer voor een nieuwe versie.", + "testBuildText": "Test Versie", + "testBuildValidateErrorText": "Niet in staat om de test versie te valideren. (geen net verbinding?)", + "testBuildValidatedText": "Test Versie Gevalideerd; Veel Plezier!", + "thankYouText": "Bedankt voor uw ondersteuning! Veel plezier met het spel!!", + "threeKillText": "DRIEDUBBELE DOOD!!", + "timeBonusText": "Tijd Bonus", + "timeElapsedText": "Tijd Verstreken", + "timeExpiredText": "Tijd Verstreken", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}u", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Tip", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Top Vrienden", + "tournamentCheckingStateText": "Toernooi status nakijken; even geduld aub...", + "tournamentEndedText": "Dit toernooi is beëindigd. Er zal snel een nieuwe beginnen.", + "tournamentEntryText": "Toernooi Inschrijving", + "tournamentResultsRecentText": "Recente Toernooi Uitslagen", + "tournamentStandingsText": "Toernooi Stand", + "tournamentText": "Toernooi", + "tournamentTimeExpiredText": "Toernooi Tijd Verlopen", + "tournamentsText": "Toernooien", + "translations": { + "characterNames": { + "Agent Johnson": "Agent Johnson", + "B-9000": "B-9000", + "Bernard": "Bernard", + "Bones": "Bones", + "Butch": "Butch", + "Easter Bunny": "Paashaas", + "Flopsy": "Flopsy", + "Frosty": "Frosty", + "Gretel": "Gretel", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Lucky", + "Mel": "Mel", + "Middle-Man": "Middle-Man", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Kerstman", + "Snake Shadow": "Snake Shadow", + "Spaz": "Spaz", + "Taobao Mascot": "Taobao Mascot", + "Todd": "Todd", + "Todd McBurton": "Todd McBurton", + "Xara": "Xara", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Oneindige\nAfslachting", + "Infinite\nRunaround": "Oneindig\nOmlopen", + "Onslaught\nTraining": "Afslacht\nTraining", + "Pro\nFootball": "Pro\nRugby", + "Pro\nOnslaught": "Pro\nAfslachting", + "Pro\nRunaround": "Pro\nOmlopen", + "Rookie\nFootball": "Groentjes\nRugby", + "Rookie\nOnslaught": "Groentjes\nAfslachting", + "The\nLast Stand": "De\nLaatste Stand", + "Uber\nFootball": "Uber\nRugby", + "Uber\nOnslaught": "Uber\nAfslachting", + "Uber\nRunaround": "Uber\nOmlopen" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Training", + "Infinite ${GAME}": "Oneindig ${GAME}", + "Infinite Onslaught": "Oneindige Afslachting", + "Infinite Runaround": "Oneindig Omlopen", + "Onslaught": "Oneindige Afslachting", + "Onslaught Training": "Afslacht Training", + "Pro ${GAME}": "Pro ${GAME}", + "Pro Football": "Pro Rugby", + "Pro Onslaught": "Pro Afslachting", + "Pro Runaround": "Pro Omlopen", + "Rookie ${GAME}": "Amateur ${GAME}", + "Rookie Football": "Groentjes Rugby", + "Rookie Onslaught": "Groentjes Afslachting", + "Runaround": "Oneindig Omlopen", + "The Last Stand": "De Laatste Stand", + "Uber ${GAME}": "Extreem ${GAME}", + "Uber Football": "Uber Rugby", + "Uber Onslaught": "Uber Afslachting", + "Uber Runaround": "Uber Omlopen" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Ben de uitverkorene voor een bepaalde tijd om te winnen.\nDood de uitverkorene om het te worden.", + "Bomb as many targets as you can.": "Bombardeer zo veel mogelijk doelwitten als u kan.", + "Carry the flag for ${ARG1} seconds.": "Draag de vlag voor ${ARG1} seconden.", + "Carry the flag for a set length of time.": "Draag de vlag voor een bepaalde tijd.", + "Crush ${ARG1} of your enemies.": "Verpletter ${ARG1} van uw vijanden.", + "Defeat all enemies.": "Versla alle vijanden.", + "Dodge the falling bombs.": "Ontwijk de vallende bommen.", + "Final glorious epic slow motion battle to the death.": "Uiteindelijke glorieuze epische slow motion gevecht tot de dood.", + "Gather eggs!": "Verzamel eieren!", + "Get the flag to the enemy end zone.": "Breng de vlag naar de vijandelijke eind zone.", + "How fast can you defeat the ninjas?": "Hoe snel kan u de ninjas verslaan?", + "Kill a set number of enemies to win.": "Dood een bepaald aantal vijanden om te winnen.", + "Last one standing wins.": "De laatste die overblijft wint.", + "Last remaining alive wins.": "De laatst overgebleven levende wint.", + "Last team standing wins.": "Het laatste team dat over blijft wint.", + "Prevent enemies from reaching the exit.": "Voorkom de vijanden de uitgang bereiken.", + "Reach the enemy flag to score.": "Bereik de vijandelijke vlag om te scoren.", + "Return the enemy flag to score.": "Breng de vijandelijke vlag terug om te scoren.", + "Run ${ARG1} laps.": "Ren ${ARG1} rondes.", + "Run ${ARG1} laps. Your entire team has to finish.": "Ren ${ARG1} rondes. Uw hele team moet finishen.", + "Run 1 lap.": "Ren 1 ronde.", + "Run 1 lap. Your entire team has to finish.": "Ren 1 rond. Uw hele team moet finishen.", + "Run real fast!": "Ren heel snel!", + "Score ${ARG1} goals.": "Score ${ARG1} doelpunten.", + "Score ${ARG1} touchdowns.": "Score ${ARG1} touchdowns.", + "Score a goal": "Score een doelpunt", + "Score a goal.": "Score een doelpunt.", + "Score a touchdown.": "Score een touchdown.", + "Score some goals.": "Score wat doelpunten.", + "Secure all ${ARG1} flags.": "Beveilig alle ${ARG1} de vlaggen.", + "Secure all flags on the map to win.": "Beveilig alle vlaggen op de kaart om te winnen.", + "Secure the flag for ${ARG1} seconds.": "Beveilig de vlag voor ${ARG1} seconden.", + "Secure the flag for a set length of time.": "Beveilig de vlag voor een bepaalde tijd.", + "Steal the enemy flag ${ARG1} times.": "Steel de vijandelijke vlag ${ARG1} keer.", + "Steal the enemy flag.": "Steel de vijandelijke vlag.", + "There can be only one.": "Er kan er maar een zijn.", + "Touch the enemy flag ${ARG1} times.": "Raak de vijandelijke vlag ${ARG1} keer aan.", + "Touch the enemy flag.": "Raak de vijandelijke vlag aan.", + "carry the flag for ${ARG1} seconds": "draag de vlag voor ${ARG1} seconden", + "kill ${ARG1} enemies": "dood ${ARG1} vijanden", + "last one standing wins": "de laatste die overblijft wint", + "last team standing wins": "het team dat als laatste overblijft wint", + "return ${ARG1} flags": "breng ${ARG1} vlaggen terug", + "return 1 flag": "breng 1 vlag terug", + "run ${ARG1} laps": "ren ${ARG1} rondes", + "run 1 lap": "ren 1 ronde", + "score ${ARG1} goals": "score ${ARG1} doelpunten", + "score ${ARG1} touchdowns": "score ${ARG1} touchdowns", + "score a goal": "score een doelpunt", + "score a touchdown": "score een touchdown", + "secure all ${ARG1} flags": "beveilig alle ${ARG1} de vlaggen", + "secure the flag for ${ARG1} seconds": "beveilig de vlag voor ${ARG1} seconden", + "touch ${ARG1} flags": "raak ${ARG1} vlaggen aan", + "touch 1 flag": "raak 1 vlag aan" + }, + "gameNames": { + "Assault": "Bestorming", + "Capture the Flag": "Verover de Vlag", + "Chosen One": "De Uitverkorene", + "Conquest": "Verovering", + "Death Match": "Dood Spel", + "Easter Egg Hunt": "Paaseieren Jacht", + "Elimination": "Eliminatie", + "Football": "Rugby", + "Hockey": "Hockey", + "Keep Away": "Hou Weg", + "King of the Hill": "Koning van de Heuvel", + "Meteor Shower": "Meteoren Regen", + "Ninja Fight": "Ninja Gevecht", + "Onslaught": "Afslachting", + "Race": "Race", + "Runaround": "Omlopen", + "Target Practice": "Bombardeer Oefening", + "The Last Stand": "De Laatste Stand" + }, + "inputDeviceNames": { + "Keyboard": "Keyboard", + "Keyboard P2": "Keyboard P2" + }, + "languages": { + "Arabic": "Arabisch", + "Belarussian": "Belarusian", + "Chinese": "Vereenvoudigd Chinees", + "ChineseTraditional": "Traditioneel Chinees", + "Croatian": "Kroatisch", + "Czech": "Tsjechisch", + "Danish": "Deens", + "Dutch": "Nederlands", + "English": "Engels", + "Esperanto": "Esperanto", + "Finnish": "Fins", + "French": "Frans", + "German": "Duits", + "Gibberish": "Brabbeltaal", + "Greek": "Grieks", + "Hindi": "Hindies", + "Hungarian": "Hongaars", + "Indonesian": "Indonesisch", + "Italian": "Italiaans", + "Japanese": "Japans", + "Korean": "Koreaans", + "Persian": "Perzisch", + "Polish": "Pools", + "Portuguese": "Portugees", + "Romanian": "Roemeens", + "Russian": "Russisch", + "Serbian": "Servisch", + "Slovak": "Sloveens", + "Spanish": "Spaans", + "Swedish": "Zweeds", + "Turkish": "Turks", + "Ukrainian": "Oekraïens", + "Vietnamese": "Vietnamees" + }, + "leagueNames": { + "Bronze": "Bronzen", + "Diamond": "Diamand", + "Gold": "Goud", + "Silver": "Zilver" + }, + "mapsNames": { + "Big G": "Grote G", + "Bridgit": "Brugut", + "Courtyard": "Binnenplaats", + "Crag Castle": "Rots Kasteel", + "Doom Shroom": "Paddenstoel des Onheils", + "Football Stadium": "Rugby Stadion", + "Happy Thoughts": "Vrolijke Gedachten", + "Hockey Stadium": "Hockey Stadion", + "Lake Frigid": "Het Frigid Meer", + "Monkey Face": "Apen Gezicht", + "Rampage": "Rampage", + "Roundabout": "Rotonde", + "Step Right Up": "Step Right Up", + "The Pad": "Het Blok", + "Tip Top": "Tip Top", + "Tower D": "Toren D", + "Zigzag": "Zigzag" + }, + "playlistNames": { + "Just Epic": "Alleen Episch", + "Just Sports": "Alleen Sporten" + }, + "promoCodeResponses": { + "invalid promo code": "ongeldige promotie code" + }, + "scoreNames": { + "Flags": "Vlaggen", + "Goals": "Doelpunten", + "Score": "Score", + "Survived": "Overleefd", + "Time": "Tijd", + "Time Held": "Tijd Gehouden" + }, + "serverResponses": { + "A code has already been used on this account.": "Een code is al gebruikt op dit account.", + "A reward has already been given for that address.": "Een beloning is al gegeven voor dat adres.", + "Account linking successful!": "Account succesvol gelinkt!", + "Account unlinking successful!": "Account verbinding succesvol verbroken!", + "Accounts are already linked.": "Accounts zijn al gelinkt.", + "An error has occurred; (${ERROR})": "er is een fout opgetreden; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "er is een fout opgetreden, neem contact op met de ondersteuning. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Er is een fout opgetreden; neem alstublieft contact op met support@froemling.net.", + "An error has occurred; please try again later.": "Er is een fout opgetreden; Probeer het later opnieuw.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Bent u zeker dat u deze accounts wilt koppelen?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nDit kan niet ongedaan gemaakt worden!", + "BombSquad Pro unlocked!": "BombSquad Pro ontgrendeld!", + "Can't link 2 accounts of this type.": "Kan geen 2 accounts van dit type linken.", + "Can't link 2 diamond league accounts.": "Kan geen 2 diamanten competitie accounts linken.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Kan niet linken; dit zou het maximum van ${COUNT} gelinkte accounts overschrijden.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Valsspelen ontdekt; scores en prijzen geschorst voor ${COUNT} dagen.", + "Could not establish a secure connection.": "Kon geen beveiligde verbinding tot stand brengen.", + "Daily maximum reached.": "Dagelijkse maximum bekijkt.", + "Entering tournament...": "Toernooi betreden...", + "Invalid code.": "Ongeldige code.", + "Invalid payment; purchase canceled.": "Ongeldige betaling; aankoop geannuleerd.", + "Invalid promo code.": "Ongeldige promo code.", + "Invalid purchase.": "Ongeldige aankoop.", + "Invalid tournament entry; score will be ignored.": "Ongeldige toernooi; score wordt genegeerd.", + "Item unlocked!": "item ontgrendeld!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "LINKENDE ONTKENNING. ${ACCOUNT} bevat\nsignificante gegevens die ALLES ZIJN VERLOREN.\nJe kunt in omgekeerde volgorde linken als je wilt\n(en verlies de gegevens van DIT account in plaats daarvan)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Verbind account ${ACCOUNT} aan dit account?\nAlle bestaande data op ${ACCOUNT} zal verloren gaan.\nDit kan niet ongedaan worden gemaakt. Weet je het zeker?", + "Max number of playlists reached.": "Maximaal aantal speellijsten bereikt.", + "Max number of profiles reached.": "Maximaal aantal profielen bereikt.", + "Maximum friend code rewards reached.": "Maximum vriend code beloning behaald", + "Message is too long.": "Bericht is te lang.", + "Profile \"${NAME}\" upgraded successfully.": "Profiel \"${NAME}\" is succesvol opgewaardeerd.", + "Profile could not be upgraded.": "Profiel kon niet worden opgewaardeerd.", + "Purchase successful!": "Aankoop succesvol!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "${COUNT} tickets ontvangen door in te loggen.\nKom morgen terug om ${TOMORROW_COUNT} te ontvangen.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Serverfunctionaliteit wordt niet langer ondersteund in deze versie van het spel;\nWerk bij naar een nieuwere versie.", + "Sorry, there are no uses remaining on this code.": "Sorry, deze code is al te veel gebruikt.", + "Sorry, this code has already been used.": "Sorry, deze code is al gebruikt.", + "Sorry, this code has expired.": "Sorry, deze code is verlopen.", + "Sorry, this code only works for new accounts.": "Sorry, deze code werkt alleen op nieuwe accounts.", + "Temporarily unavailable; please try again later.": "Tijdelijk niet beschikbaar; probeer het later opnieuw.", + "The tournament ended before you finished.": "Het toernooi eindigde voordat u klaar was.", + "This account cannot be unlinked for ${NUM} days.": "Dit account kan gedurende ${NUM} dagen niet worden ontkoppeld.", + "This code cannot be used on the account that created it.": "Deze code kan niet gebruikt worden op het account die het gemaakt heeft.", + "This requires version ${VERSION} or newer.": "Dit vereist versie ${VERSION} of nieuwer.", + "Tournaments disabled due to rooted device.": "Toernooien uitgeschakeld vanwege geworteld apparaat", + "Tournaments require ${VERSION} or newer": "toernooi vereist ${VERSION} of nieuwer", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Koppel ${ACCOUNT} los van dit account?\nAlle gegevens over ${ACCOUNT} worden opnieuw ingesteld.\n(behalve voor prestaties in sommige gevallen)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "WAARSCHUWING: klachten over hacking zijn afgegeven tegen uw account.\nAccounts die hacken blijken te zijn, worden verbannen. Speel alsjeblieft eerlijk.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Wilt u uw apparaat account hieraan koppelen?\n\nUw apparaat account is ${ACCOUNT1}\nDit account is ${ACCOUNT2}\n\nDit zal u toelaten om uw bestaande vooruitgang te houden.\nWaarschuwing: dit kan niet ongedaan gemaakt worden!", + "You already own this!": "U bezit dit al!", + "You can join in ${COUNT} seconds.": "Je kan meedoen in ${COUNT} seconden.", + "You don't have enough tickets for this!": "U heeft hier niet genoeg tickets voor!", + "You don't own that.": "Je bezit dat niet.", + "You got ${COUNT} tickets!": "U krijgt ${COUNT} tickets!", + "You got a ${ITEM}!": "U hebt een ${ITEM}!", + "You have been promoted to a new league; congratulations!": "U bent gepromoveerd naar een nieuwe competitie; gefeliciteerd!", + "You must update to a newer version of the app to do this.": "Om dit te kunnen doen moet u de app updaten naar een nieuwere versie.", + "You must update to the newest version of the game to do this.": "Je moet dit bijwerken naar de nieuwste versie van het spel.", + "You must wait a few seconds before entering a new code.": "U moet een paar seconden wachten voordat een nieuwe code ingevoerd kan worden.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "U eindigde op rang #${RANK} in het laatste toernooi. Bedankt voor het spelen!", + "Your account was rejected. Are you signed in?": "Je account is geweigerd. Ben je ingelogd?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Uw versie van het spel is aangepast.\nMaakt de veranderingen ongedaan en probeer het opnieuw.", + "Your friend code was used by ${ACCOUNT}": "Uw Vriend code is gebruikt door ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Minuut", + "1 Second": "1 Seconde", + "10 Minutes": "10 Minuten", + "2 Minutes": "2 Minuten", + "2 Seconds": "2 Seconden", + "20 Minutes": "20 Minuten", + "4 Seconds": "4 Seconden", + "5 Minutes": "5 Minuten", + "8 Seconds": "8 Seconden", + "Allow Negative Scores": "Negatieve Score Toelaten", + "Balance Total Lives": "Balanseer Totaal Aantal Levens", + "Bomb Spawning": "Bom Spawning", + "Chosen One Gets Gloves": "De Uitverkorene Krijgt Handschoenen", + "Chosen One Gets Shield": "De Uitverkorene Krijgt Een Schild", + "Chosen One Time": "Uitverkorene Tijd", + "Enable Impact Bombs": "Activeer Inslag Bommen", + "Enable Triple Bombs": "Activeer Drievoudige Bommen", + "Entire Team Must Finish": "Volledige Team Moet Voltooien", + "Epic Mode": "Epische Modus", + "Flag Idle Return Time": "Stilligende Vlag Terugkeer Tijd", + "Flag Touch Return Time": "Vlag Aanraken Terugkeer Tijd", + "Hold Time": "Vasthou Tijd", + "Kills to Win Per Player": "Doden om te Winnen Per Speler", + "Laps": "Rondes", + "Lives Per Player": "Levens Per Speler", + "Long": "Lang", + "Longer": "Langer", + "Mine Spawning": "Mijn Spawning", + "No Mines": "Geen Mijnen", + "None": "Geen", + "Normal": "Normaal", + "Pro Mode": "Pro Modus", + "Respawn Times": "Respawn Tijd", + "Score to Win": "Score om te Winnen", + "Short": "Kort", + "Shorter": "Korter", + "Solo Mode": "Individuele Modus", + "Target Count": "Doelwit Aantal", + "Time Limit": "Tijd Limiet" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} is gediskwalificeerd omdat ${PLAYER} heeft verlaten", + "Killing ${NAME} for skipping part of the track!": "${NAME} gedood door overslaan van de baan!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "waarschuwing voor ${NAME}: turbo / button spammen maakt je knock-out" + }, + "teamNames": { + "Bad Guys": "Slechteriken", + "Blue": "Blauw", + "Good Guys": "Goede Kerels", + "Red": "Rood" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Een perfect getimede ren-spring-draai-slag kan doden in een enkele slag\nen verdien uw levenslange respect van uw vrienden.", + "Always remember to floss.": "Vergeet niet te flossen.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Creëer speler profielen voor uzelf en uw vrienden met\nuw namen en uiterlijk naar voorkeur in plaatsen van de willekeurige.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Vloek dozen veranderen u in een tikkende tijd bom.\nDe enige behandeling is om snel een gezondheid-doos te pakken.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Ondanks hun uiterlijk, de vaardigheden van alle personages zijn identiek,\nu kunt dus gewoon pakken welke u het meeste aantrekt.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Word niet te verwaand met een energie schild; u kunt nog steeds van een klif af gegooid worden.", + "Don't run all the time. Really. You will fall off cliffs.": "Ren niet de hele tijd. Echt waar. U zal van kliffen af vallen.", + "Don't spin for too long; you'll become dizzy and fall.": "Draai niet te lang rond; je zal duizelig worden en vallen.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Houd een knop in om te rennen. (Trigger knoppen werken goed als u die heeft)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Houd een willekeurige knop in om te rennen. Je beweegt hierdoor sneller\nmaar zal niet erg goed kunnen draaien, dus kijk uit voor afgronden.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ijs bommen zijn niet erg krachtig, maar ze bevriezen\nwie ze ook raken, waardoor ze kwetsbaar zijn voor verbrijzeling.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Als iemand u op pakt, sla dan en ze laten los.\nDit werkt ook in het echte leven.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Als je controllers te kort komt, installeer dan de '${REMOTE_APP_NAME}' app\nop je mobiel of tablet om deze als controller te gebruiken.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Als u een tekort hebt aan controller, installeer dan de 'BombSquad Remote' app\nop uw iOS of Android apparaten om ze als controllers te gebruiken.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Als er een plak-bom aan u vast zit, spring rond en draai rondjes. U zou\nde bom nog van u af kunnen schudden, of als het niet werkt zijn uw laatste momenten vermakelijk.", + "If you kill an enemy in one hit you get double points for it.": "Als u een vijand doodt in een slag krijgt u er dubbele punten voor.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Als u een vloek op pakt, is uw enige kans op overleven om\neen gezondheids powerup te vinden in de komende paar seconden.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Als u op een plaats blijft, bent u toast. Ren en ontwijk om te overleven..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Als u veel spelers hebt die komen en gaan, zet dan 'verstoot-afwezige spelers' aan\nin instellingen voor het geval dat iemand vergeet het spel te verlaten.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Als uw apparaat te warm wordt of je als je bettarij wilt besparen,\nzet \"Visueel\" of \"Resolutie\" omlaag bij Instellingen->Grafisch", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Als uw framerate schokkerig is, probeer dan de resolutie of visuele instellingen\nomlaag te zetten bij het spel zijn grafische instellingen.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "In Verover-de-Vlag, moet uw eigen vlag in uw basis zijn om te scoren, als het andere\nteam op het punt staat om te scoren, is het stelen van hun vlag een goede manier om ze te stoppen.", + "In hockey, you'll maintain more speed if you turn gradually.": "In hockey, behoud u meer snelheid als u rustig draait.", + "It's easier to win with a friend or two helping.": "Het is makkelijker om te winnen als een vriend of twee helpen.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Spring precies als u een bom gooit om de bommen op het hoogst mogelijke te krijgen.", + "Land-mines are a good way to stop speedy enemies.": "Land-mijnen zijn een goede manier om snelle vijanden te stoppen.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Veel dingen kunnen opgepakt worden en gegooid, inclusief andere spelers. Vijanden\nvan kliffen af gooien kan een effectieve en emotioneel vervullende strategie zijn.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nee, u kunt niet de richel omhoog klimmen. U moet bommen gooien.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Spelers kunnen meedoen en stoppen midden in de meeste spellen.\nU kunt ook controllers tijdens het spelen aansluiten of ontkoppelen.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Spelers kunnen meedoen en weggaan midden in de meeste spellen,\nje kan ook gamepads in-en uitpluggen tijdens de spellen.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Powerups hebben alleen een tijd limiet in coöperatieve spellen.\nIn teams en ieder-voor-zich zijn ze van jou totdat je dood gaat.", + "Practice using your momentum to throw bombs more accurately.": "Oefen met het gebruiken van uw momentum om uw bommen accurater te gooien.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Uw klappen doen meer schade als uw vuisten in beweging zijn,\ndus probeer te rennen, springen en te draaien als een gek.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Ren heen en weer voordat u een bom gooit\nom deze 'op te zwiepen' en verder te gooien.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Schakel hele groepen vijanden uit door een bom\nte laten ontploffen naast een TNT doos.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Het hoofd is de meest zwakke plek. Dus een plak-bom\nnaar het koppie betekend meestal 'game over'.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Aan dit level komt geen einde, maar haalt u een 'high score'\ndan wacht u eeuwige respect van heel de wereld.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Werp kracht is gebaseerd op de richting die u stuurt.\nStuur in geen enkele richting om iets vlak voor uw voeten te gooien.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Verveeld van de muziek? Vervang het met uw eigen!\nZie Instellingen->Geluid->Muziek", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Zet uw bommen op scherp door ze eerst een seconde of twee vast te houden voordat u gooit.", + "Try tricking enemies into killing eachother or running off cliffs.": "Gebruik schijnbewegingen om uw tegenstanders elkaar op te laten blazen of van de randen te laten lopen.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Gebruik de 'op pak knop' om de vlag te grijpen < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Zwiep uzelf heen en weer om verder te kunnen gooien..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "U kan uw klappen 'mikken' door linksom of rechtsom te tollen.\nDit is handig om slechteriken over de rand te duwen of tijdens hockey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "U kunt beoordelen of een bom gaat ontploffen door goed naar\nde vonken van de lont te kijken: geel..oranje..rood..BOEM!", + "You can throw bombs higher if you jump just before throwing.": "U kunt bommen hoger gooien als u springt vlak voordat u de bom gooit.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Je bent niet verplicht om je profiel aan te passen om het uiterlijk van je\nkarakter te veranderen; Druk op de 'op pak knop' tijdens de karakter keuze\nom tijdelijk je standaard karakter te veranderen.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "U loopt schade op als u uw hoofd tegen voorwerpen knalt,\ndus probeer uw hoofd niet tegen voorwerpen te knallen.", + "Your punches do much more damage if you are running or spinning.": "Uw klappen doen meer schade als u rent of om uw as tolt." + } + }, + "trialPeriodEndedText": "De proeftijd is voorbij. Wil je BombSquad\naanschaffen om door te kunnen spelen?", + "trophiesRequiredText": "Dit vereist op z'n minst ${NUMBER} trofeeën.", + "trophiesText": "Trofeeën", + "trophiesThisSeasonText": "Trofeeën van dit seizoen", + "tutorial": { + "cpuBenchmarkText": "Uitleg uitvoeren op belachelijke snelheid (test voornamelijk de CPU-snelheid)", + "phrase01Text": "Hallo daar!", + "phrase02Text": "Welkom bij ${APP_NAME}!", + "phrase03Text": "Hier volgen een paar tips voor het besturen van uw karakter:", + "phrase04Text": "Veel dingen in ${APP_NAME} zijn gebaseerd op NATUURKUNDE.", + "phrase05Text": "Als u bijvoorbeeld ergens tegen duwt,..", + "phrase06Text": "..schade is afhankelijk van de snelheid van uw vuist.", + "phrase07Text": "Ziet u wel? We bewogen niet dus we deden ${NAME} amper pijn.", + "phrase08Text": "Laten we nu springen en draaien om meer snelheid te krijgen.", + "phrase09Text": "Ah, dat is beter.", + "phrase10Text": "Rennen helpt ook.", + "phrase11Text": "Hou een willekeurige knop ingedrukt om te rennen.", + "phrase12Text": "Voor extra-geweldige klappen, probeer te rennen EN te draaien.", + "phrase13Text": "Oeps; mijn excuses ${NAME}.", + "phrase14Text": "U kunt dingen oppakken en weggooien zoals vlaggen.. of ${NAME}.", + "phrase15Text": "Als laatste, zijn er ook bommen.", + "phrase16Text": "Bommen gooien vereist oefening.", + "phrase17Text": "Au! Niet zo'n beste worp.", + "phrase18Text": "Als u beweegt gooit u verder.", + "phrase19Text": "Met springen kunt u hoger gooien.", + "phrase20Text": "\"Whiplash\" uw bommen om nog verder te gooien.", + "phrase21Text": "Het juist timen van uw bommen kan lastig zijn.", + "phrase22Text": "Verdraaid.", + "phrase23Text": "Probeer de lont te \"korten\" door een paar seconden te wachten.", + "phrase24Text": "Hoera! Goed gekort.", + "phrase25Text": "Nou, dat is het wel zo'n beetje.", + "phrase26Text": "Neem ze te grazen tijger!", + "phrase27Text": "Denk aan uw training en u ZAL weer tot leven komen!", + "phrase28Text": "...nou ja, misschien...", + "phrase29Text": "Veel succes!", + "randomName1Text": "Fred", + "randomName2Text": "Harry", + "randomName3Text": "Bill", + "randomName4Text": "Chuck", + "randomName5Text": "Phil", + "skipConfirmText": "Wilt u echt de uitleg overslaan? Tik of druk om te bevestigen.", + "skipVoteCountText": "${COUNT}/${TOTAL} overslaan stemmen", + "skippingText": "les wordt overgeslagen...", + "toSkipPressAnythingText": "(tik of druk op iets om de les over te slaan)" + }, + "twoKillText": "TWEE VLIEGEN IN 1 KLAP!", + "unavailableText": "niet beschikbaar", + "unconfiguredControllerDetectedText": "Niet geconfigureerde controller ontdekt:", + "unlockThisInTheStoreText": "Hiervoor moet een aankoop gedaan worden in de winkel.", + "unlockThisProfilesText": "Om meer dan ${NUM} profielen te maken heb je nodig:", + "unlockThisText": "Om dit te ontgrendelen, moet u:", + "unsupportedHardwareText": "Sorry, deze hardware wordt niet ondersteunt door deze versie van het spel.", + "upFirstText": "Als eerste:", + "upNextText": "Daarna volgt in potje ${COUNT}:", + "updatingAccountText": "Bijwerken profiel...", + "upgradeText": "Opwaarderen", + "upgradeToPlayText": "Ontgrendel \"${PRO}\" in de winkel binnen het spel om dit te spelen.", + "useDefaultText": "Gebruik Standaard", + "usesExternalControllerText": "Dit spel maakt gebruik van een externe controller als input.", + "usingItunesText": "De muziek app wordt gebruikt voor de muziek...", + "usingItunesTurnRepeatAndShuffleOnText": "Zorg er er voor dat shuffle AAN staat en herhalen op ALLES staat in iTunes.", + "validatingBetaText": "Beta wordt Gevalideerd...", + "validatingTestBuildText": "Valideren Test Versie...", + "victoryText": "Overwinning!", + "voteDelayText": "Je kan geen andere stemming starten voor ${NUMBER} seconden", + "voteInProgressText": "Er is al een stemming bezig.", + "votedAlreadyText": "Je hebt al gestemd", + "votesNeededText": "${NUMBER} stemmen nodig", + "vsText": "tegen", + "waitingForHostText": "(wachten op ${HOST} om verder te gaan)", + "waitingForLocalPlayersText": "Wachten op lokale spelers...", + "waitingForPlayersText": "wachten op spelers om mee te doen...", + "waitingInLineText": "Wachten in de rij (partij is vol)...", + "watchAVideoText": "Bekijk een video", + "watchAnAdText": "Bekijk een Advertentie", + "watchWindow": { + "deleteConfirmText": "\"${REPLAY}\" Verwijderen?", + "deleteReplayButtonText": "Verwijder\nHerhaling", + "myReplaysText": "Mijn Herhalingen", + "noReplaySelectedErrorText": "Geen Herhaling Gekozen", + "playbackSpeedText": "afspeelsnelheid ${SPEED}", + "renameReplayButtonText": "Hernoem\nHerhaling", + "renameReplayText": "Hernoem \"${REPLAY}\" naar:", + "renameText": "Hernoemen", + "replayDeleteErrorText": "Fout bij verwijderen herhaling.", + "replayNameText": "Herhaling Naam", + "replayRenameErrorAlreadyExistsText": "Een herhaling met die naam bestaat al.", + "replayRenameErrorInvalidName": "Kan herhaling niet hernoemen; ongeldige naam.", + "replayRenameErrorText": "Fout bij hernoemen herhaling.", + "sharedReplaysText": "Gedeelde Herhalingen", + "titleText": "Bekijk", + "watchReplayButtonText": "Bekijk\nHerhaling" + }, + "waveText": "Ronde", + "wellSureText": "Maar Natuurlijk!", + "wiimoteLicenseWindow": { + "licenseTextScale": 0.62, + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Luisteren Naar Wiimotes...", + "pressText": "Druk Wiimote koppen 1 en 2 knop tegelijk in.", + "pressText2": "Gebruik bij de nieuwe Wiimotes, met ingebouwde Motion Plus, de rode 'sync' knop op de achterkant.", + "pressText2Scale": 0.55, + "pressTextScale": 1.0 + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "copyrightTextScale": 0.6, + "listenText": "Luister", + "macInstructionsText": "Zorg er voor dat de Wii uit staat en Bluetooth is ingeschakeld\nop uw Mac. Klik dan op 'Zoeken'. Wiimote ondersteuning\nis soms wat onbetrouwbaar dus het kan zijn dat u het een paar\nkeer moet proberen voordat een connectie tot stand komt.", + "macInstructionsTextScale": 0.7, + "thanksText": "Met dank aan het DarwiinRemote team\ndie dit mogelijk hebben gemaakt.", + "thanksTextScale": 0.8, + "titleText": "Wiimote Instellen" + }, + "winsPlayerText": "${NAME} Wint!", + "winsTeamText": "${NAME} Wint!", + "winsText": "${NAME} Wint!", + "worldScoresUnavailableText": "Wereldwijde scores niet beschikbaar.", + "worldsBestScoresText": "'s Werelds Beste Scores", + "worldsBestTimesText": "'s Werelds Beste Tijden", + "xbox360ControllersWindow": { + "getDriverText": "Driver downloaden", + "macInstructions2Text": "Om de draadloze controller te gebruik heeft u ook de ontvanger nodig\ndie wordt mee geleverd met de 'Xbox 360 Wireless Controller for Windows'.\nMet één ontvanger kunt u 4 controllers verbinden.\n\nBelangrijk: ontvangers van een derde partij werken niet met deze driver;\nzorg er voor dat er op de ontvanger 'Microsoft' staat vermeld, niet\n'XBOX 360'. Microsoft verkoopt deze niet langer los dus u moet de\nontvanger kopen die gebundeld is met de controller en anders zoek op ebay.\n\nAls u gebruik maakt van deze functionaliteit dan kunt u eventueel een\ngeld bedrag doneren op de website van de ontwikkelaar.", + "macInstructions2TextScale": 0.76, + "macInstructionsText": "Om een Xbox 360 controllers te gebruiken met de Mac dient\nu eerst een driver te installeren via de onderstaande link.\nDeze driver werkt met zowel bedrade als draadloze controllers.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "Om bedrade Xbox 360 controllers te gebruiken kunt u deze\ndirect een USB poort van uw apparaat aansluiten.\nGebruik een USB hub om meerdere controllers tegelijk aan\nte sluiten.\n\nOm draadloze controllers te gebruiken heeft u een speciale\ndraadloze ontvanger nodig. Deze is onderdeel van het\n\"Xbox 360 wireless Controller for Windows\" pakket maar is ook\nlos verkrijgbaar. Met deze ontvanger kunt u tot en met 4 controllers\ntegelijk verbinden.", + "ouyaInstructionsTextScale": 0.8, + "titleText": "Hoe Xbox 360 Controllers te gebruiken met ${APP_NAME}:" + }, + "yesAllowText": "Ja, Toestaan!", + "yourBestScoresText": "Uw Beste Scores", + "yourBestTimesText": "Uw Beste Tijden" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/english.json b/dist/ba_data/data/languages/english.json new file mode 100644 index 0000000..af4bab4 --- /dev/null +++ b/dist/ba_data/data/languages/english.json @@ -0,0 +1,1857 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Account names cannot contain emoji or other special characters", + "accountsText": "Accounts", + "achievementProgressText": "Achievements: ${COUNT} out of ${TOTAL}", + "campaignProgressText": "Campaign Progress [Hard]: ${PROGRESS}", + "changeOncePerSeason": "You can only change this once per season.", + "changeOncePerSeasonError": "You must wait until next season to change this again (${NUM} days)", + "customName": "Custom Name", + "linkAccountsEnterCodeText": "Enter Code", + "linkAccountsGenerateCodeText": "Generate Code", + "linkAccountsInfoText": "(share progress across different platforms)", + "linkAccountsInstructionsNewText": "To link two accounts, generate a code on the first\nand enter that code on the second. Data from the\nsecond account will then be shared between both.\n(Data from the first account will be lost)\n\nYou can link up to ${COUNT} accounts.\n\nIMPORTANT: only link accounts that you own;\nIf you link with friends’ accounts you will not\nbe able to play online at the same time.", + "linkAccountsText": "Link Accounts", + "linkedAccountsText": "Linked Accounts:", + "nameChangeConfirm": "Change your account name to ${NAME}?", + "resetProgressConfirmNoAchievementsText": "This will reset your co-op progress and\nlocal high-scores (but not your tickets).\nThis cannot be undone. Are you sure?", + "resetProgressConfirmText": "This will reset your co-op progress,\nachievements, and local high-scores\n(but not your tickets). This cannot\nbe undone. Are you sure?", + "resetProgressText": "Reset Progress", + "setAccountName": "Set Account Name", + "setAccountNameDesc": "Select the name to display for your account.\nYou can use the name from one of your linked\naccounts or create a unique custom name.", + "signInInfoText": "Sign in to collect tickets, compete online,\nand share progress across devices.", + "signInText": "Sign In", + "signInWithDeviceInfoText": "(an automatic account only available from this device)", + "signInWithDeviceText": "Sign in with device account", + "signInWithGameCircleText": "Sign in with Game Circle", + "signInWithGooglePlayText": "Sign in with Google Play", + "signInWithTestAccountInfoText": "(legacy account type; use device accounts going forward)", + "signInWithTestAccountText": "Sign in with test account", + "signOutText": "Sign Out", + "signingInText": "Signing in...", + "signingOutText": "Signing out...", + "ticketsText": "Tickets: ${COUNT}", + "titleText": "Account", + "unlinkAccountsInstructionsText": "Select an account to unlink", + "unlinkAccountsText": "Unlink Accounts", + "viaAccount": "(via account ${NAME})", + "youAreSignedInAsText": "You are signed in as:" + }, + "achievementChallengesText": "Achievement Challenges", + "achievementText": "Achievement", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Kill 3 bad guys with TNT", + "descriptionComplete": "Killed 3 bad guys with TNT", + "descriptionFull": "Kill 3 bad guys with TNT on ${LEVEL}", + "descriptionFullComplete": "Killed 3 bad guys with TNT on ${LEVEL}", + "name": "Boom Goes the Dynamite" + }, + "Boxer": { + "description": "Win without using any bombs", + "descriptionComplete": "Won without using any bombs", + "descriptionFull": "Complete ${LEVEL} without using any bombs", + "descriptionFullComplete": "Completed ${LEVEL} without using any bombs", + "name": "Boxer" + }, + "Dual Wielding": { + "descriptionFull": "Connect 2 controllers (hardware or app)", + "descriptionFullComplete": "Connected 2 controllers (hardware or app)", + "name": "Dual Wielding" + }, + "Flawless Victory": { + "description": "Win without getting hit", + "descriptionComplete": "Won without getting hit", + "descriptionFull": "Win ${LEVEL} without getting hit", + "descriptionFullComplete": "Won ${LEVEL} without getting hit", + "name": "Flawless Victory" + }, + "Free Loader": { + "descriptionFull": "Start a Free-For-All game with 2+ players", + "descriptionFullComplete": "Started a Free-For-All game with 2+ players", + "name": "Free Loader" + }, + "Gold Miner": { + "description": "Kill 6 bad guys with land-mines", + "descriptionComplete": "Killed 6 bad guys with land-mines", + "descriptionFull": "Kill 6 bad guys with land-mines on ${LEVEL}", + "descriptionFullComplete": "Killed 6 bad guys with land-mines on ${LEVEL}", + "name": "Gold Miner" + }, + "Got the Moves": { + "description": "Win without using punches or bombs", + "descriptionComplete": "Won without using punches or bombs", + "descriptionFull": "Win ${LEVEL} without any punches or bombs", + "descriptionFullComplete": "Won ${LEVEL} without any punches or bombs", + "name": "Got the Moves" + }, + "In Control": { + "descriptionFull": "Connect a controller (hardware or app)", + "descriptionFullComplete": "Connected a controller. (hardware or app)", + "name": "In Control" + }, + "Last Stand God": { + "description": "Score 1000 points", + "descriptionComplete": "Scored 1000 points", + "descriptionFull": "Score 1000 points on ${LEVEL}", + "descriptionFullComplete": "Scored 1000 points on ${LEVEL}", + "name": "${LEVEL} God" + }, + "Last Stand Master": { + "description": "Score 250 points", + "descriptionComplete": "Scored 250 points", + "descriptionFull": "Score 250 points on ${LEVEL}", + "descriptionFullComplete": "Scored 250 points on ${LEVEL}", + "name": "${LEVEL} Master" + }, + "Last Stand Wizard": { + "description": "Score 500 points", + "descriptionComplete": "Scored 500 points", + "descriptionFull": "Score 500 points on ${LEVEL}", + "descriptionFullComplete": "Scored 500 points on ${LEVEL}", + "name": "${LEVEL} Wizard" + }, + "Mine Games": { + "description": "Kill 3 bad guys with land-mines", + "descriptionComplete": "Killed 3 bad guys with land-mines", + "descriptionFull": "Kill 3 bad guys with land-mines on ${LEVEL}", + "descriptionFullComplete": "Killed 3 bad guys with land-mines on ${LEVEL}", + "name": "Mine Games" + }, + "Off You Go Then": { + "description": "Toss 3 bad guys off the map", + "descriptionComplete": "Tossed 3 bad guys off the map", + "descriptionFull": "Toss 3 bad guys off the map in ${LEVEL}", + "descriptionFullComplete": "Tossed 3 bad guys off the map in ${LEVEL}", + "name": "Off You Go Then" + }, + "Onslaught God": { + "description": "Score 5000 points", + "descriptionComplete": "Scored 5000 points", + "descriptionFull": "Score 5000 points on ${LEVEL}", + "descriptionFullComplete": "Scored 5000 points on ${LEVEL}", + "name": "${LEVEL} God" + }, + "Onslaught Master": { + "description": "Score 500 points", + "descriptionComplete": "Scored 500 points", + "descriptionFull": "Score 500 points on ${LEVEL}", + "descriptionFullComplete": "Scored 500 points on ${LEVEL}", + "name": "${LEVEL} Master" + }, + "Onslaught Training Victory": { + "description": "Defeat all waves", + "descriptionComplete": "Defeated all waves", + "descriptionFull": "Defeat all waves in ${LEVEL}", + "descriptionFullComplete": "Defeated all waves in ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Onslaught Wizard": { + "description": "Score 1000 points", + "descriptionComplete": "Scored 1000 points", + "descriptionFull": "Score 1000 points in ${LEVEL}", + "descriptionFullComplete": "Scored 1000 points in ${LEVEL}", + "name": "${LEVEL} Wizard" + }, + "Precision Bombing": { + "description": "Win without any powerups", + "descriptionComplete": "Won without any powerups", + "descriptionFull": "Win ${LEVEL} without any power-ups", + "descriptionFullComplete": "Won ${LEVEL} without any power-ups", + "name": "Precision Bombing" + }, + "Pro Boxer": { + "description": "Win without using any bombs", + "descriptionComplete": "Won without using any bombs", + "descriptionFull": "Complete ${LEVEL} without using any bombs", + "descriptionFullComplete": "Completed ${LEVEL} without using any bombs", + "name": "Pro Boxer" + }, + "Pro Football Shutout": { + "description": "Win without letting the bad guys score", + "descriptionComplete": "Won without letting the bad guys score", + "descriptionFull": "Win ${LEVEL} without letting the bad guys score", + "descriptionFullComplete": "Won ${LEVEL} without letting the bad guys score", + "name": "${LEVEL} Shutout" + }, + "Pro Football Victory": { + "description": "Win the game", + "descriptionComplete": "Won the game", + "descriptionFull": "Win the game in ${LEVEL}", + "descriptionFullComplete": "Won the game in ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Pro Onslaught Victory": { + "description": "Defeat all waves", + "descriptionComplete": "Defeated all waves", + "descriptionFull": "Defeat all waves of ${LEVEL}", + "descriptionFullComplete": "Defeated all waves of ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Pro Runaround Victory": { + "description": "Complete all waves", + "descriptionComplete": "Completed all waves", + "descriptionFull": "Complete all waves on ${LEVEL}", + "descriptionFullComplete": "Completed all waves on ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Rookie Football Shutout": { + "description": "Win without letting the bad guys score", + "descriptionComplete": "Won without letting the bad guys score", + "descriptionFull": "Win ${LEVEL} without letting the bad guys score", + "descriptionFullComplete": "Won ${LEVEL} without letting the bad guys score", + "name": "${LEVEL} Shutout" + }, + "Rookie Football Victory": { + "description": "Win the game", + "descriptionComplete": "Won the game", + "descriptionFull": "Win the game in ${LEVEL}", + "descriptionFullComplete": "Won the game in ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Rookie Onslaught Victory": { + "description": "Defeat all waves", + "descriptionComplete": "Defeated all waves", + "descriptionFull": "Defeat all waves in ${LEVEL}", + "descriptionFullComplete": "Defeated all waves in ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Runaround God": { + "description": "Score 2000 points", + "descriptionComplete": "Scored 2000 points", + "descriptionFull": "Score 2000 points on ${LEVEL}", + "descriptionFullComplete": "Scored 2000 points on ${LEVEL}", + "name": "${LEVEL} God" + }, + "Runaround Master": { + "description": "Score 500 points", + "descriptionComplete": "Scored 500 points", + "descriptionFull": "Score 500 points in ${LEVEL}", + "descriptionFullComplete": "Scored 500 points in ${LEVEL}", + "name": "${LEVEL} Master" + }, + "Runaround Wizard": { + "description": "Score 1000 points", + "descriptionComplete": "Scored 1000 points", + "descriptionFull": "Score 1000 points on ${LEVEL}", + "descriptionFullComplete": "Scored 1000 points on ${LEVEL}", + "name": "${LEVEL} Wizard" + }, + "Sharing is Caring": { + "descriptionFull": "Successfully share the game with a friend", + "descriptionFullComplete": "Successfully shared the game with a friend", + "name": "Sharing is Caring" + }, + "Stayin' Alive": { + "description": "Win without dying", + "descriptionComplete": "Won without dying", + "descriptionFull": "Win ${LEVEL} without dying", + "descriptionFullComplete": "Won ${LEVEL} without dying", + "name": "Stayin' Alive" + }, + "Super Mega Punch": { + "description": "Inflict 100% damage with one punch", + "descriptionComplete": "Inflicted 100% damage with one punch", + "descriptionFull": "Inflict 100% damage with one punch in ${LEVEL}", + "descriptionFullComplete": "Inflicted 100% damage with one punch in ${LEVEL}", + "name": "Super Mega Punch" + }, + "Super Punch": { + "description": "Inflict 50% damage with one punch", + "descriptionComplete": "Inflicted 50% damage with one punch", + "descriptionFull": "Inflict 50% damage with one punch on ${LEVEL}", + "descriptionFullComplete": "Inflicted 50% damage with one punch on ${LEVEL}", + "name": "Super Punch" + }, + "TNT Terror": { + "description": "Kill 6 bad guys with TNT", + "descriptionComplete": "Killed 6 bad guys with TNT", + "descriptionFull": "Kill 6 bad guys with TNT on ${LEVEL}", + "descriptionFullComplete": "Killed 6 bad guys with TNT on ${LEVEL}", + "name": "TNT Terror" + }, + "Team Player": { + "descriptionFull": "Start a Teams game with 4+ players", + "descriptionFullComplete": "Started a Teams game with 4+ players", + "name": "Team Player" + }, + "The Great Wall": { + "description": "Stop every single bad guy", + "descriptionComplete": "Stopped every single bad guy", + "descriptionFull": "Stop every single bad guy on ${LEVEL}", + "descriptionFullComplete": "Stopped every single bad guy on ${LEVEL}", + "name": "The Great Wall" + }, + "The Wall": { + "description": "Stop every single bad guy", + "descriptionComplete": "Stopped every single bad guy", + "descriptionFull": "Stop every single bad guy on ${LEVEL}", + "descriptionFullComplete": "Stopped every single bad guy on ${LEVEL}", + "name": "The Wall" + }, + "Uber Football Shutout": { + "description": "Win without letting the bad guys score", + "descriptionComplete": "Won without letting the bad guys score", + "descriptionFull": "Win ${LEVEL} without letting the bad guys score", + "descriptionFullComplete": "Won ${LEVEL} without letting the bad guys score", + "name": "${LEVEL} Shutout" + }, + "Uber Football Victory": { + "description": "Win the game", + "descriptionComplete": "Won the game", + "descriptionFull": "Win the game in ${LEVEL}", + "descriptionFullComplete": "Won the game in ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Uber Onslaught Victory": { + "description": "Defeat all waves", + "descriptionComplete": "Defeated all waves", + "descriptionFull": "Defeat all waves in ${LEVEL}", + "descriptionFullComplete": "Defeated all waves in ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Uber Runaround Victory": { + "description": "Complete all waves", + "descriptionComplete": "Completed all waves", + "descriptionFull": "Complete all waves on ${LEVEL}", + "descriptionFullComplete": "Completed all waves on ${LEVEL}", + "name": "${LEVEL} Victory" + } + }, + "achievementsRemainingText": "Achievements Remaining:", + "achievementsText": "Achievements", + "achievementsUnavailableForOldSeasonsText": "Sorry, achievement specifics are not available for old seasons.", + "addGameWindow": { + "getMoreGamesText": "Get More Games...", + "titleText": "Add Game" + }, + "allowText": "Allow", + "alreadySignedInText": "Your account is signed in from another device;\nplease switch accounts or close the game on your\nother devices and try again.", + "apiVersionErrorText": "Can't load module ${NAME}; it targets api-version ${VERSION_USED}; we require ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" enables this only when headphones are plugged in)", + "headRelativeVRAudioText": "Head-Relative VR Audio", + "musicVolumeText": "Music Volume", + "soundVolumeText": "Sound Volume", + "soundtrackButtonText": "Soundtracks", + "soundtrackDescriptionText": "(assign your own music to play during games)", + "titleText": "Audio" + }, + "autoText": "Auto", + "backText": "Back", + "banThisPlayerText": "Ban This Player", + "bestOfFinalText": "Best-of-${COUNT} Final", + "bestOfSeriesText": "Best of ${COUNT} series:", + "bestOfUseFirstToInstead": 0, + "bestRankText": "Your best is #${RANK}", + "bestRatingText": "Your best rating is ${RATING}", + "bombBoldText": "BOMB", + "bombText": "Bomb", + "boostText": "Boost", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} is configured in the app itself.", + "buttonText": "button", + "canWeDebugText": "Would you like BombSquad to automatically report\nbugs, crashes, and basic usage info to the developer?\n\nThis data contains no personal information and helps\nkeep the game running smoothly and bug-free.\n", + "cancelText": "Cancel", + "cantConfigureDeviceText": "Sorry, ${DEVICE} is not configurable.", + "challengeEndedText": "This challenge has ended.", + "chatMuteText": "Mute Chat", + "chatMutedText": "Chat Muted", + "chatUnMuteText": "Unmute Chat", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "You must complete\nthis level to proceed!", + "completionBonusText": "Completion Bonus", + "configControllersWindow": { + "configureControllersText": "Configure Controllers", + "configureKeyboard2Text": "Configure Keyboard P2", + "configureKeyboardText": "Configure Keyboard", + "configureMobileText": "Mobile Devices as Controllers", + "configureTouchText": "Configure Touchscreen", + "ps3Text": "PS3 Controllers", + "titleText": "Controllers", + "wiimotesText": "Wiimotes", + "xbox360Text": "Xbox 360 Controllers" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Note: controller support varies by device and Android version.", + "pressAnyButtonText": "Press any button on the controller\n you want to configure...", + "titleText": "Configure Controllers" + }, + "configGamepadWindow": { + "advancedText": "Advanced", + "advancedTitleText": "Advanced Controller Setup", + "analogStickDeadZoneDescriptionText": "(turn this up if your character 'drifts' when you release the stick)", + "analogStickDeadZoneText": "Analog Stick Dead Zone", + "appliesToAllText": "(applies to all controllers of this type)", + "autoRecalibrateDescriptionText": "(enable this if your character does not move at full speed)", + "autoRecalibrateText": "Auto-Recalibrate Analog Stick", + "axisText": "axis", + "clearText": "clear", + "dpadText": "dpad", + "extraStartButtonText": "Extra Start Button", + "ifNothingHappensTryAnalogText": "If nothing happens, try assigning to the analog stick instead.", + "ifNothingHappensTryDpadText": "If nothing happens, try assigning to the d-pad instead.", + "ignoreCompletelyDescriptionText": "(prevent this controller from affecting either the game or menus)", + "ignoreCompletelyText": "Ignore Completely", + "ignoredButton1Text": "Ignored Button 1", + "ignoredButton2Text": "Ignored Button 2", + "ignoredButton3Text": "Ignored Button 3", + "ignoredButton4Text": "Ignored Button 4", + "ignoredButtonDescriptionText": "(use this to prevent 'home' or 'sync' buttons from affecting the UI)", + "pressAnyAnalogTriggerText": "Press any analog trigger...", + "pressAnyButtonOrDpadText": "Press any button or dpad...", + "pressAnyButtonText": "Press any button...", + "pressLeftRightText": "Press left or right...", + "pressUpDownText": "Press up or down...", + "runButton1Text": "Run Button 1", + "runButton2Text": "Run Button 2", + "runTrigger1Text": "Run Trigger 1", + "runTrigger2Text": "Run Trigger 2", + "runTriggerDescriptionText": "(analog triggers let you run at variable speeds)", + "secondHalfText": "Use this to configure the second half\nof a 2-controllers-in-1 device that\nshows up as a single controller.", + "secondaryEnableText": "Enable", + "secondaryText": "Secondary Controller", + "startButtonActivatesDefaultDescriptionText": "(turn this off if your start button is more of a 'menu' button)", + "startButtonActivatesDefaultText": "Start Button Activates Default Widget", + "titleText": "Controller Setup", + "twoInOneSetupText": "2-in-1 Controller Setup", + "uiOnlyDescriptionText": "(prevent this controller from actually joining a game)", + "uiOnlyText": "Limit to Menu Use", + "unassignedButtonsRunText": "All Unassigned Buttons Run", + "unsetText": "", + "vrReorientButtonText": "VR Reorient Button" + }, + "configKeyboardWindow": { + "configuringText": "Configuring ${DEVICE}", + "keyboard2NoteText": "Note: most keyboards can only register a few keypresses at\nonce, so having a second keyboard player may work better\nif there is a separate keyboard attached for them to use.\nNote that you'll still need to assign unique keys to the\ntwo players even in that case." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Action Control Scale", + "actionsText": "Actions", + "buttonsText": "buttons", + "dragControlsText": "< drag controls to reposition them >", + "joystickText": "joystick", + "movementControlScaleText": "Movement Control Scale", + "movementText": "Movement", + "resetText": "Reset", + "swipeControlsHiddenText": "Hide Swipe Icons", + "swipeInfoText": "'Swipe' style controls take a little getting used to but\nmake it easier to play without looking at the controls.", + "swipeText": "swipe", + "titleText": "Configure Touchscreen" + }, + "configureItNowText": "Configure it now?", + "configureText": "Configure", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsScale": 0.65, + "bestResultsText": "For best results you'll need a lag-free wifi network. You can\nreduce wifi lag by turning off other wireless devices, by\nplaying close to your wifi router, and by connecting the\ngame host directly to the network via ethernet.", + "explanationText": "To use a smart-phone or tablet as a wireless controller,\ninstall the \"${REMOTE_APP_NAME}\" app on it. Any number of devices\ncan connect to a ${APP_NAME} game over Wi-Fi, and it's free!", + "forAndroidText": "for Android:", + "forIOSText": "for iOS:", + "getItForText": "Get ${REMOTE_APP_NAME} for iOS at the Apple App Store\nor for Android at the Google Play Store or Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Using Mobile Devices as Controllers:" + }, + "continuePurchaseText": "Continue for ${PRICE}?", + "continueText": "Continue", + "controlsText": "Controls", + "coopSelectWindow": { + "activenessAllTimeInfoText": "This does not apply to all-time rankings.", + "activenessInfoText": "This multiplier rises on days when you\nplay and drops on days when you do not.", + "activityText": "Activity", + "campaignText": "Campaign", + "challengesInfoText": "Earn prizes for completing mini-games.\n\nPrizes and difficulty levels increase\neach time a challenge is completed and\ndecrease when one expires or is forfeited.", + "challengesText": "Challenges", + "currentBestText": "Current Best", + "customText": "Custom", + "entryFeeText": "Entry", + "forfeitConfirmText": "Forfeit this challenge?", + "forfeitNotAllowedYetText": "This challenge cannot be forfeited yet.", + "forfeitText": "Forfeit", + "multipliersText": "Multipliers", + "nextChallengeText": "Next Challenge", + "nextPlayText": "Next Play", + "ofTotalTimeText": "of ${TOTAL}", + "playNowText": "Play Now", + "pointsText": "Points", + "powerRankingFinishedSeasonUnrankedText": "(finished season unranked)", + "powerRankingNotInTopText": "(not in the top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} pts", + "powerRankingPointsMultText": "(x ${NUMBER} pts)", + "powerRankingPointsText": "${NUMBER} pts", + "powerRankingPointsToRankedText": "(${CURRENT} of ${REMAINING} pts)", + "powerRankingText": "Power Ranking", + "prizesText": "Prizes", + "proMultInfoText": "Players with the ${PRO} upgrade\nreceive a ${PERCENT}% point boost here.", + "seeMoreText": "More...", + "skipWaitText": "Skip Wait", + "timeRemainingText": "Time Remaining", + "toRankedText": "To Ranked", + "totalText": "total", + "tournamentInfoText": "Compete for high scores with\nother players in your league.\n\nPrizes are awarded to the top scoring\nplayers when tournament time expires.", + "welcome1Text": "Welcome to ${LEAGUE}. You can improve your\nleague rank by earning star ratings, completing\nachievements, and winning trophies in tournaments.", + "welcome2Text": "You can also earn tickets from many of the same activities.\nTickets can be used to unlock new characters, maps, and\nmini-games, to enter tournaments, and more.", + "yourPowerRankingText": "Your Power Ranking:" + }, + "copyOfText": "${NAME} Copy", + "createEditPlayerText": "", + "createText": "Create", + "creditsWindow": { + "additionalAudioArtIdeasText": "Additional Audio, Early Artwork, and Ideas by ${NAME}", + "additionalMusicFromText": "Additional music from ${NAME}", + "allMyFamilyText": "All of my friends and family who helped play test", + "codingGraphicsAudioText": "Coding, Graphics, and Audio by ${NAME}", + "languageTranslationsText": "Language Translations:", + "legalText": "Legal:", + "publicDomainMusicViaText": "Public-domain music via ${NAME}", + "softwareBasedOnText": "This software is based in part on the work of ${NAME}", + "songCreditText": "${TITLE} Performed by ${PERFORMER}\nComposed by ${COMPOSER}, Arranged by ${ARRANGER}, Published by ${PUBLISHER},\nCourtesy of ${SOURCE}", + "soundAndMusicText": "Sound & Music:", + "soundsText": "Sounds (${SOURCE}):", + "specialThanksText": "Special Thanks:", + "thanksEspeciallyToText": "Thanks especially to ${NAME}", + "titleText": "${APP_NAME} Credits", + "whoeverInventedCoffeeText": "Whoever invented coffee" + }, + "currentStandingText": "Your current standing is #${RANK}", + "customizeText": "Customize...", + "deathsTallyText": "${COUNT} deaths", + "deathsText": "Deaths", + "debugText": "debug", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Note: it is recommended that you set Settings->Graphics->Textures to 'High' while testing this.", + "runCPUBenchmarkText": "Run CPU Benchmark", + "runGPUBenchmarkText": "Run GPU Benchmark", + "runMediaReloadBenchmarkText": "Run Media-Reload Benchmark", + "runStressTestText": "Run stress test", + "stressTestPlayerCountText": "Player Count", + "stressTestPlaylistDescriptionText": "Stress Test Playlist", + "stressTestPlaylistNameText": "Playlist Name", + "stressTestPlaylistTypeText": "Playlist Type", + "stressTestRoundDurationText": "Round Duration", + "stressTestTitleText": "Stress Test", + "titleText": "Benchmarks & Stress Tests", + "totalReloadTimeText": "Total reload time: ${TIME} (see log for details)" + }, + "defaultGameListNameText": "Default ${PLAYMODE} Playlist", + "defaultNewGameListNameText": "My ${PLAYMODE} Playlist", + "deleteText": "Delete", + "demoText": "Demo", + "denyText": "Deny", + "desktopResText": "Desktop Res", + "difficultyEasyText": "Easy", + "difficultyHardOnlyText": "Hard Mode Only", + "difficultyHardText": "Hard", + "difficultyHardUnlockOnlyText": "This level can only be unlocked in hard mode.\nDo you think you have what it takes!?!?!", + "directBrowserToURLText": "Please direct a web-browser to the following URL:", + "disableRemoteAppConnectionsText": "Disable Remote-App Connections", + "disableXInputDescriptionText": "Allows more than 4 controllers but may not work as well.", + "disableXInputText": "Disable XInput", + "doneText": "Done", + "drawText": "Draw", + "duplicateText": "Duplicate", + "editGameListWindow": { + "addGameText": "Add\nGame", + "cantOverwriteDefaultText": "Can't overwrite the default playlist!", + "cantSaveAlreadyExistsText": "A playlist with that name already exists!", + "cantSaveEmptyListText": "Can't save an empty playlist!", + "editGameText": "Edit\nGame", + "listNameText": "Playlist Name", + "nameText": "Name", + "removeGameText": "Remove\nGame", + "saveText": "Save List", + "titleText": "Playlist Editor" + }, + "editProfileWindow": { + "accountProfileInfoText": "This special profile has a name\nand icon based on your account.\n\n${ICONS}\n\nCreate custom profiles to use\ndifferent names or custom icons.", + "accountProfileText": "(account profile)", + "availableText": "The name \"${NAME}\" is available.", + "characterText": "character", + "checkingAvailabilityText": "Checking availability for \"${NAME}\"...", + "colorText": "color", + "getMoreCharactersText": "Get More Characters...", + "getMoreIconsText": "Get More Icons...", + "globalProfileInfoText": "Global player profiles are guaranteed to have unique\nnames worldwide. They also include custom icons.", + "globalProfileText": "(global profile)", + "highlightText": "highlight", + "iconText": "icon", + "localProfileInfoText": "Local player profiles have no icons and their names are\nnot guaranteed to be unique. Upgrade to a global profile\nto reserve a unique name and add a custom icon.", + "localProfileText": "(local profile)", + "nameDescriptionText": "Player Name", + "nameText": "Name", + "randomText": "random", + "titleEditText": "Edit Profile", + "titleNewText": "New Profile", + "unavailableText": "\"${NAME}\" is unavailable; try another name.", + "upgradeProfileInfoText": "This will reserve your player name worldwide\nand allow you to assign a custom icon to it.", + "upgradeToGlobalProfileText": "Upgrade to Global Profile" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "You can't delete the default soundtrack.", + "cantEditDefaultText": "Can't edit default soundtrack. Duplicate it or create a new one.", + "cantOverwriteDefaultText": "Can't overwrite default soundtrack", + "cantSaveAlreadyExistsText": "A soundtrack with that name already exists!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Default Soundtrack", + "deleteConfirmText": "Delete Soundtrack:\n\n'${NAME}'?", + "deleteText": "Delete\nSoundtrack", + "duplicateText": "Duplicate\nSoundtrack", + "editSoundtrackText": "Soundtrack Editor", + "editText": "Edit\nSoundtrack", + "fetchingITunesText": "fetching Music App playlists...", + "musicVolumeZeroWarning": "Warning: music volume is set to 0", + "nameText": "Name", + "newSoundtrackNameText": "My Soundtrack ${COUNT}", + "newSoundtrackText": "New Soundtrack:", + "newText": "New\nSoundtrack", + "selectAPlaylistText": "Select A Playlist", + "selectASourceText": "Music Source", + "testText": "test", + "titleText": "Soundtracks", + "useDefaultGameMusicText": "Default Game Music", + "useITunesPlaylistText": "Music App Playlist", + "useMusicFileText": "Music File (mp3, etc)", + "useMusicFolderText": "Folder of Music Files" + }, + "editText": "Edit", + "endText": "End", + "enjoyText": "Enjoy!", + "epicDescriptionFilterText": "${DESCRIPTION} In epic slow motion.", + "epicNameFilterText": "Epic ${NAME}", + "errorAccessDeniedText": "access denied", + "errorOutOfDiskSpaceText": "out of disk space", + "errorText": "Error", + "errorUnknownText": "unknown error", + "exitGameText": "Exit ${APP_NAME}?", + "exportSuccessText": "'${NAME}' exported.", + "externalStorageText": "External Storage", + "failText": "Fail", + "fatalErrorText": "Uh oh; something is missing or broken.\nPlease try reinstalling the app or\ncontact ${EMAIL} for help.", + "fileSelectorWindow": { + "titleFileFolderText": "Select a File or Folder", + "titleFileText": "Select a File", + "titleFolderText": "Select a Folder", + "useThisFolderButtonText": "Use This Folder" + }, + "filterText": "Filter", + "finalScoreText": "Final Score", + "finalScoresText": "Final Scores", + "finalTimeText": "Final Time", + "finishingInstallText": "Finishing install; one moment...", + "fireTVRemoteWarningText": "* For a better experience, use\nGame Controllers or install the\n'${REMOTE_APP_NAME}' app on your\nphones and tablets.", + "firstToFinalText": "First-to-${COUNT} Final", + "firstToSeriesText": "First-to-${COUNT} Series", + "fiveKillText": "FIVE KILL!!!", + "flawlessWaveText": "Flawless Wave!", + "fourKillText": "QUAD KILL!!!", + "friendScoresUnavailableText": "Friend scores unavailable.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Game ${COUNT} Leaders", + "gameListWindow": { + "cantDeleteDefaultText": "You can't delete the default playlist.", + "cantEditDefaultText": "Can't edit the default playlist! Duplicate it or create a new one.", + "cantShareDefaultText": "You can't share the default playlist.", + "deleteConfirmText": "Delete \"${LIST}\"?", + "deleteText": "Delete\nPlaylist", + "duplicateText": "Duplicate\nPlaylist", + "editText": "Edit\nPlaylist", + "newText": "New\nPlaylist", + "showTutorialText": "Show Tutorial", + "shuffleGameOrderText": "Shuffle Game Order", + "titleText": "Customize ${TYPE} Playlists" + }, + "gameSettingsWindow": { + "addGameText": "Add Game" + }, + "gamesToText": "${WINCOUNT} games to ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Remember: any device in a party can have more\nthan one player if you have enough controllers.", + "aboutDescriptionText": "Use these tabs to assemble a party.\n\nParties let you play games and tournaments\nwith your friends across different devices.\n\nUse the ${PARTY} button at the top right to\nchat and interact with your party.\n(on a controller, press ${BUTTON} while in a menu)", + "aboutText": "About", + "addressFetchErrorText": "", + "appInviteMessageText": "${NAME} sent you ${COUNT} tickets in ${APP_NAME}", + "appInviteSendACodeText": "Send Them A Code", + "appInviteTitleText": "${APP_NAME} App Invite", + "bluetoothAndroidSupportText": "(works with any Android device supporting Bluetooth)", + "bluetoothDescriptionText": "Host/join a party over Bluetooth:", + "bluetoothHostText": "Host over Bluetooth", + "bluetoothJoinText": "Join over Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "checking...", + "copyCodeConfirmText": "Code copied to clipboard.", + "copyCodeText": "Copy Code", + "dedicatedServerInfoText": "For best results, set up a dedicated server. See bombsquadgame.com/server to learn how.", + "disconnectClientsText": "This will disconnect the ${COUNT} player(s)\nin your party. Are you sure?", + "earnTicketsForRecommendingAmountText": "Friends will receive ${COUNT} tickets if they try the game\n(and you will receive ${YOU_COUNT} for each who does)", + "earnTicketsForRecommendingText": "Share the game\nfor free tickets...", + "emailItText": "Email It", + "favoritesSaveText": "Save As Favorite", + "favoritesText": "Favorites", + "freeCloudServerAvailableMinutesText": "Next free cloud server available in ${MINUTES} minutes.", + "freeCloudServerAvailableNowText": "Free cloud server available!", + "freeCloudServerNotAvailableText": "No free cloud servers available.", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} tickets from ${NAME}", + "friendPromoCodeAwardText": "You will receive ${COUNT} tickets each time it is used.", + "friendPromoCodeExpireText": "The code will expire in ${EXPIRE_HOURS} hours and only works for new players.", + "friendPromoCodeInstructionsText": "To use it, open ${APP_NAME} and go to \"Settings->Advanced->Enter Code\".\nSee bombsquadgame.com for download links for all supported platforms.", + "friendPromoCodeRedeemLongText": "It can be redeemed for ${COUNT} free tickets by up to ${MAX_USES} people.", + "friendPromoCodeRedeemShortText": "It can be redeemed for ${COUNT} tickets in the game.", + "friendPromoCodeWhereToEnterText": "(in \"Settings->Advanced->Enter Code\")", + "getFriendInviteCodeText": "Get Friend Invite Code", + "googlePlayDescriptionText": "Invite Google Play players to your party:", + "googlePlayInviteText": "Invite", + "googlePlayReInviteText": "There are ${COUNT} Google Play player(s) in your party\nwho will be disconnected if you start a new invite.\nInclude them in the new invitation to get them back.", + "googlePlaySeeInvitesText": "See Invites", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play version)", + "hostPublicPartyDescriptionText": "Host a Public Party", + "hostingUnavailableText": "Hosting Unavailable", + "inDevelopmentWarningText": "Note:\n\nNetwork play is a new and still-evolving feature.\nFor now, it is highly recommended that all\nplayers be on the same Wi-Fi network.", + "internetText": "Internet", + "inviteAFriendText": "Friends don't have the game? Invite them to\ntry it and they'll receive ${COUNT} free tickets.", + "inviteFriendsText": "Invite Friends", + "joinPublicPartyDescriptionText": "Join a Public Party", + "localNetworkDescriptionText": "Join a Nearby Party (LAN, Bluetooth, etc.)", + "localNetworkText": "Local Network", + "makePartyPrivateText": "Make My Party Private", + "makePartyPublicText": "Make My Party Public", + "manualAddressText": "Address", + "manualConnectText": "Connect", + "manualDescriptionText": "Join a party by address:", + "manualJoinSectionText": "Join By Address", + "manualJoinableFromInternetText": "Are you joinable from the internet?:", + "manualJoinableNoWithAsteriskText": "NO*", + "manualJoinableYesText": "YES", + "manualRouterForwardingText": "*to fix this, try configuring your router to forward UDP port ${PORT} to your local address", + "manualText": "Manual", + "manualYourAddressFromInternetText": "Your address from the internet:", + "manualYourLocalAddressText": "Your local address:", + "nearbyText": "Nearby", + "noConnectionText": "", + "otherVersionsText": "(other versions)", + "partyCodeText": "Party Code", + "partyInviteAcceptText": "Accept", + "partyInviteDeclineText": "Decline", + "partyInviteGooglePlayExtraText": "(see the 'Google Play' tab in the 'Gather' window)", + "partyInviteIgnoreText": "Ignore", + "partyInviteText": "${NAME} has invited\nyou to join their party!", + "partyNameText": "Party Name", + "partyServerRunningText": "Your party server is running.", + "partySizeText": "party size", + "partyStatusCheckingText": "checking status...", + "partyStatusJoinableText": "your party is now joinable from the internet", + "partyStatusNoConnectionText": "unable to connect to server", + "partyStatusNotJoinableText": "your party is not joinable from the internet", + "partyStatusNotPublicText": "your party is not public", + "pingText": "ping", + "portText": "Port", + "privatePartyCloudDescriptionText": "Private parties run on dedicated cloud servers; no router configuration required.", + "privatePartyHostText": "Host a Private Party", + "privatePartyJoinText": "Join a Private Party", + "privateText": "Private", + "publicHostRouterConfigText": "This may require configuring port-forwarding on your router. For an easier option, host a private party.", + "publicText": "Public", + "requestingAPromoCodeText": "Requesting a code...", + "sendDirectInvitesText": "Send Direct Invites", + "shareThisCodeWithFriendsText": "Share this code with friends:", + "showMyAddressText": "Show My Address", + "startHostingPaidText": "Host Now For ${COST}", + "startHostingText": "Host", + "startStopHostingMinutesText": "You can start and stop hosting for free for the next ${MINUTES} minutes.", + "stopHostingText": "Stop Hosting", + "titleText": "Gather", + "wifiDirectDescriptionBottomText": "If all devices have a 'Wi-Fi Direct' panel, they should be able to use it to find\nand connect to each other. Once all devices are connected, you can form parties\nhere using the 'Local Network' tab, just the same as with a regular Wi-Fi network.\n\nFor best results, the Wi-Fi Direct host should also be the ${APP_NAME} party host.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct can be used to connect Android devices directly without\nneeding a Wi-Fi network. This works best on Android 4.2 or newer.\n\nTo use it, open Wi-Fi settings and look for 'Wi-Fi Direct' in the menu.", + "wifiDirectOpenWiFiSettingsText": "Open Wi-Fi Settings", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(works between all platforms)", + "worksWithGooglePlayDevicesText": "(works with devices running the Google Play (android) version of the game)", + "youHaveBeenSentAPromoCodeText": "You have been sent a ${APP_NAME} promo code:" + }, + "getTicketsWindow": { + "freeText": "FREE!", + "freeTicketsText": "Free Tickets", + "inProgressText": "A transaction is in progress; please try again in a moment.", + "purchasesRestoredText": "Purchases restored.", + "receivedTicketsText": "Received ${COUNT} tickets!", + "restorePurchasesText": "Restore Purchases", + "ticketPack1Text": "Small Ticket Pack", + "ticketPack2Text": "Medium Ticket Pack", + "ticketPack3Text": "Large Ticket Pack", + "ticketPack4Text": "Jumbo Ticket Pack", + "ticketPack5Text": "Mammoth Ticket Pack", + "ticketPack6Text": "Ultimate Ticket Pack", + "ticketsFromASponsorText": "Get ${COUNT} tickets\nfrom a sponsor", + "ticketsText": "${COUNT} Tickets", + "titleText": "Get Tickets", + "unavailableLinkAccountText": "Sorry, purchases are not available on this platform.\nAs a workaround, you can link this account to an account on\nanother platform and make purchases there.", + "unavailableTemporarilyText": "This is currently unavailable; please try again later.", + "unavailableText": "Sorry, this is not available.", + "versionTooOldText": "Sorry, this version of the game is too old; please update to a newer one.", + "youHaveShortText": "you have ${COUNT}", + "youHaveText": "you have ${COUNT} tickets" + }, + "googleMultiplayerDiscontinuedText": "Sorry, Google’s multiplayer service is no longer available.\nI am working on a replacement as fast as possible.\nUntil then, please try another connection method.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Always", + "fullScreenCmdText": "Fullscreen (Cmd-F)", + "fullScreenCtrlText": "Fullscreen (Ctrl-F)", + "gammaText": "Gamma", + "highText": "High", + "higherText": "Higher", + "lowText": "Low", + "mediumText": "Medium", + "neverText": "Never", + "resolutionText": "Resolution", + "showFPSText": "Show FPS", + "texturesText": "Textures", + "titleText": "Graphics", + "tvBorderText": "TV Border", + "verticalSyncText": "Vertical Sync", + "visualsText": "Visuals" + }, + "helpWindow": { + "bombInfoText": "- Bomb -\nStronger than punches, but\ncan result in grave self-injury.\nFor best results, throw towards\nenemy before fuse runs out.", + "bombInfoTextScale": 0.6, + "canHelpText": "${APP_NAME} can help.", + "controllersInfoText": "You can play ${APP_NAME} with friends over a network, or you\ncan all play on the same device if you have enough controllers.\n${APP_NAME} supports a variety of them; you can even use phones\nas controllers via the free '${REMOTE_APP_NAME}' app.\nSee Settings->Controllers for more info.", + "controllersText": "Controllers", + "controlsSubtitleText": "Your friendly ${APP_NAME} character has a few basic actions:", + "controlsText": "Controls", + "devicesInfoText": "The VR version of ${APP_NAME} can be played over the network with\nthe regular version, so whip out your extra phones, tablets,\nand computers and get your game on. It can even be useful to\nconnect a regular version of the game to the VR version just to\nallow people outside to watch the action.", + "devicesText": "Devices", + "friendsGoodText": "These are good to have. ${APP_NAME} is most fun with several\nplayers and can support up to 8 at a time, which leads us to:", + "friendsText": "Friends", + "jumpInfoText": "- Jump -\nJump to cross small gaps,\nto throw things higher, and\nto express feelings of joy.", + "jumpInfoTextScale": 0.6, + "orPunchingSomethingExtraSpace": 0, + "orPunchingSomethingText": "Or punching something, throwing it off a cliff, and blowing it up on the way down with a sticky bomb.", + "pickUpInfoText": "- Pick Up -\nGrab flags, enemies, or anything\nelse not bolted to the ground.\nPress again to throw.", + "pickUpInfoTextScale": 0.6, + "powerupBombDescriptionText": "Lets you whip out three bombs\nin a row instead of just one.", + "powerupBombNameText": "Triple-Bombs", + "powerupCurseDescriptionText": "You probably want to avoid these.\n ...or do you?", + "powerupCurseNameText": "Curse", + "powerupHealthDescriptionText": "Restores you to full health.\nYou'd never have guessed.", + "powerupHealthNameText": "Med-Pack", + "powerupIceBombsDescriptionText": "Weaker than normal bombs\nbut leave your enemies frozen\nand particularly brittle.", + "powerupIceBombsNameText": "Ice-Bombs", + "powerupImpactBombsDescriptionText": "Slightly weaker than regular\nbombs, but they explode on impact.", + "powerupImpactBombsNameText": "Trigger-Bombs", + "powerupLandMinesDescriptionText": "These come in packs of 3;\nUseful for base defense or\nstopping speedy enemies.", + "powerupLandMinesNameText": "Land-Mines", + "powerupPunchDescriptionText": "Makes your punches harder,\nfaster, better, stronger.", + "powerupPunchNameText": "Boxing-Gloves", + "powerupShieldDescriptionText": "Absorbs a bit of damage\nso you don't have to.", + "powerupShieldNameText": "Energy-Shield", + "powerupStickyBombsDescriptionText": "Stick to anything they hit.\nHilarity ensues.", + "powerupStickyBombsNameText": "Sticky-Bombs", + "powerupsSubtitleText": "Of course, no game is complete without powerups:", + "powerupsSubtitleTextScale": 0.8, + "powerupsText": "Powerups", + "punchInfoText": "- Punch -\nPunches do more damage the\nfaster your fists are moving, so\nrun and spin like a madman.", + "punchInfoTextScale": 0.6, + "runInfoText": "- Run -\nHold ANY button to run. Triggers or shoulder buttons work well if you have them.\nRunning gets you places faster but makes it hard to turn, so watch out for cliffs.", + "runInfoTextScale": 0.6, + "someDaysExtraSpace": 0, + "someDaysText": "Some days you just feel like punching something. Or blowing something up.", + "titleText": "${APP_NAME} Help", + "toGetTheMostText": "To get the most out of this game, you'll need:", + "welcomeText": "Welcome to ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} is navigating menus like a boss -", + "importPlaylistCodeInstructionsText": "Use the following code to import this playlist elsewhere:", + "importPlaylistSuccessText": "Imported ${TYPE} playlist '${NAME}'", + "importText": "Import", + "importingText": "Importing...", + "inGameClippedNameText": "in-game will be\n\"${NAME}\"", + "installDiskSpaceErrorText": "ERROR: Unable to complete the install.\nYou may be out of space on your device.\nClear some space and try again.", + "internal": { + "arrowsToExitListText": "press ${LEFT} or ${RIGHT} to exit list", + "buttonText": "button", + "cantKickHostError": "You can't kick the host.", + "chatBlockedText": "${NAME} is chat-blocked for ${TIME} seconds.", + "connectedToGameText": "Joined '${NAME}'", + "connectedToPartyText": "Joined ${NAME}'s party!", + "connectingToPartyText": "Connecting...", + "connectionFailedHostAlreadyInPartyText": "Connection failed; host is in another party.", + "connectionFailedPartyFullText": "Connection failed; the party is full.", + "connectionFailedText": "Connection failed.", + "connectionFailedVersionMismatchText": "Connection failed; host is running a different version of the game.\nMake sure you are both up-to-date and try again.", + "connectionRejectedText": "Connection rejected.", + "controllerConnectedText": "${CONTROLLER} connected.", + "controllerDetectedText": "1 controller detected.", + "controllerDisconnectedText": "${CONTROLLER} disconnected.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} disconnected. Please try connecting again.", + "controllerForMenusOnlyText": "This controller can not be used to play; only to navigate menus.", + "controllerReconnectedText": "${CONTROLLER} reconnected.", + "controllersConnectedText": "${COUNT} controllers connected.", + "controllersDetectedText": "${COUNT} controllers detected.", + "controllersDisconnectedText": "${COUNT} controllers disconnected.", + "corruptFileText": "Corrupt file(s) detected. Please try re-installing, or email ${EMAIL}", + "errorPlayingMusicText": "Error playing music: ${MUSIC}", + "errorResettingAchievementsText": "Unable to reset online achievements; please try again later.", + "hasMenuControlText": "${NAME} has menu control.", + "incompatibleNewerVersionHostText": "Host is running a newer version of the game.\nUpdate to the latest version and try again.", + "incompatibleVersionHostText": "Host is running a different version of the game.\nMake sure you are both up-to-date and try again.", + "incompatibleVersionPlayerText": "${NAME} is running a different version of the game.\nMake sure you are both up-to-date and try again.", + "invalidAddressErrorText": "Error: invalid address.", + "invalidNameErrorText": "Error: invalid name.", + "invalidPortErrorText": "Error: invalid port.", + "invitationSentText": "Invitation sent.", + "invitationsSentText": "${COUNT} invitations sent.", + "joinedPartyInstructionsText": "Someone has joined your party.\nGo to 'Play' to start a game.", + "keyboardText": "Keyboard", + "kickIdlePlayersKickedText": "Kicking ${NAME} for being idle.", + "kickIdlePlayersWarning1Text": "${NAME} will be kicked in ${COUNT} seconds if still idle.", + "kickIdlePlayersWarning2Text": "(you can turn this off in Settings -> Advanced)", + "leftGameText": "Left '${NAME}'.", + "leftPartyText": "Left ${NAME}'s party.", + "noMusicFilesInFolderText": "Folder contains no music files.", + "playerJoinedPartyText": "${NAME} joined the party!", + "playerLeftPartyText": "${NAME} left the party.", + "rejectingInviteAlreadyInPartyText": "Rejecting invite (already in a party).", + "serverRestartingText": "Server is restarting. Please rejoin in a moment...", + "serverShuttingDownText": "Server is shutting down...", + "signInErrorText": "Error signing in.", + "signInNoConnectionText": "Unable to sign in. (no internet connection?)", + "telnetAccessDeniedText": "ERROR: user has not granted telnet access.\n", + "timeOutText": "(times out in ${TIME} seconds)", + "touchScreenJoinWarningText": "You have joined with the touchscreen.\nIf this was a mistake, tap 'Menu->Leave Game' with it.", + "touchScreenText": "TouchScreen", + "unableToResolveHostText": "Error: unable to resolve host.", + "unavailableNoConnectionText": "This is currently unavailable (no internet connection?)", + "vrOrientationResetCardboardText": "Use this to reset the VR orientation.\nTo play the game you'll need an external controller.", + "vrOrientationResetText": "VR orientation reset.", + "willTimeOutText": "(will time out if idle)" + }, + "jumpBoldText": "JUMP", + "jumpText": "Jump", + "keepText": "Keep", + "keepTheseSettingsText": "Keep these settings?", + "keyboardChangeInstructionsText": "Double press space to change keyboards.", + "keyboardNoOthersAvailableText": "No other keyboards available.", + "keyboardSwitchText": "Switching keyboard to \"${NAME}\".", + "kickOccurredText": "${NAME} was kicked.", + "kickQuestionText": "Kick ${NAME}?", + "kickText": "Kick", + "kickVoteCantKickAdminsText": "Admins can't be kicked.", + "kickVoteCantKickSelfText": "You can't kick yourself.", + "kickVoteFailedNotEnoughVotersText": "Not enough players for a vote.", + "kickVoteFailedText": "Kick-vote failed.", + "kickVoteStartedText": "A kick vote has been started for ${NAME}.", + "kickVoteText": "Vote to Kick", + "kickVotingDisabledText": "Kick voting is disabled.", + "kickWithChatText": "Type ${YES} in chat for yes and ${NO} for no.", + "killsTallyText": "${COUNT} kills", + "killsText": "Kills", + "kioskWindow": { + "easyText": "Easy", + "epicModeText": "Epic Mode", + "fullMenuText": "Full Menu", + "hardText": "Hard", + "mediumText": "Medium", + "singlePlayerExamplesText": "Single Player / Co-op Examples", + "versusExamplesText": "Versus Examples" + }, + "languageSetText": "Language is now \"${LANGUAGE}\".", + "lapNumberText": "Lap ${CURRENT}/${TOTAL}", + "lastGamesText": "(last ${COUNT} games)", + "leaderboardsText": "Leaderboards", + "league": { + "allTimeText": "All Time", + "currentSeasonText": "Current Season (${NUMBER})", + "leagueFullText": "${NAME} League", + "leagueRankText": "League Rank", + "leagueText": "League", + "rankInLeagueText": "#${RANK}, ${NAME} League${SUFFIX}", + "seasonEndedDaysAgoText": "Season ended ${NUMBER} days ago.", + "seasonEndsDaysText": "Season ends in ${NUMBER} days.", + "seasonEndsHoursText": "Season ends in ${NUMBER} hours.", + "seasonEndsMinutesText": "Season ends in ${NUMBER} minutes.", + "seasonText": "Season ${NUMBER}", + "tournamentLeagueText": "You must reach ${NAME} league to enter this tournament.", + "trophyCountsResetText": "Trophy counts will reset next season." + }, + "levelBestScoresText": "Best scores on ${LEVEL}", + "levelBestTimesText": "Best times on ${LEVEL}", + "levelIsLockedText": "${LEVEL} is locked.", + "levelMustBeCompletedFirstText": "${LEVEL} must be completed first.", + "levelText": "Level ${NUMBER}", + "levelUnlockedText": "Level Unlocked!", + "livesBonusText": "Lives Bonus", + "loadingText": "loading", + "loadingTryAgainText": "Loading; try again in a moment...", + "macControllerSubsystemBothText": "Both (not recommended)", + "macControllerSubsystemClassicText": "Classic", + "macControllerSubsystemDescriptionText": "(try changing this if your controllers aren't working)", + "macControllerSubsystemMFiNoteText": "Made-for-iOS/Mac controller detected;\nYou may want to enable these in Settings -> Controllers", + "macControllerSubsystemMFiText": "Made-for-iOS/Mac", + "macControllerSubsystemTitleText": "Controller Support", + "mainMenu": { + "creditsText": "Credits", + "demoMenuText": "Demo Menu", + "endGameText": "End Game", + "exitGameText": "Exit Game", + "exitToMenuText": "Exit to menu?", + "howToPlayText": "How to Play", + "justPlayerText": "(Just ${NAME})", + "leaveGameText": "Leave Game", + "leavePartyConfirmText": "Really leave the party?", + "leavePartyText": "Leave Party", + "quitText": "Quit", + "resumeText": "Resume", + "settingsText": "Settings" + }, + "makeItSoText": "Make it So", + "mapSelectGetMoreMapsText": "Get More Maps...", + "mapSelectText": "Select...", + "mapSelectTitleText": "${GAME} Maps", + "mapText": "Map", + "maxConnectionsText": "Max Connections", + "maxPartySizeText": "Max Party Size", + "maxPlayersText": "Max Players", + "modeArcadeText": "Arcade Mode", + "modeClassicText": "Classic Mode", + "modeDemoText": "Demo Mode", + "mostValuablePlayerText": "Most Valuable Player", + "mostViolatedPlayerText": "Most Violated Player", + "mostViolentPlayerText": "Most Violent Player", + "moveText": "Move", + "multiKillText": "${COUNT}-KILL!!!", + "multiPlayerCountText": "${COUNT} players", + "mustInviteFriendsText": "Note: you must invite friends in\nthe \"${GATHER}\" panel or attach\ncontrollers to play multiplayer.", + "nameBetrayedText": "${NAME} betrayed ${VICTIM}.", + "nameDiedText": "${NAME} died.", + "nameKilledText": "${NAME} killed ${VICTIM}.", + "nameNotEmptyText": "Name cannot be empty!", + "nameScoresText": "${NAME} Scores!", + "nameSuicideKidFriendlyText": "${NAME} accidentally died.", + "nameSuicideText": "${NAME} committed suicide.", + "nameText": "Name", + "nativeText": "Native", + "newPersonalBestText": "New personal best!", + "newTestBuildAvailableText": "A newer test build is available! (${VERSION} build ${BUILD}).\nGet it at ${ADDRESS}", + "newText": "New", + "newVersionAvailableText": "A newer version of ${APP_NAME} is available! (${VERSION})", + "nextAchievementsText": "Next Achievements:", + "nextLevelText": "Next Level", + "noAchievementsRemainingText": "- none", + "noContinuesText": "(no continues)", + "noExternalStorageErrorText": "No external storage found on this device", + "noGameCircleText": "Error: not logged into GameCircle", + "noScoresYetText": "No scores yet.", + "noThanksText": "No Thanks", + "noTournamentsInTestBuildText": "WARNING: Tournament scores from this test build will be ignored.", + "noValidMapsErrorText": "No valid maps found for this game type.", + "notEnoughPlayersRemainingText": "Not enough players remaining; exit and start a new game.", + "notEnoughPlayersText": "You need at least ${COUNT} players to start this game!", + "notNowText": "Not Now", + "notSignedInErrorText": "You must sign in to do this.", + "notSignedInGooglePlayErrorText": "You must sign in with Google Play to do this.", + "notSignedInText": "not signed in", + "nothingIsSelectedErrorText": "Nothing is selected!", + "numberText": "#${NUMBER}", + "offText": "Off", + "okText": "Ok", + "onText": "On", + "oneMomentText": "One Moment...", + "onslaughtRespawnText": "${PLAYER} will respawn in wave ${WAVE}", + "orText": "${A} or ${B}", + "otherText": "Other...", + "outOfText": "(#${RANK} out of ${ALL})", + "ownFlagAtYourBaseWarning": "Your own flag must be\nat your base to score!", + "packageModsEnabledErrorText": "Network-play is not allowed while local-package-mods are enabled (see Settings->Advanced)", + "partyWindow": { + "chatMessageText": "Chat Message", + "emptyText": "Your party is empty", + "hostText": "(host)", + "sendText": "Send", + "titleText": "Your Party" + }, + "pausedByHostText": "(paused by host)", + "perfectWaveText": "Perfect Wave!", + "pickUpText": "Pick Up", + "playModes": { + "coopText": "Co-op", + "freeForAllText": "Free-for-All", + "multiTeamText": "Multi-Team", + "singlePlayerCoopText": "Single Player / Co-op", + "teamsText": "Teams" + }, + "playText": "Play", + "playWindow": { + "oneToFourPlayersText": "1-4 players", + "titleText": "Play", + "twoToEightPlayersText": "2-8 players" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerDelayedJoinText": "${PLAYER} will enter at the start of the next round.", + "playerInfoText": "Player Info", + "playerLeftText": "${PLAYER} left the game.", + "playerLimitReachedText": "Player limit of ${COUNT} reached; no joiners allowed.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "You can't delete your account profile.", + "deleteButtonText": "Delete\nProfile", + "deleteConfirmText": "Delete '${PROFILE}'?", + "editButtonText": "Edit\nProfile", + "explanationText": "(custom player names and appearances for this account)", + "newButtonText": "New\nProfile", + "titleText": "Player Profiles" + }, + "playerText": "Player", + "playlistNoValidGamesErrorText": "This playlist contains no valid unlocked games.", + "playlistNotFoundText": "playlist not found", + "playlistText": "Playlist", + "playlistsText": "Playlists", + "pleaseRateText": "If you're enjoying ${APP_NAME}, please consider taking a\nmoment and rating it or writing a review. This provides\nuseful feedback and helps support future development.\n\nthanks!\n-eric", + "pleaseWaitText": "Please wait...", + "pluginsDetectedText": "New plugin(s) detected. Enable/configure them in settings.", + "pluginsText": "Plugins", + "practiceText": "Practice", + "pressAnyButtonPlayAgainText": "Press any button to play again...", + "pressAnyButtonText": "Press any button to continue...", + "pressAnyButtonToJoinText": "press any button to join...", + "pressAnyKeyButtonPlayAgainText": "Press any key/button to play again...", + "pressAnyKeyButtonText": "Press any key/button to continue...", + "pressAnyKeyText": "Press any key...", + "pressJumpToFlyText": "** Press jump repeatedly to fly **", + "pressPunchToJoinText": "press PUNCH to join...", + "pressToOverrideCharacterText": "press ${BUTTONS} to override your character", + "pressToSelectProfileText": "press ${BUTTONS} to select a player", + "pressToSelectTeamText": "press ${BUTTONS} to select a team", + "promoCodeWindow": { + "codeText": "Code", + "enterText": "Enter" + }, + "promoSubmitErrorText": "Error submitting code; check your internet connection", + "ps3ControllersWindow": { + "macInstructionsText": "Switch off the power on the back of your PS3, make sure\nBluetooth is enabled on your Mac, then connect your controller\nto your Mac via a USB cable to pair the two. From then on, you\ncan use the controller's home button to connect it to your Mac\nin either wired (USB) or wireless (Bluetooth) mode.\n\nOn some Macs you may be prompted for a passcode when pairing.\nIf this happens, see the following tutorial or google for help.\n\n\n\n\nPS3 controllers connected wirelessly should show up in the device\nlist in System Preferences->Bluetooth. You may need to remove them\nfrom that list when you want to use them with your PS3 again.\n\nAlso make sure to disconnect them from Bluetooth when not in\nuse or their batteries will continue to drain.\n\nBluetooth should handle up to 7 connected devices,\nthough your mileage may vary.", + "macInstructionsTextScale": 0.74, + "ouyaInstructionsText": "To use a PS3 controller with your OUYA, simply connect it with a USB cable\nonce to pair it. Doing this may disconnect your other controllers, so\nyou should then restart your OUYA and unplug the USB cable.\n\nFrom then on you should be able to use the controller's HOME button to\nconnect it wirelessly. When you are done playing, hold the HOME button\nfor 10 seconds to turn the controller off; otherwise it may remain on\nand waste batteries.", + "ouyaInstructionsTextScale": 0.74, + "pairingTutorialText": "pairing tutorial video", + "titleText": "Using PS3 Controllers with ${APP_NAME}:" + }, + "punchBoldText": "PUNCH", + "punchText": "Punch", + "purchaseForText": "Purchase for ${PRICE}", + "purchaseGameText": "Purchase Game", + "purchasingText": "Purchasing...", + "quitGameText": "Quit ${APP_NAME}?", + "quittingIn5SecondsText": "Quitting in 5 seconds...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Random", + "rankText": "Rank", + "ratingText": "Rating", + "reachWave2Text": "Reach wave 2 to rank.", + "readyText": "ready", + "recentText": "Recent", + "remoteAppInfoShortText": "${APP_NAME} is most fun when played with family & friends.\nConnect one or more hardware controllers or install the\n${REMOTE_APP_NAME} app on phones or tablets to use them\nas controllers.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Button Position", + "button_size": "Button Size", + "cant_resolve_host": "Can't resolve host.", + "capturing": "Capturing…", + "connected": "Connected.", + "description": "Use your phone or tablet as a controller with BombSquad.\nUp to 8 devices can connect at once for epic local multiplayer madness on a single TV or tablet.", + "disconnected": "Disconnected by server.", + "dpad_fixed": "fixed", + "dpad_floating": "floating", + "dpad_position": "D-Pad Position", + "dpad_size": "D-Pad Size", + "dpad_type": "D-Pad Type", + "enter_an_address": "Enter an Address", + "game_full": "The game is full or not accepting connections.", + "game_shut_down": "The game has shut down.", + "hardware_buttons": "Hardware Buttons", + "join_by_address": "Join by Address…", + "lag": "Lag: ${SECONDS} seconds", + "reset": "Reset to default", + "run1": "Run 1", + "run2": "Run 2", + "searching": "Searching for BombSquad games…", + "searching_caption": "Tap on the name of a game to join it.\nMake sure you are on the same wifi network as the game.", + "start": "Start", + "version_mismatch": "Version mismatch.\nMake sure BombSquad and BombSquad Remote\nare the latest versions and try again." + }, + "removeInGameAdsText": "Unlock \"${PRO}\" in the store to remove in-game ads.", + "renameText": "Rename", + "replayEndText": "End Replay", + "replayNameDefaultText": "Last Game Replay", + "replayReadErrorText": "Error reading replay file.", + "replayRenameWarningText": "Rename \"${REPLAY}\" after a game if you want to keep it; otherwise it will be overwritten.", + "replayVersionErrorText": "Sorry, this replay was made in a different\nversion of the game and can't be used.", + "replayWatchText": "Watch Replay", + "replayWriteErrorText": "Error writing replay file.", + "replaysText": "Replays", + "reportPlayerExplanationText": "Use this email to report cheating, inappropriate language, or other bad behavior.\nPlease describe below:", + "reportThisPlayerCheatingText": "Cheating", + "reportThisPlayerLanguageText": "Inappropriate Language", + "reportThisPlayerReasonText": "What would you like to report?", + "reportThisPlayerText": "Report This Player", + "requestingText": "Requesting...", + "restartText": "Restart", + "retryText": "Retry", + "revertText": "Revert", + "runText": "Run", + "saveText": "Save", + "scanScriptsErrorText": "Error(s) scanning scripts; see log for details.", + "scoreChallengesText": "Score Challenges", + "scoreListUnavailableText": "Score list unavailable.", + "scoreText": "Score", + "scoreUnits": { + "millisecondsText": "Milliseconds", + "pointsText": "Points", + "secondsText": "Seconds" + }, + "scoreWasText": "(was ${COUNT})", + "selectText": "Select", + "seriesWinLine1PlayerText": "WINS THE", + "seriesWinLine1TeamText": "WINS THE", + "seriesWinLine1Text": "WINS THE", + "seriesWinLine2Text": "SERIES!", + "settingsWindow": { + "accountText": "Account", + "advancedText": "Advanced", + "audioText": "Audio", + "controllersText": "Controllers", + "graphicsText": "Graphics", + "playerProfilesMovedText": "Note: Player Profiles have moved to the Account window in the main menu.", + "titleText": "Settings" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(a simple, controller-friendly on-screen keyboard for text editing)", + "alwaysUseInternalKeyboardText": "Always Use Internal Keyboard", + "benchmarksText": "Benchmarks & Stress-Tests", + "disableCameraGyroscopeMotionText": "Disable Camera Gyroscope Motion", + "disableCameraShakeText": "Disable Camera Shake", + "disableThisNotice": "(you can disable this notice in advanced settings)", + "enablePackageModsDescriptionText": "(enables extra modding capabilities but disables net-play)", + "enablePackageModsText": "Enable Local Package Mods", + "enterPromoCodeText": "Enter Code", + "forTestingText": "Note: these values are only for testing and will be lost when the app exits.", + "helpTranslateText": "${APP_NAME}'s non-English translations are a community\nsupported effort. If you'd like to contribute or correct\na translation, follow the link below. Thanks in advance!", + "kickIdlePlayersText": "Kick Idle Players", + "kidFriendlyModeText": "Kid-Friendly Mode (reduced violence, etc)", + "languageText": "Language", + "moddingGuideText": "Modding Guide", + "mustRestartText": "You must restart the game for this to take effect.", + "netTestingText": "Network Testing", + "resetText": "Reset", + "showBombTrajectoriesText": "Show Bomb Trajectories", + "showPlayerNamesText": "Show Player Names", + "showUserModsText": "Show Mods Folder", + "titleText": "Advanced", + "translationEditorButtonText": "${APP_NAME} Translation Editor", + "translationFetchErrorText": "translation status unavailable", + "translationFetchingStatusText": "checking translation status...", + "translationInformMe": "Inform me when my language needs updates", + "translationNoUpdateNeededText": "the current language is up to date; woohoo!", + "translationUpdateNeededText": "** the current language needs updates!! **", + "vrTestingText": "VR Testing" + }, + "shareText": "Share", + "sharingText": "Sharing...", + "showText": "Show", + "signInForPromoCodeText": "You must sign in to an account for codes to take effect.", + "signInWithGameCenterText": "To use a Game Center account,\nsign in with the Game Center app.", + "singleGamePlaylistNameText": "Just ${GAME}", + "singlePlayerCountText": "1 player", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Character Selection", + "Chosen One": "Chosen One", + "Epic": "Epic Mode Games", + "Epic Race": "Epic Race", + "FlagCatcher": "Capture the Flag", + "Flying": "Happy Thoughts", + "Football": "Football", + "ForwardMarch": "Assault", + "GrandRomp": "Conquest", + "Hockey": "Hockey", + "Keep Away": "Keep Away", + "Marching": "Runaround", + "Menu": "Main Menu", + "Onslaught": "Onslaught", + "Race": "Race", + "Scary": "King of the Hill", + "Scores": "Score Screen", + "Survival": "Elimination", + "ToTheDeath": "Death Match", + "Victory": "Final Score Screen" + }, + "spaceKeyText": "space", + "statsText": "Stats", + "storagePermissionAccessText": "This requires storage access", + "store": { + "alreadyOwnText": "You already own ${NAME}!", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Removes in-game ads and nag-screens\n• Unlocks more game settings\n• Also includes:", + "buyText": "Buy", + "charactersText": "Characters", + "comingSoonText": "Coming Soon...", + "extrasText": "Extras", + "freeBombSquadProText": "BombSquad is now free, but since you originally purchased it you are\nreceiving the BombSquad Pro upgrade and ${COUNT} tickets as a thank-you.\nEnjoy the new features, and thank you for your support!\n-Eric", + "holidaySpecialText": "Holiday Special", + "howToSwitchCharactersText": "(go to \"${SETTINGS} -> ${PLAYER_PROFILES}\" to assign & customize characters)", + "howToUseIconsText": "(create global player profiles (in the account window) to use these)", + "howToUseMapsText": "(use these maps in your own teams/free-for-all playlists)", + "iconsText": "Icons", + "loadErrorText": "Unable to load page.\nCheck your internet connection.", + "loadingText": "loading", + "mapsText": "Maps", + "miniGamesText": "MiniGames", + "oneTimeOnlyText": "(one time only)", + "purchaseAlreadyInProgressText": "A purchase of this item is already in progress.", + "purchaseConfirmText": "Purchase ${ITEM}?", + "purchaseNotValidError": "Purchase not valid.\nContact ${EMAIL} if this is an error.", + "purchaseText": "Purchase", + "saleBundleText": "Bundle Sale!", + "saleExclaimText": "Sale!", + "salePercentText": "(${PERCENT}% off)", + "saleText": "SALE", + "searchText": "Search", + "teamsFreeForAllGamesText": "Teams / Free-for-All Games", + "totalWorthText": "*** ${TOTAL_WORTH} value! ***", + "upgradeQuestionText": "Upgrade?", + "winterSpecialText": "Winter Special", + "youOwnThisText": "- you own this -" + }, + "storeDescriptionText": "8 Player Party Game Madness!\n\nBlow up your friends (or the computer) in a tournament of explosive mini-games such as Capture-the-Flag, Bomber-Hockey, and Epic-Slow-Motion-Death-Match!\n\nSimple controls and extensive controller support make it easy for up to 8 people to get in on the action; you can even use your mobile devices as controllers via the free ‘BombSquad Remote’ app!\n\nBombs Away!\n\nCheck out www.froemling.net/bombsquad for more info.\n", + "storeDescriptions": { + "blowUpYourFriendsText": "Blow up your friends.", + "competeInMiniGamesText": "Compete in mini-games ranging from racing to flying.", + "customize2Text": "Customize characters, mini-games, and even the soundtrack.", + "customizeText": "Customize characters and create your own mini-game playlists.", + "sportsMoreFunText": "Sports are more fun with explosives.", + "teamUpAgainstComputerText": "Team up against the computer." + }, + "storeText": "Store", + "submitText": "Submit", + "submittingPromoCodeText": "Submitting Code...", + "teamNamesColorText": "Team Names/Colors...", + "telnetAccessGrantedText": "Telnet access enabled.", + "telnetAccessText": "Telnet access detected; allow?", + "testBuildErrorText": "This test build is no longer active; please check for a new version.", + "testBuildText": "Test Build", + "testBuildValidateErrorText": "Unable to validate test build. (no net connection?)", + "testBuildValidatedText": "Test Build Validated; Enjoy!", + "thankYouText": "Thank you for your support! Enjoy the game!!", + "threeKillText": "TRIPLE KILL!!", + "timeBonusText": "Time Bonus", + "timeElapsedText": "Time Elapsed", + "timeExpiredText": "Time Expired", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Tip", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Top Friends", + "tournamentCheckingStateText": "Checking tournament state; please wait...", + "tournamentEndedText": "This tournament has ended. A new one will start soon.", + "tournamentEntryText": "Tournament Entry", + "tournamentResultsRecentText": "Recent Tournament Results", + "tournamentStandingsText": "Tournament Standings", + "tournamentText": "Tournament", + "tournamentTimeExpiredText": "Tournament Time Expired", + "tournamentsText": "Tournaments", + "translations": { + "characterNames": { + "Agent Johnson": null, + "B-9000": null, + "Bernard": null, + "Bones": null, + "Butch": null, + "Easter Bunny": null, + "Flopsy": null, + "Frosty": null, + "Gretel": null, + "Grumbledorf": null, + "Jack Morgan": null, + "Kronk": null, + "Lee": null, + "Lucky": null, + "Mel": null, + "Middle-Man": null, + "Minimus": null, + "Pascal": null, + "Pixel": null, + "Sammy Slam": null, + "Santa Claus": null, + "Snake Shadow": null, + "Spaz": null, + "Taobao Mascot": null, + "Todd McBurton": null, + "Zoe": null, + "Zola": null + }, + "coopLevelNames": { + "${GAME} Training": null, + "Infinite ${GAME}": null, + "Infinite Onslaught": null, + "Infinite Runaround": null, + "Onslaught Training": null, + "Pro ${GAME}": null, + "Pro Football": null, + "Pro Onslaught": null, + "Pro Runaround": null, + "Rookie ${GAME}": null, + "Rookie Football": null, + "Rookie Onslaught": null, + "The Last Stand": null, + "Uber ${GAME}": null, + "Uber Football": null, + "Uber Onslaught": null, + "Uber Runaround": null + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": null, + "Bomb as many targets as you can.": null, + "Carry the flag for ${ARG1} seconds.": null, + "Carry the flag for a set length of time.": null, + "Crush ${ARG1} of your enemies.": null, + "Defeat all enemies.": null, + "Dodge the falling bombs.": null, + "Final glorious epic slow motion battle to the death.": null, + "Gather eggs!": null, + "Get the flag to the enemy end zone.": null, + "How fast can you defeat the ninjas?": null, + "Kill a set number of enemies to win.": null, + "Last one standing wins.": null, + "Last remaining alive wins.": null, + "Last team standing wins.": null, + "Prevent enemies from reaching the exit.": null, + "Reach the enemy flag to score.": null, + "Return the enemy flag to score.": null, + "Run ${ARG1} laps.": null, + "Run ${ARG1} laps. Your entire team has to finish.": null, + "Run 1 lap.": null, + "Run 1 lap. Your entire team has to finish.": null, + "Run real fast!": null, + "Score ${ARG1} goals.": null, + "Score ${ARG1} touchdowns.": null, + "Score a goal.": null, + "Score a touchdown.": null, + "Score some goals.": null, + "Secure all ${ARG1} flags.": null, + "Secure all flags on the map to win.": null, + "Secure the flag for ${ARG1} seconds.": null, + "Secure the flag for a set length of time.": null, + "Steal the enemy flag ${ARG1} times.": null, + "Steal the enemy flag.": null, + "There can be only one.": null, + "Touch the enemy flag ${ARG1} times.": null, + "Touch the enemy flag.": null, + "carry the flag for ${ARG1} seconds": null, + "kill ${ARG1} enemies": null, + "last one standing wins": null, + "last team standing wins": null, + "return ${ARG1} flags": null, + "return 1 flag": null, + "run ${ARG1} laps": null, + "run 1 lap": null, + "score ${ARG1} goals": null, + "score ${ARG1} touchdowns": null, + "score a goal": null, + "score a touchdown": null, + "secure all ${ARG1} flags": null, + "secure the flag for ${ARG1} seconds": null, + "touch ${ARG1} flags": null, + "touch 1 flag": null + }, + "gameNames": { + "Assault": null, + "Capture the Flag": null, + "Chosen One": null, + "Conquest": null, + "Death Match": null, + "Easter Egg Hunt": null, + "Elimination": null, + "Football": null, + "Hockey": null, + "Keep Away": null, + "King of the Hill": null, + "Meteor Shower": null, + "Ninja Fight": null, + "Onslaught": null, + "Race": null, + "Runaround": null, + "Target Practice": null, + "The Last Stand": null + }, + "inputDeviceNames": { + "Keyboard": null, + "Keyboard P2": null + }, + "languages": { + "Arabic": null, + "Belarussian": null, + "Chinese": "Chinese Simplified", + "ChineseTraditional": "Chinese Traditional", + "Croatian": null, + "Czech": null, + "Danish": null, + "Dutch": null, + "English": null, + "Esperanto": null, + "Finnish": null, + "French": null, + "German": null, + "Gibberish": null, + "Greek": null, + "Hindi": null, + "Hungarian": null, + "Indonesian": null, + "Italian": null, + "Japanese": null, + "Korean": null, + "Persian": null, + "Polish": null, + "Portuguese": null, + "Romanian": null, + "Russian": null, + "Serbian": null, + "Slovak": null, + "Spanish": null, + "Swedish": null, + "Turkish": null, + "Ukrainian": null, + "Venetian": null, + "Vietnamese": null + }, + "leagueNames": { + "Bronze": null, + "Diamond": null, + "Gold": null, + "Silver": null + }, + "mapsNames": { + "Big G": null, + "Bridgit": null, + "Courtyard": null, + "Crag Castle": null, + "Doom Shroom": null, + "Football Stadium": null, + "Happy Thoughts": null, + "Hockey Stadium": null, + "Lake Frigid": null, + "Monkey Face": null, + "Rampage": null, + "Roundabout": null, + "Step Right Up": null, + "The Pad": null, + "Tip Top": null, + "Tower D": null, + "Zigzag": null + }, + "playlistNames": { + "Just Epic": null, + "Just Sports": null + }, + "scoreNames": { + "Flags": null, + "Goals": null, + "Score": null, + "Survived": null, + "Time": null, + "Time Held": null + }, + "serverResponses": { + "A code has already been used on this account.": null, + "A reward has already been given for that address.": null, + "Account linking successful!": null, + "Account unlinking successful!": null, + "Accounts are already linked.": null, + "An error has occurred; (${ERROR})": null, + "An error has occurred; please contact support. (${ERROR})": null, + "An error has occurred; please contact support@froemling.net.": null, + "An error has occurred; please try again later.": null, + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": null, + "BombSquad Pro unlocked!": null, + "Can't link 2 accounts of this type.": null, + "Can't link 2 diamond league accounts.": null, + "Can't link; would surpass maximum of ${COUNT} linked accounts.": null, + "Cheating detected; scores and prizes suspended for ${COUNT} days.": null, + "Could not establish a secure connection.": null, + "Daily maximum reached.": null, + "Entering tournament...": null, + "Invalid code.": null, + "Invalid payment; purchase canceled.": null, + "Invalid promo code.": null, + "Invalid purchase.": null, + "Invalid tournament entry; score will be ignored.": null, + "Item unlocked!": null, + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": null, + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": null, + "Max number of playlists reached.": null, + "Max number of profiles reached.": null, + "Maximum friend code rewards reached.": null, + "Message is too long.": null, + "Profile \"${NAME}\" upgraded successfully.": null, + "Profile could not be upgraded.": null, + "Purchase successful!": null, + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": null, + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": null, + "Sorry, there are no uses remaining on this code.": null, + "Sorry, this code has already been used.": null, + "Sorry, this code has expired.": null, + "Sorry, this code only works for new accounts.": null, + "Temporarily unavailable; please try again later.": null, + "The tournament ended before you finished.": null, + "This account cannot be unlinked for ${NUM} days.": null, + "This code cannot be used on the account that created it.": null, + "This is currently unavailable; please try again later.": null, + "This requires version ${VERSION} or newer.": null, + "Tournaments disabled due to rooted device.": null, + "Tournaments require ${VERSION} or newer": null, + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": null, + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": null, + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": null, + "You already own this!": null, + "You can join in ${COUNT} seconds.": null, + "You don't have enough tickets for this!": null, + "You don't own that.": null, + "You got ${COUNT} tickets!": null, + "You got a ${ITEM}!": null, + "You have been promoted to a new league; congratulations!": null, + "You must update to a newer version of the app to do this.": null, + "You must update to the newest version of the game to do this.": null, + "You must wait a few seconds before entering a new code.": null, + "You ranked #${RANK} in the last tournament. Thanks for playing!": null, + "Your account was rejected. Are you signed in?": null, + "Your copy of the game has been modified.\nPlease revert any changes and try again.": null, + "Your friend code was used by ${ACCOUNT}": null + }, + "settingNames": { + "1 Minute": null, + "1 Second": null, + "10 Minutes": null, + "2 Minutes": null, + "2 Seconds": null, + "20 Minutes": null, + "4 Seconds": null, + "5 Minutes": null, + "8 Seconds": null, + "Allow Negative Scores": null, + "Balance Total Lives": null, + "Bomb Spawning": null, + "Chosen One Gets Gloves": null, + "Chosen One Gets Shield": null, + "Chosen One Time": null, + "Enable Impact Bombs": null, + "Enable Triple Bombs": null, + "Entire Team Must Finish": null, + "Epic Mode": null, + "Flag Idle Return Time": null, + "Flag Touch Return Time": null, + "Hold Time": null, + "Kills to Win Per Player": null, + "Laps": null, + "Lives Per Player": null, + "Long": null, + "Longer": null, + "Mine Spawning": null, + "No Mines": null, + "None": null, + "Normal": null, + "Pro Mode": null, + "Respawn Times": null, + "Score to Win": null, + "Short": null, + "Shorter": null, + "Solo Mode": null, + "Target Count": null, + "Time Limit": null + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": null, + "Killing ${NAME} for skipping part of the track!": null, + "Warning to ${NAME}: turbo / button-spamming knocks you out.": null + }, + "teamNames": { + "Bad Guys": null, + "Blue": null, + "Good Guys": null, + "Red": null + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": null, + "Always remember to floss.": null, + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": null, + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": null, + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": null, + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": null, + "Don't run all the time. Really. You will fall off cliffs.": null, + "Don't spin for too long; you'll become dizzy and fall.": null, + "Hold any button to run. (Trigger buttons work well if you have them)": null, + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": null, + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": null, + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": null, + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": null, + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": null, + "If you kill an enemy in one hit you get double points for it.": null, + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": null, + "If you stay in one place, you're toast. Run and dodge to survive..": null, + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": null, + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": null, + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": null, + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": null, + "In hockey, you'll maintain more speed if you turn gradually.": null, + "It's easier to win with a friend or two helping.": null, + "Jump just as you're throwing to get bombs up to the highest levels.": null, + "Land-mines are a good way to stop speedy enemies.": null, + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": null, + "No, you can't get up on the ledge. You have to throw bombs.": null, + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": null, + "Practice using your momentum to throw bombs more accurately.": null, + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": null, + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": null, + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": null, + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": null, + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": null, + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": null, + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": null, + "Try 'Cooking off' bombs for a second or two before throwing them.": null, + "Try tricking enemies into killing eachother or running off cliffs.": null, + "Use the pick-up button to grab the flag < ${PICKUP} >": null, + "Whip back and forth to get more distance on your throws..": null, + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": null, + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": null, + "You can throw bombs higher if you jump just before throwing.": null, + "You take damage when you whack your head on things,\nso try to not whack your head on things.": null, + "Your punches do much more damage if you are running or spinning.": null + } + }, + "trophiesRequiredText": "This requires at least ${NUMBER} trophies.", + "trophiesText": "Trophies", + "trophiesThisSeasonText": "Trophies This Season", + "tutorial": { + "cpuBenchmarkText": "Running tutorial at ludicrous-speed (primarily tests CPU speed)", + "phrase01Text": "Hi there!", + "phrase02Text": "Welcome to ${APP_NAME}!", + "phrase03Text": "Here's a few tips for controlling your character:", + "phrase04Text": "Many things in ${APP_NAME} are PHYSICS based.", + "phrase05Text": "For example, when you punch,..", + "phrase06Text": "..damage is based on the speed of your fists.", + "phrase07Text": "See? We weren't moving, so that barely hurt ${NAME}.", + "phrase08Text": "Now let's jump and spin to get more speed.", + "phrase09Text": "Ah, that's better.", + "phrase10Text": "Running helps too.", + "phrase11Text": "Hold down ANY button to run.", + "phrase12Text": "For extra-awesome punches, try running AND spinning.", + "phrase13Text": "Whoops; sorry 'bout that ${NAME}.", + "phrase14Text": "You can pick up and throw things such as flags.. or ${NAME}.", + "phrase15Text": "Lastly, there's bombs.", + "phrase16Text": "Throwing bombs takes practice.", + "phrase17Text": "Ouch! Not a very good throw.", + "phrase18Text": "Moving helps you throw farther.", + "phrase19Text": "Jumping helps you throw higher.", + "phrase20Text": "\"Whiplash\" your bombs for even longer throws.", + "phrase21Text": "Timing your bombs can be tricky.", + "phrase22Text": "Dang.", + "phrase23Text": "Try \"cooking off\" the fuse for a second or two.", + "phrase24Text": "Hooray! Nicely cooked.", + "phrase25Text": "Well, that's just about it.", + "phrase26Text": "Now go get 'em, tiger!", + "phrase27Text": "Remember your training, and you WILL come back alive!", + "phrase28Text": "...well, maybe...", + "phrase29Text": "Good luck!", + "randomName1Text": "Fred", + "randomName2Text": "Harry", + "randomName3Text": "Bill", + "randomName4Text": "Chuck", + "randomName5Text": "Phil", + "skipConfirmText": "Really skip the tutorial? Tap or press to confirm.", + "skipVoteCountText": "${COUNT}/${TOTAL} skip votes", + "skippingText": "skipping tutorial...", + "toSkipPressAnythingText": "(tap or press anything to skip tutorial)" + }, + "twoKillText": "DOUBLE KILL!", + "unavailableText": "unavailable", + "unconfiguredControllerDetectedText": "Unconfigured controller detected:", + "unlockThisInTheStoreText": "This must be unlocked in the store.", + "unlockThisProfilesText": "To create more than ${NUM} profiles, you need:", + "unlockThisText": "To unlock this, you need:", + "unsupportedHardwareText": "Sorry, this hardware is not supported by this build of the game.", + "upFirstText": "Up first:", + "upNextText": "Up next in game ${COUNT}:", + "updatingAccountText": "Updating your account...", + "upgradeText": "Upgrade", + "upgradeToPlayText": "Unlock \"${PRO}\" in the in-game store to play this.", + "useDefaultText": "Use Default", + "usesExternalControllerText": "This game uses an external controller for input.", + "usingItunesText": "Using Music App for soundtrack...", + "validatingTestBuildText": "Validating Test Build...", + "victoryText": "Victory!", + "voteDelayText": "You can't start another vote for ${NUMBER} seconds", + "voteInProgressText": "A vote is already in progress.", + "votedAlreadyText": "You already voted", + "votesNeededText": "${NUMBER} votes needed", + "vsText": "vs.", + "waitingForHostText": "(waiting for ${HOST} to continue)", + "waitingForPlayersText": "waiting for players to join...", + "waitingInLineText": "Waiting in line (party is full)...", + "watchAVideoText": "Watch a Video", + "watchAnAdText": "Watch an Ad", + "watchWindow": { + "deleteConfirmText": "Delete \"${REPLAY}\"?", + "deleteReplayButtonText": "Delete\nReplay", + "myReplaysText": "My Replays", + "noReplaySelectedErrorText": "No Replay Selected", + "playbackSpeedText": "Playback Speed: ${SPEED}", + "renameReplayButtonText": "Rename\nReplay", + "renameReplayText": "Rename \"${REPLAY}\" to:", + "renameText": "Rename", + "replayDeleteErrorText": "Error deleting replay.", + "replayNameText": "Replay Name", + "replayRenameErrorAlreadyExistsText": "A replay with that name already exists.", + "replayRenameErrorInvalidName": "Can't rename replay; invalid name.", + "replayRenameErrorText": "Error renaming replay.", + "sharedReplaysText": "Shared Replays", + "titleText": "Watch", + "watchReplayButtonText": "Watch\nReplay" + }, + "waveText": "Wave", + "wellSureText": "Well Sure!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Listening For Wiimotes...", + "pressText": "Press Wiimote buttons 1 and 2 simultaneously.\n", + "pressText2": "On newer Wiimotes with Motion Plus built in, press the red 'sync' button on the back instead." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "listenText": "Listen", + "macInstructionsText": "Make sure your Wii is off and Bluetooth is enabled\non your Mac, then press 'Listen'. Wiimote support can\nbe a bit flaky, so you may have to try a few times\nbefore you get a connection.\n\nBluetooth should handle up to 7 connected devices,\nthough your mileage may vary.\n\nBombSquad supports the original Wiimotes, Nunchuks,\nand the Classic Controller.\nThe newer Wii Remote Plus now works too\nbut not with attachments.", + "thanksText": "Thanks to the DarwiinRemote team\nFor making this possible.\n", + "titleText": "Wiimote Setup" + }, + "winsPlayerText": "${NAME} Wins!", + "winsTeamText": "${NAME} Wins!", + "winsText": "${NAME} Wins!", + "worldScoresUnavailableText": "World scores unavailable.", + "worldsBestScoresText": "World's Best Scores", + "worldsBestTimesText": "World's Best Times", + "xbox360ControllersWindow": { + "getDriverText": "Get Driver", + "macInstructions2Text": "To use controllers wirelessly, you'll also need the receiver that\ncomes with the 'Xbox 360 Wireless Controller for Windows'.\nOne receiver allows you to connect up to 4 controllers.\n\nImportant: 3rd-party receivers will not work with this driver;\nmake sure your receiver says 'Microsoft' on it, not 'XBOX 360'.\nMicrosoft no longer sells these separately, so you'll need to get\nthe one bundled with the controller or else search ebay.\n\nIf you find this useful, please consider a donation to the\ndriver developer at his site.", + "macInstructionsText": "To use Xbox 360 controllers, you'll need to install\nthe Mac driver available at the link below.\nIt works with both wired and wireless controllers.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "To use wired Xbox 360 controllers with BombSquad, simply\nplug them into your device's USB port. You can use a USB hub\nto connect multiple controllers.\n\nTo use wireless controllers you'll need a wireless receiver,\navailable as part of the \"Xbox 360 wireless Controller for Windows\"\npackage or sold separately. Each receiver plugs into a USB port and\nallows you to connect up to 4 wireless controllers.", + "titleText": "Using Xbox 360 Controllers with ${APP_NAME}:" + }, + "yesAllowText": "Yes, Allow!", + "yourBestScoresText": "Your Best Scores", + "yourBestTimesText": "Your Best Times" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/esperanto.json b/dist/ba_data/data/languages/esperanto.json new file mode 100644 index 0000000..d7abb9d --- /dev/null +++ b/dist/ba_data/data/languages/esperanto.json @@ -0,0 +1,1527 @@ +{ + "accountSettingsWindow": { + "accountProfileText": "(konta profilo)", + "achievementProgressText": "Atingoj: ${COUNT} el ${TOTAL}", + "campaignProgressText": "Kampanja progreso: ${PROGRESS}", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Ĉi tio reigos vian kunlaboran progreson\nkaj lokajn plej altajn poentojn (sed ne\nviajn biletojn). Ne eblas malfari.\nĈu vi certas?", + "resetProgressConfirmText": "Ĉi tio reigos vian kunlaboran\nprogreson, atingojn kaj lokajn\nplej altajn poentojn (sed ne\nviajn biletojn). Ne eblas malfari.\nĈu vi certas?", + "resetProgressText": "Reigi progreson", + "signInInfoText": "Ensalutu por konservi vian progreson\nen la nubon, gajni biletojn kaj\npartopreni en turniroj.", + "signInText": "Ensaluti", + "signOutText": "Elsaluti", + "signingInText": "Ensalutas...", + "signingOutText": "Elsalutas...", + "testAccountWarningCardboardText": "Atentu: vi ensalutis per testa konto.\nĜin anstataŭos Google-konto kiam ili haveblos\npor cardboard-aplikaĵoj.\n\nPor nun vi devos akiri ĉiujn biletojn enlude.\n(tamen, vi ricevas la ĝisdatigon BombSquad Pro senpage)", + "testAccountWarningOculusText": "Atentu: vi ensalutiĝis per test-konto.\nPli poste ĉi-jare tion anstataŭos Oculus-kontoj,\ndonante aĉetadon de biletoj kaj aliaj ebloj.\n\nPor nun vi devos gajni biletojn enlude.\n(tamen, vi havos la ĝisdatigon Bombsquad Pro senpage)", + "testAccountWarningText": "Atentu: vi estas ensalutita per test-konto.\nĈi tiu konto estas specifa por ĉi tiu aparato kaj\npovas esti fojfoje reigata. (do ne dum multa tempo\nkolektu kaj malŝlosu aferojn per ĝi)\n\nUzu vendo-version de la ludo por uzi \"veran\" konton\n(Game-Center, Google Plus ktp). Tio ankaŭ permesas\nal vi konservi progreson en la nubon kaj kunhavigi\ntion inter diversaj aparatoj.", + "ticketsText": "Biletoj: ${COUNT}", + "titleText": "Konto", + "youAreLoggedInAsText": "Vi ensalutis kiel:", + "youAreSignedInAsText": "Vi ensalutiĝis kiel:" + }, + "achievementChallengesText": "Atingaj defioj", + "achievementText": "Atingo", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Mortigu 3 fiulojn per dinamito", + "descriptionComplete": "Mortigis 3 fiulojn per dinamito", + "descriptionFull": "Mortigu 3 fiulojn per dinamito en ${LEVEL}", + "descriptionFullComplete": "Mortigis 3 fiulojn per dinamito en ${LEVEL}", + "name": "Bum-as la dinamito" + }, + "Boxer": { + "description": "Venku sen uzi bombojn", + "descriptionComplete": "Venkis sen uzi bombojn", + "descriptionFull": "Kompletigu ${LEVEL} sen uzi bombojn", + "descriptionFullComplete": "Kompletigis ${LEVEL} sen uzi bombojn", + "name": "Batadanto" + }, + "Flawless Victory": { + "description": "Venku sen esti batata", + "descriptionComplete": "Venkis sen esti batata", + "descriptionFull": "Venku ${LEVEL} sen esti batata", + "descriptionFullComplete": "Venkis ${LEVEL} sen esti batata", + "name": "Perfekta venko" + }, + "Gold Miner": { + "description": "Mortigu 6 fiulojn per minoj", + "descriptionComplete": "Mortigis 6 fiulojn per minoj", + "descriptionFull": "Mortigu 6 fiulojn per minoj en ${LEVEL}", + "descriptionFullComplete": "Mortigis 6 fiulojn per minoj en ${LEVEL}", + "name": "Orministo" + }, + "Got the Moves": { + "description": "Venku sen uzi batojn aŭ bombojn", + "descriptionComplete": "Venkis sen uzi batojn aŭ bombojn", + "descriptionFull": "Venku ${LEVEL} sen uzi batojn aŭ bombojn", + "descriptionFullComplete": "Venkis ${LEVEL} sen uzi batojn aŭ bombojn", + "name": "Movadano" + }, + "Last Stand God": { + "description": "Poentu 1000 poentojn", + "descriptionComplete": "Poentis 1000 poentojn", + "descriptionFull": "Poentu 1000 poentojn en ${LEVEL}", + "descriptionFullComplete": "Poentis 1000 poentojn en ${LEVEL}", + "name": "Dio de ${LEVEL}" + }, + "Last Stand Master": { + "description": "Poentu 250 poentojn", + "descriptionComplete": "Poentis 250 poentojn", + "descriptionFull": "Poentu 250 poentojn en ${LEVEL}", + "descriptionFullComplete": "Poentis 250 poentojn en ${LEVEL}", + "name": "Majstro de ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Poentu 500 poentojn", + "descriptionComplete": "Poentis 500 poentojn", + "descriptionFull": "Poentu 500 poentojn en ${LEVEL}", + "descriptionFullComplete": "Poentis 500 poentojn en ${LEVEL}", + "name": "Sorĉisto de ${LEVEL}" + }, + "Mine Games": { + "description": "Mortigu 3 fiulojn per minoj", + "descriptionComplete": "Mortigis 3 fiulojn per minoj", + "descriptionFull": "Mortigu 3 fiulojn per minoj en ${LEVEL}", + "descriptionFullComplete": "Mortigis 3 fiulojn per minoj en ${LEVEL}", + "name": "Minoludoj" + }, + "Off You Go Then": { + "description": "Ĵetu 3 fiulojn ekster la mapon", + "descriptionComplete": "Ĵetis 3 fiulojn ekster la mapon", + "descriptionFull": "Ĵetu 3 fiulojn ekster la mapon en ${LEVEL}", + "descriptionFullComplete": "Ĵetis 3 fiulojn ekster la mapon en ${LEVEL}", + "name": "Do jen vi iras" + }, + "Onslaught God": { + "description": "Poentu 5000 poentojn", + "descriptionComplete": "Poentis 5000 poentojn", + "descriptionFull": "Poentu 5000 poentojn en ${LEVEL}", + "descriptionFullComplete": "Poentis 5000 poentojn en ${LEVEL}", + "name": "Dio de ${LEVEL}" + }, + "Onslaught Master": { + "description": "Poentu 500 poentojn", + "descriptionComplete": "Poentis 500 poentojn", + "descriptionFull": "Poentu 500 poentojn en ${LEVEL}", + "descriptionFullComplete": "Poentis 500 poentojn en ${LEVEL}", + "name": "Majstro de ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Superu ĉiujn ondojn", + "descriptionComplete": "Superis ĉiujn ondojn", + "descriptionFull": "Superu ĉiujn ondojn en ${LEVEL}", + "descriptionFullComplete": "Superis ĉiujn ondojn en ${LEVEL}", + "name": "Venko de ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Poentu 1000 poentojn", + "descriptionComplete": "Poentis 1000 poentojn", + "descriptionFull": "Poentu 1000 poentojn en ${LEVEL}", + "descriptionFullComplete": "Poentis 1000 poentojn en ${LEVEL}", + "name": "Sorĉisto de ${LEVEL}" + }, + "Precision Bombing": { + "description": "Venku sen donacetojn", + "descriptionComplete": "Venkis sen donacetoj", + "descriptionFull": "Venku ${LEVEL} sen donacetoj", + "descriptionFullComplete": "Venkis ${LEVEL} sen donacetoj", + "name": "Precizbombado" + }, + "Pro Boxer": { + "description": "Venku sen uzi bombojn", + "descriptionComplete": "Venkis sen uzi bombojn", + "descriptionFull": "Kompletigu ${LEVEL} sen uzi bombojn", + "descriptionFullComplete": "Kompletigis ${LEVEL} sen uzi bombojn", + "name": "Profesia batadanto" + }, + "Pro Football Shutout": { + "description": "Venku sen ke la fiuloj poentu", + "descriptionComplete": "Venkis sen poentoj por la fiuloj", + "descriptionFull": "Venku ${Level} sen ke la fiuloj poentu", + "descriptionFullComplete": "Venkis ${LEVEL} sen poentoj por la fiuloj", + "name": "Eltenanto de ${LEVEL}" + }, + "Pro Football Victory": { + "description": "Venku la ludon", + "descriptionComplete": "Venkis la ludon", + "descriptionFull": "Venku la ludon en ${LEVEL}", + "descriptionFullComplete": "Venkis la ludon en ${LEVEL}", + "name": "Venko de ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Superu ĉiujn ondojn", + "descriptionComplete": "Superis ĉiujn ondojn", + "descriptionFull": "Superu ĉiujn ondojn de ${LEVEL}", + "descriptionFullComplete": "Superis ĉiujn ondojn de ${LEVEL}", + "name": "Venko de ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Kompletigu ĉiujn ondojn", + "descriptionComplete": "Kompletigis ĉiujn ondojn", + "descriptionFull": "Kompletigu ĉiujn ondojn en ${LEVEL}", + "descriptionFullComplete": "Kompletigis ĉiujn ondojn en ${LEVEL}", + "name": "Venko de ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Venku sen ke la fiuloj poentu", + "descriptionComplete": "Venkis sen poentoj por la fiuloj", + "descriptionFull": "Venku ${LEVEL} sen ke la fiuloj poentu", + "descriptionFullComplete": "Venkis ${LEVEL} sen poentoj por la fiuloj", + "name": "Eltenanto de ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Venku la ludon", + "descriptionComplete": "Venkis la ludon", + "descriptionFull": "Venku la ludon en ${LEVEL}", + "descriptionFullComplete": "Venkis la ludon en ${LEVEL}", + "name": "Venko de ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Superu ĉiujn ondojn", + "descriptionComplete": "Superis ĉiujn ondojn", + "descriptionFull": "Superu ĉiujn ondojn en ${LEVEL}", + "descriptionFullComplete": "Superis ĉiujn ondojn en ${LEVEL}", + "name": "Venko de ${LEVEL}" + }, + "Runaround God": { + "description": "Poenti 2000 poentojn", + "descriptionComplete": "Poentis 2000 poentojn", + "descriptionFull": "Poentu 2000 poentojn en ${LEVEL}", + "descriptionFullComplete": "Poentis 2000 poentojn en ${LEVEL}", + "name": "Dio de ${LEVEL}" + }, + "Runaround Master": { + "description": "Poentu 500 poentojn", + "descriptionComplete": "Poentis 500 poentojn", + "descriptionFull": "Poentu 500 poentojn en ${LEVEL}", + "descriptionFullComplete": "Poentis 500 poentojn en ${LEVEL}", + "name": "Majstro de ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Poentu 1000 poentojn", + "descriptionComplete": "Poentis 1000 poentojn", + "descriptionFull": "Poentu 1000 poentojn en ${LEVEL}", + "descriptionFullComplete": "Poentis 1000 poentojn en ${LEVEL}", + "name": "Sorĉisto de ${LEVEL}" + }, + "Stayin' Alive": { + "description": "Venku sen morti", + "descriptionComplete": "Venkis sen morti", + "descriptionFull": "Venku ${LEVEL} sen morti", + "descriptionFullComplete": "Venkis ${LEVEL} sen morti", + "name": "Resti viva" + }, + "Super Mega Punch": { + "description": "Efiku 100% da damaĝo per unu bato", + "descriptionComplete": "Efikis 100% da damaĝo per unu bato", + "descriptionFull": "Efiku 100% da damaĝo per unu bato en ${LEVEL}", + "descriptionFullComplete": "Efikis 100% da damaĝo per unu bato en ${LEVEL}", + "name": "Superbatego" + }, + "Super Punch": { + "description": "Efiku 50% da damaĝo per unu bato", + "descriptionComplete": "Efikis 50% da damaĝo per unu bato", + "descriptionFull": "Efiku 50% da damaĝo per unu bato en ${LEVEL}", + "descriptionFullComplete": "Efikis 50% da damaĝo per unu bato en ${LEVEL}", + "name": "Superbato" + }, + "TNT Terror": { + "description": "Mortigu 6 fiulojn per dinamito", + "descriptionComplete": "Mortigis 6 fiulojn per dinamito", + "descriptionFull": "Mortigu 6 fiulojn per dinamito en ${LEVEL}", + "descriptionFullComplete": "Mortigis 6 fiulojn per dinamito en ${LEVEL}", + "name": "Dinamitoteroro" + }, + "The Great Wall": { + "description": "Haltigu ĉiun fiulon", + "descriptionComplete": "Haltigis ĉiun fiulon", + "descriptionFull": "Haltigu ĉiun fiulon en ${LEVEL}", + "descriptionFullComplete": "Haltigis ĉiun fiulon en ${LEVEL}", + "name": "La granda muro" + }, + "The Wall": { + "description": "Haltigu ĉiun fiulon", + "descriptionComplete": "Haltigis ĉiun fiulon", + "descriptionFull": "Haltigu ĉiun fiulon en ${LEVEL}", + "descriptionFullComplete": "Haltigis ĉiun fiulon en ${LEVEL}", + "name": "La muro" + }, + "Uber Football Shutout": { + "description": "Venku sen ke la fiuloj poentu", + "descriptionComplete": "Venkis sen poentoj por la fiuloj", + "descriptionFull": "Venku ${LEVEL} sen ke la fiuloj poentu", + "descriptionFullComplete": "Venkis ${LEVEL} sen poentoj por la fiuloj", + "name": "Eltenanto de ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Venku la ludon", + "descriptionComplete": "Venkis la ludon", + "descriptionFull": "Venku la ludon en ${LEVEL}", + "descriptionFullComplete": "Venkis la ludon en ${LEVEL}", + "name": "Venko de ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Superu ĉiujn ondojn", + "descriptionComplete": "Superis ĉiujn ondojn", + "descriptionFull": "Superu ĉiujn ondojn en ${LEVEL}", + "descriptionFullComplete": "Superis ĉiujn ondojn en ${LEVEL}", + "name": "Venko de ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Kompletigu ĉiujn ondojn", + "descriptionComplete": "Kompletigis ĉiujn ondojn", + "descriptionFull": "Kompletigu ĉiujn ondojn en ${LEVEL}", + "descriptionFullComplete": "Kompletigis ĉiujn ondojn en ${LEVEL}", + "name": "Venko de ${LEVEL}" + } + }, + "achievementsRemainingText": "Restantaj atingoj:", + "achievementsText": "Atingoj", + "achievementsUnavailableForOldSeasonsText": "Pardonu, atingaj specifaĵoj ne haveblas por malnovaj sezonoj.", + "addGameWindow": { + "getMoreGamesText": "Pli da ludoj...", + "titleText": "Aldoni ludon" + }, + "allowText": "Permesi", + "apiVersionErrorText": "Ne eblas ŝargi modulon ${NAME}; ĝi celas api-version ${VERSION_USED}; ni bezonas ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Aŭtomate\" nur ebligas tion kiam kapaŭskultiloj enestas)", + "headRelativeVRAudioText": "Kap-rilata VR-aŭdsistemo", + "musicVolumeText": "Laŭteco de muziko", + "soundVolumeText": "Sonlaŭteco", + "soundtrackButtonText": "Sontrakoj", + "soundtrackDescriptionText": "(elektu vian propran muzikon por ludi dum ludoj)", + "titleText": "Aŭdaĵoj" + }, + "autoText": "Aŭtomate", + "backText": "Reen", + "bestOfFinalText": "Plejbona-el-${COUNT}-finalo", + "bestOfSeriesText": "Plejbona el ${COUNT} serio:", + "bestRankText": "Via plejbono estas #${RANK}", + "bestRatingText": "Via plej bona ranko estas ${RATING}", + "betaErrorText": "Ĉi tiu beta ne plu estas aktiva; bonvolu kontroli por pli nova versio.", + "betaValidateErrorText": "Ne eblas validigi betan. (ĉu sen retkonekto?)", + "betaValidatedText": "Beta validiĝis; Ĝuu!", + "bombBoldText": "BOMBO", + "bombText": "Bombo", + "bsRemoteConfigureInAppText": "BombSquad teleregilo estas konfigurita en la aplikaĵo mem.", + "buttonText": "butono", + "canWeDebugText": "Ĉu vi ŝatus ke BombSquad aŭtomate raportu cimojn,\nkraŝojn kaj bazajn uzadajn informojn al la programisto?\n\nĈi tiuj datumoj ne enhavas personajn informojn kaj helpas\npor manteni la ludon flua kaj sencima.", + "cancelText": "Nuligi", + "cantConfigureDeviceText": "Bedaŭrinde ${DEVICE} ne konfigureblas.", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Vi devas kompletigi ĉi\ntiun nivelon por pluiri!", + "completionBonusText": "Kompletiga premio", + "configControllersWindow": { + "configureControllersText": "Konfiguri regilojn", + "configureGamepadsText": "Agordi ludregtabulojn", + "configureKeyboard2Text": "Konfiguri klavaron Lud2", + "configureKeyboardText": "Konfiguri klavaron", + "configureMobileText": "Poŝaparatoj kiel regiloj", + "configureTouchText": "Konfiguri tuŝekranon", + "ps3Text": "PS3-regiloj", + "titleText": "Ludregiloj", + "wiimotesText": "Wii-regiloj", + "xbox360Text": "Regiloj de Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Notu: regila subteno varias laŭ aparato kaj versio de Android.", + "pressAnyButtonText": "Premu ajnan butonon sur la regilo\n kiun vi volas konfigurigi...", + "titleText": "Konfiguri regilojn" + }, + "configGamepadWindow": { + "advancedText": "Por spertuloj", + "advancedTitleText": "Pli specifaj regilaj agordoj", + "analogStickDeadZoneDescriptionText": "(altigu ĉi tion se via rolulo 'ŝoviĝas' post stirado per la bastoneto)", + "analogStickDeadZoneText": "Analogbastoneta morta regiono", + "appliesToAllText": "(aplikiĝas al ĉiuj regiloj de ĉi tiu tipo)", + "autoRecalibrateDescriptionText": "(ebligu ĉi tion se via rolulo ne moviĝas je plena rapideco)", + "autoRecalibrateText": "Aŭtomate rekalibrigi analogan bastoneton", + "axisText": "akso", + "clearText": "viŝi", + "dpadText": "stirkruceto", + "extraStartButtonText": "Aldona start-butono", + "ifNothingHappensTryAnalogText": "Se nenio okazas, provu anstataŭe konfiguri per la analoga bastoneto.", + "ifNothingHappensTryDpadText": "Se nenio okazas, provu anstataŭe konfiguri per la stirkruceto.", + "ignoredButton1Text": "Ignorata butono 1", + "ignoredButton2Text": "Ignorata butono 2", + "ignoredButton3Text": "Ignorata butono 3", + "ignoredButtonDescriptionText": "(uzu ĉi tion por eviti ke butonoj 'home' aŭ 'sync' efiku al la interfaco)", + "ignoredButtonText": "Ignorata butono", + "pressAnyAnalogTriggerText": "Premu ajnan analogan klavaĵon...", + "pressAnyButtonOrDpadText": "Premu ajnan butonon aŭ stirkruceton...", + "pressAnyButtonText": "Premu ajnan butonon...", + "pressLeftRightText": "Premu dekstren aŭ maldekstren...", + "pressUpDownText": "Premu supren aŭ malsupren...", + "runButton1Text": "Kurbutono 1", + "runButton2Text": "Kurbutono 2", + "runTrigger1Text": "Kurklavaĵo 1", + "runTrigger2Text": "Kurklavaĵo 2", + "runTriggerDescriptionText": "(analogaj klavaĵoj ebligas al vi kuri laŭ diversaj rapidecoj)", + "secondHalfText": "Uzu ĉi tion por konfiguri duan duonon\nde 2-reguloj-en-1-aparato kiu montriĝas\nkiel ununura regilo.", + "secondaryEnableText": "Ebligi", + "secondaryText": "Aldona regilo", + "startButtonActivatesDefaultDescriptionText": "(malŝaltu ĉi tion se via startbutono estas fakte menubutono)", + "startButtonActivatesDefaultText": "Startbutono aktivigas defaŭltan umaĵon", + "titleText": "Regilaj agordoj", + "twoInOneSetupText": "Agordoj por regilo 2-en-1", + "unassignedButtonsRunText": "Ĉiuj nekonfiguritaj butonoj kurigas", + "unsetText": "" + }, + "configKeyboardWindow": { + "configuringText": "Konfiguriĝas ${DEVICE}", + "keyboard2NoteText": "Notu: plej multaj klavaroj nur registras kelkajn klavojn\nsamtempe, do eblas ke plej bone funkcias dua klavara\nludanto en disa klavaro. Tamen eĉ tiel vi devos konfiguri\nunikajn klavojn por ambaŭ ludantoj." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Grandeco de ago-regumo", + "actionsText": "Agoj", + "buttonsText": "butonoj", + "dragControlsText": "< ŝovu regumojn por repoziciigi ilin >", + "joystickText": "bastoneto", + "movementControlScaleText": "Grandeco de movo-regumo", + "movementText": "Movado", + "resetText": "Reigi", + "swipeControlsHiddenText": "Ne montri glittuŝajn bildetojn", + "swipeInfoText": "Glittuŝaj regumoj bezonas iom da kutimiĝo, sed pli\nfaciligas ludadon sen rigardi la regumojn.", + "swipeText": "glittuŝi", + "titleText": "Konfiguri tuŝekranon", + "touchControlsScaleText": "Grandeco de tuŝregumoj" + }, + "configureItNowText": "Ĉu konfiguri nun?", + "configureText": "Agordi", + "connectMobileDevicesWindow": { + "amazonText": "Aplikaĵvendejo de Amazon", + "appStoreText": "Aplikaĵvendejo", + "bestResultsText": "Por plej bonaj rezultoj vi bezonas senhezitan reton. Vi povas\nmalpliigi heziton de sendrata reto malŝaltante aliajn sendratajn\naparatojn, ludante proksime de la disvojilo kaj ligante la ludan\ngastiganton rekte al la reto per drato.", + "explanationText": "Por uzi poŝtelefonon aŭ tabletkomputilon kiel sendratan\nregilon, instalu la aplikaĵon \"BombSquad Remote\" en ĝi.\nKiom ajn da aparatoj povas konektiĝi sendratrete, kaj senpage!", + "forAndroidText": "por Android:", + "forIOSText": "por iOS:", + "getItForText": "Akiru BombSquad Remote por iOS en la aplikaĵvendejo de \nApple aŭ por Android en la aplikaĵvendejo de Google aŭ Amazon", + "googlePlayText": "Google Play", + "titleText": "Uzi poŝaparatojn kiel regilojn:" + }, + "continuePurchaseText": "Ĉu pluiri por ${PRICE}?", + "continueText": "Pluiri", + "controlsText": "Kontrolklavoj", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Tio ne aplikeblas al rankoj pri ĉiam.", + "activenessInfoText": "Ĉi tiu multobligo supreniras en tagoj\nkiam vi ludas, kaj malsupreniras se ne.", + "activityText": "Aktiveco", + "campaignText": "Kampanjo", + "challengesText": "Defioj", + "currentBestText": "Nuna plejbono", + "customText": "Adaptite", + "entryFeeText": "Enirpago", + "multipliersText": "Multobligiloj", + "ofTotalTimeText": "el ${TOTAL}", + "pointsText": "Poentoj", + "powerRankingNotInTopText": "(ne en la supraj ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} poentoj", + "powerRankingPointsMultText": "(x ${NUMBER} poentoj)", + "powerRankingPointsText": "${NUMBER} ptj", + "powerRankingPointsToRankedText": "(${CURRENT} el ${REMAINING} poentoj)", + "powerRankingText": "Ranka supervido", + "prizesText": "Premioj", + "proMultInfoText": "Ludantoj kun la ĝisdatigo ${PRO}\nricevas ${PERCENT}% poentan supreniĝon ĉi tie.", + "seeMoreText": "Pli...", + "timeRemainingText": "Restanta tempo", + "titleText": "Kunlabore", + "toRankedText": "Ĝis rankumita", + "totalText": "entute", + "welcome1Text": "Bonvenon al ${LEAGUE}. Vi povas plibonigi vian\nligan rankon gajnante stelajn rankojn, kompletante\natingojn, kaj gajnante trofeojn en turniroj.", + "welcome2Text": "Vi ankaŭ povas gajni biletojn per multaj de la samaj aktivaĵoj.\nBiletoj povas esti uzataj por malŝlosi novajn rolulojn, mapojn\nkaj ludetojn, por eniri turnirojn ktp.", + "yourPowerRankingText": "Via ranka pozicio:" + }, + "copyOfText": "Kopio de ${NAME}", + "createAPlayerProfileText": "Ĉu krei ludantprofilon?", + "createText": "Krei", + "creditsWindow": { + "additionalAudioArtIdeasText": "Aldonaj aŭdaĵoj, unuaj artaĵoj kaj ideoj fare de ${NAME}", + "additionalMusicFromText": "Aldona muziko de ${NAME}", + "allMyFamilyText": "Ĉiuj miaj amikoj kaj familio kiuj helpis ludtesti", + "codingGraphicsAudioText": "Kodumado, grafikaĵoj kaj aŭdaĵoj fare de ${NAME}", + "languageTranslationsText": "Lingvotradukoj:", + "legalText": "Leĝaj aferoj:", + "publicDomainMusicViaText": "Publikdomajna muziko per ${NAME}", + "softwareBasedOnText": "Ĉi tiu programaro estas parte bazita je la laboro de ${NAME}", + "songCreditText": "${TITLE} plenumita de ${PERFORMER}\nKomponita de ${COMPOSER}, aranĝita de ${ARRANGER}, publikigita de ${PUBLISHER},\nDanke al ${SOURCE}", + "soundAndMusicText": "Sono kaj muziko:", + "soundsText": "Sonoj (${SOURCE}):", + "specialThanksText": "Speciale dankon:", + "thanksEspeciallyToText": "Specife dankon al ${NAME}", + "titleText": "Dankdiroj de BombSquad", + "whoeverInventedCoffeeText": "Kiu ajn inventis kafon" + }, + "currentStandingText": "Via nuna pozicio estas #${RANK}", + "customizeText": "Adapti...", + "deathsTallyText": "${COUNT} mortoj", + "deathsText": "Mortoj", + "debugText": "sencimigi", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Notu: ni rekomendas ke vi konfiguru Agordoj->Grafikaĵoj->Teksturoj kiel 'Alta' dum vi testas tion.", + "runCPUBenchmarkText": "Kompartesto por CPU", + "runGPUBenchmarkText": "Kompartesto por GPU", + "runMediaReloadBenchmarkText": "Kompartesto de rimeda reŝargo", + "runStressTestText": "Streĉtesti", + "stressTestPlayerCountText": "Ludantokvanto", + "stressTestPlaylistDescriptionText": "Streĉtesta ludlisto", + "stressTestPlaylistNameText": "Ludlista nomo", + "stressTestPlaylistTypeText": "Ludlista tipo", + "stressTestRoundDurationText": "Daŭro de ludvico", + "stressTestTitleText": "Streĉtesto", + "titleText": "Kompartestoj kaj streĉtestoj", + "totalReloadTimeText": "Entuta reŝargtempo: ${TIME} (vidu protokolon por detaloj)", + "unlockCoopText": "Malŝlosi kunlaborajn nivelojn" + }, + "defaultFreeForAllGameListNameText": "Normala ludaro por Ĉiuj kune", + "defaultGameListNameText": "Kutima ludlisto de ${PLAYMODE}", + "defaultNewFreeForAllGameListNameText": "Mia ludaro por Ĉiuj kune", + "defaultNewGameListNameText": "Mia ludlisto de ${PLAYMODE}", + "defaultNewTeamGameListNameText": "Miaj teamludoj", + "defaultTeamGameListNameText": "Normala teamludaro", + "denyText": "Rifuzi", + "desktopResText": "Distingivo", + "difficultyEasyText": "Facile", + "difficultyHardOnlyText": "Nurmalfacila", + "difficultyHardText": "Malfacile", + "difficultyHardUnlockOnlyText": "Ĉi tiu nivelo nur estas malŝlosebla en la \nmalfacila modo. Ĉu vi kapablas fari tion!?!?!", + "directBrowserToURLText": "Bonvolu viziti en retumilo la jenan ligilon:", + "doneText": "Farite", + "drawText": "Egalludo", + "editGameListWindow": { + "addGameText": "Aldoni\nludon", + "cantOverwriteDefaultText": "Ne eblas transskribi la implicitan ludliston!", + "cantSaveAlreadyExistsText": "Ludlisto kun tiu nomo jam ekzistas!", + "cantSaveEmptyListText": "Ne eblas savi malplenan ludliston!", + "editGameText": "Redakti\nludon", + "gameListText": "Ludolisto", + "listNameText": "Ludlista nomo", + "nameText": "Nomo", + "removeGameText": "Forigi\nludon", + "saveText": "Savi liston", + "titleText": "Ludlista redaktilo" + }, + "editProfileWindow": { + "changesNotAffectText": "Notu: ŝanĝoj ne efikos al ludantoj jam enludaj", + "characterText": "rolulo", + "colorText": "koloro", + "getMoreCharactersText": "Ekhavi pli da roluloj...", + "highlightText": "kromkoloro", + "nameDescriptionText": "Ludantonomo", + "nameText": "Nomo", + "randomText": "hazarde", + "titleEditText": "Redakti profilon", + "titleNewText": "Novan profilon" + }, + "editProfilesAnyTimeText": "(vi povas redakti profilojn iam ajn sub 'agordoj')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Vi ne povas forviŝi la dekomencan sontrakon.", + "cantEditDefaultText": "Ne eblas redakti implicitan sontrakon. Duobligu ĝin aŭ kreu novan.", + "cantEditWhileConnectedOrInReplayText": "Ne eblas redakti sontrakojn dum vi estas konektita al ludantaro aŭ en remontro.", + "cantOverwriteDefaultText": "Ne eblas transskribi implicitan sontrakon", + "cantSaveAlreadyExistsText": "Sontrako kun tiu nomo jam ekzistas!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Normala sontrako", + "deleteConfirmText": "Ĉu forviŝi sontrakon:\n\n'${NAME}'?", + "deleteText": "Forviŝi\nsontrakon", + "duplicateText": "Duobligi\nsontrakon", + "editSoundtrackText": "Sontraka redaktilo", + "editText": "Redakti\nsontrakon", + "fetchingITunesText": "kaptante ludlistojn de iTunes...", + "musicVolumeZeroWarning": "Averto: muzika laŭteco estas je 0", + "nameText": "Nomo", + "newSoundtrackNameText": "Mia sontrako ${COUNT}", + "newSoundtrackText": "Nova sontrako:", + "newText": "Nova\nsontrako", + "selectAPlaylistText": "Elektu ludliston", + "selectASourceText": "Muzikfonto", + "soundtrackText": "SonTrako", + "testText": "testi", + "titleText": "Sontrakoj", + "useDefaultGameMusicText": "Kunliverita ludmuziko", + "useITunesPlaylistText": "Ludlisto de iTunes", + "useMusicFileText": "Muzikdosiero (mp3, k.s.)", + "useMusicFolderText": "Dosierujo da muzikdosieroj" + }, + "endText": "Fini", + "epicDescriptionFilterText": "${DESCRIPTION} en brila malrapideco", + "epicNameFilterText": "Brila ${NAME}", + "errorAccessDeniedText": "malpermeso aliri", + "errorOutOfDiskSpaceText": "elĉerpiĝis diskspaco", + "errorText": "Eraro", + "errorUnknownText": "nekonata eraro", + "exitGameText": "Ĉu eliri el BombSquad?", + "externalStorageText": "Ekstera konservujo", + "failText": "Fiasko", + "fatalErrorText": "Ho ve; io mankas aŭ rompiĝis.\nBonvolu provi reinstali BombSquad aŭ\nkontaktu ${EMAIL} por helpo.", + "fileSelectorWindow": { + "titleFileFolderText": "Elektu dosieron aŭ dosierujon", + "titleFileText": "Elektu dosieron", + "titleFolderText": "Elektu dosierujon", + "useThisFolderButtonText": "Uzi ĉi tiun dosierujon" + }, + "finalScoreText": "Fina poentaro", + "finalScoresText": "Finaj poentoj", + "finalTimeText": "Fina tempo", + "finishingInstallText": "Finante instaliĝon, bonvolu atendi...", + "fireTVRemoteWarningText": "* Por pli bona sperto, uzu\nludregilojn aŭ instalu la\n'BombSquad Remote'-aplikaĵon\nen viaj telefonoj kaj tabuletoj.", + "firstToFinalText": "Unua-ĝis-${COUNT} finalo", + "firstToSeriesText": "Unua-ĝis-${COUNT} serio", + "fiveKillText": "KVINMORTIGO!!!", + "flawlessWaveText": "Perfekta ondo!", + "fourKillText": "KVARMORTIGO!!!", + "freeForAllText": "Ĉiuj kune", + "friendScoresUnavailableText": "Amikaj poentoj nehaveblas.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Plejpoentuloj en ludo ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "Vi ne povas forviŝi la implicitan ludliston!", + "cantEditDefaultText": "Ne eblas redakti la implicitan ludliston! Duobligu ĝin aŭ kreu novan.", + "deleteConfirmText": "Ĉu forviŝi \"${LIST}\"?", + "deleteText": "Forviŝi\nludliston", + "duplicateText": "Duobligi\nludliston", + "editText": "Redakti\nludliston", + "gameListText": "Ludolisto", + "newText": "Nova\nludlisto", + "showTutorialText": "Montri klarigon", + "shuffleGameOrderText": "Miksi ludsinsekvon", + "titleText": "Adapti ludliston de ${TYPE}" + }, + "gameSettingsWindow": { + "addGameText": "Aldoni ludon" + }, + "gamepadDetectedText": "1 ludregtabulo detektita", + "gamepadsDetectedText": "${COUNT} ludregtabuloj detektitaj.", + "gamesToText": "per ${WINCOUNT} ludoj kontraŭ ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Memoru: ajna aparato en ludantaro povas havi\npli ol unu ludanto se vi havas sufiĉe da regiloj.", + "aboutDescriptionText": "Uzu ĉi tiujn paĝojn por kunigi ludantaron.\n\nPer ludantaroj vi povas ludi ludojn kaj turnirojn\nkun viaj amikoj inter diversaj aparatoj.\n\nUzu la butonon ${PARTY} supre dekstre por\nbabili kaj interagi kun la ludantaro.\n(per regilo, premu ${BUTTON} dum vi estas en menuo)", + "aboutText": "Pri", + "addressFetchErrorText": "", + "bluetoothAndroidSupportText": "(funkcias kun ajna Android-aparato kiu subtenas Bluetooth)", + "bluetoothDescriptionText": "Gastigi/aliĝi ludantaron per Bluetooth", + "bluetoothHostText": "Gastigi per Bluetooth", + "bluetoothJoinText": "Aliĝi per Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "kontrolante...", + "disconnectClientsText": "Tio malkonektos la ${COUNT} ludanto(j)n\nen via ludantaro. Ĉu vi certas?", + "googlePlayDescriptionText": "Inviti ludantojn al la ludantaro per Google Play:", + "googlePlayInviteText": "Inviti", + "googlePlayReInviteText": "Estas ${COUNT} ludanto(j) de Google Play en via ludantaro\nkiuj malkonektiĝos se vi komencos novan inviton.\nEnmetu ilin en la nova invito por rehavi ilin.", + "googlePlaySeeInvitesText": "Vidi invitojn", + "googlePlayText": "Google Play", + "inDevelopmentWarningText": "Notu:\n\nReta ludado estas nova kaj ankoraŭ disvolvata.\nPor nun, estas rekomendate ke ĉiuj ludantoj\nestu en la sama sendrata reto.", + "localNetworkDescriptionText": "Aliĝi al ludantaro en via reto:", + "localNetworkText": "Loka reto", + "manualAddressText": "Adreso", + "manualConnectText": "Konekti", + "manualDescriptionText": "Aliĝi al ludantaro per adreso:", + "manualJoinableFromInternetText": "Ĉu eblas aliĝi al vi de la interreto?:", + "manualJoinableNoWithAsteriskText": "NE*", + "manualJoinableYesText": "JES", + "manualRouterForwardingText": "*por ebligi tion, provu konfiguri vian disvojilon sendante UDP-pordon ${PORT} al via loka adreso", + "manualText": "Mane", + "manualYourAddressFromInternetText": "Via adreso de la interreto:", + "manualYourLocalAddressText": "Via loka adreso:", + "noConnectionText": "", + "partyInviteAcceptText": "Akcepti", + "partyInviteDeclineText": "Malakcepti", + "partyInviteGooglePlayExtraText": "(vidu la paĝon 'Google Play' en la fenestro 'Kolektiĝi')", + "partyInviteIgnoreText": "Ignori", + "partyInviteText": "${NAME} invitis vin\npor aliĝi al la ludantaro!", + "titleText": "Kolektiĝi", + "wifiDirectDescriptionBottomText": "Se ĉiuj aparatoj havas panelon 'Wi-Fi Direct', ili devus povi uzi tion por trovi\nkaj konektiĝi unu al la alia. Kiam ĉiuj aparatoj estas konektitaj, vi povas formi\nludantarojn ĉi tie per la paĝo 'Loka reto', same kiel kun normala Vifi-reto.\n\nPrefere la gastiganto 'Wi-Fi Direct' ankaŭ estu la ludantargastiganto de BombSquad.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct povas esti uzata por konekti Android-aparatojn rekte sen\nbezoni Vifi-reton. Tio plej bone funkcias per Android 4.2 aŭ pli nova.\n\nPor uzi ĝin, malfermu la Vifiajn agordojn kaj serĉu 'Wi-Fi Direct' en la menuo.", + "wifiDirectOpenWiFiSettingsText": "Malfermi Vifiajn agordojn", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(funkcias inter ĉiuj platformoj)", + "worksWithGooglePlayDevicesText": "(funkcias kun aparatoj kun la versio Google Play (android) de la ludo)" + }, + "getCoinsWindow": { + "coinDoublerText": "Monerduobligilo", + "coinsText": "${COUNT} moneroj", + "freeCoinsText": "Senpagaj moneroj", + "restorePurchasesText": "Rehavigi aĉetojn", + "titleText": "Ekhavi monerojn" + }, + "getTicketsWindow": { + "freeText": "SENPAGE!", + "freeTicketsText": "Senpagaj biletoj", + "inProgressText": "Transakcio okazas; bonvolu provi denove baldaŭ.", + "purchasesRestoredText": "Aĉetoj restarigitaj.", + "receivedTicketsText": "Riceviĝis ${COUNT} biletoj!", + "restorePurchasesText": "Restarigi aĉetojn", + "ticketDoublerText": "Biletduobligilo", + "ticketPack1Text": "Eta biletopakaĵo", + "ticketPack2Text": "Meza biletopakaĵo", + "ticketPack3Text": "Granda biletopakaĵo", + "ticketPack4Text": "Ega biletopakaĵo", + "ticketPack5Text": "Egega biletopakaĵo", + "ticketPack6Text": "Plejega biletopakaĵo", + "ticketsFromASponsorText": "Akiri ${COUNT} biletojn\nde sponsoro", + "ticketsText": "${COUNT} biletoj", + "titleText": "Ekhavi biletojn", + "unavailableTemporarilyText": "Nun tio ne haveblas; provu denove poste.", + "unavailableText": "Pardonu, tio ne haveblas.", + "versionTooOldText": "Pardonu, ĉi tiu ludversio tro malnovas; bonvolu ĝisdatigi al pli nova.", + "youHaveShortText": "vi havas ${COUNT}", + "youHaveText": "vi havas ${COUNT} biletojn" + }, + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Ĉiam", + "fullScreenCmdText": "Plena ekrano (Cmd-F)", + "fullScreenCtrlText": "Plena ekrano (Stir-F)", + "gammaText": "Gamo (brileco)", + "highText": "Alta", + "higherText": "Pli alta", + "lowText": "Malalta", + "mediumText": "Meza", + "neverText": "Neniam", + "resolutionText": "Distingivo", + "showFPSText": "Montri FPS", + "texturesText": "Teksturoj", + "titleText": "Grafikaĵoj", + "tvBorderText": "Rando por televido", + "verticalSyncText": "Vertikala sinkronigo", + "visualsText": "Vidaĵoj" + }, + "helpWindow": { + "bombInfoText": "- Bombo -\nPli forta ol batoj, sed povas\nefektivigi gravan sindifekton.\nPor plej bonaj rezultoj, ĵetu al\nmalamiko antaŭ ol la drato finiĝas.", + "canHelpText": "BombSquad povas helpi.", + "controllersInfoText": "Vi povas ludi BombSquad kun amikoj en reto, aŭ vi povas ĉiuj ludi\nen la sama aparato se vi havas sufiĉe da regiloj. BombSquad subtenas\nmultajn regilojn; vi eĉ povas uzi vian poŝtelefonon kiel regilon\nper la senpaga aplikaĵo 'BombSquad Remote'.\nVidu Agordoj->Regiloj por pliaj informoj.", + "controllersInfoTextFantasia": "Unu persono povas uzi la teleregilon kiel regilo, sed mi forte\nrekomendus uzi ludregtabulojn. Vi ankaŭ povas uzi viajn poŝ-\naparatojn per la senpaga aplikaĵo 'BombSquad Remote'.\nVidu 'Regiloj' sub 'Agordoj' por pliaj informoj.", + "controllersInfoTextMac": "Unu aŭ du ludantoj povas uzi la klavaron, sed BombSquad plej bonas kun\nludregtabuloj. BombSquad povas uzi USB-ludregtabulojn, PS3-regilojn, regilojn\nde XBox 360, Wii-regilojn kaj iOS/Android-aparatojn por regi rolulojn. Espereble\nvi havas kelkajn tiajn disponeblaj. Vidu 'Regiloj' sub 'Agordoj' por pliaj informoj.", + "controllersInfoTextOuya": "Vi povas uzi OUYA-regilon, PS3-regilojn, regilojn de XBox 360 kaj multajn\naliajn ludregtabulojn Bludentajn kaj USBajn ene de BombSquad.\nVi ankaŭ povas uzi iOS-aparatojn kaj Android por regi la ludon per la\nsenpaga aplikaĵo 'BombSquad Remote'. Vidu 'Regiloj' sub 'Agordoj' por\npliaj informoj.", + "controllersText": "Regiloj", + "controlsSubtitleText": "Via amikema BombSquad-rolulo havas kelkajn bazajn agojn:", + "controlsText": "Regumoj", + "devicesInfoText": "La VR-versio de BombSquad povas esti ludata en la reto kun la\nnormala versio, do ektrovu viajn aldonajn telefonojn, tabuletojn\nkaj komputilojn, kaj ekludu. Eĉ povas esti utile konekti normalan\nversion de la ludo al la VR-versio por permesi al homoj ekster vi\nkunvidi la agadon.", + "devicesText": "Aparatoj", + "friendsGoodText": "Bonas havi ilin. BombSquad plej amuzas kun pluraj ludantoj kaj\npovas subteni ĝis 8 samtempe, kio kondukas nin al:", + "friendsText": "Amikoj", + "jumpInfoText": "- Salti -\nSaltu por transiri truetojn,\npor ĵeti pli alten, kaj por\nesprimi ĝojsentojn.", + "orPunchingSomethingText": "Aŭ bati ion, ĵeti ion de deklivo, kaj eksplodigi ĝin survoje malsupren per gluanta bombo.", + "pickUpInfoText": "- Preni -\nPreni flagojn, malamikojn, aŭ ion\najn ne fiksitan je la planko.\nPremu denove por ĵeti.", + "powerupBombDescriptionText": "Ebligas al vi ĵeti tri bombojn\nen vico anstataŭ nur unu.", + "powerupBombNameText": "Trioblaj bomboj", + "powerupCurseDescriptionText": "Vi verŝajne volas eviti ĉi tiujn.\n...ĉu ne?", + "powerupCurseNameText": "Malbeno", + "powerupHealthDescriptionText": "Tutreplenigas vian sanon.\nVi neniam divenintus.", + "powerupHealthNameText": "Sanpakaĵo", + "powerupIceBombsDescriptionText": "Malpli fortaj ol normalaj bomboj\nsed lasas viajn malamikojn\nfrostitaj kaj rompiĝemaj.", + "powerupIceBombsNameText": "Glacibomboj", + "powerupImpactBombsDescriptionText": "Iom malpli fortaj ol normalaj\nbomboj, sed eksplodas je impakto.", + "powerupImpactBombsNameText": "Ekpafbomboj", + "powerupLandMinesDescriptionText": "Ili venas triope;\nUtilas por defendi bazon aŭ\nhaltigi rapidajn malamikojn.", + "powerupLandMinesNameText": "Minoj", + "powerupPunchDescriptionText": "Faras viajn batojn pli duraj,\nrapidaj, bonaj, fortaj.", + "powerupPunchNameText": "Boksglovoj", + "powerupShieldDescriptionText": "Absorbas iom da damaĝo\npor ke vi ne bezonu.", + "powerupShieldNameText": "Energiŝirmilo", + "powerupStickyBombsDescriptionText": "Gluiĝas al ĉio kion ĝi tuŝas.\nVidu kaj ridu.", + "powerupStickyBombsNameText": "Glubomboj", + "powerupsSubtitleText": "Kompreneble, neniu ludo kompletas sen donacetoj:", + "powerupsText": "Donacetoj", + "punchInfoText": "- Bato -\nBatoj pli damaĝas ju pli viaj\npugnoj moviĝas, do kuru kaj\nrotaciu kiel frenezulo.", + "runInfoText": "- Kuri -\nPremadu AJNAN butonon por kuri. Flankbutonoj kaj pafkliko taŭgas se vi havas ilin.\nKurado helpas vin iri ien pli rapide, sed malpli facilas turni, do atentu deklivojn.", + "someDaysText": "Fojfoje vi simple sentas emon bati ion. Aŭ eksplodigi ion.", + "titleText": "Helpo de BombSquad", + "toGetTheMostText": "Por profiti plej de ĉi tiu ludo, necesos:", + "welcomeText": "Bonvenon al BombSquad!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} majstre tranavigas menuojn", + "installDiskSpaceErrorText": "ERARO: Neeble kompletigi la instalon.\nEble ne estas sufiĉe da spaco en via\naparato. Viŝu ion kaj provu denove.", + "internal": { + "arrowsToExitListText": "premu ${LEFT} aŭ ${RIGHT} por eliri liston", + "buttonText": "butono", + "connectedToPartyText": "En la ludantaro de ${NAME}!", + "connectingToPartyText": "Konektiĝas...", + "connectionFailedHostAlreadyInPartyText": "Konekto fiaskis; gastiganto estas en alia ludantaro.", + "connectionFailedText": "Konekto fiaskis.", + "connectionFailedVersionMismatchText": "Konekto fiaskis; gastiganto havas alian version de la ludo.\nCertigu vin ke vi ambaŭ estu ĝisdataj kaj provu denove.", + "connectionRejectedText": "Konekto malakceptita.", + "controllerConnectedText": "${CONTROLLER} konektiĝis.", + "controllerDetectedText": "1 regilo detektita.", + "controllerDisconnectedText": "${CONTROLLER} malkonektiĝis.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} malkonektiĝis. Bonvolu reprovi konektiĝi.", + "controllerReconnectedText": "${CONTROLLER} rekonektiĝis.", + "controllersConnectedText": "${COUNT} regiloj konektitaj.", + "controllersDetectedText": "${COUNT} regiloj detektitaj.", + "controllersDisconnectedText": "${COUNT} regiloj malkonektitaj.", + "corruptFileText": "Detektiĝis koruptaj dosieroj. Bonvolu provi reinstali, aŭ retpoŝtu ${EMAIL}", + "errorPlayingMusicText": "Eraro ludante muzikon: ${MUSIC}", + "errorResettingAchievementsText": "Ne eblas reigi enretajn atingojn; provu denove pli malfrue.", + "hasMenuControlText": "${NAME} nun regas la menuon.", + "incompatibleVersionHostText": "Gastiganto uzas alian version de la ludo.\nCertigu vin ke vi ambaŭ estu ĝisdataj kaj provu denove.", + "incompatibleVersionPlayerText": "${NAME} uzas alian version de la ludo.\nCertigu vin ke vi ambaŭ estu ĝisdataj kaj provu denove.", + "invalidAddressErrorText": "Eraro: nevalida adreso.", + "invitationSentText": "Invito sendiĝis.", + "invitationsSentText": "${COUNT} invitoj sendiĝis.", + "joinedPartyInstructionsText": "Amiko aliĝis al via ludantaro.\nIru al 'Ekludi' por komenci ludon.", + "keyboardText": "Klavaro", + "kickIdlePlayersKickedText": "${NAME} forpuŝiĝas pro neaktiveco.", + "kickIdlePlayersWarning1Text": "${NAME} forpuŝiĝos post ${COUNT} sekundoj se ankoraŭ neaktiva.", + "kickIdlePlayersWarning2Text": "(eblas malŝalti tion en Agordoj -> Por spertuloj)", + "leftPartyText": "Eliris el ludantaro de ${NAME}.", + "noMusicFilesInFolderText": "Dosierujo ne enhavas muzikdosierojn.", + "playerJoinedPartyText": "${NAME} aliĝis al la ludantaro!", + "playerLeftPartyText": "${NAME} foriris de la ludantaro.", + "rejectingInviteAlreadyInPartyText": "Malakceptiĝas invito (jam en ludantaro).", + "signInErrorText": "Eraro ensaluti.", + "signInNoConnectionText": "Ne eblas ensaluti. (ĉu sen retkonekto?)", + "teamNameText": "Teamo ${NAME}", + "telnetAccessDeniedText": "ERARO: uzanto ne donis telnet-aliron.", + "timeOutText": "(tempo elĉerpiĝos post ${TIME} sekundoj)", + "touchScreenJoinWarningText": "Vi aliĝis per la tuŝekrano.\nSe tio estis eraro, tuŝu 'Menuo->Foriri de ludo' per ĝi.", + "touchScreenText": "TuŝEkrano", + "trialText": "provversio", + "unavailableNoConnectionText": "Ĉi tio momente ne haveblas (manko de retkonekto?)", + "vrOrientationResetCardboardText": "Uzu ĉi tion por reigi la VR-orientiĝon.\nPor ludi la ludon vi bezonos aldonan regilon.", + "vrOrientationResetText": "Reiĝis orientiĝo de VR.", + "willTimeOutText": "(sen aktiveco tempo elĉerpiĝos)" + }, + "jumpBoldText": "SALTI", + "jumpText": "Salti", + "keepText": "Teni", + "keepTheseSettingsText": "Ĉu teni ĉi tiujn agordojn?", + "killsTallyText": "${COUNT} mortigoj", + "killsText": "Mortigoj", + "kioskWindow": { + "easyText": "Facile", + "epicModeText": "Brila modo", + "fullMenuText": "Plena menuo", + "hardText": "Malfacile", + "mediumText": "Mezfacile", + "singlePlayerExamplesText": "Ekzemploj de unu ludanto / kunlabore", + "versusExamplesText": "Ekzemploj de ludoj unu kontraŭ la alia" + }, + "languageSetText": "Lingvo nun estas \"${LANGUAGE}\".", + "lapNumberText": "Ĉirkaŭiro ${CURRENT} el ${TOTAL}", + "lastGamesText": "(lastaj ${COUNT} ludoj)", + "leaderboardsText": "Plejpoentularoj", + "league": { + "allTimeText": "Pri ĉiam", + "currentSeasonText": "Nuna sezono (${NUMBER})", + "leagueFullText": "Ligo ${NAME}", + "leagueRankText": "Liga ranko", + "leagueText": "Ligo", + "seasonEndedDaysAgoText": "Sezono finiĝis antaŭ ${NUMBER} tagoj.", + "seasonEndsDaysText": "Sezono finiĝos en ${NUMBER} tagoj.", + "seasonEndsHoursText": "Sezono finiĝos en ${NUMBER} horoj.", + "seasonEndsMinutesText": "Sezono finiĝos en ${NUMBER} minutoj.", + "seasonText": "Sezono ${NUMBER}", + "tournamentLeagueText": "Vi devas atingi la ligon ${NAME} por eniri ĉi tiun turniron.", + "trophyCountsResetText": "Premikalkuloj reiĝos sekvasezone." + }, + "levelFastestTimesText": "Plej rapidaj tempoj en ${LEVEL}", + "levelHighestScoresText": "Plej altaj poentoj en ${LEVEL}", + "levelIsLockedText": "${LEVEL} estas ŝlosita.", + "levelMustBeCompletedFirstText": "Unue necesas kompletigi ${LEVEL}.", + "levelUnlockedText": "Nivelo malŝlosita!", + "livesBonusText": "Vivopremio", + "loadingText": "ŝargas", + "mainMenu": { + "creditsText": "Dankdiroj", + "demoMenuText": "Montromenuo", + "endGameText": "Fini ludon", + "exitGameText": "Eliri de ludo", + "exitToMenuText": "Ĉu reiri al menuo?", + "howToPlayText": "Kiel ludi", + "justPlayerText": "(Nur ${NAME})", + "leaveGameText": "Foriri de ludo", + "leavePartyConfirmText": "Ĉu vere foriri de la ludantaro?", + "leavePartyText": "For de la ludantaro", + "leaveText": "Foriri", + "quitText": "Fermi", + "resumeText": "Pluiri", + "settingsText": "Agordoj" + }, + "makeItSoText": "Tiel estu", + "mapSelectGetMoreMapsText": "Pli da mapoj...", + "mapSelectText": "Elektu...", + "mapSelectTitleText": "Mapoj de ${GAME}", + "mapText": "Mapo", + "mostValuablePlayerText": "Plej valora ludanto", + "mostViolatedPlayerText": "Plej perfortita ludanto", + "mostViolentPlayerText": "Plej perforta ludanto", + "moveText": "Movi", + "multiKillText": "${COUNT}-MORTIGO!!!", + "multiPlayerCountText": "${COUNT} ludantoj", + "mustInviteFriendsText": "Notu: vi devas inviti amikojn en\nla panelo \"${GATHER}\" aŭ ligi\nregilojn por ludi plurope.", + "nameBetrayedText": "${NAME} perfidis al ${VICTIM}", + "nameDiedText": "${NAME} mortis.", + "nameKilledText": "${VICTIM} estas mortigita de ${NAME}", + "nameNotEmptyText": "Nomo ne povas esti malplena!", + "nameScoresText": "${NAME} poentas!", + "nameSuicideKidFriendlyText": "${NAME} ging per ongeluk dood", + "nameSuicideText": "${NAME} mortigis sin.", + "nativeText": "Propra", + "newPersonalBestText": "Nova persona plejbono!", + "newTestBuildAvailableText": "Nova testkonstruo haveblas! (${VERSION} konstruo ${BUILD}).\nEkhavu ĝin ĉe ${ADDRESS}", + "newVersionAvailableText": "Nova versio de BombSquad estas havebla! (${VERSION})", + "nextLevelText": "Sekva nivelo", + "noAchievementsRemainingText": "- neniom", + "noContinuesText": "(sen pluiroj)", + "noExternalStorageErrorText": "Ne trovis eksteran konservujon en ĉi tiu aparato", + "noGameCircleText": "Eraro: ne ensalutita en GameCircle", + "noJoinCoopMidwayText": "Ne eblas aliĝi al kunlaboraj ludoj dumlude.", + "noProfilesErrorText": "Vi ne havas ludantoprofilojn, do ni devos nomi vin '${NAME}'.\nIru al Agordoj->Ludantprofiloj por fari profilon por vi.", + "noScoresYetText": "Ne jam poentoj.", + "noThanksText": "Ne dankon", + "noValidMapsErrorText": "Ne trovis validajn mapojn por ĉi tiu ludotipo.", + "notEnoughPlayersRemainingText": "Ne sufiĉe da ludantoj restas; eliri kaj komenci novan ludon.", + "notEnoughPlayersText": "Vi bezonas almenaŭ ${COUNT} ludantojn por komenci ĉi tiun ludon!", + "notNowText": "Ne nun", + "notSignedInErrorText": "Vi devas esti ensalutita en via konto por fari tion.", + "notSignedInText": "(ne ensalutita)", + "nothingIsSelectedErrorText": "Nenio elektiĝis!", + "numberText": "#${NUMBER}", + "offText": "Malŝaltite", + "okText": "Bone", + "onText": "Ŝaltite", + "onslaughtRespawnText": "${PLAYER} reviviĝos en ondo ${WAVE}", + "orText": "${A} aŭ ${B}", + "outOfText": "(#${RANK} el ${ALL})", + "ownFlagAtYourBaseWarning": "Via propra flago estu\nje via bazo por poenti!", + "packageModsEnabledErrorText": "Reta ludado ne estas permesata dum lokaj pakaĵaj modifikaĵoj estas aktivaj (vidu Agordoj->Por spertuloj)", + "partyWindow": { + "chatMessageText": "Babilmesaĝo", + "emptyText": "Via ludantaro estas malplena", + "hostText": "(gastiganto)", + "sendText": "Sendi", + "titleText": "Via ludantaro" + }, + "pausedByHostText": "(paŭzigita de gastiganto)", + "perfectWaveText": "Perfekta ondo!", + "pickUpText": "Preni", + "playModes": { + "coopText": "Kunlabore", + "freeForAllText": "Ĉiuj kune", + "multiTeamText": "Plurteame", + "singlePlayerCoopText": "Unuope aŭ kunlabore", + "teamsText": "Teame" + }, + "playText": "Ekludi", + "playWindow": { + "coopText": "Kunlabore", + "freeForAllText": "Ĉiuj kune", + "oneToFourPlayersText": "1 ĝis 4 ludantoj", + "teamsText": "Teamoj", + "titleText": "Ludi", + "twoToEightPlayersText": "2 ĝis 8 ludantoj" + }, + "playerCountAbbreviatedText": "${COUNT}lud", + "playerDelayedJoinText": "${PLAYER} eniros komence de la sekva ludo.", + "playerLeftText": "${PLAYER} foriris de la ludo.", + "playerLimitReachedText": "Ludanto-limo de ${COUNT} atingita; neniu plu aliĝu.", + "playerLimitReachedUnlockProText": "Ĝisdatigu al \"${PRO}\" en la vendejo por ludi kun pli ol ${COUNT} ludantoj.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Vi ne povas forviŝi vian konto-profilon.", + "deleteButtonText": "Forviŝi\nprofilon", + "deleteConfirmText": "Ĉu forviŝi '${PROFILE}'?", + "editButtonText": "Redakti\nprofilon", + "explanationText": "(krei adaptitajn nomojn k aperojn por ludantoj en ĉi tiu aparato)", + "newButtonText": "Nova\nprofilo", + "titleText": "Ludanto-profiloj" + }, + "playerText": "Ludanto", + "playlistNoValidGamesErrorText": "Ĉi tiu ludlisto ne enhavas validajn malŝlositajn ludojn.", + "playlistNotFoundText": "ludlisto ne troviĝis", + "playlistsText": "Ludlistoj", + "pleaseRateText": "Se vi ŝatas BombSquad, bonvolu konsideri momenton\npor rankumi ĝin aŭ verki prijuĝon. Tio donos utilan\nretrokuplon kaj helpos subteni pluan disvolviĝon.\n\ndankon!\n-eric", + "pressAnyButtonPlayAgainText": "Premu ajnan butonon por reekludi...", + "pressAnyButtonText": "Premu ajnan butonon por pluiri...", + "pressAnyButtonToJoinText": "premu ajnan butonon por aliĝi...", + "pressAnyKeyButtonPlayAgainText": "Premu ajnan klavon/butonon por reekludi...", + "pressAnyKeyButtonText": "Premu ajnan klavon/butonon por pluiri...", + "pressAnyKeyText": "Premu ajnan klavon...", + "pressJumpToFlyText": "** Sinripete premu salti por flugi **", + "pressPunchToJoinText": "premu BATI por aliĝi...", + "pressToOverrideCharacterText": "premu ${BUTTONS} por ŝanĝi vian rolulon", + "pressToSelectProfileText": "premu ${BUTTONS} por elekti vian profilon", + "pressToSelectTeamText": "premu ${BUTTONS} por elekti vian teamon", + "profileInfoText": "Kreu profilojn por vi mem kaj viajn amikojn\npor adapti viajn nomojn, rolulojn kaj kolorojn.", + "promoCodeWindow": { + "codeText": "Kodo", + "codeTextDescription": "Promocia kodo", + "enterText": "Enigi" + }, + "promoSubmitErrorText": "Eraro aplikante vian promocian kodon; kontrolu vian retkonekton", + "ps3ControllersWindow": { + "macInstructionsText": "Malŝaltu la energion malantaŭe de via PS3, certigu ke Bludento\nestu aktiva en via Makintoŝo, tiam konektu vian regilon al via\nMakintoŝo per USB-kablo por parigi ilin. De tiam, vi povas uzi la\nhejmbutonon de la regilo por konekti ĝin kun via Makintoŝo ĉu\ndrate (USB) ĉu sendrate (Bludento).\n\nEn kelkaj Makintoŝo la sistemo povas peti vin enigi paskodon por\nparigi. Se tio okazas, jen la sekva klarigo aŭ guglu por helpo.\n\n\n\n\nPS3-regiloj konektitaj sendrate aperu en la aparatlisto en Sistem-\npreferoj->Bludento. Povas esti necese forigi ilin de tiu listo por\nuzi ilin denove kun via PS3.\n\nAnkaŭ certiĝu diskonekti ilin de Bludento kiam ne en uzo, alikaze\nmalŝparante la bateriojn.\n\nBludento povu trakti ĝis 7 konektitajn aparatojn, sed via sperto\npovus esti alia.", + "ouyaInstructionsText": "Por uzi PS3-regilon kun via OUYA, simple konektu ĝin per USB-kablo unufoje\npor parigi ĝin. Farante tion viaj aliaj regiloj povas malŝaltiĝi, do tiam restartu\nvian OUYA kaj malligu la USB-kablon.\n\nDe tiam sufiĉu uzi la regilan hejmbutonon por konekti ĝin sendrate. Post kiam\nvi ludis, premu la HEJM-butonon dum 10 sekundoj por malŝalti la regilon; alie\nĝi povas resti ŝaltita, tiel malŝparante bateriojn.", + "pairingTutorialText": "klariga video por parigi", + "titleText": "Uzi PS3-regilojn kun BombSquad:" + }, + "publicBetaText": "PUBLIKA BETA", + "punchBoldText": "BATO", + "punchText": "Bato", + "purchaseForText": "Aĉetu por ${PRICE}", + "purchaseGameText": "Aĉeti ludon", + "purchasingText": "Aĉetiĝas...", + "quitGameText": "Ĉu fermi BombSquad?", + "quittingIn5SecondsText": "Fermos en 5 sekundoj...", + "randomText": "Hazarde", + "rankText": "Ranko", + "ratingText": "Ranko", + "reachWave2Text": "Atingu la duan ondon por ranki.", + "readyText": "preta", + "recentText": "Lastatempaj", + "remainingInTrialText": "restas en elprovo", + "removeInGameAdsText": "Malŝlosu \"${PRO}\" en la vendejo por forigi enludajn reklamojn.", + "renameText": "Renomi", + "replayEndText": "Fini remontron", + "replayNameDefaultText": "Lastluda remontro", + "replayReadErrorText": "Eraro legante remontran dosieron.", + "replayRenameWarningText": "Ŝangu la nomon de \"${REPLAY}\" post ludo por teni ĝin; alikaze ĝi transskribiĝos.", + "replayVersionErrorText": "Pardonu, ĉi tiu remontro estas farita en alia\nversio de la ludo kaj ne povas esti uzata.", + "replayWatchText": "Spekti remontron", + "replayWriteErrorText": "Eraro skribante remontran dosieron.", + "replaysText": "Remontroj", + "requestingText": "Petante...", + "restartText": "Restarti", + "retryText": "Reprovi", + "revertText": "Reigi", + "runText": "Kuri", + "saveText": "Savi", + "scoreChallengesText": "Poentaraj defioj", + "scoreListUnavailableText": "Poentara listo nehavebla.", + "scoreText": "Poentaro", + "scoreUnits": { + "millisecondsText": "Milisekundoj", + "pointsText": "Poentoj", + "secondsText": "Sekundoj" + }, + "scoreWasText": "(estis ${COUNT})", + "selectText": "Elektu", + "seriesWinLine1PlayerText": "VENKAS LA", + "seriesWinLine1TeamText": "VENKAS LA", + "seriesWinLine1Text": "VENKAS LA", + "seriesWinLine2Text": "SERION!", + "settingsWindow": { + "accountText": "Konto", + "advancedText": "Por spertuloj", + "audioText": "Aŭdaĵoj", + "controllersText": "Regiloj", + "graphicsText": "Grafikaĵoj", + "playerProfilesText": "Ludantprofiloj", + "titleText": "Agordoj" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(facila, regilo-amika surekrana klavaro por redakti tekstojn)", + "alwaysUseInternalKeyboardText": "Ĉiam uzi internan klavaron", + "benchmarksText": "Kompartestoj kaj streĉtestoj", + "enablePackageModsDescriptionText": "(ebligas aldonajn modifikajn kapablojn sed malebligas retludadon)", + "enablePackageModsText": "Ebligi lokajn pakaĵajn modifikaĵojn", + "enterPromoCodeText": "Enigu promocian kodon", + "forTestingText": "Notu: ĉi tiuj valoroj estas nur por testado kaj perdiĝos je fermiĝo de la aplikaĵo.", + "helpTranslateText": "La neanglaj tradukoj de BombSquad nur eblas pro la komunumo.\nSe vi volas kontribui aŭ korekti tradukon, sekvu la suban\nligilon. Jam antaŭe dankon!", + "kickIdlePlayersText": "Forpuŝi neaktivajn ludantojn", + "kidFriendlyModeText": "Infanamika modo (malpli da perforto ktp)", + "languageText": "Lingvo", + "moddingGuideText": "Modifikaĵa gvidlibro", + "mustRestartText": "Vi devas restarti la ludon por efektivigi tion.", + "netTestingText": "Testi la reton", + "resetText": "Reigi", + "showPlayerNamesText": "Montri ludantonomojn", + "showUserModsText": "Montri modifikaĵujon", + "titleText": "Por spertuloj", + "translationEditorButtonText": "BombSquad traduko-redaktilo", + "translationFetchErrorText": "tradukstato ne haveblas", + "translationFetchingStatusText": "kontrolante staton de traduko...", + "translationNoUpdateNeededText": "la nuna lingvo estas ĝisdata, hura!", + "translationUpdateNeededText": "** la nuna lingvo bezonas ĝisdatigojn!! **", + "vrTestingText": "Testo de VR" + }, + "signInWithGameCenterText": "Uzu la Ludcentran aplikaĵon por ensaluti.", + "singleGamePlaylistNameText": "Nur ${GAME}", + "singlePlayerCountText": "1 ludanto", + "soloNameFilterText": "Unuopa ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Elekti rolulon", + "Chosen One": "La elektita", + "Epic": "Brilmodaj ludoj", + "Epic Race": "Brila konkurso", + "FlagCatcher": "Kaptu la flagon", + "Flying": "Feliĉaj pensoj", + "Football": "Futbalo", + "ForwardMarch": "Sturmo", + "GrandRomp": "Konkero", + "Hockey": "Hokeo", + "Keep Away": "Fortenu", + "Marching": "Trakuri", + "Menu": "Ĉefmenuo", + "Onslaught": "Impeto", + "Race": "Konkurso", + "Scary": "Reĝo de la monto", + "Scores": "Poenta ekrano", + "Survival": "Eliminado", + "ToTheDeath": "Ĝismorte", + "Victory": "Finpoenta ekrano" + }, + "spaceKeyText": "spaco", + "store": { + "alreadyOwnText": "Vi jam posedas ${NAME}!", + "bombSquadProDescriptionText": "Duobligos biletojn gajnitajn enlude\n\nForigos enludan reklamon\n\nInkluzivas ${COUNT} aldonajn biletojn\n\n+${PERCENT}% aldone en ligaj poentoj\n\nMalŝlosos nivelojn '${INF_ONSLAUGHT}'\n kaj '${INF_RUNAROUND}' (kunlaborajn)", + "bombSquadProFeaturesText": "Avantaĝoj:", + "bombSquadProNameText": "BombSquad Pro", + "buyText": "Aĉeti", + "charactersText": "Roluloj", + "comingSoonText": "Baldaŭ...", + "extrasText": "Aldonaĵoj", + "freeBombSquadProText": "BombSquad nun estas senpaga, sed ĉar vi origine aĉetis ĝin vi ricevas\nla ĝisdatigon BombSquad Pro kaj ${COUNT} biletojn kiel dankomontro.\nĜuu la novajn funkciojn, kaj dankon pro via subteno!\n-Eric", + "gameUpgradesText": "Ludaj ĝisdatigoj", + "getCoinsText": "Ekhavi monerojn", + "holidaySpecialText": "Feria specialaĵo", + "howToSwitchCharactersText": "(iru al \"${SETTINGS} -> ${PLAYER_PROFILES}\" por konfiguri kaj adapti rolulojn)", + "loadErrorText": "Ne eblas ŝargi paĝon.\nKontrolu vian retkonekton.", + "loadingText": "ŝargas", + "mapsText": "Mapoj", + "miniGamesText": "Ludetoj", + "oneTimeOnlyText": "(nur unufoje)", + "purchaseAlreadyInProgressText": "Aĉeto de ĉi tio jam okazas.", + "purchaseConfirmText": "Ĉu aĉeti ${ITEM}?", + "purchaseNotValidError": "Aĉeto ne validas.\nKontaktu ${EMAIL} se tio estas eraro.", + "purchaseText": "Aĉeti", + "saleBundleText": "Kombin-rabato!", + "saleExclaimText": "Rabato!", + "salePercentText": "(${PERCENT} malpli)", + "saleText": "RABATE", + "searchText": "Serĉi", + "teamsFreeForAllGamesText": "Ludoj teamaj / Ĉiuj kune", + "winterSpecialText": "Vintra specialaĵo", + "youOwnThisText": "- jam havas tion -" + }, + "storeDescriptionText": "Festluda frenezo kun 8 ludantoj!\n\nEksplodigu viajn amikojn (aŭ la komputilon) en turniro da eksplodemaj ludetoj kiel Kaptu la flagon, Bomba hokeo kaj Brila-malrapida-Ĝis-la-morto!!\n\nSimplaj regumoj kaj bonega regila subteno faciligas 8ope ekagi; vi eĉ povas uzi viajn poŝaparatojn kiel regiloj per la senpaga aplikaĵo 'BombSquad Remote'!\n\nEkbombu!\n\nVidu www.froemling.net/bombsquad por pliaj informoj.", + "storeDescriptions": { + "blowUpYourFriendsText": "Eksplodigu viajn amikojn.", + "competeInMiniGamesText": "Batalu en ludetoj kiaj kurado kaj flugado.", + "customize2Text": "Adaptu rolulojn, ludetojn kaj eĉ la sontrakon.", + "customizeText": "Adaptu rolulojn kaj kreu viajn proprajn ludlistojn da ludetoj.", + "sportsMoreFunText": "Sporto pli amuzas kun eksplodiloj.", + "teamUpAgainstComputerText": "Teame alfrontu la komputilon." + }, + "storeText": "Vendejo", + "teamsText": "Teamoj", + "telnetAccessGrantedText": "Telnet-aliro ebligita.", + "telnetAccessText": "Telnet-aliro detektita, ĉu permesi?", + "testBuildErrorText": "Ĉi tiu testkonstruo ne plu estas aktiva; bonvolu kontroli por nova versio.", + "testBuildText": "Testkonstruo", + "testBuildValidateErrorText": "Ne eblas validigi testkonstruon. (ĉu sen retkonekto?)", + "testBuildValidatedText": "Testkonstruo validas; ĝuu!", + "thankYouText": "Dankon pro via subteno! Ĝuu la ludon!!", + "threeKillText": "TRIOBLA MORTIGO!!", + "timeBonusText": "Tempopremio", + "timeElapsedText": "Tempo pasis", + "timeExpiredText": "Tempo elĉerpiĝis", + "tipText": "Konsilo", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Plej bonaj amikoj", + "tournamentCheckingStateText": "Kontrolante turniran staton; bonvolu atendi...", + "tournamentEndedText": "Ĉi tiu turniro finiĝis. Nova baldaŭ komenciĝos.", + "tournamentEntryText": "Eniri turniron", + "tournamentResultsRecentText": "Lastatempaj turniraj rezultoj", + "tournamentStandingsText": "Turniraj pozicioj", + "tournamentText": "Turniro", + "tournamentTimeExpiredText": "Turnira tempo elĉerpiĝis", + "tournamentsText": "Turniroj", + "translations": { + "characterNames": { + "Bernard": "Bernardo", + "Bones": "Ĝisostulo", + "Santa Claus": "Sankta Nikolao" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} trejniĝo", + "Infinite ${GAME}": "Senfina ${GAME}", + "Infinite Onslaught": "Senfina impeto", + "Infinite Runaround": "Senfina trakuro", + "Onslaught Training": "Impeta trejniĝo", + "Pro ${GAME}": "Profesia ${GAME}", + "Pro Football": "Profesia futbalo", + "Pro Onslaught": "Profesia impeto", + "Pro Runaround": "Profesia trairo", + "Rookie ${GAME}": "Komencanta ${GAME}", + "Rookie Football": "Komencanta futbalo", + "Rookie Onslaught": "Komencanta impeto", + "The Last Stand": "Ĝis la lasta", + "Uber ${GAME}": "Ega ${GAME}", + "Uber Football": "Futbalego", + "Uber Onslaught": "Impetego", + "Uber Runaround": "Trairego" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Estu la elektita dum tiom da tempo por venki.\nMortigu la elektitan por mem fariĝi.", + "Bomb as many targets as you can.": "Bombigu kiel eble plej da celoj.", + "Carry the flag for ${ARG1} seconds.": "Portu la flagon dum ${ARG1} sekundoj.", + "Carry the flag for a set length of time.": "Portu la flagon dum difinita tempo.", + "Crush ${ARG1} of your enemies.": "Detruu ${ARG1} el viaj malamikoj.", + "Defeat all enemies.": "Superu ĉiujn malamikojn.", + "Dodge the falling bombs.": "Evitu la falantajn bombojn.", + "Final glorious epic slow motion battle to the death.": "Fina glora brila malrapida batalo ĝis la morto.", + "Get the flag to the enemy end zone.": "Portu la flagon ĝis la malamika finregiono.", + "How fast can you defeat the ninjas?": "Kiom rapide vi povas privenki la ŝinobojn.", + "Kill a set number of enemies to win.": "Mortigu difinitan kvanton da malamikoj por venki.", + "Last one standing wins.": "La lasta restanta venkas.", + "Last remaining alive wins.": "La lasta kiu ankoraŭ vivas, venkas.", + "Last team standing wins.": "Lasta teamo restanta venkas.", + "Prevent enemies from reaching the exit.": "Malhelpu al malamikoj atingi la elirejon.", + "Reach the enemy flag to score.": "Atingu la malamikan flagon por poenti.", + "Return the enemy flag to score.": "Reportu la malamikan flagon por poenti.", + "Run ${ARG1} laps.": "Kuru ${ARG1} ĉirkaŭirojn.", + "Run ${ARG1} laps. Your entire team has to finish.": "Kuru ${ARG1} ĉirkaŭirojn. Via tuta teamo devos alveni.", + "Run 1 lap.": "Kuru 1 ĉirkaŭiron.", + "Run 1 lap. Your entire team has to finish.": "Kuru 1 ĉirkaŭiron. Via tuta teamo devos alveni.", + "Run real fast!": "Kuru tre rapide!", + "Score ${ARG1} goals.": "Poentu ${ARG1} golojn.", + "Score ${ARG1} touchdowns.": "Poentu ${ARG1} postliniaĵojn.", + "Score a goal.": "Poentu golon.", + "Score a touchdown.": "Poentu postliniaĵon.", + "Score some goals.": "Poentu golojn.", + "Secure all ${ARG1} flags.": "Sekurigu ĉiujn ${ARG1} flagojn.", + "Secure all flags on the map to win.": "Sekurigu ĉiujn flagojn sur la mapo por gajni.", + "Secure the flag for ${ARG1} seconds.": "Sekurigu la flagon por ${ARG1} sekundoj.", + "Secure the flag for a set length of time.": "Sekurigu la flagon por difinita daŭro.", + "Steal the enemy flag ${ARG1} times.": "Ŝtelu la malamikan flagon ${ARG1}-foje.", + "Steal the enemy flag.": "Ŝtelu la malamikan flagon.", + "There can be only one.": "Nur povas esti unu.", + "Touch the enemy flag ${ARG1} times.": "Tuŝu la malamikan flagon ${ARG1}-foje.", + "Touch the enemy flag.": "Tuŝu la malamikan flagon.", + "carry the flag for ${ARG1} seconds": "portu la flagon dum ${ARG1} sekundoj", + "kill ${ARG1} enemies": "mortigu ${ARG1} malamikojn", + "last one standing wins": "la lasta restanta venkas", + "last team standing wins": "lasta teamo restanta venkas", + "return ${ARG1} flags": "reportu ${ARG1} flagojn", + "return 1 flag": "reportu 1 flagon", + "run ${ARG1} laps": "kuru ${ARG1} ĉirkaŭirojn", + "run 1 lap": "kuru 1 ĉirkaŭiron", + "score ${ARG1} goals": "poentu ${ARG1} golojn", + "score ${ARG1} touchdowns": "poentu ${ARG1} postliniaĵojn", + "score a goal": "poentu golon", + "score a touchdown": "poentu postliniaĵon", + "secure all ${ARG1} flags": "sekurigu ĉiujn ${ARG1} flagojn", + "secure the flag for ${ARG1} seconds": "sekurigu la flagon por ${ARG1} sekundoj", + "touch ${ARG1} flags": "tuŝu ${ARG1} flagojn", + "touch 1 flag": "tuŝu 1 flagon" + }, + "gameNames": { + "Assault": "Sturmo", + "Capture the Flag": "Kaptu la flagon", + "Chosen One": "La elektita", + "Conquest": "Konkero", + "Death Match": "Ĝis la morto", + "Elimination": "Eliminado", + "Football": "Futbalo", + "Hockey": "Hokeo", + "Keep Away": "Fortenu", + "King of the Hill": "Reĝo de la monto", + "Meteor Shower": "Meteora pluvo", + "Ninja Fight": "Ŝinoba batalo", + "Onslaught": "Impeto", + "Race": "Konkurso", + "Runaround": "Trairo", + "Target Practice": "Cel-praktikado", + "The Last Stand": "Ĝis la lasta" + }, + "inputDeviceNames": { + "Keyboard": "Klavaro", + "Keyboard P2": "Klavaro Lud2" + }, + "languages": { + "Chinese": "Ĉina", + "Croatian": "Kroata", + "Czech": "Ĉeĥa", + "Danish": "Dana", + "Dutch": "Nederlanda", + "English": "Angla", + "Esperanto": "Esperanto", + "Finnish": "Finna", + "French": "Franca", + "German": "Germana", + "Gibberish": "Sensenca", + "Hungarian": "Hungara", + "Italian": "Itala", + "Japanese": "Japana", + "Korean": "Korea", + "Polish": "Pola", + "Portuguese": "Portugala", + "Russian": "Rusa", + "Spanish": "Hispana", + "Swedish": "Sveda" + }, + "leagueNames": { + "Bronze": "Bronza", + "Diamond": "Diamanta", + "Gold": "Ora", + "Silver": "Arĝenta" + }, + "mapsNames": { + "Big G": "Granda G", + "Bridgit": "Trapontu", + "Courtyard": "Korto", + "Crag Castle": "Brikkastelo", + "Doom Shroom": "Fungegaĉo", + "Football Stadium": "Futbala stadio", + "Happy Thoughts": "Feliĉaj pensoj", + "Hockey Stadium": "Hokea stadio", + "Lake Frigid": "Frosta lago", + "Monkey Face": "Simivizaĝo", + "Rampage": "Rultabulejo", + "Roundabout": "Rondiro", + "Step Right Up": "Supreniru", + "The Pad": "La platformo", + "Tip Top": "Piramido", + "Tower D": "Turo D", + "Zigzag": "Zigzago" + }, + "playlistNames": { + "Just Epic": "Nur brile", + "Just Sports": "Nur sportoj" + }, + "promoCodeResponses": { + "invalid promo code": "nevalida promocia kodo" + }, + "scoreNames": { + "Flags": "Flagoj", + "Goals": "Goloj", + "Score": "Poentoj", + "Survived": "Supervivis", + "Time": "Tempo", + "Time Held": "Tenodaŭro" + }, + "serverResponses": { + "BombSquad Pro unlocked!": "BombSquad Pro malŝlosiĝis!", + "Entering tournament...": "Enirante turniron...", + "Invalid promo code.": "Nevalida promocia kodo.", + "Invalid purchase.": "Nevalida aĉeto.", + "Max number of playlists reached.": "Atingiĝis maksimumo da ludlistoj.", + "Max number of profiles reached.": "Atingiĝis maksimumo da profiloj.", + "Purchase successful!": "Aĉeto sukcesa!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Vi ricevis ${COUNT} biletojn por ensaluti.\nRevenu morgaŭ por ricevi ${TOMORROW_COUNT}.", + "The tournament ended before you finished.": "La turniro finiĝis antaŭ ol vi finis.", + "This requires version ${VERSION} or newer.": "Ĉi tio petas version ${VERSION} aŭ pli nova.", + "You already own this!": "Vi jam posedas ĉi tion!", + "You don't have enough tickets for this!": "Vi ne havas sufiĉe da biletoj por ĉi tio!", + "You got ${COUNT} tickets!": "Vi havas ${COUNT} biletojn!", + "You got a ${ITEM}!": "Vi havas ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Vi promoviĝis al nova ligo; gratulojn!", + "You must wait a few seconds before entering a new code.": "Vi devas atendi kelkajn sekundojn antaŭ ol enigi novan kodon.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Vi rankis #${RANK} en la lasta turniro. Dankon por ludi!", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Via kopio de la ludo adaptiĝis.\nBonvolu reigi ajnajn ŝanĝojn kaj provu denove." + }, + "settingNames": { + "1 Minute": "1 minuto", + "1 Second": "1 sekundo", + "10 Minutes": "10 minutoj", + "2 Minutes": "2 minutoj", + "2 Seconds": "2 sekundoj", + "20 Minutes": "20 minutoj", + "4 Seconds": "4 sekundoj", + "5 Minutes": "5 minutoj", + "8 Seconds": "8 sekundoj", + "Balance Total Lives": "Samigu entutajn vivojn", + "Chosen One Gets Gloves": "La elektita havos glovojn", + "Chosen One Gets Shield": "La elektita havos ŝirmilon", + "Chosen One Time": "Daŭro de la elektita", + "Enable Impact Bombs": "Ebligi ekpafbombojn", + "Enable Triple Bombs": "Ebligi trioblajn bombojn", + "Epic Mode": "Brila modo", + "Flag Idle Return Time": "Reirtempo de senaktiva flago", + "Flag Touch Return Time": "Reirtempo de tuŝita flago", + "Hold Time": "Tenodaŭro", + "Kills to Win Per Player": "Po mortigoj por venki", + "Laps": "Ĉirkaŭiroj", + "Lives Per Player": "Vivoj laŭ ludanto", + "Long": "Longa", + "Longer": "Pli longa", + "Mine Spawning": "Minaj ekaperoj", + "No Mines": "Sen minoj", + "None": "Neniom", + "Normal": "Normala", + "Respawn Times": "Reventempo", + "Score to Win": "Poenti por venki", + "Short": "Mallonga", + "Shorter": "Pli mallonga", + "Solo Mode": "Unuopa modo", + "Target Count": "Kiom da celoj", + "Time Limit": "Templimo" + }, + "statements": { + "Killing ${NAME} for skipping part of the track!": "${NAME} mortas pro nefaro de parto de la trako!" + }, + "teamNames": { + "Bad Guys": "Fiuloj", + "Blue": "Blua", + "Good Guys": "Bonuloj", + "Red": "Ruĝa" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Perfekttempa kur-salt-rotaci-bato povas mortigi en unu bato kaj\ngajnigi al vi eternan respekton de viaj amikoj.", + "Always remember to floss.": "Purigante viajn dentojn, ne forgesu la malfacilajn partojn.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Kreu ludanto-profilojn por vi kaj viaj amikoj kun\nviaj preferataj nomoj kaj aperoj anstataŭ uzi hazardajn.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Malbeno-kestoj ŝanĝas vin en tempobombon.\nLa sola kuracilo estas rapide kapti sanopakaĵon.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Malgraŭ iliaj aspektoj, la ebloj de la roluloj estas tutsamaj,\ndo simple elektu kiun ajn plej similan al vi.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Ne tro atendu de la energiŝirmilo; vi ankoraŭ povas fali de deklivo.", + "Don't run all the time. Really. You will fall off cliffs.": "Ne kuru la tutan tempon. Vere. Vi falos de deklivoj.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Premadu ajnan butonon por kuri. (La malantaŭaj pafbutonoj bone funkcias se vi havas ilin)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Premadu ajnan butonon por kuri. Vi pli rapide atingos\nlokojn sed ne bone turniĝos, do atentu pri deklivoj.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Glacibomboj ne estas tre potencaj, sed ili frostigos\nĉiujn kiujn ili tuŝas, tiel ebligante ke ili dispeciĝu.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Se iu prenas vin, batu al tiu kaj vi povos foriri.\nTio ankaŭ funkcias en la vera vivo.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Se vi havas maltro da regiloj, instalu la aplikaĵon 'BombSquad Remote'\nen viaj iOS-aparatoj kaj Android por uzi ilin kiel regiloj.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Se glubombo ekgluas al vi, eksaltu kaj rotaciadu. Eble vi tiel seniĝos de la bombo\nkaj almenaŭ viaj lastaj momentoj estos amuzaj.", + "If you kill an enemy in one hit you get double points for it.": "Se vi mortigas malamikon tuj unuafoje, vi duoble poentas.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Se vi prenas malbenon, via sola espero por supervivi\nestas trovi sanpakaĵon en la sekvaj sekundetoj.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Se vi restas en unu loko, vi tuj foros. Kuru kaj evitu por travivi..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Se multaj ludantoj venas kaj iras, enŝaltu 'aŭtomate-forigu-neaktivajn-ludantojn'\nsub la agordoj, kaze ke iu forgesas foriri en la ludo.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Se via aparato tro varmiĝas aŭ vi volas konservi baterian potencon,\nmalpliigu \"Vidaĵoj\" aŭ \"Distingivo\" en Agordoj->Grafikaĵoj", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Se la ludo malrapide montras la ekranojn, provu malpliigi\nla distingivon aŭ vidaĵojn en la grafikaĵo-agordoj.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "En Kaptu la flagon, via propra flago devas esti en via bazo por poenti. Se la alia\nteamo preskaŭ poentas, ŝteli ilian flagon povas esti bona maniero haltigi ilin.", + "In hockey, you'll maintain more speed if you turn gradually.": "En hokeo, via rapideco restas pli konstanta se vi laŭgrade turnas.", + "It's easier to win with a friend or two helping.": "Pli facilas venki se unu aŭ du amikoj helpas.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Saltu kiam vi ĵetas por meti bombojn je la plej altajn nivelojn.", + "Land-mines are a good way to stop speedy enemies.": "Minoj estas bona maniero haltigi rapidegajn malamikojn.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Multajn aferojn vi povas preni kaj ĵeti, eĉ aliajn ludantojn. Ĵeti viajn malamikojn\nde deklivo povas esti efika kaj emocie plenuma strategio.", + "No, you can't get up on the ledge. You have to throw bombs.": "Ne, ne eblas suriri la bretojn. Vi devas ĵeti bombojn.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Ludantoj povas aliĝi kaj foriri meze de plej da ludoj,\nkaj vi ankaŭ povas enmeti kaj forigi regilojn dum la ludo.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Ludantoj povas aliĝi kaj foriri meze de plejmulto da ludoj,\nkaj vi ankaŭ povas elpreni kaj aldoni regilojn dum la ludo.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Donacetoj nur havas tempolimojn en kunlaboraj ludoj.\nEn teamaj ludoj kaj Ĉiuj kune vi povas uzi ilin ĝismorte.", + "Practice using your momentum to throw bombs more accurately.": "Trejnu vian movefikon por ĵeti bombojn pli precize.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Batoj pli damaĝas se viaj pugnoj pli rapide moviĝas, do\nprovu kuri, salti kaj rotacii kiel frenezulo.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Kuru tien kaj reen antaŭ ĵeti bombon por\nrotaciadumi ĝin kaj ĵeti pli for.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Forigu grupon da malamikoj metante\nbombon proksime de dinamitujo.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "La kapo estas la plej sentema regiono, do glubombo\nal la okulujo kutime signifas ĝis ĝis.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Ĉi tiu nivelo neniam finiĝas, sed altaj poentoj ĉi tie\ngajnigos al vi eternan respekton tra la mondo.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Ĵetoforto estas bazita je la tenata direkto.\nPor ĵeti ion dolĉe antaŭ vi, ne tenu direkton.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Ĉu lacigas vin la sono? Faru mem sontrakojn!\nVidu Agordoj->Aŭdaĵoj->Sontrako", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Provu 'trankviligi' bombojn por unuj du sekundoj antaŭ ol ĵeti ilin.", + "Try tricking enemies into killing eachother or running off cliffs.": "Provu igi viajn malamikojn mortigi unu la alian aŭ kuri de deklivoj.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Uzu la preno-buteno por preni la flagon < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Balancu tien kaj reen por havi pli da distanco en viaj ĵetoj..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Vi povas 'celi' viajn batojn rotaciante maldekstren aŭ dekstren.\nTio utilas por puŝi fiulojn de randoj aŭ por poenti en hokeo.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Vi povas scii kiam bombo eksplodos laŭ la koloro\nde la fajreroj je la drato: flava..oranĝa..ruĝa..BUM.", + "You can throw bombs higher if you jump just before throwing.": "Vi povas ĵeti bombojn pli alten se vi saltas ĝuste antaŭ ĵeti.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Vi damaĝas vin se vi kapbatas kontraŭ aferoj,\ndo provu ne kapbati kontraŭ aferoj.", + "Your punches do much more damage if you are running or spinning.": "Viaj batoj pli damaĝas se vi kuras aŭ rotacias." + } + }, + "trialPeriodEndedText": "Via provperiodo finiĝis. Ĉu vi ŝatus aĉeti\nBombSquad kaj pluludi?", + "trophiesText": "Trofeoj", + "tutorial": { + "cpuBenchmarkText": "Rapidege irante tra la klarigo (precipe por testi la rapidecon de CPU)", + "phrase01Text": "Saluton!", + "phrase02Text": "Bonvenon al BombSquad!", + "phrase03Text": "Jen kelkaj konsiloj por regi vian rolulon:", + "phrase04Text": "Multaj aferoj en BombSquad estas bazitaj je FIZIKO.", + "phrase05Text": "Ekzemple, se vi batas,...", + "phrase06Text": "...damaĝo estas bazita je la rapideco de viaj pugnoj.", + "phrase07Text": "Ĉu vi vidis? Ni ne movis, do tio apenaŭ damaĝis al ${NAME}", + "phrase08Text": "Nun ni eksaltu kaj rotaciu por havi pli da rapideco.", + "phrase09Text": "Jen, jam pli bone.", + "phrase10Text": "Kurado ankaŭ helpas.", + "phrase11Text": "Premadu AJNAN butonon por kuri.", + "phrase12Text": "Por pli mojosaj batoj, provu kuri KAJ rotacii.", + "phrase13Text": "Ups; pardonu pri tio ${NAME}.", + "phrase14Text": "Vi povas kapti kaj ĵeti aferojn kiel flagoj.. aŭ ${NAME}.", + "phrase15Text": "Laste, estas bomboj.", + "phrase16Text": "Necesas trejniĝi por ĵeti bombojn.", + "phrase17Text": "Aj! Ne vere bona ĵeto.", + "phrase18Text": "Movi helpas vin ĵeti pli for.", + "phrase19Text": "Saltado helpas vin ĵeti pli alten.", + "phrase20Text": "Rotaciadumu viajn bombojn por eĉ pli longaj ĵetoj.", + "phrase21Text": "Tempumi viajn bombojn povas esti tikla.", + "phrase22Text": "Fek.", + "phrase23Text": "Provu \"trankviligi\" la ŝnuron por unuj du sekundoj.", + "phrase24Text": "Hura! Bone farite.", + "phrase25Text": "Nu, jen pli-malpli ĉio.", + "phrase26Text": "Nun ek al la venk', mojosul'!", + "phrase27Text": "Memoru vian trejnadon, kaj vi JA revenos viva!", + "phrase28Text": "...nu, eble...", + "phrase29Text": "Bonan ŝancon!", + "randomName1Text": "Zamĉjo", + "randomName2Text": "Joĉjo", + "randomName3Text": "Klara", + "randomName4Text": "Ivo", + "randomName5Text": "Osmo", + "skipConfirmText": "Ĉu vi vere volas transsalti la klarigon? Tuŝu aŭ premu por konfirmi.", + "skipVoteCountText": "${COUNT} el ${TOTAL} voĉdonoj por transsalti", + "skippingText": "saltante klarigon...", + "toSkipPressAnythingText": "(tuŝu aŭ premu ion ajn por transsalti klarigon)" + }, + "twoKillText": "DUOBLA MORTIGO!", + "unavailableText": "nehavebla", + "unconfiguredControllerDetectedText": "Detektis nekonfiguritan regilon:", + "unlockThisInTheStoreText": "Ĉi tio devas malŝlosiĝi en la vendejo.", + "unsupportedHardwareText": "Pardonu, ĉi tiu aparato ne estas subtenata de ĉi tiu konstruo de la ludo.", + "upFirstText": "Venos unue:", + "upNextText": "Venos poste en ludo ${COUNT}:", + "updatingAccountText": "Ĝisdatigante vian konton...", + "upgradeToPlayText": "Malŝlosu \"${PRO}\" en la enluda vendejo por ludi ĉi tion.", + "useDefaultText": "Uzi ekvaloron", + "usesExternalControllerText": "Ĉi tiu ludo uzas eksteran regilon por enigo.", + "usingItunesText": "Uzante iTunes por sontrako...", + "usingItunesTurnRepeatAndShuffleOnText": "Bonvolu certigi ke miksmodo estu AKTIVA kaj ripetmodo je ĈIUJ en iTunes.", + "validatingBetaText": "Validigas betan...", + "validatingTestBuildText": "Kontrolas validecon de testkonstruo...", + "victoryText": "Venko!", + "vsText": "kontraŭ", + "waitingForHostText": "(atendas ${HOST} por pluiri)", + "waitingForLocalPlayersText": "atendante lokajn ludantojn...", + "waitingForPlayersText": "atendas aliĝon de ludantoj...", + "watchAnAdText": "Spekti reklamon", + "watchWindow": { + "deleteConfirmText": "Ĉu forviŝi \"${REPLAY}\"?", + "deleteReplayButtonText": "Forviŝi\nremontron", + "myReplaysText": "Miaj remontroj", + "noReplaySelectedErrorText": "Vi ne selektis remontron", + "renameReplayButtonText": "Renomi\nremontron", + "renameReplayText": "Ŝanĝi nomon de \"${REPLAY}\" al:", + "renameText": "Renomi", + "replayDeleteErrorText": "Eraro forviŝante remontron.", + "replayNameText": "Remontra nomo", + "replayRenameErrorAlreadyExistsText": "Jam ekzistas remontro kun tiu nomo.", + "replayRenameErrorInvalidName": "Ne povas renomi remontron; nomo ne validas.", + "replayRenameErrorText": "Eraro renomante remontron.", + "sharedReplaysText": "Remontroj kun aliaj", + "titleText": "Spekti", + "watchReplayButtonText": "Spekti\nremontron" + }, + "waveText": "Ondo", + "wellSureText": "Nu certe!", + "wiimoteLicenseWindow": { + "titleText": "Kopirajto DarwiinRemote" + }, + "wiimoteListenWindow": { + "listeningText": "Aŭskultante por Wii-regiloj...", + "pressText": "Premu la Wii-regilajn butonojn 1 kaj 2 samtempe.", + "pressText2": "En pli novaj Wii-regiloj kun Motion Plus en ili, anstataŭe premu la ruĝan butonon 'sync' dorsflanke." + }, + "wiimoteSetupWindow": { + "copyrightText": "Kopirajto DarwiinRemote", + "listenText": "Aŭskulti", + "macInstructionsText": "Certigu ke via Wii estas malŝaltitia kaj Bludento estas\naktiva en via Makintoŝo, kaj premu 'Aŭskulti'. Wii-regila\nsubteno povas esti iom fuŝa, do foje reprovu kelkfoje\npor ekhavi konekton.\n\nBludento devus subteni ĝis 7 konektitaj aparatoj, sed\nvia sperto povus esti alia.\n\nBombSquad subtenas la originajn Wii-regilojn, Nunchuk\nkaj la klasikan regilon.\nLa pli nova Wii Remote Plus nun ankaŭ funcias\nsed sen etendaĵoj.", + "thanksText": "Dankon al la teamo DarwiinRemote\npor ebligi ĉi tion.", + "titleText": "Agordado de Wii-regilo" + }, + "winsPlayerText": "${NAME} venkas!", + "winsTeamText": "${NAME} venkas!", + "winsText": "${NAME} venkas!", + "worldScoresUnavailableText": "Mondaj poentoj nehaveblas.", + "worldsBestScoresText": "Mondaj plej bonaj poentoj", + "worldsBestTimesText": "Mondaj plej bonaj tempoj", + "xbox360ControllersWindow": { + "getDriverText": "Ekhavi pelilon", + "macInstructions2Text": "Por uzi regilojn sendrate, vi ankaŭ bezonos la ricevilon kiu\nvenas kun 'Xbox 360 Wireless Controller for Windows'.\nUnu ricevilo permesas konekti ĝis 4 regilojn.\n\nGrave: riceviloj de ekstera deveno ne funkcios kun ĉi tiu pelilo;\ncertigu vin ke la ricevilo diras 'Microsoft' kaj ne 'XBOX 360'.\nMicrosoft ne plu vendas ĉi tiujn dise, do vi bezonos ekhavi tiun\nkiu venas kune kun la regilo aŭ serĉi per vendaj retejoj kiel eBay.\n\nSe vi trovas ĉi tion utila, bonvolu konsideri mondonon al la pelila\nprogramisto per lia retejo.", + "macInstructionsText": "Por uzi la regilojn Xbox 360, vi devas instali la pelilon\npor Makintoŝo havebla je la malsupra ligilo.\nĜi funkcias kaj por drataj kaj sendrataj regiloj.", + "ouyaInstructionsText": "Por uzi dratajn regilojn de Xbox 360 kun BombSquad, simple\nkonektu ilin al la USB-eniro de via aparato. Vi povas uzi USB-nabon\npor konekti al pluraj regiloj.\n\nPor uzi sendratajn regilojn, ‎vi bezonas sendratan ricevilon,\nhaveblan kiel parto de la pakaĵo \"Xbox 360 wireless Controller\nfor Windows\" aŭ vendite dise. Ĉiu ricevilo eniras USB-pordon kaj\npermesas al vi konekti ĝis 4 sendratajn regilojn.", + "titleText": "Uzi regilojn de Xbox 360 kun BombSquad:" + }, + "yesAllowText": "Jes, permesu!", + "yourBestScoresText": "Viaj plej bonaj poentoj", + "yourBestTimesText": "Viaj plej bonaj tempoj" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/french.json b/dist/ba_data/data/languages/french.json new file mode 100644 index 0000000..c05b372 --- /dev/null +++ b/dist/ba_data/data/languages/french.json @@ -0,0 +1,1955 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Les noms de compte ne peuvent pas contenir d'emoji ou d'autres caractères spéciaux", + "accountProfileText": "(profil compte)", + "accountsText": "Comptes", + "achievementProgressText": "Succès: ${COUNT} sur ${TOTAL}", + "campaignProgressText": "Progression de la campagne [Difficile]: ${PROGRESS}", + "changeOncePerSeason": "Vous ne pouvez changer cela qu'une fois par saison.", + "changeOncePerSeasonError": "Vous devez attendre la prochaine saison pour le modifier à nouveau (${NUM} jours)", + "customName": "Nom personnalisé", + "linkAccountsEnterCodeText": "Entrer code", + "linkAccountsGenerateCodeText": "Générez votre code", + "linkAccountsInfoText": "(partager la progression entre différentes plateformes)", + "linkAccountsInstructionsNewText": "Pour lier deux comptes, générez un code sur le premier\net entrez ce code sur la seconde. Les données du\nle second compte sera ensuite partagé entre les deux.\n(Les données du premier compte seront perdues)\n\nVous pouvez lier jusqu' à ${COUNT} comptes.\n\nIMPORTANT: ne lier que les comptes que vous possédez; et\nSi vous créez un lien avec des comptes d'amis, vous ne pourrez pas\npouvoir jouer en ligne en même temps.", + "linkAccountsInstructionsText": "Pour lier deux comptes, générez un code sur l'un\nd'entre eux et entrer ce code sur l'autre.\nLes progrès et l'inventaire seront combinés.\nVous pouvez lier jusqu'à ${COUNT} comptes.\n\nIMPORTANT: reliez uniquement les comptes que vous possédez!\nSi vous reliez des comptes à vos amis, vous\nne pourrais pas jouer en même temps!\n\nAussi: cela ne peut actuellement pas être annulé, alors faites attention!", + "linkAccountsText": "Lier vos comptes", + "linkedAccountsText": "Comptes Liés:", + "nameChangeConfirm": "Changer le nom de votre compte pour ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Ceci réinitialisera votre progression co-op et\nvos meilleurs scores locaux (mais pas vos tickets).\nCette action est irréversible. Êtes-vous certain(e)?", + "resetProgressConfirmText": "Ceci réinitialisera votre progression co-op,\nvos succès, et vos meilleurs scores\nlocaux (mais pas vos tickets). Cette action\nest irréversible. Êtes-vous certain(e)?", + "resetProgressText": "Réinitialiser votre progression", + "setAccountName": "Définir le nom du compte", + "setAccountNameDesc": "Sélectionnez le nom à afficher pour votre compte.\nVous pouvez utiliser le nom d'un de vos comptes\n associés ou créer un nom personnalisé unique.", + "signInInfoText": "Connectez-vous pour gagner des tickets, participer aux tournois en ligne,\net partager votre progression entre plusieurs appareils.", + "signInText": "Connexion", + "signInWithDeviceInfoText": "(un compte généré automatiquement utilisable seulement sur cet appareil)", + "signInWithDeviceText": "Connectez-vous avec le compte de cet appareil", + "signInWithGameCircleText": "Connectez-vous avec Game Circle", + "signInWithGooglePlayText": "Connectez-vous avec Google Play", + "signInWithTestAccountInfoText": "(ancien compte; utilisez les comptes de cet appareil pour les prochaines fois)", + "signInWithTestAccountText": "Connectez-vous avec un compte test", + "signOutText": "Se déconnecter", + "signingInText": "Connexion...", + "signingOutText": "Déconnexion...", + "testAccountWarningCardboardText": "Attention: vous êtes connectés avec un compte \"test\".\nIl va être remplacé par un comte Google lorsque ces\nderniers seront supportés dans les apps cardboard.\n\nPour l'instant vous devrez gagner les tickets en jeu.\n(vous pourrez cependant obtenir l'amélioration BombSquad Pro gratuitement)", + "testAccountWarningOculusText": "Attention: vous vous connectez avec un compte \"test\".\nIl sera remplacé par un compte Oculus plus tard cette année\nce qui incluera l'achat de tickets et de bien d'autres choses.\n\nPour l'instant, vous devez gagner les tickets en jeu.\n(Vous pourrez tout de même bénéficier de BombSquad Pro gratuitement)", + "testAccountWarningText": "Attention: vous vous connectez avec un compte \"test\".\nCe compte est relié à une machine spécifique et peut\nêtre effacé périodiquement. (ne passez trop de temps \ndessus pour débloquer/collecter des trucs svp)\n\nLancer une version officielle du jeu pour utiliser un\n\"vrai\" compte (Game-Center, Google Plus, etc.)\nCela vous permettra aussi de sauvegarder votre\nprogression dans le cloud et le partager avec vos\nautres machines. ", + "ticketsText": "Tickets: ${COUNT}", + "titleText": "Compte", + "unlinkAccountsInstructionsText": "Sélectionnez un compte à délier", + "unlinkAccountsText": "Délier des comptes", + "viaAccount": "(via le compte ${NAME})", + "youAreLoggedInAsText": "Vous êtes connecté en temps que :", + "youAreSignedInAsText": "Vous êtes connecté(e) en tant que:" + }, + "achievementChallengesText": "Succès", + "achievementText": "Succès", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Éliminer 3 méchants avec une caisse de TNT", + "descriptionComplete": "Vous avez éliminé 3 méchants avec une caisse de TNT", + "descriptionFull": "Éliminer 3 méchants avec une caisse de TNT dans ${LEVEL}", + "descriptionFullComplete": "Vous avez éliminé 3 méchants avec une caisse de TNT dans ${LEVEL}", + "name": "La dynamite fait \"Boum\"" + }, + "Boxer": { + "description": "Gagner sans utiliser de bombes", + "descriptionComplete": "Vous avez gagné sans utiliser de bombes", + "descriptionFull": "Finir le niveau ${LEVEL} sans utiliser de bombes", + "descriptionFullComplete": "Vous avez fini ${LEVEL} sans utiliser de bombes", + "name": "Boxeur" + }, + "Dual Wielding": { + "descriptionFull": "Connecter 2 manettes (hardware ou app)", + "descriptionFullComplete": "Vous avez connecté 2 manettes (hardware ou app)", + "name": "Ambidextre" + }, + "Flawless Victory": { + "description": "Gagner sans égratinures", + "descriptionComplete": "Vous avez gagné sans une égratinure", + "descriptionFull": "Gagner ${LEVEL} sans égratinures", + "descriptionFullComplete": "Vous avez gagné ${LEVEL} sans une égratinure", + "name": "Victoire parfaite" + }, + "Free Loader": { + "descriptionFull": "Lancer une mêlée générale avec au moins 2 joueurs", + "descriptionFullComplete": "Vous avez lancé une mêlée générale avec au moins 2 joueurs", + "name": "Lanceurs en plus" + }, + "Gold Miner": { + "description": "Tuer 6 méchants avec des mines", + "descriptionComplete": "Vous avez tué 6 méchants avec des mines", + "descriptionFull": "Tuer 6 méchants avec des mines dans ${LEVEL}", + "descriptionFullComplete": "Vous avez tué 6 méchants avec des mines dans ${LEVEL}", + "name": "Chercheur d'Or" + }, + "Got the Moves": { + "description": "Gagner sans utiliser de bombes ou de gants de boxe", + "descriptionComplete": "Vous avez gagné sans utiliser de bombes ou de gants de boxe", + "descriptionFull": "Gagner ${LEVEL} sans utiliser de bombes ou de gants de boxe", + "descriptionFullComplete": "Vous avez gagné ${LEVEL} sans utiliser de bombes ou de gants de boxe", + "name": "Chorégraphe" + }, + "In Control": { + "descriptionFull": "Connecter une manette (hardware ou app)", + "descriptionFullComplete": "Vous avez connecté une manette. (hardware ou app)", + "name": "Sous Contrôle" + }, + "Last Stand God": { + "description": "Obtenir 1000 points", + "descriptionComplete": "Vous avez obtenu 1000 points", + "descriptionFull": "Obtenir 1000 point dans ${LEVEL}", + "descriptionFullComplete": "Vous avez obtenu 1000 points dans ${LEVEL}", + "name": "Dieu de ${LEVEL}" + }, + "Last Stand Master": { + "description": "Obtenir 250 points", + "descriptionComplete": "Vous avez obtenu 250 points", + "descriptionFull": "Obtenir 250 points dans ${LEVEL}", + "descriptionFullComplete": "Vous avez obtenu 250 points dans ${LEVEL}", + "name": "Maître de ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Obtenir 500 points", + "descriptionComplete": "Vous avez obtenu 500 points", + "descriptionFull": "Obtenir 500 points dans ${LEVEL}", + "descriptionFullComplete": "Vous avez obtenu 500 points dans ${LEVEL}", + "name": "Magicien de ${LEVEL}" + }, + "Mine Games": { + "description": "Tuer 3 méchants avec des mines", + "descriptionComplete": "Vous avez tué 3 méchants avec des mines", + "descriptionFull": "Tuer 3 méchants avec des mines dans ${LEVEL}", + "descriptionFullComplete": "Vous avez tué 3 méchants avec des mines dans ${LEVEL}", + "name": "Démineur" + }, + "Off You Go Then": { + "description": "Jeter 3 méchants hors du champ de bataille", + "descriptionComplete": "Vous avez jeté 3 méchants hors du champ de bataille", + "descriptionFull": "Jeter 3 méchants hors du champ de bataille dans ${LEVEL}", + "descriptionFullComplete": "Vous avez jeté 3 méchants hors du champ de bataille dans ${LEVEL}", + "name": "Bon Débarras" + }, + "Onslaught God": { + "description": "Obtenir 5000 points", + "descriptionComplete": "Vous avez obtenu 5000 points", + "descriptionFull": "Obtenir 5000 points dans ${LEVEL}", + "descriptionFullComplete": "Vous avez obtenu 5000 points dans ${LEVEL}", + "name": "Dieu de ${LEVEL}" + }, + "Onslaught Master": { + "description": "Obtenir 500 points", + "descriptionComplete": "Vous avez obtenu 500 points", + "descriptionFull": "Obtenir 500 points dans ${LEVEL}", + "descriptionFullComplete": "Vous avez obtenu 500 points dans ${LEVEL}", + "name": "Maître de ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Survivre à toutes les vagues", + "descriptionComplete": "Vous avez survécu à toutes les vagues", + "descriptionFull": "Survivre à toutes les vagues dans ${LEVEL}", + "descriptionFullComplete": "Vous avez survécu à toutes les vagues dans ${LEVEL}", + "name": "Victoire à ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Obtenir 1000 points", + "descriptionComplete": "Vous avez obtenu 1000 points", + "descriptionFull": "Obtenir 1000 points dans ${LEVEL}", + "descriptionFullComplete": "Vous avez obtenu 1000 points dans ${LEVEL}", + "name": "Magicien de ${LEVEL}" + }, + "Precision Bombing": { + "description": "Gagner sans utiliser de bonus", + "descriptionComplete": "Vous avez gagné sans utiliser de bonus", + "descriptionFull": "Gagner ${LEVEL} sans utiliser de bonus", + "descriptionFullComplete": "Vous avez gagné ${LEVEL} sans utiliser de bonus", + "name": "Bombardement précis" + }, + "Pro Boxer": { + "description": "Gagner sans utiliser de bombes", + "descriptionComplete": "Vous avez gagné sans utiliser de bombes", + "descriptionFull": "Compléter ${LEVEL} sans utiliser de bombes", + "descriptionFullComplete": "Vous avez complété ${LEVEL} sans utiliser de bombes", + "name": "Boxeur Professionnel" + }, + "Pro Football Shutout": { + "description": "Gagner sans laisser les méchants marquer", + "descriptionComplete": "Vous avez gagné sans laisser les méchants marquer", + "descriptionFull": "Gagner ${LEVEL} sans laisser les méchants marquer", + "descriptionFullComplete": "Vous avez gagné ${LEVEL} sans laisser les méchants marquer", + "name": "${LEVEL} Blanchissage" + }, + "Pro Football Victory": { + "description": "Gagner le match", + "descriptionComplete": "Vous avez gagné le match", + "descriptionFull": "Gagner le match dans ${LEVEL}", + "descriptionFullComplete": "Vous avez gagné le match dans ${LEVEL}", + "name": "Victoire dans ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Survivre à toutes les vagues", + "descriptionComplete": "Vous avez survécu à toutes les vagues", + "descriptionFull": "Survivre à toutes les vagues dans ${LEVEL}", + "descriptionFullComplete": "Vous avez survécu à toutes les vagues dans ${LEVEL}", + "name": "Victoire dans ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Compléter toutes les vagues", + "descriptionComplete": "Vous avez complété toutes les vagues", + "descriptionFull": "Compléter toutes les vagues dans ${LEVEL}", + "descriptionFullComplete": "Vous avez complété toutes les vagues dans ${LEVEL}", + "name": "Victoire dans ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Gagner sans laisser les méchants marquer", + "descriptionComplete": "Vous avez gagné sans laisser les méchants marquer", + "descriptionFull": "Gagner ${LEVEL} sans laisser les méchants marquer", + "descriptionFullComplete": "Vous avez gagné ${LEVEL} sans laisser les méchants marquer", + "name": "Blanchissage dans ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Gagner le match", + "descriptionComplete": "Vous avez gagné le match", + "descriptionFull": "Gagner le match dans ${LEVEL}", + "descriptionFullComplete": "Vous avez gagné le match dans ${LEVEL}", + "name": "Victoire dans ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Survivre à toutes les vagues", + "descriptionComplete": "Vous avez survécu à toutes les vagues", + "descriptionFull": "Vaincre toutes les vagues dans ${LEVEL}", + "descriptionFullComplete": "Vous avez vaincu toutes les vagues dans ${LEVEL}", + "name": "Victoire dans ${LEVEL}" + }, + "Runaround God": { + "description": "Obtenir 2000 points", + "descriptionComplete": "Vous avez obtenu 2000 points", + "descriptionFull": "Obtenir 2000 points dans ${LEVEL}", + "descriptionFullComplete": "Vous avez obtenu 2000 points dans ${LEVEL}", + "name": "Dieu de ${LEVEL}" + }, + "Runaround Master": { + "description": "Obtenir 500 points", + "descriptionComplete": "Vous avez obtenu 500 points", + "descriptionFull": "Obtenir 500 points dans ${LEVEL}", + "descriptionFullComplete": "Vous avez obtenu 500 points dans ${LEVEL}", + "name": "Maitre de ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Obtenir 1000 points", + "descriptionComplete": "Vous avez obtenu 1000 points", + "descriptionFull": "Obtenir 1000 points dans ${LEVEL}", + "descriptionFullComplete": "Vous avez obtenu 1000 points dans ${LEVEL}", + "name": "Magicien de ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Partager le jeu avec un ami", + "descriptionFullComplete": "Vous avez partagé le jeu avec un ami", + "name": "Partager c'est aimer" + }, + "Stayin' Alive": { + "description": "Gagner sans mourir", + "descriptionComplete": "Vous avez gagné sans mourir", + "descriptionFull": "Gagner ${LEVEL} sans mourir", + "descriptionFullComplete": "Vous avez gagné ${LEVEL} sans mourir", + "name": "Rester en vie" + }, + "Super Mega Punch": { + "description": "Infliger 100% de dommage avec un seul coup de poing", + "descriptionComplete": "Vous avez infligé 100% de dommage avec un seul coup de poing", + "descriptionFull": "Infliger 100% de dommage avec un seul coup de poing dans ${LEVEL}", + "descriptionFullComplete": "Vous avez infligé 100% de dommage avec un seul coup de poing dans ${LEVEL}", + "name": "One Punch Man" + }, + "Super Punch": { + "description": "Infliger 50% de dommage avec un seul coup de poing", + "descriptionComplete": "Vous avez infligé 50% de dommage avec un seul coup de poing", + "descriptionFull": "Infliger 50% de dommage avec un seul coup de poing dans ${LEVEL}", + "descriptionFullComplete": "Infliger 50% de dommage avec un seul coup de poing dans ${LEVEL}", + "name": "Poing d'acier" + }, + "TNT Terror": { + "description": "Tuer 6 méchants avec de la TNT", + "descriptionComplete": "Vous avez tué 6 méchants avec de la TNT", + "descriptionFull": "Tuer 6 méchants avec de la TNT dans ${LEVEL}", + "descriptionFullComplete": "Vous avez tué 6 méchants avec de la TNT dans ${LEVEL}", + "name": "Feu d'Artifice" + }, + "Team Player": { + "descriptionFull": "Lancer un jeu en équipe avec au moins 4 joueurs", + "descriptionFullComplete": "Vous avez lancé un jeu en équipe avec au moins 4 joueurs", + "name": "Esprit d'Équipe" + }, + "The Great Wall": { + "description": "Arrêter tous les méchants", + "descriptionComplete": "Vous avez arrêté tous les méchants", + "descriptionFull": "Arrêter tous les méchants dans ${LEVEL}", + "descriptionFullComplete": "Vous avez arrêté tous les méchants dans ${LEVEL}", + "name": "La Grande Muraille" + }, + "The Wall": { + "description": "Arrêter tous les méchants", + "descriptionComplete": "Vous avez arrêté tous les méchants", + "descriptionFull": "Arrêter tous les méchants dans ${LEVEL}", + "descriptionFullComplete": "Vous avez arrêté tous les méchants dans ${LEVEL}", + "name": "La Muraille" + }, + "Uber Football Shutout": { + "description": "Gagner sans laisser les méchants marqués", + "descriptionComplete": "Vous avez gagné sans laisser les méchants marqués", + "descriptionFull": "Gagner ${LEVEL} sans laisser les méchants marqués", + "descriptionFullComplete": "Vous avez gagné ${LEVEL} sans laisser les méchants marqués", + "name": "Blanchissage dans ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Gagner le match", + "descriptionComplete": "Vous avez gagné le match", + "descriptionFull": "Gagner le match dans ${LEVEL}", + "descriptionFullComplete": "Vous avez gagné le match dans ${LEVEL}", + "name": "Victoire dans ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Survivre à toutes les vagues", + "descriptionComplete": "Vous avez survécu à toutes les vagues", + "descriptionFull": "Survivre à toutes les vagues dans ${LEVEL}", + "descriptionFullComplete": "Vous avez survécu à toutes les vagues dans ${LEVEL}", + "name": "Victoire dans ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Compléter toutes les vagues", + "descriptionComplete": "Vous avez complété toutes les vagues", + "descriptionFull": "Compléter toutes les vagues dans ${LEVEL}", + "descriptionFullComplete": "Vous avez complété toutes les vagues dans ${LEVEL}", + "name": "Victoire dans ${LEVEL}" + } + }, + "achievementsRemainingText": "Succès restant à remporter :", + "achievementsText": "Succès", + "achievementsUnavailableForOldSeasonsText": "Désolé, les spécifications des succès ne sont pas disponibles pour les saisons passées.", + "addGameWindow": { + "getMoreGamesText": "Obtenir plus de jeux...", + "titleText": "Ajouter un Jeu" + }, + "allowText": "Autoriser", + "alreadySignedInText": "Votre compte est connecté sur un autre appareil;\n s'il vous plait changez les comptes ou fermez le jeu sur \nles autres appareils et rééssayez", + "apiVersionErrorText": "Impossible de charger le jeu ${NAME}; sa version api est ${VERSION_USED}; nous demandons la version ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" s'active seulement quand un casque est branché)", + "headRelativeVRAudioText": "Son à position relative (RV)", + "musicVolumeText": "Volume de la musique", + "soundVolumeText": "Volume des bruitages", + "soundtrackButtonText": "Bandes son", + "soundtrackDescriptionText": "(Ecouter votre propre musique durant les matchs)", + "titleText": "Son" + }, + "autoText": "Auto", + "backText": "Retour", + "banThisPlayerText": "Bannir ce joueur", + "bestOfFinalText": "Finale du meilleur en ${COUNT} points", + "bestOfSeriesText": "Premier à ${COUNT} points:", + "bestOfUseFirstToInstead": 1, + "bestRankText": "Votre meilleur est #${RANK}", + "bestRatingText": "Votre meilleure évaluation est ${RATING}", + "betaErrorText": "Cette version beta n'est plus active, veuillez rechercher la nouvelle version.", + "betaValidateErrorText": "Impossible de valider la beta. (pas de connection internet?)", + "betaValidatedText": "Version bêta validée ; bon jeu !", + "bombBoldText": "BOMBE", + "bombText": "Bombe", + "boostText": "Accroître", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} est configuré dans sa propre application.", + "buttonText": "bouton", + "canWeDebugText": "Voulez-vous que BombSquad envoie automatiquement un rapport des bugs, \ncrash et certaines informations relatives au jeu au développeur?\n\nCes rapports ne contiendront aucune information personnelle \net aideront à maintenir un jeu sans bug ni ralentissements.", + "cancelText": "Annuler", + "cantConfigureDeviceText": "Désolé, ${DEVICE} ne peut pas être configuré.", + "challengeEndedText": "Ce défi est terminé.", + "chatMuteText": "Tchat muet", + "chatMutedText": "Tchat muet", + "chatUnMuteText": "Réactiver tchat", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Vous devez compléter ce \nniveau pour continuer!", + "completionBonusText": "Bonus de fin", + "configControllersWindow": { + "configureControllersText": "Configurer les Manettes", + "configureGamepadsText": "Configurer les Manettes", + "configureKeyboard2Text": "Configurer Clavier J2", + "configureKeyboardText": "Configurer le Clavier", + "configureMobileText": "Appareils Mobiles comme Manettes", + "configureTouchText": "Configurer l'Ecran Tactile", + "ps3Text": "Manettes de PS3", + "titleText": "Mannettes", + "wiimotesText": "Wiimotes", + "xbox360Text": "Manettes Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Note: le support varie selon l'appareil et la version Android.", + "pressAnyButtonText": "Appuyez sur n'importe quel bouton du support\nque vous voulez configurer...", + "titleText": "Configurer les manettes" + }, + "configGamepadWindow": { + "advancedText": "Avancé", + "advancedTitleText": "Configuration Avancée de la manette", + "analogStickDeadZoneDescriptionText": "(augmentez ceci si votre personnage bouge quand vous relachez le stick)", + "analogStickDeadZoneText": "Zone Morte du Stick Analogique", + "appliesToAllText": "(s'applique à toutes les manettes de ce type)", + "autoRecalibrateDescriptionText": "(activez ceci si votre personnage ne se déplace pas à vitesse normale)", + "autoRecalibrateText": "Recalibrage-Auto du Stick Analogique", + "axisText": "axe", + "clearText": "effacer", + "dpadText": "croix directionnelle", + "extraStartButtonText": "Bouton Start supplémentaire", + "ifNothingHappensTryAnalogText": "Si rien ne se passe, essayez plutôt d'assigner le stick analogique à la place.", + "ifNothingHappensTryDpadText": "Si rien ne se passe, essayez plutôt d'assigner à la croix directionnelle à la place.", + "ignoreCompletelyDescriptionText": "(empêcher cette manette d'interférer avec le jeu ou les menus)", + "ignoreCompletelyText": "Ignorer complètement", + "ignoredButton1Text": "Bouton Ignoré 1", + "ignoredButton2Text": "Bouton Ignoré 2", + "ignoredButton3Text": "Bouton Ignoré 3", + "ignoredButton4Text": "Bouton Ignoré 4", + "ignoredButtonDescriptionText": "(utilisez ceci pour empêcher les boutons 'home' ou 'sync' d’interférer avec l'interface)", + "ignoredButtonText": "Bouton Ignoré", + "pressAnyAnalogTriggerText": "Appuyer sur n'importe quelle commande analogique...", + "pressAnyButtonOrDpadText": "Appuyez sur n'importe quel bouton ou croix directionnelle...", + "pressAnyButtonText": "Appuyez sur n'importe quel bouton...", + "pressLeftRightText": "Appuyez à gauche ou à droite...", + "pressUpDownText": "Appuyez en haut ou en bas...", + "runButton1Text": "Appuyez sur le Bouton 1", + "runButton2Text": "Appuyez sur le Bouton 2", + "runTrigger1Text": "Appuyez sur la Gâchette 1", + "runTrigger2Text": "Appuyez sur la Gâchette 2", + "runTriggerDescriptionText": "(les gachettes analogiques vous permettent de courir à des vitesses variables)", + "secondHalfText": "Utilisez ceci pour configurer la deuxième \nmoitié d'un système de 2-manettes-en-un\nqui se comportent comme une seule manette.", + "secondaryEnableText": "Activer", + "secondaryText": "Manette Secondaire", + "startButtonActivatesDefaultDescriptionText": "(décochez ceci si votre bouton start est considéré comme un bouton 'menu')", + "startButtonActivatesDefaultText": "Le Bouton Start Active Le Widget Par Défaut", + "titleText": "Configuration de la Manette", + "twoInOneSetupText": "Configuration de la Manette 2-en-1", + "uiOnlyDescriptionText": "(empêcher cette manette de rejoindre une partie)", + "uiOnlyText": "Limiter cette manette à l’utilisation du menu seulement", + "unassignedButtonsRunText": "Tous Les Boutons Non-Assignés Fonctionnent", + "unsetText": "", + "vrReorientButtonText": "Bouton de réorientation RV" + }, + "configKeyboardWindow": { + "configuringText": "Configuration de ${DEVICE}", + "keyboard2NoteText": "Attention: la plupart des claviers ne peuvent enregistrer \nune multitude de commandes en même temps. Il est donc préférable \nd'avoir un deuxième clavier branché à l'ordinateur. Mais, \nmême dans ce cas, il faudra assigner des touches différentes \npour les deux joueurs." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Taille des boutons d'actions", + "actionsText": "Actions", + "buttonsText": "boutons", + "dragControlsText": "< bougez les commandes pour les repositionner >", + "joystickText": "Joystick", + "movementControlScaleText": "Taille des boutons de mouvements", + "movementText": "Mouvement", + "resetText": "Réinitialiser", + "swipeControlsHiddenText": "Masquer les Icônes de Contrôle en mode libre", + "swipeInfoText": "Le style 'Libre' demande un peu de pratique pour s'y habituer mais\nrend les contrôles plus intuitifs en évitant de regarder les touches à l'écran.", + "swipeText": "Libre", + "titleText": "Configurer l'Écran Tactile", + "touchControlsScaleText": "Taille des boutons tactiles" + }, + "configureItNowText": "Configurer maintenant?", + "configureText": "Configurer", + "connectMobileDevicesWindow": { + "amazonText": "Appstore Amazon", + "appStoreText": "App Store", + "bestResultsText": "Pour de meilleurs résultats, il est nécessaire d'avoir un wifi qui fonctionne\ncorrectement. Vous pouvez réduire les ralentissements de votre wifi en\ndésactivant les autres appareils utilisant le wifi, en jouant à côté de l'émetteur\nwifi, ou en vous connectant à l'hôte directement par ethernet.", + "explanationText": "Pour utiliser un smartphone ou une tablette comme manette, installez \nl'application \"${REMOTE_APP_NAME}\" sur l'appareil. Plusieurs appareils \npeuvent se connecter à la partie ${APP_NAME} par WiFi, et c'est gratuit!", + "forAndroidText": "pour Android:", + "forIOSText": "pour iOS:", + "getItForText": "Téléchargez ${REMOTE_APP_NAME} pour iOS dans l'App Store d'Apple\nou pour Android dans le Google Play Store ou l'Amazon Appstore.", + "googlePlayText": "Google Play", + "titleText": "Utiliser des Appareils Mobiles comme manettes:" + }, + "continuePurchaseText": "Continuer pour ${PRICE}?", + "continueText": "Continuer", + "controlsText": "Commandes", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Ceci ne s'applique pas au classement permanent.", + "activenessInfoText": "Ce bonus multiplicateur augmente lorsque vous jouez\net baisse quand vous ne jouez pas.", + "activityText": "Activité", + "campaignText": "Campagne", + "challengesInfoText": "Gagnez des prix en remportant des mini-jeux.\n\nLes prix et la difficulté des niveaux augmentent\ndès qu'un défi est remporté et diminue\nlorsqu'un défi expire ou est abandonné.", + "challengesText": "Défis", + "currentBestText": "Meilleur Score Actuel", + "customText": "Personnaliser", + "entryFeeText": "Accès", + "forfeitConfirmText": "Abandonner ce défi?", + "forfeitNotAllowedYetText": "Vous ne pouvez pas abandonner ce défi pour le moment.", + "forfeitText": "Abandonner", + "multipliersText": "Bonus Multiplicateurs", + "nextChallengeText": "Prochain Défi", + "nextPlayText": "Prochain Jeu", + "ofTotalTimeText": "de ${TOTAL}", + "playNowText": "Jouer Maintenant", + "pointsText": "Points", + "powerRankingFinishedSeasonUnrankedText": "(saison terminée non-classé)", + "powerRankingNotInTopText": "(pas dans les meilleurs ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} pts", + "powerRankingPointsMultText": "(x ${NUMBER} pts)", + "powerRankingPointsText": "${NUMBER} pts", + "powerRankingPointsToRankedText": "(${CURRENT} de ${REMAINING} pts)", + "powerRankingText": "Classement Mondial", + "prizesText": "Prix", + "proMultInfoText": "Les joueurs en version ${PRO} \nreçoivent ${PERCENT}% de points en plus.", + "seeMoreText": "Plus...", + "skipWaitText": "Ne pas attendre", + "timeRemainingText": "Temps Restant", + "titleText": "Co-op", + "toRankedText": "Avant d'Être Classé", + "totalText": "total", + "tournamentInfoText": "Commencez la course au meilleur score\navec les joueurs de votre ligue.\n\nLes prix seront décernés à la fin du tournoi \naux joueurs ayant totalisés le score le plus haut.", + "welcome1Text": "Bienvenue à ${LEAGUE}. Vous pouvez améliorer votre\nrang en gagnant des étoiles, en complétant des\nsuccès et en gagnant des trophées durant les tournois.", + "welcome2Text": "Vous pouvez aussi gagner des tickets en participant à bien d'autres activités.\nLes tickets sont utiles pour débloquer de nouveaux personnages, \ndes nouvelles cartes, mini-jeux, participer à des tournois et bien plus.", + "yourPowerRankingText": "Votre Classement Mondial:" + }, + "copyOfText": "Copie de ${NAME}", + "copyText": "Copier", + "copyrightText": "© 2013 Eric Froemling", + "createAPlayerProfileText": "Créer un profil de joueur?", + "createEditPlayerText": "", + "createText": "Créer", + "creditsWindow": { + "additionalAudioArtIdeasText": "Son Additionnel, Design Initial, et Idées par ${NAME}", + "additionalMusicFromText": "Musique additionnelle par ${NAME}", + "allMyFamilyText": "Toute ma famille et mes amis qui m'ont aidés à tester le jeu", + "codingGraphicsAudioText": "Codage, Graphiques, et Audio par ${NAME}", + "languageTranslationsText": "Traductions:", + "legalText": "Légal:", + "publicDomainMusicViaText": "Musique du domaine publique par ${NAME}", + "softwareBasedOnText": "Ce logiciel est basé en partie du travail de ${NAME}", + "songCreditText": "${TITLE} Exécutée par ${PERFORMER}\nComposé par ${COMPOSER}, Arrangé par ${ARRANGER}, Publié par ${PUBLISHER},\nCourtoisie de ${SOURCE}", + "soundAndMusicText": "Son et Musique:", + "soundsText": "Sons (${SOURCE}):", + "specialThanksText": "Remerciement Spécial:", + "thanksEspeciallyToText": "Remerciement surtout à ${NAME}", + "titleText": "Crédits de ${APP_NAME}", + "whoeverInventedCoffeeText": "Celui qui a inventé le café" + }, + "currentStandingText": "Votre Classement Actuel #${RANK}", + "customizeText": "Customiser...", + "deathsTallyText": "${COUNT} morts", + "deathsText": "Morts", + "debugText": "déboguer", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Note: il est recommandé de changer Paramètres->Graphismes->Textures à 'Élevé' pour tester ceci.", + "runCPUBenchmarkText": "Lancer le test CPU (processeur)", + "runGPUBenchmarkText": "Lancer le test GPU (carte graphique)", + "runMediaReloadBenchmarkText": "Lancer le test de Media-Reload", + "runStressTestText": "Test de robustèsse", + "stressTestPlayerCountText": "Nombre de Joueurs", + "stressTestPlaylistDescriptionText": "Playlist des tests de robustèsse", + "stressTestPlaylistNameText": "Nom de la Playlist", + "stressTestPlaylistTypeText": "Genre de la Playlist", + "stressTestRoundDurationText": "Durée du Niveau", + "stressTestTitleText": "Test de robustèsse", + "titleText": "Tests graphiques/processeur & de robustèsse", + "totalReloadTimeText": "Temps de redémarrage total: ${TIME} (référez-vous au registre plus de détails)", + "unlockCoopText": "Débloquer les niveaux co-op" + }, + "defaultFreeForAllGameListNameText": "Partie Mélée-Générale par défaut", + "defaultGameListNameText": "Playlist ${PLAYMODE} Défaut", + "defaultNewFreeForAllGameListNameText": "Mes parties Mélée-Générale", + "defaultNewGameListNameText": "Ma Playlist ${PLAYMODE}", + "defaultNewTeamGameListNameText": "Mes parties en équipes", + "defaultTeamGameListNameText": "Partie en Equipes par défaut", + "deleteText": "Supprimer", + "demoText": "Démo", + "denyText": "Refuser", + "desktopResText": "Résolution de l'Ordinateur", + "difficultyEasyText": "Facile", + "difficultyHardOnlyText": "Mode Difficile Seulement", + "difficultyHardText": "Difficile", + "difficultyHardUnlockOnlyText": "Ce niveau ne peut être débloqué qu'en mode difficile.\nPensez-vous être à la hauteur!?!?!", + "directBrowserToURLText": "Entrez cette URL dans un navigateur:", + "disableRemoteAppConnectionsText": "Désactiver les connexions d'applications-manettes", + "disableXInputDescriptionText": "Permet plus que 4 manettes mais risque à malfonctionner.", + "disableXInputText": "Désactiver XInput", + "doneText": "Terminé", + "drawText": "Égalité", + "duplicateText": "Dupliquer", + "editGameListWindow": { + "addGameText": "Ajouter\nUn jeu", + "cantOverwriteDefaultText": "Vous ne pouvez pas remplacer la playlist par défaut!", + "cantSaveAlreadyExistsText": "Une playlist existe déjà avec ce nom!", + "cantSaveEmptyListText": "Vous ne pouvez pas sauvegarder une playlist vide!", + "editGameText": "Modifer\nce Match", + "gameListText": "Liste de Parties", + "listNameText": "Nom de la Playlist", + "nameText": "Nom", + "removeGameText": "Enlever\nCe jeu", + "saveText": "Sauvegarder la Playlist", + "titleText": "Éditeur de Playlist" + }, + "editProfileWindow": { + "accountProfileInfoText": "Ce profil spécial à un nom \net une icône basés sur votre compte. \n\n${ICONS}\n\nCréez des profils personnalisés pour \nutiliser d'autres noms et icônes.", + "accountProfileText": "(profil du compte)", + "availableText": "Le nom \"${NAME}\" est disponible.", + "changesNotAffectText": "Note: les changements n'auront pas d'effet sur les personnages déjà présent dans le jeu", + "characterText": "personnage", + "checkingAvailabilityText": "Vérifier la disponibilité pour \"${NAME}\"...", + "colorText": "couleur", + "getMoreCharactersText": "Obtenir plus de personnages...", + "getMoreIconsText": "Obtenir plus d'icônes...", + "globalProfileInfoText": "Chaque profil Mondial de chaque joueur est sûr d'avoir un unique nom\ndans le monde entier. De même pour les icônes personnalisées.", + "globalProfileText": "(profil Mondial)", + "highlightText": "Variante", + "iconText": "icône", + "localProfileInfoText": "Un profil Local ne possède pas d'icône ou d'un unique nom \ndans le monde entier. Passez à un profil Mondial\npour réserver un unique nom et y ajouter une icône personnalisée.", + "localProfileText": "(profil local)", + "nameDescriptionText": "Nom du Joueur", + "nameText": "Nom", + "randomText": "aléatoire", + "titleEditText": "Éditer ce Profil", + "titleNewText": "Nouveau Profil", + "unavailableText": "\"${NAME}\" n'est pas disponible; essayez un autre nom.", + "upgradeProfileInfoText": "Ceci vous réserve le droit à un nom de joueur unique dans le monde\net vous permettre d'y assigner une icône personnalisée.", + "upgradeToGlobalProfileText": "Passer à un Profil Mondial" + }, + "editProfilesAnyTimeText": "(vous pouvez éditer les profils à tout moment dans 'paramètres')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Vous ne pouvez pas effacer la bande sonore par défaut.", + "cantEditDefaultText": "Vous ne pouvez pas modifier la bande sonore par défaut. Dupliquez-la ou créez-en une nouvelle.", + "cantEditWhileConnectedOrInReplayText": "Impossible d'éditer la bande-son en étant connecté à un groupe ou en visionnant un replay.", + "cantOverwriteDefaultText": "Vous ne pouvez pas remplacer la bande sonore par défaut", + "cantSaveAlreadyExistsText": "Une bande sonore avec ce nom existe déjà!", + "copyText": "Copier", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Bande sonore par défaut", + "deleteConfirmText": "Effacer la Bande Sonore:\n\n'${NAME}'?", + "deleteText": "Effacer la \nBande Sonore", + "duplicateText": "Dupliquer la\nBande Sonore", + "editSoundtrackText": "Éditeur de Bandes Sonore", + "editText": "Modifier la\nBande Sonore", + "fetchingITunesText": "chercher des playlists Music App", + "musicVolumeZeroWarning": "Attention: le volume de la musique est à 0", + "nameText": "Nom", + "newSoundtrackNameText": "Ma Bande Sonore ${COUNT}", + "newSoundtrackText": "Nouvelle Bande Sonore:", + "newText": "Nouvelle\nBande Sonore", + "selectAPlaylistText": "Séléctionner une Playlist", + "selectASourceText": "Source de la Musique", + "soundtrackText": "Musique", + "testText": "test", + "titleText": "Bandes Sonores", + "useDefaultGameMusicText": "Musique par Défaut du Jeu", + "useITunesPlaylistText": "Playlist Music App", + "useMusicFileText": "Fichier Musique (mp3, etc)", + "useMusicFolderText": "Dossier de Fichiers Musicaux" + }, + "editText": "Éditer", + "endText": "Terminé", + "enjoyText": "Amusez-vous Bien!", + "epicDescriptionFilterText": "${DESCRIPTION} Dans un \"slow-motion\" épique.", + "epicNameFilterText": "${NAME} Épique", + "errorAccessDeniedText": "accès refusé", + "errorOutOfDiskSpaceText": "pas d'éspace sur le disque", + "errorText": "Erreur", + "errorUnknownText": "erreur inconnue", + "exitGameText": "Quitter ${APP_NAME}?", + "exportSuccessText": "'${NAME}' exporté.", + "externalStorageText": "Stockage Externe", + "failText": "Échec", + "fatalErrorText": "Oh zut; quelque chose manque ou est endommagé.\nEssayez de réinstaller l'application ou \ncontactez ${EMAIL} pour de l'aide.", + "fileSelectorWindow": { + "titleFileFolderText": "Sélectionner un Fichier ou un Dossier", + "titleFileText": "Sélectionner un Fichier", + "titleFolderText": "Sélectionner un Dossier", + "useThisFolderButtonText": "Utiliser ce Dossier" + }, + "filterText": "Filtre", + "finalScoreText": "Score Final", + "finalScoresText": "Scores Finaux", + "finalTimeText": "Temps Final", + "finishingInstallText": "L'installation se termine; un moment s'il vous plaît...", + "fireTVRemoteWarningText": "* Pour une meilleure expérience, utilisez \ndes manettes ou installez l'application\n'${REMOTE_APP_NAME}' sur vos smartphones et \ntablettes.", + "firstToFinalText": "Final du premier à ${COUNT} points", + "firstToSeriesText": "Le premier à ${COUNT} points", + "fiveKillText": "QUINTUPLE MEURTRE!!!", + "flawlessWaveText": "Vague Parfaite!", + "fourKillText": "QUADRUPLE MEURTRE!!!", + "freeForAllText": "Mélée générale", + "friendScoresUnavailableText": "Les scores de vos amis sont indisponibles.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Les ${COUNT} meilleurs du jeu", + "gameListWindow": { + "cantDeleteDefaultText": "Vous ne pouvez pas effacer la playlist par défaut.", + "cantEditDefaultText": "Vous ne pouvez pas modifier la playlist par défaut! Dupliquez-la ou créez-en une nouvelle.", + "cantShareDefaultText": "Partage de la playlist par défaut impossible", + "deleteConfirmText": "Effacer \"${LIST}\"?", + "deleteText": "Effacer\nLa Playlist", + "duplicateText": "Dupliquer\nLa Playlist", + "editText": "Modifier\nLa Playlist", + "gameListText": "Liste de jeu", + "newText": "Nouvelle\nPlaylist", + "showTutorialText": "Voir le Tutoriel", + "shuffleGameOrderText": "Mélanger l'ordre des jeux", + "titleText": "Personnaliser les Playlists ${TYPE}" + }, + "gameSettingsWindow": { + "addGameText": "Ajouter un Match" + }, + "gamepadDetectedText": "1 manette détecté", + "gamepadsDetectedText": "${COUNT} manettes détectés.", + "gamesToText": "${WINCOUNT} jeux à ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Rappel: n'importe quelle appareil peut supporter\nplus d'un joueur si vous avez plusieurs manettes.", + "aboutDescriptionText": "Utilisez ces onglets pour créer une partie.\n\nLes parties vous permettent de faire des jeux et \ntournois avec vos amis entre différents appareils.\n\nUtilisez le bouton ${PARTY} en haut à droite pour\ndiscuter et interagir avec votre partie. (sur une \nmanette, appuyez sur ${BUTTON} lorsque vous êtes dans le menu)", + "aboutText": "Info", + "addressFetchErrorText": "", + "appInviteInfoText": "Invitez vos amis pour jouer BombSquad et \nils gagneraient ${COUNT} billets. Vous \ngagneriez ${YOU_COUNT} pour chaque ami.", + "appInviteMessageText": "${NAME} vous a envoyé ${COUNT} tickets sur ${APP_NAME}", + "appInviteSendACodeText": "Envoyer un code", + "appInviteTitleText": "Invitation à ${APP_NAME}", + "bluetoothAndroidSupportText": "(fonctionne avec n'importe quel appareil Android équipé de Bluetooth)", + "bluetoothDescriptionText": "Héberger/joindre une partie via Bluetooth:", + "bluetoothHostText": "Héberger via Bluetooth", + "bluetoothJoinText": "Joindre via Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "vérification...", + "copyCodeConfirmText": "Le code a bien été copié dans le presse-papier.", + "copyCodeText": "Copier le code", + "dedicatedServerInfoText": "Pour un meilleur résultat, créer un server dédié. Voir bombsquadgame.com/server pour plus d'info.", + "disconnectClientsText": "Ceci déconnectera le(s) ${COUNT} joueur(s)\nde votre partie. Êtes-vous sûr?", + "earnTicketsForRecommendingAmountText": "Vos amis recevront ${COUNT} tickets si ils essayent le jeu\n(et vous recevrez ${YOU_COUNT} pour chacun d'entre eux qui le feront)", + "earnTicketsForRecommendingText": "Partagez le jeu pour \ndes tickets gratuits...", + "emailItText": "Evoyer par e-mail", + "favoritesSaveText": "Mettre en favori", + "favoritesText": "Favoris", + "freeCloudServerAvailableMinutesText": "Le prochain serveur libre sera disponible dans ${MINUTES} minutes.", + "freeCloudServerAvailableNowText": "Un serveur libre est disponible !", + "freeCloudServerNotAvailableText": "Aucun serveur libre n'est disponible...", + "friendHasSentPromoCodeText": "${COUNT} tickets ${APP_NAME} de la part de ${NAME}", + "friendPromoCodeAwardText": "Vous recevrez ${COUNT} tickets à chaque fois qu'il sera utilisé.", + "friendPromoCodeExpireText": "Ce code expirera dans ${EXPIRE_HOURS} heures et ne fonctionne que pour les nouveaux joueurs.", + "friendPromoCodeInstructionsText": "Pour l'utiliser, ouvrez ${APP_NAME} puis aller dans \"Paramètres->Avancé->Entrer code\".\nAllez sur bombsquadgame.com pour les liens de téléchargement pour toutes les plateformes supportées.", + "friendPromoCodeRedeemLongText": "Peut être utilisé pour ${COUNT} tickets gratuits jusqu'à ${MAX_USES} personnes maximum.", + "friendPromoCodeRedeemShortText": "Il peut-être utilisé pour ${COUNT} tickets dans le jeu.", + "friendPromoCodeWhereToEnterText": "(dans \"Paramètres->Avancé->Entrer code\")", + "getFriendInviteCodeText": "Obtenir un Code pour Inviter mes Amis", + "googlePlayDescriptionText": "Invitez des joueurs Google Play à votre partie:", + "googlePlayInviteText": "Inviter", + "googlePlayReInviteText": "Le(s) ${COUNT} joueur(s) Google Play seront déconnectés \nsi vous faîtes une autre invitation. Incluez-les dans \nla nouvelle invitation pour continuer de jouer avec eux.", + "googlePlaySeeInvitesText": "Voir les Invitations", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Version Android / Google Play)", + "hostPublicPartyDescriptionText": "Héberger une partie publique", + "hostingUnavailableText": "Hébergement de serveur indisponible", + "inDevelopmentWarningText": "Note:\n\nLe jeu en réseau est une fonctionnalité encore en \ndéveloppement. Pour l'instant, il est recommandé \nque tous les joueurs soient sur le même réseau Wi-Fi.", + "internetText": "Internet", + "inviteAFriendText": "Vos amis n'ont pas le jeu? Invitez-les à\nl'essayer et ils recevront ${COUNT} tickets gratuit.", + "inviteFriendsText": "Invitez vos Amis", + "joinPublicPartyDescriptionText": "Rejoindre une Partie Publique", + "localNetworkDescriptionText": "Rejoindre une partie sur votre réseau (LAN, Bluetooth, Wi-Fi, etc.)", + "localNetworkText": "Réseau Local", + "makePartyPrivateText": "Rend Ma Partie Privée", + "makePartyPublicText": "Rend Ma Partie Publique", + "manualAddressText": "Adresse", + "manualConnectText": "Connexion", + "manualDescriptionText": "Joindre une partie par adresse:", + "manualJoinSectionText": "Rejoindre un utilisateur par Adresse IP", + "manualJoinableFromInternetText": "Peut-on vous rejoindre via internet?:", + "manualJoinableNoWithAsteriskText": "NON*", + "manualJoinableYesText": "OUI", + "manualRouterForwardingText": "*pour arranger ceci, configurez votre routeur pour rediriger le port UDP ${PORT} à votre adresse locale", + "manualText": "Manuel", + "manualYourAddressFromInternetText": "Votre adresse depuis internet:", + "manualYourLocalAddressText": "Votre adresse locale:", + "nearbyText": "Proche", + "noConnectionText": "", + "otherVersionsText": "(autres versions)", + "partyCodeText": "Code de la partie", + "partyInviteAcceptText": "Accepter", + "partyInviteDeclineText": "Refuser", + "partyInviteGooglePlayExtraText": "(référez-vous à l'onglet 'Google Play' dans 'Rassembler')", + "partyInviteIgnoreText": "Ignorer", + "partyInviteText": "${NAME} a vous invité \ndans sa partie!", + "partyNameText": "Nom de la Partie", + "partyServerRunningText": "Votre serveur est en train de fonctionner.", + "partySizeText": "nombre joueurs", + "partyStatusCheckingText": "Vérifie le status...", + "partyStatusJoinableText": "votre partie est maintenant joignable via l'internet", + "partyStatusNoConnectionText": "incapable à connecter au serveur", + "partyStatusNotJoinableText": "votre partie n'est pas joignable via l'internet", + "partyStatusNotPublicText": "votre partie n'est pas publique", + "pingText": "vitesse", + "portText": "Port", + "privatePartyCloudDescriptionText": "Les parties privées tournent dans les serveurs cloud dédiés; aucune configuration de routeur n'est requise", + "privatePartyHostText": "Héberger une partie privée", + "privatePartyJoinText": "Rejoindre une partie privée", + "privateText": "Privé", + "publicHostRouterConfigText": "Vous pourriez avoir besoin de configurer la redirection de port sur votre routeur. Pour une option plus facile, hébergez une partie privée.", + "publicText": "Public", + "requestingAPromoCodeText": "Demande d'un code...", + "sendDirectInvitesText": "Envoyez des invitations directes", + "shareThisCodeWithFriendsText": "Partagez ce code avec vos amis:", + "showMyAddressText": "Montrer mon adresse", + "startAdvertisingText": "Démarrer la diffussion", + "startHostingPaidText": "Héberger maintenant pour ${COST}", + "startHostingText": "Héberger", + "startStopHostingMinutesText": "Vous pourrez commencer et arrêter d'héberger une partie privée dans les ${MINUTES} prochaines minutes.", + "stopAdvertisingText": "Arrêter la diffusion", + "stopHostingText": "Arrêter d'héberger", + "titleText": "Rassembler", + "wifiDirectDescriptionBottomText": "Si tous les appareils ont un onglet 'Wi-Fi Direct', ils devraient être capables de \nse connecter entre eux. Quand tous les appareils sont connectés, vous pouvez créer des \nparties en utilisant l'onglet 'Réseau Local', comme pour un réseau Wi-Fi ordinaire. \n\nPour des résultats optimaux, l'hôte de ${APP_NAME} devrait être l'hôte de la partie.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct peux être utilisé pour connecter des appareils Android directement\nsans besoin un réseau Wi-Fi. Fonctionne mieux avec Android 4.2 ou plus récent.\n\nPour l'utiliser, ouvrez les paramètres Wi-Fi et cherchez 'Wi-Fi Direct' dans le menu.", + "wifiDirectOpenWiFiSettingsText": "Ouvrir les Paramètres Wi-Fi", + "wifiDirectText": "Direct Wi-Fi", + "worksBetweenAllPlatformsText": "(fonctionne avec toutes les plateformes)", + "worksWithGooglePlayDevicesText": "(fonctionne avec les appareils qui ont la version Google Play (android) du jeu)", + "youHaveBeenSentAPromoCodeText": "Vous avez reçu un code promo pour ${APP_NAME}:" + }, + "getCoinsWindow": { + "coinDoublerText": "Doubleur de pièces", + "coinsText": "${COUNT} Pièces", + "freeCoinsText": "Pièces gratuites", + "restorePurchasesText": "Actualiser les achats", + "titleText": "Avoir des pièces" + }, + "getTicketsWindow": { + "freeText": "GRATUIT!", + "freeTicketsText": "Tickets gratuit", + "inProgressText": "Une transaction est encore en cours; réessayez dans un insatant.", + "purchasesRestoredText": "Achats réinitialisés.", + "receivedTicketsText": "Vous avez reçu ${COUNT} tickets!", + "restorePurchasesText": "Restaurer les achats", + "ticketDoublerText": "Doubleur des Billets", + "ticketPack1Text": "Petit pack de tickets", + "ticketPack2Text": "Pack moyen de tickets", + "ticketPack3Text": "Gros pack de tickets", + "ticketPack4Text": "Pack de tickets géant", + "ticketPack5Text": "Énorme pack de tickets", + "ticketPack6Text": "Ultime pack de tickets", + "ticketsFromASponsorText": "Gagnez ${COUNT} tickets\nd'un sponsor", + "ticketsText": "${COUNT} Tickets", + "titleText": "Plus de tickets", + "unavailableLinkAccountText": "Désolé , les achats ne sont pas disponibles sur cette plateforme.\nSi vous voulez , vous pouvez lier ce compte à une\nautre plateforme et faire vos achats sur celle-ci.", + "unavailableTemporarilyText": "Non disponible pour le moment; merci de réessayez plus tard.", + "unavailableText": "Désolé, ceci n'est pas disponible.", + "versionTooOldText": "Désolé, cette version du jeu est trop vieille; télécharger la dernière version.", + "youHaveShortText": "vous avez ${COUNT}", + "youHaveText": "vous avez ${COUNT} tickets" + }, + "googleMultiplayerDiscontinuedText": "Désolé, le service multijoueur de Google n'est plus disponible.\nJe travaille sur un moyen de le remplacer aussi vite que possible.\nEn attendant, veuillez essayer une nouvelle méthode de connexion.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Toujours", + "fullScreenCmdText": "Plein Écran (Cmd-F)", + "fullScreenCtrlText": "Plein Écran (Ctrl-F)", + "gammaText": "Gamma", + "highText": "Élevé", + "higherText": "Très élevé", + "lowText": "Bas", + "mediumText": "Moyen", + "neverText": "Jamais", + "resolutionText": "Résolution", + "showFPSText": "Montrer les FPS", + "texturesText": "Textures", + "titleText": "Graphismes", + "tvBorderText": "Bordure TV", + "verticalSyncText": "Synchronisation Verticale", + "visualsText": "Visuels" + }, + "helpWindow": { + "bombInfoText": "- Bombe -\nPlus fort que les coups de poing, mais\npeut entraîner une grave automutilation.\nPour bien faire, jetez-la vers l'ennemi\navant que la mèche n'arrive au bout.", + "bombInfoTextScale": 0.6, + "canHelpText": "${APP_NAME} peut aider.", + "controllersInfoText": "Vous pouvez jouer à ${APP_NAME} avec des amis par réseau, ou vous \npouvez tous jouer sur le même appareil avec assez de contrôleurs. \n${APP_NAME} en supporte une variété; vous pouvez même utiliser des \nsmartphones comme contrôleurs avec l'app '${REMOTE_APP_NAME}' \ngratuite. Allez à Paramètres->Contrôleurs pour plus d'info.", + "controllersInfoTextFantasia": "Un joueur peut utiliser la télécommande comme manette, mais les\nmanettes sont vivement recommandées. Vous pouvez toujours utiliser\ndes systèmes iOS/Android comme manettes via l'application gratuite\n'BombSquad'. Allez voir 'Manettes' dans 'Paramètres' pour plus d'infos.", + "controllersInfoTextMac": "Un ou deux joueurs peuvent utiliser le clavier, mais Bombsquad est meilleur avec\nmanettes. BombSquad supporte des manettes USB, PS3, XBOX 360, Wiimotes\net des systèmes iOS/Android pour controler les personnages. J'espere que vous en avez. Allez voir 'Manettes' dans 'Paramètres' pour plus d'info.", + "controllersInfoTextOuya": "Vous pouvez utiliser des manettes OUYA, PS3, XBOX 360 et beaucoup \nd'autres manettes USB et Bluetooth avec BombSquad. Vous pouvez aussi \nutiliser des sytèmes iOS et Android comme manette gratuitement via \nl'application 'BombSquad Remote'. Allez voir 'Manettes' dans 'Paramètres' \npour plus d'info.", + "controllersText": "Manettes", + "controlsSubtitleText": "Votre personnage ${APP_NAME} possède plusieurs actions basiques:", + "controlsText": "Contrôles", + "devicesInfoText": "La version RV de ${APP_NAME} peut être jouée par réseau avec \nla version originale, alors utilisez vos smartphones, tablettes, \net ordinateurs et jouer ensemble. Il peut aussi être utile \nde connecter une version originale du jeu à la version RV \npour permettre aux gens autour de visionner l'action.", + "devicesText": "Appareils", + "friendsGoodText": "Il est bon d'en avoir. ${APP_NAME} est plus amusant avec plusieurs\njoueurs et peut en supporter jusqu'à 8 en même temps, ce qui nous mène à:", + "friendsText": "Amis", + "jumpInfoText": "- Sauter -\nSautez pour éviter les trous,\npour jeter des objets plus haut,\nou pour la beauté du geste.", + "jumpInfoTextScale": 0.6, + "orPunchingSomethingExtraSpace": 0, + "orPunchingSomethingText": "Ou frapper quelque chose, la jeter d'une falaise, et l'exploser en descendant avec une bombe gluante.", + "pickUpInfoText": "- Ramasser -\nSaisissez des drapeaux, ennemis, ou \ntout autre chose non boulonné au sol.\nAppuyez à nouveau pour lancer.", + "powerupBombDescriptionText": "Vous permet de lancer trois bombes \nd'affilée au lieu d'une seule.", + "powerupBombNameText": "Triple bombes", + "powerupCurseDescriptionText": "Vous ne voulez probablement pas y toucher.\n...ou peut-être que si?", + "powerupCurseNameText": "Malédiction", + "powerupHealthDescriptionText": "Vous restaure votre santé.\nVous n'aurez jamais pu deviner.", + "powerupHealthNameText": "Trousse de soin", + "powerupIceBombsDescriptionText": "Moins puissantes que les bombes normales \nmais congèlent vos ennemis et les \nrendent particulièrement cassant.", + "powerupIceBombsNameText": "Bombes glacées", + "powerupImpactBombsDescriptionText": "Un peu moins puissantes que les bombes \nnormales, elles explosent à l'impact.", + "powerupImpactBombsNameText": "Bombes d'impact", + "powerupLandMinesDescriptionText": "3 pour le prix d'une, \nutile pour se défendre \nou arrêter les ennemis rapides.", + "powerupLandMinesNameText": "Mines", + "powerupPunchDescriptionText": "Rend vos poings durs\ntrès, très durs.", + "powerupPunchNameText": "Gants de boxe", + "powerupShieldDescriptionText": "Absorbe les dommages\npour préserver votre santé.", + "powerupShieldNameText": "Bouclier d'énergie", + "powerupStickyBombsDescriptionText": "Collent à tout ce qu'elles touchent.\nFous rires garantis.", + "powerupStickyBombsNameText": "Bombes gluantes", + "powerupsSubtitleText": "Évidemment, aucun jeu n'est complet sans bonus:", + "powerupsSubtitleTextScale": 0.8, + "powerupsText": "Bonus", + "powerupsTextScale": 1.4, + "punchInfoText": "- Frapper -\nLes poings font plus de dégât \nplus vite ils bougent, donc courez\net tournoyez comme un fou.", + "punchInfoTextScale": 0.6, + "runInfoText": "- Courir -\nMaintenez n'importe quel bouton pour courir. Ceci fonctionne aussi avec les gâchettes. \nCourir est rapide, mais tourner devient difficile, attention aux falaises.", + "runInfoTextScale": 0.6, + "someDaysExtraSpace": 0, + "someDaysText": "Parfois vous désirez frapper quelque chose. Ou exploser quelque chose.", + "someDaysTextScale": 0.66, + "titleText": "Aide de ${APP_NAME}", + "toGetTheMostText": "Pour profiter à fond du jeu, vous aurez besoin:", + "toGetTheMostTextScale": 1.0, + "welcomeText": "Bienvenue dans ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} navigue dans les menus comme un chef -", + "importPlaylistCodeInstructionsText": "Utilisez le code suivant pour importer cette playlist ailleurs:", + "importPlaylistSuccessText": "${TYPE} playlist '${NAME}' importée", + "importText": "Importer", + "importingText": "Importation...", + "inGameClippedNameText": "dans le jeu sera\n\"${NAME}\"", + "installDiskSpaceErrorText": "ERREUR: Incapable de compléter l'installation.\nL'appareil manque paut-être d'espace.\nLibérez de l'espace et ressayez.", + "internal": { + "arrowsToExitListText": "appuyez sur ${LEFT} ou ${RIGHT} pour quitter la liste", + "buttonText": "bouton", + "cantKickHostError": "Impossible d’éjecter l'hôte.", + "chatBlockedText": "Tchat bloqué pour ${NAME} pendant ${TIME} secondes.", + "connectedToGameText": "Vous avez rejoint '${NAME}'.", + "connectedToPartyText": "Vous avez joint la partie de ${NAME}!", + "connectingToPartyText": "Connexion...", + "connectionFailedHostAlreadyInPartyText": "La connexion a échoué; l'hôte est dans une autre partie.", + "connectionFailedPartyFullText": "Connexion échouée; la partie est pleine.", + "connectionFailedText": "La connexion a échouée.", + "connectionFailedVersionMismatchText": "La connexion a échouée; l'hôte joue avec une version différente du jeu.\nVérifiez que vous avez la même version et réessayez.", + "connectionRejectedText": "La connexion a été rejetée.", + "controllerConnectedText": "${CONTROLLER} connecté.", + "controllerDetectedText": "1 manette détectée.", + "controllerDisconnectedText": "${CONTROLLER} déconnecté.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} déconnecté. Réessayez de le/la reconnecter.", + "controllerForMenusOnlyText": "Ce contrôleur ne peut pas être utilisé pour jouer; seulement pour naviguer les menus.", + "controllerReconnectedText": "${CONTROLLER} reconnecté.", + "controllersConnectedText": "${COUNT} manettes connectées.", + "controllersDetectedText": "${COUNT} manettes détectées.", + "controllersDisconnectedText": "${COUNT} manettes déconnectées.", + "corruptFileText": "Fichier(s) corrompu(s) détecté(s). Essayez de réinstaller, ou email à ${EMAIL} (En Anglais SVP)", + "errorPlayingMusicText": "Impossible de jouer la musique: ${MUSIC}", + "errorResettingAchievementsText": "Impossible de réinitialiser les succès en ligne; merci de réessayez plus tard.", + "hasMenuControlText": "${NAME} à le contrôle du menu.", + "incompatibleNewerVersionHostText": "L'hôte utilise une version plus récente du jeu. \nMettez à jour vers la dernière version et réessayez.", + "incompatibleVersionHostText": "L'hôte execute une version différente du jeu.\nVérifiez que vous êtes tous les deux à jour et réessayez.", + "incompatibleVersionPlayerText": "${NAME} execute une version différente du jeu.\nVérifiez que vous êtes tous les deux à jour et réessayez.", + "invalidAddressErrorText": "Erreur: adresse invalide.", + "invalidNameErrorText": "Erreur: nom invalide.", + "invalidPortErrorText": "Erreure:port invalide", + "invitationSentText": "Invitation envoyée.", + "invitationsSentText": "${COUNT} invitations envoyées.", + "joinedPartyInstructionsText": "Quelqu'un a rejoint votre partie.\nAllez à 'Jouer' pour commencer un match.", + "keyboardText": "Clavier", + "kickIdlePlayersKickedText": "${NAME} été éjecté pour inactivité.", + "kickIdlePlayersWarning1Text": "${NAME} sera éjecté dans ${COUNT} secondes s'il reste inactif.", + "kickIdlePlayersWarning2Text": "(vous pouvez désactiver ceci dans Paramètres -> Avancé)", + "leftGameText": "Vous avez quitté '${NAME}'.", + "leftPartyText": "Vous avez quitté la partie de ${NAME}.", + "noMusicFilesInFolderText": "Ce dossier contient aucun fichier musical.", + "playerJoinedPartyText": "${NAME} a joint la partie!", + "playerLeftPartyText": "${NAME} a quitté la partie.", + "rejectingInviteAlreadyInPartyText": "Rejete l'invitation (déjà dans une partie).", + "serverRestartingText": "Le serveur redémarre. Rejoignez dans un moment", + "serverShuttingDownText": "Arrêt du serveur...", + "signInErrorText": "Erreur pendant la connexion.", + "signInNoConnectionText": "Impossible de se connecter. (pas de connexion internet?)", + "teamNameText": "L'équipe ${NAME} ", + "telnetAccessDeniedText": "ERREUR: l'utilisateur n'a pas autorisé l'accès telnet.", + "timeOutText": "(expire dans ${TIME} secondes)", + "touchScreenJoinWarningText": "Vous avez joint avec l'écran tactile.\nSi c'était une erreur, touchez 'Menu->Quitter le Jeu'.", + "touchScreenText": "Écran Tactile", + "trialText": "test", + "unableToResolveHostText": "Erreur: impossible de résoudre l'hôte.", + "unavailableNoConnectionText": "Pas disponible à l'instant (pas de connexion internet?)", + "vrOrientationResetCardboardText": "Utilisez ceci pour réinitialiser l'orientation RV.\nPour jouer au jeu vous aurez besoin d'un contrôleur externe.", + "vrOrientationResetText": "Réinitialiser l'orientation RV.", + "willTimeOutText": "(expirera si inactive)" + }, + "jumpBoldText": "SAUT", + "jumpText": "Saut", + "keepText": "Garder", + "keepTheseSettingsText": "Garder ces paramètres?", + "keyboardChangeInstructionsText": "Appuyez deux fois sur espace pour changer le clavier.", + "keyboardNoOthersAvailableText": "Aucun autre clavier n'est disponible.", + "keyboardSwitchText": "Changer le clavier vers \"${NAME}\".", + "kickOccurredText": "${NAME} a été renvoyé(e) du serveur", + "kickQuestionText": "Ban ${NAME} ?", + "kickText": "Renvoyer", + "kickVoteCantKickAdminsText": "L'administrateur ne peut pas être expulsée.", + "kickVoteCantKickSelfText": "Tu ne peut pas t'expulsé toi-même.", + "kickVoteFailedNotEnoughVotersText": "Pas assez de joueurs pour un vote.", + "kickVoteFailedText": "Le vote de renvoi est annulé.", + "kickVoteStartedText": "Un vote pour exclure '${NAME}' a commencé.", + "kickVoteText": "Vote pour exclure", + "kickVotingDisabledText": "Le vote d'expulsion est désactivé.", + "kickWithChatText": "Écrit ${YES} dans le chat pour Oui et ${NO} pour Non", + "killsTallyText": "${COUNT} meurtres", + "killsText": "Meurtres", + "kioskWindow": { + "easyText": "Facile", + "epicModeText": "Mode Épique", + "fullMenuText": "Menu Complet", + "hardText": "Difficile", + "mediumText": "Moyen", + "singlePlayerExamplesText": "Exemples Solo / Co-op", + "versusExamplesText": "Exemples de Versus" + }, + "languageSetText": "La langue est maintenant \"${LANGUAGE}\".", + "lapNumberText": "Tour ${CURRENT}/${TOTAL}", + "lastGamesText": "(dernier ${COUNT} jeux)", + "leaderboardsText": "Classements", + "league": { + "allTimeText": "De tous les temps", + "currentSeasonText": "Saison en Cours (${NUMBER})", + "leagueFullText": "Ligue ${NAME}", + "leagueRankText": "Classement de Ligue", + "leagueText": "Ligue", + "rankInLeagueText": "#${RANK}, Ligue ${NAME}${SUFFIX}", + "seasonEndedDaysAgoText": "La saison est terminée depuis ${NUMBER} jours.", + "seasonEndsDaysText": "La saison se termine dans ${NUMBER} jours.", + "seasonEndsHoursText": "La saison termine dans ${NUMBER} heures.", + "seasonEndsMinutesText": "La saison se termine dans ${NUMBER} minutes.", + "seasonText": "Saison ${NUMBER}", + "tournamentLeagueText": "Vous devez atteindre la ligue ${NAME} pour participer à ce tournoi.", + "trophyCountsResetText": "Les trophées seront remis à zéro la saison suivante." + }, + "levelBestScoresText": "Meilleurs scores dans ${LEVEL}", + "levelBestTimesText": "Meilleurs temps dans ${LEVEL}", + "levelFastestTimesText": "Meilleurs temps en ${LEVEL}", + "levelHighestScoresText": "Meilleurs marques en ${LEVEL}", + "levelIsLockedText": "${LEVEL} est verrouillé.", + "levelMustBeCompletedFirstText": "${LEVEL} doit être complété d'abord.", + "levelText": "Niveau ${NUMBER}", + "levelUnlockedText": "Niveau Déverrouillé!", + "livesBonusText": "Bonus à vie", + "loadingText": "chargement", + "loadingTryAgainText": "Chargement; réessayez dans quelques instants...", + "macControllerSubsystemBothText": "Les deux (non recommandé)", + "macControllerSubsystemClassicText": "Manettes classiques", + "macControllerSubsystemDescriptionText": "(changez ceci si vos manettes ne marchent pas)", + "macControllerSubsystemMFiNoteText": "Une manette IOS/Mac détectée;\nVous pouvez les activer dans réglages -> manettes", + "macControllerSubsystemMFiText": "IOS/Mac", + "macControllerSubsystemTitleText": "Aide manette", + "mainMenu": { + "creditsText": "Crédits", + "demoMenuText": "Menu Démo", + "endGameText": "Terminer le jeu", + "exitGameText": "Quitter le jeu", + "exitToMenuText": "Retourner au menu?", + "howToPlayText": "Guide Pratique", + "justPlayerText": "(Seulement ${NAME})", + "leaveGameText": "Quitter le jeu", + "leavePartyConfirmText": "Voulez-vous vraiment quitter la partie?", + "leavePartyText": "Quitter la Partie", + "leaveText": "Retour au menu", + "quitText": "Quitter", + "resumeText": "Reprendre", + "settingsText": "Paramètres" + }, + "makeItSoText": "Appliquer", + "mapSelectGetMoreMapsText": "Obtenir plus de cartes...", + "mapSelectText": "Choisir...", + "mapSelectTitleText": "Cartes pour ${GAME}", + "mapText": "Carte", + "maxConnectionsText": "Limite de Connexions", + "maxPartySizeText": "Maximum Participants", + "maxPlayersText": "Joueurs Max", + "modeArcadeText": "Mode Arcade", + "modeClassicText": "Mode classique", + "modeDemoText": "Mode Demo", + "mostValuablePlayerText": "Meilleur joueur", + "mostViolatedPlayerText": "Joueur le plus violé", + "mostViolentPlayerText": "Joueur le plus violent", + "moveText": "Bouger", + "multiKillText": "${COUNT}-MEURTRES!!!", + "multiPlayerCountText": "${COUNT} joueurs", + "mustInviteFriendsText": "Note: vous devez inviter des amis dans \nle menu \"${GATHER}\" ou brancher des \ncontrôleurs pour jouer en multi-joueurs.", + "nameBetrayedText": "${NAME} a trahi ${VICTIM}", + "nameDiedText": "${NAME} est mort.", + "nameKilledText": "${NAME} a tué ${VICTIM}.", + "nameNotEmptyText": "Le nom ne peut pas être vide!", + "nameScoresText": "${NAME} a marqué!", + "nameSuicideKidFriendlyText": "${NAME} est mort accidentellement.", + "nameSuicideText": "${NAME} s'est suicidé.", + "nameText": "Nom", + "nativeText": "Native", + "newPersonalBestText": "Nouveau record personnel!", + "newTestBuildAvailableText": "Une nouvelle version test est disponible ! (${VERSION} ${BUILD}).\nObtenez à ${ADDRESS}", + "newText": "Nouveau", + "newVersionAvailableText": "Une nouvelle version de ${APP_NAME} est disponible! (${VERSION})", + "nextAchievementsText": "Prochains Succès:", + "nextLevelText": "Niveau Suivant", + "noAchievementsRemainingText": "- aucun", + "noContinuesText": "(sans continus)", + "noExternalStorageErrorText": "Aucun stockage externe a été trouvé pour cet appareil", + "noGameCircleText": "Erreur: vous n'êtes pas connecté au GameCircle", + "noJoinCoopMidwayText": "Vous ne pouvez pas rejoindre une partie co-cop en plein milieu.", + "noProfilesErrorText": "Vous avez aucun profil de joueur, vous êtes donc coincés avec '${NAME}'.\nAllez à Paramètres->Profils des Joueurs pour vous créer un profil.", + "noScoresYetText": "Aucun score pour le moment.", + "noThanksText": "Non Merci", + "noTournamentsInTestBuildText": "AVERTISSEMENT: les scores de tournoi de cette version de test seront ignorés.", + "noValidMapsErrorText": "Aucune carte valide a été trouvée pour ce type de jeu.", + "notEnoughPlayersRemainingText": "Pas assez de joueurs restant; quittez et commencez un nouveau jeu.", + "notEnoughPlayersText": "Vous avez besoin d'au moins ${COUNT} joueurs pour commencer ce jeu!", + "notNowText": "Pas maintenant", + "notSignedInErrorText": "Vous devez vous connecter pour faire ceci.", + "notSignedInGooglePlayErrorText": "Vous devez vous connecter avec Google Play pour faire ceci.", + "notSignedInText": "pas connecté", + "nothingIsSelectedErrorText": "Rien n'est sélectionné!", + "numberText": "#${NUMBER}", + "offText": "Désactivé", + "okText": "Ok", + "onText": "Activé", + "oneMomentText": "Juste un moment...", + "onslaughtRespawnText": "${PLAYER} réapparaîtra à la vague ${WAVE}", + "orText": "${A} ou ${B}", + "otherText": "Autre...", + "outOfText": "(#${RANK} sur ${ALL})", + "ownFlagAtYourBaseWarning": "Votre drapeau doit être\nà votre base pour marquer!", + "packageModsEnabledErrorText": "Le jeu en réseau n'est pas autorisé tant que les package mods locaux sont activés (référez à Paramètres->Avancé)", + "partyWindow": { + "chatMessageText": "Message", + "emptyText": "Votre partie est vide", + "hostText": "(hôte)", + "sendText": "Envoyer", + "titleText": "Votre Partie" + }, + "pausedByHostText": "(pausé par l'hôte)", + "perfectWaveText": "Vague parfaite!", + "pickUpBoldText": "PRIS", + "pickUpText": "Ramasser", + "playModes": { + "coopText": "Co-op", + "freeForAllText": "Mêlée Générale", + "multiTeamText": "Multi-Équipes", + "singlePlayerCoopText": "Mode Solo / Co-op", + "teamsText": "Équipes" + }, + "playText": "Jouer", + "playWindow": { + "coopText": "Co-op", + "freeForAllText": "Mêlée générale", + "oneToFourPlayersText": "1-4 joueurs", + "teamsText": "Équipes", + "titleText": "Jouer", + "twoToEightPlayersText": "2-8 joueurs" + }, + "playerCountAbbreviatedText": "${COUNT}j", + "playerDelayedJoinText": "${PLAYER} rejoindra au debut de la prochaine partie.", + "playerInfoText": "Info du Joueur", + "playerLeftText": "${PLAYER} a quitté le jeu.", + "playerLimitReachedText": "La limite de ${COUNT} joueurs est atteinte; aucun autre joueur ne pourra rejoindre.", + "playerLimitReachedUnlockProText": "Évoluez en \"${PRO}\" dans le magasin pour jouer avec plus de ${COUNT} joueurs. ", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Vous ne pouvez pas effacer le profil de votre compte.", + "deleteButtonText": "Effacer \nCe Profil", + "deleteConfirmText": "Effacer '${PROFILE}'?", + "editButtonText": "Éditer\nCe Profil", + "explanationText": "(noms et apparences personalisés pour ce compte)", + "newButtonText": "Nouveau\nProfil", + "titleText": "Profils Des Joueurs" + }, + "playerText": "Joueur", + "playlistNoValidGamesErrorText": "Cette playlist ne contient pas des jeux valides débloqués.", + "playlistNotFoundText": "playlist introuvable", + "playlistText": "Playlist", + "playlistsText": "Playlists", + "pleaseRateText": "Si vous aimez ${APP_NAME}, SVP, prenez un moment pour \névaluez ou écrire un commentaire. Ceci nous donnera un \nretour d'info utile pour le développement futur du jeu.\n\nmerci!\n-eric", + "pleaseWaitText": "Veuillez patienter...", + "pluginsDetectedText": "Nouveaux plugins détectés. Activez / configurez-les dans les paramètres.", + "pluginsText": "Plugins", + "practiceText": "Entraînement", + "pressAnyButtonPlayAgainText": "Appuyez n'importe quel bouton pour rejouer...", + "pressAnyButtonText": "Appuyez n'importe quel bouton pour continuer...", + "pressAnyButtonToJoinText": "Appuyez n'importe quel bouton pour joindre...", + "pressAnyKeyButtonPlayAgainText": "Appuyez sur n'importe quel touche/bouton pour rejouer...", + "pressAnyKeyButtonText": "Appuyez n'importe quel touche/bouton pour continuer...", + "pressAnyKeyText": "Appuyez sur n'importe quelle touche...", + "pressJumpToFlyText": "** Appuyez SAUTE à plusieurs reprises pour voler **", + "pressPunchToJoinText": "appuyez sur FRAPPER pour joindre...", + "pressToOverrideCharacterText": "appuyez sur ${BUTTONS} pour remplacer votre personnage", + "pressToSelectProfileText": "appuyez sur ${BUTTONS} pour sélectionner un joueur", + "pressToSelectTeamText": "appuyez ${BUTTONS} pour sélectionner une équipe", + "profileInfoText": "Créez des profils pour vous et vos amis\npour customizer vos noms, personnages, et couleurs.", + "promoCodeWindow": { + "codeText": "Code", + "codeTextDescription": "Code Promo", + "enterText": "Vérifier" + }, + "promoSubmitErrorText": "Erreur lors de la soumission du code; vérifiez votre connexion internet", + "ps3ControllersWindow": { + "macInstructionsText": "Coupez l'alimentation de votre PS3, soyez certain que le\nBluetooth est activé sur votre Mac, puis connectez votre manette\nà votre Mac par USB pour lier les deux. À partir de ce moment, vous\npouvez utiliser le bouton d'accueil de la manette pour la connectée au \nMac par USB (filaire) ou Bluetooth (sans-fil).\n\nQuelques Macs demandent un code d'accès quand vous liez la manette.\nDans ce cas, référez vous à Google ou le tutoriel qui suit.\n\n\n\n\nLes manettes de PS3 connectées sans-fil devraient être présentes dans la liste\ndes Préférences du Système->Bluetooth. Vous devrez peut-être l'enlever de cette\nliste pour l'utiliser de nouveau avec votre PS3.\n\nSoyez certain de déconnecter vos manettes lorsqu'elles sont inactives, sinon, \nles batteries continueront à s'épuiser. \n\nLe bluetooth peut supporter jusqu'à 7 manettes,\nmais ce n'est pas garanti.", + "ouyaInstructionsText": "Pour utiliser un contrôleur de PS3 avec votre OUYA, connectez le avec un \ncâble USB. Faites ceci pouvoir déconnecter vos autres contrôleurs, vous \ndevez donc redémarrer votre OUYA et débrancher le câble USB.\n\nÀ partir de maintenant, vous devriez être capable d'utiliser le bouton \nd'accueil pour connecter sans fil. Quand vous avez finir de jouer, \nappuyez sur le bouton d'accueil pour 10 seconds pour éteindre le \ncontrôleur; si non les batteries continueraient à épuiser.", + "pairingTutorialText": "Vidéo Tutoriel - Liaisons des manettes PS3", + "titleText": "Utiliser les manettes PS3 avec ${APP_NAME}:" + }, + "publicBetaText": "BETA PUBLIQUE", + "punchBoldText": "FRAPPER", + "punchText": "Frapper", + "purchaseForText": "Achetez pour ${PRICE}", + "purchaseGameText": "Acheter le Jeu", + "purchasingText": "Achat en cours...", + "quitGameText": "Quitter ${APP_NAME}?", + "quittingIn5SecondsText": "Le jeu fermera dans 5 secondes...", + "randomPlayerNamesText": "DEFAULT_NAMES, Guy, Jean, Jacques, Jérémy, Maurice, Henri, Patrick, François, Frédéric, Éric, Gabrielle, Érica, Marie, Hélène, Isabelle, Jacqueline, Jasmine, Zoé, Juliette, Catherine, Natalie, Alexandra, Alexandre, Claire, Christophe, Pierre, Quentin, Philippe, Francis", + "randomText": "Hasard", + "rankText": "Classement", + "ratingText": "Évaluation", + "reachWave2Text": "Atteignez la 2e vague pour être classé.", + "readyText": "prêt", + "recentText": "Récents", + "remainingInTrialText": "restant en test", + "remoteAppInfoShortText": "${APP_NAME} est plus amusant quand vous jouez avec votre famille ou des amis.\nBranchez une ou plusieurs manettes ou installez l'application \n${REMOTE_APP_NAME} sur des téléphones ou des tablettes \npour les utiliser en tant que manettes.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Position du Bouton", + "button_size": "Taille du Bouton", + "cant_resolve_host": "Ne peut pas résoudre l'hôte.", + "capturing": "Capturer...", + "connected": "Connecté.", + "description": "Utilisez votre smartphone ou tablette comme manette avec BombSquad. \nJusqu'à 8 appareils peuvent se connecter en même temps pour des parties locales épiques sur un seul écran.", + "disconnected": "Déconnecté par le serveur", + "dpad_fixed": "fixé", + "dpad_floating": "flotter", + "dpad_position": "Position D-Pad", + "dpad_size": "Taille du D-Pad", + "dpad_type": "Type D-Pad", + "enter_an_address": "Tapez une Adresse", + "game_full": "Le jeu est plein ou n'accepte pas d'autres joueurs.", + "game_shut_down": "Le jeu a fermé.", + "hardware_buttons": "Boutons d'Hardware", + "join_by_address": "Joindre par adresse ip...", + "lag": "Décalage: ${SECONDS} secondes", + "reset": "Réinitialiser", + "run1": "Exécution 1", + "run2": "Exécution 2", + "searching": "Rechercher pour les jeux de BombSquad...", + "searching_caption": "Touchez le nom d'un jeu pour rejoindre.\nVérifiez que vous êtes sur le même réseau wifi que le jeu.", + "start": "Commencer", + "version_mismatch": "Mauvaise version.\nVérifiez que BombSquad et BombSquad Remote \nsont mis à jour et réessayez." + }, + "removeInGameAdsText": "Débloquez \"${PRO}\" dans le magasin pour enlever les annonces.", + "renameText": "Renommer", + "replayEndText": "Terminer la Reprise", + "replayNameDefaultText": "Reprise du Match Précédent", + "replayReadErrorText": "Erreur de lecture de la reprise.", + "replayRenameWarningText": "Renommer \"${REPLAY}\" après un jeu si vous voulez le conserver; autrement il sera écrasé.", + "replayVersionErrorText": "Désolé, cette reprise a été créée avec une version \ndifférente du jeu et ne peut pas être utilisé.", + "replayWatchText": "Regarder La Reprise", + "replayWriteErrorText": "Erreur d'écriture du fichier de la reprise.", + "replaysText": "Reprises", + "reportPlayerExplanationText": "Utilisez cet email pour rapporter tricherie, langage inapproprié, ou d'autres comportements mauvais.\nDécrivez en-dessous:", + "reportThisPlayerCheatingText": "Tricherie", + "reportThisPlayerLanguageText": "Langage Inapproprié", + "reportThisPlayerReasonText": "Que voulez-vous rapporter?", + "reportThisPlayerText": "Envoyez Un Rapport", + "requestingText": "En cours de demande...", + "restartText": "Redémarrer", + "retryText": "Réessayer", + "revertText": "Retour", + "runBoldText": "COURIR", + "runText": "Courir", + "saveText": "Sauvegarder", + "scanScriptsErrorText": "Erreur(s) dans les scripts; voir journal pour détails.", + "scoreChallengesText": "Défis de Score", + "scoreListUnavailableText": "Liste des scores indisponible.", + "scoreText": "Score", + "scoreUnits": { + "millisecondsText": "Millisecondes", + "pointsText": "Points", + "secondsText": "Secondes" + }, + "scoreWasText": "(A été ${COUNT})", + "selectText": "Sélectionner", + "seriesWinLine1PlayerText": "A GAGNÉ LA", + "seriesWinLine1TeamText": "A GAGNÉ LA", + "seriesWinLine1Text": "A GAGNÉ", + "seriesWinLine2Text": "SÉRIE!", + "settingsWindow": { + "accountText": "Compte", + "advancedText": "Avancé", + "audioText": "Son", + "controllersText": "Contrôleurs", + "enterPromoCodeText": "Entrer un code promotionnel", + "graphicsText": "Graphiques", + "playerProfilesMovedText": "Note: les Profiles de Joueurs ont été déplacés dans la page de Compte, dans le menu principal.", + "playerProfilesText": "Profils des Joueurs", + "titleText": "Paramètres" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(un simple clavier sur l'écran pour taper du texte)", + "alwaysUseInternalKeyboardText": "Toujours utiliser le clavier interne", + "benchmarksText": "Benchmarks et Tests de Stress", + "disableCameraGyroscopeMotionText": "Désactiver le mouvement de gyroscope de la caméra", + "disableCameraShakeText": "Désactiver le tremblement de la camera", + "disableThisNotice": "(vous pouvez désactiver cette notification dans les paramètres avancés)", + "enablePackageModsDescriptionText": "(activer plusieurs capabilités des mods mais désactiver le jeu en réseau)", + "enablePackageModsText": "Activer les Packages Mods Locaux", + "enterPromoCodeText": "Entrez code", + "forTestingText": "Note: ces valeurs sont exclusivement pour les tests et seront perdus à la fermeture de l'application.", + "helpTranslateText": "Les traductions de ${APP_NAME} proviennent des efforts de \nla communauté. Si vous voulez contribuer ou corriger une \ntraduction, suivez le lien ci-dessous. Merci d'avance!", + "kickIdlePlayersText": "Déconnecter les joueurs inactifs", + "kidFriendlyModeText": "Mode Enfant-Gentil (moins de violence, etc)", + "languageText": "Langage", + "moddingGuideText": "Guide pour Modder", + "mustRestartText": "Vous devez redémarrer le jeu pour que les changements prennent effet.", + "netTestingText": "Tester Votre Réseau", + "resetText": "Réinitialiser", + "showBombTrajectoriesText": "Montrer les trajectoires de bombe", + "showPlayerNamesText": "Montrer les Noms des Joueurs", + "showUserModsText": "Montrer le Dossier des Mods", + "titleText": "Avancé", + "translationEditorButtonText": "Éditeur des Traductions de ${APP_NAME}", + "translationFetchErrorText": "statut de la traduction indisponible", + "translationFetchingStatusText": "vérification du statut de la traduction...", + "translationInformMe": "Informez-moi quand ma langue a besoin de mises à jour", + "translationNoUpdateNeededText": "ce langage est à jour; woohoo!", + "translationUpdateNeededText": "** ce langage à besoin des modifications!! **", + "vrTestingText": "Test de la RV" + }, + "shareText": "Partager", + "sharingText": "Partage...", + "showText": "Montre", + "signInForPromoCodeText": "Vous devez vous connecter à un compte pour que les codes prennent effet.", + "signInWithGameCenterText": "Pour l'utilisation d'un compte Game \nCenter, connectez-vous avec l'application Game Center.", + "singleGamePlaylistNameText": "Seulement ${GAME}", + "singlePlayerCountText": "1 joueur", + "soloNameFilterText": "${NAME} Solo", + "soundtrackTypeNames": { + "CharSelect": "Sélection du Personnage", + "Chosen One": "Élu", + "Epic": "Jeux Épiques", + "Epic Race": "Course Épique", + "FlagCatcher": "Capturer le Drapeau", + "Flying": "Pensées Heureuses", + "Football": "Football", + "ForwardMarch": "Assaut", + "GrandRomp": "Conquête", + "Hockey": "Hockey", + "Keep Away": "Tenir à l'Écart", + "Marching": "Défense du portail", + "Menu": "Menu Principal", + "Onslaught": "Bousculade", + "Race": "Course", + "Scary": "Roi de la Colline", + "Scores": "Écran des scores", + "Survival": "Élimination", + "ToTheDeath": "Tuerie", + "Victory": "Écran des scores finaux" + }, + "spaceKeyText": "espace", + "statsText": "Stats", + "storagePermissionAccessText": "Cette action a besoin de l'accès au stockage", + "store": { + "alreadyOwnText": "Vous avez déja acheté ${NAME}!", + "bombSquadProDescriptionText": "• Double le nombre des billets gagnés grâce aux succès\n• Enlève les annonces\n• Inclus ${COUNT} billets bonus\n• +${PERCENT}% de score dans la ligue\n• Débloque les niveaux co-op\n '${INF_ONSLAUGHT}' et '${INF_RUNAROUND}'", + "bombSquadProFeaturesText": "Fonctionnalités:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "* Supprime les annonces et publicités dans le jeu\n* Déverrouille des paramètres supplémentaires\n* Comprend également:", + "buyText": "Acheter", + "charactersText": "Personnages", + "comingSoonText": "À Venir...", + "extrasText": "Extras", + "freeBombSquadProText": "BombSquad est désormais gratuit, mais comme vous l'avez acheté, vous\nrecevez la mise à jour BombSquad Pro et ${COUNT} tickets en remerciement.\nAmusez-vous bien avec les nouveautés, et merci pour votre soutien!\n-Eric", + "gameUpgradesText": "Les Mises À Niveau", + "getCoinsText": "Avoir des pièces", + "holidaySpecialText": "Spécial Vacances", + "howToSwitchCharactersText": "(allez à \"${SETTINGS} -> ${PLAYER_PROFILES}\" pour assigner et customiser des personnages)", + "howToUseIconsText": "(créez des profils de joueur global (dans la page du compte) pour les utiliser)", + "howToUseMapsText": "(utilisez ces cartes dans vos propres playlists équipes/mêlée générale)", + "iconsText": "Icônes", + "loadErrorText": "Impossible de charger la page.\nVérifiez votre connexion internet.", + "loadingText": "chargement en cours", + "mapsText": "Cartes", + "miniGamesText": "Mini Jeux", + "oneTimeOnlyText": "(une fois seulement)", + "purchaseAlreadyInProgressText": "Un achat de ceci est déjà en cours.", + "purchaseConfirmText": "Acheter ${ITEM}?", + "purchaseNotValidError": "L'Achat n'est pas valide.\nContactez ${EMAIL} (En anglais) si c'est une erreur.", + "purchaseText": "Acheter", + "saleBundleText": "Vente Groupée!", + "saleExclaimText": "Promo!", + "salePercentText": "(${PERCENT}% de rabais)", + "saleText": "PROMO", + "searchText": "Rechercher", + "teamsFreeForAllGamesText": "Jeux d'Équipes / Mêlée Générale", + "totalWorthText": "*** une valeur de ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Une amélioration ?", + "winterSpecialText": "Spéciale Hivernale", + "youOwnThisText": "- vous possédez ceci -" + }, + "storeDescriptionText": "Un Jeu Fou à 8 Joueurs!\n\nExplosez vos amis (ou l'ordinateur) dans un tournoi de mini-jeux explosifs comme la capture du drapeau, le hockey à la bombe, et les Match-à-Mort dans un slow-motion épique!\n\nLes contrôles simples et l'ajout facile de contrôleur permet de regrouper jusqu'à 8 personnes dans l'action; vous pouvez même utiliser vos appareils mobiles comme contrôleurs via l'application 'BombSquad Remote' gratuite!\n\nLarguez Les Bombes!\n\nVisitez www.froemling.net/bombsquad pour plus d'information.", + "storeDescriptions": { + "blowUpYourFriendsText": "Explosez vos amis.", + "competeInMiniGamesText": "Participez aux mini-jeux délirants.", + "customize2Text": "Customisez les personnages, les mini-jeux, et même la bande-sonore.", + "customizeText": "Customisez les personnages et créez vos propres playlists de mini-jeux.", + "sportsMoreFunText": "Les sports sont plus amusants avec des explosifs.", + "teamUpAgainstComputerText": "Faites equipe contre l'ordinateur." + }, + "storeText": "Magasin", + "submitText": "Soumettre", + "submittingPromoCodeText": "Envoi du code...", + "teamNamesColorText": "Noms d'équipe/Couleurs...", + "teamsText": "Équipes", + "telnetAccessGrantedText": "L'accès Telnet est activé.", + "telnetAccessText": "Accès Telnet détecté; autoriser?", + "testBuildErrorText": "Cette version test n'est plus active; cherchez une version nouvelle.", + "testBuildText": "Version Test", + "testBuildValidateErrorText": "Impossible de valider cette version de test. (pas de connexion internet?)", + "testBuildValidatedText": "Version Test Validée; Amusez-Vous!", + "thankYouText": "Merci pour votre soutien! Amusez-vous!!", + "threeKillText": "TRIPLE MEURTRE!!!", + "timeBonusText": "Bonus de Temps", + "timeElapsedText": "Temps Passé", + "timeExpiredText": "Temps Expiré", + "timeSuffixDaysText": "${COUNT}j", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Conseil", + "titleText": "BombSquad", + "titleVRText": "RV BombSquad", + "topFriendsText": "Les amis les mieux classés", + "tournamentCheckingStateText": "Vérification de l'état du tournoi; attendez SVP...", + "tournamentEndedText": "Ce tournoi est terminé. Un nouveau commencera bientôt.", + "tournamentEntryText": "Inscription au Tournoi", + "tournamentResultsRecentText": "Résultats des Tournois Récents", + "tournamentStandingsText": "Classements du Tournoi", + "tournamentText": "Tournoi", + "tournamentTimeExpiredText": "Le temps de ce tournoi a expiré", + "tournamentsText": "Tournois", + "translations": { + "characterNames": { + "Agent Johnson": "Agent Johnson", + "B-9000": "B-9000", + "Bernard": "Bernard", + "Bones": "Osseux", + "Butch": "Butch", + "Easter Bunny": "Lapin de Pâques", + "Flopsy": "Flopsy", + "Frosty": "Gel", + "Gretel": "Gretel", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Jack Sp'Arr", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Lucky", + "Mel": "Mel", + "Middle-Man": "Middle-Man", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Chelem", + "Santa Claus": "Père Noël", + "Snake Shadow": "Ombre du Serpent", + "Spaz": "Spaz", + "Taobao Mascot": "Taobao Mascot", + "Todd": "Todd", + "Todd McBurton": "Todd McBurton", + "Xara": "Xara", + "Zoe": "Zoé", + "Zola": "Zola" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Boucherie\nInfinie", + "Infinite\nRunaround": "Défense du portail\nInfinie", + "Onslaught\nTraining": "Boucherie\nEntrainement", + "Pro\nFootball": "Football\nPro", + "Pro\nOnslaught": "Boucherie\nPro", + "Pro\nRunaround": "Pro\nDéfense du portail", + "Rookie\nFootball": "Football\nDébutant", + "Rookie\nOnslaught": "Boucherie\nDébutant", + "The\nLast Stand": "Le\nDernier Rempart", + "Uber\nFootball": "Über\nFootball", + "Uber\nOnslaught": "Über\nBoucherie", + "Uber\nRunaround": "Über\nDéfense du portail" + }, + "coopLevelNames": { + "${GAME} Training": "Entraînement pour ${GAME}", + "Infinite ${GAME}": "${GAME} Infini", + "Infinite Onslaught": "Boucherie Infinie", + "Infinite Runaround": "Défense du portail infinie", + "Onslaught": "Boucherie Infinie", + "Onslaught Training": "Entraînement pour Bousculade", + "Pro ${GAME}": "${GAME} Pro", + "Pro Football": "Football Pro", + "Pro Onslaught": "Bousculade Pro", + "Pro Runaround": "Défense du portail Pro", + "Rookie ${GAME}": "${GAME} Novice", + "Rookie Football": "Football Novice", + "Rookie Onslaught": "Bousculade Novice", + "Runaround": "Défense du portail infinie", + "The Last Stand": "Le Dernier Rempart", + "Uber ${GAME}": "${GAME} Uber", + "Uber Football": "Football Uber", + "Uber Onslaught": "Bousculade Uber", + "Uber Runaround": "Défense du portail Uber" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Restez l'élu pendant un certain temps pour gagner.\nTuez l'élu pour devenir l'élu.", + "Bomb as many targets as you can.": "Bombardez autant de cibles que vous pouvez.", + "Carry the flag for ${ARG1} seconds.": "Tenez le drapeau pendant ${ARG1} secondes.", + "Carry the flag for a set length of time.": "Tenez le drapeau pendant une durée définie.", + "Crush ${ARG1} of your enemies.": "Terrassez ${ARG1} de vos ennemis.", + "Defeat all enemies.": "Tuez tous les ennemis.", + "Dodge the falling bombs.": "Esquivez les bombes chutantes.", + "Final glorious epic slow motion battle to the death.": "Bataille finale jusqu'à la mort dans un glorieux slow-motion épique.", + "Gather eggs!": "Ramassez les œufs !", + "Get the flag to the enemy end zone.": "Amenez le drapeau à la zone de but opposée.", + "How fast can you defeat the ninjas?": "À quelle vitesse pouvez-vous vaincre les ninjas?", + "Kill a set number of enemies to win.": "Tuez plusieurs ennemis pour gagner.", + "Last one standing wins.": "Le dernier joueur debout gagne.", + "Last remaining alive wins.": "Le dernier joueur debout gagne.", + "Last team standing wins.": "La dernière équipe debout gagne.", + "Prevent enemies from reaching the exit.": "Empêchez les ennemis d'arriver au portail.", + "Reach the enemy flag to score.": "Touchez le drapeau ennemi pour marquer.", + "Return the enemy flag to score.": "Ramener le drapeau ennemi pour marquer.", + "Run ${ARG1} laps.": "Faites ${ARG1} tours.", + "Run ${ARG1} laps. Your entire team has to finish.": "Faites ${ARG1} tours. Toute votre équipe doit finir.", + "Run 1 lap.": "Faites 1 tour.", + "Run 1 lap. Your entire team has to finish.": "Faites 1 tour. Toute votre équipe doit finir.", + "Run real fast!": "Courez très rapidement!", + "Score ${ARG1} goals.": "Marquez ${ARG1} buts.", + "Score ${ARG1} touchdowns.": "Marquez ${ARG1} touchés.", + "Score a goal": "Marquez un but.", + "Score a goal.": "Marquez un but.", + "Score a touchdown.": "Marquez un touché.", + "Score some goals.": "Marquez quelques buts.", + "Secure all ${ARG1} flags.": "Sécurisez tous les ${ARG1} drapeaux.", + "Secure all flags on the map to win.": "Sécurisez tous les drapeaux sur la carte pour gagner.", + "Secure the flag for ${ARG1} seconds.": "Sécurisez le drapeau pendant ${ARG1} secondes.", + "Secure the flag for a set length of time.": "Sécurisez le drapeau pendant une durée définie.", + "Steal the enemy flag ${ARG1} times.": "Volez le drapeau ennemi ${ARG1} fois.", + "Steal the enemy flag.": "Volez le drapeau ennemi.", + "There can be only one.": "Il ne peut y'en avoir qu'un seul.", + "Touch the enemy flag ${ARG1} times.": "Touchez le drapeau ennemi ${ARG1} fois.", + "Touch the enemy flag.": "Touchez le drapeau ennemi.", + "carry the flag for ${ARG1} seconds": "Tenez le drapeau pendant ${ARG1} secondes", + "kill ${ARG1} enemies": "Tuez ${ARG1} ennemis", + "last one standing wins": "Le dernier joueur debout gagne", + "last team standing wins": "La dernière équipe debout gagne", + "return ${ARG1} flags": "Ramener ${ARG1} drapeaux", + "return 1 flag": "Ramener 1 drapeau", + "run ${ARG1} laps": "Faites ${ARG1} tours", + "run 1 lap": "Faites 1 tour", + "score ${ARG1} goals": "Marquez ${ARG1} buts", + "score ${ARG1} touchdowns": "Marquez ${ARG1} touchés", + "score a goal": "Marquez un but", + "score a touchdown": "Marquez un touché", + "secure all ${ARG1} flags": "Sécurisez tous les ${ARG1} drapeaux", + "secure the flag for ${ARG1} seconds": "Sécurisez le drapeau pendant ${ARG1} secondes", + "touch ${ARG1} flags": "Touchez ${ARG1} drapeaux", + "touch 1 flag": "Touchez 1 drapeau" + }, + "gameNames": { + "Assault": "Assaut", + "Capture the Flag": "Capturer le Drapeau", + "Chosen One": "L'Élu", + "Conquest": "Conquête", + "Death Match": "Tuerie", + "Easter Egg Hunt": "Chasse aux œufs", + "Elimination": "Élimination", + "Football": "Football", + "Hockey": "Hockey", + "Keep Away": "Tenir à l'Écart", + "King of the Hill": "Roi de La colline", + "Meteor Shower": "Pluie de météorites", + "Ninja Fight": "Lutte des Ninjas", + "Onslaught": "Bousculade", + "Race": "Course", + "Runaround": "Défense du portail", + "Target Practice": "Tir sur Cible", + "The Last Stand": "Le dernier rempart" + }, + "inputDeviceNames": { + "Keyboard": "Clavier", + "Keyboard P2": "Clavier J2" + }, + "languages": { + "Arabic": "Arabe", + "Belarussian": "Biélorusse", + "Chinese": "Chinois simplifié", + "ChineseTraditional": "Chinois Traditionnel", + "Croatian": "Croate", + "Czech": "Tchèque", + "Danish": "Danois", + "Dutch": "Néerlandais", + "English": "Anglais", + "Esperanto": "Espéranto", + "Finnish": "Finnois", + "French": "Français", + "German": "Allemand", + "Gibberish": "Charabia", + "Greek": "Grec", + "Hindi": "Hindi", + "Hungarian": "Hongrois", + "Indonesian": "Indonésien", + "Italian": "Italien", + "Japanese": "Japonais", + "Korean": "Coréen", + "Persian": "Persan", + "Polish": "Polonais", + "Portuguese": "Portugais", + "Romanian": "Roumain", + "Russian": "Russe", + "Serbian": "Serbe", + "Slovak": "Slovaque", + "Spanish": "Espagnol", + "Swedish": "Suédois", + "Turkish": "Turc", + "Ukrainian": "Ukrainien", + "Venetian": "Vénitien", + "Vietnamese": "Vietnamien" + }, + "leagueNames": { + "Bronze": "Bronze", + "Diamond": "Diamant", + "Gold": "Or", + "Silver": "Argent" + }, + "mapsNames": { + "Big G": "Grand G", + "Bridgit": "Bridgit", + "Courtyard": "Cour", + "Crag Castle": "Château Escarpé", + "Doom Shroom": "Champignon de la Tragédie", + "Football Stadium": "Stade de Football", + "Happy Thoughts": "Pensées Heureuses", + "Hockey Stadium": "Stade de Hockey", + "Lake Frigid": "Lac Frigo", + "Monkey Face": "Visage d'un Singe", + "Rampage": "Carnage", + "Roundabout": "Rond-Point", + "Step Right Up": "Prendre un Pas", + "The Pad": "La Plate-Forme", + "Tip Top": "Le Sommet", + "Tower D": "Tour D", + "Zigzag": "Zigzag" + }, + "playlistNames": { + "Just Epic": "Seulement Épique", + "Just Sports": "Seulement les Sports" + }, + "promoCodeResponses": { + "invalid promo code": "Code promo invalide" + }, + "scoreNames": { + "Flags": "Drapeaux", + "Goals": "Buts", + "Score": "Points", + "Survived": "A Survécu", + "Time": "Temps", + "Time Held": "Temps Tenu" + }, + "serverResponses": { + "A code has already been used on this account.": "Un code a déjà été utilisé sur ce compte.", + "A reward has already been given for that address.": "Une récompense a déjà été attribuée pour cette adresse.", + "Account linking successful!": "Liaison des comptes réussie!", + "Account unlinking successful!": "Compte dissocié avec succès!", + "Accounts are already linked.": "Ces comptes sont déjà liés.", + "An error has occurred; (${ERROR})": "Une erreur est survenue; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Une erreur est survenue; veuillez contacter le support.(${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Une erreur est survenue; SVP contacter support@froemling.net.", + "An error has occurred; please try again later.": "Une erreur s'est produite. Réessayez plus tard.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Voulez-vous vraiment lier ces comptes?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nAction irréversible!", + "BombSquad Pro unlocked!": "Bombsquad Pro débloqué!", + "Can't link 2 accounts of this type.": "Impossible de lier 2 comptes de ce genre.", + "Can't link 2 diamond league accounts.": "Impossible de lier 2 comptes dans la ligue diamant.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Impossible de lier les comptes, le maximum de ${COUNT} comptes liés est déjà atteint.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Tricherie détectée, scores et prix suspendus pour ${COUNT} jours.", + "Could not establish a secure connection.": "Impossible d'établir une connexion sécurisé.", + "Daily maximum reached.": "Limite quotidienne atteinte.", + "Entering tournament...": "Accès au tournoi...", + "Invalid code.": "Code invalide.", + "Invalid payment; purchase canceled.": "Payement invalide. Achat annulé.", + "Invalid promo code.": "Code promo invalide.", + "Invalid purchase.": "Achat invalide.", + "Invalid tournament entry; score will be ignored.": "Entrée invalide; votre score sera ignoré.", + "Item unlocked!": "Article débloqué!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "LIEN REFUSÉ. ${ACCOUNT} contient des\ndonnées importantes qui seraient perdus.\nVous pouvez lier dans l'ordre inverse si vous le souhaitez\n(et perdre les données de CE compte à la place)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Lier le compte ${ACCOUNT} à ce compte?\nToutes les données existantes sur ${ACCOUNT} seront perdues.\nCela ne peut pas être annulé. Vous en êtes sûr", + "Max number of playlists reached.": "Nombre maximum de playlists atteint.", + "Max number of profiles reached.": "Nombre maximum de profils atteint.", + "Maximum friend code rewards reached.": "Maximum récompenses de code ami atteint.", + "Message is too long.": "Message trop long.", + "Profile \"${NAME}\" upgraded successfully.": "Le profil \"${NAME}\" à été mis à jour.", + "Profile could not be upgraded.": "Le profil ne peut pas être mis à jour.", + "Purchase successful!": "Achat réussi!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Vous avez reçu ${COUNT} tickets pour votre connexion.\nRevenez demain pour en recevoir ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "La fonctionnalité serveur n'est plus prise en charge dans cette version du jeu;\nVeuillez mettre à jour vers une version plus récente", + "Sorry, there are no uses remaining on this code.": "Désolé, ce code ne peut plus être utilisé.", + "Sorry, this code has already been used.": "Désolé, ce code a déjà été utilisé.", + "Sorry, this code has expired.": "Désolé, ce code a expiré.", + "Sorry, this code only works for new accounts.": "Désolé, ce code fonctionne seulement pour les nouveaux comptes.", + "Temporarily unavailable; please try again later.": "Temporairement indisponible; veuillez réessayer plus tard.", + "The tournament ended before you finished.": "Le tournoi s'est terminé avant que vous finissiez.", + "This account cannot be unlinked for ${NUM} days.": "Ce compte ne peux pas être dissocié pendant ${NUM} jours.", + "This code cannot be used on the account that created it.": "Ce code ne peut pas être utilisé par le compte qui l'a créé.", + "This is currently unavailable; please try again later.": "Ceci est actuellement indisponible ; s'il vous plaît essayez plus tard.", + "This requires version ${VERSION} or newer.": "Ceci requiert la version ${VERSION} ou plus récente.", + "Tournaments disabled due to rooted device.": "Tournois désactivés car appareil rooté.", + "Tournaments require ${VERSION} or newer": "Les tournois nécessitent ${VERSION} ou plus récent", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Dissocier ${ACCOUNT} de ce compte?\nToutes les données sur ${ACCOUNT} seront réinitialisées.\n(à l'exception des achèvements dans certains cas)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ATTENTION: des plaintes de piratage/tricherie ont été émises contre votre compte.\nLes comptes piratés sont interdits et bannis. S'il vous plaît, jouez fair play", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Voulez-vous lier votre compte d'appareil à celui-ci?\n\nVotre compte d'appareil est ${ACCOUNT1}\nCe compte est ${ACCOUNT2}\n\nCeci vous permet de garder votre progression.\nAttention: ceci est permanent!", + "You already own this!": "Vous possédez déjà ceci!", + "You can join in ${COUNT} seconds.": "Vous pouvez rejoindre dans ${COUNT} secondes.", + "You don't have enough tickets for this!": "Vous n'avez pas assez de tickets pour ça!", + "You don't own that.": "Cela ne vous appartient pas!", + "You got ${COUNT} tickets!": "Vous avez reçu ${COUNT} tickets!", + "You got a ${ITEM}!": "Vous avez reçu un ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Vous avez été promu à une ligue supérieure; félicitations!", + "You must update to a newer version of the app to do this.": "L'app doit être mise à jour pour faire ceci.", + "You must update to the newest version of the game to do this.": "Vous devez mettre à jour vers une version plus récente pour faire ça.", + "You must wait a few seconds before entering a new code.": "Vous devez attendre quelques secondes avant d'entrer un nouveau code.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Vous avez été classé #${RANK} au dernier tournoi. Merci d'avoir participé!", + "Your account was rejected. Are you signed in?": "Votre compte a été rejeté. Êtes-vous connecté?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Votre copie du jeu a été modifié.\nRéinitialisez tous changements et réessayez.", + "Your friend code was used by ${ACCOUNT}": "Votre code d'ami a été utilisé par ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Minute", + "1 Second": "1 Seconde", + "10 Minutes": "10 Minutes", + "2 Minutes": "2 Minutes", + "2 Seconds": "2 Secondes", + "20 Minutes": "20 Minutes", + "4 Seconds": "4 Secondes", + "5 Minutes": "5 Minutes", + "8 Seconds": "8 Secondes", + "Allow Negative Scores": "Permettre des scores négatifs", + "Balance Total Lives": "Équilibrer total des vies", + "Bomb Spawning": "Apparitions de bombes", + "Chosen One Gets Gloves": "L'élu reçoit des gants de boxe", + "Chosen One Gets Shield": "L'élu reçoit un bouclier", + "Chosen One Time": "Temps pour l'élu", + "Enable Impact Bombs": "Activer les bombes d'impact", + "Enable Triple Bombs": "Activer les triples bombes", + "Entire Team Must Finish": "L'équipe entière doit finir", + "Epic Mode": "Mode épique", + "Flag Idle Return Time": "délai de retour drapeau perdu", + "Flag Touch Return Time": "Temps à toucher drapeau pour retour", + "Hold Time": "Temps de possession", + "Kills to Win Per Player": "Meurtres par joueur pour gagner", + "Laps": "Tours", + "Lives Per Player": "Vies par joueur", + "Long": "Long", + "Longer": "Plus long", + "Mine Spawning": "Apparition des mines", + "No Mines": "Aucune mines", + "None": "Aucun", + "Normal": "Normal", + "Pro Mode": "Mode Pro", + "Respawn Times": "Temps de réapparition", + "Score to Win": "Score pour gagner", + "Short": "Court", + "Shorter": "Très court", + "Solo Mode": "Mode solo", + "Target Count": "Nombre de cibles", + "Time Limit": "Limite de temps" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "L'équipe ${TEAM} est disqualifiée parce que ${PLAYER} a quitté la partie", + "Killing ${NAME} for skipping part of the track!": "${NAME} a été tué pour avoir pris un raccourci!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Attention à ${NAME}: turbo / bouton-spamming vous assomme." + }, + "teamNames": { + "Bad Guys": "Méchants", + "Blue": "Bleu", + "Good Guys": "Gentils", + "Red": "Rouge" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Un coup de poing avec le saut, la vitesse et la bonne rotation fait au \nmoment opportun, peut tuer un ennemi en un coup.", + "Always remember to floss.": "Rappelez-vous de la patate suprême.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Créez un profil de joueur pour vous même et vos amis avec vos noms\net apparences préférés au lieu d'utiliser ceux au hasard.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Les boites de malédiction vous transforment en bombe à retardement. \nLe seul remède est un paquet de santé.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Malgré leurs apparences, toutes les personnages ont les mêmes \nabilités, donc choisissez celui qui vous ressemble le plus.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Ne soyez pas trop sûr de vous avec le bouclier d'énergie; vous pouvez toujours être jeté hors de la falaise.", + "Don't run all the time. Really. You will fall off cliffs.": "Ne courez pas tout le temps. Vraiment. Vous chuterez hors des falaises.", + "Don't spin for too long; you'll become dizzy and fall.": "Ne tournez pas trop longtemps; vous allez avoir le tournis et tomber.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Gardez n'importe quel bouton appuyé pour courir. (Les gâchettes marchent aussi si vous avez)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Gardez n'importe quel bouton appuyé pour courir. Vous arriverez aux endroits \nplus rapidement mais tourner c'est plus difficile, donc faites attention aux falaises.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Les bombes glacées ne sont pas très puissantes, mais elles\ngèlent tous ce qu'elles touchent, ce qui facilite le fracassement.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Si quelqu'un vous ramasse, frappez-le et il vous lâchera. \nCeci fonctionne en réalité aussi.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Si vous manquez de manettes, installez l'app '${REMOTE_APP_NAME}'\nsur vos appareils mobiles pour les utiliser comme manettes.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Si vous manquez les contrôleurs, installez l'application 'BombSquad Remote' \nsur votre système iOS/Android pour les utiliseront comme contrôleurs.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Si une bombe gluante est collée sur vous, sautez et tournez en rond. Vous pourriez\nvous en débarrasser, ou sinon vos derniers moments seront divertissants.", + "If you kill an enemy in one hit you get double points for it.": "Si vous tuez un ennemi en un coup vous obtiendriez deux fois plus de points.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Si vous prenez une malédiction, votre seule chance est de\ntrouver un paquet de santé dans les prochaines secondes.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Si vous restez en place, vous êtes foutu. Courez et esquiver pour survivre..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Si vous avez plusieurs joueurs qui sont intermittent, activez 'déconnecter-les-joueurs-inactifs'\ndans les paramètres au cas où quelqu'un oublierait de quitter le jeu.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Si votre appareil chauffe trop ou vous souhaitez économiser de la batterie,\nbaissez les \"Graphiques\" ou \"Résolution\" dans Paramètres->Graphiques", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Si votre fréquence d'images est agité, essayez de diminuer la résolution\nou les visuels dans les options graphiques.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "Dans Capturer le Drapeau, votre propre drapeau doit être à votre base pour marquer un point. Si l'autre\néquipe est proche de marquer, vous pouvez les ralentir en volant leur drapeau.", + "In hockey, you'll maintain more speed if you turn gradually.": "Dans hockey, vous maintiendrez votre vitesse si vous tournez graduellement.", + "It's easier to win with a friend or two helping.": "C'est plus facile de gagner avec l'aide d'un ou deux amis.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Sautez quand vous lancez une bombe pour envoyer le plus loin possible.", + "Land-mines are a good way to stop speedy enemies.": "Les mines terrestres sont un bon moyen pour ralentir les ennemis.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Beaucoup de choses peuvent être ramassées et jetées, même les autres joueurs. Jetez vos \nennemis hors des falaises est une stratégie efficace et hilarante.", + "No, you can't get up on the ledge. You have to throw bombs.": "Non, vous ne pouvez pas monter sur le rebord. Vous devez lancer des bombes.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Les joueurs peuvent joindre et quitter quand les jeux sont en cours, \nvous pouvez aussi brancher et débrancher les contrôleurs rapidement.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Les joueurs peuvent rejoindre et quitter en jeu dans la plupart des modes de jeu,\nvous pouvez aussi débrancher et rebrancher une manette en cours de partie.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Les bonus ont seulement une limite de temps en co-op.\nEn équipe et en mêlée générale ils sont à vous jusqu'à ce que vous mouriez.", + "Practice using your momentum to throw bombs more accurately.": "Entraînez vous à utiliser votre momentum pour vous lancez les bombes plus précisément.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Les coups de poing font plus de dommage quand vous bougez plus vite,\nessayez de courir, sauter, et tourner comme un fou.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Courez en arrière et en avant avant de lancer votre bombe pour lancer plus loin.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Éliminez un groupe d'ennemis en fesant\nexploser une bombe près du TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "La tête est la plus vulnérable, une bombe gluante sur \nla caboche et c'est généralement la fin de la partie.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Ce niveau ne finit jamais, mais un score élevé vous \nfera gagner le respect éternel à travers le monde.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "La force du lancé est basée sur la direction que vous maintenez.\nPour lancez gentiment devant vous, ne maintenez aucune direction.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Ennuyé par la bande sonore? Remplacez la avec votre propre musique!\nAllez à Paramètres->Audio->Bande Sonore", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Essayez de 'tenir au chaud' les bombes une ou deux secondes avant de les lancer.", + "Try tricking enemies into killing eachother or running off cliffs.": "Essayez de duper vos ennemis pour qu'ils se tuent entre eux ou les faire sauter des falaises.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Utilisez le bouton rammaser pour saisir le drapeau < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Marchez en arrière puis en avant pour avoir plus de distance avec vos lances..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Vous pouvez 'viser' avec vos poings en tournant à droite ou à gauche.\nC'est utile pour frappez les \"bad guys\" hors des bords ou marquez en hockey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Vous pouvez juger quand la bombe va exploser basant sur la \ncouleur de la mèche: jaune..orange..rouge..BOOM.", + "You can throw bombs higher if you jump just before throwing.": "Vous pouvez lancer les bombes plus loin en sautant avant de lancer.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Vous n'avez pas besoin d'éditer votre profil pour changer de personnage; appuyez juste sur\nle bouton du haut (prendre) quand vous rejoignez une partie pour remplacer celui par défaut.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Vous subissez des dégâts quand vous vous cognez la tête,\nalors essayez de ne pas vous cogner la tête.", + "Your punches do much more damage if you are running or spinning.": "Vos poings font plus de dommage si vous êtes en train de courir ou tourner." + } + }, + "trialPeriodEndedText": "Votre période d'essai est terminée. Voulez vous\nacheter BombSquad et continuer à jouer?", + "trophiesRequiredText": "Cela nécessite au moins ${NUMBER} trophées.", + "trophiesText": "Trophées", + "trophiesThisSeasonText": "Trophées de cette saison", + "tutorial": { + "cpuBenchmarkText": "Exécution tutoriel à vitesse grotesque (teste principalement la vitesse CPU)", + "phrase01Text": "Bonjour!", + "phrase02Text": "Bienvenue à ${APP_NAME}!", + "phrase03Text": "Voici quelques conseils pour contrôler votre personnage:", + "phrase04Text": "Beaucoup de choses sont basées sur la PHYSIQUE dans ${APP_NAME}.", + "phrase05Text": "Par exemple, quand vous frappez,..", + "phrase06Text": "..le dommage est basé sur la vitesse de vos poings.", + "phrase07Text": "Vous avez vu? Nous n'avons pas bougé et les dégâts ont été minuscule pour ${NAME}.", + "phrase08Text": "Maintenant nous allons sauter et tourner pour gagner plus de vitesse.", + "phrase09Text": "Voilà, c'est mieux.", + "phrase10Text": "Courir aide aussi.", + "phrase11Text": "Gardez N'IMPORTE quel bouton enfoncé pour courir.", + "phrase12Text": "Pour des coup de poings extraordinaires, essayez de courir ET tourner.", + "phrase13Text": "Oups; pardon ${NAME}.", + "phrase14Text": "Vous pouvez ramasser et jeter des choses comme des drapeaux... ou ${NAME}.", + "phrase15Text": "Enfin, il y a les bombes.", + "phrase16Text": "Lancer les bombes demande de la pratique.", + "phrase17Text": "Ouch! Mauvais lancé.", + "phrase18Text": "Bouger vous permet de lancer plus loin.", + "phrase19Text": "Sauter vous permet de lancer plus haut.", + "phrase20Text": "Donnez un \"Coup de fouet\" à vos bombes pour des lancés encore plus long.", + "phrase21Text": "Synchroniser vos bombes peut-être difficile.", + "phrase22Text": "Zut.", + "phrase23Text": "Essayez de \"garder au chaud\" les bombes pour réduire la mèche.", + "phrase24Text": "Hourra! Une bombe chaude, une !", + "phrase25Text": "Et bien, c'est à peu près tout.", + "phrase26Text": "Maintenant, terrassez vos ennemis!", + "phrase27Text": "Souvenez-vous de votre entraînement, et vous allez revenir en vie!", + "phrase28Text": "...bien, peut-être...", + "phrase29Text": "Bonne Chance!", + "randomName1Text": "Jean", + "randomName2Text": "Foux", + "randomName3Text": "David", + "randomName4Text": "Julie", + "randomName5Text": "Marie", + "skipConfirmText": "Vraiment passer le tutoriel? Appuyez pour confirmer.", + "skipVoteCountText": "${COUNT}/${TOTAL} votes pour passer", + "skippingText": "passage du tutoriel...", + "toSkipPressAnythingText": "(appuyez n'importe où pour passer le tutoriel)" + }, + "twoKillText": "DOUBLE MEURTRE!", + "unavailableText": "indisponible", + "unconfiguredControllerDetectedText": "Contrôleur non-configuré détecté:", + "unlockThisInTheStoreText": "Cela doit être débloqué dans le magasin.", + "unlockThisProfilesText": "Pour créer plus de ${NUM} profiles, vous avez besoin de:", + "unlockThisText": "Pour débloquer ceci, vous avez besoin:", + "unsupportedHardwareText": "Désolé, ce hardware n'est pas supporté par cette version du jeu.", + "upFirstText": "En premier:", + "upNextText": "Le jeu ${COUNT} sera:", + "updatingAccountText": "Mise à jour du compte...", + "upgradeText": "Mise à jour", + "upgradeToPlayText": "Débloquez \"${PRO}\" dans le magasin pour jouer ceci.", + "useDefaultText": "Utilisez le Défaut", + "usesExternalControllerText": "Ce jeu utilise un contrôleur externe pour l'input.", + "usingItunesText": "Utilisez Music App pour la bande-son...", + "usingItunesTurnRepeatAndShuffleOnText": "Vérifiez que 'Lecture Aléatoire'='ACTIF' et 'Reprise'='TOUT' dans iTunes.", + "validatingBetaText": "Validation de la beta...", + "validatingTestBuildText": "Validation de la Version Test...", + "victoryText": "Victoire!", + "voteDelayText": "Vous ne pouvez commencer un autre vote que dans ${NUMBER} secondes", + "voteInProgressText": "Un vote est déjà en cours.", + "votedAlreadyText": "Vous avez déjà voté", + "votesNeededText": "${NUMBER} Vote son requis", + "vsText": "vs.", + "waitingForHostText": "(en attente de ${HOST} pour continuer)", + "waitingForLocalPlayersText": "En attente de joueur en local...", + "waitingForPlayersText": "attente de connexion d'autres joueurs...", + "waitingInLineText": "En attente (la partie est pleine)...", + "watchAVideoText": "Regarder une vidéo", + "watchAnAdText": "Visionnez une Annonce", + "watchWindow": { + "deleteConfirmText": "Effacer \"${REPLAY}\"?", + "deleteReplayButtonText": "Effacer\nLa Reprise", + "myReplaysText": "Mes Reprises", + "noReplaySelectedErrorText": "Aucun Reprise Sélectionné", + "playbackSpeedText": "Vitesse de lecture: ${SPEED}", + "renameReplayButtonText": "Renommer\nLa Reprise", + "renameReplayText": "Renommer \"${REPLAY}\" à:", + "renameText": "Renommer", + "replayDeleteErrorText": "Impossible d'effacer la reprise.", + "replayNameText": "Nom de la Reprise", + "replayRenameErrorAlreadyExistsText": "Une reprise avec ce nom existe déjà.", + "replayRenameErrorInvalidName": "Impossible de renommer la reprise; nom invalide.", + "replayRenameErrorText": "Impossible de renommer la reprise.", + "sharedReplaysText": "Reprises Partagées", + "titleText": "Reprises", + "watchReplayButtonText": "Visionner\nLa Reprise" + }, + "waveText": "Vague", + "wellSureText": "Bien Sûr!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "En attente de connexion de wiimotes...", + "pressText": "Appuyez sur les boutons 1 et 2 simultanément.", + "pressText2": "Avec les nouveaux wiimotes avec Motion Plus, appuyez le bouton rouge 'sync' en arrière." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "listenText": "Écoute", + "macInstructionsText": "Vérifiez que votre Wii est éteinte et que le Bluetooth est\nactivé sur votre Mac, puis appuyez sur 'Écoute'. Le support\nWiimote peut-être un peu incertain, alors vous devrez (peut-\nêtre) essayer plusieurs fois avant d'avoir une connection.\n\nBluetooth doit supporter jusqu'à 7 appareils,\nmais ce n'est pas garanti.\n\nBombSquad supporte les Wiimotes originales, les Nunchuks,\net le Contrôleur Classique.\nLe nouvel Wii Remote Plus fonctionne\naussi mais sans attachements.", + "thanksText": "Merci à l'équipe DarwiinRemote pour\navoir rendu ceci possible.", + "titleText": "Configuration Wiimote" + }, + "winsPlayerText": "${NAME} a Gagné!", + "winsTeamText": "${NAME} a Gagné!", + "winsText": "${NAME} a Gagné!", + "worldScoresUnavailableText": "Les scores mondial sont indisponibles.", + "worldsBestScoresText": "Meilleurs scores mondiaux", + "worldsBestTimesText": "Meilleurs temps mondiaux", + "xbox360ControllersWindow": { + "getDriverText": "Obtenez les pilotes", + "macInstructions2Text": "Pour utiliser les contrôleurs sans-fils, vous avez besoin du récepteur \nqui vient avec le 'Contrôleur Xbox 360 Sans-Fils pour Windows'.\nUn seul récepteur est suffisante pour jusqu'à 4 contrôleurs.\n\nImportant: les recepteurs 3e-Party ne fonctionneront pas avec\nce pilote; soyez sûr que votre récepteur a 'Microsoft' écrit,\npas 'Xbox 360'. Microsoft ne vend plus séparément, alors vous \ndevez acheter avec le ontrôleur ou cherchez sur eBay.\n\nSi vous avez trouvé ceci utile, considerez à contribuer au développeur\ndu pilote sur son site-web.", + "macInstructionsText": "Pour utiliser les contrôleurs Xbox 360, vous devez installer \nle pilote pour Mac qui est disponible au lien en-dessous. \nIl fonctionne avec les contrôleurs sans-fils et filaires.", + "ouyaInstructionsText": "Pour utiliser les contrôleurs filaires Xbox 360 avec BombSquad,\nbranchez simplement dans le port USB. Vous pouvez utiliser\nun hub USB pour connecter plusieurs contrôleurs.\n\nPour utilisez les contrôleurs sans-fils, vous devez avoir un récepteur, \ndisponible avec le \"Contrôleur Xbox 360 sans-fils pour Windows\"\npaquet ou vendre individuellement. Chaque récepteur se branche dans un port\nUSB et vous permet à connecter 4 contrôleurs sans-fils", + "titleText": "Utiliser les Contrôleurs Xbox 360 avec ${APP_NAME}:" + }, + "yesAllowText": "Oui, Autoriser!", + "yourBestScoresText": "Vos meilleurs scores", + "yourBestTimesText": "Vos meilleurs temps" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/german.json b/dist/ba_data/data/languages/german.json new file mode 100644 index 0000000..cab23bb --- /dev/null +++ b/dist/ba_data/data/languages/german.json @@ -0,0 +1,1968 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Der Account Name darf kein Emoji oder andere spezielle Buchstaben enthalten.", + "accountProfileText": "Benutzerprofil", + "accountsText": "Konten", + "achievementProgressText": "Erfolge: ${COUNT} von ${TOTAL}", + "campaignProgressText": "Kampagnen Fortschritt [Schwer]: ${PROGRESS}", + "changeOncePerSeason": "Du kannst dies nur einmal pro Saison ändern.", + "changeOncePerSeasonError": "Du musst warten bis du das wieder in der nächsten Saison ändern kannst. (${NUM} Tage)", + "customName": "Benutzerdefinierter Name", + "linkAccountsEnterCodeText": "Code eingeben", + "linkAccountsGenerateCodeText": "Code generieren", + "linkAccountsInfoText": "(Teile deine Erfolge über verschiedene Platformen)", + "linkAccountsInstructionsNewText": "Um zwei Accounts zu verknüpfen muss ein Code auf dem ersten Gerät generiert werden und\ndieser Code auf dem zweiten Gerät eingegeben werden.\nDaten von dem zweiten Account werden dann zwischen beiden Geräten geteilt.\n(Daten vom ersten Account gehen verloren!)\n\nDu kannst bis zu ${COUNT} Accounts verknüpfen.\n\nWICHTIG: Verknüpfe nur Accounts die dir gehören;\nWenn du deine Accounts mit denen deiner Freunden\nverknüpft kannst du nicht mit deinen Freunden gleichzeitig online spielen.", + "linkAccountsInstructionsText": "Um zwei Accounts miteinander zu verknüpfen,generiere einen \nCode auf einem der Geräte und trage ihn auf dem anderen ein.\nFortschritt und Inventar werden kombiniert.\nDu kannst bis zu ${COUNT} Accounts miteinander verknüpfen.\n\nSei vorsichtig; das kann nicht rückgängig gemacht werden!", + "linkAccountsText": "Accounts verknüpfen", + "linkedAccountsText": "Verknüpfte Accounts:", + "nameChangeConfirm": "Möchtest du deinen Account Namen in ${NAME} ändern?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Das wird deinen Koop-Fortschritt und deine\nHigh-Scores zurücksetzen (aber nicht deine Tickets).\nDies kann nicht rückgängig gemacht werden. Bist du sicher?", + "resetProgressConfirmText": "Das wird deinen Koop-Fortschritt, deine\nErfolge und deine lokalen High-Scores\nzurücksetzen (aber nicht deine Tickets). Dies kann\nnicht rückgängig gemacht werden. Bist du sicher?", + "resetProgressText": "Fortschritt zurücksetzen", + "setAccountName": "Account Name festlegen", + "setAccountNameDesc": "Wählen Sie den Namen aus, der für Ihr Konto angezeigt werden soll.\nSie können den Namen von einem Ihrer verlinkten\nKonten benutzen oder erstellen Sie einen einzigartigen benutzerdefinierten Name.", + "signInInfoText": "Melde dich an, um deinen Fortschritt in der Cloud zu speichern,\nTickets zu verdienen und an Turnieren teilzunehmen.", + "signInText": "Anmelden", + "signInWithDeviceInfoText": "(ein automatischer Account, den du nur von diesem Gerät benutzen kannst)", + "signInWithDeviceText": "Mit Gerät-Account einloggen", + "signInWithGameCircleText": "Mit Game Circle einloggen", + "signInWithGooglePlayText": "Mit Google Play anmelden", + "signInWithTestAccountInfoText": "(veraltete Accountart, benutze Gerät-Accounts)", + "signInWithTestAccountText": "Mit Testaccount einloggen", + "signOutText": "Abmelden", + "signingInText": "Anmelden...", + "signingOutText": "Abmelden...", + "testAccountWarningCardboardText": "Achtung: Du bist mit einen \"Testaccount\" angemeldet.\nDiese werden durch Google-Konten ersetzt, sobald\ndiese in Google Cardboard unterstützt werden.\n\nFürs erste wirst du alle Tickets im Spiel verdienen müssen.\n(Du bekommst aber dafür das BombSquad Pro Upgrade umsonst)", + "testAccountWarningOculusText": "Achtung: Du bist mit einen \"Testaccount\" angemeldet.\nDieser wird im Laufe diesen Jahres mit einem Oculus Account ersetzt. \nDadurch kann man Tickets kaufen und vieles mehr.\n\nFürs erste wirst du alle Tickets im Spiel verdienen müssen.\n(Du bekommst aber dafür das BombSquad Pro Upgrade umsonst)", + "testAccountWarningText": "Achtung: Du meldest dich mit einem \"Testaccount\" an.\nDieser Account ist an dieses Gerät gebunden und wird\nregelmäßig zurückgesetzt. (Also bitte gib dir nicht \nzuviel Mühe um etwas zu sammeln/freizuschalten)\n\nVerwende eine offizielle Version des Spiels, um einen \"echten\" \nAccount zu benutzen (Game-Center, Google Plus, etc.) \nDas erlaubt es dir auch, deinen Spielstand in der Cloud zu \nspeichern und auf einem anderen Gerät weiterzuspielen.", + "ticketsText": "Tickets: ${COUNT}", + "titleText": "Account", + "unlinkAccountsInstructionsText": "Wähle ein Account aus um die Verknüpfung zu trennen", + "unlinkAccountsText": "Accounts entknüpfen", + "viaAccount": "(über dem Account ${NAME})", + "youAreLoggedInAsText": "Du bist eingeloggt als:", + "youAreSignedInAsText": "Du bist angemeldet als:" + }, + "achievementChallengesText": "Herausforderungen", + "achievementText": "Erfolg", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Töte 3 Bösewichte mit TNT", + "descriptionComplete": "3 Bösewichte mit TNT getötet", + "descriptionFull": "Töte 3 Bösewichte mit TNT bei ${LEVEL}", + "descriptionFullComplete": "3 Bösewichte mit TNT bei ${LEVEL} getötet", + "name": "Bumm macht das Dynamit" + }, + "Boxer": { + "description": "Gewinne ohne Bomben zu nutzen", + "descriptionComplete": "Ohne Bomben zu nutzen gewonnen", + "descriptionFull": "Bestehe ${LEVEL} ohne Bomben zu nutzen", + "descriptionFullComplete": "${LEVEL} beendet ohne Bomben zu nutzen", + "name": "Boxer" + }, + "Dual Wielding": { + "descriptionFull": "Verbinde 2 Controller (Hardware oder App.)", + "descriptionFullComplete": "Verbinde 2 Controller (Hardware oder App.)", + "name": "Beidhändig" + }, + "Flawless Victory": { + "description": "Gewinne ohne getroffen zu werden", + "descriptionComplete": "Gewonnen ohne getroffen zu werden", + "descriptionFull": "Gewinne ${LEVEL} ohne getroffen zu werden", + "descriptionFullComplete": "${LEVEL} gewonnen ohne getroffen zu werden", + "name": "Tadelloser Sieg" + }, + "Free Loader": { + "descriptionFull": "Starte ein freies Spiel für alle (2+ Spieler)", + "descriptionFullComplete": "Starte ein freies Spiel für alle (2+ Spieler)", + "name": "Schnorrer" + }, + "Gold Miner": { + "description": "Töte 6 Bösewichte mit Landminen", + "descriptionComplete": "6 Bösewichte mit Landminen getötet", + "descriptionFull": "Töte 6 Bösewichte mit Landminen bei ${LEVEL}", + "descriptionFullComplete": "6 Bösewichte mit Landminen bei ${LEVEL} getötet", + "name": "Goldgräber" + }, + "Got the Moves": { + "description": "Gewinne ohne Schläge und Bomben", + "descriptionComplete": "Ohne Schläge und Bomben gewonnen", + "descriptionFull": "Gewinne ${LEVEL} ohne Schläge und Bomben", + "descriptionFullComplete": "${LEVEL} ohne Schläge und Bomben gewonnen", + "name": "Du hast es raus" + }, + "In Control": { + "descriptionFull": "Verbinde einen Controller (Hardware oder App.)", + "descriptionFullComplete": "Verbinde einen Controller. (Hardware oder App.)", + "name": "Am Steuer" + }, + "Last Stand God": { + "description": "Sammle 1000 Punkte", + "descriptionComplete": "1000 Punkte gesammelt", + "descriptionFull": "Sammle 1000 Punkte in ${LEVEL}", + "descriptionFullComplete": "1000 Punkte in ${LEVEL} erreicht", + "name": "${LEVEL} Gott" + }, + "Last Stand Master": { + "description": "Sammle 250 Punkte", + "descriptionComplete": "250 Punkte gesammelt", + "descriptionFull": "Sammle 250 Punkte in ${LEVEL}", + "descriptionFullComplete": "250 Punkte in ${LEVEL} gesammelt", + "name": "${LEVEL} Meister" + }, + "Last Stand Wizard": { + "description": "Sammle 500 Punkte", + "descriptionComplete": "500 Punkte gesammelt", + "descriptionFull": "Sammle 500 Punkte in ${LEVEL}", + "descriptionFullComplete": "500 Punkte in ${LEVEL} gesammelt", + "name": "${LEVEL} Magier" + }, + "Mine Games": { + "description": "Töte 3 Bösewichte mit Landminen", + "descriptionComplete": "3 Bösewichte mit Landminen getötet", + "descriptionFull": "Töte 3 Bösewichte mit Landminen bei ${LEVEL}", + "descriptionFullComplete": "3 Bösewichte mit Landminen bei ${LEVEL} getötet", + "name": "Minenspiele" + }, + "Off You Go Then": { + "description": "Wirf 3 Bösewichte vom Schlachtfeld", + "descriptionComplete": "3 Bösewichte vom Schlachtfeld geworfen", + "descriptionFull": "Wirf 3 Bösewichte vom Schlachtfeld bei ${LEVEL}", + "descriptionFullComplete": "3 Bösewichte vom Schlachtfeld geworfen bei ${LEVEL}", + "name": "Und raus bist du" + }, + "Onslaught God": { + "description": "Sammle 5000 Punkte", + "descriptionComplete": "5000 Punkte gesammelt", + "descriptionFull": "Sammle 5000 Punkte in ${LEVEL}", + "descriptionFullComplete": "5000 Punkte in ${LEVEL} gesammelt", + "name": "${LEVEL} Gott" + }, + "Onslaught Master": { + "description": "Sammle 500 Punkte", + "descriptionComplete": "500 Punkte gesammelt", + "descriptionFull": "Sammle 500 Punkte in ${LEVEL}", + "descriptionFullComplete": "500 Punkte in ${LEVEL} gesammelt", + "name": "${LEVEL} Meister" + }, + "Onslaught Training Victory": { + "description": "Vernichte alle Wellen", + "descriptionComplete": "Alle Wellen vernichtet", + "descriptionFull": "Vernichte alle Wellen bei ${LEVEL}", + "descriptionFullComplete": "Alle Wellen bei ${LEVEL} vernichtet", + "name": "${LEVEL} Sieg" + }, + "Onslaught Wizard": { + "description": "Sammle 1000 Punkte", + "descriptionComplete": "1000 Punkte gesammelt", + "descriptionFull": "Sammle 1000 Punkte in ${LEVEL}", + "descriptionFullComplete": "1000 Punkte in ${LEVEL} gesammelt", + "name": "${LEVEL} Magier" + }, + "Precision Bombing": { + "description": "Gewinne ohne Powerups", + "descriptionComplete": "Ohne Powerups gewonnen", + "descriptionFull": "Gewinne ${LEVEL} ohne Powerups", + "descriptionFullComplete": "${LEVEL} ohne Powerups gewonnen", + "name": "Präzisionsbomben" + }, + "Pro Boxer": { + "description": "Gewinne ohne Bomben zu nutzen", + "descriptionComplete": "Gewonnen ohne Bomben zu nutzen", + "descriptionFull": "Bestehe ${LEVEL} ohne Bomben zu nutzen", + "descriptionFullComplete": "${LEVEL} bestanden ohne Bomben zu nutzen", + "name": "Pro Boxer" + }, + "Pro Football Shutout": { + "description": "Gewinne ohne dass die Bösewichte punkten", + "descriptionComplete": "Gewonnen ohne dass die Bösewichte punkten", + "descriptionFull": "Gewinne ${LEVEL} ohne dass die Bösewichte punkten", + "descriptionFullComplete": "${LEVEL} gewonnen ohne dass die Bösewichte punkten", + "name": "${LEVEL} Shutout" + }, + "Pro Football Victory": { + "description": "Gewinne das Spiel", + "descriptionComplete": "Spiel gewonnen", + "descriptionFull": "Gewinne das Spiel in ${LEVEL}", + "descriptionFullComplete": "${LEVEL} Spiel gewonnen", + "name": "${LEVEL} Sieg" + }, + "Pro Onslaught Victory": { + "description": "Vernichte alle Wellen", + "descriptionComplete": "Alle Wellen vernichtet", + "descriptionFull": "Vernichte alle Wellen bei ${LEVEL}", + "descriptionFullComplete": "Alle Wellen bei ${LEVEL} vernichtet", + "name": "${LEVEL} Sieg" + }, + "Pro Runaround Victory": { + "description": "Überstehe alle Wellen", + "descriptionComplete": "Alle Wellen überstanden", + "descriptionFull": "Überstehe alle Wellen bei ${LEVEL}", + "descriptionFullComplete": "Alle Wellen bei ${LEVEL} überstanden", + "name": "${LEVEL} Sieg" + }, + "Rookie Football Shutout": { + "description": "Gewinne ohne dass die Bösewichte punkten", + "descriptionComplete": "Gewonnen ohne dass die Bösewichte punkten", + "descriptionFull": "Gewinne ${LEVEL} ohne dass die Bösewichte punkten", + "descriptionFullComplete": "${LEVEL} gewonnen ohne dass die Bösewichte punkten", + "name": "${LEVEL} Shutout" + }, + "Rookie Football Victory": { + "description": "Gewinne das Spiel", + "descriptionComplete": "Spiel gewonnen", + "descriptionFull": "Gewinne das ${LEVEL} Spiel", + "descriptionFullComplete": "${LEVEL} Spiel gewonnen", + "name": "${LEVEL} Sieg" + }, + "Rookie Onslaught Victory": { + "description": "Vernichte alle Wellen", + "descriptionComplete": "Alle Wellen vernichtet", + "descriptionFull": "Vernichte alle Wellen bei ${LEVEL}", + "descriptionFullComplete": "Alle Wellen bei ${LEVEL} vernichtet", + "name": "${LEVEL} Sieg" + }, + "Runaround God": { + "description": "Sammle 2000 Punkte", + "descriptionComplete": "2000 Punkte gesammelt", + "descriptionFull": "Sammle 2000 Punkte bei ${LEVEL}", + "descriptionFullComplete": "2000 Punkte bei ${LEVEL} gesammelt", + "name": "${LEVEL} Gott" + }, + "Runaround Master": { + "description": "Sammle 500 Punkte", + "descriptionComplete": "500 Punkte gesammelt", + "descriptionFull": "Sammle 500 Punkte bei ${LEVEL}", + "descriptionFullComplete": "500 Punkte bei ${LEVEL} gesammelt", + "name": "${LEVEL} Meister" + }, + "Runaround Wizard": { + "description": "Sammle 1000 Punkte", + "descriptionComplete": "1000 Punkte gesammelt", + "descriptionFull": "Erreiche 1000 Punkte bei ${LEVEL}", + "descriptionFullComplete": "1000 Punkte bei ${LEVEL} erreicht", + "name": "${LEVEL} Magier" + }, + "Sharing is Caring": { + "descriptionFull": "Teile das Spiel erfolgreich mit einem Freund", + "descriptionFullComplete": "Erfolgreich das Spiel mit einem Freund geteilt.", + "name": "Teilen ist freundlich" + }, + "Stayin' Alive": { + "description": "Gewinne ohne zu sterben", + "descriptionComplete": "Gewonnen ohne zu sterben", + "descriptionFull": "Gewinne ${LEVEL} ohne zu sterben", + "descriptionFullComplete": "${LEVEL} gewonnen ohne zu sterben", + "name": "Am Leben bleiben" + }, + "Super Mega Punch": { + "description": "Verursache 100% Schaden mit einem Schlag", + "descriptionComplete": "100% Schaden mit einem Schlag verursacht", + "descriptionFull": "Verursache 100% Schaden mit einem Schlag bei ${LEVEL}", + "descriptionFullComplete": "100% Schaden mit einem Schlag bei ${LEVEL} verursacht", + "name": "Super Mega Schlag" + }, + "Super Punch": { + "description": "Verursache 50% Schaden mit einem Schlag", + "descriptionComplete": "50% Schaden mit einem Schlag verursacht", + "descriptionFull": "Verursache 50% Schaden mit einem Schlag bei ${LEVEL}", + "descriptionFullComplete": "50% Schaden mit einem Schlag bei ${LEVEL} verursacht", + "name": "Super Schlag" + }, + "TNT Terror": { + "description": "Töte 6 Bösewichte mit TNT", + "descriptionComplete": "6 Bösewichte mit TNT getötet", + "descriptionFull": "Töte 6 Bösewichte mit TNT bei ${LEVEL}", + "descriptionFullComplete": "6 Bösewichte in ${LEVEL} mit TNT getötet", + "name": "TNT Terror" + }, + "Team Player": { + "descriptionFull": "Starte ein Team Spiel mit 4+ Spielern", + "descriptionFullComplete": "Ein Spiel mit 4+ Spielern gestartet.", + "name": "Team Spieler" + }, + "The Great Wall": { + "description": "Halte jeden einzelnen Bösewicht auf", + "descriptionComplete": "Jeden einzelnen Bösewicht aufgehalten", + "descriptionFull": "Halte jeden einzelnen Bösewicht in ${LEVEL} auf", + "descriptionFullComplete": "Jeden einzelnen Bösewicht in ${LEVEL} aufgehalten", + "name": "Die große Mauer" + }, + "The Wall": { + "description": "Halte jeden einzelnen Bösewicht auf", + "descriptionComplete": "Jeden einzelnen Bösewichte aufgehalten", + "descriptionFull": "Halte jeden einzelnen Bösewicht bei ${LEVEL} auf", + "descriptionFullComplete": "Jeden einzelnen Bösewicht bei ${LEVEL} aufgehalten", + "name": "Die Mauer" + }, + "Uber Football Shutout": { + "description": "Gewinne ohne dass die Bösewichte punkten", + "descriptionComplete": "Gewonnen ohne dass die Bösewichte punkten", + "descriptionFull": "Gewinne ${LEVEL} ohne dass die Bösewichte punkten", + "descriptionFullComplete": "${LEVEL} gewonnen ohne dass die Bösewichte punkten", + "name": "${LEVEL} Shutout" + }, + "Uber Football Victory": { + "description": "Gewinne das Spiel", + "descriptionComplete": "Spiel gewonnen", + "descriptionFull": "Gewinne das Spiel in ${LEVEL}", + "descriptionFullComplete": "Spiel gewonnen in ${LEVEL}", + "name": "${LEVEL} Sieg" + }, + "Uber Onslaught Victory": { + "description": "Vernichte alle Wellen", + "descriptionComplete": "Alle Wellen vernichtet", + "descriptionFull": "Vernichte alle Wellen in ${LEVEL}", + "descriptionFullComplete": "Alle Wellen in ${LEVEL} vernichtet", + "name": "${LEVEL} Sieg" + }, + "Uber Runaround Victory": { + "description": "Überstehe alle Wellen", + "descriptionComplete": "Alle Wellen überstanden", + "descriptionFull": "Überstehe alle Wellen in ${LEVEL}", + "descriptionFullComplete": "Alle Wellen in ${LEVEL} überstanden", + "name": "${LEVEL} Sieg" + } + }, + "achievementsRemainingText": "Fehlende Erfolge:", + "achievementsText": "Erfolge", + "achievementsUnavailableForOldSeasonsText": "Leider Leistung Besonderheiten nicht für alte Jahreszeiten zur Verfügung.", + "addGameWindow": { + "getMoreGamesText": "Hol dir mehr Spiele...", + "titleText": "Spiel hinzufügen", + "titleTextScale": 1.0 + }, + "allowText": "Erlauben", + "alreadySignedInText": "Dein Account wird schon von einem anderen Gerät verwendet;\nbitte wechsle den Account oder schließe das Spiel auf\ndeinem anderen Gerät und versuche es nochmal.", + "apiVersionErrorText": "Das Modul ${NAME} kann nicht geladen werden. Es benutzt API-Version ${VERSION_USED}, aber wir brauchen ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" erlaubt das nur, wenn Kopfhörer angeschlossen sind)", + "headRelativeVRAudioText": "Kopf-Orientiertes VR Audio", + "musicVolumeText": "Musiklautstärke", + "soundVolumeText": "Soundlautstärke", + "soundtrackButtonText": "Musiktitel", + "soundtrackDescriptionText": "(Lasse deine eigene Musik während dem Spielen laufen)", + "titleText": "Audio" + }, + "autoText": "Automatisch", + "backText": "Zurück", + "banThisPlayerText": "Diesen Spieler verbannen", + "bestOfFinalText": "Bester-aus-${COUNT} Finale", + "bestOfSeriesText": "Bester-aus-${COUNT} Serie:", + "bestRankText": "Dein Rekord ist #${RANK}", + "bestRatingText": "Dein bestes Ergebnis ist ${RATING}", + "betaErrorText": "Diese Beta ist nicht mehr aktiv; bitte hole dir die neueste Version.", + "betaValidateErrorText": "Beta nicht verifizierbar. (keine Internetverbindung?)", + "betaValidatedText": "Beta verifiziert; Viel Spaß!", + "bombBoldText": "BOMBE", + "bombText": "Bombe", + "boostText": "Boost", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} ist in der App konfiguriert.", + "buttonText": "Taste", + "canWeDebugText": "Möchtest du, dass BombSquad automatisch Fehler, Abstürze\nund Benutzeraktivitäten an den Entwickler sendet?\n\nDiese Daten enthalten keinerlei persönliche Informationen\nund helfen, das Spiel reibungslos und fehlerfrei zu erhalten.", + "cancelText": "Abbrechen", + "cantConfigureDeviceText": "Entschuldigung, ${DEVICE} ist nicht konfigurierbar.", + "challengeEndedText": "Diese Herausforderung ist beendet.", + "chatMuteText": "Chat stummschalten", + "chatMutedText": "Chat stumm", + "chatUnMuteText": "Chat aktivieren", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Du musst dieses Level\nabschließen um fortzufahren!", + "completionBonusText": "Bewältigungsbonus", + "configControllersWindow": { + "configureControllersText": "Controller Einstellungen", + "configureGamepadsText": "Gamepads Konfigurieren", + "configureKeyboard2Text": "Tastatur konfigurieren P2", + "configureKeyboardText": "Tastatur konfigurieren", + "configureMobileText": "Mobilgeräte als Controller", + "configureTouchText": "Touchscreen konfigurieren", + "ps3Text": "PS3 Controller", + "titleText": "Controllers", + "wiimotesText": "Wiimotes", + "xbox360Text": "Xbox 360 Controller" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Info: Controller Unterstützung ist vom Gerät und der Android Version abhängig", + "pressAnyButtonText": "Drücke einen Knopf am Controller, um ihn zu konfigurieren...", + "titleText": "Controller konfigurieren" + }, + "configGamepadWindow": { + "advancedText": "Erweitert", + "advancedTitleText": "Erweiterte Controller Einstellungen", + "analogStickDeadZoneDescriptionText": "(Erhöhen, wenn dein Charakter beim Loslassen des Sticks abtreibt)", + "analogStickDeadZoneText": "Analog Stick Dead Zone", + "appliesToAllText": "(wird auf alle Controller dieses Typs angewendet)", + "autoRecalibrateDescriptionText": "(aktivieren, falls dein Charakter sich nicht in voller Geschwindigkeit bewegt)", + "autoRecalibrateDescriptionTextScale": 0.4, + "autoRecalibrateText": "Automatisches Kalibrieren des Analog Sticks", + "autoRecalibrateTextScale": 0.65, + "axisText": "Achsen", + "clearText": "Löschen", + "dpadText": "Steuerkreuz", + "extraStartButtonText": "Extra Start Button", + "ifNothingHappensTryAnalogText": "Wenn nichts passiert, versuche den Analog Stick zuzuweisen.", + "ifNothingHappensTryDpadText": "Wenn nichts passiert, versuche das Steuerkreuz zuzuweisen.", + "ignoreCompletelyDescriptionText": "(verhindere, dass dieser Controller das Spiel oder Menüs beeinflusst)", + "ignoreCompletelyText": "Komplett ignorieren", + "ignoredButton1Text": "Ignorierte Taste 1", + "ignoredButton2Text": "Ignorierte Taste 2", + "ignoredButton3Text": "Ignorierte Taste 3", + "ignoredButton4Text": "Ignorierte Taste 4", + "ignoredButtonDescriptionText": "(Benutze das, damit die \"Home\" oder \"Sync\" Tasten nicht ausversehen das Spiel beeinflussen)", + "ignoredButtonDescriptionTextScale": 0.4, + "ignoredButtonText": "Ignorierter Button", + "pressAnyAnalogTriggerText": "Drücke einen beliebigen Analogknopf...", + "pressAnyButtonOrDpadText": "Drücke einen beliebigen Button oder Steuerkreuz...", + "pressAnyButtonText": "Drücke eine beliebige Taste...", + "pressLeftRightText": "Drücke links oder rechts...", + "pressUpDownText": "Drücke hoch oder runter...", + "runButton1Text": "Renntaste 1", + "runButton2Text": "Renntaste 2", + "runTrigger1Text": "Rennauslöser 1", + "runTrigger2Text": "Rennauslöser 2", + "runTriggerDescriptionText": "(Analoge Auslöser für unterschiedliche Laufgeschwindigkeiten)", + "secondHalfText": "Benutze das, um die zweite Seite\neines 2-Controllers-in-1 Gerätes einzustellen\ndass als ein einzelner Controller angezeigt wird.", + "secondaryEnableText": "Aktivieren", + "secondaryText": "Zweiter Controller", + "startButtonActivatesDefaultDescriptionText": "Deaktivieren, wenn dein Startbutton eher ein Menübutton ist", + "startButtonActivatesDefaultText": "Starttaste aktiviert Standard-Widget", + "startButtonActivatesDefaultTextScale": 0.65, + "titleText": "Controllereinstellungen", + "twoInOneSetupText": "2-in-1 Controllereinstellungen", + "uiOnlyDescriptionText": "Verhindere, dass dieser Controller dem Spiel beitritt.", + "uiOnlyText": "Nur das Menü bedienen lassen", + "unassignedButtonsRunText": "Alle unbelegten Tasten zum Rennen", + "unassignedButtonsRunTextScale": 0.8, + "unsetText": "", + "vrReorientButtonText": "VR Neu Ausrichten Taste" + }, + "configKeyboardWindow": { + "configuringText": "${DEVICE} Konfigurieren", + "keyboard2NoteScale": 0.7, + "keyboard2NoteText": "Notiz: Die meisten Tastaturen können nur wenige Tastendrücke\ngleichzeitig registrieren, deshalb funktioniert es möglicherweise\nbesser, wenn der zweite Tastaturspieler eine separate Tastatur\nbenutzt. Beachte, dass du trotzem verschiedene Tasten für die\neinzelnen Spieler belegen musst." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Größe der Aktionstasten", + "actionsText": "Aktionen", + "buttonsText": "Tasten", + "dragControlsText": "< ziehe die Steuerungen, um sie neu zu positionieren >", + "joystickText": "Joystick", + "movementControlScaleText": "Steuerkreuzgröße", + "movementText": "Bewegung", + "resetText": "Reset", + "swipeControlsHiddenText": "Steuerkreuz ausblenden", + "swipeInfoText": "'Wisch'-Steuerung braucht etwas Eingewöhnungszeit, macht\nes aber leichter zu spielen, ohne auf die Steuerung zu sehen.", + "swipeText": "wischen", + "titleText": "Touchscreen einstellen", + "touchControlsScaleText": "Touch Steuerung Skalierung" + }, + "configureItNowText": "Konfiguriere es jetzt?", + "configureText": "Konfigurieren", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsScale": 0.65, + "bestResultsText": "Um das beste Ergebnis zu erzielen, benötigst du ein verzögerungsfreies\nWlan Netzwerk. Du kannst die Verzögerung reduzieren, indem du\nandere drahtlose Geräte abschaltest, näher am WLan-Rounter spielst\nund indem du dich direkt per Ethernet mit dem Spiele-Host verbindest.", + "explanationScale": 0.8, + "explanationText": "Um ein Smartphone oder Tablet als kabellosen Controller zu nutzen,\nmuss die \"${REMOTE_APP_NAME}\"-App installiert werden. Jegliche Anzahl an Geräten\nkönnen sich mit ${APP_NAME} per WLAN verbinden, und es ist kostenlos!", + "forAndroidText": "für Android:", + "forIOSText": "für iOS:", + "getItForText": "Hol dir ${REMOTE_APP_NAME} für iOS im Apple App Store,\noder für Android im Google Play Store oder Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Mobilgeräte als Controller nutzen:" + }, + "continuePurchaseText": "Für ${PRICE} fortfahren?", + "continueText": "Fortfahren", + "controlsText": "Steuerung", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Das ändert nichts an der dauerhaften Rangliste.", + "activenessInfoText": "Der Multiplikator steigt, wenn du viel spielst...\nWenn du weniger spielst sinkt er wieder", + "activityText": "Aktivität", + "campaignText": "Kampagne", + "challengesInfoText": "Gewinne Preise für das Beenden von Mini-Spielen.\n\nPreise und Schwierigkeitsgrade steigen mit jeder\nbeendeten Herausforderung und fallen, wenn\nHerausforderungen ablaufen oder aufgegeben werden.", + "challengesText": "Herausforderungen", + "currentBestText": "Aktueller Highscore", + "customText": "Benutzerdefiniert", + "entryFeeText": "Einsatz", + "forfeitConfirmText": "Diese Herausforderung aufgeben?", + "forfeitNotAllowedYetText": "Diese Herausforderung kann noch nicht aufgegeben werden.", + "forfeitText": "Aufgeben", + "multipliersText": "Multiplikatoren", + "nextChallengeText": "Nächste Herausforderung", + "nextPlayText": "Nächstes Spiel", + "ofTotalTimeText": "Von ${TOTAL}", + "playNowText": "Jetzt spielen", + "pointsText": "Punkte", + "powerRankingFinishedSeasonUnrankedText": "(Beendete Saison nicht plaziert)", + "powerRankingNotInTopText": "(Nicht in der top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} Pkt", + "powerRankingPointsMultText": "(x ${NUMBER} Pkt)", + "powerRankingPointsText": "${NUMBER} pkte", + "powerRankingPointsToRankedText": "(${CURRENT} von ${REMAINING} pkte)", + "powerRankingText": "Power Rang", + "prizesText": "Preise", + "proMultInfoText": "Spieler mit dem ${PRO} Upgrade\nbekommen zu ${PERCENT}% mehr Punkte hier.", + "seeMoreText": "Sieh weitere...", + "skipWaitText": "Überspringen", + "timeRemainingText": "Übrige Zeit", + "titleText": "Koop", + "toRankedText": "Bis zu den Rängen", + "totalText": "Gesamt", + "tournamentInfoText": "Trete gegen andere Spieler in deiner Liga an\nund ringe mit ihnen um die High-Scores.\n\nNach Ablauf der Turnierzeit werden die Preise \nan die Spieler mit den höchsten Punkten verliehen.", + "welcome1Text": "Wilkommen zur ${LIGA}.Du kannst einen Liga-rang\n ereichen, indem du Sterne bei deiner Bewertung \nkriegst,achivements erfüllstoder indem du Trophähen\n bei Tournamenten gewinnst.", + "welcome2Text": "Sie können auch Tickets zu verdienen aus viele der Aktivitäten.\nKarten können verwendet werden, um neue Charaktere, Karten freizuschalten , und\nMini-Spiele , Turniere und mehr geben .", + "yourPowerRankingText": "Dein Power Rang:" + }, + "copyOfText": "${NAME} Kopieren", + "copyText": "Kopieren", + "copyrightText": "© 2013 Eric Froemling", + "createAPlayerProfileText": "Erstelle ein Spielerprofil?", + "createEditPlayerText": "", + "createText": "Erstellen", + "creditsWindow": { + "additionalAudioArtIdeasText": "Zusätzlicher Ton, frühes Artwork und Ideen von ${NAME}", + "additionalMusicFromText": "Zusätzliche Musik von ${NAME}", + "allMyFamilyText": "All meine Freunde und meine Familie, die mir beim Testspielen halfen", + "codingGraphicsAudioText": "Programmierung, Grafiken und Ton von ${NAME}", + "creditsText": " Coding, Graphics, and Audio by Eric Froemling\n \n Additional Audio, Early Artwork, and Ideas by Raphael Suter\n \n Sound & Music:\n\n${SOUND_AND_MUSIC}\n\n Public-domain music via Musopen.com (a great site for classical music)\n Thanks especially to the US Army, Navy, and Marine Bands.\n\n Big thanks to the following freesound.org contributors for use of their sounds:\n${FREESOUND_NAMES}\n\n Special Thanks:\n\n Todd, Laura, and Robert Froemling\n All of my friends and family who helped play test\n Whoever invented coffee\n\n Legal:\n\n${LEGAL_STUFF}\n\n www.froemling.net", + "languageTranslationsText": "Sprachübersetzung:", + "legalText": "Rechtlich:", + "publicDomainMusicViaText": "Public-domain Musik durch ${NAME}", + "softwareBasedOnText": "Diese Software basiert teilweise auf der Arbeit von ${NAME}", + "songCreditText": "${TITLE} aufgeführt von ${PERFORMER}\nkomponiert von ${COMPOSER}, arrangiert von ${ARRANGER}, Veröffentlicht von ${PUBLISHER},\nmit großzügiger Erlaubnis von ${SOURCE}", + "soundAndMusicText": "Ton & Musik:", + "soundsText": "Sounds (${SOURCE}):", + "specialThanksText": "Besonderen Dank:", + "thanksEspeciallyToText": "Speziellen Dank an ${NAME}", + "titleText": "${APP_NAME} Credits", + "whoeverInventedCoffeeText": "Wer auch immer Kaffee erfunden hat" + }, + "currentStandingText": "Dein derzeitiger Rang ist #${RANK}", + "customizeText": "Anpassen...", + "deathsTallyText": "${COUNT} Tode", + "deathsText": "Tode", + "debugText": "Fehler beseitigen", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Für den Test wird empfohlen, Einstellungen->Grafik->Texturen auf \"Hoch\" zu stellen", + "runCPUBenchmarkText": "CPU Benchmark starten", + "runGPUBenchmarkText": "GPU Benchmark starten", + "runMediaReloadBenchmarkText": "Media-Reload Benchmark starten", + "runStressTestText": "Starte Leistungstest", + "stressTestPlayerCountText": "Spielerzahl", + "stressTestPlaylistDescriptionText": "Stresstest Playlist", + "stressTestPlaylistNameText": "Name der Playlist", + "stressTestPlaylistTypeText": "Art der Playlist", + "stressTestRoundDurationText": "Rundendauer", + "stressTestTitleText": "Stresstest", + "titleText": "Benchmarks & Stress Tests", + "totalReloadTimeText": "Gesamt Nachladezeit : ${TIME} (siehe Protokoll für Details )", + "unlockCoopText": "Schalte Koop-Levels frei" + }, + "defaultFreeForAllGameListNameText": "Standard Jeder gegen Jeden Spiele", + "defaultGameListNameText": "Standard ${PLAYMODE} Playlist", + "defaultNewFreeForAllGameListNameText": "Meine Jeder gegen Jeden Spiele", + "defaultNewGameListNameText": "Meine ${PLAYMODE} Playlist", + "defaultNewTeamGameListNameText": "Meine Teamspiele", + "defaultTeamGameListNameText": "Standard Teamspiele", + "deleteText": "Löschen", + "demoText": "Demo", + "denyText": "Verweigern", + "desktopResText": "Desktop Auflösung", + "difficultyEasyText": "Leicht", + "difficultyHardOnlyText": "Nur auf Schwer", + "difficultyHardText": "Schwer", + "difficultyHardUnlockOnlyText": "Dieser Level kann nur auf Schwer freigeschaltet werden.\nDenkst du, du könntest es schaffen!?!?!", + "directBrowserToURLText": "Bitte navigieren Sie einen Web-Browser auf die folgende URL:", + "disableRemoteAppConnectionsText": "Remote-App Verbindungen verbieten", + "disableXInputDescriptionText": "Erlaubt mehr als 4 Controller aber kann schlechter funktionieren.", + "disableXInputText": "XInput deaktivieren", + "doneText": "Fertig", + "drawText": "Unentschieden", + "duplicateText": "dublizieren", + "editGameListWindow": { + "addGameText": "Spiel\nhinzufügen", + "cantOverwriteDefaultText": "Standard Liste kann nicht überschrieben werden!", + "cantSaveAlreadyExistsText": "Eine Liste mit diesem Namen existiert bereits!", + "cantSaveEmptyListText": "Leere Liste kann nicht gespeichert werden!", + "editGameText": "Spiel\nbearbeiten", + "gameListText": "Spielliste", + "listNameText": "Listenname", + "nameText": "Name", + "removeGameText": "Spiel\nentfernen", + "saveText": "Liste Speichern", + "titleText": "Playlist Editor" + }, + "editProfileWindow": { + "accountProfileInfoText": "Diese spezielles Spielerprofil mit Namen und Symbol\nbezieht sich auf dein Konto mit dem du momentan eingeloggt bist.\n\n${ICONS}\n\nErstelle ein Benutzerprofil wenn du andere\nNamen und Symbole benutzen willst.", + "accountProfileText": "Profilkonto", + "availableText": "Der Name \"${NAME}\" ist verfügbar.", + "changesNotAffectText": "Hinweis: Änderungen wirken sich nicht auf Charaktere aus, die schon im Spiel sind", + "characterText": "Charakter", + "checkingAvailabilityText": "Prüfe Verfügbarkeit von \"${NAME}\"...", + "colorText": "Farbe", + "getMoreCharactersText": "Mehr Charaktere...", + "getMoreIconsText": "Bekomme mehr Symbole...", + "globalProfileInfoText": "Globale Spielerprofile haben garantiert einzigartige\nNamen weweltweit. Sie beinhalten auch eigene Benutzersymbole.", + "globalProfileText": "Globales Profil", + "highlightText": "Hervorhebung", + "iconText": "Symbol", + "localProfileInfoText": "Lokale Spielerprofile haben keine Symbole und die Namen haben keine Garantie\neinzigartig zu sein. Aktualisiere zu einem globalen Profil um einen\neinzigartigen Namen zu bekommen und ein Benutzersymbol hinzuzufügen.", + "localProfileText": "Lokales Profil", + "nameDescriptionText": "Spielername", + "nameText": "Name", + "randomText": "zufällig", + "titleEditText": "Profil bearbeiten", + "titleNewText": "Neues Profil", + "unavailableText": "\"${NAME}\" ist nicht verfügbar; versuche einen anderen Namen.", + "upgradeProfileInfoText": "Reserviert ihren Spielernamen weltweit und\nerlaubt ihnen das Hinzufügen eines Symbols.", + "upgradeToGlobalProfileText": "Aktualisiere zu einem globalen Profil" + }, + "editProfilesAnyTimeText": "(du kannst das Profil unter \"Einstellungen\" jederzeit ändern)", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Du kannst den Standard-Soundtrack nicht löschen.", + "cantEditDefaultText": "Standard Soundtrack kann nicht geändert werden. Dupliziere ihn oder erstelle einen neuen.", + "cantEditWhileConnectedOrInReplayText": "Soundtracks können während einer Party oder einem Replay bearbeitet werden.", + "cantOverwriteDefaultText": "Der Standard-Soundtrack kann nicht überschrieben werden", + "cantSaveAlreadyExistsText": "Ein Soundtrack mit diesem Namen existiert bereits!", + "copyText": "Kopieren", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Standard Musiktitel", + "deleteConfirmText": "Lösche Musiktitel:\n\n'${NAME}'?", + "deleteText": "Musiktitel\nlöschen", + "duplicateText": "Musiktitel\nduplizieren", + "editSoundtrackText": "Musiktitel Editor", + "editText": "Musiktitel\nändern", + "fetchingITunesText": "übertrage Musik-App Playlisten...", + "musicVolumeZeroWarning": "Achtung: Musiklautstärke ist auf 0 gesetzt", + "nameText": "Name", + "newSoundtrackNameText": "Mein Musiktitel ${COUNT}", + "newSoundtrackText": "Neuer Musiktitel", + "newText": "Neuer\nMusiktitel", + "selectAPlaylistText": "Wähle eine Playlist aus", + "selectASourceText": "Musikquelle", + "soundtrackText": "SoundTrack", + "testText": "Test", + "titleText": "Musiktitel", + "useDefaultGameMusicText": "Voreingestellte Spielemusik", + "useITunesPlaylistText": "Musik-App Playlist", + "useMusicFileText": "Musikdatei (mp3, usw.)", + "useMusicFolderText": "Ordner mit Musikdateien" + }, + "editText": "bearbeiten", + "endText": "Ende", + "enjoyText": "Viel Spaß!", + "epicDescriptionFilterText": "${DESCRIPTION} In epischer Zeitlupe.", + "epicNameFilterText": "Episch ${NAME}", + "errorAccessDeniedText": "Zugriff verweigert", + "errorOutOfDiskSpaceText": "Nicht genug Speicherplatz", + "errorText": "Fehler", + "errorUnknownText": "unbekannter Fehler", + "exitGameText": "${APP_NAME} verlassen?", + "exportSuccessText": "'${NAME}' exportiert.", + "externalStorageText": "externer Datenspeicher", + "failText": "Fehlgeschlagen", + "fatalErrorText": "Oh oh; Etwas fehlt oder ist beschädigt.\nVersuche das Spiel neu zu installieren\noder kontaktiere ${EMAIL} für Hilfe.", + "fileSelectorWindow": { + "titleFileFolderText": "Wähle Datei oder Ordner aus", + "titleFileText": "Wähle Datei", + "titleFolderText": "Wähle Ordner", + "useThisFolderButtonText": "Nutze diesen Ordner" + }, + "filterText": "Filter", + "finalScoreText": "Gesamtergebnis", + "finalScoresText": "Gesamtergebnisse", + "finalTimeText": "Gesamtzeit", + "finishingInstallText": "Installation wird beendet; einen moment...", + "fireTVRemoteWarningText": "* Für ein besseres Spielerlebnis benutze\nbitte Controller oder lade die\n'${REMOTE_APP_NAME}' App auf dein\nHandy oder Tablet.", + "firstToFinalText": "Erster-aus-${COUNT} Finale", + "firstToSeriesText": "Erster-aus-${COUNT} Serie", + "fiveKillText": "MONSTER KILL!!!", + "flawlessWaveText": "Tadellose Welle!", + "fourKillText": "QUAD KILL!!!", + "freeForAllText": "Jeder gegen Jeden", + "friendScoresUnavailableText": "Ergebnisse des Freundes nicht verfügbar.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Spiel ${COUNT} Führer", + "gameListWindow": { + "cantDeleteDefaultText": "Standard-Playlist kann nicht gelöscht\nwerden!", + "cantEditDefaultText": "Standard Playlist kann nicht geändert\nwerden! Dupliziere sie, oder erstelle\neine Neue.", + "cantShareDefaultText": "Die Standartplaylist kann nicht geteilt werden.", + "deleteConfirmText": "Lösche \"${LIST}\"?", + "deleteText": "Playlist löschen", + "duplicateText": "Playlist\nduplizieren", + "editText": "Playlist\nbearbeiten", + "gameListText": "Spieleliste", + "newText": "Neue\nPlaylist", + "showTutorialText": "Tutorial anzeigen", + "shuffleGameOrderText": "Zufällige Spielreihenfolge", + "titleText": "${TYPE}-Playlists bearbeiten" + }, + "gameSettingsWindow": { + "addGameText": "Spiel hinzufügen" + }, + "gamepadDetectedText": "1 Gamepad gefunden.", + "gamepadsDetectedText": "${COUNT} Gamepads gefunden.", + "gamesToText": "${WINCOUNT} Siege zu ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Vergiss nicht: Jedes Gerät in einer Party kann mehr als\neinen Spieler haben, wenn du genug Controller hast.", + "aboutDescriptionText": "Benutze diese Tabs, um eine Party zu erstellen.\n\nIn Partys kannst du Spiele und Turniere\nmit deinen Freunden über verschiedene Geräte spielen.\n\nBenutze den ${PARTY} Button oben rechts um\nmit deiner Party zu interagieren oder zu chatten.\n(Im Menü ${BUTTON} auf einem Controller drücken)", + "aboutText": "Über", + "addressFetchErrorText": "", + "appInviteInfoText": "Lade Freunde ein, BombSquad auszuprobieren und sie \nbekommen kostenlos ${COUNT} Tickets. Du bekommst\n${YOU_COUNT} für jeden Freund der dies tut.", + "appInviteMessageText": "${NAME} sendet dir ${COUNT} Tickets in\n${APP_NAME}", + "appInviteSendACodeText": "Ihnen ein Code senden", + "appInviteTitleText": "Einladung zu ${APP_NAME}", + "bluetoothAndroidSupportText": "(Funktioniert auf jedem Android-Gerät das Bluetooth unterstützt)", + "bluetoothDescriptionText": "Erstelle oder tritt einer Party über Bluetooth bei:", + "bluetoothHostText": "Über Bluetooth erstellen", + "bluetoothJoinText": "Über Bluetooth beitreten", + "bluetoothText": "Bluetooth", + "checkingText": "überprüfe...", + "dedicatedServerInfoText": "Das beste Ergebnis wird mit einem dedizierten Server erreicht. Auf bombsquadgame.com/server steht wie es geht.", + "disconnectClientsText": "Dadurch werden die ${COUNT} Spieler in Ihrer\nParty getrennt. Sind Sie sicher ?", + "earnTicketsForRecommendingAmountText": "Freunde bekommen ${COUNT} Tickets, wenn sie das Spiel ausprobieren.\n(Du bekommst ${YOU_COUNT} für jeden, der es tut.)", + "earnTicketsForRecommendingText": "Empfehle das Spiel an Freunde \nund ihr bekommt beide Tickets.", + "emailItText": "E-Mail an ihn", + "favoritesSaveText": "Als Favorit speichern", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} Tickets von ${NAME}", + "friendPromoCodeAwardText": "Du bekommst ${COUNT} Tickets bei jeder Verwendung.", + "friendPromoCodeExpireText": "Der Code wird in ${EXPIRE_HOURS} Stunden ungültig und funktioniert nur für neue Spieler.", + "friendPromoCodeInfoText": "Kann für ${COUNT} Tickets eingelöst werden.\n\nGehe zu \"Einstellungen -> Erweitert -> Gutscheincode eingeben\" im Spiel um den Code einzulösen.\nBesuche bombsquadgame.com um das Spiel für alle unterstützten Platformen herunterzuladen.\nDieser Code wird in ${EXPIRE_HOURS} Stunden ungültig und kann nur von neuen Spielern eingelöst werden.", + "friendPromoCodeInstructionsText": "Um ihn einzulösen, öffne ${APP_NAME} und gehe zu \"Einstellungen->Erweitert->Gutscheincode eingeben\".\nAuf bombsquadgame.com findest du Links zum Download für alle unterstützten Plattformen.", + "friendPromoCodeRedeemLongText": "Er kann für ${COUNT} kostenlose Tickets von bis zu ${MAX_USES} Personen eingelöst werden.", + "friendPromoCodeRedeemShortText": "Es kann im Spiel für ${COUNT} eingelöst werden.", + "friendPromoCodeWhereToEnterText": "(in \"Einstellungen->Erweitert->Gutscheincode eingeben\")", + "getFriendInviteCodeText": "Bekomme einen Einladungscode für Freunde", + "googlePlayDescriptionText": "Lade Google Play Freunde zu deiner Party ein:", + "googlePlayInviteText": "Einladen", + "googlePlayReInviteText": "Es sind ${COUNT} Google Play Spieler in deiner Party,\ndie getrennt werden, wenn du neue Spieler einlädst.\nFüge sie deiner neuen Einladung hinzu, um sie wiederzubekommen.", + "googlePlaySeeInvitesText": "Einladungen anzeigen", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play Version)", + "hostPublicPartyDescriptionText": "Public Party erstellen:", + "inDevelopmentWarningText": "Achtung:\n\nDer Online-Modus ist ein neues Feature, das noch in\nder Entwicklung ist. Zurzeit wird es dringend empfohlen,\ndass alle Spieler im selben Netzwerk sind.", + "internetText": "Internet", + "inviteAFriendText": "Deine Freunde haben dieses Spiel noch nicht?\nLade sie ein und sie erhalten ${COUNT} gratis Tickets.", + "inviteFriendsText": "Lade Freunde ein", + "joinPublicPartyDescriptionText": "Public Party beitreten:", + "localNetworkDescriptionText": "Trete einer Party im Lokalen Netzwerk bei:", + "localNetworkText": "Lokales Netzwerk", + "makePartyPrivateText": "Stelle die Party privat", + "makePartyPublicText": "Stelle die Party öffentlich", + "manualAddressText": "Adresse", + "manualConnectText": "Verbinden", + "manualDescriptionText": "Einer Party per Adresse beitreten:", + "manualJoinSectionText": "Per Adresse beitreten", + "manualJoinableFromInternetText": "Bist du über das Internet zu erreichen?:", + "manualJoinableNoWithAsteriskText": "NEIN*", + "manualJoinableYesText": "JA", + "manualRouterForwardingText": "*um das zu beheben, versuche in deinem Router den UDP Port ${PORT} für deine Lokale Adresse freizugeben", + "manualText": "Manuell", + "manualYourAddressFromInternetText": "Deine Adresse über das Internet:", + "manualYourLocalAddressText": "Deine Lokale Adresse", + "nearbyText": "In der Nähe", + "noConnectionText": "", + "otherVersionsText": "(andere Versionen)", + "partyInviteAcceptText": "Akzeptieren", + "partyInviteDeclineText": "Ablehnen", + "partyInviteGooglePlayExtraText": "(Schaue beim 'Google Play' Tab im 'Versammeln' Fenster)", + "partyInviteIgnoreText": "Ignorieren", + "partyInviteText": "${NAME} hat dich eingeladen\nseiner Party beizutreten!", + "partyNameText": "Party Name", + "partySizeText": "Gruppengröße", + "partyStatusCheckingText": "Status wird überprüft...", + "partyStatusJoinableText": "Man kann deiner Party jetzt aus dem Internet beitreten", + "partyStatusNoConnectionText": "keine Verbindung zum Server möglich", + "partyStatusNotJoinableText": "Man kann deiner Party nicht mehr aus dem Internet beitreten", + "partyStatusNotPublicText": "Deine Party ist nicht public", + "pingText": "Ping", + "portText": "Port", + "privateText": "Privat", + "publicText": "Öffentlich", + "requestingAPromoCodeText": "Gutscheincode anfordern...", + "sendDirectInvitesText": "Einladungen direkt versenden", + "sendThisToAFriendText": "Sende diesen Code einem Freund:", + "shareThisCodeWithFriendsText": "Teile diesen Code mit Freunden:", + "showMyAddressText": "Meine Adresse anzeigen", + "titleText": "Versammeln", + "wifiDirectDescriptionBottomText": "Wenn alle Geräte ein 'Wi-Fi Direktfeld' haben, sollten sie in der Lage sein, sich\ngegenseitig zu finden und zu verbinden. Sind alle Geräte miteinander verbunden, kannst\ndu in dem Tab 'Lokales Netzwerk' genauso wie in einem normalen Wi-Fi Netzwerk Partys erstellen.\n\nUm Probleme zu vermeiden, sollte der Wi-Fi Direct host auch der ${APP_NAME} Party host sein.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct kann genutzt werden, um Android-Geräte direkt miteinander\nzu Verbinden, ohne ein Wi-Fi Netzwerk zu benötigen. Das funktioniert am\nbesten auf Android 4.2 oder neuer.\n\nUm es zu benutzen, öffne die Wi-Fi Einstellungen und aktiviere 'Wi-Fi Direct' im Menü.", + "wifiDirectOpenWiFiSettingsText": "Wi-Fi Einstellungen öffnen", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(Funktioniert zwischen allen Plattformen)", + "worksWithGooglePlayDevicesText": "(Funktioniert auf Geräten mit der Google Play (Android) Version von dem Spiel)", + "youHaveBeenSentAPromoCodeText": "Dir wurde ein ${APP_NAME} Gutscheincode geschickt:" + }, + "getCoinsWindow": { + "coinDoublerText": "Münzenverdoppler", + "coinsText": "${COUNT} Münzen", + "freeCoinsText": "Gratis Münzen", + "restorePurchasesText": "Kauf wiederherstellen", + "titleText": "Münzen holen" + }, + "getTicketsWindow": { + "freeText": "KOSTENLOS!", + "freeTicketsText": "Gratis Tickets", + "inProgressText": "Es wird bereits etwas eingekauft; Bitte\nversuche es später nochmal.", + "purchasesRestoredText": "Einkäufe wiederhergestellt.", + "receivedTicketsText": "${COUNT} Tickets erhalten!", + "restorePurchasesText": "Einkäufe wiederherstellen", + "ticketDoublerText": "Tickets verdoppeln", + "ticketPack1Text": "Kleines Ticketpack", + "ticketPack2Text": "Mittleres Ticketpack", + "ticketPack3Text": "Großes Ticketpack", + "ticketPack4Text": "Riesiges Ticketpack", + "ticketPack5Text": "Kolossales Ticketpack", + "ticketPack6Text": "Ultimatives Ticketpack", + "ticketsFromASponsorText": "Bekomme ${COUNT} Tickets\ndurch Werbung", + "ticketsText": "${COUNT} Tickets", + "titleText": "Hol dir Tickets", + "unavailableLinkAccountText": "Sorry, Einkäufe sind auf dieser Plattform nicht verfügbar.\nUm das zu umgehen verlinke deinen Account mit einem Account auf\neiner anderen Plattform, um dort Einkäufe zu machen.", + "unavailableTemporarilyText": "Dies ist zur Zeit nicht verfügbar; bitte versuch es später nochmal.", + "unavailableText": "Sorry, zur Zeit leider nicht verfügbar.", + "versionTooOldText": "Sorry, diese Version von dem Spiel ist zu alt; Bitte update es zu einer neueren Version.", + "youHaveShortText": "Du hast ${COUNT}", + "youHaveText": "Du hast ${COUNT} Tickets" + }, + "googleMultiplayerDiscontinuedText": "Sorry, Googles Multiplayerservice ist nicht länger verfügbar.\nIch arbeite so schnell wie möglich an einem Ersatz.\nBis dahin, versuche bitte eine andere Verbindungsmethode.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Immer", + "autoText": "Automatisch", + "fullScreenCmdText": "Vollbildmodus (Cmd-F)", + "fullScreenCtrlText": "Vollbildmodus (Strg-F)", + "gammaText": "Gamma", + "highText": "Hoch", + "higherText": "Höher", + "lowText": "Niedrig", + "mediumText": "Mittel", + "neverText": "Niemals", + "resolutionText": "Auflösung", + "showFPSText": "Zeige FPS", + "texturesText": "Texturen", + "titleText": "Grafik", + "tvBorderText": "TV Rand", + "verticalSyncText": "Verticale Synchronisation", + "visualsText": "Visuelles" + }, + "helpWindow": { + "bombInfoText": "- Bombe -\nStärker als Schläge, kann\naber zum eigenen Grab führen.\nWerfe in die Richtung der Gegner,\nbevor die Zündschnur abbrennt,\num die besten Ergebnisse zu erzielen.", + "bombInfoTextScale": 0.6, + "canHelpText": "${APP_NAME} kann helfen.", + "controllersInfoText": "Du kannst ${APP_NAME} mit Freunden übers Netzwerk spielen, oder\nihr könnt alle auf demselben Gerät spielen wenn ihr genug\nController habt. ${APP_NAME} unterstützt eine Menge von ihnen:\nüber die kostenlose '${REMOTE_APP_NAME}' App kannst du sogar Handys als Controller nutzen.\nGehe zu Einstellungen->Controller für mehr Informationen", + "controllersInfoTextFantasia": "Ein Spieler kann die Fernbedienung als Controller verwenden,\naber Gamepads sind empfolen. Du kannst auch mobile Geräte\nals Controller benutzen. Downlode die kostenlose\n'BombSquad Remote' App. Schau unter 'Controller' in den \n'Einstellungen' für mehr Info.", + "controllersInfoTextMac": "Ein oder zwei Spieler können die Tastatur benutzen, allerdings spielt sich\nBombSquad am Besten mit Gamepads. Bombsquad kann USB Gamepads,\nPS3 Controller, XBox 360 Controller, Wiimotes und iOS-/Androidgeräte\nbenutzen, um die Charakter zu steuern. Hoffentlich hast du einige von Diesen.\nIn den Einstellungen unter 'Controller' findest du weitere Informationen.", + "controllersInfoTextOuya": "Du kannst OUYA Controller, PS3 Controller, XBox 360 Controller und viele weitere\nUSB und Bluetooth Gamepads mit BombSquad benutzen. Desweiteren kannst du\nauch iOS- und Androidgeräte mit der kostenlosen 'BombSquad Remote' App nutzen.\nIn den Einstellungen unter 'Controller' findest du weitere Informationen.", + "controllersInfoTextScaleFantasia": 0.51, + "controllersInfoTextScaleMac": 0.58, + "controllersInfoTextScaleOuya": 0.63, + "controllersText": "Controller", + "controllersTextScale": 0.67, + "controlsSubtitleText": "Dein befreundeter ${APP_NAME} Charakter hat ein paar Basisfähigkeiten.", + "controlsSubtitleTextScale": 0.7, + "controlsText": "Steuerung", + "controlsTextScale": 1.4, + "devicesInfoText": "Die VR Version von ${APP_NAME} kann übers Netzerk mit der normalen\nVersion gespielt werden, also packt eure Handys, Tablets\nund Computer aus und los gehts. Es kann auch nützlich sein eine\nnormale Version des Spiels mit der VR Version zu Verbinden,\ndamit Leute von außen zuschauen können.", + "devicesText": "Geräte", + "friendsGoodText": "es ist gut, sie zu haben. ${APP_NAME} macht am meisten Spaß mit mehreren\nSpielern und unterstützt bis zu 8 Spieler gleichzeitig, was zu folgemdem führt:", + "friendsGoodTextScale": 0.62, + "friendsText": "Freunde", + "friendsTextScale": 0.67, + "jumpInfoText": "- Springen -\nSpringe, um kleine Lücken zu überwinden,\num Dinge höher zu werfen, und\num Glücksgefühle zu zeigen.", + "jumpInfoTextScale": 0.54, + "orPunchingSomethingText": "Oder um etwas zu schlagen, eine Klippe herunterzuwerfen und es fertigzumachen, auf dem Weg hinunter mit einer Klebebombe.", + "orPunchingSomethingTextScale": 0.44, + "pickUpInfoText": "- Aufnehmen -\nNimm Fahnen, Gegner oder alles\nandere, dass nicht auf dem Boden\nfestgemacht wurde. Drücke\nnochmal, um zu werfen.", + "pickUpInfoTextScale": 0.6, + "powerupBombDescriptionText": "Lässt dich drei, anstatt nur\neiner Bombe werfen.", + "powerupBombNameText": "Dreifach-Bomben", + "powerupCurseDescriptionText": "Diese möchtest du wohl vermeiden.\n ...oder etwa nicht?", + "powerupCurseNameText": "Fluch", + "powerupHealthDescriptionText": "Stellt dein vollständiges Leben wieder her.\nHättest du nie erraten.", + "powerupHealthNameText": "Medipack", + "powerupIceBombsDescriptionText": "Schwächer als normale Bomben,\nallerdings frierst du deine Gegner ein,\nund sie werden ziemlich zerbrechlich.", + "powerupIceBombsNameText": "Frostbomben", + "powerupImpactBombsDescriptionText": "Etwas schwächer als normale Bomben,\naber sie explodieren bei Berührung", + "powerupImpactBombsNameText": "Auslöserbomben", + "powerupLandMinesDescriptionText": "Diese kommen immer im Dreierpack;\nNützlich um die Basis zu verteidigen\noder schnelle Gegner zu stoppen.", + "powerupLandMinesNameText": "Landminen", + "powerupPunchDescriptionText": "Macht deine Schläge härter,\nschneller, besser, stärker.", + "powerupPunchNameText": "Boxhandschuhe", + "powerupShieldDescriptionText": "Absorbiert etwas Schaden,\ndeshalb musst du nicht wegrennen.", + "powerupShieldNameText": "Energieschild", + "powerupStickyBombsDescriptionText": "Kleben an allem, was sie berühren.\nDas wird lustig.", + "powerupStickyBombsNameText": "Klebebomben", + "powerupsSubtitleText": "Natürlich ist kein Spiel vollständig ohne Powerups:", + "powerupsSubtitleTextScale": 0.8, + "powerupsText": "Powerups", + "powerupsTextScale": 1.4, + "punchInfoText": "- Schlagen -\nSchlagen macht mehr Schaden, je\nschneller sich deine Fäuste bewegen, deshalb\nrenn und dreh dich, so schnell es geht.", + "punchInfoTextScale": 0.54, + "runInfoText": "- Rennen -\nHalte IRGENDEINEN Knopf gedrückt, um zu rennen. Drücker oder Schultertasten sind gut dafür geeignet, wenn\ndu welche hast. Rennen bringt dich schnell voran, macht es aber schwer zu steuern, also pass auf Abgründe auf.", + "runInfoTextScale": 0.6, + "someDaysText": "Manchmal ist einem einfach danach, etwas zu schlagen. Oder etwas in die Luft zu jagen.", + "someDaysTextScale": 0.64, + "titleText": "${APP_NAME} Hilfe", + "toGetTheMostText": "Um alles aus diesem Spiel herauszuholen, brauchst du folgendes:", + "toGetTheMostTextScale": 0.85, + "welcomeText": "Willkommen zu ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} navigiert die Menüs wie ein Boss -", + "importPlaylistCodeInstructionsText": "Benutze den folgenden Code, um diese Playlist irgendwo anders zu importieren:", + "importPlaylistSuccessText": "Die ${TYPE} Playlist '${NAME}' wurde importiert", + "importText": "Importieren", + "importingText": "Wird importiert...", + "inGameClippedNameText": "Im Spiel angezeigter Name wird \n\"${NAME}\"", + "installDiskSpaceErrorText": "ERROR: Installation wurde nicht abgeschlossen.\nEs ist nicht genug Speicherplatz verfügbar.\nVersuche es erneut, nachdem genügend Speicher freigegeben wurde.", + "internal": { + "arrowsToExitListText": "drücke ${LEFT} oder ${RIGHT}, um die Liste zu verlassen", + "buttonText": "Knopf", + "cantKickHostError": "Der Host kann nicht entfernt werden!", + "chatBlockedText": "Die Kommunikation von ${NAME} wurde für ${TIME} Sekunden blockiert.", + "connectedToGameText": "'${NAME}' beigetreten", + "connectedToPartyText": "Ist ${NAME}'s Party beigetreten!", + "connectingToPartyText": "verbindet...", + "connectionFailedHostAlreadyInPartyText": "Verbindung fehlgeschlagen; Host ist in einer anderen Party.", + "connectionFailedPartyFullText": "Verbindung fehlgeschlagen; Die Gruppe ist voll.", + "connectionFailedText": "Verbindung fehlgeschlagen.", + "connectionFailedVersionMismatchText": "Verbindung fehlgeschlagen; Der Host hat eine andere Version des Spiels.\nStellt sicher, dass ihr beide die neuste Version habt und versucht es erneut.", + "connectionRejectedText": "Verbindung abgelehnt.", + "controllerConnectedText": "${CONTROLLER} verbunden.", + "controllerDetectedText": "1 Controller entdeckt.", + "controllerDisconnectedText": "${CONTROLLER} getrennt.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} getrennt. Bitte versuche erneut zu verbinden.", + "controllerForMenusOnlyText": "Dieser Controller kann nicht zum spielen verwendet werden, nur zur Bedienung des Menüs.", + "controllerReconnectedText": "${CONTROLLER} wieder verbunden.", + "controllersConnectedText": "${COUNT} Controllers sind verbunden.", + "controllersDetectedText": "${COUNT} Controllers entdeckt.", + "controllersDisconnectedText": "${COUNT} Controllers sind nicht mehr verbunden.", + "corruptFileText": "Beschädigte Datei(en) Gefunden. Bitte Neu installieren, oder kontaktiere ${EMAIL}", + "errorPlayingMusicText": "Fehler beim abspielen der Musik: ${MUSIC}", + "errorResettingAchievementsText": "Es ist nicht möglich, die Online-Erfolge zurückzusetzen; Bitte versuche es später nochmal.", + "hasMenuControlText": "${NAME} hat die Menükontrolle", + "incompatibleNewerVersionHostText": "Der Host besitzt eine neuere Version des Spiels. \nAktualisiere es und versuche es erneut.", + "incompatibleVersionHostText": "Der Host hat eine andere Version des Spiels.\nStellt sicher, dass ihr beide die neuste Version habt und versucht es erneut.", + "incompatibleVersionPlayerText": "${NAME} hat eine andere Version des Spiels.\nStellt sicher, dass ihr beide die neuste Version habt und versuchts erneut.", + "invalidAddressErrorText": "Fehler: Ungültige Adresse.", + "invalidNameErrorText": "Error: ungültiger Name", + "invalidPortErrorText": "Fehler: Falscher Port", + "invitationSentText": "Einladung wurde gesendet.", + "invitationsSentText": "${COUNT} Einladungen wurden gesendet.", + "joinedPartyInstructionsText": "Jemand ist deiner Party beigetreten.\nDrücke 'Spielen' um das Spiel zu starten.", + "keyboardText": "Tastatur", + "kickIdlePlayersKickedText": "${NAME} wird rausgeschmissen, weil er untätig ist!", + "kickIdlePlayersWarning1Text": "Mach was ${NAME}, sonst wirst du in ${COUNT} Sekunden rausgeschmissen.", + "kickIdlePlayersWarning2Text": "(Du kannst das in den Einstellungen ausschalten -> Erweitert)", + "leftGameText": "'${NAME}' hat verlassen", + "leftPartyText": "hat ${NAME}'s Party verlassen.", + "noMusicFilesInFolderText": "Der Ordner enthält keine Musikdateien.", + "playerJoinedPartyText": "${NAME} ist der Party beigetreten!", + "playerLeftPartyText": "${NAME} hat die Party verlassen.", + "rejectingInviteAlreadyInPartyText": "Lehne Einladung ab (bereits in einer Party).", + "serverRestartingText": "Server wird neugestartet. Bitte trete gleich erneut bei...", + "serverShuttingDownText": "Server wird abgeschaltet...", + "signInErrorText": "Error beim Einloggen.", + "signInNoConnectionText": "Login nicht möglich. (Keine Internetverbindung?)", + "teamNameText": "Team ${NAME}", + "telnetAccessDeniedText": "FEHLER: Benutzer hat keinen telnet-Zugang gewährt.", + "timeOutText": "(unterbreche in ${TIME} Sekunden)", + "touchScreenJoinWarningText": "Du bist mit dem Touchscreen beigetreten.\nWenn das ein Versehen war, drücke 'Menü->Spiel Beenden' damit.", + "touchScreenText": "TouchScreen", + "trialText": "Testversion", + "unableToResolveHostText": "Fehler: Verbindung zum Host konnte nicht aufgelöst werden.", + "unavailableNoConnectionText": "Zurzeit nicht verfügbar. (Keine Internetverbindung?)", + "vrOrientationResetCardboardText": "Benutze dies,um die VR orientierung zurückzusetzen.\nUm das Spiel spielen zu können, brauchst du einen externen controller.", + "vrOrientationResetText": "Kalibriere VR Orientierung.", + "willTimeOutText": "(wird unterbrochen, wenn keine Eingabe)" + }, + "jumpBoldText": "SPRING", + "jumpText": "Spring", + "keepText": "Halten", + "keepTheseSettingsText": "Diese Einstellungen beibehalten?", + "keyboardChangeInstructionsText": "Drücke die Leertaste doppelt, um die Tastaturen zu wechseln.", + "keyboardNoOthersAvailableText": "Keine anderen Tastaturen verfügbar.", + "keyboardSwitchText": "Tastatur umschalten auf \"${NAME}\".", + "kickOccurredText": "${NAME} wurde entfernt.", + "kickQuestionText": "${NAME} entfernen?", + "kickText": "Entfernen", + "kickVoteCantKickAdminsText": "Admins können nicht rausgeworfen werden.", + "kickVoteCantKickSelfText": "Du kannst dich nicht selber rauswerfen.", + "kickVoteFailedNotEnoughVotersText": "Nicht genügend Spieler für eine Abstimmung.", + "kickVoteFailedText": "Abstimmung zum Hinauswerfen fehlgeschlagen.", + "kickVoteStartedText": "Abstimmung zum Hinauswerfen von ${NAME}.", + "kickVoteText": "Stimme zum Hinauswerfen ab", + "kickVotingDisabledText": "Votekick ist deaktiviert.", + "kickWithChatText": "Tippe ${YES} für Ja oder ${NO} für Nein in den Chat.", + "killsTallyText": "${COUNT} Kills", + "killsText": "Kills", + "kioskWindow": { + "easyText": "Anfänger", + "epicModeText": "Episch", + "fullMenuText": "Vollständiges Menü", + "hardText": "Schwer", + "mediumText": "Mittel", + "singlePlayerExamplesText": "Einzelspieler / Koop Beispiele", + "versusExamplesText": "Gegeneinander Beispiele" + }, + "languageSetText": "Sprache ist nun \"${LANGUAGE}\".", + "lapNumberText": "Runde ${CURRENT}/${TOTAL}", + "lastGamesText": "(letzte ${COUNT} Spiele)", + "leaderboardsText": "Rangliste", + "league": { + "allTimeText": "Gesamt", + "currentSeasonText": "Aktuelle Saison (${NUMBER})", + "leagueFullText": "${NAME} Liga", + "leagueRankText": "Liga Rang", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga${SUFFIX}", + "seasonEndedDaysAgoText": "Die Saison ist vor ${NUMBER} Tagen abgelaufen.", + "seasonEndsDaysText": "Die Saison endet in ${NUMBER} Tagen.", + "seasonEndsHoursText": "Die Saison endet in ${NUMBER} Stunden.", + "seasonEndsMinutesText": "Die Saison endet in ${NUMBER} Minuten.", + "seasonText": "Saison ${NUMBER}", + "tournamentLeagueText": "Du musst die ${NAME} Liga erreichen um dieses Turnier zu spielen.", + "trophyCountsResetText": "Die Trophäenzahl wird nächste Season zurückgesetzt." + }, + "levelBestScoresText": "Bestes Ergebnis bei ${LEVEL}", + "levelBestTimesText": "Beste Zeiten bei ${LEVEL}", + "levelFastestTimesText": "Schnellste Zeit auf ${LEVEL}", + "levelHighestScoresText": "Höchste Punktzahl auf ${LEVEL}", + "levelIsLockedText": "${LEVEL} ist gesperrt.", + "levelMustBeCompletedFirstText": "${LEVEL} muss zuerst abgeschlossen werden.", + "levelText": "Level ${NUMBER}", + "levelUnlockedText": "Level entsperrt!", + "livesBonusText": "Lebensbonus", + "loadingText": "lädt", + "loadingTryAgainText": "Lädt; Bitte gleich nochmal versuchen...", + "macControllerSubsystemBothText": "Beide (nicht empfohlen)", + "macControllerSubsystemClassicText": "Klassisch", + "macControllerSubsystemDescriptionText": "(versuche das zu ändern wenn deine Controller nicht funktionieren)", + "macControllerSubsystemMFiNoteText": "Gemacht-für-iOS/Mac Controller erkannt;\nDu solltest diese aktivieren in Einstellungen -> Controller", + "macControllerSubsystemMFiText": "Gemacht-für-iOS/Mac", + "macControllerSubsystemTitleText": "Controller Unterstützung", + "mainMenu": { + "creditsText": "Credits", + "demoMenuText": "Demo Menü", + "endGameText": "Spiel beenden", + "exitGameText": "Spiel verlassen", + "exitToMenuText": "Zurück zum Menü?", + "howToPlayText": "Anleitung", + "justPlayerText": "(Nur ${NAME})", + "leaveGameText": "Spiel beenden", + "leavePartyConfirmText": "Willst du die Party wirklich\nverlassen?", + "leavePartyText": "Party verlassen", + "leaveText": "Verlassen", + "quitText": "Verlassen", + "resumeText": "Weiter", + "settingsText": "Einstellungen" + }, + "makeItSoText": "Mach es so", + "mapSelectGetMoreMapsText": "Hol dir mehr Karten...", + "mapSelectText": "Auswählen...", + "mapSelectTitleText": "${GAME} Karten", + "mapText": "Karte", + "maxConnectionsText": "Maximale Anzahl verbundener Geräte", + "maxPartySizeText": "Maximale Gruppengröße", + "maxPlayersText": "Maximale Spielerzahl", + "modeArcadeText": "Arcade-Modus", + "modeClassicText": "Klassischer Modus", + "modeDemoText": "Demo Modus", + "mostValuablePlayerText": "Wertvollster Spieler", + "mostViolatedPlayerText": "Meist getöteter Spieler", + "mostViolentPlayerText": "Brutalster Spieler", + "moveText": "Bewegen", + "multiKillText": "${COUNT}-KILL!!!", + "multiPlayerCountText": "${COUNT} Spieler", + "mustInviteFriendsText": "Achtung: Du musst Freunde im\n\"${GATHER}\" Feld einladen oder Controller \nhinzufügen, um im Multiplayer spielen zu können.", + "nameBetrayedText": "${NAME} hat ${VICTIM} verraten.", + "nameDiedText": "${NAME} ist gestorben.", + "nameKilledText": "${NAME} hat ${VICTIM} getötet.", + "nameNotEmptyText": "Name kann nicht leer sein!", + "nameScoresText": "${NAME} punktet!", + "nameSuicideKidFriendlyText": "${NAME} starb versehentlich.", + "nameSuicideText": "${NAME} begeht Selbstmord.", + "nameText": "Name", + "nativeText": "Native Einstellung", + "newPersonalBestText": "Neuer persönlicher Rekord!", + "newTestBuildAvailableText": "Ein neuerer Test-Build ist erhältlich (${VERSION} Build ${BUILD}).\nZu holen auf ${ADDRESS}", + "newText": "Neu", + "newVersionAvailableText": "Eine neuere Version von ${APP_NAME} ist erhältlich! (${VERSION})", + "nextAchievementsText": "Nächste Erfolge:", + "nextLevelText": "Nächstes Level", + "noAchievementsRemainingText": "- keine", + "noContinuesText": "(keine weiterführung)", + "noExternalStorageErrorText": "Kein externer Datenspeicher auf dem Gerät gefunden", + "noGameCircleText": "Fehler: nicht in GameCircle eingeloggt", + "noJoinCoopMidwayText": "Koop-Spiele können nicht während des Spiels betreten werden.", + "noProfilesErrorText": "Du hast kein Spielerprofil, deshalb ist dein Name \"${NAME}\".\nGehe zu Einstellungen->Spielerprofile um ein Profil anzulegen.", + "noScoresYetText": "Noch kein Punktestand.", + "noThanksText": "Nein Danke", + "noTournamentsInTestBuildText": "WARNUNG: Turnierpunkte von dieser Testversion werden ignoriert.", + "noValidMapsErrorText": "Keine gültigen Karten für diesen Spieltyp gefunden.", + "notEnoughPlayersRemainingText": "Nicht genug Spieler übrig; Spiel verlassen oder neues Spiel starten.", + "notEnoughPlayersText": "Du benötigst mindestens ${COUNT} Spieler, um dieses Spiel zu starten.", + "notNowText": "Nicht jetzt", + "notSignedInErrorText": "Du musst dich hierfür einloggen.", + "notSignedInGooglePlayErrorText": "Hierfür musst du dich mit Google Play anmelden", + "notSignedInText": "Nicht angemeldet", + "nothingIsSelectedErrorText": "Nichts ausgewählt!", + "numberText": "#${NUMBER}", + "offText": "Aus", + "okText": "Ok", + "onText": "An", + "onslaughtRespawnText": "${PLAYER} steigt in Welle ${WAVE} wieder ein", + "orText": "${A} oder ${B}", + "otherText": "Sonstiges...", + "outOfText": "(#${RANK} von ${ALL})", + "ownFlagAtYourBaseWarning": "Deine Flagge muss in deiner\nBasis sein um zu punkten!", + "packageModsEnabledErrorText": "Netzwerkmodus ist nicht erlaubt wenn local-package-mods aktiv sind (Einstellungen->Erweitert)", + "partyWindow": { + "chatMessageText": "Chat Nachricht", + "emptyText": "Deine Party ist leer", + "hostText": "(Moderator)", + "sendText": "Senden", + "titleText": "Deine Party" + }, + "pausedByHostText": "(Pausiert vom Host)", + "perfectWaveText": "Perfekte Welle!", + "pickUpBoldText": "PICKUP", + "pickUpText": "Aufsammeln", + "playModes": { + "coopText": "Koop", + "freeForAllText": "Frei für alle", + "multiTeamText": "Mutli-Team", + "singlePlayerCoopText": "Einzelspieler / Co-op", + "teamsText": "Teams" + }, + "playText": "Spielen", + "playWindow": { + "coopText": "Koop", + "freeForAllText": "Jeder gegen Jeden", + "oneToFourPlayersText": "1-4 Spieler", + "teamsText": "Teams", + "titleText": "Spiel starten", + "twoToEightPlayersText": "2-8 Spieler" + }, + "playerCountAbbreviatedText": "${COUNT}S", + "playerDelayedJoinText": "${PLAYER} spielt ab der nächsten Runde mit.", + "playerInfoText": "Spieler Information", + "playerLeftText": "${PLAYER} hat das Spiel verlassen.", + "playerLimitReachedText": "Die maximale Spieleranzahl von ${COUNT} ist erreicht; kein Beitritt möglich.", + "playerLimitReachedUnlockProText": "Upgrade auf \"${PRO}\" im Store um mit mehr als ${COUNT} Spielern zu spielen.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Du kannst dein Account Profil nicht löschen.", + "deleteButtonText": "Profil\nlöschen", + "deleteConfirmText": "'${PROFILE}' löschen?", + "editButtonText": "Profil\nBearbeiten", + "explanationText": "(Erstelle eigene Namen & Skins für Spieler auf diesem Account)", + "newButtonText": "Neues\nProfil", + "titleText": "Spieler Profile" + }, + "playerText": "Spieler", + "playlistNoValidGamesErrorText": "Diese Playlist enthält nicht zulässig freigeschaltete Spiele.", + "playlistNotFoundText": "Wiedergabeliste konnte nicht gefunden werden", + "playlistsText": "Playlists", + "pleaseRateText": "Wenn dir ${APP_NAME} Spaß macht, nimm dir kurz die Zeit\nund bewerte es oder schreib ein Review. Durch das Feedback\nwird zukünftige Arbeit an dem Spiel unterstützt.\n\nVielen Dank!\n-eric", + "pleaseWaitText": "Bitte warte...", + "pluginsDetectedText": "Neue Plugins erkannt. Aktivieren / konfigurieren Sie sie in den Einstellungen.", + "pluginsText": "Plugins", + "practiceText": "Übung", + "pressAnyButtonPlayAgainText": "Drücke einen Knopf um nochmal zu spielen...", + "pressAnyButtonText": "Drücke einen Knopf um fortzufahren...", + "pressAnyButtonToJoinText": "Drücke einen Knopf um beizutreten...", + "pressAnyKeyButtonPlayAgainText": "Drücke einen Knopf/Taste um nochmal zu spielen...", + "pressAnyKeyButtonText": "Drücke einen Knopf/Taste um fortzufahren...", + "pressAnyKeyText": "Drücke eine Taste...", + "pressJumpToFlyText": "** Drücke wiederholt Springen zum Fliegen **", + "pressPunchToJoinText": "Drücke SCHLAG um beizutreten...", + "pressToOverrideCharacterText": "Drücke ${BUTTONS}, um deinen Charakter zu überschreiben", + "pressToSelectProfileText": "Drücke ${BUTTONS}, um deinen Spieler auszuwählen", + "pressToSelectTeamText": "Drücke ${BUTTONS}, um ein Team auszuwählen", + "profileInfoText": "Erstelle Profile für dich und deine Freunde,\num Namen, Charakter und Farben anzupassen.", + "promoCodeWindow": { + "codeText": "Code", + "codeTextDescription": "Gutscheincode", + "enterText": "Bestätigen" + }, + "promoSubmitErrorText": "Fehler beim Senden des Codes; bitte überprüfe deine Internetverbindung", + "ps3ControllersWindow": { + "macInstructionsText": "Schalte die PS3 an ihrer Rückseite aus, versichere dich, dass\nBluetooth an deinem Mac aktiviert ist, verbinde dann deinen Controller\nmit dem Mac mit einem USB Kabel, um beide zu verbinden. Danach kannst du\nden Home-Button des Controllers benutzen, um ihn entweder per Kabel (USB),\noder drahtlos (Bluetooth) mit deinem Mac zu verbinden.\n\nAn manchen Macs wird möglicherweise die Nachricht erscheinen, dass du einen Code\neigeben musst. Falls dies passiert, findest du in der folgenden Anleitung oder bei Google Hilfe.\n\n\n\n\nPS3 Controller, die drahtlos verbunden sind, werden in den Systemeinstellungen unter\nBluetooh aufgelistet. Es kann sein, dass du sie von dieser Liste entfernen musst,\nfalls du sie wieder an deiner PS3 benutzen möchtest.\n\nUm die Batterien zu schonen, versichere dich zudem, dass du die Bluetooth-Verbindung\nkappst, wenn du sie nicht benutzt.\n\nBluetooth kann in der Regel bis zu 7 Geräte verwalten,\nwobei die Leistung variieren kann.", + "macInstructionsTextScale": 0.74, + "ouyaInstructionsText": "Um einen PS3 Controller mit deiner OUYA zu benutzen, verbinde ihn einfach mit einem USB-Kabel,\num ihn zu aktivieren. Dies kann bewirken, dass deine anderen Controller die Verbindung verlieren,\ndeshalb solltest du danach deine OUYA neu starten und das USB-Kabel entfernen.\n\nAb dann solltest du die Möglichkeit haben, den HOME-Button des Controllers zu verwenden,\num ihn kabellos zu verbinden. Sobald du fertig bist mit spielen, halte den Home-Button\nfür 10 Sekunden gedrückt, um den Controller auszuschalten; ansonsten bleibt dieser möglicherweise an\nund verschwendet Batterien.", + "ouyaInstructionsTextScale": 0.74, + "pairingTutorialText": "Verbindungsanleitung Video", + "titleText": "Benutzt PS3 Controller mit ${APP_NAME}:" + }, + "publicBetaText": "PUBLIC BETA", + "punchBoldText": "SCHLAGEN", + "punchText": "Schlagen", + "purchaseForText": "Für ${PRICE} kaufen", + "purchaseGameText": "Spiel kaufen", + "purchasingText": "Bezahlt...", + "quitGameText": "${APP_NAME} beenden?", + "quittingIn5SecondsText": "Verlasse das Spiel in 5 Sekunden...", + "randomPlayerNamesText": "DEFAULT_NAMES, Horst, Eugen, Lutz, Kai", + "randomText": "Zufällig", + "rankText": "Rang", + "ratingText": "Bewertung", + "reachWave2Text": "Erreiche Welle 2 für die Rangliste.", + "readyText": "bereit", + "recentText": "kürzlich", + "remainingInTrialText": "verbleibend in der Testphase", + "remoteAppInfoShortText": "${APP_NAME} macht am meisten Spaß, wenn Ihr mit Freunden & Familie spielt.\nSchließen einen oder mehr Hardware Controller oder installiere die ${REMOTE_APP_NAME}\napp auf deinem Handy oder Tablet,\num diese als Controller zu nutzen.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Knopfposition", + "button_size": "Knopfgröße", + "cant_resolve_host": "Kann Host nicht finden.", + "capturing": "Drück eine Taste...", + "connected": "Verbunden.", + "description": "Nutze dein Handy / Tablet als controller für BombSquad.\nBis zu 8 Geräte können gleichzeitig für epischen lokalen Multiplayer-Wahnsinn an einem einzigen TV oder Tablet verbunden werden.", + "disconnected": "Verbindung vom Server getrennt.", + "dpad_fixed": "statisch", + "dpad_floating": "bewegend", + "dpad_position": "D-Pad Position", + "dpad_size": "D-Pad Größe", + "dpad_type": "D-Pad Typ", + "enter_an_address": "Gib eine Adresse ein", + "game_full": "Das Spiel ist voll oder akzeptiert keine Verbindungen", + "game_shut_down": "Das Spiel wurde beendet.", + "hardware_buttons": "Hardwaretasten", + "join_by_address": "Mit Adresse verbinden...", + "lag": "Lag: ${SECONDS} Sekunden", + "reset": "Zurücksetzen", + "run1": "Rennen 1", + "run2": "Rennen 2", + "searching": "Suche nach BombSquad Spielen...", + "searching_caption": "Tippe auf den Namen des Spieles, um es bei zu treten.\nAchte darauf, auf dem selben Netzwerk wie der Spiel zu sein.", + "start": "Starten", + "version_mismatch": "Versionskonflikt.\nVersuch es mit den aktuellen Versionen von\nBombSquad und BombSquad Remote noch einmal." + }, + "removeInGameAdsText": "Schalte \"${PRO}\" im Store frei, um Werbung zu entfernen.", + "renameText": "Name ändern", + "replayEndText": "Wiederholung beenden", + "replayNameDefaultText": "Letztes Spiel ansehen", + "replayReadErrorText": "Die Wiederholung kann nicht abgespielt werden.", + "replayRenameWarningText": "Benenne \"${REPLAY}\" nach einem Spiel um, um es zu behalten; Sonst wird es überschrieben.", + "replayVersionErrorText": "Es tut mir leid, diese Spielwiederholung wurde in einer anderen\nVersion erstellt und kann deswegen nicht angeschaut werden.", + "replayWatchText": "Wiederholung anschauen", + "replayWriteErrorText": "Wiederholungsdatei kann nicht erstellt werden.", + "replaysText": "Wiederholungen", + "reportPlayerExplanationText": "Benutze diese Email, um Cheating, unangemessene Sprache oder anderes schlechtes Verhalten zu melden.\nBeschreibe das Verhalten bitte:", + "reportThisPlayerCheatingText": "Cheating", + "reportThisPlayerLanguageText": "unangemessen Sprache", + "reportThisPlayerReasonText": "Was möchten sie melden?", + "reportThisPlayerText": "Spieler melden", + "requestingText": "Fordert an...", + "restartText": "Neustart", + "retryText": "Nochmal", + "revertText": "Zurücksetzen", + "runText": "Rennen", + "saveText": "Speichern", + "scanScriptsErrorText": "Fehler beim scannen der Skripts; sehe Protokoll für Details.", + "scoreChallengesText": "Punkte Herausforderungen", + "scoreListUnavailableText": "Bestenliste ist nicht verfügbar", + "scoreText": "Punkte", + "scoreUnits": { + "millisecondsText": "Millisekunden", + "pointsText": "Punkte", + "secondsText": "Sekunden" + }, + "scoreWasText": "(war ${COUNT})", + "selectText": "Auswählen", + "seriesWinLine1PlayerText": "GEWINNT DIE", + "seriesWinLine1Scale": 0.65, + "seriesWinLine1TeamText": "GEWINNT DIE", + "seriesWinLine1Text": "GEWINNT DIE", + "seriesWinLine2Scale": 1.0, + "seriesWinLine2Text": "SERIE!", + "settingsWindow": { + "accountText": "Konto", + "advancedText": "Erweitert", + "audioText": "Audio", + "controllersText": "Controller", + "enterPromoCodeText": "Gib einen Gutschein ein", + "graphicsText": "Grafik", + "kickIdlePlayersText": "Untätige Mitspieler rauswerfen", + "playerProfilesMovedText": "Beachte: Spieler Profil wurde zum Account Fenster verschoben.", + "playerProfilesText": "Spielerprofile", + "showPlayerNamesText": "Zeige die Spielernamen an", + "titleText": "Einstellungen" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(Eine einfache, Controller-freundliche Bildschirmtastatur für Texte)", + "alwaysUseInternalKeyboardText": "Immer interne Tastatur benutzen", + "benchmarksText": "Benchmarks & Stress-Tests", + "disableCameraGyroscopeMotionText": "Deaktivieren Sie die Kamera-Gyroskop-Bewegung", + "disableCameraShakeText": "Deaktivieren Sie Camera Shake", + "disableThisNotice": "(Du kannst diese Notiz in den erweiterten Einstellungen ändern.)", + "enablePackageModsDescriptionText": "(Aktiviert installierte Mods aber deaktiviert den Mehrspielermodus)", + "enablePackageModsText": "Local Package Mods aktivieren", + "enterPromoCodeText": "Gutscheincode eingeben", + "forTestingText": "Wichtig: Diese Werte sind nur für Tests und werden später wieder zurückgesetzt.", + "helpTranslateText": "${APP_NAME}'s Übersetzungen sind der Community zu\nverdanken. Wenn du eine Übersetzung hinzufügen oder \nverbessern willst, folge dem Link unten. Danke im Voraus!", + "kickIdlePlayersText": "Inaktive Spieler verbannen", + "kidFriendlyModeText": "Kinderfreundlicher Modus (Reduzierte Gewalt, etc.)", + "languageText": "Sprache", + "moddingGuideText": "Modding Anleitung", + "mustRestartText": "Das Spiel muss neugestartet werden um die Änderungen wirksam zu machen.", + "netTestingText": "Netzwerk Tests", + "resetText": "Zurücksetzen", + "showBombTrajectoriesText": "Zeige die Bomben-Flugbahn", + "showPlayerNamesText": "Zeige Spielernamen", + "showUserModsText": "Zeige Mods-Ordner", + "titleText": "Erweitert", + "translationEditorButtonText": "${APP_NAME} Übersetzungseditor", + "translationFetchErrorText": "Übersetzungsstatus nicht verfügbar....", + "translationFetchingStatusText": "prüfe Übersetzungsstatus...", + "translationInformMe": "Informiere mich, wenn meine Sprache Updates benötigt.", + "translationNoUpdateNeededText": "Die ausgewählte Sprache ist aktuell; Juhuuu!", + "translationUpdateNeededText": "** die ausgewählte Sprache ist nicht aktuell!! **", + "vrTestingText": "VR Tests" + }, + "shareText": "Teilen", + "sharingText": "Teilt...", + "showText": "Zeige", + "signInForPromoCodeText": "Du musst dich bei einem Account anmelden um den Code einzulösen.", + "signInWithGameCenterText": "Nutze die Game Center app,\num dich anzumelden.", + "singleGamePlaylistNameText": "Nur ${GAME}", + "singlePlayerCountText": "1 Spieler", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Charakterauswahl", + "Chosen One": "Auserwählter", + "Epic": "Spiele im Epik-Modus", + "Epic Race": "Epik-Rennen", + "FlagCatcher": "Fahne erobern", + "Flying": "Glückliche Gedanken", + "Football": "Football", + "ForwardMarch": "Überfall", + "GrandRomp": "Eroberung", + "Hockey": "Hockey", + "Keep Away": "Fernhalten", + "Marching": "An der Nase herumführen", + "Menu": "Hauptmenü", + "Onslaught": "Heftiger Angriff", + "Race": "Rennen", + "Scary": "Bergkönig", + "Scores": "Punktebildschirm", + "Survival": "Eliminieren", + "ToTheDeath": "Deathmatch", + "Victory": "Endpunktestand Bildschirm" + }, + "spaceKeyText": "Leer", + "statsText": "Statistiken", + "storagePermissionAccessText": "Dies benötigt Zugriff auf deinen Speicher", + "store": { + "alreadyOwnText": "Du besitzt bereits ${NAME}!", + "bombSquadProDescriptionText": "• Verdoppelt die Erfolge-Tickets Belohnung\n• Entfernt Werbung\n• Enthält ${COUNT} Bonustickets\n• +${PERCENT}% Ranglisten Bonus\n• Schaltet '${INF_ONSLAUGHT}' und\n '${INF_RUNAROUND}' Koop Spiele frei", + "bombSquadProFeaturesText": "Eigenschaften:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Entfernt die Werbung\n• Schaltet weitere Spieleinstellungen frei\n• Enthält außerdem:", + "buyText": "kaufen", + "charactersText": "Charaktere", + "comingSoonText": "Kommt bald...", + "extrasText": "Extras", + "freeBombSquadProText": "BombSquad ist nun kostenlos, aber da du es bereits gekauft hast, bekommst\ndu das BombSquad Pro Upgrade und ${COUNT} Tickets als ein Dankeschön.\nGenieße die neuen Funktionen, und danke für die Unterstützung!\n-Eric", + "gameUpgradesText": "Spiel Upgrades", + "getCoinsText": "Münzen holen", + "holidaySpecialText": "Ferien Spezial", + "howToSwitchCharactersText": "(Gehe zu \"${SETTINGS} -> ${PLAYER_PROFILES}\" um deine Charactere zu bearbeiten)", + "howToUseIconsText": "(Erstelle öffentliche Spieler Profile (im Account Fenster), um diese zu benutzen.)", + "howToUseMapsText": "(Benutze diese Mappen in deinen eigenen Teams/Frei-für-alle Playlisten)", + "iconsText": "Symbole", + "loadErrorText": "Seite kann nicht geladen werden.\nÜberprüfe deine Internetverbindung.", + "loadingText": "laden", + "mapsText": "Karten", + "miniGamesText": "MiniSpiele", + "oneTimeOnlyText": "(einmalig)", + "purchaseAlreadyInProgressText": "Das Item wird gerade bezahlt. Bitte warte ein bisschen.", + "purchaseConfirmText": "${ITEM} kaufen?", + "purchaseNotValidError": "Kauf nicht gültig.\nKontaktiere ${EMAIL} wenn es ein Fehler ist.", + "purchaseText": "Einkaufen", + "saleBundleText": "Paket Rabatt!", + "saleExclaimText": "Sale!", + "salePercentText": "(um ${PERCENT}% reduziert)", + "saleText": "SALE", + "searchText": "Suchen", + "teamsFreeForAllGamesText": "Teams / Frei für alle Spiele", + "totalWorthText": "*** Wert: ${TOTAL_WORTH}!", + "upgradeQuestionText": "Upgraden?", + "winterSpecialText": "Weihnachtsspezial", + "youOwnThisText": "- In Besitz -" + }, + "storeDescriptionText": "Verrücktes Partyspiel mit bis zu 8 Mitspielern.\n\nZeige es deinen Freunden (oder dem Computer) in einem Turnier explosiver Minispiele, wie beispielsweise Fahne erobern, Bomben-Hockey und Epik-Zeitlupen-Deathmatch!\n\nEinfache Bedienung und ein erweiterter Controller-Support vereinfachen es mit bis zu 8 Spielern sofort durchzustarten; du kannst sogar dein Mobilgerät mit der kostenlosen 'BombSquad Remote' App als Controller benutzen!\n\nBomb sie alle weg!\n\nFür weitere Informationen, besuche www.froemling.net/bombsquad.", + "storeDescriptions": { + "blowUpYourFriendsText": "Jag deine Freunde in die Luft.", + "competeInMiniGamesText": "Behaupte dich in Minispielen von Wettrennen bis hin zu Fliegen.", + "customize2Text": "Passe Charaktere, Minispiele und sogar die Musik an.", + "customizeText": "Passe Charaktere an und erstelle deine eigenen Listen von Minispielen.", + "sportsMoreFunText": "Mit Sprengstoff macht Sport mehr Spaß.", + "teamUpAgainstComputerText": "Verbündet euch gegen den Computer." + }, + "storeText": "Laden", + "submitText": "Bestätigen", + "submittingPromoCodeText": "Code wird übertragen...", + "teamNamesColorText": "Team Namen/Farben...", + "teamsText": "Teams", + "telnetAccessGrantedText": "Telnet Zugang aktiviert.", + "telnetAccessText": "Telnet Zugang erkannt; erlauben?", + "testBuildErrorText": "Diese Testversion ist veraltet; Bitte überprüfe auf Updates.", + "testBuildText": "Testversion", + "testBuildValidateErrorText": "Unfähig die Testversion zu bestätigen. (Kein Internet?)", + "testBuildValidatedText": "Testversion ist gültig; Viel Spaß!", + "thankYouText": "Vielen Dank für deine Hilfe und viel Spaß!!", + "threeKillText": "DREIFACH KILL!!", + "timeBonusText": "Zeitbonus", + "timeElapsedText": "Vergangene Zeit", + "timeExpiredText": "Zeit abgelaufen!", + "timeSuffixDaysText": "${COUNT}T", + "timeSuffixHoursText": "${COUNT}S", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Tipp", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Top Freunde", + "tournamentCheckingStateText": "Überprüfe Turnier Status; Bitte warten...", + "tournamentEndedText": "Dieses Turnier ist zu Ende. Ein neues startet bald.", + "tournamentEntryText": "Turnier beitreten", + "tournamentResultsRecentText": "Neueste Turnierergebnisse", + "tournamentStandingsText": "Tournier Tabelle", + "tournamentText": "Turnier", + "tournamentTimeExpiredText": "Turnierzeit abgelaufen", + "tournamentsText": "Turniere", + "translations": { + "characterNames": { + "Agent Johnson": "Agent Johnson", + "B-9000": "Roboter", + "Bernard": "Bernard", + "Bones": "Gebeine", + "Butch": "Butch", + "Easter Bunny": "Osterhase", + "Flopsy": "Flopsy", + "Frosty": "Eisi", + "Gretel": "Gretel", + "Grumbledorf": "Grumbledore", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Glückspilz", + "Mel": "Mel", + "Middle-Man": "Mittelmann", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Weihnachtsmann", + "Snake Shadow": "Schlangenschatten", + "Spaz": "Spaz", + "Taobao Mascot": "TaoBao", + "Todd": "Todd", + "Todd McBurton": "Todd McBurton", + "Xara": "Xara", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Endlos\nOnslaught", + "Infinite\nRunaround": "Endlos\nRunaround", + "Onslaught\nTraining": "Onslaught\nTraining", + "Pro\nFootball": "Pro\nFootball", + "Pro\nOnslaught": "Pro\nOnslaught", + "Pro\nRunaround": "Pro\nRunaround", + "Rookie\nFootball": "Anfänger\nFootball", + "Rookie\nOnslaught": "Anfänger\nOnslaught", + "The\nLast Stand": "The\nLast Stand", + "Uber\nFootball": "Über\nFootball", + "Uber\nOnslaught": "Über\nOnslaught", + "Uber\nRunaround": "Über\nRunaround" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Training", + "Infinite ${GAME}": "Unendliches ${GAME}", + "Infinite Onslaught": "Endlos heftige Angriffe", + "Infinite Runaround": "Endlos an der Nase herumführen", + "Onslaught": "Endlos Onslaught", + "Onslaught Training": "heftiger Angriff Training", + "Pro ${GAME}": "Profi ${GAME}", + "Pro Football": "Profi Football", + "Pro Onslaught": "Profi heftiger Angriff", + "Pro Runaround": "Profi an der Nase herumführen", + "Rookie ${GAME}": "Anfänger ${GAME}", + "Rookie Football": "Anfänger Football", + "Rookie Onslaught": "Anfänger heftiger Angriff", + "Runaround": "Endlos Runaround", + "The Last Stand": "Der letzte Widerstand", + "Uber ${GAME}": "Unmöglich ${GAME}", + "Uber Football": "Unmöglich Football", + "Uber Onslaught": "Unmöglich heftiger Angriff", + "Uber Runaround": "Unmöglich an der Nase herumführen" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Sei eine Zeit lang der Auserwählte, um zu gewinnen.\nTöte ihn, um selbst zum Auserwählten zu werden.", + "Bomb as many targets as you can.": "Zerbombe so viele Ziele wie Du kannst.", + "Carry the flag for ${ARG1} seconds.": "Trage die Fahne für ${ARG1} Sekunden.", + "Carry the flag for a set length of time.": "Trage die Fahne über den eingestellten Zeitraum.", + "Crush ${ARG1} of your enemies.": "Zerquetsche ${ARG1} deiner Gegner.", + "Defeat all enemies.": "Besiege alle Gegner.", + "Dodge the falling bombs.": "Weiche den fallenden Bomben aus.", + "Final glorious epic slow motion battle to the death.": "Letzter, glorreicher, epischer Kampf bis in den Tod in Zeitlupe.", + "Gather eggs!": "Sammle Eier!", + "Get the flag to the enemy end zone.": "Bring die Fahne bis zum Endbereich deiner Gegner.", + "How fast can you defeat the ninjas?": "Wie schnell kannst du die Ninjas besiegen?", + "Kill a set number of enemies to win.": "Töte die eingestellte Anzahl an Gegner, um zu gewinnen.", + "Last one standing wins.": "Der letzte Überlebende gewinnt.", + "Last remaining alive wins.": "Der letzte Überlebende gewinnt.", + "Last team standing wins.": "Das letzte überlebende Team gewinnt.", + "Prevent enemies from reaching the exit.": "Halte die Gegner davon ab, den Ausgang zu erreichen.", + "Reach the enemy flag to score.": "Erreiche die gegnerische Fahne, um zu punkten.", + "Return the enemy flag to score.": "Bringe die gegnerische Fahne zurück, um zu punkten.", + "Run ${ARG1} laps.": "Renne ${ARG1} Runden.", + "Run ${ARG1} laps. Your entire team has to finish.": "Renne ${ARG1} Runden. Dein gesamtes Team muss das Ziel erreichen.", + "Run 1 lap.": "Renne 1 Runde.", + "Run 1 lap. Your entire team has to finish.": "Renne 1 Runde. Dein gesamtes Team muss das Ziel erreichen.", + "Run real fast!": "Renne richtig schnell!", + "Score ${ARG1} goals.": "Schieße ${ARG1} Tore.", + "Score ${ARG1} touchdowns.": "Mache ${ARG1} Touchdowns.", + "Score a goal": "Schieße ein Tor", + "Score a goal.": "Schieß ein Tor.", + "Score a touchdown.": "Erziel einen Touchdown.", + "Score some goals.": "Schieße ein paar Tore.", + "Secure all ${ARG1} flags.": "Nimm alle ${ARG1} Fahnen ein.", + "Secure all flags on the map to win.": "Nimm alle Fahnen auf der Karte ein, um zu gewinnen.", + "Secure the flag for ${ARG1} seconds.": "Nimm die Fahne für ${ARG1} Sekunden ein.", + "Secure the flag for a set length of time.": "Nimm die Fahne für die Länge der eingestellten Zeit ein.", + "Steal the enemy flag ${ARG1} times.": "Stehle die gegnerische Fahne ${ARG1} Mal.", + "Steal the enemy flag.": "Stehle die gegnerische Fahne.", + "There can be only one.": "Es kann nur Einen geben.", + "Touch the enemy flag ${ARG1} times.": "Berühre die gegnerische Fahne ${ARG1} Mal.", + "Touch the enemy flag.": "Berühre die gegnerische Fahne.", + "carry the flag for ${ARG1} seconds": "Trage die Fahne für ${ARG1} Sekunden", + "kill ${ARG1} enemies": "töte ${ARG1} Gegner", + "last one standing wins": "der letzte Überlebende gewinnt", + "last team standing wins": "das letzte überlebende Team gewinnt", + "return ${ARG1} flags": "Gib ${ARG1} Flaggen zurück", + "return 1 flag": "Gib eine Flagge zurück", + "run ${ARG1} laps": "renne ${ARG1} runden", + "run 1 lap": "renne 1 runde", + "score ${ARG1} goals": "schieße ${ARG1} Tore", + "score ${ARG1} touchdowns": "mache ${ARG1} Touchdowns", + "score a goal": "schieß ein Tor", + "score a touchdown": "erziel einen Touchdown", + "secure all ${ARG1} flags": "Sichere alle ${ARG1} Flaggen", + "secure the flag for ${ARG1} seconds": "beschütze die Flagge ${ARG1} Sekunden", + "touch ${ARG1} flags": "berühre ${ARG1} Flaggen", + "touch 1 flag": "berühre eine Flagge" + }, + "gameNames": { + "Assault": "Überfall", + "Capture the Flag": "Fahne erobern", + "Chosen One": "Auserwählte", + "Conquest": "Eroberung", + "Death Match": "Deathmatch", + "Easter Egg Hunt": "Ostereier-Jagd", + "Elimination": "Eliminieren", + "Football": "Football", + "Hockey": "Hockey", + "Keep Away": "Fernhalten", + "King of the Hill": "Bergkönig", + "Meteor Shower": "Kometenschauer", + "Ninja Fight": "Ninjakampf", + "Onslaught": "heftiger Angriff", + "Race": "Rennen", + "Runaround": "An der Nase herumführen", + "Target Practice": "Zielübung", + "The Last Stand": "Der letzte Widerstand" + }, + "inputDeviceNames": { + "Keyboard": "Tastatur", + "Keyboard P2": "Tastatur Spieler 2" + }, + "languages": { + "Arabic": "Arabisch", + "Belarussian": "Weißrussland", + "Chinese": "Chinesisch vereinfacht", + "ChineseTraditional": "Chinesisch Traditionell", + "Croatian": "Kroatisch", + "Czech": "Tschechisch", + "Danish": "Dänisch", + "Dutch": "Holländisch", + "English": "Englisch", + "Esperanto": "Esperanto", + "Finnish": "Finnisch", + "French": "Französisch", + "German": "Deutsch", + "Gibberish": "Kauderwelsch", + "Greek": "Griechisch", + "Hindi": "Hindi", + "Hungarian": "Ungarisch", + "Indonesian": "Indonesisch", + "Italian": "Italienisch", + "Japanese": "Japanisch", + "Korean": "Koreanisch", + "Persian": "Persisch", + "Polish": "Polnisch", + "Portuguese": "Portugiesisch", + "Romanian": "Rumänisch", + "Russian": "Russisch", + "Serbian": "Serbisch", + "Slovak": "Slovakisch", + "Spanish": "Spanisch", + "Swedish": "Schwedisch", + "Turkish": "Türkisch", + "Ukrainian": "Ukrainisch", + "Venetian": "Venezianisch", + "Vietnamese": "Vietnamesisch" + }, + "leagueNames": { + "Bronze": "Bronze", + "Diamond": "Diamant", + "Gold": "Gold", + "Silver": "Silber" + }, + "mapsNames": { + "Big G": "Großes G", + "Bridgit": "Steg", + "Courtyard": "Innenhof", + "Crag Castle": "Felszacken-Schloss", + "Doom Shroom": "Verhängnis-Pilz", + "Football Stadium": "Football Stadion", + "Happy Thoughts": "Glückliche Gedanken", + "Hockey Stadium": "Hockey Stadion", + "Lake Frigid": "Eisiger See", + "Monkey Face": "Affengesicht", + "Rampage": "Randale", + "Roundabout": "Drunter und Drüber", + "Step Right Up": "Treppe nach oben", + "The Pad": "Das Pad", + "Tip Top": "Gipfel", + "Tower D": "Turm D", + "Zigzag": "Zick-Zack" + }, + "playlistNames": { + "Just Epic": "Einfach episch", + "Just Sports": "Nur Sport" + }, + "promoCodeResponses": { + "invalid promo code": "ungültiger Promo Code" + }, + "scoreNames": { + "Flags": "Flaggen", + "Goals": "Tore", + "Score": "Punktestand", + "Survived": "Überlebt", + "Time": "Zeit", + "Time Held": "Haltezeit" + }, + "serverResponses": { + "A code has already been used on this account.": "Mit diesem Account wurde bereits ein Code eingelöst.", + "A reward has already been given for that address.": "Eine Belohnung wurde bereits vergeben für diese Adresse.", + "Account linking successful!": "Accounts erfolgreich verknüpft!", + "Account unlinking successful!": "Aufheben der Kontoverknüpfung erfolgreich!", + "Accounts are already linked.": "Accounts sind schon verknüpft.", + "An error has occurred; (${ERROR})": "Ein Fehler ist aufgetreten; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Ein Fehler ist aufgetreten; bitte kontaktiere den Support. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Ein Fehler ist aufgetreten; bitte kontaktiere: support@froemling.net.", + "An error has occurred; please try again later.": "Ein Error ist aufgetreten; bitte später nochmal versuchen.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Bist du sicher, dass du diese Accounts verlinken willst?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nDu kannst das nicht mehr rückgängig machen!", + "BombSquad Pro unlocked!": "BombSquad Pro freigeschaltet!", + "Can't link 2 accounts of this type.": "Du kannst nicht 2 Accounts des selben Typs verknüpfen.", + "Can't link 2 diamond league accounts.": "Du kannst keine 2 Accounts der Diamanten Liga verknüpfen.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Kann nicht verlinken; Maximum sind ${COUNT} verknüpfte Accounts.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Cheating erkannt; deine Punkte und Preise sind für ${COUNT} Tage gesperrt.", + "Could not establish a secure connection.": "Konnte keine sichere Verbindung herstellen.", + "Daily maximum reached.": "Tageslimit erreicht.", + "Entering tournament...": "Trete Turnier bei...", + "Invalid code.": "Ungültiger Code", + "Invalid payment; purchase canceled.": "Ungültige Zahlung; Einkauf abgebrochen.", + "Invalid promo code.": "Ungültiger Promo Code.", + "Invalid purchase.": "Einkauf ungültig.", + "Invalid tournament entry; score will be ignored.": "Ungültiger Turnier Eintrag; die Punktzahl wird ignoriert.", + "Item unlocked!": "Gegenstand freigeschaltet!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "VERLINKUNG VERWEIGERT. ${ACCOUNT} beinhaltet\nwichtige Daten, die ALLE VERLOREN wären.\nDu kannst andersherum verlinken, wenn du willst\n(Du würdest stattdessen DIESE Account-Daten verlieren)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Den Account ${ACCOUNT} mit diesem Account verknüpfen?\nJeglicher Fortschritt des Accounts ${ACCOUNT} wird verloren gehen!\nDies kann nicht rückgängig gemacht werden! Fortfahren?", + "Max number of playlists reached.": "Max Anzahl von Wiedergabelisten erreicht.", + "Max number of profiles reached.": "Max Anzahl der Profile erreicht.", + "Maximum friend code rewards reached.": "Maximale Anzahl an Einladungsbelohnungen erreicht.", + "Message is too long.": "Nachricht ist zu lang.", + "Profile \"${NAME}\" upgraded successfully.": "Profil \"${NAME}\" erfolgreich aktualisiert.", + "Profile could not be upgraded.": "Profil konnte nicht aktualisiert werden.", + "Purchase successful!": "Einkauf erfolgreich!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "${COUNT} Tickets für den Login erhalten.\nKomm morgen wieder für ${TOMORROW_COUNT}!", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Die Funktionalität des Servers wird nicht länger in dieser Spielversion unterstützt;\nBitte updaten sie eine aktuellere Version.", + "Sorry, there are no uses remaining on this code.": "Leider ist das Nutzungs-Limit für diesen Code schon erreicht.", + "Sorry, this code has already been used.": "Tut uns leid, dieser Code wurde bereits eingelöst.", + "Sorry, this code has expired.": "Tut uns leid, dieser Code ist nicht mehr gültig.", + "Sorry, this code only works for new accounts.": "Tut uns leid, dieser Code kann nur mit einem neuen Account eingelöst werden.", + "Temporarily unavailable; please try again later.": "Vorübergehend nicht verfügbar. Bitte versuche es später noch einmal.", + "The tournament ended before you finished.": "Das Turnier endete bevor du ins Ziel kamst.", + "This account cannot be unlinked for ${NUM} days.": "Dieser Account kann nicht für ${NUM} Tage entknüpft werden.", + "This code cannot be used on the account that created it.": "Dieser Code kann nur mit dem Account eingelöst werden, mit dem er erzeugt wurde.", + "This requires version ${VERSION} or newer.": "Hierfür wird Version ${VERSION} oder neuer benötigt.", + "Tournaments disabled due to rooted device.": "Tourniere wegen gerootetem Gerät deaktiviert.", + "Tournaments require ${VERSION} or newer": "Turniere benötigen Version ${VERSION} oder neuer", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Den Account ${ACCOUNT} von diesem Account entknüpfen?\nJeglicher Fortschritt auf ${ACCOUNT} wird zurückgesetzt.\n(außer Erfolge in manchen Fällen)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "Beschwerden von Hacking sind für deinen Account aufgetreten.\nAccount, welche des Hackings verdächtigt werden, werden gesperrt. Bitte spiele gerecht.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Willst du deinen Gerät-Account mit diesem Account verlinken?\n\nDein Gerät-Account: ${ACCOUNT1}\nDieser Account: ${ACCOUNT2}\n\nDies wird dir ermöglichen, deinen Fortschritt zu behalten.\nAchtung: Du kannst es nicht rückgängig machen!", + "You already own this!": "Du besitzt es bereits!", + "You can join in ${COUNT} seconds.": "Du kannst in ${COUNT} Sekunden beitreten", + "You don't have enough tickets for this!": "Du hast nicht genug Tickets dafür!", + "You don't own that.": "Du besitzt das nicht.", + "You got ${COUNT} tickets!": "Du hast ${COUNT} Tickets!", + "You got a ${ITEM}!": "Du kriegst ein ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Du bist eine Liga aufgestiegen; Glückwunsch!", + "You must update to a newer version of the app to do this.": "Du musst deine Version updaten, um dies zu tun.", + "You must update to the newest version of the game to do this.": "Du musst zur neusten Version des Spiels updaten um dies zu tun.", + "You must wait a few seconds before entering a new code.": "Du musst ein wenig warten bevor du einen neuen Code eingeben kannst.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Du hast den #${RANK} Platz im letzten Turnier erreicht. Danke fürs Spielen!", + "Your account was rejected. Are you signed in?": "Dein Account wurde abgelehnt. Bist du eingeloggt?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Die Kopie dieses Spiels wurde modifiziert.\nÄnderungen rückgängig machen & neu versuchen.", + "Your friend code was used by ${ACCOUNT}": "Dein Freundschafts-Code wurde von ${ACCOUNT} verwendet." + }, + "settingNames": { + "1 Minute": "1 Minute", + "1 Second": "1 Sekunde", + "10 Minutes": "10 Minuten", + "2 Minutes": "2 Minuten", + "2 Seconds": "2 Sekunden", + "20 Minutes": "20 Minuten", + "4 Seconds": "4 Sekunden", + "5 Minutes": "5 Minuten", + "8 Seconds": "8 Sekunden", + "Allow Negative Scores": "Erlaube negative Punktzahlen.", + "Balance Total Lives": "Ausgleich der gesamten Leben", + "Bomb Spawning": "Bomben spawnen", + "Chosen One Gets Gloves": "Auserwählter bekommt Handschuhe", + "Chosen One Gets Shield": "Auserwählter bekommt ein Schild", + "Chosen One Time": "Wähle eine Zeit", + "Enable Impact Bombs": "Sofortbomben aktivieren", + "Enable Triple Bombs": "Dreifachbomben altivieren", + "Entire Team Must Finish": "Das ganze Team muss die Ziellinie überqueren.", + "Epic Mode": "Epischer Modus", + "Flag Idle Return Time": "Reset der fallengelassenen Fahne", + "Flag Touch Return Time": "Flaggen Rückkehr Zeit", + "Hold Time": "Haltezeit", + "Kills to Win Per Player": "Abschüsse zum Sieg pro Spieler", + "Laps": "Runden", + "Lives Per Player": "Leben pro Spieler", + "Long": "Lang", + "Longer": "Länger", + "Mine Spawning": "Erscheinende Minen", + "No Mines": "Keine Minen", + "None": "Keine", + "Normal": "Normal", + "Pro Mode": "Pro Mode", + "Respawn Times": "Wiederbelebungszeit", + "Score to Win": "Punkte zum Sieg", + "Short": "Kurz", + "Shorter": "Kürzer", + "Solo Mode": "Eins-gegen-Eins", + "Target Count": "Anzahl der Ziele", + "Time Limit": "Zeitlimit" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} ist disqualifiziert, da ${PLAYER} verlassen hat.", + "Killing ${NAME} for skipping part of the track!": "${NAME} wurde wegen Betrugsversuch ausgeschaltet!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Warnung an ${NAME}: turbo / Knopf spammen macht dich bewusstlos." + }, + "teamNames": { + "Bad Guys": "Die Bösen", + "Blue": "Blau", + "Good Guys": "Die Guten", + "Red": "Rot" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Ein perfekter Lauf-Sprung-Dreh-Kick haut jeden um \nund sichert dir den Respekt deiner Freunde.", + "Always remember to floss.": "Vergesse es nie, Zahnseide zu verwenden.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Erstelle Spielerprofile für dich und deine Freunde, mit den von\neuch gewünschten Namen und Aussehen, anstatt Zufällige zu verwenden.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Fluchkisten verwandeln dich in eine tickende Zeitbombe.\nDie einzige Hoffnung ist es, schnell eine Heilkiste aufzusammeln.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Bis auf das Aussehen sind alle Charaktere gleich.\nNimm deswegen den, mit dem du dich am besten identifizierst.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Werde nicht zu frech mit dem Energieschild; es hilft dir nichts, wenn du die Klippe hinunterstürzt.", + "Don't run all the time. Really. You will fall off cliffs.": "Lauf nicht die ganze Zeit. Wirklich. Du fällst nur runter.", + "Don't spin for too long; you'll become dizzy and fall.": "Drehe dich nicht zu lange, dir wird schwindelig und du fällst herunter.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Halte einen beliebigen Knopf gedrückt um zu rennen. (Auch Schultertasten, falls vorhanden.)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Halte irgendeinen Knopf gedrückt um zu laufen. Du bewegst \ndich schneller aber bist nicht mehr so agil. Vorsicht vor Kanten.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Eisbomben sind nicht gerade die stärksten, aber sie frieren\nandere ein machen sie überaus zerbrechlich.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Falls dich jemand packt, schlag zu und sie lassen dich gehen.\nFunktioniert auch bei Türstehern. Meistens.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Wenn du zu wenige Controller hast,installiere die '${REMOTE_APP_NAME}' App \nauf deinem mobilen Gerät, um diesen als Controller zu nutzen.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Zu wenig Controller für alle? Installier dir die 'BombSquad Remote' App \nauf deinem iOs oder Android Gerät und nutze diese als Controller.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Klebt eine Klebebombe an dir? Spring herum und dreh dich im Kreis. Vielleicht\nkannst du die Bombe abschütteln. Falls nicht, sehen deine letzten Sekunden wenigstens lustig aus.", + "If you kill an enemy in one hit you get double points for it.": "Erledigst du einen Gegner mit 1 Schlag bekommst du dafür doppelte Punkte.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Bist du mit einem Fluch besessen, ist deine einzige Hoffnung des Überlebens ein Erste-Hilfe-Kit", + "If you stay in one place, you're toast. Run and dodge to survive..": "Bleibst du stehen, bist du erledigt. Bleibe in Bewegung um zu überleben...", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Wenn viele Spieler kommen und gehen, (auf einer Party z.b.) stelle, 'auto-kick-idle-players'\nunter Optionen ein, um inaktive Spieler automatisch aus dem Spiel zu entfernen.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Wenn dein Gerät zu warm wird oder du Akku sparen möchtest, stelle\n\"Visuelles\" oder \"Auflösung\" in Einstellungen->Grafik auf Niedrig.", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Zu wenig FPS? Versuche die Auflösung und Details in\nden Einstellungen zu verringern.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "In Fahne erobern muss die eigene Flagge in der Basis sein, um Punkten zu können.\nDie gegnerische Flagge zu stehlen kann also ein guter Weg sein ihn am Punkten zu hindern.", + "In hockey, you'll maintain more speed if you turn gradually.": "Halte deine Geschwindigkeit in Hockey, indem du weite Kurven fährst.", + "It's easier to win with a friend or two helping.": "Es ist leichter zu gewinnen, wenn ein paar deiner Freunde helfen.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Springe und werfe deine Bomben gleichzeitig um höhere Ebenen mit ihnen zu erreichen.", + "Land-mines are a good way to stop speedy enemies.": "Landminen eignen sich gut um rennende Gegner zu stoppen.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Viele Dinge können aufgehoben und geworfen werden, auch andere Spieler.\nGegner in Abgründe zu werfen kann eine effektive und genugtuende Strategie sein.", + "No, you can't get up on the ledge. You have to throw bombs.": "Manche Vorsprünge lassen sich nicht zu Fuß erreichen. Benutze Bomben.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Spieler können mitten im Spiel beitreten und verlassen,\nund du kannst auch Controller im Betrieb an- und abschließen.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Spieler können den meisten Spielmodi mitten im Match beitreten,\ndu kannst auch Gamepads während des Spiels verbinden oder trennen.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Powerups wirken nur in Koop-Spielen für begrenzte Zeit.\nIn Team und Jeder gegen Jeden Spielen wirken sie bis zu deinem Tod.", + "Practice using your momentum to throw bombs more accurately.": "Übe den Schwung einzusetzen, um Bomben noch genauer zu werfen.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Schläge fügen mehr Schaden zu, umso schneller sich deine Fäuste bewegen.\nAlso lauf, spring und dreh dich wie ein Verrückter!", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Lauf vor und zurück bevor du eine Bombe wirfst,\num diese mit Schwung weiter zu werfen.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Erledige eine ganze Gruppe, indem du eine \nBombe neben einer TNT Kiste platzierst", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Der Kopf ist die empfindlichste Stelle. Klebebomben an deiner\nBirne wirst du deswegen fast nie überleben.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Dieses Level endet niemals, aber ein Highscore \nwird dir ewigen Respekt in der Welt einbringen.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Die Wurfkraft hängt von den Richtungstasten ab.\nUm etwas direkt vor dich zu werfen, drücke beim Werfen keine Richtungstaste.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Genervt vom Soundtrack? Wechsel ihn gegen deinen Eigenen!\nSchaue bei Einstellungen->Audio->Soundtrack", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Time Explosionen indem du Bomben noch einen Moment in der Hand hältst bevor du wirfst.", + "Try tricking enemies into killing eachother or running off cliffs.": "Trickse Gegner aus, sich gegenseitig zu töten oder von Klippen zu fallen.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Benutze die Aufnehmen-Taste um die Flagge aufzuheben < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Drehe dich ruckartig um, um Objekte weiter zu werfen.", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Du kannst mit deiner Faust \"zielen\" indem du dich nach links oder rechts drehst.\nDas ist nützlich um Gegner von Kanten zu stoßen oder Tore in Hockey zu schießen.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Du weißt nicht wann die Bombe explodiert? Schau auf die Zündschnur!\nGelb..Orange..Rot..BOOM.", + "You can throw bombs higher if you jump just before throwing.": "Wirf deine Bomben weiter indem du kurz vor dem Wurf springst.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Du brauchst nicht dein Profil zu bearbeiten um deine Figur zu ändern; Drücke einfach den \nGreifen-Button wenn du einem Spiel beitrittst um deine Standardfigur zu überschreiben.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Du nimmst Schaden, wenn dein Kopf gegen Objekte oder andere Spieler knallt,\nalso versuche das zu vermeiden.", + "Your punches do much more damage if you are running or spinning.": "Deine Schläge verursachen mehr Schaden wenn du dich drehst oder rennst" + } + }, + "trialPeriodEndedText": "Die Demoversion ist vorbei. Willst du \nBombsquad kaufen und weiterspielen?", + "trophiesRequiredText": "Es werden ${NUMBER} Trophäen benötigt.", + "trophiesText": "Trophäen", + "trophiesThisSeasonText": "Erfolge dieser Saison", + "tutorial": { + "cpuBenchmarkText": "Tutorial läuft in Krasser Geschwindigkeit (CPU Leistung wird getestet)", + "phrase01Text": "Hallo Leute!", + "phrase02Text": "Willkommen zu ${APP_NAME}!", + "phrase03Text": "Hier sind ein paar Tipps zur Kontrolle eures Charakters:", + "phrase04Text": "Viele Dinge in ${APP_NAME} basieren auf PHYSIK.", + "phrase05Text": "Zum Beispiel, wenn du jemanden schlägst,..", + "phrase06Text": "..wird mehr Schaden für schnellere Schläge verteilt.", + "phrase07Text": "Bemerkt? Wir haben uns kaum bewegt, das tat ${NAME} also nicht so weh.", + "phrase08Text": "Jetzt lass uns springen und uns drehen um schneller zu werden.", + "phrase09Text": "Ah, schon besser.", + "phrase10Text": "Laufen hilft ebenfalls.", + "phrase11Text": "Halte einen BELIEBIGEN Knopf gedrückt um zu laufen.", + "phrase12Text": "Für besonders tolle Schläge, versuch mal zu laufen und dich zu drehen.", + "phrase13Text": "Huch; tut mir Leid, ${NAME}.", + "phrase14Text": "Du kannst Dinge aufsammeln und werfen, zum Beispiel Flaggen.. oder ${NAME}.", + "phrase15Text": "Schließlich gibt es noch Bomben.", + "phrase16Text": "Bomben zu werfen braucht Übung.", + "phrase17Text": "Aua! Kein wirklich guter Wurf.", + "phrase18Text": "Bewegung hilft weiter zu werfen.", + "phrase19Text": "Sprünge lassen dich höher werfen.", + "phrase20Text": "Schleuder deine Bomben für noch weitere Würfe.", + "phrase21Text": "Das Timen des Abwurfs kann schwierig sein.", + "phrase22Text": "Verdammt.", + "phrase23Text": "Warte ein bis zwei Sekunden.", + "phrase24Text": "Hurra! Du kannst es.", + "phrase25Text": "So, das war's auch schon.", + "phrase26Text": "Und jetzt schnapp sie dir, Tiger!", + "phrase27Text": "Verinnerliche dein Training und du wirst überleben!", + "phrase28Text": "..naja, vielleicht...", + "phrase29Text": "Viel Glück!", + "randomName1Text": "Fred", + "randomName2Text": "Harry", + "randomName3Text": "Bill", + "randomName4Text": "Chuck", + "randomName5Text": "Phil", + "skipConfirmText": "Willst du wirklich das Tutorial überspringen? Drücke einen Knopf, wenn ja.", + "skipVoteCountText": "${COUNT}/${TOTAL} Stimmen für Überspringen", + "skippingText": "überspringe Tutorial...", + "toSkipPressAnythingText": "(beliebige Taste drücken um Tutorial zu überspringen)" + }, + "twoKillText": "DOPPEL KILL!", + "unavailableText": "nicht verfügbar", + "unconfiguredControllerDetectedText": "Unkonfigurierter Controller erkannt:", + "unlockThisInTheStoreText": "Das muss im Store freigeschaltet werden.", + "unlockThisProfilesText": "Um mehr als ${NUM} Profile zu erstellen, brauchst du:", + "unlockThisText": "Um das zu entsperren brauchst du:", + "unsupportedHardwareText": "Entschuldigung, diese Hardware wird nicht unterstützt.", + "upFirstText": "Zuerst:", + "upNextText": "Als Nächstes in Spiel ${COUNT}:", + "updatingAccountText": "Aktualisiere dein Konto...", + "upgradeText": "Verbessern", + "upgradeToPlayText": "Upgrade auf \"${PRO}\" im Store um das zu Spielen.", + "useDefaultText": "Standard benutzen", + "usesExternalControllerText": "Das Spiel nutzt einen externen Controller für die Eingaben.", + "usingItunesText": "benutze Musik-App für Hintergrundmusik...", + "usingItunesTurnRepeatAndShuffleOnText": "Stelle sicher, dass iTunes ZUFÄLLIG wiedergibt und ALLE wiederholt.", + "validatingBetaText": "Verifizieren der Beta...", + "validatingTestBuildText": "Bestätige Testversion...", + "victoryText": "Sieg!", + "voteDelayText": "Die nächste Abstimmung kann erst in ${NUMBER} Sekunden gestartet werden.", + "voteInProgressText": "Es wird bereits eine Abstimmung durchgeführt.", + "votedAlreadyText": "Du hast schon abgestimmt", + "votesNeededText": "${NUMBER} Stimmen benötigt", + "vsText": "vs.", + "waitingForHostText": "(Warten auf ${HOST} um fortzufahren)", + "waitingForLocalPlayersText": "Warten auf lokale Spieler...", + "waitingForPlayersText": "Warte auf Spieler...", + "waitingInLineText": "In der Warteschlange (Party ist voll)...", + "watchAVideoText": "Video anschauen", + "watchAnAdText": "Schaue ein Video", + "watchWindow": { + "deleteConfirmText": "\"${REPLAY}\" löschen?", + "deleteReplayButtonText": "Wiederholung \nlöschen", + "myReplaysText": "Meine Wiederholungen", + "noReplaySelectedErrorText": "Keine Wiederholung ausgewählt", + "playbackSpeedText": "Abspielgeschwindigkeit: ${SPEED}", + "renameReplayButtonText": "Wiederholung\numbennen", + "renameReplayText": "\"${REPLAY}\" umbenennen zu:", + "renameText": "Umbennen", + "replayDeleteErrorText": "Fehler beim Löschen.", + "replayNameText": "Wiederholungs Name", + "replayRenameErrorAlreadyExistsText": "Eine Wiederholung mit diesem Namen existiert bereits.", + "replayRenameErrorInvalidName": "Kann nicht umbenannt werden; Name ungültig.", + "replayRenameErrorText": "Konnte nicht umbenannt werden.", + "sharedReplaysText": "geteilte \nWiederholungen", + "titleText": "Anschauen", + "watchReplayButtonText": "Wiederholung\nansehen" + }, + "waveText": "Welle", + "wellSureText": "Natürlich!", + "wiimoteLicenseWindow": { + "licenseText": "Copyright (c) 2007, DarwiinRemote Team\nAll rights reserved.\n\n Redistribution and use in source and binary forms, with or without modification,\n are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright notice, this\n list of conditions and the following disclaimer in the documentation and/or other\n materials provided with the distribution.\n3. Neither the name of this project nor the names of its contributors may be used to\n endorse or promote products derived from this software without specific prior\n written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.", + "licenseTextScale": 0.62, + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Nach Wiimotes suchen...", + "pressText": "Drücke Wiimote Knöpfe 1 und 2 gleichzeitig.", + "pressText2": "Bei neueren Wiimotes mit Motion Plus, drücke den roten \"sync\" Knopf auf der Unterseite.", + "pressText2Scale": 0.55, + "pressTextScale": 1.0 + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "copyrightTextScale": 0.6, + "listenText": "Suchen", + "macInstructionsText": "Deine Wii muss ausgeschaltet und Bluetooth \nan deinem Mac eingeschaltet sein, anschließend drücke \"Suchen\". \nWiimote Support spinnt häufig. \nDu musst es u.U. mehrmals versuchen.", + "macInstructionsTextScale": 0.7, + "thanksText": "Danke an das DarwiinRemote team\nfür die Umsetzung.", + "thanksTextScale": 0.8, + "titleText": "Wiimote einrichten" + }, + "winsPlayerText": "${NAME} Gewinnt!", + "winsTeamText": "${NAME} Gewinnt!", + "winsText": "${NAME} gewinnt!", + "worldScoresUnavailableText": "Weltrangliste ist nicht verfügbar", + "worldsBestScoresText": "Beste Punktzahl weltweit", + "worldsBestTimesText": "Beste Zeit weltweit", + "xbox360ControllersWindow": { + "getDriverText": "Treiber herunterladen", + "macInstructions2Text": "Nur Original Xbox 360 Controller funktionieren mit dem\n'Xbox 360 Wireless Controller for Windows'.\nDieser erlaubt bis zu vier Verbindungen.\n\nAchtung: Der Treiber funktioniert nicht mit Drittanbietern;\nGekennzeichnet mit 'Microsoft' nicht 'XBOX 360'.\nMicrosoft verkauft diese nicht mehr einzeln. Also muss man\nmit Controller oder woanders kaufen, zBsp. bei eBay.", + "macInstructions2TextScale": 0.76, + "macInstructionsText": "Um den Xbox 360 Controller benutzen zu können,\ninstalliere folgenden Mac Treiber.\nGilt für Kabel und Wireless Controller.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "Um einen Xbox 360 Controller mit Kabel benutzen zu können,\nverbinde diesen einfach über USB. Mehrere Controller können \nüber ein USB Hub verbunden werden.\n\nUm einen Wireless Controller benutzen zu können brauchst du einen\nWireless Adapter, wie er als Teil des \"Xbox 360 wireless Controller for Windows\"\nSet oder einzeln erhältlich ist. Jeder Wireless Adapter wird über USB Port\nverbunden so das du bis zu 4 Wireless Controller benutzen kannst.", + "ouyaInstructionsTextScale": 0.8, + "titleText": "Xbox 360 Controller mit ${APP_NAME} benutzen:" + }, + "yesAllowText": "Ja, erlauben!", + "yourBestScoresText": "Deine besten Punktzahlen", + "yourBestTimesText": "Deine besten Zeiten" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/gibberish.json b/dist/ba_data/data/languages/gibberish.json new file mode 100644 index 0000000..608b14f --- /dev/null +++ b/dist/ba_data/data/languages/gibberish.json @@ -0,0 +1,1999 @@ +{ + "accountRejectedText": "You ac woefije obj acwoew. Aj cowier wore cs?", + "accountSettingsWindow": { + "accountNameRules": "Acoief coej. c woejf. cwoef ocoweofwjfj c wjefowfowef wocjowefff", + "accountProfileText": "(acczntl prfflzlf)", + "accountsText": "Acctntzz", + "achievementProgressText": "Achilfjasdflz: ${COUNT} ouzt of ${TOTAL}", + "campaignProgressText": "Cmapghan Progflzl: ${PROGRESS}", + "changeOncePerSeason": "owe c wow chofu wefwoefjwofjowcowfwf.", + "changeOncePerSeasonError": "You cows ow woefj woifjwo ec oweo fowijf owiejf (${NUM} cowefwe)", + "customName": "Cow oj wojNaoa", + "deviceSpecificAccountText": "Crrlzfjowf uznfl a divwfo-zpijfwo ancnfo ${NAME}", + "linkAccountsEnterCodeText": "Enrlr Cfdsz", + "linkAccountsGenerateCodeText": "Gncowf CFdzz", + "linkAccountsInfoText": "(fwoco par owcj woef oac we paoi oijof)", + "linkAccountsInstructionsNewText": "Tl link j twice. owfj w off jeoifjwocjowiejfowef\ncoajfaocj wfjw efowjo. cowejf DO Oajofjwefjeo\norc aofj agpweoijv aefhwefo dj owiejf ewofj.\n(Dofjw jeotja theatric. owjfwfjjwf)\n\nYouc wfowej toij ${COUNT} cow oef.\n\nICMPEWOT: Coewij wthehowcjwef weofijweof;\ncIf wef htheojc;woej wejocowefjwe f wetowe\nbaoefhcw ecywoeutowermwocwoeitjs.", + "linkAccountsInstructionsText": "Tz lknf twz aoc coj, goi woc woef \nacoweof oac oia oow towjoifowj\nProwfo and in oir wojoc owinofiff.\nYouc owo iwoe oijf ${COUNT} accoaoijf.\n\nIMPCO oOIFOWEFJ\nowijecojwef\noijoicjwe\n\nAOicjowijeojwoeifjwef", + "linkAccountsText": "Lnk Accnffzz", + "linkedAccountsText": "Lnkfdf Accnrts:", + "nameChangeConfirm": "Cho weft cowejf coiwjocwe. ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Thzz wflzz resta yrsr co-opz proglfzlz\nand hghzl scrdz. Itz cntljdf be zunfzz.\nArz yoz srz?", + "resetProgressConfirmText": "Thzlz wlz rzlt yrlzz co-pz plglrz,\nachroifzlz, and hghzl-scrlrz.\nTz cndft b undozn.\nAnz youz sér?", + "resetProgressText": "Rztlsf Proggzfrz", + "setAccountName": "Soil owe o animas", + "setAccountNameDesc": "Selcow f cjwo ot afoa fw jcpaoewj fwocowefj.\nYou wolf wc. weowyc oawe awoefm mcapowefi \ncoat aocjweo towirowmcowiefownr.", + "signInInfoText": "Sgn inz tz strz yr prograrzz inz\nthz cld, rnzr tckrz, rnz", + "signInText": "Sggnz Inz", + "signInWithDeviceInfoText": "(an coiwjfow fcoa cowj efj woj cowij eofjwoj foijs aofij )", + "signInWithDeviceText": "Sngi foicj oj de voa cwoiejfw", + "signInWithGameCircleText": "Sgn in gh Gm Cirflc", + "signInWithGooglePlayText": "Snf ocj weo fGOofl Plfl", + "signInWithTestAccountInfoText": "(lgjo cac cojef ot; oeco doic w eofjw oero )", + "signInWithTestAccountText": "Sjc weo fwtjwoefj cowefwf", + "signOutText": "Sgngz Ozt", + "signingInText": "Sgngngn infz..", + "signingOutText": "Sngning ozt..", + "testAccountWarningCardboardText": "Wrrjowifj a f;aoiwejc owj efaoiwjef owjef oaje wf\naiwje ofjw eoijaocijw oeifjowefj owijefoiwjefowi ef\naowj aocj weoif ja;woiefj woioc woef woiefjow fjweo.\n\nc weoiwo efjwo cowiejf oaiwje oaiwj i ogamaoiw.\n(yoauc owi odf oaijfo wojaojpjf oafpo ewf owejrw er)", + "testAccountWarningOculusText": "Wrrjowifj a f;aoiwejc owj efaoiwjef owjef oaje wf\naiwje ofjw eoijaocijw oeifjowefj owijefoiwjefowi ef\naowj aocj weoif ja;woiefj woioc woef woiefjow fjweo.\n\nc weoiwo efjwo cowiejf oaiwje oaiwj i ogamaoiw.\n(yoauc owi odf oaijfo wojaojpjf oafpo ewf owejrw er)", + "testAccountWarningText": "Wrznnzn: yzz asdfa aofijs fo asd; ofiasdfo jasdo\na;sdj faoisdfj oiasdfo; asdoi aoisjdfasdf\nasdoj asodi oasdjf osijdf oaijsdf oiasfd\nasdof jaosdfj oasdjf ;oaisjdf;oijas dfoijasdf", + "ticketsText": "Tzcktzz: ${COUNT}", + "titleText": "Acnfnnz", + "unlinkAccountsInstructionsText": "Slj cwefjwe f owcjwoeijowief", + "unlinkAccountsText": "Uldfj owjowerjsr", + "viaAccount": "(fc cowefjwef ${NAME})", + "youAreLoggedInAsText": "Yzrl arz lgzffd iz az:", + "youAreSignedInAsText": "Yz arz sngnfd inz arz:" + }, + "achievementChallengesText": "Achéivmznt Chalzlngesz", + "achievementText": "Áchíévzmúnt", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Kílzl 3 bád gúyz wúth TNTz", + "descriptionComplete": "Killéd 3 bád gúyz wíth TNT", + "descriptionFull": "Kíll 3 bád gzys wíth TNT ónz ${LEVEL}", + "descriptionFullComplete": "Kzlléd 3 bad gzs wéth TNT ón ${LEVEL}", + "name": "Boóm Gúzz thz Dynámitz" + }, + "Boxer": { + "description": "Win withóut uzing any bómbz", + "descriptionComplete": "Wén withéut uzing any bémbzz", + "descriptionFull": "Czmplétz ${LEVEL} wéithzt uznng anyz bómbz", + "descriptionFullComplete": "Complzzted ${LEVEL} wíthoutz úsing ány bómbz", + "name": "Bóxzr" + }, + "Dual Wielding": { + "descriptionFull": "Coocjf co eofw jc9ha oc jweofi jwoe)", + "descriptionFullComplete": "Cncof o 2 Ocno come o f(hard co who oc)", + "name": "Dual Wlfjl c" + }, + "Flawless Victory": { + "description": "Win withóut getting hit", + "descriptionComplete": "Wén withéut getting hit", + "descriptionFull": "Wín ${LEVEL} wíthozt géttzng hétz", + "descriptionFullComplete": "Wón ${LEVEL} wíthóut géttnz hit", + "name": "Fláwlzzz Victoryz" + }, + "Free Loader": { + "descriptionFull": "Stalk two owiej of ; cow 2+ cpoijefz", + "descriptionFullComplete": "Stoic wo reif owe jo j oweo woeijo 2+ cowpef", + "name": "Frizz Ljdfefwz" + }, + "Gold Miner": { + "description": "Kill 6 búd guyz wíth lánd-minez", + "descriptionComplete": "Killed 6 bad guyz with land-minez", + "descriptionFull": "Kílz 6 bád gzsz wéth lánz-mínz on ${LEVEL}", + "descriptionFullComplete": "Kílzd 6 bád gzsz wéth lánz-mínz on ${LEVEL}", + "name": "Góld Mínzr" + }, + "Got the Moves": { + "description": "Win withóut uzing púnchez ór bómbz", + "descriptionComplete": "Wén withéut uzing punchez ér bémbz", + "descriptionFull": "Wín ${LEVEL} wiznfft ands bpunchpd so bmzlf", + "descriptionFullComplete": "Wonz ${LEVEL} wlznfif anz pnchr or bmzfz", + "name": "Gót thz Móvzz" + }, + "In Control": { + "descriptionFull": "Coin oil of jo owe fo(her dcc oiowe)", + "descriptionFullComplete": "Coco oe owe fwoinoi (where ocwo wjef)", + "name": "Ina Conoiwjef" + }, + "Last Stand God": { + "description": "Zcóre 1000 póintz", + "descriptionComplete": "Zcéred 1000 péintz", + "descriptionFull": "Scorz 1000 prtz on ${LEVEL}", + "descriptionFullComplete": "Scorzed 1000 Ptz onz ${LEVEL}", + "name": "${LEVEL} Gzd" + }, + "Last Stand Master": { + "description": "Zcóre 250 póintz", + "descriptionComplete": "Scórzd 250 póinzfzs", + "descriptionFull": "Scórz 250 póoinzf on ${LEVEL}", + "descriptionFullComplete": "Scórzd 250 póoinzf on ${LEVEL}", + "name": "${LEVEL} Mztstrz" + }, + "Last Stand Wizard": { + "description": "Zcóre 500 póintz", + "descriptionComplete": "Zcéred 500 péintz", + "descriptionFull": "Sczore 500 póintz ón ${LEVEL}", + "descriptionFullComplete": "Sczored 500 póintz ón ${LEVEL}", + "name": "${LEVEL} Wizárd" + }, + "Mine Games": { + "description": "Kill 3 bad guyz with land-minez", + "descriptionComplete": "Killéd 3 bód gúyz with land-minez", + "descriptionFull": "Kzll 3 bd guzl wzz lndz mdnz on ${LEVEL}", + "descriptionFullComplete": "Kzlld 3 bd guzl wzz lndz mdnz on ${LEVEL}", + "name": "Minz Gámzz" + }, + "Off You Go Then": { + "description": "Tózz 3 bad guyz óff the map", + "descriptionComplete": "Tézzed 3 bad guyz éff the map", + "descriptionFull": "Tlzzl 3 bd guzz ofz thz mp in ${LEVEL}", + "descriptionFullComplete": "Tlzzld 3 bd guzz ofz thz mp in ${LEVEL}", + "name": "Ofz Yóú Gú Thzn" + }, + "Onslaught God": { + "description": "Zcóre 5000 póintz", + "descriptionComplete": "Zcéred 5000 péintz", + "descriptionFull": "Scorlz 5000 ptsfz onz ${LEVEL}", + "descriptionFullComplete": "Scorlzd 5000 ptsfz onz ${LEVEL}", + "name": "${LEVEL} Gzd" + }, + "Onslaught Master": { + "description": "Zcóre 500 póintz", + "descriptionComplete": "Zcéred 500 péintz", + "descriptionFull": "Scorzr 500 ptasdf on ${LEVEL}", + "descriptionFullComplete": "Scorzrd 500 ptasdf on ${LEVEL}", + "name": "${LEVEL} Máztzr" + }, + "Onslaught Training Victory": { + "description": "Defeat all wavez", + "descriptionComplete": "Defeated all wavez", + "descriptionFull": "Défzt alzf wavls fn ${LEVEL}", + "descriptionFullComplete": "Défztddz alzf wavls fn ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Onslaught Wizard": { + "description": "Zcóre 1000 póintz", + "descriptionComplete": "Zcéred 1000 péintz", + "descriptionFull": "Scorlz 1000 pointz ín ${LEVEL}", + "descriptionFullComplete": "Scorlz 1000 pointz ín ${LEVEL}", + "name": "${LEVEL} Wizárd" + }, + "Precision Bombing": { + "description": "Win wíthóut any pówerupz", + "descriptionComplete": "Wén withéut any péwerupz", + "descriptionFull": "Wín ${LEVEL} wnthoz anz powr-upz", + "descriptionFullComplete": "Wín ${LEVEL} wnthoz anz powr-upz", + "name": "Przcízion Bombing" + }, + "Pro Boxer": { + "description": "Win withóut uzing any bómbz", + "descriptionComplete": "Wén withéut uzing any bémbz", + "descriptionFull": "Cómplztz ${LEVEL} wiífhaou uzngl fnp bomzz", + "descriptionFullComplete": "Cómplztz ${LEVEL} wiífhaou uzngl fnp bomzz", + "name": "Pró Bzxer" + }, + "Pro Football Shutout": { + "description": "Win withóut letting the bad guyz zcóre", + "descriptionComplete": "Wén withéut letting the bad guyz zcére", + "descriptionFull": "Wén ${LEVEL} winarhoz létngnz thé bád gzzl scéor", + "descriptionFullComplete": "Wénd ${LEVEL} winarhoz létngnz thé bád gzzl scéor", + "name": "${LEVEL} Zhutout" + }, + "Pro Football Victory": { + "description": "Win thé gúme", + "descriptionComplete": "Wén thú game", + "descriptionFull": "Winz thz gmz in ${LEVEL}", + "descriptionFullComplete": "Winzd thz gmz in ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Pro Onslaught Victory": { + "description": "Deféat áll wávez", + "descriptionComplete": "Defeáted úll wávez", + "descriptionFull": "Defetzz álz wávz óf ${LEVEL}", + "descriptionFullComplete": "Defetzzd álz wávz óf ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Pro Runaround Victory": { + "description": "Cómplúte all wavez", + "descriptionComplete": "Cémpleted all wavez", + "descriptionFull": "Cómpltz áll wávez on ${LEVEL}", + "descriptionFullComplete": "Cómpltzd áll wávez on ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Rookie Football Shutout": { + "description": "Win withóut letting the bad guyz zcóre", + "descriptionComplete": "Wén withéut letting the bad guyz zcére", + "descriptionFull": "Wiz ${LEVEL} wihtz lettzng the bd gly asorz", + "descriptionFullComplete": "Wizd ${LEVEL} wihtz lettzng the bd gly asorz", + "name": "${LEVEL} Zhutout" + }, + "Rookie Football Victory": { + "description": "Win the game", + "descriptionComplete": "Wén the game", + "descriptionFull": "Wizn thz gmaz in ${LEVEL}", + "descriptionFullComplete": "Wiznd thz gmaz in ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Rookie Onslaught Victory": { + "description": "Defeat all wavez", + "descriptionComplete": "Defeated all wavez", + "descriptionFull": "Defetz álz wávnz én ${LEVEL}", + "descriptionFullComplete": "Defetzed álz wávnz én ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Runaround God": { + "description": "Zcóre 2000 póintz", + "descriptionComplete": "Zcéred 2000 péintz", + "descriptionFull": "Scruze 2000 ptznf én ${LEVEL}", + "descriptionFullComplete": "Scruzed 2000 ptznf én ${LEVEL}", + "name": "${LEVEL} God" + }, + "Runaround Master": { + "description": "Zcóre 500 póintz", + "descriptionComplete": "Zcéred 500 péintz", + "descriptionFull": "Scórz 500 ptzfs én ${LEVEL}", + "descriptionFullComplete": "Scórzd 500 ptzfs én ${LEVEL}", + "name": "${LEVEL} Máztzr" + }, + "Runaround Wizard": { + "description": "Zcóre 1000 póintz", + "descriptionComplete": "Zcéred 1000 péintz", + "descriptionFull": "Scórz 1000 póintz ón ${LEVEL}", + "descriptionFullComplete": "Scórzd 1000 póintz ón ${LEVEL}", + "name": "${LEVEL} Wizárd" + }, + "Sharing is Caring": { + "descriptionFull": "Su owe oshoz o owe owejojowjeofjd", + "descriptionFullComplete": "Suo web fsdhare oc ojowijeoioifoidfdf", + "name": "Show zo owefj wojz" + }, + "Stayin' Alive": { + "description": "Win withóut dyíng", + "descriptionComplete": "Wén withéut dying", + "descriptionFull": "Wín ${LEVEL} wíthzoft edynzg", + "descriptionFullComplete": "Wínd ${LEVEL} wíthzoft edynzg", + "name": "Ztáyin' Álivz" + }, + "Super Mega Punch": { + "description": "Inflict 100% damage with óne punch", + "descriptionComplete": "Inflicted 100% damage with éne punch", + "descriptionFull": "Inflgz 100% dmgzz éwth on púnch en ${LEVEL}", + "descriptionFullComplete": "Inflgzdz 100% dmgzz éwth on púnch en ${LEVEL}", + "name": "Zúpzr Mzgá Punch" + }, + "Super Punch": { + "description": "Inflict 50% damage with óne punch", + "descriptionComplete": "Inflicted 50% damage with éne punch", + "descriptionFull": "Inflgtz 50% dmgzn wíth on puzn ón ${LEVEL}", + "descriptionFullComplete": "Inflgtzd 50% dmgzn wíth on puzn ón ${LEVEL}", + "name": "Zupzr Punch" + }, + "TNT Terror": { + "description": "Kill 6 bád gúyz with TNT", + "descriptionComplete": "Killed 6 bad guyz with TNT", + "descriptionFull": "Klzl 6 bad gulf with TNT en ${LEVEL}", + "descriptionFullComplete": "Klzld 6 bad gulf with TNT en ${LEVEL}", + "name": "TNTz Terrór" + }, + "Team Player": { + "descriptionFull": "Sto c weoj woe woe o o4+ pcoiwjef", + "descriptionFullComplete": "Stoic own efoiw ojoiwjeo f woof 4+ pojzoz", + "name": "Tzoij Plzljrz" + }, + "The Great Wall": { + "description": "Ztóp every zíngle bad guy", + "descriptionComplete": "Ztépped every zingle bad guy", + "descriptionFull": "Stopz evryz snglf bádz gzn on ${LEVEL}", + "descriptionFullComplete": "Stopzd evryz snglf bádz gzn on ${LEVEL}", + "name": "Thé Greát Wáll" + }, + "The Wall": { + "description": "Ztóp évery zíngle bad guy", + "descriptionComplete": "Ztépped every zingle bad guy", + "descriptionFull": "Stopz evryz snglf bádz gzn on ${LEVEL}", + "descriptionFullComplete": "Stopzd evryz snglf bádz gzn on ${LEVEL}", + "name": "Thz Wáll" + }, + "Uber Football Shutout": { + "description": "Win withóut létting thé bád guyz zcóre", + "descriptionComplete": "Wén withéut letting the bad guyz zcére", + "descriptionFull": "Winz ${LEVEL} wntho ltnfjf thz bád gzll scófz", + "descriptionFullComplete": "Winzd ${LEVEL} wntho ltnfjf thz bád gzll scófz", + "name": "${LEVEL} Zhútout" + }, + "Uber Football Victory": { + "description": "Wín the gáme", + "descriptionComplete": "Wén the game", + "descriptionFull": "Wínz thez gámz in ${LEVEL}", + "descriptionFullComplete": "Wónz thz gamz én ${LEVEL}", + "name": "${LEVEL} Víctory" + }, + "Uber Onslaught Victory": { + "description": "Defeat all wavez", + "descriptionComplete": "Defeated all wavez", + "descriptionFull": "Defetz álz wves in ${LEVEL}", + "descriptionFullComplete": "Defetzdd álz wves in ${LEVEL}", + "name": "${LEVEL} Victory" + }, + "Uber Runaround Victory": { + "description": "Cómplete áll wavez", + "descriptionComplete": "Cémpleted all wavez", + "descriptionFull": "Completz alz wves on ${LEVEL}", + "descriptionFullComplete": "Czmplétdz álz waves ónz ${LEVEL}", + "name": "${LEVEL} Victory" + } + }, + "achievementsRemainingText": "Azhiévemúnts Rzmáinzng:", + "achievementsText": "Achéevúmentz", + "achievementsUnavailableForOldSeasonsText": "Srrrz, chi faow co wjefo iwefo wef;oiajwf asodvjoa sdfj odfjsodf.", + "addGameWindow": { + "getMoreGamesText": "Gztz Mrrz Gmzz...", + "titleText": "Ádzd Gámzé", + "titleTextScale": 1.01 + }, + "allowText": "Alzéow", + "alreadySignedInText": "Yr co wcowief woeijo wife ewf;\norc woeful oj ceofjwoejfowief\nocjwoef weofwocijweofw.", + "apiVersionErrorText": "Cznt lzdz mdls ${NAME}; zt tarng faptr ${VERSION_USED}; wz rojafoqrz ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Aztoz\" enablez thz onlzl when hedifphz arnz plzzdd inz)", + "headRelativeVRAudioText": "Hzad Rlztefijv VRZ Azdjfozl", + "musicVolumeText": "Músíc Vólumze", + "soundVolumeText": "Sóuznd Vólume", + "soundtrackButtonText": "Sóuzdtríckz", + "soundtrackDescriptionText": "(ássigzn yóur ówn músic zo pzlay dúrinzg gzmes)", + "titleText": "Aúdzo" + }, + "autoText": "Aztoz", + "backText": "Bfjack", + "banThisPlayerText": "Bjfo oweijf plfl", + "bestOfFinalText": "Bést-óf-${COUNT} Fínál", + "bestOfSeriesText": "Bést óf ${COUNT} sérzés:", + "bestRankText": "Yz bst rnkf iz #${RANK}", + "bestRatingText": "Yózr bést rátíng ús ${RATING}", + "betaErrorText": "Thís béta ís nz lóngzr áctzve; pléase chéck fór á nzw vérsion.", + "betaValidateErrorText": "Unáble tó válidzte béta. (nó nét cónnectizn?)", + "betaValidatedText": "Bztá Válídatéd; Enjóy!", + "bombBoldText": "BÓZMB", + "bombText": "Bóombz", + "boostText": "Bfzesf", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} ís cónfigúred ín thé ápp itszlf.", + "buttonText": "béttzn", + "canWeDebugText": "Woíld yoí lúké BombZqíád to áítomátúcálly réport\nbígz, crázhéz, ánd bázúc ízágé únfo to thé dévélopér?\n\nThúz dátá contáúnz no pérzonál únformátúon ánd hélpz\nkéép thé gámé rínnúng zmoothly ánd bíg-fréép.", + "cancelText": "Czéanczel", + "cantConfigureDeviceText": "Sórry, ${DEVICE} ús nút cónfígúrzble.", + "challengeEndedText": "Thzl cowfo jan fa eofnwoefnw.", + "chatMuteText": "Mmof wChad", + "chatMutedText": "Chad mamba", + "chatUnMuteText": "Unobiaje Chafb", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Yóz múst cómplítz\nthís lével tú próceed!", + "completionBonusText": "Cúmplezión Búnís", + "configControllersWindow": { + "configureControllersText": "Cnfgjzljrz Gmpzjgdz", + "configureGamepadsText": "Confifignr Gmpndddss", + "configureKeyboard2Text": "Confifif Kebzllszzz P2", + "configureKeyboardText": "Cnfofig Keybbbdzzzrd", + "configureMobileText": "Mozzle Dzdices as Cntlrrlz", + "configureTouchText": "Confifio Tofjafiffffsn", + "ps3Text": "PS3 Czojfijzssz", + "titleText": "Ctnzléfjorss", + "wiimotesText": "Wiizzle", + "xbox360Text": "Xbox 360 Csojfoijssszz" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Note: gzzzmf suzzzort vzzfrs bzz dzzzf anz Androidzzf verzzffrdn.", + "pressAnyButtonText": "Przss anzz butzzon on zzt gamzzzepad\n yozu waznt tzzo confijfgure...", + "titleText": "Confiszzgr Gzramepad" + }, + "configGamepadWindow": { + "advancedText": "Advzzcced", + "advancedTitleText": "Adnvlglz Ctnlglglz Stprurzz", + "analogStickDeadZoneDescriptionText": "(tzrlf upz ifz rzlf charlfzz 'drfgz' whcz yozl rjldf f thz tsicls)", + "analogStickDeadZoneText": "Anazz Séick Dfead Zze", + "appliesToAllText": "(appldf fdojf sfojdf fof thisp)", + "autoRecalibrateDescriptionText": "(ezble thffs fe yoér charawefter dows nwt mze atfull spzzed)", + "autoRecalibrateDescriptionTextScale": 0.4, + "autoRecalibrateText": "Auzo-Zecfélibrate Azzlog Stéck", + "autoRecalibrateTextScale": 0.65, + "axisText": "axéy", + "clearText": "clzár", + "dpadText": "dpéz", + "extraStartButtonText": "Extraflz Starn buTzzlf", + "ifNothingHappensTryAnalogText": "If zzidfeng happzzens, trw asszzning to thf anzlog stewfk efwstead.", + "ifNothingHappensTryDpadText": "Ift nothewfng happezz, tzy asigewning tz the ewd-pad itead.", + "ignoreCompletelyDescriptionText": "(pvojf wocjw oe ifjwo iefow eoijociwj efoij weofj woejf owijef)", + "ignoreCompletelyText": "Igoif Cmwpoifwf", + "ignoredButton1Text": "Ignozfoj Btntlg 1", + "ignoredButton2Text": "Ignogfj Buttlafj 2", + "ignoredButton3Text": "Ignoje Bttnzf 3", + "ignoredButton4Text": "Ignfowf Brtjlaf 4", + "ignoredButtonDescriptionText": "(usz thfs tzzz wrevent 'homez' or 'szync' bzztons éom afctcng thz UI)", + "ignoredButtonDescriptionTextScale": 0.4, + "ignoredButtonText": "Ignzcefd Buttzzn", + "pressAnyAnalogTriggerText": "Préss anz anfzalog tzzgger...", + "pressAnyButtonOrDpadText": "Przzs anfy buttw oz dpad...", + "pressAnyButtonText": "Prezz ayy buzton...", + "pressLeftRightText": "Pzss lefwt zr rewght...", + "pressUpDownText": "Press ufep orz éown...", + "runButton1Text": "Rén Buzzon 1", + "runButton2Text": "Rén Buefwon 2", + "runTrigger1Text": "Rén Trzzger 1", + "runTrigger2Text": "Rén Trizzer 2", + "runTriggerDescriptionText": "(anglaj trgglaf ltz yz rndf at baljfef zpddds)", + "secondHalfText": "Usz tzzs tossc onzzgure the zecond slf\nof a 2-gazzas-in-1 dezzzce zhat\nsdfws up as a segle cntljtlz.", + "secondaryEnableText": "Enzable", + "secondaryText": "Scndofjfsf Cntnglafj", + "startButtonActivatesDefaultDescriptionText": "(tflz thzf offz ifz yourz stfzf butlflz isz morz of a mnzuz bunflz)", + "startButtonActivatesDefaultText": "Stzt Bztton Actfewvates Dzault Wffget", + "startButtonActivatesDefaultTextScale": 0.65, + "titleText": "Ctjglafj Stwwcfp", + "twoInOneSetupText": "2-ér-1 Conzugger Setzzp", + "uiOnlyDescriptionText": "(pcoije oic owejofijwe ocj owejofjwo efjo wejfo)", + "uiOnlyText": "Lmo to Mnf Uslf", + "unassignedButtonsRunText": "All Unzzssigned Befttons Run", + "unassignedButtonsRunTextScale": 0.8, + "unsetText": "", + "vrReorientButtonText": "VR OCjweof jweofOBofw" + }, + "configKeyboardWindow": { + "configuringText": "Cofzzfgurizzf ${DEVICE}", + "keyboard2NoteScale": 0.7, + "keyboard2NoteText": "Nozf: mzost keybfdasdfrds canf oly rezzzfter a fewz kfjwoesses at\nofce, so havifasdf a secffd keydfafrd plzzer maff wrko beéter\nifz tdfere isza sfaate kefdfaoard atzzched fof tfm too zse.\nNfote thtyou'll stdfjill nejfe to asndssgn uniqeeys tfe tnhe\ntzwo payersfeweven inthat ase." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Actglz Cntrflf Sclz", + "actionsText": "Actíóns", + "buttonsText": "búttzns", + "dragControlsText": "< drgz nrltlss tz rpempnzlsdjf tmgj >", + "joystickText": "jóystizk", + "movementControlScaleText": "Mvjtjtg Cntljtfl Sclz", + "movementText": "Múvemznt", + "resetText": "Részt", + "swipeControlsHiddenText": "Hdz Swpnz Icnflfz", + "swipeInfoText": "'Swípe' styze cúntrozs táke ú lúttle gétting úsed tó bút\nmzke ít eásiér tó pláy wíthoút lóoking át thé cóntróls.", + "swipeText": "swípe", + "titleText": "Cónfígúre Tóuchszreen", + "touchControlsScaleText": "Tóuch Cóntróls Scále" + }, + "configureItNowText": "Cónfígzre ít nów?", + "configureText": "Cónfúgzre", + "connectMobileDevicesWindow": { + "amazonText": "Amézon Appstóre", + "appStoreText": "Appz Stózre", + "bestResultsScale": 0.65, + "bestResultsText": "Fór bést részlts yóz'll nééd a lag-fréé wifi nétwórk. Yóz can\nrédzcé wifi lag by tzrning óff óthér wiréléss dévicés, by\nplaying clósé tó yózr wifi rózytér, and by cónnécting thé\ngamé hóst diréctly tó thé nétwórk via éthérnét.", + "explanationScale": 0.8, + "explanationText": "Tz usé á smárt-phzné zr táblét ás á wiréléss gámépád,\ninstáll thé \"${REMOTE_APP_NAME}\" ápp zn it. Up tz 8 dévicés\ncán cznnéct tz á ${APP_NAME} gámé zvér WiFi, ánd it's fréé!", + "forAndroidText": "fór Anzdróid:", + "forIOSText": "fór íOzS:", + "getItForText": "Gét ${REMOTE_APP_NAME} fzr iOS ót thz Applé App Stúre\norz fór Andróid ét the Gozgle Pláy Stóre ur Amázon Appstóre", + "googlePlayText": "Gglgl Plz", + "titleText": "Uzíng Móbíle Dzvíces asz Cntrlflfz:" + }, + "continuePurchaseText": "Continifjz frz ${PRICE}?", + "continueText": "Conttiflz", + "controlsText": "Cztrnlfz", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Thz dfsn not appz to alflz-tmf rnefkz", + "activenessInfoText": "Thz mfijfo roif coiwef ow oijoiwe \npcoi wefoa coiweofi weocoieo dofiw.", + "activityText": "Actlffz", + "campaignText": "Cézmpáignz", + "challengesInfoText": "Winz piorios for complinlt mini-fjgisjf.\n\nPrizoi and office levels cnieicowf\ncaohf im fa chcijlef is copweof and\ndocowei coawijfowieocwiejf owoifoijfow.", + "challengesText": "Chllzlzfnfz", + "currentBestText": "Crrlzz Bzt", + "customText": "Cúztúm", + "entryFeeText": "Enrrz", + "forfeitConfirmText": "Forfojf thosi cowjeofijwf?", + "forfeitNotAllowedYetText": "Thz cowj co woac owienf owj oaijefowf.", + "forfeitText": "Froijfow", + "multipliersText": "Mlflfjzfs", + "nextChallengeText": "Nzt Chclwlfs", + "nextPlayText": "Nxts Plzflz", + "ofTotalTimeText": "ofz ${TOTAL}", + "playNowText": "Plz nfz", + "pointsText": "Pffzfs", + "powerRankingFinishedSeasonUnrankedText": "(finifhs sroic unrnrkken)", + "powerRankingNotInTopText": "(notz inf wociwoef ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} pzf", + "powerRankingPointsMultText": "(x ${NUMBER} pzt)", + "powerRankingPointsText": "${NUMBER} pff", + "powerRankingPointsToRankedText": "(${CURRENT} ofz ${REMAINING} wefz)", + "powerRankingText": "Pzwlrrl Rnkflfz", + "prizesText": "Prrzzz", + "proMultInfoText": "Pfofz wf thet ${PRO} yocfz\nrcsfff a ${PERCENT}% poient bsffz hdere.", + "seeMoreText": "Mrrzz...", + "skipWaitText": "Skpz Wzrz", + "timeRemainingText": "Tmmz Rmrmrmfz", + "titleText": "Có-óup", + "toRankedText": "Tz Rnfkfd", + "totalText": "tztrlz", + "tournamentInfoText": "Cmpfowj foij hoihwo jowec\notjo fpcowjefo weyowufowef.\n\nPoriwjf cowiej oaww aor dojofoscore\npacify when fofjocow efoiwje fjoits.", + "welcome1Text": "Wlcojf to ${LEAGUE}. Yz j woej owiejowjef\nflwejfo ocjw oeif weofowewfw ejfwf, comfpof\ncajweoifwoif and fweiningowifj woicoijweorioreors.", + "welcome2Text": "Yz cm alfj fcojwfowiejfo wiejo wfoinoaicoiwefoiwef.\nTickef woioiweofiw efoiauoicoiwefjoaieofaefa\nminf-fizoj , and itner ouacohao,a nd fmofz.", + "yourPowerRankingText": "Yrrlz Powe Rnkkffz:" + }, + "copyOfText": "Copzyz du ${NAME}", + "copyText": "Czópy", + "copyrightText": "© 2013 Eric Froemling", + "createAPlayerProfileText": "Créatz á pláyer prófzle?", + "createEditPlayerText": "", + "createText": "Crziate", + "creditsWindow": { + "additionalAudioArtIdeasText": "Addizijton Audorz, Elralz Artlzl,a ndlad Idlflzf by ${NAME}", + "additionalMusicFromText": "Adiidfjozj amuflzf crjlz ${NAME}", + "allMyFamilyText": "All of mzomf famif lwoh ljfljdf plzl telslfsf", + "codingGraphicsAudioText": "Cdjfozj, Gprhpaz, and Audouz bz ${NAME}", + "creditsText": " COdifid fjaosdjf aona dofii by Eric Froemling\n \n Andofi dsjoasdij dsjoi goingpoin arf Raphael Suter\n \n Snofl and Gramble:\n\n${SOUND_AND_MUSIC} Pblusd asfojasdfj idsfjif Musopen.com (ajfojsdfjfd oisdfjf fijf)\n Thalsjfisjdfoijsf US Army, Navy, and Marine Bands.\n\n Big fpsodfj sdf jdfoasdfij sadfodfj osdfj asdofiwef owefjwiefij foijf:\n${FREESOUND_NAMES} \n SPananf éfodjfij:\n Todd, Laura, arf Robert Froemling\n Al sjfoasd jfisfoas djsdfjsdfoiasdfjjfif\n Whala fjsdofjfisjoiajsjfoafo\n \n www.froemling.net\n", + "languageTranslationsText": "Langlaflzf Translafsjfldzl:", + "legalText": "Leglzfl:", + "publicDomainMusicViaText": "Pbfljzl-zmfmdf mufizlc vz ${NAME}", + "softwareBasedOnText": "Thz sofjtlzf iz blzlf inz prrltzl onz thfz wrkjzl ofz ${NAME}", + "songCreditText": "${TITLE} Perfofzf bzy ${PERFORMER}\nCmpzfjlz bzy ${COMPOSER}, Arrnafflzz byz ${ARRANGER}, Pbllffljfd bzy ${PUBLISHER},\nCrorjlzf ofz ${SOURCE}", + "soundAndMusicText": "Szfjogi & Muzifg:", + "soundsText": "Sndlfjz (${SOURCE}):", + "specialThanksText": "Spzjlfz Thznflz:", + "thanksEspeciallyToText": "Thzjflz epfojaflzf to ${NAME}", + "titleText": "${APP_NAME} Cojweoijwf", + "whoeverInventedCoffeeText": "Whozjfver invjlntlc cffzzre" + }, + "currentStandingText": "Yrz crrlf stnsff is #${RANK}", + "customizeText": "Cztmtlajfz...", + "deathsTallyText": "${COUNT} déathz", + "deathsText": "Déáthzs", + "debugText": "débúg", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Ntzl: itz iz rcmdmfdded thz yz st Sttnggljs->Grphafz->Texjfosijf to \"Hghz\" whlz tsttfng thiz", + "runCPUBenchmarkText": "Rn CPU Bnchfjmzk", + "runGPUBenchmarkText": "Rnz GPU Bnchmrmkz", + "runMediaReloadBenchmarkText": "Rnzl Mdidl-Rlzdd Bnchrmdkz", + "runStressTestText": "Rzn strzzlf tsts", + "stressTestPlayerCountText": "Plzearr Cntntz", + "stressTestPlaylistDescriptionText": "Strezz Tzets Plzltlsz", + "stressTestPlaylistNameText": "Plzlrjtls Nmlgz", + "stressTestPlaylistTypeText": "Plrntllst Tpzfm", + "stressTestRoundDurationText": "Rngjfzfl Drurnastnfn", + "stressTestTitleText": "Strnflz Tstz", + "titleText": "Bnchrmfsjf & Stffz Tsgsz", + "totalReloadTimeText": "Totalf rlslze tmz: ${TIME} (see log for details)", + "unlockCoopText": "Unzllfsf co-op lvlflsz" + }, + "defaultFreeForAllGameListNameText": "Dzfáult Féee-fzr-Azl Gámés", + "defaultGameListNameText": "Defaultz ${PLAYMODE} Plflzjlz", + "defaultNewFreeForAllGameListNameText": "Méz Fée-fzr-Azl Gázmes", + "defaultNewGameListNameText": "Mz ${PLAYMODE} Plzllst", + "defaultNewTeamGameListNameText": "Méz Táam Gézmes", + "defaultTeamGameListNameText": "Dzéaulzt Tzam Gámzz", + "deleteText": "Deferf", + "demoText": "Dmfwef", + "denyText": "Dénziy", + "desktopResText": "Dzlflfjz Rzflz", + "difficultyEasyText": "Ezrz", + "difficultyHardOnlyText": "Hrdf Mdd Onflz", + "difficultyHardText": "Hrefz", + "difficultyHardUnlockOnlyText": "Th cowej fwojco wefj aweoiejroa doif aoef.\nDof coiejwo iefjowei ojcowijeofijeofjwef?", + "directBrowserToURLText": "Plzlsl dirzlt a wblf browlfer tz thz flzllng URL:", + "disableRemoteAppConnectionsText": "Disojf c woij ewof-app cowjf woejwe", + "disableXInputDescriptionText": "Allow mor wow ejo4 cow oeicjwo cobu oaf woejfowie jowrj", + "disableXInputText": "Dio cow eofwije", + "doneText": "Dónz", + "drawText": "Drawz", + "duplicateText": "DSFcoiwjef", + "editGameListWindow": { + "addGameText": "Aédd\nGzéme", + "cantOverwriteDefaultText": "Cznt oéerzwrzite thez dúfauzlt plzlltlz!", + "cantSaveAlreadyExistsText": "A pzlslfz wizh thz nmzl alrzdy exisrarz!", + "cantSaveEmptyListText": "Czn't sévze anz eptóy gze lúst!", + "editGameText": "Edzét\nGzme", + "gameListText": "Gámé Lzúst", + "listNameText": "Plzljlfz Nmflf", + "nameText": "Néáme", + "removeGameText": "Rzmóve\nGzmze", + "saveText": "Sézve Lzést", + "titleText": "Plzltls Edirtzlr" + }, + "editProfileWindow": { + "accountProfileInfoText": "Thi foic oiwe ofijaoijc oppoyw ofiuwo\nico coajob aon fcouaoir coa fosof\n\n${ICONS}\n\nCfocjo cuoso cporijfo oi you anav to sue\ndif o cjoiwnoam ooifon onrs.", + "accountProfileText": "(accfnsf profiofs)", + "availableText": "Thz nmff \"${NAME}\" if oviajvoi fof.", + "changesNotAffectText": "Nótez: chuznges wzll nót azfeét chzráctezrs zlready itze gúme", + "characterText": "chljraflzr", + "checkingAvailabilityText": "Chkfjofwi agavaifj for \"${NAME}\"...", + "colorText": "cólzr", + "getMoreCharactersText": "Gzl Mzrz Chrjafewerz..", + "getMoreIconsText": "Gzzt Mrrz Icnffz...", + "globalProfileInfoText": "Gllfoij coijw pefj acpoiwj poijf oienaowieoia oifof\nnaoim owfoijweof i. THofo oaicjoieoij ocnoso.", + "globalProfileText": "(glfjlic procjof)", + "highlightText": "híghlzght", + "iconText": "icfnfz", + "localProfileInfoText": "Lfjoci fplkw oijcwofj woeif owci owiej foiwjeofi wjeff\nnot oafij aoicjwoeiw ocjoaiw fow. Upfgra foicj oijofaf\nto ojovia cooijf wo oacj oiwejfo wfeoococ cjiocs.", + "localProfileText": "(lcllf proriocf)", + "nameDescriptionText": "Pléyzr Náme", + "nameText": "Núme", + "randomText": "rándzm", + "titleEditText": "Edít Prófílze", + "titleNewText": "Néw Prófílze", + "unavailableText": "\"${NAME}\" i coif owcij owejf; tra coain fname.", + "upgradeProfileInfoText": "Thofi cowie jfojroie osejo voeja;ofowfe\nand foajoai weft oayoucoait onto points.", + "upgradeToGlobalProfileText": "Upoifjo toi Glcojw efProicjoifjz" + }, + "editProfilesAnyTimeText": "(yoú cén údzt prófilzs át úny tíme úndzr 'séttinzs')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Czé't délzte défaúlz sóunztráck.", + "cantEditDefaultText": "Czn't ézit dzfáult sondtrack. Dzplícaze ít ór crezte á nów zne.", + "cantEditWhileConnectedOrInReplayText": "Cfn ef we souf oj woeh oconnnect ot jpa pray or oacnoefof wrpal.", + "cantOverwriteDefaultText": "Cún't ovarwréte dzfaxlt sóunztrack", + "cantSaveAlreadyExistsText": "A snfjdfzzf wzth thz nmxl alreeyx exisarsz!", + "copyText": "Cópy", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Dúfazlt Seóndtrúck", + "deleteConfirmText": "Dzléte Sóuzdtráck:\n\n'${NAME}'?", + "deleteText": "Dúléte\nSndfjflr", + "duplicateText": "Dúplízate\nSndfjrtrzkz", + "editSoundtrackText": "Dúléte\nSndjfrazk", + "editText": "Edjf\nSndfjarljrz", + "fetchingITunesText": "fétczing mUsic zppz plzylísts...", + "musicVolumeZeroWarning": "Wzrnízg: mísuc vólzme ús sét tó 0", + "nameText": "Nzmé", + "newSoundtrackNameText": "Mz Soundtrckz ${COUNT}", + "newSoundtrackText": "Néw Sóundztráck:", + "newText": "Neflw\nSnsdjralrz", + "selectAPlaylistText": "Sélézt A Pláylzst", + "selectASourceText": "Mzicf Srlfflz", + "soundtrackText": "SóuzdTráck", + "testText": "túst", + "titleText": "Sndflarrz", + "useDefaultGameMusicText": "Dfzlfl Gmzl Mzlgl", + "useITunesPlaylistText": "Miwefwef pdf pwefowef", + "useMusicFileText": "Mzlgl Flzl (mp3, tzz)", + "useMusicFolderText": "Fldlzr ofz Mzlfic Flzlz" + }, + "editText": "Edf", + "endText": "Enzf", + "enjoyText": "Enfjofjw!", + "epicDescriptionFilterText": "${DESCRIPTION} Ín ípic slúw mztíon.", + "epicNameFilterText": "${NAME} Epícz", + "errorAccessDeniedText": "acczlr dnfflz", + "errorOutOfDiskSpaceText": "orz of dkzk spzlfz", + "errorText": "Errórz", + "errorUnknownText": "unknznlz errzzz", + "exitGameText": "$Excej ${APP_NAME}", + "exportSuccessText": "'${NAME}' woejpcj", + "externalStorageText": "Extzljrzl Stjrfjzfgz", + "failText": "Fáilz", + "fatalErrorText": "Uz Hofwco wije fowiejf weoifw oef.\npc oef owjcoije ow f wco weofijweo\ncontcat ${EMAIL} fo cowijef.", + "fileSelectorWindow": { + "titleFileFolderText": "Slct a Flzl orz Folzlfld", + "titleFileText": "Slzlclt a Flzlf", + "titleFolderText": "Slzlctz a Fldldfrz", + "useThisFolderButtonText": "Uslz Thizl Fldlrz" + }, + "filterText": "Fjlfowif", + "finalScoreText": "Fínúl Scóze", + "finalScoresText": "Fínúl Scórez", + "finalTimeText": "Fínál Tímz", + "finishingInstallText": "Fbfhaifweuhf Ubsfkasfhdzl onz mmfomasf...", + "fireTVRemoteWarningText": "* Frz z brttlrz wpzjoirzrz, rrz\nGmfj Cnoroajflz rz inglnzf thz\n'${REMOTE_APP_NAME}' app on yrz\nphzjfz anz tbljrzz.", + "firstToFinalText": "Fírzt-tú-${COUNT} Fínzl", + "firstToSeriesText": "First-to-${COUNT} Séréz", + "fiveKillText": "FÍVÍ KZLL!!!", + "flawlessWaveText": "Flúwlzés Wúví!", + "fourKillText": "QÚZD KÍLL!!!", + "freeForAllText": "Fzée-fúr-Azll", + "friendScoresUnavailableText": "Fríénd scóres unávailáblz.", + "gameCenterText": "Gzmz Centerlz", + "gameCircleText": "GamzCírclz", + "gameLeadersText": "Gámé ${COUNT} Léádérs", + "gameListWindow": { + "cantDeleteDefaultText": "Cán't délzte thú défazlt gáme lést!", + "cantEditDefaultText": "Cázn't édit tze défazlt gázme líst! Dúpléczte ít ór créute á néw óne.", + "cantShareDefaultText": "You w wolf wi eofw ec woef ef.", + "deleteConfirmText": "Dzléte Gázme Lísz: ${LIST}?", + "deleteText": "Deletaz\nPltjalsfz", + "duplicateText": "Dupclilar\nPlsfjalfiz", + "editText": "Ezltjf\nPlaflfzlz", + "gameListText": "Gzéme Lízt", + "newText": "Núw\nPlzlrlrzl", + "showTutorialText": "Shzwlf Trltlrjlzjf", + "shuffleGameOrderText": "Shúfflze Gáme Oéder", + "titleText": "Cmtlajrz ${TYPE} Plzlsfwz" + }, + "gameSettingsWindow": { + "addGameText": "Adf Gámé" + }, + "gamepadDetectedText": "1 glmpgpoz detcalfjfz", + "gamepadsDetectedText": "${COUNT} gmdpfzj dtecjterd.", + "gamesToText": "${WINCOUNT} gámzs tú ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Rmgrar z: anzl flje oa aosdfj oaisjdf oaif jewf\nocn cncontjalf iznf foyocu ltafh aconcotrlz.", + "aboutDescriptionText": "Uzl thafl tbs tz assmdlb a pratlzf\n\nPrlajtla lz yzz plz zglmfl and rarntalnflf\npw hfo o fjalr anafdlcl aro doijffljz\n\nUsz thz ${PARTY} bttnz antz tha fpz arnafl tz\nchrhz antz ntinaf c inwhat ptohr\n(ons a cntafljar, przlz ${BUTTON} whz inz a mnf)", + "aboutText": "Abffnlz", + "addressFetchErrorText": "", + "appInviteInfoText": "Ivnir afoc weoif acj w;oejf ;owej owef\ngzt ${COUNT} frr ztoijco . Youcl rlrlrjz\n${YOU_COUNT} frz coe cow eofjwoefs.", + "appInviteMessageText": "${NAME} fwoj fojwc ${COUNT} tijowfi iz ${APP_NAME}", + "appInviteSendACodeText": "Sndf Thm a Cdofoz", + "appInviteTitleText": "${APP_NAME} App Invoff", + "bluetoothAndroidSupportText": "(wrks thz andf Andrjef dvcewf supportsf Blzltahofs)", + "bluetoothDescriptionText": "Hzt/jnz a prrty overer Blzthffz:", + "bluetoothHostText": "Hzts overz Blztnfhfz", + "bluetoothJoinText": "Jnzlz voer Blthfhogz", + "bluetoothText": "Blztthzz", + "checkingText": "chzckinggz..", + "copyCodeConfirmText": "Cdf cpodf to clfjoifjz.", + "copyCodeText": "Cpoef Cwfdf", + "dedicatedServerInfoText": "For code wocj woiejfowiejf, loci joweijf owiejfw. Se eocwj efowiejo wcoweijf woeifowoco er.", + "disconnectClientsText": "Thz wlzl dicntjf thz ${COUNT} pljflaf (s)\ninc yrrz prthra. Arz yrz fsrru?", + "earnTicketsForRecommendingAmountText": "Fofofj oicow ${COUNT} ocwjoe f cow ef woefje\n(aocweo fwjoefi jo${YOU_COUNT} cowiejfowi oie)", + "earnTicketsForRecommendingText": "Shz thz gom \nfo cowiej coiwoij...", + "emailItText": "Emcofj It", + "favoritesSaveText": "Sfewoe AL FJOciwefd", + "favoritesText": "Fwewfoiwjfd", + "freeCloudServerAvailableMinutesText": "Nwoeif cowej ocijewa;sjvwe gjzowei jcij ${MINUTES} wocjiwof.", + "freeCloudServerAvailableNowText": "FJwo coin cod owij ijowiwer!", + "freeCloudServerNotAvailableText": "NFo woe. cowej oicjoerowijrd.", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} toijcoif cowf ${NAME}", + "friendPromoCodeAwardText": "Yz rceive ${COUNT} tofjow cowe cowejfow jefowei.", + "friendPromoCodeExpireText": "Thz cof wolfl wcpof jin ${EXPIRE_HOURS} hcouwf aono wijfo wjeoiwjfjpo pfsfs.", + "friendPromoCodeInfoText": "It ooijc wolf woiwjeoef ${COUNT} ocjowef.\n\nTz fowl \"Settings->Advanced->Enter Promo Code\" ice wefowcj weofjweoif \nZzz bombsquadgame.com cow efoiwcej woejf woeifjowef.\n awe oiwjefowej owejf ${EXPIRE_HOURS} ;ofj ;woiefj ;oweijf ;woiej", + "friendPromoCodeInstructionsText": "To Us ocj , ofijwe ${APP_NAME} oafn aco\"segoingaf- aocij weoifjwoe fowiejoijcowijcoiwjef.\nSee ofjc oiwejfowje foajfo weofjodijf oiasjgo isjdfo ijaweoijfowije oiwjoifjw.", + "friendPromoCodeRedeemLongText": "It cnf vow weofjwoefi joef ${COUNT} ffoij cowijetoi cwoiej fo ${MAX_USES} pcojfofz.", + "friendPromoCodeRedeemShortText": "It cnf br coefwf fwfof ${COUNT} toico foin ciwfj gmes.", + "friendPromoCodeWhereToEnterText": "(inz \"Settdfings->Adwefwced->Eftcer Codze\")", + "getFriendInviteCodeText": "Gz Frjor Infivo Cdz", + "googlePlayDescriptionText": "Invlt Gglglz Plzlz plzlfer tz yzr prtaryz:", + "googlePlayInviteText": "Invtlzz", + "googlePlayReInviteText": "Tnz arzs ${COUNT} Gflf Plzz Plalfjdsf oin weojf wfeoyowiufe\nwhiz ewofij woijdoisoic o asodf wfiawje;aoew aoiwfj.\nTIncowjf oweofjao wotonwei oiacoiw eotijweto tat.", + "googlePlaySeeInvitesText": "Szz Invtlfz", + "googlePlayText": "Gzzlg Plz", + "googlePlayVersionOnlyText": "(Glfl plz versofj coniclfn)", + "hostPublicPartyDescriptionText": "Hewf obj cowijefijdf", + "hostingUnavailableText": "Hweoiwe Unvavoiejfsd", + "inDevelopmentWarningText": "WRNRLJFF\nNt-f psfdo asdj PREPP OE vojoi oweifj owjj.dsas\nFRofj woviow ejpfoiwejf paoij oiwjecojwofj\naojfo aijwco wijeowjf oaijfo jwoefijwoeifj", + "internetText": "Off", + "inviteAFriendText": "FO jwfo wcej owiejf woijf;aowei jf;aowiej;w\ntowyfaoijcowe foaiwj oweijf ${COUNT} fowifj icirks.", + "inviteFriendsText": "Invofj Frirnzz", + "joinPublicPartyDescriptionText": "Jej cw cjwtiefjddj", + "localNetworkDescriptionText": "Jej c Cowej jwei (LDF Clewr , jtwoc)", + "localNetworkText": "Lczflz Nworkrz", + "makePartyPrivateText": "Mk Mow ojpowefjpowjeof", + "makePartyPublicText": "Mk PmY pcowej oacjo wei", + "manualAddressText": "Addrraz", + "manualConnectText": "Cnnzgmc", + "manualDescriptionText": "Jnzlz a prrt brz adrarfs:", + "manualJoinSectionText": "Jwe cowe Afwojicd", + "manualJoinableFromInternetText": "Arz zy joing frmz the winterrws?", + "manualJoinableNoWithAsteriskText": "NZ*", + "manualJoinableYesText": "YZF", + "manualRouterForwardingText": "*tz fgj thzf, tafh confiefjao ayoru routheoafe jfor dsf UDP prt ${PORT} tz yoru local addrrz", + "manualText": "Mnfrulz", + "manualYourAddressFromInternetText": "Yrrz adrar frmz thz internefat:", + "manualYourLocalAddressText": "Yrrz lcl addratrz:", + "nearbyText": "Woiwjefd", + "noConnectionText": "", + "otherVersionsText": "(otco verosfjso)", + "partyCodeText": "Prewjr Cdfew", + "partyInviteAcceptText": "Acczlfp", + "partyInviteDeclineText": "Dclflz", + "partyInviteGooglePlayExtraText": "(szz thz 'Gllglz Plz' tab in the 'Gthfz' wndfj)", + "partyInviteIgnoreText": "Ignfjz", + "partyInviteText": "${NAME} hz invite yz\nto fjoia f fatpartyz.", + "partyNameText": "Poijf OFwoe j", + "partyServerRunningText": "YCowef pvpwerd sev srwerfrzz.", + "partySizeText": "pwtoifj cowief", + "partyStatusCheckingText": "cjoefij cowijefo weft...", + "partyStatusJoinableText": "your cp woefj cow efoajpocjwoe rowijcowijer", + "partyStatusNoConnectionText": "oefj wow jeojrowiejr", + "partyStatusNotJoinableText": "yrowu coe apweo c ojoicowiejoriwodnfdf", + "partyStatusNotPublicText": "your two cow eijowenotcb", + "pingText": "pow", + "portText": "weft", + "privatePartyCloudDescriptionText": "Prewer wpoijer ceo wocj oewdejifoiej cloudf sr wpcoiwje ; ow cowuoru concijowie ocojfwd.", + "privatePartyHostText": "Hoc f cpOer Poweirjd", + "privatePartyJoinText": "Jweorj c po wer Powert", + "privateText": "Prwoecd", + "publicHostRouterConfigText": "The Eocene fowl woes wow poirotoww-e wow vcowejaopoiefj ov oiwje. For www owej odd. cowiejowier p fjfowj paper.", + "publicText": "Pcowijefd", + "requestingAPromoCodeText": "Roifjo jc a promco fojror...", + "sendDirectInvitesText": "Snd Direoc Invirrzz", + "sendThisToAFriendText": "Snd roij coij to fowo owijr:", + "shareThisCodeWithFriendsText": "Shzrr tiz crrd thtf fforjrz.", + "showMyAddressText": "Show oe cow eijfwef", + "startAdvertisingText": "Stasi Advoierowijrz", + "startHostingPaidText": "How cow fi 4t j ${COST}", + "startHostingText": "Hfwef", + "startStopHostingMinutesText": "You co e weorij c obj ads. cowej w oerjw cjwejcwoijrewrs ${MINUTES} ocwiejrd.", + "stopAdvertisingText": "Stop Aodvoierz", + "stopHostingText": "Staff Hororwd", + "titleText": "Gthzrz", + "wifiDirectDescriptionBottomText": "Ifz allz dvicoew hfz a 'WOFJFJ JDf pnc, thoo asdhousdfl lboeij woife oiuzeufnd\nand conecwn to fhaoeiof Onc alz eodoeiv ad caoocnornct, yz can bpzl afprt\naowihe and comlcoa lntoweij ftab, jowf asdms reag awe fiw fnweorwerwekr\n\nFzr befj weojwea, wowf oaf dsfoac aosidjf oasdjf oajsdf ${APP_NAME} party fhofz.", + "wifiDirectDescriptionTopText": "Wjf a can def ajsfd lajsd flakjsdflsdljoiwjef oiwjef oaje ofijwe foijwfe\nnednf and andfoidjf wane two thou foiwa eoiANdo irjo 4.1 or manor.\n\nTh ojfwo uousdf, Opn Witi foiwefoij oiad asd;o awofjweoijfo iwjmne..", + "wifiDirectOpenWiFiSettingsText": "Opnz WifZ Snnfaz", + "wifiDirectText": "Wz-Fz Dract", + "worksBetweenAllPlatformsText": "(wrorkz btwflf alz pltafmmrs)", + "worksWithGooglePlayDevicesText": "(wlokrlz thf divcie srnngaf thz Gllglz Plz (android) gvjerajf ofz thez gmz)", + "youHaveBeenSentAPromoCodeText": "Yo hv ofj wcasn a ${APP_NAME} pwofj wcode:" + }, + "getCoinsWindow": { + "coinDoublerText": "Cznn Dnblzrz", + "coinsText": "${COUNT} Cnfjflz", + "freeCoinsText": "Frzz Cntnz", + "restorePurchasesText": "Rzatlsf Pncharlrzz", + "titleText": "Gzt Cnflzz" + }, + "getTicketsWindow": { + "freeText": "FRZZ!!", + "freeTicketsText": "Frzz Tkkckztz", + "inProgressText": "A tnfajocj in nfo cpoaro fl p lejfaowf oit agiosd fomoemnt.", + "purchasesRestoredText": "Pfojfofjf resotores.", + "receivedTicketsText": "Recvnrrd ${COUNT} rclfjsf!", + "restorePurchasesText": "Rzsgrrz Prchfrrtz", + "ticketDoublerText": "Tckfkrz Dblrrerz", + "ticketPack1Text": "Smllz Tckrt Pck", + "ticketPack2Text": "Mdmfm Tkfkf Pck", + "ticketPack3Text": "Lrggl Tkcct Pck", + "ticketPack4Text": "Jmfpmf Tkckf CPkf", + "ticketPack5Text": "Mmthf Tckff Pckc", + "ticketPack6Text": "Ulzjtla Tkckdf Pck", + "ticketsFromASponsorText": "Gzt ${COUNT} tkckjtz\nfzn a sobfhrkz", + "ticketsText": "${COUNT} Tckrtzz", + "titleText": "Gtz Tckrtz", + "unavailableLinkAccountText": "Srryrc, cpofj caodn oiwrj coi weo joweijwoief.\nIf wc woiejf w, cocwi eoiaoth aciw jeojfow iejoijwef\npcoj weo faona omowio wejfowijerwe.", + "unavailableTemporarilyText": "Thz iz cjrntelr unvalijrlwif; plz tarz agagnzl pttzzrz.", + "unavailableText": "Srrz, thz is nz avoarjowwz.", + "versionTooOldText": "Srrr.z haf voij eowij fowj aoig;o wido; ojep aowefj owepaidsof oefwoef.", + "youHaveShortText": "you hv f ${COUNT}", + "youHaveText": "yz hv ${COUNT} tickrrz" + }, + "googleMultiplayerDiscontinuedText": "Sowoer Gojf wel wouwen weoioc long wf won.\nI wow oe wefwjr pif g wfpawouja c oeij fw ocjaoiejowr.\nUntil. cowier oa j fapefij cpoypt ao coonnec awoiery.\n-Ercff", + "googlePlayText": "Gzzggl Plzz", + "graphicsSettingsWindow": { + "alwaysText": "Azlwáys", + "autoText": "Aúto", + "fullScreenCmdText": "Fúllézreen (Cmd-F)", + "fullScreenCtrlText": "Fúlzcréen (Ctrl-F)", + "gammaText": "Gámza", + "highText": "Hígh", + "higherText": "Híghrz", + "lowText": "Lózw", + "mediumText": "Mzdíum", + "neverText": "Névzer", + "resolutionText": "Résólution", + "showFPSText": "Shów FPS", + "texturesText": "Téxzures", + "titleText": "Gézphícs", + "tvBorderText": "TV Bórdzr", + "verticalSyncText": "Vértícal Sync", + "visualsText": "Vísuzls" + }, + "helpWindow": { + "bombInfoText": "- Bmmbb -\nStzzzfgr thff pzzfjnch, bfft\ncfn rssltf inf grazzf sfff-injjoiury.\nFr bfzst refuzzts, thzz tzzzff\nenfffmy bezzfere fuése rzs outffz.", + "bombInfoTextScale": 0.6, + "canHelpText": "${APP_NAME} cjo cweoff", + "controllersInfoText": "Yz can fj plsjdf ${APP_NAME} osfasfoiajwefoa soadfove rhte nwgo\ncan fpdalsdf asoifn aodifoeub weofa faodo aoocnaotorlers.\n${APP_NAME} ucouspod ra voarar elfoaf othw. yo aoidf ands ven ae\na sd controajjsdefowfj boijg ${REMOTE_APP_NAME} app\nse t adsoenga;odva;ocntrononrod fomr orminfo.", + "controllersInfoTextFantasia": "Onfe ojfr twjo ploayers cjan usje tjhe keyoiboard, bjut BombjjSquad ifs bfest width gamjepads.\nWiimotejs, adnd iOS/Android devfdsfs to condsdfrol chasdfcters. Hopezzfully you havesf\nsomef of thesfe handffy. See 'Controllefdrs' undzer 'Settinfewgs' fofr moree inefo.", + "controllersInfoTextMac": "Onfe ojfr twjo ploayers cjan usje tjhe keyoiboard, bjut BombjjSquad ifs bfest width gamjepads.\nBombSqufad cajn ufe USB gamepd, PS3 codzntrollers, XBsox 360 codsfnolers,\nWiimotejs, adnd iOS/Android devfdsfs to condsdfrol chasdfcters. Hopezzfully you havesf\nsomef of thesfe handffy. See 'Controllefdrs' undzer 'Settinfewgs' fofr moree inefo.", + "controllersInfoTextOuya": "Yzz czf usfd OUYA contdsfasdf, PS3 conconafdsf, XBox 360 coancofffnf,\nafg ladsf of ofafsdf USB and Bluetooth gamepdfhfj with BombSquadz.\nYouf canzz alssf use ijOS aknd Androioid dejoivices as cojjntrollers vjia thej fjree\n'BombSquadj Remoftef' appf. Seef 'Controllerss' underz 'Settinfgsf' fozr mosre inffo.", + "controllersInfoTextScaleFantasia": 0.56, + "controllersInfoTextScaleMac": 0.58, + "controllersInfoTextScaleOuya": 0.63, + "controllersText": "Czontrollzerssz", + "controllersTextScale": 0.63, + "controlsSubtitleText": "Yzjfo frrizjfly ${APP_NAME} czlfharcter hasf af fzew bzsic acsszzzcténs:", + "controlsSubtitleTextScale": 0.7, + "controlsText": "Czfontrollzz", + "controlsTextScale": 1.4, + "devicesInfoText": "Thz VR vojeora fo ${APP_NAME} afsdcn ab asdfpladf oasdjfaoveornt\nthwa oefia oerj avoae a;oi joaje fo wfja wfj asodf ;aosj ;af\nand acoapute randf amge aw asm con aofdi jas; oa;oinc;oianr\noncoain;d a; f;a owerj;aogiu;goau;o a;odi a;osidf oto joust\nconasdf as dnl ob pwero pacoi joaijtoiajt.", + "devicesText": "Dvnlfz", + "friendsGoodText": "Frizjdzofs gozn gnno godr poof. ${APP_NAME} za funzzgoo da sevvrzsll playerszgo\nanff supportgo up to 8 at argo time, which leadszfoo usz tof:", + "friendsGoodTextScale": 0.6, + "friendsText": "Friendificzs", + "friendsTextScale": 0.6, + "jumpInfoText": "- Jzzum -\nJumjfp t crodfssfsmall gzps,\ntothroéw thzfjgs farthff, ando\ntof eresffés feezzclings ofjoy.", + "jumpInfoTextScale": 0.6, + "orPunchingSomethingText": "Or punching something, throwing it off a cliff, and blowing it up on the way down with a sticky bomb.", + "orPunchingSomethingTextScale": 0.5, + "pickUpInfoText": "- PizzckUp -\nGrazb flffgs, znemies, or anffhing\nezse notolted toz fji groffnd.\nPresz againf tofféthrow.", + "pickUpInfoTextScale": 0.6, + "powerupBombDescriptionText": "Thf sdf asodjafds sdfosj fosfj\njfoiadf oiisfj owefj aweofj owfj.", + "powerupBombNameText": "Tr asdf Bzzfzz", + "powerupCurseDescriptionText": "Yousd ofjafj osjfj jsis fjiji\n ...orz dfo yodsffu?", + "powerupCurseNameText": "Czzrse", + "powerupHealthDescriptionText": "Rah dsflaj fdhfoifi.\nR dsfojasdofijdsf jfffjf.", + "powerupHealthNameText": "Mzfsdf fff", + "powerupIceBombsDescriptionText": "Walfkjaf dsojsdfj\nbjaosdf jfoasdfj sfdj sdfij\nland foif goaoif.", + "powerupIceBombsNameText": "Icz Fjfiwf", + "powerupImpactBombsDescriptionText": "Osjdf asdofjsdfjifjoasdfj\nobjsd, poig fjidfso fimpac ff.", + "powerupImpactBombsNameText": "Trasdf-Bozmbs", + "powerupLandMinesDescriptionText": "Thsojfi fojsofj of threosfj\nUsefjfojs ofo bjbos\nstoiz fo bosoijji.", + "powerupLandMinesNameText": "Lzjfo-Minfof", + "powerupPunchDescriptionText": "Mzaz yffr pzzches zfffr,\nfzzzrsr, ssbsr, stééger.", + "powerupPunchNameText": "Bzzxing-Gzzoves", + "powerupShieldDescriptionText": "Abslablh sdlfjaisfd fjfo fo sadoijad sf sdfjosaidjf.", + "powerupShieldNameText": "Ezzgy-Szfield", + "powerupStickyBombsDescriptionText": "Sticjf asdofjidf jfjj\nHoidfjfjhll", + "powerupStickyBombsNameText": "Stifiziz-Bzbs", + "powerupsSubtitleText": "Ozf courffse, nof gzme izs cozzlete wizzzut possfaups:", + "powerupsSubtitleTextScale": 0.8, + "powerupsText": "Pzioowers", + "powerupsTextScale": 1.4, + "punchInfoText": "- Pzznch -\nPuzfjoi dof mosefre damafsdfe thfe\nfasdfer yzr fsdfts af mfffng, so\nrzn and szzin lizz a mffffs.", + "punchInfoTextScale": 0.6, + "runInfoText": "- Runf -\nHolf ANZY buttonfo tof runz. Shoullzder buoions workzwell eeifu havfe thez.\nRfzunning getssyou plaffz fastfro butk makezos thaozrd to turnzo, sofjwatch out for cljoiwiffs.", + "runInfoTextScale": 0.6, + "someDaysText": "Sasdfasdf days ydfadf just feelz ldfasdf pujnlching something. Or bzjlowing ztosmethign upz.", + "someDaysTextScale": 0.55, + "titleText": "${APP_NAME} Hfzlf", + "toGetTheMostText": "Tzo grtt thrr mosta oufo zeez gammezsnort, youzl'll needzgo:", + "toGetTheMostTextScale": 0.8, + "welcomeText": "Wwocjowe to ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} isa nfojfo efj emnz klk fj voij -", + "importPlaylistCodeInstructionsText": "Ou o who far cj owejow o woefj aoc oapo paoel cwewer.", + "importPlaylistSuccessText": "Imps wef ${TYPE} cow e ${NAME}", + "importText": "Icwefwe", + "importingText": "Imcowiew..", + "inGameClippedNameText": "ic weof owef\n\"${NAME}\"", + "installDiskSpaceErrorText": "ERROR: Unzlbj ao ppc pwef oj oinosjs.\nYos may aoefo wocw oeitjoidosdfdve.\nCjfewf wocjdo spac ando ro gaing.", + "internal": { + "arrowsToExitListText": "préss ${LEFT} ór ${RIGHT} tó exút lzst", + "buttonText": "béttzn", + "cantKickHostError": "You loci woof woeijcoonh.", + "chatBlockedText": "${NAME} is fwocj woefj woeifj ${TIME} cowejof.", + "connectedToGameText": "Jowiejwf '${NAME}'", + "connectedToPartyText": "Joined ${NAME}'s parthrlz", + "connectingToPartyText": "Cnncttaignz...", + "connectionFailedHostAlreadyInPartyText": "Cnfnecn fjlzlkefe; hzfj iz fjn fnoanowpar ry.", + "connectionFailedPartyFullText": "Coiwje woie fow. oijwfo woecowefu.", + "connectionFailedText": "Cnnflg fnldkfd.", + "connectionFailedVersionMismatchText": "Cnndcat flld; hfdfj orurn sd adoifdoifj oawe ve;woirodfj osdf the gaz.\nMk fowie cowie fowi coc woef woiejfoiafosid oaisdf.", + "connectionRejectedText": "Cnndmcat rnecjected.", + "controllerConnectedText": "${CONTROLLER} czfznnected.", + "controllerDetectedText": "1 cnototlrlfz dtecttzff.", + "controllerDisconnectedText": "${CONTROLLER} dizcénnectud.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} diszcénezctz. Pléaze tréy coznéctíng agáín.", + "controllerForMenusOnlyText": "Thoc weo woef woecowje ofj; cow eocijwoejowiejofj.", + "controllerReconnectedText": "${CONTROLLER} récónnzécted.", + "controllersConnectedText": "${COUNT} czntlafjorrz cntjafoctd.", + "controllersDetectedText": "${COUNT} czntafjlrifl dtctatcz.", + "controllersDisconnectedText": "${COUNT} cntjofaifz dscrontctdz.", + "corruptFileText": "Cppro ff jfosjf dtecoewf w Plz wtoy finwocei wo iefj ro emaf ${EMAIL}", + "errorPlayingMusicText": "Error plzling muicf: ${MUSIC}", + "errorResettingAchievementsText": "Unzlbl tz rezerlz onzlflz achivirmvnrz; plz trz agnz ltrz.", + "hasMenuControlText": "${NAME} owjie. woejf owc.", + "incompatibleNewerVersionHostText": "Howe c ow ego inc wejoijwe ofijweof.\nUps who who ineoifjwoc woejoiw ejorwer.", + "incompatibleVersionHostText": "Hstn iz rningf a difefer ver ejoaf of thz gmz; vn't cndnocet.\nMkf woeic woefj owe cwefowfo woeowei oijtwf.", + "incompatibleVersionPlayerText": "${NAME} iz nfowij f and ifid fioew af versino af oidf c acnat aonncect.\nMk weocijw efow oc awejfowj eojcoiwjeojroewjf.", + "invalidAddressErrorText": "Errrlr; Invalidjf arrddrrs.", + "invalidNameErrorText": "Errr: coijwef noamwef", + "invalidPortErrorText": "Ero cowej woepor.", + "invitationSentText": "Invitiadf sendt.", + "invitationsSentText": "${COUNT} invitaosfod sdnt.", + "joinedPartyInstructionsText": "A frnsd azh joinfiwef yr prtr.\nGz tz 'Plaz' tz start a gmfz.", + "keyboardText": "Kéybzard", + "kickIdlePlayersKickedText": "Kkgjgf ${NAME} frz bnglfz idlzf.", + "kickIdlePlayersWarning1Text": "${NAME} wllz bz kfkzfl in ${COUNT} scirzlr if stfzl idlfz.", + "kickIdlePlayersWarning2Text": "(yz cnz trzn offz inz Sttnglgz -> Advnffzd)", + "leftGameText": "Lefzf '${NAME}'", + "leftPartyText": "Left ${NAME}'s prtatz.", + "noMusicFilesInFolderText": "Fldrz contlfzl nz mzlfig fizlflz.", + "playerJoinedPartyText": "${NAME} jfnd th z parrty!", + "playerLeftPartyText": "${NAME} lft thz party.", + "rejectingInviteAlreadyInPartyText": "Rjcjofe invljf (lweljf cojwe aoaf)", + "serverRestartingText": "Sereo s ewe Lowe. Plwfjw cow erwoejowi domoewner...", + "serverShuttingDownText": "Srjwvoe soc oie weoi ojsldfwef...", + "signInErrorText": "Errlr zinging inf.", + "signInNoConnectionText": "Unzlbl tz fsing fina f(no intnerlafj conencetion?)", + "teamNameText": "Tzlljm ${NAME}", + "telnetAccessDeniedText": "ERZOR: uzér hzs nát grúntzd télnzt accéss.", + "timeOutText": "(tímes óut zn ${TIME} seconds)", + "touchScreenJoinWarningText": "Yz hv jofndsf woijt iafh doctoucohdsf.\nIf oth dfawas do oasdf osdft mwne owijf oadsjf.", + "touchScreenText": "TóuchScrzén", + "trialText": "tríál", + "unableToResolveHostText": "Error: cow e fwjeociwjeorir.", + "unavailableNoConnectionText": "Tho fiw ficj woiejf paowejf (no itnwotower connecotjs?)", + "vrOrientationResetCardboardText": "Us this to weoojeif co VR owfjow ego\nTz pc we fwotj oa ocw eoowf eo awoifoacoincwr.", + "vrOrientationResetText": "VR ooiwjfowif rnefz.", + "willTimeOutText": "(wzlf tmz oat if idle)" + }, + "jumpBoldText": "JZMP", + "jumpText": "Júmp", + "keepText": "Kéepz", + "keepTheseSettingsText": "Kéép thúse síttzngs?", + "keyboardChangeInstructionsText": "Dlbjwf pcowej spec wot cioci ckdosrs.", + "keyboardNoOthersAvailableText": "No code vojwdfkdjcoi osjofwjefs.", + "keyboardSwitchText": "Wjcwoeif cjowefi oto sj \"${NAME}\".", + "kickOccurredText": "${NAME} weir eowifjwoe.", + "kickQuestionText": "Kfoicj ${NAME}?", + "kickText": "Kcwef", + "kickVoteCantKickAdminsText": "Afwoif opowij weka kcijksfd.", + "kickVoteCantKickSelfText": "You cow er wodfijw eofoydf.", + "kickVoteFailedNotEnoughVotersText": "No wc weo ra;so asdpaj cowier.", + "kickVoteFailedText": "Kfeoiw-vofj flwjef", + "kickVoteStartedText": "A cjf co cow ef woa c ocwjeo jwof ${NAME}.", + "kickVoteText": "Voice woes ocowieki", + "kickVotingDisabledText": "Kcjow cowejfdf owdofijwd.", + "kickWithChatText": "Typ ${YES} oef cwoiejf owe ${NO} fojw c.", + "killsTallyText": "${COUNT} kíllz", + "killsText": "Kíllz", + "kioskWindow": { + "easyText": "Ezrz", + "epicModeText": "Epcz Md", + "fullMenuText": "Fllz Menuz", + "hardText": "Hrzzd", + "mediumText": "Mddnfmz", + "singlePlayerExamplesText": "Snflgj Plzlrz / Co-opz Exmapflz", + "versusExamplesText": "Versuz Exmapez" + }, + "languageSetText": "Lngofijf isa tnsfl ${LANGUAGE}", + "lapNumberText": "Lzp ${CURRENT} ofz ${TOTAL}", + "lastGamesText": "(lász ${COUNT} gámúz)", + "launchingItunesText": "Lznching iTnzlflz...", + "leaderboardsText": "Lezérbórdz", + "league": { + "allTimeText": "Allz Tmz", + "currentSeasonText": "Currngjg ZSfowife (${NUMBER})", + "leagueFullText": "${NAME} Lfjofz", + "leagueRankText": "Lglfl Rnrka", + "leagueText": "Lggfzf", + "rankInLeagueText": "#${RANK}, ${NAME} Lzlglf${SUFFIX}", + "seasonEndedDaysAgoText": "Sfsf endf f ${NUMBER} fjosf afoc.", + "seasonEndsDaysText": "Sfowefj wfn dofi ${NUMBER} dfjowf.", + "seasonEndsHoursText": "Swso fo jiwf woie${NUMBER} fowhfz.", + "seasonEndsMinutesText": "Seaonf woeo cowi fj${NUMBER} mcoinwefoij.", + "seasonText": "Seocwofj ${NUMBER}", + "tournamentLeagueText": "Yzz mff erwr ${NAME} leg foic woefin woef oeijfwfew.", + "trophyCountsResetText": "Trphy cofo wuecw owef owioiafowf." + }, + "levelBestScoresText": "Bsfwo cowiejo ef${LEVEL}", + "levelBestTimesText": "Best wfm coi ${LEVEL}", + "levelFastestTimesText": "Fztjst tmzf on ${LEVEL}", + "levelHighestScoresText": "Hghglz scorlz on ${LEVEL}", + "levelIsLockedText": "${LEVEL} ís lúckzd.", + "levelMustBeCompletedFirstText": "${LEVEL} múst bz cómplztíd fúrst.", + "levelText": "Lvfjosfi ${NUMBER}", + "levelUnlockedText": "Lévzl Unlóckzd!", + "livesBonusText": "Lívzs Bónús", + "loadingText": "lztijding", + "loadingTryAgainText": "Lcowejf c weoifj wecoi woeijo...", + "macControllerSubsystemBothText": "Both (eco woicowijef)", + "macControllerSubsystemClassicText": "Cladsfsf", + "macControllerSubsystemDescriptionText": "(tic oc woef. coli hoi ogief eer woeofowiejfoj)", + "macControllerSubsystemMFiNoteText": "Mwoe cw obj c woe kiwi eoijwe;\nYou wo. och weil foeiwjf oiwjc owije - > COijowiejfs.", + "macControllerSubsystemMFiText": "Mcojwe-coj-cwoejowif", + "macControllerSubsystemTitleText": "Coijcw oijSuporer", + "mainMenu": { + "creditsText": "Crizetzzts", + "demoMenuText": "Dmzm Mnzz", + "endGameText": "Enzs Gzmé´fez", + "exitGameText": "Excczzt Gamzzéé", + "exitToMenuText": "Exít tzú ménu?", + "howToPlayText": "Hwopm T´wPlayynf", + "justPlayerText": "(Jzft ${NAME})", + "leaveGameText": "Lvffz Gmmz", + "leavePartyConfirmText": "Rellr lvvz thz parrttyz?", + "leavePartyText": "Lvvz Prttzz", + "leaveText": "Lvveeaavv", + "quitText": "Qztiiizt", + "resumeText": "Rssmzmmee", + "settingsText": "Staettengis" + }, + "makeItSoText": "Mrazzk itzz Séooee", + "mapSelectGetMoreMapsText": "Gzzt Mrrz Mppz...", + "mapSelectText": "Sélézt...", + "mapSelectTitleText": "Mápz dú ${GAME}", + "mapText": "Mázp", + "maxConnectionsText": "Mc COnoweijwer", + "maxPartySizeText": "McCoy cpweo fwocwoef", + "maxPlayersText": "Mx Plweffz", + "modeArcadeText": "Aroc Mofwfz", + "modeClassicText": "Cjwofej mDofd", + "modeDemoText": "Dfwocij Mmdff", + "mostValuablePlayerText": "Móst Válúablz Pláyér", + "mostViolatedPlayerText": "Móst Víoláted Pláyer", + "mostViolentPlayerText": "Móst Víolznt Pláyér", + "moveText": "Móvév", + "multiKillText": "${COUNT}-KÍZL!!!", + "multiPlayerCountText": "${COUNT} pláyzrs", + "mustInviteFriendsText": "Ntz: yz mlfj coi jowe oif\noj \"${GATHER}\" cows coe ojwoef\nocnotjoi to pla coimcourlr.", + "nameBetrayedText": "${NAME} bétráyzd ${VICTIM}.", + "nameDiedText": "${NAME} díed.", + "nameKilledText": "${NAME} killzéd ${VICTIM}.", + "nameNotEmptyText": "Náme cúnnzt bé émptz!", + "nameScoresText": "${NAME} Scórzsz!", + "nameSuicideKidFriendlyText": "${NAME} accfijflzfj dzrz.", + "nameSuicideText": "${NAME} cómmittéd súizide.", + "nameText": "Noiwjfe", + "nativeText": "Nztvvz", + "newPersonalBestText": "Néw pérsónzl bést!", + "newTestBuildAvailableText": "A nwlf tat blfjl is aviflflzblz! (${VERSION} blzd ${BUILD}).\nGzt iz tat ${ADDRESS}", + "newText": "Ncw", + "newVersionAvailableText": "A newo coi efo ${APP_NAME} i c ifoi wf ${VERSION}", + "nextAchievementsText": "Nx aoio ijoijefez", + "nextLevelText": "Néxt Lévzl", + "noAchievementsRemainingText": "- nónz", + "noContinuesText": "(nz contoijfows)", + "noExternalStorageErrorText": "Nz xtenrlf stlfsdf fnff onf thz dfvfojfzz", + "noGameCircleText": "Errór: nút lúggzd íntó Góme Cúrclz", + "noJoinCoopMidwayText": "Có-óp gúmzs cán't bz jóinzd mídwáy.", + "noProfilesErrorText": "Yoú hávz nó pláyerz prófilzs, só yóu're stzck wíth '${NAME}'.\nGó tz Sétzings->Pláyerz Prófiles tú mzke yóurszlf á prófile.", + "noScoresYetText": "Nz scrrlz ytz.", + "noThanksText": "Nó Thánkz", + "noTournamentsInTestBuildText": "WOREwr: WOTo cwoefw coif wefiidfjdf cow ekfwoejowijerwdifwdf.", + "noValidMapsErrorText": "Nó válíd máps fóund fúr thzs gáme typz.", + "notEnoughPlayersRemainingText": "Nt zefwnoef plrjr rmeinging; exit anfo iwfj owjf oj ga emga;", + "notEnoughPlayersText": "Yf nfdf a taflew ${COUNT} pfo wotj wofijow afo game!", + "notNowText": "Nót Núw", + "notSignedInErrorText": "Yz mst bz snginf intof yrrz accnt tz dz thzz.", + "notSignedInGooglePlayErrorText": "Ym fij cow fj wo cowe toiw jcoj ojwoefj owi.", + "notSignedInText": "(nf cow eofw)", + "nothingIsSelectedErrorText": "Nothflf iz seleftcetzd!", + "numberText": "#${NUMBER}", + "offText": "Ófz", + "okText": "OzKy", + "onText": "Ón", + "oneMomentText": "One Mmcowmerz..", + "onslaughtRespawnText": "${PLAYER} wúll réspawn das wávz ${WAVE}", + "orText": "${A} orz ${B}", + "otherText": "Ofowiejf....", + "outOfText": "(#${RANK} oút ófz ${ALL})", + "ownFlagAtYourBaseWarning": "Yoúr owzn flág múst bé\nát yzúr báse tó scóre!", + "packageModsEnabledErrorText": "Ntwrkz-plz nzt allrzd whlz pcket zmof aosdf oidsfwoe wfe(szz wsetnoaf adsasdnvanoce)", + "partyWindow": { + "chatMessageText": "Chclz Mafnfz", + "emptyText": "Yrrz partzy ist memptyz", + "hostText": "(hzt)", + "sendText": "Snene", + "titleText": "Yrrz Partar" + }, + "pausedByHostText": "(pzzde bz hzstt)", + "perfectWaveText": "Púrfzct Wávz!", + "pickUpBoldText": "PZCKÚP", + "pickUpText": "Píczk Uúp", + "playModes": { + "coopText": "Cz-opz", + "freeForAllText": "Frzz-frz-Alz", + "multiTeamText": "Mzltiz-Tmzz", + "singlePlayerCoopText": "Sion Player /Cpfpowfji", + "teamsText": "Tmmz" + }, + "playText": "Pfnplay", + "playWindow": { + "coopText": "Czoo-op", + "freeForAllText": "Férree-for-Azzxll", + "oneToFourPlayersText": "1-4 plzzrzzz", + "teamsText": "Téééémés", + "titleText": "Plzaayz", + "twoToEightPlayersText": "2-8 zpllzrrz" + }, + "playerCountAbbreviatedText": "${COUNT}pz", + "playerDelayedJoinText": "${PLAYER} wlzl enters atz z stzf nfae ggmmz.", + "playerInfoText": "Plzer Infor", + "playerLeftText": "${PLAYER} lzfjt zjh gzmls.", + "playerLimitReachedText": "Plzer lmzt of ${COUNT} rzched; nz jofjinsdfn allrlwzdz.", + "playerLimitReachedUnlockProText": "Upfjfwef tz \"${PRO}\" wofj coje fwoe cojwiej woe ${COUNT} pffjz.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Yz cntn delfaje coweir anccnoar proffz.", + "deleteButtonText": "Delefa\nPrproif", + "deleteConfirmText": "Delfgt '${PROFILE}'?", + "editButtonText": "Edtz\nPrifaflz", + "explanationText": "(crtz custmo nmcm and appaoarnz frz)", + "newButtonText": "Nwwz\nPoriflrz", + "titleText": "Plzzerr Prfillz" + }, + "playerText": "Pláyér", + "playlistNoValidGamesErrorText": "Thz fpoiwfj coweoo conw ovj aodsf owcoi wefgages.", + "playlistNotFoundText": "plzlst not fozfnd", + "playlistText": "Plzlzlzf", + "playlistsText": "Plzlntsfs", + "pleaseRateText": "If yóu're énjoyíng ${APP_NAME}, pléase cónsider táking z\nmúment ánd rúting zt ór wrúting í reváew. Thzs próvidzs\níseful fzédbíck ánd hélps súpport fútúre dévelópmént.\n\nthénkz!\n-eric", + "pleaseWaitText": "Poke focwoe fjowef.", + "pluginsDetectedText": "Nz pweo woe dfowocewr. Enfwoc/cowefe thzm incowrdss.", + "pluginsText": "Plfzlfez", + "practiceText": "Pcoifjzzz", + "pressAnyButtonPlayAgainText": "Prézs ánz búttún tóz pláy agánz...", + "pressAnyButtonText": "Prézs ánz búttún tóz cúntinze...", + "pressAnyButtonToJoinText": "przés azny búttoz tz jóin...", + "pressAnyKeyButtonPlayAgainText": "Prézs ánz kéy/búttún tóz pláy agánz...", + "pressAnyKeyButtonText": "Prézs ánz kéy/búttún tóz cúntinze...", + "pressAnyKeyText": "Préss ány kézy...", + "pressJumpToFlyText": "** Przss júmp repéatedly tó flyz **", + "pressPunchToJoinText": "przss PEZCH té júin...", + "pressToOverrideCharacterText": "przss ${BUTTONS} tó óvézríde yózr chárazfter", + "pressToSelectProfileText": "préezs ${BUTTONS} té seézlect yúr prófíle", + "pressToSelectTeamText": "przéss ${BUTTONS} tó sézlezt yoór túém", + "profileInfoText": "Créate prófilzs fór yourself and your friends to assign\nyóurszlf námzs ánz cháractzrs ínstzad óf géttíng rándzm ónzs.", + "promoCodeWindow": { + "codeText": "Czdfé", + "codeTextDescription": "Prémsz Crdde", + "enterText": "Entzzr" + }, + "promoSubmitErrorText": "Errór szbmítting códe; chéck yóur ínternét cznnéctión", + "ps3ControllersWindow": { + "macInstructionsText": "Swízch zff zhe pzwer zn zhe báck zf yzur PS3, máke sure\nBluezzzzh ís enábled zn yzur Mác, zhen cznnecz yzur cznzrzller\nzz yzur Mác víá á USB cáble zz páír zhe zwz. Frzm zhen zn, yzu\ncán use zhe cznzrzller's hzme buzzzn zz cznnecz íz zz yzur Mác\nín eízher wíred (USB) zr wíreless (Bluezzzzh) mzde.\n\nZn szme Mács yzu máy be przmpzed fzr á pássczde when páíríng.\nÍf zhís háppens, see zhe fzllzwíng zuzzríál zr gzzgle fzr help.\n\n\n\n\nPS3 cznzrzllers cznneczed wírelessly shzuld shzw up ín zhe devíce\nlísz ín Syszem Preferences->Bluezzzzh. Yzu máy need zz remzve zhem\nfrzm zház lísz when yzu wánz zz use zhem wízh yzur PS3 ágáín.\n\nÁlsz máke sure zz díscznnecz zhem frzm Bluezzzzh when nzz ín\nuse zr zheír bázzeríes wíll cznzínue zz dráín.\n\nBluezzzzh shzuld hándle up zz 7 cznneczed devíces,\nzhzugh yzur míleáge máy váry.", + "macInstructionsTextScale": 0.74, + "ouyaInstructionsText": "Tó usz a PS3 cóntróllzr wsth yóur ÓUYA, ssmply cónnzct st wsth a USB cablz\nóncz tó pasr st. Dósng thss may dsscónnzct yóur óthzr cóntróllzrs, só\nyóu shóuld thzn rzstart yóur ÓUYA and unplug thz USB cablz.\n\nFróm thzn ón yóu shóuld bz ablz tó usz thz cóntróllzr's HÓMZ buttón tó\ncónnzct st wsrzlzssly. Whzn yóu arz dónz playsng, hóld thz HÓMZ buttón\nfór 10 szcónds tó turn thz cóntróllzr óff; óthzrwssz st may rzmasn ón\nand wastz battzrszs.", + "ouyaInstructionsTextScale": 0.74, + "pairingTutorialText": "péiríng tútóríal veídeo", + "titleText": "Uzséng PS3 Cónzrollérs wízh ${APP_NAME}:" + }, + "publicBetaText": "PZÚBLIC BÉZTA", + "punchBoldText": "PÚNZCH", + "punchText": "Púzch", + "purchaseForText": "Púrcháse fúrz ${PRICE}", + "purchaseGameText": "Púrchúze Gáme", + "purchasingText": "Prjrcjfz...", + "quitGameText": "Qcoifj ${APP_NAME}", + "quittingIn5SecondsText": "Quéttzng ín 5 sécznds...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Rzandomz", + "rankText": "Rnkfz", + "ratingText": "Rátzng", + "reachWave2Text": "Réách wávé 2 tó ránk.", + "readyText": "réády", + "recentText": "Rcfjfzz", + "remainingInTrialText": "rémáfiníng ín tríál", + "remoteAppInfoShortText": "${APP_NAME} iz cow eofi co wefjowiej fowiejfowef\nCow enow c oweifj owiej oc owei fowije fowj ;join thsefaf\n${REMOTE_APP_NAME} api c woe fj owiejc owe coiwjeowef\nasdf contorfwefwe.", + "remote_app": { + "app_name": "Bomc Rcorjorj", + "app_name_short": "BSFmowcwe", + "button_position": "Brroz Posijfoiwjr", + "button_size": "Brrltjt Szr", + "cant_resolve_host": "Cntto Rrrlj hrorz.", + "capturing": "Cporrjz...", + "connected": "Ctnntoicc.", + "description": "Usf coi cyao o acj wof joic oaijojef a;owi ocaobOBm.\nUps to co w of avow wf ocw oei iwfor acoa wf mult apc a wefjo cp sonota toav a fowieort.", + "disconnected": "Doc wolf by user vowe.", + "dpad_fixed": "ffojoc", + "dpad_floating": "fljtoffz", + "dpad_position": "D-Pd Pofjoijor", + "dpad_size": "D-Pf zrrz", + "dpad_type": "D-pd Tprpz", + "enter_an_address": "Entro an addrrs", + "game_full": "Thz gmc wif coj f cow tnoac aoj fojwoefjwe.", + "game_shut_down": "Thz gocm ah soc ow townw.", + "hardware_buttons": "Horjr oBurrlzz.", + "join_by_address": "Jrorij boj Addrrzz.", + "lag": "Lggz ${SECONDS} scofjfz", + "reset": "Rzwf tz deferz", + "run1": "Rrrnz", + "run2": "Rnr z2", + "searching": "Serarojc ofwe BBomsojo ggzfz...", + "searching_caption": "Tpa co w toia noa cowef owiejower.\nMkf wuseo youc aeno toi oaoew rowjtooaor.", + "start": "Strwfz", + "start_button": "Stwow", + "version_mismatch": "Veroi jmoiowoejf\nMkac aot co boaisoijero caoioisd fjaoer\nadc ot oacouweotu vowe ron aga." + }, + "removeInGameAdsText": "Unlkjfj \"${PRO}\" in cowj wje of wejfoiwfoo oifoiwjef.", + "renameText": "Rzngmlz", + "replayEndText": "Enz Rpllz", + "replayNameDefaultText": "Lzts Gmzl Rplzlz", + "replayReadErrorText": "Errzr rdnglgz rplzlz flz.", + "replayRenameWarningText": "Renznf \"${REPLAY}\" aftnf gmz oidf aosf asdj aoij keep aoioheor wofijo aivjoe rowijefowif.", + "replayVersionErrorText": "Srrz, tzh replalz wz mddd inzl ad fidfffr\nvererjz o f at the agamemfzz and acjewo tjj zluszd.", + "replayWatchText": "Wzzlt Rlpllzl", + "replayWriteErrorText": "Errzz wrtzzlg rplzl fizlz.", + "replaysText": "Rpzlzz", + "reportPlayerExplanationText": "Uf woc ow efow efiwjco owe,we woi woiej ofijwoe fowe,wd fowidj owejfowief.\nPef woc woejfowifw:", + "reportThisPlayerCheatingText": "Chttocz", + "reportThisPlayerLanguageText": "Inpcowej oij Lnagoijafoj", + "reportThisPlayerReasonText": "Wh cco wefo co aogjwo fjowef?", + "reportThisPlayerText": "Rporijf Thif Plzlirz", + "requestingText": "Rcjowcijc...", + "restartText": "Réstártz", + "retryText": "Rtefnz", + "revertText": "Révúrt", + "runBoldText": "RUZLN", + "runText": "Rúun", + "saveText": "Sávez", + "scanScriptsErrorText": "Error(w) frolic weir ro; cj weowje ici woeijwer.", + "scoreChallengesText": "Scóre Chálléngúz", + "scoreListUnavailableText": "Sczerl lzst unvnslfljblz.", + "scoreText": "Szóre", + "scoreUnits": { + "millisecondsText": "Mzlifjltlfjndfz", + "pointsText": "Pntnz", + "secondsText": "Sclnldfjz" + }, + "scoreWasText": "(wús ${COUNT})", + "selectText": "Slézcz", + "seriesWinLine1PlayerText": "WZNNPLL THZ", + "seriesWinLine1Scale": 0.65, + "seriesWinLine1TeamText": "WNZTTTM THZ", + "seriesWinLine1Text": "WÍNZ THZ", + "seriesWinLine2Scale": 1.0, + "seriesWinLine2Text": "SÉRÍZS!", + "settingsWindow": { + "accountText": "Acncljtz", + "advancedText": "Advnfsdfjzd", + "audioText": "Aédudief", + "controllersText": "Cosljfoiwefjf", + "enterPromoCodeText": "Enlsfl Przmfm Codnfff", + "graphicsText": "Graphzzzz", + "kickIdlePlayersText": "Kzjfk Idlze Plzerrls", + "playerProfilesMovedText": "Ntzz: Plwef Profjowc hsv mcfw tz the Accfoj wenof co omain mcur.", + "playerProfilesText": "Plefzjf Profifjss", + "showPlayerNamesText": "Sfoz Plzersd Nzjmm", + "titleText": "Szeetzgsss" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(a smpd a, condofia efjwefoajewofoiaj;s a sodifj a;osdfjoitest f)", + "alwaysUseInternalKeyboardText": "Alzlt usl Intenralf kyBerz", + "benchmarksText": "Bnchmkwr & Staf-Tsfsfz", + "disableCameraGyroscopeMotion": "Disa fe come won aweGowyeo CMosndf", + "disableCameraGyroscopeMotionText": "Dislike og wefo cwei oweiaMowerdns", + "disableCameraShakeText": "SDi Cmanan Blobofs", + "disableThisNotice": "(you come for wolf jwfowejoijer cowed wfoiwej jay)", + "enablePackageModsDescriptionText": "(enleaf exjpaf mdoodn gcpappwro buf dsilas new t plz)", + "enablePackageModsText": "Ebnblaf Lcjof Pcfowf Mdsfsz", + "enterPromoCodeText": "Eznter Cdzs", + "forTestingText": "Ntz: thz vlarj farz olflf rzz tsffcn anz wllz bz llfsf whnzl thzz app exrtzz.", + "helpTranslateText": "${APP_NAME} lngalsdf transaldflksdfj arz fjocdmaosdf\nsprnarted effrztzs. Ifsdf oud'fu likzz to cmdfoasdf corectsf\na sd fjafdsoijdf, flfowefj thz fljlnkdd blzljf. Thansdlfn in andncas!", + "kickIdlePlayersText": "Kzkck Idlzlf Plzjrs", + "kidFriendlyModeText": "Krz-Frjijglfz-Mzdz (rdfjifz voioifjf, fz)", + "languageText": "Lnglfjslfd", + "moddingGuideText": "Mddzng Gdufnzsd", + "mustRestartText": "Yz msg restar thz gmm fof this tk ttk effectsz.", + "netTestingText": "Ntwkrz Tsstcg", + "resetText": "Rsttz", + "showBombTrajectoriesText": "Shzlz Bomf Tfwoejcwoefz", + "showPlayerNamesText": "SHzlfjl Plzlrr Nmzlzlls", + "showUserModsText": "Shzl Mdsz Fldlrz", + "titleText": "Advnafjfljdz", + "translationEditorButtonText": "${APP_NAME} Tjofwijf Eoidjor", + "translationFetchErrorText": "trnzlfgweg stdlsf unvavalrz", + "translationFetchingStatusText": "chcking tranfsldf sttuffsz...", + "translationInformMe": "Ofiw co. eom wccowoijef weiofwe owef wc owefwrs.", + "translationNoUpdateNeededText": "the crjlfidsj lnglfjdz iz upzl tx dtzzfs; woojofjd!", + "translationUpdateNeededText": "** thz furur languares nezdsjd updates!!!! **", + "vrTestingText": "VR Tzntgng" + }, + "shareText": "Shcwef", + "sharingText": "Chwoefz..", + "showText": "Shz", + "signInForPromoCodeText": "Yz mst singo in o accnno for cods ot thk weffets.", + "signInWithGameCenterText": "To loci wo gamg wofiw efoiwjef.\ncoaej goajb oaj Game foc etapp.", + "singleGamePlaylistNameText": "Jzff ${GAME}", + "singlePlayerCountText": "1 pláyzr", + "soloNameFilterText": "${NAME} Sólzo", + "soundtrackTypeNames": { + "CharSelect": "Czárazter Sélection", + "Chosen One": "Chósén Onz", + "Epic": "Epíc Móde Games", + "Epic Race": "Epíc Ráze", + "FlagCatcher": "Cáptúre thz Flág", + "Flying": "Háppy Thózghts", + "Football": "Foótbazll", + "ForwardMarch": "Aszáult", + "GrandRomp": "Cónqúest", + "Hockey": "Hóckzy", + "Keep Away": "Kézp Awáy", + "Marching": "Rúnazróund", + "Menu": "Mzín Ménú", + "Onslaught": "Onéslaúght", + "Race": "Race", + "Scary": "Kíng óf thé Híll", + "Scores": "Scóre Scréen", + "Survival": "Ezíminátion", + "ToTheDeath": "Dáath Mzétch", + "Victory": "Fínal Scóre Scréen" + }, + "spaceKeyText": "spzz", + "statsText": "Sfawfwf", + "storagePermissionAccessText": "Tho cowefj woiejowefw.", + "store": { + "alreadyOwnText": "Yorz alrlfzl wozl ${NAME}!", + "bombSquadProDescriptionText": "• Rmoi come fai oifwjeojf\n• Ubckff ${COUNT} bifn ciuefuh\n• +${PERCENT}% power foiacoiwfl\n• Unlclf '${INF_ONSLAUGHT}' anz\n '${INF_RUNAROUND}' co of jews", + "bombSquadProFeaturesText": "Fettnafars:", + "bombSquadProNameText": "${APP_NAME} Prz", + "bombSquadProNewDescriptionText": "• OWjoc owejfowef dfwefwefwef\n• Woijc wefjweocjjcw fjweofijwef\n• wocje fowiejf:", + "buyText": "Bzy", + "charactersText": "Czharggczzls", + "comingSoonText": "Cmzngl Snznlsz...", + "extrasText": "Extrrfz", + "freeBombSquadProText": "Bfjaoefj ij foawje foijcowje oifjwo jf woaifjo waf\naowf aosdjfoaijcow iejojefo wjfowiefjo ${COUNT} foco wofeij a;iofj woj.\nENojfowaif oithwoefij ac owejfwoef jaoijf owicjoijcowiefj!\n-Erfc", + "gameUpgradesText": "Gzmm Unzprjfzz", + "getCoinsText": "Gzt Cnfljfz", + "holidaySpecialText": "Hzlolidz Spzlzlflz", + "howToSwitchCharactersText": "(gz tz ${SETTINGS} -> ${PLAYER_PROFILES} tz wfoiejf coijwoicoijwf)", + "howToUseIconsText": "(croijoj oaf japocj paowejf owef\"(in thc accj f enfo)\" otj acoiejf w)", + "howToUseMapsText": "(usf thwemc woeifj aojco wefjo wjaocj paowfj woeifjos)", + "iconsText": "Icnofjf", + "loadErrorText": "Unzlblf to zlfl bpgong.\nChlzl yrlzn intnlzfnl cnnfncsz.", + "loadingText": "lzdng", + "mapsText": "Mzpz", + "miniGamesText": "MnflzGmz", + "oneTimeOnlyText": "(onc jf onlyf)", + "purchaseAlreadyInProgressText": "A prnoif of thwf if mwof wofi aoer wofoiefw.", + "purchaseConfirmText": "Prucoief ${ITEM}?", + "purchaseNotValidError": "Prwoj focj owejf\nContoewf ${EMAIL} if fowl efjw oajof.", + "purchaseText": "Pfoijcrz", + "saleBundleText": "Bzfjoc Salfz!", + "saleExclaimText": "Slfo!", + "salePercentText": "(${PERCENT}% fifz)", + "saleText": "SLFL", + "searchText": "Srnczl", + "teamsFreeForAllGamesText": "Tmmg / Frrzz-rff/Allz Gmffz", + "totalWorthText": "*** ${TOTAL_WORTH} vafwf! ***", + "upgradeQuestionText": "Upwfwef?", + "winterSpecialText": "Wznter Spzlflfz", + "youOwnThisText": "- yz wnfz thz -" + }, + "storeDescriptionText": "asdfj fwofjwe fwjfjwf asdlfjsdf fdjatoijw;ojfpwoef wefjwoefjwfe.\n\nBlojsd;flj asdfj asd;ofjasdfoi a;sdijfs;dfsd jfdasoidjf ;aidsjf ;adsf\nasdofa sdfijs;odf ;aosidjf ;asdfj;asdof;ijasdf;\nsdjfsjd fasdf asdfasdfasdf", + "storeDescriptions": { + "blowUpYourFriendsText": "Blzw upz yrzeor frdnfnz", + "competeInMiniGamesText": "Cmplzl in mdmfl gmzlz rnglng fmzz rcngng to flznfg", + "customize2Text": "Cztmzls chrcctsrsl, mnnin-gmzz, and evnf zl snftrrkckz.", + "customizeText": "Cstmzlze chrzclterz dand crzlsldf yrzw ownz emin-gmzm sndorglrks.", + "sportsMoreFunText": "Sprtzs arz mrzr fnzrs wthzr explowoafsdfs.", + "teamUpAgainstComputerText": "Tmzls upz agansft thz cmpturzs." + }, + "storeText": "Stzlrle", + "submitText": "Scowiejfwef", + "submittingPromoCodeText": "Subjfoif Cdssz...", + "teamNamesColorText": "Tmcoj Conor/ Coarle...", + "teamsText": "Tééáémés", + "telnetAccessGrantedText": "Tzélnt acczzss énazledz.", + "telnetAccessText": "Tzélnt azcezs détzfcted; aélow?", + "testBuildErrorText": "Thz tst-bld iz nz lnglsf actv.fz. plz hckc frz nv vnarerz.", + "testBuildText": "Tstg Blldz", + "testBuildValidateErrorText": "Unbblldf tz validfajr tstb blflz. (nz netf cdnfdljfec?)", + "testBuildValidatedText": "Tst Bjldf Vlaldkfsf; Enjzjf!", + "thankYouText": "Thánk yóu fór yóur súppórt! Enjóy thé gáme!!", + "threeKillText": "TRÍPLZ KÚLL!!", + "timeBonusText": "Tíme Bónús", + "timeElapsedText": "Tíme Elápszd", + "timeExpiredText": "Tzmz Exprireizdd", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}hz", + "timeSuffixMinutesText": "${COUNT}mz", + "timeSuffixSecondsText": "${COUNT}sz", + "tipText": "Tízp", + "titleText": "Bmbmsqdz", + "titleVRText": "BomboFjof VR", + "topFriendsText": "Tóp Fríendz", + "tournamentCheckingStateText": "Chkfjfowef oitwof oweifja oef pawoej owef...", + "tournamentEndedText": "This foil jefoi weoa echo sd. An ewoifj wo oowifjowe soon.", + "tournamentEntryText": "Tnofjwfe oecowiejw", + "tournamentResultsRecentText": "Rcnet Touaofius aRamalsdf.", + "tournamentStandingsText": "Tzewfjwoij Stndfalfjz", + "tournamentText": "Tanfowijfowef", + "tournamentTimeExpiredText": "Tmcoef Tm Epzoiejfefz", + "tournamentsText": "Trzzmfnmflfzzs", + "translations": { + "characterNames": { + "Agent Johnson": "Agjofi Jocwef", + "B-9000": "B-90000", + "Bernard": "Brzlfjfoz", + "Bones": "Bnzl", + "Butch": "Brzffd", + "Easter Bunny": "Eefowj Boifjwoef", + "Flopsy": "Flzpzfz", + "Frosty": "Frrozf", + "Gretel": "Gflrlz", + "Grumbledorf": "Goijcowiefdf", + "Jack Morgan": "Jcjo Mrggz", + "Kronk": "Krrorzz", + "Lee": "Lcwef", + "Lucky": "Loiwjef", + "Mel": "Mzzl", + "Middle-Man": "Gmdflj-cmdf", + "Minimus": "Mijowcf", + "Pascal": "Pasclfz", + "Pixel": "Poxiejf", + "Sammy Slam": "Sm df cwoSflm", + "Santa Claus": "Santa Clzous", + "Santa Clause": "Szntana Clzlrnas", + "Snake Shadow": "Snlj Co woefj", + "Spaz": "Spzz", + "Taobao Mascot": "Taco Mcwojefo", + "Todd": "Tweet", + "Todd McBurton": "Taj O woejfd", + "Xara": "Xfwef", + "Zoe": "Zob", + "Zola": "Zlefw" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Infíníte\nÓnsláught", + "Infinite\nRunaround": "Ínfíníte\nRúnaróúnd", + "Onslaught\nTraining": "Onsláughtz\nTráiníng", + "Pro\nFootball": "Pró\nFóotbáll", + "Pro\nOnslaught": "Pró\nOnsláughtz", + "Pro\nRunaround": "Pró\nRúnaróund", + "Rookie\nFootball": "Róokíe\nFóotbálz", + "Rookie\nOnslaught": "Róokze\nOnsláughz", + "The\nLast Stand": "Thé\nLást Stánd", + "Uber\nFootball": "Úbér\nFóotbáll", + "Uber\nOnslaught": "Ubér\nOnsláúght", + "Uber\nRunaround": "Úbér\nRúnároúnd" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Tfjozijzs", + "Infinite ${GAME}": "Infifiew ${GAME}", + "Infinite Onslaught": "Íznfíníte Onzláught", + "Infinite Runaround": "Iznfinite Runaround", + "Onslaught": "Íznfíníte Onzláught", + "Onslaught Training": "Onzláughtz Trzáíníng", + "Pro ${GAME}": "Prz ${GAME}", + "Pro Football": "Pró Football", + "Pro Onslaught": "Pró Onzláught", + "Pro Runaround": "Pró Runáround", + "Rookie ${GAME}": "Rzzlfjf ${GAME}", + "Rookie Football": "Rzokie Fúotbáll", + "Rookie Onslaught": "Rúokíe Onzláught", + "Runaround": "Iznfinite Runaround", + "The Last Stand": "Thú Lázt Ztánd", + "Uber ${GAME}": "Ubezr ${GAME}", + "Uber Football": "Ubúr Football", + "Uber Onslaught": "Ubzr Onzláught", + "Uber Runaround": "Ubúr Runáround" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Bé thé chósen ónz fór a léngth éf tíme tó wzn.\nKzll thé chósen óne tz bécome út.", + "Bomb as many targets as you can.": "Bmb ojwe fo cj woef weoijcoj ofz.", + "Carry the flag for ${ARG1} seconds.": "Cárrz thé flág fór ${ARG1} sécóndz.", + "Carry the flag for a set length of time.": "Cárry thé flág fór á sét léngth oz tíme.", + "Crush ${ARG1} of your enemies.": "Crúsz ${ARG1} óf yóúr énemies", + "Defeat all enemies.": "Défetz álz enemíz.", + "Dodge the falling bombs.": "Ddfjgzl thz flzjflg bjbjlfz.", + "Final glorious epic slow motion battle to the death.": "Fínzl glóriozs épzc slów mótizn báttlz tó thz déath.", + "Gather eggs!": "Gjowicj ewfgw!", + "Get the flag to the enemy end zone.": "Gét thé flág tó thz enémy énd zóne.", + "How fast can you defeat the ninjas?": "Hz wfwe cowej oiwcowiefo idnininas?", + "Kill a set number of enemies to win.": "Kílz á sét númbér óf énmiez tó wín", + "Last one standing wins.": "Lást ónz stándíng wíns.", + "Last remaining alive wins.": "Lást rzmáiníng álzve wíns.", + "Last team standing wins.": "Lást tézm stándíng wíns.", + "Prevent enemies from reaching the exit.": "Prévúnt énemizs fróm reáching thé exít.", + "Reach the enemy flag to score.": "Réach thz enémy flág tó scórz.", + "Return the enemy flag to score.": "Réturz thz énemy flág tó scórz.", + "Run ${ARG1} laps.": "Rún ${ARG1} lápz.", + "Run ${ARG1} laps. Your entire team has to finish.": "Rún ${ARG1} lápz. Yóur éntzre téam has tó fznish.", + "Run 1 lap.": "Rún 1 láp.", + "Run 1 lap. Your entire team has to finish.": "Rún 1 láp. Yóur éntire téam has tó fznish.", + "Run real fast!": "Rún réaz fázt", + "Score ${ARG1} goals.": "Scóré ${ARG1} góalz.", + "Score ${ARG1} touchdowns.": "Scóré ${ARG1} tóuchdównz.", + "Score a goal": "Scórz á góal", + "Score a goal.": "Scórz á góal.", + "Score a touchdown.": "Scórz á tóuchdówz.", + "Score some goals.": "Scórz sómz góalz.", + "Secure all ${ARG1} flags.": "Sécúrz álz ${ARG1} flágz.", + "Secure all flags on the map to win.": "Sécurz álz flúgs ón zú máp té wín", + "Secure the flag for ${ARG1} seconds.": "Séczre thé flág fúr ${ARG1} sécúnds.", + "Secure the flag for a set length of time.": "Séczré thé flúg fzr á sét lzngth óf túme.", + "Steal the enemy flag ${ARG1} times.": "Stéál thé enémy flág ${ARG1} tímés.", + "Steal the enemy flag.": "Stéál thé ezémy flúg", + "There can be only one.": "Théré cán bé ónlz óne.", + "Touch the enemy flag ${ARG1} times.": "Toúch thé enémy flág ${ARG1} tímés.", + "Touch the enemy flag.": "Tóúcz thé ezémy flúg", + "carry the flag for ${ARG1} seconds": "cárrz thé flág fór ${ARG1} sécóndz", + "kill ${ARG1} enemies": "kílz ${ARG1} énzmíez", + "last one standing wins": "lást ónz stándíng wíns", + "last team standing wins": "lást tézm stándíng wíns", + "return ${ARG1} flags": "rétúrn ${ARG1} flágz", + "return 1 flag": "rétúrn 1 flág", + "run ${ARG1} laps": "rún ${ARG1} lápz", + "run 1 lap": "rún 1 láp", + "score ${ARG1} goals": "Scóré ${ARG1} góalz", + "score ${ARG1} touchdowns": "scórz ${ARG1} tóuchdównz", + "score a goal": "scórz á góal", + "score a touchdown": "scóre á tóuchdówn", + "secure all ${ARG1} flags": "sécúrz álz ${ARG1} flágz", + "secure the flag for ${ARG1} seconds": "séczre thé flág fúr ${ARG1} sécúnds", + "touch ${ARG1} flags": "tóúch ${ARG1} flúgz", + "touch 1 flag": "tóúch 1 flúg" + }, + "gameNames": { + "Assault": "Azzáulz", + "Capture the Flag": "Cápzuré thé Flágz", + "Chosen One": "Chzénz Ónez", + "Conquest": "Cónquzézt", + "Death Match": "Déátzh Mátczh", + "Easter Egg Hunt": "Eogwiej owije ghuf", + "Elimination": "Élímznizaín", + "Football": "Fútbolz", + "Hockey": "Hóckzy", + "Keep Away": "Kéép Awáz", + "King of the Hill": "Kíng óf thz Hílz", + "Meteor Shower": "Mtrlrlr Shghglzrz", + "Ninja Fight": "Njnff Fiffz", + "Onslaught": "Ónsláughtz", + "Race": "Ráze", + "Runaround": "Rúnároundz", + "Target Practice": "Tafo Prajcfz", + "The Last Stand": "Thú Lzst Stándz" + }, + "inputDeviceNames": { + "Keyboard": "Kblflzfz", + "Keyboard P2": "Kzboardz P2z" + }, + "languages": { + "Arabic": "Aroijwe", + "Belarussian": "Blfrurzfozz", + "Chinese": "Choifwef Soimcwoef", + "ChineseTraditional": "Cheifwoefjw Trwwefsdfs", + "Croatian": "Crrlzlrrs", + "Czech": "Czffef", + "Danish": "Dnishsdl", + "Dutch": "Dtchjdflz", + "English": "Englfjlzjsh", + "Esperanto": "Esprorjjzlz", + "Finnish": "Fnnizhsh", + "French": "Frnzhfhn", + "German": "Grmmzndn", + "Gibberish": "Gibberish", + "Greek": "Gaofwef", + "Hindi": "Hfjofz", + "Hungarian": "Hngjgozf", + "Indonesian": "Inofiqdson", + "Italian": "Itzllfjssnn", + "Japanese": "Jpndjsjzes", + "Korean": "Kornesnzn", + "Persian": "Psdfsdf", + "Polish": "Pzlishz", + "Portuguese": "Portuguenejs", + "Romanian": "Rmrfoijfzf", + "Russian": "Rzznrsn", + "Serbian": "Socowiejf", + "Slovak": "Slihdtbjoy", + "Spanish": "Snsdnsh", + "Swedish": "Swdiiszh", + "Turkish": "Twfoijwef", + "Ukrainian": "Ukckwef", + "Venetian": "Vwvowefdf", + "Vietnamese": "Vjefowiewer" + }, + "leagueNames": { + "Bronze": "Blfoijzf", + "Diamond": "Dfoifffz", + "Gold": "Glfdf", + "Silver": "Slfjfozz" + }, + "mapsNames": { + "Big G": "Bíg Gz", + "Bridgit": "Brídzgzt", + "Courtyard": "Coúrtyárd", + "Crag Castle": "Crág Cástlé", + "Doom Shroom": "Dóóm Shróóm", + "Football Stadium": "Fóózball Stédzm", + "Happy Thoughts": "Háppy Thóúghts", + "Hockey Stadium": "Hóckzy Stzéimz", + "Lake Frigid": "Lk Frojfz", + "Monkey Face": "Mónkey Fáce", + "Rampage": "Rampágé", + "Roundabout": "Róúndzboót", + "Step Right Up": "Stép Reíghz Úp", + "The Pad": "Thz Péd", + "Tip Top": "Típ Tzp", + "Tower D": "Tówér D", + "Zigzag": "Zigzúzg" + }, + "playlistNames": { + "Just Epic": "Jst Pcpc", + "Just Sports": "Jspsf Zprttz" + }, + "promoCodeResponses": { + "invalid promo code": "invlidf promoz cdfd" + }, + "scoreNames": { + "Flags": "Flágz", + "Goals": "Góálz", + "Score": "Sczórz", + "Survived": "Súrvizvéd", + "Time": "Tímé", + "Time Held": "Tímz Hélz" + }, + "serverResponses": { + "A code has already been used on this account.": "A cfodfj hd cowefj aweof cwjeo fjweo fijw eofjwocij.", + "A reward has already been given for that address.": "Ac wolf wo Code coi weojoifow e ocjw oiejfwer.", + "Account linking successful!": "Accjo link succeosf!", + "Account unlinking successful!": "Accjow cowejowejr sucefwewr!", + "Accounts are already linked.": "Accojif co woie owjilkn.", + "An error has occurred; (${ERROR})": "An cow fc woof wcef; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "An cowed c woefj wcoi woof weiojrwe rj goije ${ERROR})", + "An error has occurred; please contact support@froemling.net.": "An f oco ao ocio wj ; pocj oco woei fsuupo co co ifoiwnet", + "An error has occurred; please try again later.": "An cowjef win woes owe p apowejrowjers.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Ac woef oi eowic woiej fowije foijcwe?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nTHci weo woi efoiw ojo ij!!", + "BombSquad Pro unlocked!": "Boijfdfsjef pon occoijfs!", + "Can't link 2 accounts of this type.": "Cnoj' oi n aoco i ot oaifftz.", + "Can't link 2 diamond league accounts.": "Cnoi' oin koci o2 doi co fla cairjrs.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Cn't coi oi; could up vo io ${COUNT} oi c ioicoicuors.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Chowcj eof weoc jco; oe jwoeijf wojcwjeowfj woejf o${COUNT} dzf.", + "Could not establish a secure connection.": "Cjfoi cn ooi a tac c oweocoicoinr.", + "Daily maximum reached.": "Dfilaf mciwfjiwf rechced.", + "Entering tournament...": "Ernwoefijweo jfowjefw...", + "Invalid code.": "Invflijf cddz.", + "Invalid payment; purchase canceled.": "Info ejcwpeopwer; purwup cowefjwef.", + "Invalid promo code.": "Ivnfjfo pmpwf cdffz.", + "Invalid purchase.": "Ivnifjf cnwerojf.", + "Invalid tournament entry; score will be ignored.": "Invoc oic owe fo; co ot coi aingoired.", + "Item unlocked!": "Iwerw cowefjwoeijwer!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "LINKING FOFJWEF. ${ACCOUNT} o iwjof\nowe oijwe c woeoijwf oAL FJOEJI OCJW.\nYou off leojwoer wcowe foiwjefojweoiwf\n(a c owfw oefjTHIS c weoojw oesf)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Lcjwe fwofj o ${ACCOUNT} to cowejfweof?\nAll wefoiwejtthosjic w ${ACCOUNT} cowiejf tjsl.\nThis wefowondot onsof. Aojr tyocu wsure?", + "Max number of playlists reached.": "Mx nfmow oijeplyalfaf rcnoahfd.", + "Max number of profiles reached.": "Mc fjpasdofj c owiejfowe oiwjefsd.", + "Maximum friend code rewards reached.": "Cmwoe oc wo Ego wel jacoweij weer.", + "Message is too long.": "CMew ociwje owe el.", + "Profile \"${NAME}\" upgraded successfully.": "Profjojf \"${NAME}\" oupfuap coj woijsfsf.", + "Profile could not be upgraded.": "Profojfo coild onot foj bupfrade.", + "Purchase successful!": "Pcjofj scwcserfflz!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Rclfjoc ${COUNT} otjoa ojofj oweijf.\nComcowef ocw weofjwefjowef ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Sowejr cowj fwoe co wefj owe f c woefijw ego wejfoiwjejwfjoweiwfe;\nPowijfw c wejfwoij oiwje wr erowijrowe.", + "Sorry, there are no uses remaining on this code.": "Srrryr, the war no cufowief rime woijwof con etude.", + "Sorry, this code has already been used.": "Srrc, thiz codds has aoiwjre bndd usdd.", + "Sorry, this code has expired.": "Sorrro, thi cowf ahs arowjers.", + "Sorry, this code only works for new accounts.": "Srror, this cod ojfowf work for know accarrn.", + "Temporarily unavailable; please try again later.": "Tmwoef wf oucwof wf; cowejf awoj cwoijers.", + "The tournament ended before you finished.": "Thf weoijw oeij aoejf aowejf owjeof aiwjeofjwef.", + "This account cannot be unlinked for ${NUM} days.": "Th ow co ref w owjoso o ${NUM} dyafsoef.", + "This code cannot be used on the account that created it.": "Thiz codf cow oicna weoiw oaijwoefoa sdoausd cjoeri wod.", + "This is currently unavailable; please try again later.": "Tjwoe jej c fjwoief uynvoiejfowij; plaza ffjweo. ona oifowijdfld.", + "This requires version ${VERSION} or newer.": "Thif efowjefo f${VERSION} or wofjweor.", + "Tournaments disabled due to rooted device.": "Toiwfj woicw few doff our r woiwjeowods.", + "Tournaments require ${VERSION} or newer": "Toijfw qojwce ${VERSION} or wcoiwej", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Uncljwf ${ACCOUNT} cowed wthosijf?\nAll cowejt ocjiwf ${ACCOUNT} cowiest dois.\n(expo fowefj etwohoa tand Costers)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "WARNOEF: com owe fw epfojwe. cj weo fjwefo wejfo wef jocij eowiefwef.\nCoaj owe fwefjwoo oweoifwoe fj ewrj woc. Ppel fwo c worywwr.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Wocj weo ocj woiej fowj ofj aioj ojt;oi df joijwotijs?\n\nYoc uweof c owej owijf ${ACCOUNT1}\nThoci wef coj woeijf ${ACCOUNT2}\n\nThoc jweo owej fow cwi efod odo focwoe.\nWocj fo : cwo c weof odo fauoino!", + "You already own this!": "Yz fwowefoi coiweno fjoz!", + "You can join in ${COUNT} seconds.": "You won cowier fwoef ${COUNT} coiwejf.", + "You don't have enough tickets for this!": "Yz dfoiwf ahv weoj fwoitjoicker fz thzz!", + "You don't own that.": "You loci jeff jwoefwe.", + "You got ${COUNT} tickets!": "Y zfowej f${COUNT{ ticeofjwe!", + "You got a ${ITEM}!": "Yzz gtzz z ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Yf co efoj woef woecj owejfoiwef lfj ; congaroiwjf woes!", + "You must update to a newer version of the app to do this.": "Yz mocu upc oig owc owt oc o ca; ;apc oi oj ;oj.", + "You must update to the newest version of the game to do this.": "Yocwe fweoowo weotjosij;ow. woeower weroso taotoautats.", + "You must wait a few seconds before entering a new code.": "Yzz msfgt wt a fz cwcfwf bfrrzz entef wcnfz cdszz.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Yz wf ooiwef #${RANK} ofijwe oijw oijef . Thansf afpaflalay!", + "Your account was rejected. Are you signed in?": "Yoew cowcjwe woe woeiowirwo eirjw co weoijfow ej?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Yr coijw epowf oief oaefoiwfowjef\nPlejasefoaw efojw oefjwoejo aciowejrow.", + "Your friend code was used by ${ACCOUNT}": "Yrrj cof owf owcjowj fwoafeof ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Mínuzz", + "1 Second": "1 Scfmfz", + "10 Minutes": "10 Mínuzz", + "2 Minutes": "2 Mínuzz", + "2 Seconds": "2 Sfljcjfsz", + "20 Minutes": "20 Mínuzz", + "4 Seconds": "4 Sfcljfdz", + "5 Minutes": "5 Mínuzz", + "8 Seconds": "8 Sfcljrjdz", + "Allow Negative Scores": "Alllzz Nglfljcw Scorrzz", + "Balance Total Lives": "Bálncz Tótzl Líves", + "Bomb Spawning": "SOCj efoijw come", + "Chosen One Gets Gloves": "Chózn One Gétz Glóvz", + "Chosen One Gets Shield": "Chózn One Gétz Shíéld", + "Chosen One Time": "Chzénz Ónz Tímz", + "Enable Impact Bombs": "Enfojf Impac BombS", + "Enable Triple Bombs": "Enafojf Tapowef Bobms", + "Entire Team Must Finish": "Enfowief Tem Musc Fiifsf", + "Epic Mode": "Épizc Módz", + "Flag Idle Return Time": "Flág Ídle Rétúrn Tímz", + "Flag Touch Return Time": "Flág Tzch Rétúrn Tímz", + "Hold Time": "Hólz Tímz", + "Kills to Win Per Player": "Kílls tó Wín Pér Plzáyer", + "Laps": "Lápz", + "Lives Per Player": "Lívz Pérz Pláyrr", + "Long": "Lónggz", + "Longer": "Lóngrzz", + "Mine Spawning": "Míne Spáwning", + "No Mines": "Nz Mfjfnzl", + "None": "Núnz", + "Normal": "Nórmzll", + "Pro Mode": "Por Msdf", + "Respawn Times": "Rézpnzz Tímzz", + "Score to Win": "Scúrz té Wínz", + "Short": "Shórtz", + "Shorter": "Shórtzrr", + "Solo Mode": "Sólzo Módz", + "Target Count": "Tfof Coufnfz", + "Time Limit": "Tímz Límtzz" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} oj who cos weoijwoeij c woes ${PLAYER} cjowef", + "Killing ${NAME} for skipping part of the track!": "Kllzng ${NAME} fr ckpoing ocij epo jwof jat!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Wjocief cwf ${NAME}: jojwo / ocjo efoiwefj wcoweok owerjoijdof." + }, + "teamNames": { + "Bad Guys": "Bázd Gúyzs", + "Blue": "Blzúe", + "Good Guys": "Gzóod Gúys", + "Red": "Réd" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Á perfectly timed running-jumping-zpin-punch cán kill in á zingle hit\nánd eárn yóu lifelóng rezpect fróm yóur friendz.", + "Always remember to floss.": "Álwáyz remember tó flózz.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Creáte pláyer prófilez fór yóurzelf ánd yóur friendz with\nyóur preferred námez ánd áppeáráncez inzteád óf uzing rándóm ónez.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Curze bóxez turn yóu intó á ticking time bómb.\nThe ónly cure iz tó quickly gráb á heálth-páck.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Dezpite their lóókz, áll chárácterz' ábilitiez áre identicál,\nzó juzt pick whichever óne yóu mózt clózely rezemble.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Dón't get tóó cócky with thát energy zhield; yóu cán ztill get yóurzelf thrówn óff á cliff.", + "Don't run all the time. Really. You will fall off cliffs.": "Dón't run áll the time. Reálly. Yóu will fáll óff cliffz.", + "Don't spin for too long; you'll become dizzy and fall.": "Dino pcoijw cow erowic ow; ocuwe fowei owie roan faf.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Hóld ány buttón tó run. (Trigger buttónz wórk well if yóu háve them)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Hóld dówn ány buttón tó run. Yóu'll get plácez fázter\nbut wón't turn very well, zó wátch óut fór cliffz.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ice bómbz áre nót very pówerful, but they freeze\nwhóever they hit, leáving them vulneráble tó zháttering.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "If zómeóne pickz yóu up, punch them ánd they'll let gó.\nThiz wórkz in reál life tóó.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Iffz oc wo acoiw oifj woc ojef '${REMOTE_APP_NAME}' ars\noc of code pow c owe fowjfoijweojc oocjwef.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "If yóu áre zhórt ón cóntróllerz, inztáll the 'BómbZquád Remóte' ápp\nón yóur iÓZ ór Ándróid devicez tó uze them áz cóntróllerz.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "If yóu get á zticky-bómb ztuck tó yóu, jump áróund ánd zpin in circlez. Yóu might\nzháke the bómb óff, ór if nóthing elze yóur lázt mómentz will be entertáining.", + "If you kill an enemy in one hit you get double points for it.": "If yóu kill án enemy in óne hit yóu get dóuble póintz fór it.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "If yóu pick up á curze, yóur ónly hópe fór zurvivál iz tó\nfind á heálth pówerup in the next few zecóndz.", + "If you stay in one place, you're toast. Run and dodge to survive..": "If yóu ztáy in óne pláce, yóu're tóázt. Run ánd dódge tó zurvive..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "If yóu've gót lótz óf pláyerz cóming ánd góing, turn ón 'áutó-kick-idle-pláyerz'\nunder zettingz in cáze ányóne fórgetz tó leáve the gáme.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Iw fowj eoaj wecojaw ;eofja ;woefj ;aowiejoawjef;oajw;eofja; wef\naoj etoaj ;cojwe;aoj ef;oawe; faw;oe fj;aow e;owjef;oajf", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "If yóur frámeráte iz chóppy, try turning dówn rezólutión\nór vizuálz in the gáme'z gráphicz zettingz.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "In Cápture-the-Flág, yóur ówn flág muzt be át yóur báze tó zcóre, If the óther\nteám iz ábóut tó zcóre, zteáling their flág cán be á góód wáy tó ztóp them.", + "In hockey, you'll maintain more speed if you turn gradually.": "In hóckey, yóu'll máintáin móre zpeed if yóu turn gráduálly.", + "It's easier to win with a friend or two helping.": "It'z eázier tó win with á friend ór twó helping.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Jump juzt áz yóu're thrówing tó get bómbz up tó the highezt levelz.", + "Land-mines are a good way to stop speedy enemies.": "Lánd-múnes áré z gúod wáy tó stóp spéédy enémíes.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Mány thingz cán be picked up ánd thrówn, including óther pláyerz. Tózzing\nyóur enemiez óff cliffz cán be án effective ánd emótiónálly fulfilling ztrátegy.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nó, yóu cán't get up ón the ledge. Yóu háve tó thrów bómbz.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Plref can join and flasdj afl ion the mdc aosdfoasdf,\naodf sdoyuou can aodfj asdfpluf asdna moasdfp sdfocnor.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Pláyerz cán jóin ánd leáve in the middle óf mózt gámez,\nánd yóu cán álzó plug ánd unplug gámepádz ón the fly.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Pówerupz ónly háve time limitz in có-óp gámez.\nIn teámz ánd free-fór-áll they're yóurz until yóu die.", + "Practice using your momentum to throw bombs more accurately.": "Práctice uzing yóur mómentum tó thrów bómbz móre áccurátely.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Punchez dó móre dámáge the fázter yóur fiztz áre móving,\nzó try running, jumping, ánd zpinning like crázy.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Run báck ánd fórth befóre thrówing á bómb\ntó 'whiplázh' it ánd thrów it fárther.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Táke óut á gróup óf enemiez by\nzetting óff á bómb neár á TNT bóx.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "The heád iz the mózt vulneráble áreá, zó á zticky-bómb\ntó the nóggin uzuálly meánz gáme-óver.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Thiz level never endz, but á high zcóre here\nwill eárn yóu eternál rezpect thróughóut the wórld.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Thrów ztrength iz bázed ón the directión yóu áre hólding.\nTó tózz zómething gently in frónt óf yóu, dón't hóld ány directión.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Trrd fo toasdoasdfm? Ararap cale fasoif;osda!\nSe sdfoajs dof cjspdof apsodi paosdjf", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Try 'Cóóking óff' bómbz fór á zecónd ór twó befóre thrówing them.", + "Try tricking enemies into killing eachother or running off cliffs.": "Try tricking enemiez intó killing eáchóther ór running óff cliffz.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Uze the pick-up buttón tó gráb the flág < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Whip báck ánd fórth tó get móre diztánce ón yóur thrówz..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Yóu cán 'áim' yóur punchez by zpinning left ór right.\nThiz iz uzeful fór knócking bád guyz óff edgez ór zcóring in hóckey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Yóu cán judge when á bómb iz góing tó explóde bázed ón the\ncólór óf zpárkz fróm itz fuze: yellów..óránge..red..BÓÓM.", + "You can throw bombs higher if you jump just before throwing.": "Yóu cán thrów bómbz higher if yóu jump juzt befóre thrówing.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Yóu dón't need tó edit yóur prófile tó chánge chárácterz; Juzt prezz the tóp\nbuttón (pick-up) when jóining á gáme tó óverride yóur defáult.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Yóu táke dámáge when yóu wháck yóur heád ón thingz,\nzó try tó nót wháck yóur heád ón thingz.", + "Your punches do much more damage if you are running or spinning.": "Yóur punchez dó much móre dámáge if yóu áre running ór zpinning." + } + }, + "trialPeriodEndedText": "Yozr tríal púrizd hás úndzd. Wóuld yóu lúke\ntz pórchúse BómbSquád ánd kéép plzying?", + "trophiesRequiredText": "This cow fwoeif of ${NUMBER} cowejfwe", + "trophiesText": "Tjfpojfz", + "trophiesThisSeasonText": "Trphpc Thzi Ssrcon", + "tutorial": { + "cpuBenchmarkText": "Rnnging tuoriral fan lurkdc-spdd (primirarl ytest CPU spd)", + "phrase01Text": "Hz Thrzfl!", + "phrase02Text": "Wlgjlmcz tz ${APP_NAME}!", + "phrase03Text": "Hrllz a fjflf tijpefa fjor conotrlaij yoru chchrirzzzr:", + "phrase04Text": "Mntn thngo foin boia${APP_NAME}foibajsfdo aoh PCHYRALZ bajffz.", + "phrase05Text": "Frjo exnapfdnp, whfz ynz pnpchru...", + "phrase06Text": "..dmglaf iz bzfz onf zpdfz ofz yrz fzfassz.", + "phrase07Text": "Szz.? Wz wrjogz mvojgsfz so that barelzl hurt ${NAME}.", + "phrase08Text": "Nwz ltzf jmpz n spnz tz gtz mrz spdfrz.", + "phrase09Text": "Ahz thzjo's b;rtjz.", + "phrase10Text": "Rngjgojz hpzpoijr too.", + "phrase11Text": "Hzljt fjofj ANZ butlzf tz rnzrlz.", + "phrase12Text": "Frz txpro-apwez pnpghch, trz rnognp ANZ pafong;z.", + "phrase13Text": "Whppz; srryz 'bout that ${NAME}.", + "phrase14Text": "Yz cnz pick ups nz trhowz thgfz szhc az flglafz.. or ${NAME}.", + "phrase15Text": "Lasfzjtoz, thzfzo bmjgz.", + "phrase16Text": "Throwzlf bmgzz tkzgfz prcgjtoz.", + "phrase17Text": "Oucz! Nzt a vrz gdz thrworz.", + "phrase18Text": "Mvngofj hpzrz yz throwz fratherz.", + "phrase19Text": "Jmpngpg hpz yz thorwz highrzr.", + "phrase20Text": "\"whoppers\" yrs bomgjl for enve longer thowawrz.", + "phrase21Text": "Tmingo yr bombos cnz bs trixkrz.", + "phrase22Text": "Dngz.", + "phrase23Text": "Try \"ckkzllj off\" thz fuze frs a scrnd orz stwoz.", + "phrase24Text": "Hrrzrlar! Nirlefzl cokkred.", + "phrase25Text": "Wllz, thrat's jztst borts itz.", + "phrase26Text": "Nowz gz get'z em tzgrrs!", + "phrase27Text": "Rembjgeorz yrz trnings, andz yz WLLZ cmd bazk alvss!", + "phrase28Text": "..wllz.mbbfyz..", + "phrase29Text": "Gddz lkxks!", + "randomName1Text": "Fréz", + "randomName2Text": "Hurrzl", + "randomName3Text": "Bllfz", + "randomName4Text": "Chrjrzl", + "randomName5Text": "Phllz", + "skipConfirmText": "Rlldf asdf oajc sdjf asodf asdf? dfj oasidj foasdc.", + "skipVoteCountText": "${COUNT}/${TOTAL} skpzj vnttjzlz", + "skippingText": "skpzjflj toutoroafjz...", + "toSkipPressAnythingText": "(tpz rz pzrzl anthfljzf tz skpfz tuzfjrlrrz)" + }, + "twoKillText": "DÓÚBLZ KÍLL!", + "unavailableText": "unavlfldsfjlbz", + "unconfiguredControllerDetectedText": "Uncónfzgúred cúntrzllír dítzctíd:", + "unlockThisInTheStoreText": "Thz mf voi eunlcoef owef joiefsfrwe.", + "unlockThisProfilesText": "To cowier co we ${NUM} pcoer, cow oicoj:", + "unlockThisText": "Tz ncowifj ocje, ycon fneeds:", + "unsupportedHardwareText": "Srrz, thz hardwrz isz nt spprted bz thz vejerelr dlzl gmpzz.", + "upFirstText": "Uzp férzt:", + "upNextText": "Úp nézt ín gámz ${COUNT}:", + "updatingAccountText": "Updfoifjz yrrl cacmrmr...", + "upgradeText": "Upgrorz", + "upgradeToPlayText": "Upgglf tz \"${PRO}\" iz j wojcwoef paljf zs.", + "useDefaultText": "Uzl Dflfjtzlz", + "usesExternalControllerText": "Thz gjf coiwjef oif owicoiwefj owejf owejoojof", + "usingItunesText": "Ufwefw Mfwoef co ef srnweoicjowe...", + "usingItunesTurnRepeatAndShuffleOnText": "Plzelz mkdk srzlc shfflds isON anz andpreld is ALZ unz iTunes", + "validatingBetaText": "Válúdztíng Bztá...", + "validatingTestBuildText": "Vldfjdfoi jtese-bsdfasfd...", + "victoryText": "Vúctzry!", + "voteDelayText": "You caecowe oefo wocj woeiwo ${NUMBER} wocijwoef.", + "voteInProgressText": "A voiajf coiwje fwooeio wcwer.", + "votedAlreadyText": "C who foil owejrs", + "votesNeededText": "${NUMBER} vowjeowi jw oefwe", + "vsText": "vús.", + "waitingForHostText": "(twatijf ffarz ${HOST} to cnfojoweifz)", + "waitingForLocalPlayersText": "wzzéatnng fér lzcal pzéayers...", + "waitingForPlayersText": "wtnginag frz plers to jnnz..", + "waitingInLineText": "Wowcij cowije fo wf(powc owe oweir)...", + "watchAVideoText": "Few A covjowdf", + "watchAnAdText": "e aowej ocjowef", + "watchWindow": { + "deleteConfirmText": "Delzdfe \"${REPLAY}\"?", + "deleteReplayButtonText": "Dlrlrjz\nRplrlz", + "myReplaysText": "Mz Rplgllz", + "noReplaySelectedErrorText": "Nz Rplglz Slelectdz", + "playbackSpeedText": "Plwecowi jef SPcowef ${SPEED}", + "renameReplayButtonText": "Rnrmz\nRplrlrz", + "renameReplayText": "Rnmgm \"${REPLAY}\" tz:", + "renameText": "Rnzmz", + "replayDeleteErrorText": "Errlz dlernlg rmllzlz", + "replayNameText": "Rpprlz Nmz", + "replayRenameErrorAlreadyExistsText": "A rnofk wug h af c adfkahf wcuiuhz,", + "replayRenameErrorInvalidName": "Can'f co fojo sf oiejc n vailid anme.", + "replayRenameErrorText": "Errz rnemvifj rpcojz.", + "sharedReplaysText": "Srhrrm Rplclrlz.", + "titleText": "Wtcchs", + "watchReplayButtonText": "Wtchf\nRplzz" + }, + "waveText": "Wávz", + "wellSureText": "Wélz Súre!", + "wiimoteLicenseWindow": { + "licenseText": "Copyríght (c) 2007, DarwíínRúmotú Túam\nAll ríghtz rúzúrvúd.\n\n Rúdíztríbutíon and uzú ín zourcú and bínary formz, wíth or wíthout modífícatíon,\n arú púrmíttúd provídúd that thú followíng condítíonz arú mút:\n\n1. Rúdíztríbutíonz of zourcú codú muzt rútaín thú abovú copyríght notícú, thíz\n lízt of condítíonz and thú followíng dízclaímúr.\n2. Rúdíztríbutíonz ín bínary form muzt rúproducú thú abovú copyríght notícú, thíz\n lízt of condítíonz and thú followíng dízclaímúr ín thú documúntatíon and/or othúr\n matúríalz provídúd wíth thú díztríbutíon.\n3. Núíthúr thú namú of thíz projúct nor thú namúz of ítz contríbutorz may bú uzúd to\n úndorzú or promotú productz dúrívúd from thíz zoftwarú wíthout zpúcífíc príor\n wríttún púrmízzíon.\n\nTHÍZ ZOFTWARÚ ÍZ PROVÍDÚD BY THÚ COPYRÍGHT HOLDÚRZ AND CONTRÍBUTORZ \"AZ ÍZ\"\nAND ANY ÚXPRÚZZ OR ÍMPLÍÚD WARRANTÍÚZ, ÍNCLUDÍNG, BUT NOT LÍMÍTÚD TO, THÚ\nÍMPLÍÚD WARRANTÍÚZ OF MÚRCHANTABÍLÍTY AND FÍTNÚZZ FOR A PARTÍCULAR PURPOZÚ\nARÚ DÍZCLAÍMÚD. ÍN NO ÚVÚNT ZHALL THÚ COPYRÍGHT OWNÚR OR CONTRÍBUTORZ BÚ\nLÍABLÚ FOR ANY DÍRÚCT, ÍNDÍRÚCT, ÍNCÍDÚNTAL, ZPÚCÍAL, ÚXÚMPLARY, OR\nCONZÚQUÚNTÍAL DAMAGÚZ (ÍNCLUDÍNG, BUT NOT LÍMÍTÚD TO, PROCURÚMÚNT OF\n ZUBZTÍTUTÚ GOODZ OR ZÚRVÍCÚZ; LOZZ OF UZÚ, DATA, OR PROFÍTZ; OR BUZÍNÚZZ\nÍNTÚRRUPTÍON) HOWÚVÚR CAUZÚD AND ON ANY THÚORY OF LÍABÍLÍTY, WHÚTHÚR ÍN\nCONTRACT, ZTRÍCT LÍABÍLÍTY, OR TORT (ÍNCLUDÍNG NÚGLÍGÚNCÚ OR OTHÚRWÍZÚ)\nARÍZÍNG ÍN ANY WAY OUT OF THÚ UZÚ OF THÍZ ZOFTWARÚ, ÚVÚN ÍF ADVÍZÚD OF THÚ\nPOZZÍBÍLÍTY OF ZUCH DAMAGÚ.\n", + "licenseTextScale": 0.62, + "titleText": "DarwiónReéoze Czpyrúght" + }, + "wiimoteListenWindow": { + "listeningText": "Líszéngng Fúr Wíímztes...", + "pressText": "Prész Wíimzte bóttzns 1 azd 2 símultáneoúsly.", + "pressText2": "Onz néwer Wiímótes wíth Moóión Plús búilt ón, przss thé réd 'sync' búttón ón thé bzck ónstzad.", + "pressText2Scale": 0.55, + "pressTextScale": 1.0 + }, + "wiimoteSetupWindow": { + "copyrightText": "DúrwéinRemzote Cúpyrzght", + "copyrightTextScale": 0.6, + "listenText": "Lústzn", + "macInstructionsText": "Makú zurú your Wáá áz off and Bluútooth áz únablúd\non your Mac, thún prúzz 'Láztún'. Wáámotú zupport can\nbú a bát flaky, zo you may havú to try a fúw támúz\nbúforú you gút a connúctáon.\n\nBluútooth zhould handlú up to 7 connúctúd dúvácúz,\nthough your málúagú may vary.\n\nBombZquad zupportz thú orágánal Wáámotúz, Nunchukz,\nand thú Clazzác Controllúr.\nThú núwúr Wáá Rúmotú Pluz now workz too\nbut not wáth attachmúntz.", + "macInstructionsTextScale": 0.7, + "thanksText": "Thznks té thz DérwiinRémote táam\nFúr máking thés pzsséble.", + "thanksTextScale": 0.8, + "titleText": "Wzimóte Sztúp" + }, + "winsPlayerText": "${NAME} Wnzpl!", + "winsPluralText": "${NAME} WznflPP!", + "winsSingularText": "${NAME} WinzfjlzSS!", + "winsTeamText": "${NAME} Wnttm!", + "winsText": "${NAME} Wínsz!", + "worldScoresUnavailableText": "Wrlzld scrzzl unvlfldsjbzl.", + "worldsBestScoresText": "Wúrld's Bést Scórzs", + "worldsBestTimesText": "Wúrld's Bzst Tímés", + "xbox360ControllersWindow": { + "getDriverText": "Gét Drúvzr", + "macInstructions2Text": "Tá uzá cántrállárz wirálázzly, yáu'll alzá náád thá rácáivár that\ncámáz with thá 'Xbáx 360 Wirálázz Cántrállár fár Windáwz'.\nÁná rácáivár alláwz yáu tá cánnáct up tá 4 cántrállárz.\n\nImpártant: 3rd-party rácáivárz will nát wárk with thiz drivár;\nmaká zurá yáur rácáivár zayz 'Micrázáft' án it, nát 'XBÁX 360'.\nMicrázáft ná lángár zállz tházá záparatály, zá yáu'll náád tá gát\nthá áná bundlád with thá cántrállár ár álzá záarch ábay.\n\nIf yáu find thiz uzáful, pláazá cánzidár a dánatián tá thá\ndrivár dáválápár at hiz zitá.", + "macInstructions2TextScale": 0.76, + "macInstructionsText": "Tó uzá Xbóx 360 cóntróllárz, yóu'll náád tó inztall\nthá Mac drivár availablá at thá link bálów.\nIt wórkz with bóth wirád and wirálázz cóntróllárz..", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "Tz úze wéred Xbzx 360 czntrzllerz wéth yzúr ZÚYA, zémply\nplúg them én the ZÚYA'z ÚZB pzrt. Yzú can úze a ÚZB húb\ntz cznnect múltéple czntrzllerz.\n\nTz úze wérelezz czntrzllerz yzú'll need a wérelezz receéver,\navaélable az part zf the \"Xbzx 360 wérelezz Czntrzller fzr Wéndzwz\"\npackage zr zzld zeparately. Each receéver plúgz éntz a ÚZB pzrt and\nallzwz yzú tz cznnect úp tz 4 wérelezz czntrzllerz..", + "ouyaInstructionsTextScale": 0.8, + "titleText": "Uzíng Xbóx 360 Cónztróllzrs wzth ${APP_NAME}:" + }, + "yesAllowText": "Yús, Allúwz!", + "yourBestScoresText": "Yózr Bést Scúrzszz", + "yourBestTimesText": "Yózr Bést Tímés" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/greek.json b/dist/ba_data/data/languages/greek.json new file mode 100644 index 0000000..3942da1 --- /dev/null +++ b/dist/ba_data/data/languages/greek.json @@ -0,0 +1,1828 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Τα ονόματα δεν μπορούν να περιέχουν emoji ή άλλους ειδικούς χαρακτήρες", + "accountsText": "Λογαριασμοί", + "achievementProgressText": "Επιτεύγματα: ${COUNT} από ${TOTAL}", + "campaignProgressText": "Πρόοδος Ιστορίας [Δύσκολο]: ${PROGRESS}", + "changeOncePerSeason": "Μπορείτε να το αλλάξετε μόνο μία φορά ανά σεζόν.", + "changeOncePerSeasonError": "Πρέπει να περιμένετε μέχρι την επόμενη σεζόν για να το αλλάξετε ξανά (${NUM} days)", + "customName": "Προσαρμοσμένο Όνομα", + "linkAccountsEnterCodeText": "Εισάγετε Κωδικό", + "linkAccountsGenerateCodeText": "Δημιουργήστε Κωδικό", + "linkAccountsInfoText": "(Μοιραστείτε την πρόοδο ανάμεσα σε διάφορες πλατφόρμες)", + "linkAccountsInstructionsNewText": "Για να δεσμεύσετε δύο λογαριασμούς μεταξύ τους, δημιούργήστε έναν\nκωδικό στον πρώτο και εισάγετέ τον στον δεύτερο. Τα δεδομένα \nμεταξύ των δύο θα συγχωνευτούν με τον δεύτερο λογαριασμό.\n(τα δεδομένα από τον πρώτο λογαριασμό θα χαθούν)\n\nΜπορείτε να δεσμεύσετε εώς και ${COUNT} λογαριασμούς.\n\nΣΗΜΑΝΤΙΚΟ: δεσμεύστε μόνο λογαριασμούς που σας ανήκουν!\nΆμα δεσμεύσετε με τον λογαριασμό ενός φίλου σας δεν θα\nμπορείτε να παίξετε online ταυτόχρονα.", + "linkAccountsInstructionsText": "Για να ενώσετε δυο λογαριασμούς, δημιουργήστε κωδικό\nσε έναν από αυτούς και εισάγετέ τον στον άλλο.\nΗ πρόοδος και τα περιεχόμενα θα συγχωνεύθουν.\nΜπορείτε να ενώσεις μέχρι και ${COUNT} λογαριασμούς.\n\nΠροσοχή: η ενέργεια είναι μη αναστρέψιμη!\n\n", + "linkAccountsText": "Δεσμεύστε Λογαριασμούς", + "linkedAccountsText": "Δεσεμευμένοι λογαριασμοί:", + "nameChangeConfirm": "Αλλαγή του ονόματος του λογαριασμού σας σε ${NAME};", + "resetProgressConfirmNoAchievementsText": "Αυτό θα επαναφέρει τη πρόοδο σας στη \"συνεργασία\" και τις\nτοπικές υψηλές σας βαθμολογίες (αλλά όχι τα εισιτήρια).\nΗ ενέργεια είναι μη αντιστρέψιμη. Είστε σίγουροι;", + "resetProgressConfirmText": "Αυτό θα επαναφέρει τη πρόοδο σας στη \"συνεργασία\",\nτα επιτεύγματα και τις τοπικές υψηλές σας\nβαθμολογίες (αλλά όχι τα εισιτήρια). Η ενέργεια\nείναι μη αντιστρέψιμη. Είστε σίγουροι;", + "resetProgressText": "Επαναφορά Προόδου.", + "setAccountName": "Ορισμός Ονόματος Λογαριασμού", + "setAccountNameDesc": "Επιλέξτε το όνομα που θα φαίνεται στο λογαριασμό σας. Μπορείτε\nνα χρησιμοποιήσετε το όνομα ενός από τους δεσμευμένους σας\nλογαριασμούς ή να δημιουργήσετε ένα μοναδικό, προσαρμοσμένο όνομα.", + "signInInfoText": "Συνδεθείτε για να συλλέξετε εισητήρια, να συναγωνιστείτε στο διαδίκτυο\nκαι να μοιραστείτε τη πρόοδο σας ανάμεσα σε διάφορες συσκευές.", + "signInText": "Σύνδεση", + "signInWithDeviceInfoText": "(ένας λογαριασμός μονάχα διαθέσιμος από αυτή τη συσκευή)", + "signInWithDeviceText": "Σύνδεση με λογαριασμό συσκευής", + "signInWithGameCircleText": "Σύνδεση με Game Circle", + "signInWithGooglePlayText": "Σύνδεση με Google Play", + "signInWithTestAccountInfoText": "(προσωρινός λογαριασμός. Δημιουργήστε λογαριασμό συσκευής για συνέχιση προόδου)", + "signInWithTestAccountText": "Σύνδεση με δοκιμαστικό λογαριασμό", + "signOutText": "Αποσύνδεση", + "signingInText": "Σύνδεση...", + "signingOutText": "Αποσύνδεση...", + "ticketsText": "Εισιτήρια: ${COUNT}", + "titleText": "Λογαριασμός", + "unlinkAccountsInstructionsText": "Επιλέξτε έναν λογαριασμό για αποδέσμευση", + "unlinkAccountsText": "Αποδέσμευση Λογαριασμών", + "viaAccount": "(μέσω λογαριασμού ${NAME})", + "youAreSignedInAsText": "Είστε συνδεδεμένοι ως:" + }, + "achievementChallengesText": "Προκλήσεις Επιτευγμάτων", + "achievementText": "Επίτευγμα", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Σκοτώστε 3 κακούς με TNT", + "descriptionComplete": "Σκοτώσατε 3 κακούς με TNT", + "descriptionFull": "Σκοτώστε 3 κακούς με TNT στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοτώσατε 3 κακούς με TNT στο επίπεδο ${LEVEL}", + "name": "Ο Δυναμίτης Κάνει Μπούμ" + }, + "Boxer": { + "description": "Νικήστε χωρίς να χρησιμοποιήσετε βόμβες", + "descriptionComplete": "Νικήσατε χωρίς να χρησιμοποιήσετε βόμβες", + "descriptionFull": "Ολοκληρώστε το επίπεδο ${LEVEL} χωρίς να χρησιμοποιήσετε βόμβες", + "descriptionFullComplete": "Ολοκληρώσατε το επίπεδο ${LEVEL} χωρίς να χρησιμοποιήσετε βόμβες", + "name": "Πυγμάχος" + }, + "Dual Wielding": { + "descriptionFull": "Συνδέστε 2 χειριστήρια (υλισμικό ή εφαρμογή)", + "descriptionFullComplete": "Συνδέσατε 2 χειριστήρια (υλισμικό ή εφαρμογή)", + "name": "Διπλό Κράτημα" + }, + "Flawless Victory": { + "description": "Νικήστε χωρίς να χτυπηθείτε", + "descriptionComplete": "Νικήσατε χωρίς να χτυπηθείτε", + "descriptionFull": "Νικήστε το επίπεδο ${LEVEL} χωρίς να χτυπηθείτε", + "descriptionFullComplete": "Νικήσατε το επίπεδο ${LEVEL} χωρίς να χτυπηθείτε", + "name": "Άψογη Νίκη" + }, + "Free Loader": { + "descriptionFull": "Ξεκινήστε ένα παιχνίδι Free-For-All με 2+ παίκτες", + "descriptionFullComplete": "Ξεκινήσατε ένα παιχνίδι Free-For-All με 2+ παίκτες", + "name": "Είσοδος Ελεύθερη" + }, + "Gold Miner": { + "description": "Σκοτώστε 6 κακούς με νάρκες-εδάφους", + "descriptionComplete": "Σκοτώσατε 6 κακούς με νάρκες-εδάφους", + "descriptionFull": "Σκοτώστε 6 κακούς με νάρκες-εδάφους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοτώσατε 6 κακούς με νάρκες-εδάφους στο επίπεδο ${LEVEL}", + "name": "Χρυσορύχος" + }, + "Got the Moves": { + "description": "Νικήστε χωρίς να χρησιμοποιήσετε γροθιές ή βόμβες", + "descriptionComplete": "Νικήσατε χωρίς να χρησιμοποιήσετε γροθιές ή βόμβες", + "descriptionFull": "Νικήστε το επίπεδο ${LEVEL} χωρίς να χρησιμοποιήσετε γροθιές ή βόμβες", + "descriptionFullComplete": "Νικήσατε το επίπεδο ${LEVEL} χωρίς να χρησιμοποιήσετε γροθιές ή βόμβες", + "name": "Επιδέξιες Κινήσεις" + }, + "In Control": { + "descriptionFull": "Συνδέστε ένα χειριστήριο (υλισμικό ή εφαρμογή)", + "descriptionFullComplete": "Συνδέσατε ένα χειριστήριο (υλισμικό ή εφαρμογή)", + "name": "Υπό Έλεγχο" + }, + "Last Stand God": { + "description": "Σκοράρετε 1000 πόντους", + "descriptionComplete": "Σκοράρατε 1000 πόντους", + "descriptionFull": "Σκοράρετε 1000 πόντους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοράρατε 1000 πόντους στο επίπεδο ${LEVEL}", + "name": "Θεός του επιπέδου ${LEVEL}" + }, + "Last Stand Master": { + "description": "Σκοράρετε 250 πόντους", + "descriptionComplete": "Σκοράρατε 250 πόντους", + "descriptionFull": "Σκοράρετε 250 πόντους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοράρατε 250 πόντους στο επίπεδο ${LEVEL}", + "name": "Άρχοντας του επιπέδου ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Σκοράρετε 500 πόντους", + "descriptionComplete": "Σκοράρατε 500 πόντους", + "descriptionFull": "Σκοράρετε 500 πόντους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοράρατε 500 πόντους στο επίπεδο ${LEVEL}", + "name": "Μάγος του επιπέδου ${LEVEL}" + }, + "Mine Games": { + "description": "Σκοτώστε 3 κακούς με νάρκες-εδάφους", + "descriptionComplete": "Σκοτώσατε 3 κακούς με νάρκες-εδάφους", + "descriptionFull": "Σκοτώστε 3 κακούς με νάρκες-εδάφους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοτώσατε 3 κακούς με νάρκες-εδάφους στο επίπεδο ${LEVEL}", + "name": "Αγώνες Νάρκης" + }, + "Off You Go Then": { + "description": "Πετάξτε 3 κακούς εκτός χάρτη", + "descriptionComplete": "Πετάξατε 3 κακούς εκτός χάρτη", + "descriptionFull": "Πετάξτε 3 κακούς εκτός χάρτη στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Πετάξατε 3 κακούς εκτός χάρτη στο επίπεδο ${LEVEL}", + "name": "Πέταξε Το Πουλάκι" + }, + "Onslaught God": { + "description": "Σκοράρετε 5000 πόντους", + "descriptionComplete": "Σκοράρατε 5000 πόντους", + "descriptionFull": "Σκοράρετε 5000 πόντους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοράρατε 5000 πόντους στο επίπεδο ${LEVEL}", + "name": "Θεός του επιπέδου ${LEVEL}" + }, + "Onslaught Master": { + "description": "Σκοράρετε 500 πόντους", + "descriptionComplete": "Σκοράρατε 500 πόντους", + "descriptionFull": "Σκοράρετε 500 πόντους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοράρατε 500 πόντους στο επίπεδο ${LEVEL}", + "name": "Άρχοντας του επιπέδου ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Νικήστε όλα τα κύματα", + "descriptionComplete": "Νικήσατε όλα τα κύματα", + "descriptionFull": "Νικήστε όλα τα κύματα στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Νικήσατε όλα τα κύματα στο επίπεδο ${LEVEL}", + "name": "Νίκη στο επίπεδο ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Σκοράρετε 1000 πόντους", + "descriptionComplete": "Σκοράρατε 1000 πόντους", + "descriptionFull": "Σκοράρετε 1000 πόντους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοράρατε 1000 πόντους στο επίπεδο ${LEVEL}", + "name": "Μάγος του επιπέδου ${LEVEL}" + }, + "Precision Bombing": { + "description": "Νικήστε χωρίς ενισχυτικά", + "descriptionComplete": "Νικήσατε χωρίς ενισχυτικά", + "descriptionFull": "Νικήστε το επίπεδο ${LEVEL} χωρίς ενισχυτικά", + "descriptionFullComplete": "Νικήσατε το επίπεδο ${LEVEL} χωρίς ενισχυτικά", + "name": "Βομβαρδισμός Ακριβείας" + }, + "Pro Boxer": { + "description": "Νικήστε χωρίς να χρησιμοποιήσετε βόμβες", + "descriptionComplete": "Νικήσατε χωρίς να χρησιμοποιήσετε βόμβες", + "descriptionFull": "Ολοκληρώστε το επίπεδο ${LEVEL} χωρίς να χρησιμοποιήσετε βόμβες", + "descriptionFullComplete": "Ολοκληρώσατε το επίπεδο ${LEVEL} χωρίς να χρησιμοποιήσετε βόμβες", + "name": "Επαγγελματίας Πυγμάχος" + }, + "Pro Football Shutout": { + "description": "Νικήστε χωρίς να αφήσετε τους κακούς να σκοράρουν", + "descriptionComplete": "Νικήσατε χωρίς να αφήσετε τους κακούς να σκοράρουν", + "descriptionFull": "Νικήστε το επίπεδο ${LEVEL} χωρίς να αφήσετε τους κακούς να σκοράρουν", + "descriptionFullComplete": "Νικήσατε το επίπεδο ${LEVEL} χωρίς να αφήσετε τους κακούς να σκοράρουν", + "name": "${LEVEL} Απόκλιση" + }, + "Pro Football Victory": { + "description": "Νικήστε τον αγώνα", + "descriptionComplete": "Νικήσατε τον αγώνα", + "descriptionFull": "Νικήστε τον αγώνα στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Νικήσατε τον αγώνα στο επίπεδο ${LEVEL}", + "name": "Νίκη στο επίπεδο ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Νικήστε όλα τα κύματα", + "descriptionComplete": "Νικήσατε όλα τα κύματα", + "descriptionFull": "Νικήστε όλα τα κύματα του επιπέδου ${LEVEL}", + "descriptionFullComplete": "Νικήσατε όλα τα κύματα του επιπέδου ${LEVEL}", + "name": "Νίκη στο επίπεδο ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Απωθήστε όλα τα κύματα", + "descriptionComplete": "Απωθήσατε όλα τα κύματα", + "descriptionFull": "Απωθήστε όλα τα κύματα στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Απωθήσατε όλα τα κύματα στο επίπεδο ${LEVEL}", + "name": "Νίκη στο επίπεδο ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Νικήστε χωρίς να αφήσετε τους κακούς να σκοράρουν", + "descriptionComplete": "Νικήσατε χωρίς να αφήσετε τους κακούς να σκοράρουν", + "descriptionFull": "Νικήστε στο επίπεδο ${LEVEL} χωρίς να αφήσετε τους κακούς να σκοράρουν", + "descriptionFullComplete": "Νικήσατε στο επίπεδο ${LEVEL} χωρίς να αφήσετε τους κακούς να σκοράρουν", + "name": "${LEVEL} Απόκλιση" + }, + "Rookie Football Victory": { + "description": "Νικήστε τον αγώνα", + "descriptionComplete": "Νικήσατε τον αγώνα", + "descriptionFull": "Νικήστε τον αγώνα στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Νικήσατε τον αγώνα στο επίπεδο ${LEVEL}", + "name": "Νίκη στο επίπεδο ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Νικήστε όλα τα κύματα", + "descriptionComplete": "Νικήσατε όλα τα κύματα", + "descriptionFull": "Νικήστε όλα τα κύματα στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Νικήσατε όλα τα κύματα στο επίπεδο ${LEVEL}", + "name": "Νίκη στο επίπεδο ${LEVEL}" + }, + "Runaround God": { + "description": "Σκοράρετε 2000 πόντους", + "descriptionComplete": "Σκοράρατε 2000 πόντους", + "descriptionFull": "Σκοράρετε 2000 πόντους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοράρατε 2000 πόντους στο επίπεδο ${LEVEL}", + "name": "Θεός του επιπέδου ${LEVEL}" + }, + "Runaround Master": { + "description": "Σκοράρετε 500 πόντους", + "descriptionComplete": "Σκοράρατε 500 πόντους", + "descriptionFull": "Σκοράρετε 500 πόντους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοράρατε 500 πόντους στο επίπεδο ${LEVEL}", + "name": "Άρχοντας του επιπέδου ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Σκοράρετε 1000 πόντους", + "descriptionComplete": "Σκοράρατε 1000 πόντους", + "descriptionFull": "Σκοράρετε 1000 πόντους στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοράρατε 1000 πόντους στο επίπεδο ${LEVEL}", + "name": "Μάγος του επιπέδου ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Μοιραστείτε επιτυχώς το παιχνίδι με έναν φίλο", + "descriptionFullComplete": "Μοιραστήκατε επιτυχώς το παιχνίδι με έναν φίλο", + "name": "Μοιραστείτε και Νοιαστείτε" + }, + "Stayin' Alive": { + "description": "Νικήστε χωρίς να πεθάνετε", + "descriptionComplete": "Νικήσατε χωρίς να πεθάνετε", + "descriptionFull": "Νικήστε στο επίπεδο ${LEVEL} χωρίς να πεθάνετε", + "descriptionFullComplete": "Νικήσατε στο επίπεδο ${LEVEL} χωρίς να πεθάνετε", + "name": "Παραμένοντας Ζωντανός" + }, + "Super Mega Punch": { + "description": "Προκαλέστε 100% ζημιά με μιά γροθιά", + "descriptionComplete": "Προκαλέσατε 100% ζημιά με μιά γροθιά", + "descriptionFull": "Προκαλέστε 100% ζημιά με μιά γροθιά στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Προκαλέσατε 100% ζημιά με μιά γροθιά στο επίπεδο ${LEVEL}", + "name": "Πανίσχυρη Γροθιά" + }, + "Super Punch": { + "description": "Προκαλέστε 50% ζημιά με μιά γροθιά", + "descriptionComplete": "Προκαλέσατε 50% ζημιά με μιά γροθιά", + "descriptionFull": "Προκαλέστε 50% ζημιά με μιά γροθιά στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Προκαλέσατε 50% ζημιά με μιά γροθιά στο επίπεδο ${LEVEL}", + "name": "Ισχυρή Γροθιά" + }, + "TNT Terror": { + "description": "Σκοτώστε 6 κακούς με TNT", + "descriptionComplete": "Σκοτώσατε 6 κακούς με TNT", + "descriptionFull": "Σκοτώστε 6 κακούς με TNT στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σκοτώσατε 6 κακούς με TNT στο επίπεδο ${LEVEL}", + "name": "Ο Τρόμος Του Δυναμίτη" + }, + "Team Player": { + "descriptionFull": "Ξεκινήστε Ομαδικό παιχνίδι με 4+ παίκτες", + "descriptionFullComplete": "Ξεκινήσατε Ομαδικό παιχνίδι με 4+ παίκτες", + "name": "Ομαδικός Παίκτης" + }, + "The Great Wall": { + "description": "Σταματήστε όλους τους κακούς", + "descriptionComplete": "Σταματήσατε όλους τους κακούς", + "descriptionFull": "Σταματήστε όλους τους κακούς στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σταματήσατε όλους τους κακούς στο επίπεδο ${LEVEL}", + "name": "Το Μεγάλο Τείχος" + }, + "The Wall": { + "description": "Σταματήστε όλους τους κακούς", + "descriptionComplete": "Σταματήσατε όλους τους κακούς", + "descriptionFull": "Σταματήστε όλους τους κακούς στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Σταματήσατε όλους τους κακούς στο επίπεδο ${LEVEL}", + "name": "Το Τείχος" + }, + "Uber Football Shutout": { + "description": "Νικήστε χωρίς να αφήσετε τους κακούς να σκοράρουν", + "descriptionComplete": "Νικήσατε χωρίς να αφήσετε τους κακούς να σκοράρουν", + "descriptionFull": "Νικήστε το επίπεδο ${LEVEL} χωρίς να αφήσετε τους κακούς να σκοράρουν", + "descriptionFullComplete": "Νικήσατε το επίπεδο ${LEVEL} χωρίς να αφήσετε τους κακούς να σκοράρουν", + "name": "${LEVEL} Απόκλιση" + }, + "Uber Football Victory": { + "description": "Νικήστε τον αγώνα", + "descriptionComplete": "Νικήσατε τον αγώνα", + "descriptionFull": "Νικήστε τον αγώνα στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Νικήσατε τον αγώνα στο επίπεδο ${LEVEL}", + "name": "Νίκη στο επίπεδο ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Νικήστε όλα τα κύματα", + "descriptionComplete": "Νικήσατε όλα τα κύματα", + "descriptionFull": "Νικήστε όλα τα κύματα στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Νικήσατε όλα τα κύματα στο επίπεδο ${LEVEL}", + "name": "Νίκη στο επίπεδο ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Απωθήστε όλα τα κύματα", + "descriptionComplete": "Απωθήσατε όλα τα κύματα", + "descriptionFull": "Απωθήστε όλα τα κύματα στο επίπεδο ${LEVEL}", + "descriptionFullComplete": "Απωθήσατε όλα τα κύματα στο επίπεδο ${LEVEL}", + "name": "Νίκη στο επίπεδο ${LEVEL}" + } + }, + "achievementsRemainingText": "Υπολειπόμενα Επιτεύγματα:", + "achievementsText": "Επιτεύγματα", + "achievementsUnavailableForOldSeasonsText": "Συγνώμη, ακριβείς λεπτομέρειες σχετικές με τα επιτεύγματα είναι μη διαθέσιμες για παλαιότερες σεζόν.", + "addGameWindow": { + "getMoreGamesText": "Περισσότερα Παιχνίδια...", + "titleText": "Προσθήκη Παιχνιδιού" + }, + "allowText": "Να Επιτρέπεται", + "alreadySignedInText": "Ο λογαριασμός σας είναι συνδεδεμένος από άλλη συσκευή.\nΠαρακαλώ, άλλαξε το λογαριασμό σας ή απενεργοποίησε \nτο παιχνίδι από τις άλλες συσκευες σας και ξαναπροσπάθηστε.", + "apiVersionErrorText": "Can't load module ${NAME}; it targets api-version ${VERSION_USED}; we require ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(Το \"Αυτόματο\" ενεργοποιεί αυτό μόνο όταν έχουν συνδεθεί ακουστικά)", + "headRelativeVRAudioText": "Ρυθμίσεις σχετικές με VR ακουστικά", + "musicVolumeText": "Ένταση Μουσικής", + "soundVolumeText": "Ένταση Ήχου", + "soundtrackButtonText": "Ηχητική Υπόκρουση", + "soundtrackDescriptionText": "(αναθέστε τη δική σας μουσική να ακούγεται ενώ παίζετε)", + "titleText": "Ήχος" + }, + "autoText": "Αυτόματο", + "backText": "Πίσω", + "banThisPlayerText": "Αποκλεισμός αυτόυ του Παίκτη", + "bestOfFinalText": "Επικρατών-Στα-${COUNT} Αποτελέσμα", + "bestOfSeriesText": "Σειρά επικρατών στα ${COUNT}:", + "bestRankText": "Η καλύτερή σας κατάταξη: #${RANK}", + "bestRatingText": "Η καλύτερή σας βαθμολογία: ${RATING}", + "bombBoldText": "BΟΜΒΑ", + "bombText": "Βόμβα", + "boostText": "Ώθηση", + "bsRemoteConfigureInAppText": "Το ${REMOTE_APP_NAME} είναι οριστικοποιημένο στην εφαρμογή από μόνο του.", + "buttonText": "κουμπί", + "canWeDebugText": "Θα θέλατε το BombSquad να στέλνει αυτόματη αναφορά σφαλμάτων,\nκατάρρευσης, και πληροφορίες βασικής χρήσης στο δημιουργό του;\n\nΤα δεδομένα δε περιέχουν προσωπικές σας πληροφορίες και\nβοηθούν το παιχνίδι να τρέχει ομαλά χωρίς σφάλματα.", + "cancelText": "Άκυρο", + "cantConfigureDeviceText": "Συγνώμη, η συσκευή ${DEVICE} είναι μη οριστικοποιήσιμη.", + "challengeEndedText": "Αυτή η πρόκληση έχει τελειώσει.", + "chatMuteText": "Σίγαση Συζήτησης", + "chatMutedText": "Συζήτηση σε Σίγαση", + "chatUnMuteText": "Απενεργοποίηση Σίγασης", + "choosingPlayerText": "<επιλογή παίκτη>", + "completeThisLevelToProceedText": "Πρέπει να ολοκληρώσετε αυτό\nτο επίπεδο για να προχωρήσετε!", + "completionBonusText": "Μπόνους Ολοκλήρωσης", + "configControllersWindow": { + "configureControllersText": "Οριστικοποίηση Χειριστηρίων", + "configureKeyboard2Text": "Οριστικοποίηση Πληκτρολογίου Π2", + "configureKeyboardText": "Οριστικοποίηση Πληκτρολογίου", + "configureMobileText": "Φορητές Συσκευές ως Χειριστήρια", + "configureTouchText": "Οριστικοποίηση Οθόνης Αφής", + "ps3Text": "Χειριστήρια PS3", + "titleText": "Χειριστήρια", + "wiimotesText": "Wiimotes", + "xbox360Text": "Χειριστήρια Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Σημείωση: η υποστήριξη χειριστηρίων διαφέρει ανάλογα με τη συσκευή και την έκδοση Android.", + "pressAnyButtonText": "Πατήστε οποιοδήποτε πλήκτρο στο χειριστήριο\n που θέλετε να οριστικοποιήσετε...", + "titleText": "Οριστικοποίηση Χειριστηρίων" + }, + "configGamepadWindow": { + "advancedText": "Σύνθετες", + "advancedTitleText": "Σύνθετη Εγκατάσταση Χειριστηρίων", + "analogStickDeadZoneDescriptionText": "(αλλάξτε το εάν ο χαρακτήρας σας 'γλιστράει' όταν αφήνετε το μοχλό)", + "analogStickDeadZoneText": "Νεκρή Ζώνη Αναλογικού Μοχλού", + "appliesToAllText": "(εφαρμόζεται για όλα τα χειριστήρια αυτού του τύπου)", + "autoRecalibrateDescriptionText": "(ενεργοποιήστε το αν ο χαρακτήρας σας δεν κινείται με τη μέγιστη ταχύτητα)", + "autoRecalibrateText": "Αναβαθμονόμηση Αναλογικού Μοχλού", + "axisText": "άξονας", + "clearText": "εκκαθάριση", + "dpadText": "dpad", + "extraStartButtonText": "Επιπλέον Κουμπί Έναρξης", + "ifNothingHappensTryAnalogText": "Αν δε συμβαίνει τίποτα, δοκιμάστε να το αναθέσετε στον αναλογικό μοχλό", + "ifNothingHappensTryDpadText": "Αν δε συμβεί τίποτα, δοκιμάστε να το αναθέσετε στο d-pad.", + "ignoreCompletelyDescriptionText": "(απαγόρευση αυτού του χειριστηρίου να επηρεάσει τόσο το παιχνίδι όσο και τα μενού)", + "ignoreCompletelyText": "Πλήρης Αγνόηση", + "ignoredButton1Text": "Αγνοημένο Πλήκτρο 1", + "ignoredButton2Text": "Αγνοημένο Πλήκτρο 2", + "ignoredButton3Text": "Αγνοημένο Πλήκτρο 3", + "ignoredButton4Text": "Αγνοημένο Πλήκτρο 4", + "ignoredButtonDescriptionText": "(χρησιμοποιήστε αυτή τη ρύθμιση για να απαγορεύσετε στα κουμπιά 'home' ή 'sync' να επηρεάσουν το UI)", + "pressAnyAnalogTriggerText": "Πατήστε οποιονδήποτε αναλογικό διακόπτη...", + "pressAnyButtonOrDpadText": "Πατήστε οποιοδήποτε κουμπί ή dpad...", + "pressAnyButtonText": "Πατήστε οποιοδήποτε κουμπί...", + "pressLeftRightText": "Πατήστε αριστερά ή δεξιά...", + "pressUpDownText": "Πατήστε πάνω ή κάτω", + "runButton1Text": "Κουμπί τρεξίματος 1", + "runButton2Text": "Κουμπί τρεξίματος 2", + "runTrigger1Text": "Αναλογικός Διακόπτης Τρεξίματος 1", + "runTrigger2Text": "Αναλογικός Διακόπτης Τρεξίματος 2", + "runTriggerDescriptionText": "(οι αναλογικοί διακόπτες σας επιτρέπουν να τρέχετε με διαφορετικές ταχύτητες)", + "secondHalfText": "Χρησιμοποιήστε αυτή τη ρύθμιση για να\nοριστικοποιήσετε το δεύτερο μισό συσκευής 2-σε-1\nπου εμφανίζεται ως μονό χειριστήριο.", + "secondaryEnableText": "Ενεργοποίηση", + "secondaryText": "Δευτερεύον Χειριστήριο", + "startButtonActivatesDefaultDescriptionText": "(απενεργοποιήστε αυτή τη ρύθμιση αν το κουμπί έναρξής σας μοιάζει πιό πολύ με κουμπί 'menu')", + "startButtonActivatesDefaultText": "Το Κουμπί Έναρξης Ενεργοποιεί το Προκαθορισμένο Γραφικό Στοιχείο", + "titleText": "Εγκατάσταση Χειριστηρίων", + "twoInOneSetupText": "Εγκατάσταση Χειριστηρίων 2-σε-1", + "uiOnlyDescriptionText": "(απαγόρευση αυτού του χειριστηρίου από το να ενταχθεί σε παιχνίδι)", + "uiOnlyText": "Περιορισμός Χρήσης Μόνο για τα Μενού", + "unassignedButtonsRunText": "Όλα Ανενεργά Κουμπιά Λειτουργούν για Τρέξιμο", + "unsetText": "<μη ορισμένο>", + "vrReorientButtonText": "Κουμπί Αναπροσανατολισμού VR" + }, + "configKeyboardWindow": { + "configuringText": "Οριστικοποιείται η Συσκευή ${DEVICE}", + "keyboard2NoteText": "Σημείωση: τα περισσότερα πληκτρολόγια μπορούν να υποστηρίξουν λίγα\nπατήματα κουμπιών ταυτόχρονα, οπότε έχοντας δεύτερο παίκτη πληκτρολογίου\nμπορεί να λειτουργήσει καλύτερα εάν υπάρχει και άλλο διαθέσιμο πληκτρολόγιο\nγια να χρησιμοποιήσει. Να σημειωθεί επίσης ότι πρέπει να επιλέξετε επιπλέον\nκουμπιά και για τους δύο παίκτες ακόμα και σε αυτή τη περίπτωση." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Κλίμακα Στοιχείου Χειρισμού Δράσεων", + "actionsText": "Δράσεις", + "buttonsText": "κουμπιά", + "dragControlsText": "< σύρετε τους χειρισμούς για επανατοποθέτηση >", + "joystickText": "μοχλός", + "movementControlScaleText": "Κλίμακα Στοιχείου Χειρισμού Κινήσεων", + "movementText": "Κίνηση", + "resetText": "Επαναφορά", + "swipeControlsHiddenText": "Απόκρυψη Εικονιδίων Swipe", + "swipeInfoText": "Ο χειρισμός 'Swipe' παίρνει λίγο χρόνο μέχρι να τον συνηθίσετε αλλά\nτο κάνει ευκολότερο να παίζετε χωρίς να κοιτάτε τους χειρισμούς.", + "swipeText": "swipe", + "titleText": "Οριστικοποίηση Οθόνης Αφής" + }, + "configureItNowText": "Οριστικοποίηση τώρα;", + "configureText": "Οριστικοποίηση", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsScale": 0.6, + "bestResultsText": "Για καλύτερα αποτελέσματα θα πρέπει να έχετε wifi δίκτυο χωρίς\nκαθυστερήσεις. Μπορείτε να τις μειώσετε κλείνοντας άλλες ασύρματες\nσυσκευές, παίζοντας κοντά στον δρομολογητή, και συνδέοντας τον\nοικοδεσπότη του παιχνιδιου απευθείας στο δίκτυο μέσω ethernet.", + "explanationText": "Για χρήση smart-phone ή tablet ως ασύρματο χειριστήριο,\nεγκαταστήστε το \"${REMOTE_APP_NAME}\". Κάθε αριθμός μπορεί\nνα συνδεθεί σε ένα παιχνίδι ${APP_NAME} μέσω Wi-Fi, δωρεάν!", + "forAndroidText": "για Android:", + "forIOSText": "για iOS:", + "getItForText": "Αποκτήστε το ${REMOTE_APP_NAME} για iOS στο App Store της Apple\nή για Android στο Google Play Store ή στο Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Χρησιμοποιώντας Φορητές Συσκευές ως Χειριστήρια:" + }, + "continuePurchaseText": "Συνέχιση για ${PRICE}?", + "continueText": "Συνέχιση", + "controlsText": "Χειρισμοί", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Αυτό δεν εφαρμόζεται για την διαχρονική κατάταξη.", + "activenessInfoText": "Αυτός ο πολλαπλασιαστής ανεβαίνει κατά τις μέρες\nπου παίζετε και πέφτει κατά τις μέρες που δεν παίζετε.", + "activityText": "Δραστηριότητα", + "campaignText": "Ιστορία", + "challengesInfoText": "Κερδίστε έπαθλα νικώντας μικροπαιχνίδια.\n\nΤα έπαθλα και ο βαθμός δυσκολίας αυξάνεται κάθε\nφορά που ολοκληρώνεται μία πρόκληση και μειώνεται\nκάθε φορά που κάποια λήξει ή εγκαταλειφθεί.", + "challengesText": "Προκλήσεις", + "currentBestText": "Τρέχουσα Υψηλότερη Βαθμολογία", + "customText": "Προσαρμοσμένο", + "entryFeeText": "Είσοδος", + "forfeitConfirmText": "Εγκατάλειψη της πρόκλησης?", + "forfeitNotAllowedYetText": "Δεν μπορείτε να εγκαταλείψετε ακόμα αυτή την πρόκληση.", + "forfeitText": "Εγκατάλειψη", + "multipliersText": "Πολλαπλασιαστές", + "nextChallengeText": "Επόμενη Πρόκληση", + "nextPlayText": "Επόμενο Παιχνίδι", + "ofTotalTimeText": "από ${TOTAL}", + "playNowText": "Παίξε Τώρα", + "pointsText": "Πόντοι", + "powerRankingFinishedSeasonUnrankedText": "(η σεζόν ολοκληρώθηκε δίχως να έχετε καταταχθεί)", + "powerRankingNotInTopText": "(όχι στους κορυφαίους ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} πόντοι", + "powerRankingPointsMultText": "(x ${NUMBER} πόντοι)", + "powerRankingPointsText": "${NUMBER} πόντοι", + "powerRankingPointsToRankedText": "(${CURRENT} από ${REMAINING} πόντοι)", + "powerRankingText": "Κατάταξη Δύναμης", + "prizesText": "Βραβεία", + "proMultInfoText": "Οι παίχτες με την ${PRO} αναβάθμιση\nλαμβάνουν επιπλέον ${PERCENT}% πόντους εδώ.", + "seeMoreText": "Περισσότερα...", + "skipWaitText": "Παράκαμψη Αναμονής", + "timeRemainingText": "Υπολοιπόμενος Χρόνος", + "toRankedText": "Για Κατάταξη", + "totalText": "σύνολο", + "tournamentInfoText": "Ανταγωνιστείτε για υψηλά σκορ με\nάλλους παίκτες της κατηγορίας σας.\n\nΟι παίκτες που κατέχουν τα κορυφαία σκορ όταν ο\nχρόνος του τουρνουά λήγει ανταμοίβονται με βραβεία.", + "welcome1Text": "Καλωσήρθατε στη κατηγορία ${LEAGUE}. Μπορείτε να ανεβάσετε\nτην κατάταξή σας κερδίζοντας βαθμολογίες αστεριών,\nολοκληρώνοντας επιτεύγματα, και κερδίζοντας βραβεία σε τουρνουά.", + "welcome2Text": "Μπορείτε ακόμα να κερδίσετε εισιτήρια με πολλές παρόμοιες δραστηριότητες.\nΤα εισιτήρια μπορούν να χρησιμοποιηθούν για να ξεκλειδώσετε νέους\nχαρακτήρες, χάρτες και μικροπαιχνίδια, να συμμετάσχετε σε τουρνουά, κ.α.", + "yourPowerRankingText": "Η Κατάταξη Δύναμής Σας:" + }, + "copyOfText": "${NAME} Αντίγραφο", + "createEditPlayerText": "<Δημιουργία/Επεξεργασία Παίκτη>", + "createText": "Δημιουργία", + "creditsWindow": { + "additionalAudioArtIdeasText": "Πρόσθετοι Ήχοι, Πρώιμη Καλλιτεχνική Επιμέλεια, και Ιδέες από ${NAME}", + "additionalMusicFromText": "Πρόσθετη μουσική από ${NAME}", + "allMyFamilyText": "Όλοι οι φίλοι και η οικογένειά μου που βοήθησαν στις δοκιμές του παιχνιδιού", + "codingGraphicsAudioText": "Κώδικας, Γραφικά, και Ήχοι από ${NAME}", + "languageTranslationsText": "Γλωσσικές Μεταφράσεις:", + "legalText": "Νομικά:", + "publicDomainMusicViaText": "Δημόσιος-τομέας μουσικής μέσω ${NAME}", + "softwareBasedOnText": "Αυτό το λογισμικό είναι βασισμένο σε μέρος του έργου των ${NAME}", + "songCreditText": "${TITLE} παίχτηκε από ${PERFORMER}\nΣυντέθηκε από ${COMPOSER}, Οργανώθηκε από ${ARRANGER}, Δημοσιεύτηκε από ${PUBLISHER},\nΠαραχωρήθηκε από ${SOURCE}", + "soundAndMusicText": "Ήχος & Μουσική:", + "soundsText": "Ήχοι (${SOURCE}):", + "specialThanksText": "Πολλά Ευχαριστώ:", + "thanksEspeciallyToText": "Ευχαριστώ ιδιαίτερα: ${NAME}", + "titleText": "Λίστα Συντελεστών ${APP_NAME}", + "whoeverInventedCoffeeText": "Οποιοσδήποτε επινόησε τον καφέ" + }, + "currentStandingText": "Η τρέχουσα κατάταξή σας είναι #${RANK}", + "customizeText": "Προσαρμογή...", + "deathsTallyText": "${COUNT} θάνατοι", + "deathsText": "Θάνατοι", + "debugText": "αποσφαλμάτωση", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Σημείωση: είναι προτεινόμενη η ρύθμιση Ρυθμίσεις->Γραφικά->Υφές στο 'Υψηλό' όταν το δοκιμάζετε αυτό.", + "runCPUBenchmarkText": "Εκτέλεση Αξιολόγησης Απόδοσης CPU", + "runGPUBenchmarkText": "Εκτέλεση Αξιολόγησης Απόδοσης GPU", + "runMediaReloadBenchmarkText": "Εκτέλεση Αξιολόγησης Απόδοσης Επαναφόρτωσης-Πολυμέσων", + "runStressTestText": "Εκτέλεση Ελέγχου Κόπωσης", + "stressTestPlayerCountText": "Αριθμός Παικτών", + "stressTestPlaylistDescriptionText": "Λίστα Παιχνιδιών Ελέγχου Κόπωσης", + "stressTestPlaylistNameText": "Όνομα Λίστας Παιχνιδιών", + "stressTestPlaylistTypeText": "Τύπος Λίστας Παιχνιδιών", + "stressTestRoundDurationText": "Διάρκεια Γύρου", + "stressTestTitleText": "Έλεγχος Κόπωσης", + "titleText": "Έλεγχος Αξιολόγησης & Κόπωσης", + "totalReloadTimeText": "Συνολικός χρόνος επαναφόρτωσης: ${TIME}" + }, + "defaultGameListNameText": "Προκαθορισμένη Λίστα ${PLAYMODE}", + "defaultNewGameListNameText": "Η Δική μου Λίστα ${PLAYMODE}", + "deleteText": "Διαγραφή", + "demoText": "Επίδειξη", + "denyText": "Απαγόρευση", + "desktopResText": "Ανάλυση Σταθερού Η/Υ", + "difficultyEasyText": "Εύκολο", + "difficultyHardOnlyText": "Αποκλειστικά Στο Δύσκολο", + "difficultyHardText": "Δύσκολο", + "difficultyHardUnlockOnlyText": "Αυτό το επίπεδο μπορεί να ξεκλειδωθεί μόνο στο\nΔύσκολο. Πιστεύετε ότι έχετε τα προσόντα!?!?!", + "directBrowserToURLText": "Παρακαλώ προβείτε στην παρακάτω ιστοσελίδα με τη χρήση διαδικτυακού περιηγητή:", + "disableRemoteAppConnectionsText": "Απενεργοποίηση Συνδέσεων Ασύρματης Εφαρμογής", + "disableXInputDescriptionText": "Επιτρέπει περισσότερα από 4 χειριστήρια αλλά μπορεί να μη λειτουργήσει.", + "disableXInputText": "Απενεργοποίηση XIinput", + "doneText": "Έγινε", + "drawText": "Ισοπαλία", + "duplicateText": "Διπλοτυπία", + "editGameListWindow": { + "addGameText": "Προσθήκη\nΠαιχνιδιού", + "cantOverwriteDefaultText": "Η προκαθορισμένη λίστα παιχνιδιών δεν μπορεί να αντικατασταθεί!", + "cantSaveAlreadyExistsText": "Μία λίστα παιχνιδιών με αυτό το όνομα υπάρχει ήδη!", + "cantSaveEmptyListText": "Μία άδεια λίστα παιχνιδιών δεν μπορεί να αποθηκευτεί!", + "editGameText": "Επεξεργασία\nΠαιχνιδιού", + "listNameText": "Όνομα λίστας", + "nameText": "Όνομα", + "removeGameText": "Αφαίρεση\nΠαιχνιδιού", + "saveText": "Αποθήκευση λίστας", + "titleText": "Επεξεργαστής Λίστας Παιχνιδιών" + }, + "editProfileWindow": { + "accountProfileInfoText": "Αυτό το ειδικό προφίλ έχει όνομα και\nεικονίδιο βασισμένο στον λογαριασμό σας.\n\n${ICONS}\n\nΔημιουργήστε προσαρμοσμένα προφίλ για τη\nχρήση διαφορετικών ονομάτων και εικονιδίων.", + "accountProfileText": "(προφίλ λογαριασμού)", + "availableText": "Το όνομα \"${NAME}\" είναι διαθέσιμο.", + "characterText": "χαρακτήρας", + "checkingAvailabilityText": "Έλεγχος διαθεσιμότητας για \"${NAME}\"...", + "colorText": "χρώμα", + "getMoreCharactersText": "Περισσότεροι Χαρακτήρες...", + "getMoreIconsText": "Περισσότερα εικονίδια...", + "globalProfileInfoText": "Τα παγκόσμια προφίλ παικτών εγγυώνται ότι εχουν μοναδικά\nονόματα παγκοσμίως. Επιπλέον, περιλαμβάνουν προσαρμοσμένα εικονίδια.", + "globalProfileText": "(παγκόσμιο προφίλ)", + "highlightText": "λεπτομέρεια", + "iconText": "εικονίδιο", + "localProfileInfoText": "Τα τοπικά προφίλ παικτών δεν έχουν εικονίδια και τα ονόματά\nτους δεν εγγυώνται ως μοναδικά. Αναβαθμίστε σε παγκόσμιο προφίλ\nγια την κατοχύρωση ενός ονόματος και την προσθήκη εικονιδίου.", + "localProfileText": "(τοπικό προφίλ)", + "nameDescriptionText": "Όνομα Παίκτη", + "nameText": "Όνομα", + "randomText": "τυχαίο", + "titleEditText": "Επεξεργασία Προφίλ", + "titleNewText": "Νέο Προφίλ", + "unavailableText": "Το \"${NAME}\" δεν είναι διαθέσιμο. Δοκιμάστε άλλο όνομα.", + "upgradeProfileInfoText": "Αυτό θα κατοχυρώσει το όνομά του παίκτη σας παγκοσμίως\nκαι θα σας επιτρέψει τη προσθήκη προσαρμοσμένου εικονιδίου.", + "upgradeToGlobalProfileText": "Προαγωγή σε Παγκόσμιο Προφίλ" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Δεν μπορείτε να διαγράψετε το προκαθορισμένο πακέτο.", + "cantEditDefaultText": "Δεν μπορείτε να επεξεργαστείτε το προκαθορισμένο πακέτο. Διπλοτυπίστε το ή δημιουργήστε καινούριο.", + "cantOverwriteDefaultText": "Το προκαθορισμένο πακέτο δεν μπορεί να αντικατασταθεί", + "cantSaveAlreadyExistsText": "Ένα πακέτο με το ίδιο όνομα υπάρχει ήδη!", + "defaultGameMusicText": "<προκαθορισμένη μουσική παιχνιδιού>", + "defaultSoundtrackNameText": "Προκαθορισμένο Πακέτο", + "deleteConfirmText": "Διαγραφή Πακέτου:\n\n'${NAME}';", + "deleteText": "Διαγραφή\nΠακέτου", + "duplicateText": "Διπλοτυπία\nΠακέτου", + "editSoundtrackText": "Επεξεργαστής Ηχητικής Υπόκρουσης", + "editText": "Επεξεργασία\nΠακέτου", + "fetchingITunesText": "μεταφορά λιστών μουσικής από εφαρμογή μουσικής...", + "musicVolumeZeroWarning": "Προειδοποίηση: η ένταση της μουσικής είναι ρυθμισμένη στο 0", + "nameText": "Όνομα", + "newSoundtrackNameText": "Η Ηχητική μου Υπόκρουση ${COUNT}", + "newSoundtrackText": "Νέο πακέτο:", + "newText": "Νέο\nΠακέτο", + "selectAPlaylistText": "Επιλέξτε μια Λίστα Μουσικής", + "selectASourceText": "Πηγή Μουσικής", + "testText": "έλεγχος", + "titleText": "Ηχητική Υπόκρουση", + "useDefaultGameMusicText": "Προκαθορισμένη Μουσική Παιχνιδιού", + "useITunesPlaylistText": "Λίστα μουσικής από εφαρμογή μουσικής", + "useMusicFileText": "Μουσικό Αρχείο (mp3, κλπ)", + "useMusicFolderText": "Φάκελος Αρχείων Μουσικής" + }, + "editText": "Επεξεργασία", + "endText": "Τέλος", + "enjoyText": "Απολαύστε!", + "epicDescriptionFilterText": "${DESCRIPTION} Σε επικά αργή κίνηση.", + "epicNameFilterText": "${NAME} Επικό", + "errorAccessDeniedText": "η πρόσβαση απορρίφθηκε", + "errorOutOfDiskSpaceText": "έλλειψη αποθηκευτικού χώρου", + "errorText": "Σφάλμα", + "errorUnknownText": "άγνωστο σφάλμα", + "exitGameText": "Έξοδος από το ${APP_NAME};", + "exportSuccessText": "Έγινε εξαγωγή του στοιχείου '${NAME}'.", + "externalStorageText": "Εξωτερικός Αποθηκευτικός Χώρος", + "failText": "Αποτυχία", + "fatalErrorText": "Ωχ; Κάτι λείπει ή έχει χαλάσει. Παρακαλώ,\nδοκιμάστε να επανεγκαταστήσετε την εφαρμογή\nή επικοινωνήστε στο ${EMAIL} για βοήθεια.", + "fileSelectorWindow": { + "titleFileFolderText": "Επιλέξτε Αρχείο ή Φάκελο", + "titleFileText": "Επιλέξτε Αρχείο", + "titleFolderText": "Επιλέξτε Φάκελο", + "useThisFolderButtonText": "Χρήση Αυτού Του Φακέλου" + }, + "filterText": "Φίλτρο", + "finalScoreText": "Τελική Βαθμολογία", + "finalScoresText": "Τελικές Βαθμολογίες", + "finalTimeText": "Τελικός χρόνος", + "finishingInstallText": "Ολοκλήρωση εγκατάστασης. Μια στιγμή...", + "fireTVRemoteWarningText": "* Για καλύτερη εμπειρία, χρησιμοποιήστε\nχειριστήρια Παιχνιδιών ή εγκαταστήστε την\nεφαρμογή '${REMOTE_APP_NAME}' στο κινητό\nτηλέφωνο ή το tablet σας.", + "firstToFinalText": "Πρώτος-Στα-${COUNT} Αποτέλεσμα", + "firstToSeriesText": "Σειρά πρώτος-στα-${COUNT}", + "fiveKillText": "ΠΕΝΤΑΠΛΟΣ ΦΟΝΟΣ!!!", + "flawlessWaveText": "Άψογο Κύμα!", + "fourKillText": "ΤΕΤΡΑΠΛΟΣ ΦΟΝΟΣ!!!", + "friendScoresUnavailableText": "Βαθμολογίες φίλων μη διαθέσιμες.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Πίνακας Κατάταξης ${COUNT}ου Παιχνιδιού", + "gameListWindow": { + "cantDeleteDefaultText": "Δεν μπορείτε να διαγράψετε την προκαθορισμένη λίστα παιχνιδιών.", + "cantEditDefaultText": "Δεν μπορείτε να επεξεργαστείτε την προκαθορισμένη λίστα παιχνιδιών! Διπλοτυπίστε την ή δημιουργήστε καινούρια.", + "cantShareDefaultText": "Δεν μπορειτε να μοιραστείτε την προκαθορισμένη λίστα παιχνιδιών.", + "deleteConfirmText": "Διαγραφή \"${LIST}\";", + "deleteText": "Διαγραφή\nΛίστας", + "duplicateText": "Διπλοτυπία\nΛίστας", + "editText": "Επεξεργασία\nΛίστας", + "newText": "Νέα\nΛίστα", + "showTutorialText": "Προβολή Εκπαιδευτικού Βίντεο", + "shuffleGameOrderText": "Ανακάτεμα Ουράς Παιχνιδιών", + "titleText": "Προσαρμογή Λίστων Παιχνιδιών ${TYPE}" + }, + "gameSettingsWindow": { + "addGameText": "Προσθήκη Παιχνιδιού" + }, + "gamesToText": "${WINCOUNT} - ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Θυμηθείτε: οποιαδήποτε συσκευή σε μια συγκέντρωση μπορεί να φιλοξενήσει\nπερισσότερους από έναν παίκτες, αρκεί να υπάρχουν αρκετά χειριστήρια.", + "aboutDescriptionText": "Χρησιμοποιήστε αυτές τις καρτέλες για να οργανώσετε μια συγκέντρωση.\n\nΟι συγκεντρώσεις σας επιτρέπουν να παίξετε παιχνίδια\nκαι τουρνουά με φίλους μεταξύ διαφόρων συσκευών.\n\nΠατήστε το κουμπί ${PARTY} στην κορυφή δεξιά για να συνομιλήσετε\nκαι να αλληλεπιδράσετε με τα μέλη της συγκέντρωσής σας.\n(με χειριστήριο, πατήστε το ${BUTTON} όταν βρίσκεστε σε μενού)", + "aboutText": "Σχετικά", + "addressFetchErrorText": "<σφάλμα κατά τη μεταφορά διευθύνσεων>", + "appInviteMessageText": "Ο χρήστης ${NAME} σας έστειλε ${COUNT} εισητήρια στο ${APP_NAME}", + "appInviteSendACodeText": "Στείλτε Τους Κωδικό", + "appInviteTitleText": "Πρόσκληση Στο ${APP_NAME}", + "bluetoothAndroidSupportText": "(λειτουργεί με οποιαδήποτε συσκευή Android που υποστηρίζει Bluetooth)", + "bluetoothDescriptionText": "Φιλοξενίστε/ενταχθείτε σε συγκέντρωση μέσω Bluetooth:", + "bluetoothHostText": "Φιλοξενίστε μέσω Bluetooth", + "bluetoothJoinText": "Ενταχθείτε μέσω Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "έλεγχος...", + "dedicatedServerInfoText": "Για καλύτερα αποτελέσματα, οργανώστε έναν σταθερό διακομιστή. Βλέπε bombsquadgame.com/server.", + "disconnectClientsText": "Συνεχίζοντας θα αποσυνδεθούν ${COUNT} παίκης/ες\nαπο τη συγκέντρωσή σας. Είστε σίγουροι?", + "earnTicketsForRecommendingAmountText": "Οι φίλοι σας θα λάβουν ${COUNT} εισητήρια αν δοκιμάσουν το παιχνίδι\n(και εσείς θα λάβετε ${YOU_COUNT} για τον καθένα)", + "earnTicketsForRecommendingText": "Μοιραστείτε το παιχνίδι\nγια δωρεάν εισητήρια...", + "emailItText": "Στείλτε το σε Email", + "friendHasSentPromoCodeText": "${COUNT} εισητήρια ${APP_NAME} από ${NAME}", + "friendPromoCodeAwardText": "Θα λάμβάνετε από ${COUNT} εισητήρια για κάθε χρήση.", + "friendPromoCodeExpireText": "Αυτός ο κωδικός λήγει σε ${EXPIRE_HOURS} ώρες και λειτουργεί μόνο για νέα μέλη.", + "friendPromoCodeInstructionsText": "Για να το αξιοποιήσετε, ανοίξτε το ${APP_NAME} και ακολουθήστε το \"Ρυθμίσεις->Σύνθετες->Εισαγωγή Κωδικού\".\nΒλέπε bombsquadgame.com για συνδέσμους της εφαρμογής σε όλες τις υποστηριζόμενες πλατφόρμες.", + "friendPromoCodeRedeemLongText": "Μπορεί να εξαργυρωθεί για ${COUNT} δωρεάν εισητήρια από έως ${MAX_USES} άτομα.", + "friendPromoCodeRedeemShortText": "Μπορεί να εξαργυρωθεί για ${COUNT} εισητήρια στο παιχνίδι.", + "friendPromoCodeWhereToEnterText": "(στο \"Ρυθμίσεις->Σύνθετες->Εισαγωγή Κωδικού\")", + "getFriendInviteCodeText": "Αποκτήστε Κωδικό Πρόσκλησης Φίλων", + "googlePlayDescriptionText": "Προσκάλεσε Google Play παίκτες στη συγκέντρωσή σας:", + "googlePlayInviteText": "Πρόσκληση", + "googlePlayReInviteText": "Υπάρχουν ${COUNT} Google Play παίκτης/ες στη συγκέντρωσή\nσας οι οποίοι θα αποσυνδεθούν εάν στείλετε νέα πρόσκληση.\nΠεριελάβετέ τους σε αυτή για να τους φέρετε πίσω.", + "googlePlaySeeInvitesText": "Προβολή Προσκλήσεων", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play έκδοση)", + "hostPublicPartyDescriptionText": "Φιλοξενίστε Δημόσια Συγκέντρωση:", + "inDevelopmentWarningText": "Σημείωση:\n\nΤο παιχνίδι μέσω ιστού είναι καινούριο και εξελισσόμενο\nχαρακτηριστηκό. Για την ώρα, είναι πολύ προτεινόμενο όλοι\nοι παίκτες να είναι συνδεδεμένοι στο ίδιο δίκτυο Wi-Fi.", + "internetText": "Διαδίκτυο", + "inviteAFriendText": "Οι φίλοι σας δεν έχουν το παιχνίδι? Προσκαλέστε τους\nνα το δοκιμάσουν και θα λάβουν ${COUNT} δωρεάν εισιτήρια.", + "inviteFriendsText": "Προσκαλέστε Φίλους", + "joinPublicPartyDescriptionText": "Ένταξη σε Δημόσια Συγκέντρωση:", + "localNetworkDescriptionText": "Ένταξη σε συγκέντρωση στο δίκτυο σας:", + "localNetworkText": "Τοπικό Δίκτυο", + "makePartyPrivateText": "Κάνε Την Συγκέντρωσή Μου Ιδιωτική", + "makePartyPublicText": "Κάνε Την Συγκέντρωσή Μου Δημόσια", + "manualAddressText": "Διεύθυνση", + "manualConnectText": "Σύνδεση", + "manualDescriptionText": "Ένταξη σε συγκέντρωση από διεύθυνση:", + "manualJoinableFromInternetText": "Μπορείτε να φιλοξενήσετε από το διαδίκτυο;:", + "manualJoinableNoWithAsteriskText": "ΟΧΙ*", + "manualJoinableYesText": "ΝΑΙ", + "manualRouterForwardingText": "*για να το διορθώσετε, δοκιμάστε να ρυθμίσετε τον δρομολογητή σας να στείλει τη UDP θύρα ${PORT} στην ιδιωτική σας διεύθυνση", + "manualText": "Χειροκίνητα", + "manualYourAddressFromInternetText": "Η διεύθυνση σας με βάση το διαδίκτυο:", + "manualYourLocalAddressText": "Η τοπική σας διεύθυνση:", + "noConnectionText": "<εκτός σύνδεσης>", + "otherVersionsText": "(άλλες εκδόσεις)", + "partyInviteAcceptText": "Αποδοχή", + "partyInviteDeclineText": "Απόρριψη", + "partyInviteGooglePlayExtraText": "(δείτε την καρτέλα 'Google Play' στο παράθυρο 'Συγκέντρωση')", + "partyInviteIgnoreText": "Αγνόηση", + "partyInviteText": "Ο χρήστης ${NAME} σας προσκάλεσε\nνα συμμετάσχετε στη συγκέντρωσή του!", + "partyNameText": "Όνομα Συγκέντρωσης", + "partySizeText": "μέγεθος συγκέντρωσης", + "partyStatusCheckingText": "έλεγχος κατάστασης...", + "partyStatusJoinableText": "η συγκέντρωσή σας μπορεί πλέον να φιλοξενήσει από το διαδίκτυο", + "partyStatusNoConnectionText": "αδυναμία σύνδεσης με τον διακομιστή", + "partyStatusNotJoinableText": "η συγκέντρωσή σας δεν μπορεί να φιλοξενήσει από το διαδίκτυο", + "partyStatusNotPublicText": "η συγκέντρωσή σας δεν είναι δημόσια", + "pingText": "ping", + "portText": "Θύρα", + "requestingAPromoCodeText": "Αίτημα κωδικού...", + "sendDirectInvitesText": "Αποστολή Άμεσων Προσκλήσεων", + "shareThisCodeWithFriendsText": "Μοιραστείτε αυτόν τον κωδικό με φίλους σας:", + "showMyAddressText": "Εμφάνισε τη Διεύθυνσή μου", + "titleText": "Συγκέντρωση", + "wifiDirectDescriptionBottomText": "Εάν όλες οι συσκευές έχουν πίνακα 'Wi-Fi Direct', είναι να δυνατό μπορούν να τον χρησιμοποιήσουν \nγια να συνδεθούν μεταξύ τους. Όταν όλες οι συσκευές είναι συνδεδεμένες, μπορείτε να οργανώσετε\nσυγκεντρώσεις χρησιμοποιώντας τη καρτέλα 'Τοπικό Δίκτυο',όπως ακριβώς θα κάνατε και με το δίκτυο Wi-Fi.\n\nΓια καλύτερα αποτελέσματα, ο οικοδεσπότης Wi-Fi Direct θα πρέπει να είναι και ο οικοδεσπότης της ${APP_NAME} συγκέντρωσης.", + "wifiDirectDescriptionTopText": "Το Wi-Fi Direct μπορεί να χρησιμοποιηθεί για την άμεση σύνδεση Android συσκευών χωρίς\nτη χρήση Wi-Fi δικτύου. Αυτή η μέθοδος λειτουργεί καλύτερα με Android 4.2 ή νεότερο.\n\nΓια να το χρησιμοποιήσετε, ανοίξτε τις ρυθμίσεις Wi-Fi και ψάξτε γιά το μενού 'Wi-Fi Direct'.", + "wifiDirectOpenWiFiSettingsText": "Άνοιγμα Ρυθμίσεων Wi-Fi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(λειτουργεί μεταξύ κάθε πλατφόρμας)", + "worksWithGooglePlayDevicesText": "(λειτουργεί με συσκευές που τρέχουν τη Google Play (android) έκδοση του παιχνιδιού)", + "youHaveBeenSentAPromoCodeText": "Σας έχει σταλεί ένας κωδικός ${APP_NAME}:" + }, + "getTicketsWindow": { + "freeText": "ΔΩΡΕΑΝ!", + "freeTicketsText": "Δωρεάν Εισιτήρια", + "inProgressText": "Μια συναλλαγή σε εξέλιξη. Παρακαλώ ξαναπροσπαθήστε σε λίγο.", + "purchasesRestoredText": "Οι αγορές αποκαταστάθηκαν.", + "receivedTicketsText": "Αποκτήσατε ${COUNT} εισιτήρια!", + "restorePurchasesText": "Αποκατάσταση Αγορών", + "ticketPack1Text": "Μικρό Πακέτο Εισιτηρίων", + "ticketPack2Text": "Μέσαιο Πακέτο Εισιτηρίων", + "ticketPack3Text": "Μεγάλο Πακέτο Εισιτηρίων", + "ticketPack4Text": "Πακέτο Εισιτηρίων Jumbo", + "ticketPack5Text": "Πακέτο Εισιτηρίων Μαμούθ", + "ticketPack6Text": "Υπέρτατο Πακέτο Εισιτηρίων", + "ticketsFromASponsorText": "Αποκτήστε ${COUNT} εισιτήρια\nαπό χορηγία", + "ticketsText": "${COUNT} Εισιτήρια", + "titleText": "Αποκτήστε Εισιτήρια", + "unavailableLinkAccountText": "Συγνώμη, οι αγορές δεν είναι διαθέσιμες σε αυτή την πλατφόρμα.\nΩς λύση, μπορείτε να δεσμεύσετε αυτόν τον λογαριασμό με έναν\nλογαριασμό από άλλη πλατφόρμα και να αγοράσετε από εκεί.", + "unavailableTemporarilyText": "Προς το παρόν μη διαθέσιμο. Παρακαλώ ξαναπροσπαθήστε αργότερα.", + "unavailableText": "Συγνώμη, το συγκεκριμένο δεν είναι διαθέσιμο.", + "versionTooOldText": "Συγνώμη, αυτή η έκδοση του παιχνιδιού είναι πολύ παλιά. Παρακαλώ αναβαθμίστε τη με μια πιο καινούρια.", + "youHaveShortText": "έχετε ${COUNT}", + "youHaveText": "έχετε ${COUNT} εισιτήρια" + }, + "googleMultiplayerDiscontinuedText": "Συγνώμη, φαίνεται πως η υπηρεσία πολλών παικτών της Google δεν είναι πλέον διαθέσιμη.\nΠροσπαθώ να βρω αντικατάσταση όσο πιο γρήγορα γίνεται.\nΜέχρι τότε, παρακαλώ δοκιμάστε άλλο τρόπο σύνδεσης.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Πάντα", + "fullScreenCmdText": "Πλήρης Οθόνη (Cmd-F)", + "fullScreenCtrlText": "Πλήρης Οθόνη (Ctrl-F)", + "gammaText": "Gamma", + "highText": "Υψηλό", + "higherText": "Υψηλότερο", + "lowText": "Χαμηλό", + "mediumText": "Μέτριο", + "neverText": "Ποτέ", + "resolutionText": "Ανάλυση", + "showFPSText": "Ένδειξη FPS", + "texturesText": "Υφές", + "titleText": "Γραφικά", + "tvBorderText": "Πλαίσιο TV", + "verticalSyncText": "Κατακόρυφος Συγχρονισμός", + "visualsText": "Οπτικά" + }, + "helpWindow": { + "bombInfoText": "- Βόμβα -\nΙσχυρότερη από τις γροθιές, αλλά μπορεί\nνα προκαλέσει δυστυχήματα. Για καλύτερα\nαποτελέσματα ρίξτε τις βόμβες στον εχθρό\nπρίν τελειώσει το φυτίλι.", + "bombInfoTextScale": 0.5, + "canHelpText": "Το ${APP_NAME} μπορεί να βοηθήσει.", + "controllersInfoText": "Μπορείτε να παίξετε ${APP_NAME} με φίλους μέσω ενός δικτύου ή μπορείτε\nόλοι να παίξετε στην ίδια συσκευή εάν έχετε αρκετά χειριστήρια. Το\n${APP_NAME} υποστηρίζει ποικιλία από αυτά. Μπορείτε ακόμα να\nχρησιμοποιήσετε κινητά τηλέφωνα ως χειριστήρια μέσω της δωρεάν εφαρμογής\n'${REMOTE_APP_NAME}'. Βλέπε Ρυθμίσεις->Χειριστήρια για περισσότερες πληροφορίες.", + "controllersText": "Χειριστήρια", + "controlsSubtitleText": "Ο φιλικός σας ${APP_NAME} χαρακτήρας έχει μερικές βασικές κινήσεις:", + "controlsText": "Χειρισμοί", + "devicesInfoText": "Η έκδοση VR του ${APP_NAME} μπορεί να παίξει μέσω δικτύου\nμε την κανονική έκδοση, οπότε βγάλτε τα επιπλέον σας τηλέφωνα,\ntablet και υπολογιστές και ξεκινήστε το παιχνίδι. Θα μπορούσατε\nακόμα να συνδέσετε την έκδοση VR με την κανονική για να επιτρέψετε\nστους υπόλοιπους να παρακολουθούν.", + "devicesText": "Συσκευές", + "friendsGoodText": "Καλό θα ήταν να έχετε. Το ${APP_NAME} είναι πιό διασκεδαστικό με πολλούς\nπαίκτες και μπορεί να υποστηρίξει έως και 8 τη φορά, που μας οδηγεί σε:", + "friendsText": "Φίλοι", + "jumpInfoText": "- Άλμα -\nΠηδήξτε για να διασχίσετε μικρά κενά,\nνα πετάξετε αντικείμενα ψηλότερα\nκαι να εκφράσετε συναισθήματα χαράς.", + "orPunchingSomethingText": "Ή να γροθοκοπήσετε κάτι, να το πετάξετε από έναν γκρεμό, κάνοντας το να εκραγεί καθ' οδόν με μία κολλώδη βόμβα.", + "pickUpInfoText": "- Αρπαγή -\nΑρπάξτε σημαίες, εχθρούς ή οτιδήποτε\nάλλο δεν είναι βιδωμένο στο έδαφος.\nΞαναπατήστε για ρίψη.", + "powerupBombDescriptionText": "Σας επιτρέπει να ρίχνετε τρείς βόμβες\nμαζί με τη σειρά αντί για μία.", + "powerupBombNameText": "Τριπλές-Βόμβες", + "powerupCurseDescriptionText": "Ίσως θα προτιμούσατε να τα αποφύγετε.\n ...ή μήπως όχι;", + "powerupCurseNameText": "Κατάρα", + "powerupHealthDescriptionText": "Σας ανανεώνει όλη τη ζωή.\nΠοτέ δεν θα το μαντεύατε σωστά.", + "powerupHealthNameText": "Ιατρικό-Κουτί", + "powerupIceBombsDescriptionText": "Πιό αδύναμες από τις βασικές βόμβες\nαλλά αφήνουν τους εχθρούς σας\nπαγωμένους και εύθραυστους.", + "powerupIceBombsNameText": "Βόμβες-Πάγου", + "powerupImpactBombsDescriptionText": "Ελαφρώς πιό αδύναμες από τις βασικές\nβόμβες αλλά εκρήγνυνται με την επαφή.", + "powerupImpactBombsNameText": "Βόμβες-Ερεθίσματος", + "powerupLandMinesDescriptionText": "Έρχονται ανά τριάδες. Χρήσιμες\nγια τη προστασία μιάς περιοχής\nή για να σταματάνε γρήγορους εχθρούς.", + "powerupLandMinesNameText": "Νάρκες-Εδάφους", + "powerupPunchDescriptionText": "Κάνει τις γροθίες σας σκληρότερες,\nταχύτερες, καλύτερες, δυνατότερες.", + "powerupPunchNameText": "Γάντια-Πυγμαχίας", + "powerupShieldDescriptionText": "Απορροφά λίγες ζημιές που\nεναλλακτικά εσείς θα δεχόσασταν.", + "powerupShieldNameText": "Πεδίο-Ισχύος", + "powerupStickyBombsDescriptionText": "Κολλάνε σε ο,τι αγγίξουν.\nΑκολουθεί σαρκασμός.", + "powerupStickyBombsNameText": "Κολλώδεις-Βόμβες", + "powerupsSubtitleText": "Εννοείται πως κανένα παιχνίδι δεν είναι ολοκληρωμένο χωρίς ενισχυτικά:", + "powerupsSubtitleTextScale": 0.7, + "powerupsText": "Ενισχυτικά", + "punchInfoText": "- Γροθιά -\nΠροκαλούν περισσότερη ζημιά ανάλογα\nμε την ταχύτητα που κινούνται. Γι'\nαυτό τρέξτε και στρίψτε σαν μανιακός.", + "punchInfoTextScale": 0.55, + "runInfoText": "- Τρέξιμο -\nΚρατήστε πατημένο ΟΠΟΙΟΔΗΠΟΤΕ πλήκτρο για να τρέξετε. Πλαϊνά πλήκτρα ή διακόπτες λειτουργούν καλά αν διατίθενται.\nΤο τρέξιμο σας επιτρέπει να φτάνετε ταχύτερα στον προορισμό σας αλλά δυσκολεύει στο στρίψιμο, οπότε προσοχή στους γκρεμούς.", + "runInfoTextScale": 0.45, + "someDaysText": "Ορισμένες μέρες θέλετε να χτυπήσετε κάτι, ή να το ανατινάξετε.", + "titleText": "Βοήθεια ${APP_NAME}", + "toGetTheMostText": "Για καλύτερη εμπειρία από αυτό το παιχνίδι θα χρειαστείτε:", + "welcomeText": "Καλωσορίσατε στο ${APP_NAME}!" + }, + "holdAnyButtonText": "<κρατήστε πατημένο οποιδήποτε κουμπί>", + "holdAnyKeyText": "<κρατήστε πατημένο οποιοδήποτε πλήκτρο>", + "hostIsNavigatingMenusText": "- Ο χρήστης ${HOST} ελέγχει τα μενού σαν το αφεντικό -", + "importPlaylistCodeInstructionsText": "Χρησιμοποιήστε τον παρακάτω κωδικό για την εισαγωγή της λίστας κάπου αλλού:", + "importPlaylistSuccessText": "Έγινε εισαγωγή της λίστας '${NAME}' για ${TYPE}", + "importText": "Εισαγωγή", + "importingText": "Εισαγωγή...", + "inGameClippedNameText": "θα εμφανίζεται ως\n\"${NAME}\"", + "installDiskSpaceErrorText": "ΣΦΑΛΜΑ: Αδύνατη η ολοκλήρωση της εγκατάστασης.\nΕνδέχεται να ξεμείνατε από αποθηκευτικό χώρο στη\nσυσκεύη. Αδειάστε λίγο χώρο και ξαναπροσπαθήστε.", + "internal": { + "arrowsToExitListText": "πατήστε ${LEFT} ή ${RIGHT} για έξοδο από τη λίστα", + "buttonText": "κουμπί", + "cantKickHostError": "Δεν μπορείτε να διώξετε τον οικοδεσπότη.", + "chatBlockedText": "Ο παίκτης ${NAME} είναι αποκλεισμένος από τη συζήτηση γιά ${TIME} δευτ.", + "connectedToGameText": "Ενταχθήκατε σε: '${NAME}'", + "connectedToPartyText": "Ενταχθήκατε στη συγκέντρωση του χρήστη ${NAME}!", + "connectingToPartyText": "Σύνδεση...", + "connectionFailedHostAlreadyInPartyText": "Ανεπιτυχής σύνδεση. Ο οικοδεσπότης βρίσκεται σε άλλη συγκέντρωση.", + "connectionFailedPartyFullText": "Ανεπιτυχής σύνδεση. Η συγκέντρωση είναι γεμάτη.", + "connectionFailedText": "Ανεπιτυχής σύνδεση.", + "connectionFailedVersionMismatchText": "Ανεπιτυχής σύνδεση. Ο οικοδεσπότης τρέχει μιά διαφορετική έκδοση του παιχνιδιού.\nΣιγουρευτείτε ότι και οι δύο έχετε την πιο πρόσφατη έκδοση και ξαναπροσπαθήστε.", + "connectionRejectedText": "Η σύνδεση απορρίφθηκε.", + "controllerConnectedText": "Το χειριστήριο ${CONTROLLER} συνδέθηκε.", + "controllerDetectedText": "1 χειριστήριο εντοπίστηκε.", + "controllerDisconnectedText": "Το χειριστήριο ${CONTROLLER} αποσυνδέθηκε.", + "controllerDisconnectedTryAgainText": "Το χειριστήριο ${CONTROLLER} αποσυνδέθηκε. Παρακαλώ προσπαθήστε να το ξανασυνδέσετε.", + "controllerForMenusOnlyText": "Αυτό το χειριστήριο δεν μπορεί να χρησιμοποιηθεί στο παιχνίδι. Μονάχα για τη περιήγηση στα μενού.", + "controllerReconnectedText": "Το χειριστήριο ${CONTROLLER} ξανασυνδέθηκε.", + "controllersConnectedText": "Συνδέθηκαν ${COUNT} χειριστήρια.", + "controllersDetectedText": "Εντοπίστηκαν ${COUNT} χειριστήρια.", + "controllersDisconnectedText": "Αποσυνδέθηκαν ${COUNT} χειριστήρια.", + "corruptFileText": "Εντοπίστηκαν φθαρμένα αρχεία. Παρακαλώ επανεγκαταστήστε ή επικοινωνήστε με το email ${EMAIL}", + "errorPlayingMusicText": "Σφάλμα παίζοντας τη μουσική: ${MUSIC}", + "errorResettingAchievementsText": "Αδύνατη η επαναρύθμιση των online επιτευγμάτων. Παρακαλώ ξαναπροσπαθήστε αργότερα.", + "hasMenuControlText": "Ο παίκτης ${NAME} έχει τον έλεγχο στα μενού.", + "incompatibleNewerVersionHostText": "Ο οικοδεσπότης τρέχει μία νεότερη έκδοση το παιχνιδιού.\nΑναβαθμίστε στην πιο πρόσφατη έκδοση και ξαναπροσπαθήστε.", + "incompatibleVersionHostText": "Ο οικοδεσπότης τρέχει μιά διαφορετική έκδοση του παιχνιδιού.\nΣιγουρευτείτε ότι και οι δύο έχετε την πιο πρόσφατη έκδοση και ξαναπροσπαθήστε.", + "incompatibleVersionPlayerText": "Ο χρήστης ${NAME} τρέχει μιά διαφορετική έκδοση του παιχνιδιού.\nΣιγουρευτείτε ότι και οι δύο έχετε την πιο πρόσφατη έκδοση και ξαναπροσπαθήστε.", + "invalidAddressErrorText": "Σφάλμα: μη έγκυρη διεύθυνση.", + "invalidNameErrorText": "Σφάλμα: μη έγκυρο όνομα.", + "invalidPortErrorText": "Σφάλμα: μη έγκυρη θύρα.", + "invitationSentText": "Η πρόσκληση έχει σταλεί.", + "invitationsSentText": "${COUNT} προσκλήσεις έχουν σταλεί.", + "joinedPartyInstructionsText": "Κάποιος εντάχθηκε στη συγκέντρωσή σας.\nΠηγαίντε στο 'Παίξε' για να ξεκινήσετε παιχνίδι.", + "keyboardText": "Πληκτρολόγιο", + "kickIdlePlayersKickedText": "Αποβολή του παίκτη ${NAME} λόγω απραξίας.", + "kickIdlePlayersWarning1Text": "Ο παίκτης ${NAME} θα αποβληθεί σε ${COUNT} δευτ. αν παραμείνει άπραγος.", + "kickIdlePlayersWarning2Text": "(αυτό μπορείτε να το απενεργοποιήσετε στίς Ρυθμίσεις -> Σύνθετες)", + "leftGameText": "Αποχώρησατε από: '${NAME}'", + "leftPartyText": "Αποχωρήσατε από τη συγκέντρωση του παίκτη ${NAME}.", + "noMusicFilesInFolderText": "Ο φάκελος δεν περιέχει μουσικά αρχεία.", + "playerJoinedPartyText": "Ο παίκτης ${NAME} εντάχθηκε στη συγκέντρωση!", + "playerLeftPartyText": "Ο παίκτης ${NAME} αποχώρησε από τη συγκέντρωση.", + "rejectingInviteAlreadyInPartyText": "Η πρόσκληση απορρίπτεται (βρίσκεστε ήδη σε συγκέντρωση).", + "serverRestartingText": "Ο διακομιστής επανεκκινήτε. Παρακαλώ προσπαθήστε να επανενταχθείτε σε μία στιγμή...", + "serverShuttingDownText": "Ο διακομιστής κλείνει...", + "signInErrorText": "Σφάλμα κατά τη σύνδεση.", + "signInNoConnectionText": "Αδύνατη η σύνδεση. (χωρίς πρόσβαση στο διαδίκτυο;)", + "telnetAccessDeniedText": "ΣΦΑΛΜΑ: πρόσβαση telnet δεν έχει παραχωρηθεί στον χρήστη.", + "timeOutText": "(ο χρόνος λήγει σε ${TIME} δευτερόλεπτα)", + "touchScreenJoinWarningText": "Έχετε ενταχθεί με την οθόνη αφής.\nΑν συνέβη καταλάθος πατήστε με αυτή στο 'Μενού->Έξοδος Παιχνιδιού'.", + "touchScreenText": "Οθόνη Αφής", + "unableToResolveHostText": "Σφάλμα: αδύνατη η επίλυση του οικοδεσπότη.", + "unavailableNoConnectionText": "Αυτό είναι προς το παρόν μη διαθέσιμο (χωρίς πρόσβαση στο διαδίκτυο;)", + "vrOrientationResetCardboardText": "Χρησιμοποιήστε το για την επαναφορά το προσανατολισμού VR.\nΓια να παίξετε το παιχνίδι θα χρειαστείτε ένα εξωτερικό χειριστήριο.", + "vrOrientationResetText": "Έγινε επαναφορά του VR προσανατολισμού.", + "willTimeOutText": "(θα σταματήσει αν παραμείνει αδρανής)" + }, + "jumpBoldText": "ΑΛΜΑ", + "jumpText": "Άλμα", + "keepText": "Κράτησέ τες", + "keepTheseSettingsText": "Θέλετε να κρατήσετε έτσι τις ρυθμίσεις;", + "keyboardChangeInstructionsText": "Πατήστε δύο φορές το κουμπί κενό για να αλλάξετε πληκτρολόγιο.", + "keyboardNoOthersAvailableText": "Δεν υπάρχουν άλλα πληκτρολόγια διαθέσιμα.", + "keyboardSwitchText": "Αλλαγή πληκτρολογίου σε \"${NAME}\".", + "kickOccurredText": "Ο παίκτης ${NAME} αποβλήθηκε.", + "kickQuestionText": "Αποβολή του παίκτη ${NAME};", + "kickText": "Αποβολή", + "kickVoteCantKickAdminsText": "Οι διαχειριστές δεν μπορούν να διωχθούν.", + "kickVoteCantKickSelfText": "Δεν μπορείτε να διώξετε τον εαυτό σας.", + "kickVoteFailedNotEnoughVotersText": "Δεν υπάρχουν αρκετοί παίκτες για ψήφο.", + "kickVoteFailedText": "Η ψηφοφορία για αποβολή απέτυχε.", + "kickVoteStartedText": "Μια ψηφοφορία για την αποβολή του παίκτη ${NAME} ξεκίνησε.", + "kickVoteText": "Ψήφος προς Αποβολή", + "kickVotingDisabledText": "Το διώξημο μέσο ψήφου είναι απενεργοποιημένο.", + "kickWithChatText": "Γράψτε ${YES} στη συνομιλία για ναι ή ${NO} για όχι.", + "killsTallyText": "${COUNT} φόνοι", + "killsText": "Φόνοι", + "kioskWindow": { + "easyText": "Εύκολο", + "epicModeText": "Επική λειτουργία", + "fullMenuText": "Ολοκληρωμένο Μενού", + "hardText": "Δύσκολο", + "mediumText": "Μέτριο", + "singlePlayerExamplesText": "Μονός Παίκτης / Συνεργασία Παραδείγματα", + "versusExamplesText": "Παραδείγματα Εναντίον" + }, + "languageSetText": "Η γλώσσα είναι τώρα \"${LANGUAGE}\".", + "lapNumberText": "Γύρος ${CURRENT}/${TOTAL}", + "lastGamesText": "(τελευταία ${COUNT} παιχνίδια)", + "leaderboardsText": "Πίνακες Κατάταξης", + "league": { + "allTimeText": "Διαχρονικά", + "currentSeasonText": "Παρούσα Σεζόν (${NUMBER})", + "leagueFullText": "${NAME} Κατηγορία", + "leagueRankText": "Κατάταξη Κατηγορίας", + "leagueText": "Κατηγορία", + "rankInLeagueText": "#${RANK}, ${NAME} Κατηγορία${SUFFIX}", + "seasonEndedDaysAgoText": "Η σεζόν έληξε πριν από ${NUMBER} μέρες.", + "seasonEndsDaysText": "Η σεζόν λήγει σε ${NUMBER} μέρες.", + "seasonEndsHoursText": "Η σεζόν λήγει σε ${NUMBER} ώρες.", + "seasonEndsMinutesText": "Η σεζόν λήγει σε ${NUMBER} λεπτά.", + "seasonText": "Σεζόν ${NUMBER}", + "tournamentLeagueText": "Πρέπει να φτάσετε στην κατηγορία ${NAME} για να συμμετάσχετε σε αυτό το τουρνουά.", + "trophyCountsResetText": "Ο αριθμός των βραβείων θα υποστεί επαναφορά στην επόμενη σεζόν." + }, + "levelBestScoresText": "Οι υψηλότερες βαθμολογίες στο επίπεδο ${LEVEL}", + "levelBestTimesText": "Οι καλύτεροι χρόνοι στο επίπεδο ${LEVEL}", + "levelIsLockedText": "Το επίπεδο ${LEVEL} είναι κλειδωμένο.", + "levelMustBeCompletedFirstText": "Το επίπεδο ${LEVEL} πρέπει πρώτα να ολοκληρωθεί.", + "levelText": "Επίπεδο ${NUMBER}", + "levelUnlockedText": "Το Επίπεδο Ξεκλειδώθηκε!", + "livesBonusText": "Μπόνους Ζωών", + "loadingText": "Φόρτωση", + "loadingTryAgainText": "Φορτώνει. Παρακαλώ ξαναπροσπαθήστε αργότερα...", + "macControllerSubsystemBothText": "Και τα δύο (δεν προτείνεται)", + "macControllerSubsystemClassicText": "Κλασσικό", + "macControllerSubsystemDescriptionText": "(δοκιμάστε να το αλλάξετε αυτό αν τα χειριστήριά σας δεν λειτουργούν)", + "macControllerSubsystemMFiNoteText": "Εντοπίστηκε χειριστήριο φτιαγμένο-γιά-iOS/Mac.\nΊσως θα θέλατε να το ενεργοποιήσετε στις Ρυθμίσεις -> Χειριστήρια", + "macControllerSubsystemMFiText": "Φτιαγμένο-για-iOS/Mac", + "macControllerSubsystemTitleText": "Υποστήριξη Χειριστηρίων", + "mainMenu": { + "creditsText": "Εύσημα", + "demoMenuText": "Μενού Επίδειξης", + "endGameText": "Τέλος Παιχνιδιού", + "exitGameText": "Έξοδος Παιχνιδιού", + "exitToMenuText": "Έξοδος στο μενού;", + "howToPlayText": "Πως Παίζεται;", + "justPlayerText": "(Μονάχα ο Παίκτης ${NAME})", + "leaveGameText": "Αποχώρηση από το Παιχνίδι", + "leavePartyConfirmText": "Πραγματικά, αποχώρηση από τη συγκέντρωση;", + "leavePartyText": "Αποχώρηση από τη Συγκέντρωση", + "quitText": "Εγκατάλειψη", + "resumeText": "Συνέχιση", + "settingsText": "Ρυθμίσεις" + }, + "makeItSoText": "Κάν' το Έτσι", + "mapSelectGetMoreMapsText": "Περισσότεροι Χάρτες...", + "mapSelectText": "Επιλογή...", + "mapSelectTitleText": "Χάρτες ${GAME}", + "mapText": "Χάρτης", + "maxConnectionsText": "Μέγιστος Αριθμός Συνδέσεων", + "maxPartySizeText": "Μέγιστος Αριθμός Συγκέντρωσης", + "maxPlayersText": "Μέγιστος Αριθμός Παικτών", + "modeArcadeText": "Λειτουργία \"Arcade\"", + "modeClassicText": "Κλασσική λειτουργία", + "modeDemoText": "Δοκιμαστική λειτουργία", + "mostValuablePlayerText": "Πολυτιμότερος Παίκτης", + "mostViolatedPlayerText": "Πιο Ξυλοδαρμένος Παίκτης", + "mostViolentPlayerText": "Πιο Βίαιος Παίκτης", + "moveText": "Κίνηση", + "multiKillText": "${COUNT}-ΦΟΝΟΙ!!!", + "multiPlayerCountText": "${COUNT} παίκτες", + "mustInviteFriendsText": "Σημείωση: πρέπει να προσκαλέσετε φίλους από τον\nπίνακα \"${GATHER}\" ή να συνδέσετε χειριστήρια\nγια να παίξετε παιχνίδι για πολλαπλούς παίκτες.", + "nameBetrayedText": "Ο ${NAME} πρόδωσε τον ${VICTIM}.", + "nameDiedText": "Ο παίκτης ${NAME} πέθανε.", + "nameKilledText": "Ο ${NAME} σκότωσε τον ${VICTIM}.", + "nameNotEmptyText": "Το όνομα δεν μπορεί να είναι κενό!", + "nameScoresText": "Ο παίκτης ${NAME} Σκοράρει!", + "nameSuicideKidFriendlyText": "Ο παίκτης ${NAME} σκοτώθηκε καταλάθος.", + "nameSuicideText": "Ο παίκτης ${NAME} αυτοκτόνησε.", + "nameText": "Όνομα", + "nativeText": "Προσαρμόσιμο", + "newPersonalBestText": "Νέα προσωπικό ρεκόρ!", + "newTestBuildAvailableText": "Μια νεότερη δοκιμαστική έκδοση είναι διαθέσιμη! (κύρια ${VERSION}, δοκιμαστική ${BUILD}).\nΠαραλάβετέ την από ${ADDRESS}", + "newText": "Νέο", + "newVersionAvailableText": "Μιά νέα έκδοση ${APP_NAME} είναι διαθέσιμη! (${VERSION})", + "nextAchievementsText": "Επόμενα Επιτεύγματα:", + "nextLevelText": "Επόμενο Επίπεδο", + "noAchievementsRemainingText": "- κανένα", + "noContinuesText": "(χωρίς \"Συνέχιση\")", + "noExternalStorageErrorText": "Δεν βρέθηκε εξωτερικός αποθηκευτικός χώρος σε αυτή τη συσκευή", + "noGameCircleText": "Σφάλμα: δεν έχετε συνδεθεί στο GameCircle", + "noScoresYetText": "Δεν υπάρχουν βαθμολογίες ακόμη.", + "noThanksText": "Όχι Ευχαριστώ", + "noTournamentsInTestBuildText": "ΠΡΟΣΟΧΗ: Τα σκορ του τουρνουά από αυτή τη δοκιμαστική έκδοση θα αγνοηθούν.", + "noValidMapsErrorText": "Δεν βρέθηκαν έγκυροι χάρτες γι' αυτόν τον τύπο παιχνιδιού.", + "notEnoughPlayersRemainingText": "Οι παίκτες που παραμένουν δεν είναι αρκετοί. Αποχωρήστε και ξεκινήστε νέο παιχνίδι.", + "notEnoughPlayersText": "Χρειάζεστε τουλάχιστον ${COUNT} παίκτες για να ξεκινήσετε αυτό το παιχνίδι!", + "notNowText": "Όχι Τώρα", + "notSignedInErrorText": "Πρέπει να συνδεθείτε γι' αυτό.", + "notSignedInGooglePlayErrorText": "Πρέπει να συνδεθείτε με το Google Play γι' αυτό.", + "notSignedInText": "δεν έχετε συνδεθεί", + "nothingIsSelectedErrorText": "Τίποτα δεν επιλέχθηκε!", + "numberText": "#${NUMBER}", + "offText": "Κλειστό", + "okText": "Εντάξει", + "onText": "Ανοιχτό", + "onslaughtRespawnText": "Ο παίκτης ${PLAYER} θα ξαναδημιουργηθεί στο κύμα ${WAVE}", + "orText": "${A} ή ${B}", + "otherText": "Άλλο...", + "outOfText": "(#${RANK} από ${ALL})", + "ownFlagAtYourBaseWarning": "Η σημαία σας πρέπει να είναι\nστη βάση της για να σκοράρετε!", + "packageModsEnabledErrorText": "Το παιχνίδι σε δίκτυο δεν επιτρέπεται όσο υπάρχουν ενεργά τοπικά-πακέτα-τροποποίησης (βλέπε Ρυθμίσεις->Σύνθετες)", + "partyWindow": { + "chatMessageText": "Μύνημα Συζήτησης", + "emptyText": "Η συγκέντρωσή σας είναι άδεια", + "hostText": "(οικοδεσπότης)", + "sendText": "Αποστολή", + "titleText": "Η Συγκέντρωσή Σας" + }, + "pausedByHostText": "(παύση από τον οικοδεσπότη)", + "perfectWaveText": "Τέλειο Κύμα!", + "pickUpText": "Αρπαγή", + "playModes": { + "coopText": "Συνεργασία", + "freeForAllText": "Ελεύθερο-Για-Όλους", + "multiTeamText": "Πολλαπλές-Ομάδες", + "singlePlayerCoopText": "Μονός Παίκτης / Συνεργασία", + "teamsText": "Ομάδες" + }, + "playText": "Παίξε", + "playWindow": { + "oneToFourPlayersText": "1-4 παίκτες", + "titleText": "Παιχνίδι", + "twoToEightPlayersText": "2-8 παίκτες" + }, + "playerCountAbbreviatedText": "${COUNT}παίκ.", + "playerDelayedJoinText": "Ο παίκτης ${PLAYER} θα κάνει είσοδο στην εκκίνηση του επόμενου γύρου.", + "playerInfoText": "Πληροφορίες Παίκτη", + "playerLeftText": "Ο παίκτης ${PLAYER} αποχώρησε από το παιχνίδι.", + "playerLimitReachedText": "Το όριο των ${COUNT} επετεύχθη. Περεταίρω ένταξη δεν επιτρέπεται.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Δεν μπορείτε να διαγράψετε το προφίλ του λογαριασμού σας.", + "deleteButtonText": "Διαγραφή\nΠροφίλ", + "deleteConfirmText": "Διαγραφή '${PROFILE}';", + "editButtonText": "Επεξεργασία\nΠροφίλ", + "explanationText": "(προσαρμοσμένα ονόματα παικτών και εμφανίσεις γι' αυτόν τον λογαριασμό)", + "newButtonText": "Νέο\nΠροφίλ", + "titleText": "Προφίλ Παίκτη" + }, + "playerText": "Παίκτης", + "playlistNoValidGamesErrorText": "Αυτή η λίστα αποτελείται από μη έγκυρα ξεκλειδωμένα παιχνίδια.", + "playlistNotFoundText": "η λίστα δεν βρέθηκε", + "playlistsText": "Λίστες Παιχνιδιών", + "pleaseRateText": "Εάν απολαμβάνετε το ${APP_NAME}, παρακαλώ σκεφτείτε να αφιερώσετε μιά στιγμή\nγια να το βαθμολογήσετε ή να γράψετε μιά κριτική. Αυτό θα προσφέρει χρήσιμη\nανατροφοδότηση και θα βοηθήσει για την υποστήριξη της μέλλουσας ανάπτυξης.\n\nευχαριστώ!\n-eric", + "pleaseWaitText": "Παρακαλώ περιμένετε...", + "pluginsDetectedText": "Νέα πρόσθετο/α εντοπίστηκαν. Ενεργοποίηστε/Διαμορφώστε τα από τις ρυθμίσεις.", + "pluginsText": "Πρόσθετα", + "practiceText": "Πρακτική", + "pressAnyButtonPlayAgainText": "Πατήστε οποιοδήποτε κουμπί για να ξαναπαίξετε...", + "pressAnyButtonText": "Πατήστε οποιοδήποτε κουμπί για να συνεχίσετε...", + "pressAnyButtonToJoinText": "πατήστε οποιοδήποτε κουμπί για να ενταχθείτε...", + "pressAnyKeyButtonPlayAgainText": "Πατήστε οποιοδήποτε πλήκτρο/κουμπί για να ξαναπαίξετε...", + "pressAnyKeyButtonText": "Πατήστε οποιοδήποτε πλήκτρο/κουμπί για να συνεχίσετε...", + "pressAnyKeyText": "Πατήστε οποιοδήποτε πλήκτρο...", + "pressJumpToFlyText": "** Πατήστε το άλμα συνεχόμενα για να πετάξετε **", + "pressPunchToJoinText": "πατήστε ΓΡΟΘΙΑ για να ενταχθείτε...", + "pressToOverrideCharacterText": "πιέστε ${BUTTONS} για να αναιρέσετε τον χαρακτήρα σας", + "pressToSelectProfileText": "πατήστε ${BUTTONS} για την επιλογή παίκτη", + "pressToSelectTeamText": "πατήστε ${BUTTONS} για την επιλογή ομάδας", + "promoCodeWindow": { + "codeText": "Κωδικός", + "enterText": "Εισαγωγή" + }, + "promoSubmitErrorText": "Σφάλμα κατά την υποβολή του κωδικού. Ελέγξτε την πρόσβασή σας στο διαδίκτυο", + "ps3ControllersWindow": { + "macInstructionsText": "Απενεργοποιήστε το PS3 σας, σιγουρευτείτε ότι το Bluetooth\nείναι ενεργοποιημένο στον Mac σας, έπειτα συνδέστε το χειριστήριό\nσας στον Mac μέσω καλωδίου USB για να τα ζευγαρώσετε. Από εκεί\nκαι στο εξής μπορείτε να χρησιμοποιήσετε το κουμπί \"home\" του \nχειριστηρίου για να το συνδέσετε στον Mac σας είτε με ενσύρματη\n(USB) ή ασύρματη (Bluetooth) λειτουργία.\n\nΣε μερικούς Mac μπορεί να σας ζητηθεί κωδικός πρόσβασης\nκατά το ζευγάρωμα. Αν συμβεί αυτό, δείτε το ακόλουθο\nεκπαιδευτικό ή κάντε αναζήτηση google για βοήθεια.\n\nΤα PS3 χειριστήρια που συνδέθηκαν ασύρματα πρέπει να εμφανίζονται\nστη λίστα με τις συσκευές στο Προτιμήσεις Συστήματος->Bluetooth.\nΜπορεί να χρειαστεί να τα αφαιρέσετε από αυτή τη λίστα όταν\nθα θελήσετε να τα χρησιμοποιήσετε πάλι για το PS3 σας.\n\nΕπίσης σιγουρευτείτε ότι θα αποσυνδέσετε από το Bluetooth για όσο δεν\nχρησιμοποιούνται, ειδάλλως οι μπαταρίες θα συνεχίσουν να ξοδεύονται.\n\nΤο Bluetooth μπορεί να υποστηρίξει έως και 7 συνδεδεμένες συσκευές,\nωστόσο η ισχύς μπορεί να διαφέρει.", + "macInstructionsTextScale": 0.74, + "ouyaInstructionsText": "Για να χρησιμοποιήσετε ένα χειριστήριο PS3 με το OUYA σας, απλά συνδέστε το με ένα καλώδιο\nUSB για να τα ζευγαρώσετε. Κάνοντας αυτό μπορεί να αποσυνδεθούν τα υπόλοιπά σας χειριστήρια,\nγι' αυτό μετά πρέπει να επανεκκινήσετε το OUYA σας και να αποσυνδέσετε το καλώδιο USB.\n\nΑπό εκεί και στο εξής θα μπορείτε να χρησιμοποιείτε το κουμπί \"HOME\" για να τα\nσυνδέετε ασύρματα. Όταν τελειώσετε με το παιχνίδι, κρατήστε πατημένο το κουμπί\n\"HOME\" για 10 δευτερόλεπτα ώστε να απενεργοποιήσετε το χειριστήριο, ειδάλλως\nθα παραμείνει ανοιχτό ξοδεύοντας μπαταρία.", + "ouyaInstructionsTextScale": 0.6, + "pairingTutorialText": "εκπαιδευτικό βίντεο ζευγαρώματος", + "titleText": "Χρήση PS3 Χειριστηρίων με το ${APP_NAME}:" + }, + "punchBoldText": "ΓΡΟΘΙΑ", + "punchText": "Γροθιά", + "purchaseForText": "Αγορά για ${PRICE}", + "purchaseGameText": "Αγορά Παιχνιδιού", + "purchasingText": "Αγορά...", + "quitGameText": "Έξοδος από το ${APP_NAME};", + "quittingIn5SecondsText": "Έξοδος σε 5 δευτερόλεπτα...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Τυχαίο", + "rankText": "Κατάταξη", + "ratingText": "Βαθμολογία", + "reachWave2Text": "Φτάστε στο κύμα 2 για να καταταχθείτε.", + "readyText": "έτοιμος", + "recentText": "Πρόσφατα", + "remoteAppInfoShortText": "Το ${APP_NAME} είναι πιό διασκεδαστικό όταν παίζεται με φίλους και\nοικεγένεια. Συνδέστε ένα ή περισσότερα χειριστήρια ή εγκαταστήστε\nτην εφαρμογή ${REMOTE_APP_NAME} σε τηλέφωνα ή tablets για να τα\nχρησιμοποιήσετε ως χειριστήρια.", + "remote_app": { + "app_name": "Χειριστήριο BombSquad", + "app_name_short": "BSΧειριστήριο", + "button_position": "Θέση Κουμπιών", + "button_size": "Μέγεθος Κουμπιών", + "cant_resolve_host": "Αδύνατη η επίλυση του οικοδεσπότη.", + "capturing": "Σύλληψη...", + "connected": "Συνδέθηκε.", + "description": "Χρησιμοποίησε το τηλέφωνο ή το tablet σου ως χειριστήριο BombSquad!\nΈως και 8 συσκευές μπορούν να συνδεθούν ταυτόχρονα για ένα επικό παιχνίδι πολλαπλών παικτών (σε τοπικό δίκτυο) σε μία απλή TV ή tablet.", + "disconnected": "Ο διακομιστής σας αποσύνδεσε.", + "dpad_fixed": "σταθερό", + "dpad_floating": "ελεύθερο", + "dpad_position": "Θέση D-Pad", + "dpad_size": "Μέγεθος D-Pad", + "dpad_type": "Τύπος D-Pad", + "enter_an_address": "Εισάγετε Διεύθυνση", + "game_full": "Αυτό το παιχνίδι είναι γεμάτο ή δεν δέχεται συνδέσεις.", + "game_shut_down": "Το παιχνίδι έκλεισε.", + "hardware_buttons": "Κουμπιά Υλισμικού", + "join_by_address": "Ένταξη μέσω Διεύθυνσης...", + "lag": "Καθυστέρηση: ${SECONDS} δευτερόλεπτα", + "reset": "Επαναφορά στα Προκαθορισμένα", + "run1": "Τρέξιμο 1", + "run2": "Τρέξιμο 2", + "searching": "Αναζήτηση για παιχνίδια BombSquad...", + "searching_caption": "Πατήστε στο όνομα ένος παιχνδιού για να συνδεθείτε σε αυτό.\nΣιγουρευτείτε ότι βρίσκεστε στο ίδιο δίκτυο wifi με το παιχνίδι.", + "start": "Εκκίνηση", + "version_mismatch": "Οι εκδόσεις δεν ταιριάζουν.\nΣιγουρευτείτε ότι έχετε τις πιό πρόσφατες εκδόσεις για το\nBombSquad και για το Χειριστήριο BombSquad και ξαναπροσπαθήστε." + }, + "removeInGameAdsText": "Ξεκλειδώστε το \"${PRO}\" στο κατάστημα για να αφαιρέσετε τις διαφημήσεις κατα τη διάρκεια του παιχνιδιού.", + "renameText": "Μετονομασία", + "replayEndText": "Λήξη Επανάληψης", + "replayNameDefaultText": "Επανάληψη Τελευταίου Παιχνιδιού", + "replayReadErrorText": "Σφάλμα κατά την ανάγνωση του αρχείου επανάληψης.", + "replayRenameWarningText": "Μετονομάστε το \"${REPLAY}\" μετά από κάθε παιχνίδι άμα θέλετε να το κρατήσετε, ειδάλλως θα αντικαθίσταται.", + "replayVersionErrorText": "Συγνώμη, αυτή η επανάληψη δημιουργήθηκε σε μιά διαφορετική\nέκδοση του παιχνιδιού και δεν μπορεί να χρησιμοποιηθεί.", + "replayWatchText": "Προβολή Επανάληψης", + "replayWriteErrorText": "Σφάλμα κατά την εγγραφή του αρχείου επανάληψης.", + "replaysText": "Επαναλήψεις", + "reportPlayerExplanationText": "Χρησιμοποιήστε αυτό το email για να κάνετε αναφορά ζαβολιάς, απρεπούς λεξιλογίου ή άλλου είδους κακής συμπεριφοράς.\nΠαρακαλώ περιγράψτε παρακάτω:", + "reportThisPlayerCheatingText": "Ζαβολιά", + "reportThisPlayerLanguageText": "Απρεπές Λεξιλόγιο", + "reportThisPlayerReasonText": "Τι θα θέλατε να αναφέρετε;", + "reportThisPlayerText": "Αναφορά Αυτού του Παίκτη", + "requestingText": "Αίτηση...", + "restartText": "Επανεκκίνηση", + "retryText": "Εκ νέου απόπειρα", + "revertText": "Επαναφορά", + "runText": "Τρέξιμο", + "saveText": "Αποθήκευση", + "scanScriptsErrorText": "Σφάλματα σάρωσης σάρωσης; δείτε το αρχείο καταγραφής για λεπτομέρειες.", + "scoreChallengesText": "Προκλήσεις Βαθμολογίας", + "scoreListUnavailableText": "Λίστα βαθμολογίας μη διαθέσιμη.", + "scoreText": "Βαθμολογία", + "scoreUnits": { + "millisecondsText": "Χιλιοστά Δευτερολέπτου", + "pointsText": "Πόντοι", + "secondsText": "Δευτερόλεπτα" + }, + "scoreWasText": "(ήταν ${COUNT})", + "selectText": "Επιλογή", + "seriesWinLine1PlayerText": "ΚΕΡΔΙΣΕ ΤΗ", + "seriesWinLine1TeamText": "ΚΕΡΔΙΣΕ ΤΗ", + "seriesWinLine1Text": "ΚΕΡΔΙΣΕ ΤΗ", + "seriesWinLine2Text": "ΣΕΙΡΑ!", + "settingsWindow": { + "accountText": "Λογαριασμός", + "advancedText": "Σύνθετες", + "audioText": "Ήχος", + "controllersText": "Χειριστήρια", + "graphicsText": "Γραφικά", + "playerProfilesMovedText": "Σημείωση: η ενότητα \"Προφίλ Παίκτη\" μετακινήθηκε στο παράθυρο \"Λογαριασμός\" στο κύριο μενού.", + "titleText": "Ρυθμίσεις" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(ένα απλό, φιλικό με τα χειριστήρια πληκτρολόγιο οθόνης για επεξεργασία κειμένου)", + "alwaysUseInternalKeyboardText": "Πάντα να Χρησιμοποιείται το Εσωτερικό Πληκτρολόγιο", + "benchmarksText": "Έλεγχοι Απόδοσης & Κόπωσης", + "disableCameraGyroscopeMotionText": "Απενεργοποιήστε γυροσκοπική κίνηση της κάμερας", + "disableCameraShakeText": "Απενεργοποιήστε το κούνημα της κάμερας", + "disableThisNotice": "(μπορείτε να απενεργοποιήσετε αυτή τήν ειδοποίηση στις σύνθετες ρυθμίσεις)", + "enablePackageModsDescriptionText": "(επιτρέπει επιπλέον δυνατότητες τροποποίησης αλλά απενεργοποιεί το παιχνίδι σε δίκτυο)", + "enablePackageModsText": "Ενεργοποίηση Τοπικών Πακέτων Τροποποίησης", + "enterPromoCodeText": "Εισαγωγή Κωδικού", + "forTestingText": "Σημείωση: αυτές οι τιμές είναι μονάχα για έλεγχο και θα χαθούν όταν πραγματοποιηθεί έξοδος από την εφαρμογή.", + "helpTranslateText": "Οι μη Αγγλικές μεταφράσεις του ${APP_NAME} είναι υποστιριζόμενες\nαπό την κοινότητα. Αν θα θέλατε να συνεισφέρετε ή να διορθώσετε μια\nμετάφραση ακολουθήστε τον παρακάτω σύνδεσμο. Ευχαριστώ προκαταβολικά!", + "kickIdlePlayersText": "Αποβολή Άπραγων Παικτών", + "kidFriendlyModeText": "Λειτουργία για Παιδιά (μειωμένη βία, κτλ)", + "languageText": "Γλώσσα", + "moddingGuideText": "Οδηγός Τροποποίησης", + "mustRestartText": "Για να λειτουργήσει, πρέπει να επανεκκινήσετε το παιχνίδι.", + "netTestingText": "Έλεγχος Δικτύου", + "resetText": "Επαναφορά", + "showBombTrajectoriesText": "Εμφάνιση Πορείας Βόμβας", + "showPlayerNamesText": "Προβολή Ονομάτων Παικτών", + "showUserModsText": "Προβολή Φακέλου Πακέτων Τροποποίησης", + "titleText": "Σύνθετες", + "translationEditorButtonText": "Επεξεργαστής Γλωσσικών Μεταφράσεων ${APP_NAME}", + "translationFetchErrorText": "κατάσταση μεταφράσεων μη διαθέσιμη", + "translationFetchingStatusText": "έλεγχος κατάστασης μεταφράσεων...", + "translationInformMe": "Πληροφόρησέ με όταν η γλώσσα μου χρειάζεται ενημερώσεις", + "translationNoUpdateNeededText": "η συγγεκριμμένη γλώσσα είναι ενημερωμένη, γιούπι!", + "translationUpdateNeededText": "** η συγκεκριμένη γλώσσα χρειάζεται ενημερώσεις!! **", + "vrTestingText": "Έλεγχος VR" + }, + "shareText": "Κοινοποίηση", + "sharingText": "Κοινοποίηση...", + "showText": "Προβολή", + "signInForPromoCodeText": "Πρέπει να συνδεθείτε σε έναν λογαριασμό ώστε οι κωδικοί να πιάσουν τόπο.", + "signInWithGameCenterText": "Για να χρησιμοποιήσετε έναν λογαριασμό Game Center,\nσυνδεθείτε σε αυτόν με την εφαρμογή Game Center.", + "singleGamePlaylistNameText": "Μόνο ${GAME}", + "singlePlayerCountText": "1 παίκτης", + "soloNameFilterText": "Σόλο ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Επιλογή Χαρακτήρα", + "Chosen One": "Εκλεκτός", + "Epic": "Παιχνίδια Επικής Λειτουργίας", + "Epic Race": "Επικός Αγώνας Ταχύτητας", + "FlagCatcher": "Κατάκτηση Σημαίας", + "Flying": "Χαρούμενες Σκέψεις", + "Football": "Ποδόσφαιρο", + "ForwardMarch": "Έφοδος", + "GrandRomp": "Κυριαρχία", + "Hockey": "Χόκεϋ", + "Keep Away": "Απομάκρυνση", + "Marching": "Περίγυρος", + "Menu": "Κύριο Μενού", + "Onslaught": "Επίθεση", + "Race": "Αγώνας Ταχύτητας", + "Scary": "Βασιλιάς του Λόφου", + "Scores": "Οθόνη Βαθμολογίας", + "Survival": "Εξόντωση", + "ToTheDeath": "Μάχη Θανάτου", + "Victory": "Τελική Οθόνη Βαθμολογίας" + }, + "spaceKeyText": "κενό", + "statsText": "Στατιστικές", + "storagePermissionAccessText": "Αυτό απαιτεί πρόσβαση στον χώρο αποθήκευσης", + "store": { + "alreadyOwnText": "Το αντικείμενο \"${NAME}\" σας ανήκει ήδη!", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Αφαιρεί τις διαφιμήσεις στο παιχνίδι και τις αναδυόμενες οθόνες\n• Ξεκλειδώνει περισσότερες ρυθμίσεις\n• Επιπλέον περιλαμβάνει:", + "buyText": "Αγορά", + "charactersText": "Χαρακτήρες", + "comingSoonText": "Προσεχώς...", + "extrasText": "Επιπλέον", + "freeBombSquadProText": "Το BombSquad είναι πλέον δωρεάν, αλλά αφού το έχετε αγοράσει εξ' αρχής\nθα λάβετε την BombSquad Pro αναβάθμιση και ${COUNT} εισιτήρια ως ευχαριστώ.\nΑπολαύστε τα νέα χαρακτηριστικά, και ευχαριστώ για την υποστήριξη!\n-Eric", + "holidaySpecialText": "Ειδικά Διακοπών/Αργίας", + "howToSwitchCharactersText": "(πηγαίντε στο \"${SETTINGS} -> ${PLAYER_PROFILES}\" για να αναθέσετε και να προσαρμόσετε χαρακτήρες)", + "howToUseIconsText": "(δημιουργήστε παγκόσμια προφίλ παίκτη (στο παράθυρο των λογαριασμών) για να μπορείτε να τα χρησιμοποιήσετε)", + "howToUseMapsText": "(χρησιμοποιήστε αυτούς τους χάρτες στις δικές σας λίστες Ομάδες/Ελεύθερο-Για-Όλους)", + "iconsText": "Εικονίδια", + "loadErrorText": "Αδύνατη η φόρτωση της σελίδας.\nΕλέγξτε την πρόσβασή σας στο διαδίκτυο.", + "loadingText": "Φόρτωση", + "mapsText": "Χάρτες", + "miniGamesText": "ΜικροΠαιχνίδια", + "oneTimeOnlyText": "(μια φορά μόνο)", + "purchaseAlreadyInProgressText": "Η αγορά αυτού του αντικειμένου βρίσκεται ήδη σε εξέλιξη.", + "purchaseConfirmText": "Αγορά του αντικειμένου ${ITEM};", + "purchaseNotValidError": "Η αγορά δεν είναι έγκυρη.\nΕπικοινωνήστε στο ${EMAIL} αν αυτό είναι σφάλμα.", + "purchaseText": "Αγορά", + "saleBundleText": "Προσφορά Πακέτου!", + "saleExclaimText": "Προσφορά!", + "salePercentText": "(${PERCENT}% προσφορά)", + "saleText": "ΠΡΟΣΦΟΡΑ", + "searchText": "Αναζήτηση", + "teamsFreeForAllGamesText": "Παιχνίδια για Ομάδες / Ελεύθερο-Για-Όλους", + "totalWorthText": "*** Αξίας ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Αναβάθμιση;", + "winterSpecialText": "Ειδικά Χειμερινά", + "youOwnThisText": "- σας ανήκει -" + }, + "storeDescriptionText": "Τρελό Παιχνίδι Για 8 Άτομα!\n\nΑνατινάξτε τους φίλους σας (ή τον υπολογιστή) σε ένα τουρνουά από εκρηκτικά mini-games όπως Κατακτηση-Σημαίας, Βόμβο-Χόκεϋ, και Επική-Μάχη-Μέχρι-Θανάτου-Σε-Αργή-Κίνηση!\n\nΟι απλοί χειρισμοί και η εκτενής υποστήριξη χειριστηρίων το κάνουν εύκολο μέχρι και για 8 άτομα να πάρουν μέρος στην δράση! Είναι δυνατή και η χρήση φορητών συσκευών ως χειριστήρια μέσω της εφαρμογής 'Χειριστήριο BombSquad'!\n\nΒόμβες! Φύγαμε!\n\nΒλέπε www.froemling.net/bombsquad για περισσότερες πληροφορίες.", + "storeDescriptions": { + "blowUpYourFriendsText": "Ανατινάξτε τους φίλους σας.", + "competeInMiniGamesText": "Ανταγωνιστείτε σε mini-games από αγώνες ταχύτητας έως και πέταγμα.", + "customize2Text": "Προσαρμόστε χαρακτήρες, mini-games, ακόμα και τη μουσική.", + "customizeText": "Προσαρμόστε χαρακτήρες και δημιουργήστε τις δικές σας λίστες mini-games.", + "sportsMoreFunText": "Τα αγωνίσματα είναι πιο διασκεδαστικά με εκρήξεις.", + "teamUpAgainstComputerText": "Συμμαχήστε εναντίον του υπολογιστή." + }, + "storeText": "Κατάστημα", + "submitText": "Υποβολή", + "submittingPromoCodeText": "Υποβολή Κωδικού...", + "teamNamesColorText": "Ονόματα/Χρώμματα Ομάδων...", + "telnetAccessGrantedText": "Πρόσβαση telnet ενεργοποιημένη.", + "telnetAccessText": "Πρόσβαση telnet εντοπίστηκε. Να επιτρέπεται;", + "testBuildErrorText": "Αυτή η δοκιμαστική έκδοση είναι πλέον ανενεργή. Παρακαλώ ελέγξτε για μια νεότερη έκδοση.", + "testBuildText": "Δοκιμαστική Έκδοση", + "testBuildValidateErrorText": "Αδύνατη η επικύρωση της δοκιμαστικής έκδοσης. (χωρίς πρόσβαση στο δίκτυο;)", + "testBuildValidatedText": "Η Δοκιμαστική Έκδοση Επικυρώθηκε. Απολαύστε!", + "thankYouText": "Ευχαριστώ για την υποστήριξή σας! Απολαύστε το παιχνίδι!!", + "threeKillText": "ΤΡΙΠΛΟΣ ΦΟΝΟΣ!!", + "timeBonusText": "Μπόνους Χρόνου", + "timeElapsedText": "Διάρκεια Χρόνου", + "timeExpiredText": "Ο Χρόνος Έληξε", + "timeSuffixDaysText": "${COUNT}μερ.", + "timeSuffixHoursText": "${COUNT}ωρ.", + "timeSuffixMinutesText": "${COUNT}λεπ.", + "timeSuffixSecondsText": "${COUNT}δευτ.", + "tipText": "Υπόδειξη", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Κορυφαίοι Φίλοι", + "tournamentCheckingStateText": "Έλεγχος κατάστασης τουρνουά. Παρακαλώ περιμένετε...", + "tournamentEndedText": "Αυτό το τουρνουά έχει λήξει. Ένα νέο θα ξεκινήσει σύντομα.", + "tournamentEntryText": "Είσοδος Τουρνουά", + "tournamentResultsRecentText": "Αποτελέσματα Πρόσφατων Τουρνουά", + "tournamentStandingsText": "Πίνακας Κατάταξης Τουρνουά", + "tournamentText": "Τουρνουά", + "tournamentTimeExpiredText": "Ο Χρόνος του Τουρνουά Έληξε", + "tournamentsText": "Τουρνουά", + "translations": { + "characterNames": { + "Agent Johnson": "Πράκτορας Τζόνσον", + "B-9000": "Β-9000", + "Bernard": "Βερνάρδος", + "Bones": "Κοκαλάκιας", + "Butch": "Καουμπόι", + "Easter Bunny": "Πασχαλινός Λαγός", + "Flopsy": "Φλόπσυ", + "Frosty": "Φρόστυ", + "Gretel": "Γκρέτελ", + "Grumbledorf": "Γογγυδώρος", + "Jack Morgan": "Τζακ Μαυρογένης", + "Kronk": "Κρόνκ", + "Lee": "Λι", + "Lucky": "Τυχεράκιας", + "Mel": "Μελένιος", + "Middle-Man": "Σούπερ-Άσχετος", + "Minimus": "Μίνιμος", + "Pascal": "Πασκάλ", + "Pixel": "Πίξελ", + "Sammy Slam": "Σάμμυ Χαλκομανίας", + "Santa Claus": "Άγιος Βασίλης", + "Snake Shadow": "Σκιά Φιδιού", + "Spaz": "Σπάζ", + "Taobao Mascot": "Taobao Μασκότ", + "Todd McBurton": "Τοντ ΜακΜπάρτον", + "Zoe": "Ζωή", + "Zola": "Ζώλα" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Προπόνηση", + "Infinite ${GAME}": "${GAME} Ατελείωτο", + "Infinite Onslaught": "Επίθεση Ατελείωτο", + "Infinite Runaround": "Περίγυρος Ατελείωτο", + "Onslaught Training": "Επίθεση Προπόνηση", + "Pro ${GAME}": "${GAME} Επαγγελματικό", + "Pro Football": "Ποδόσφαιρο Επαγγελματικό", + "Pro Onslaught": "Επίθεση Επαγγελματικό", + "Pro Runaround": "Περίγυρος Επαγγελματικό", + "Rookie ${GAME}": "${GAME} Για Αρχάριους", + "Rookie Football": "Ποδόσφαιρο Για Αρχάριους", + "Rookie Onslaught": "Επίθεση Για Αρχάριους", + "The Last Stand": "Το Τελευταίο Ανάστημα", + "Uber ${GAME}": "${GAME} Ακραίο", + "Uber Football": "Ποδόσφαιρο Ακραίο", + "Uber Onslaught": "Επίθεση Ακραίο", + "Uber Runaround": "Περίγυρος Ακραίο" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Παραμείνετε εκλεκτός για ένα χρονικό διάστημα ώστε να νικήσετε.\nΣκοτώστε τον εκλεκτό για να πάρετε τη θέση του.", + "Bomb as many targets as you can.": "Βομβαρδίστε όσους στόχους μπορείτε.", + "Carry the flag for ${ARG1} seconds.": "Κουβαλήστε τη σημαία για ${ARG1} δευτερόλεπτα.", + "Carry the flag for a set length of time.": "Κουβαλήστε τη σημαία για ένα ορισμένο χρονικό διάστημα.", + "Crush ${ARG1} of your enemies.": "Συντρίψτε ${ARG1} από τους εχθρούς σας.", + "Defeat all enemies.": "Νικήστε όλους τους εχθρούς.", + "Dodge the falling bombs.": "Αποφύγετε τις βόμβες που πέφτουν.", + "Final glorious epic slow motion battle to the death.": "Τελική, δοξασμένη μάχη μέχρι θανάτου σε επικά αργή κίνηση.", + "Gather eggs!": "Μαζέψτε αυγά!", + "Get the flag to the enemy end zone.": "Μεταφέρετε τη σημαία στο εχθρικό τέρμα.", + "How fast can you defeat the ninjas?": "Πόσο γρήγορα μπορείτε να νικήσετε τους νίντζα;", + "Kill a set number of enemies to win.": "Σκοτώστε έναν ορισμένο αριθμό εχθρών για να νικήσετε.", + "Last one standing wins.": "Ο τελευταίος που θα σταθεί όρθιος κερδίζει.", + "Last remaining alive wins.": "Ο τελευταίος ζωντανός κερδίζει.", + "Last team standing wins.": "Η ομάδα που θα σταθεί τελευταία κερδίζει.", + "Prevent enemies from reaching the exit.": "Αποτρέψτε τους εχθρούς από το να φτάσουν στην έξοδο.", + "Reach the enemy flag to score.": "Αγγίξτε την εχθρική σημαία για να σκοράρετε.", + "Return the enemy flag to score.": "Επιστρέψτε την εχθρική σημαία για να σκοράρετε.", + "Run ${ARG1} laps.": "Τρέξτε ${ARG1} γύρους.", + "Run ${ARG1} laps. Your entire team has to finish.": "Τρέξτε ${ARG1} γύρους. Ολόκληρη η ομάδα σας πρέπει να τερματίσει.", + "Run 1 lap.": "Τρέξτε 1 γύρο.", + "Run 1 lap. Your entire team has to finish.": "Τρέξτε 1 γύρο. Ολόκληρη η ομάδα σας πρέπει να τερματίσει.", + "Run real fast!": "Τρέξτε πολύ γρήγορα!", + "Score ${ARG1} goals.": "Σκοράρετε ${ARG1} γκολ.", + "Score ${ARG1} touchdowns.": "Σκοράρετε ${ARG1} πόντους.", + "Score a goal.": "Σκοράρετε 1 γκολ.", + "Score a touchdown.": "Σκοράρετε 1 πόντο.", + "Score some goals.": "Σκοράρετε μερικά γκολ.", + "Secure all ${ARG1} flags.": "Προστατεύστε όλες τις ${ARG1} σημαίες.", + "Secure all flags on the map to win.": "Προστατεύστε όλες τις σημαίες στον χάρτη για να κερδίσετε.", + "Secure the flag for ${ARG1} seconds.": "Προστατεύστε τη σημαία για ${ARG1} δευτερόλεπτα.", + "Secure the flag for a set length of time.": "Προστατεύστε τη σημαία για ένα ορισμένο χρονικό διάστημα.", + "Steal the enemy flag ${ARG1} times.": "Κλέψτε την εχθρική σημαία ${ARG1} φορές.", + "Steal the enemy flag.": "Κλέψτε την εχθρική σημαία.", + "There can be only one.": "Μπορεί να υπάρξει μόνο ένας.", + "Touch the enemy flag ${ARG1} times.": "Αγγίξτε την εχθρική σημαία ${ARG1} φορές.", + "Touch the enemy flag.": "Αγγίξτε την εχθρική σημαία.", + "carry the flag for ${ARG1} seconds": "κουβαλήστε τη σημαία για ${ARG1} δευτερόλεπτα", + "kill ${ARG1} enemies": "σκοτώστε ${ARG1} εχθρούς", + "last one standing wins": "ο τελευταίος που θα σταθεί όρθιος κερδίζει", + "last team standing wins": "η ομάδα που θα σταθεί τελευταία κερδίζει", + "return ${ARG1} flags": "επιστρέψτε ${ARG1} σημαίες", + "return 1 flag": "επιστρέψτε 1 σημαία", + "run ${ARG1} laps": "τρέξτε ${ARG1} γύρους", + "run 1 lap": "τρέξτε 1 γύρο", + "score ${ARG1} goals": "σκοράρετε ${ARG1} γκολ", + "score ${ARG1} touchdowns": "σκοράρετε ${ARG1} πόντους", + "score a goal": "σκοράρετε 1 γκολ", + "score a touchdown": "σκοράρετε 1 πόντο", + "secure all ${ARG1} flags": "προστατεύστε όλες τις ${ARG1} σημαίες", + "secure the flag for ${ARG1} seconds": "προστατεύστε τη σημαία για ${ARG1} δευτερόλεπτα", + "touch ${ARG1} flags": "αγγίξτε ${ARG1} σημαίες", + "touch 1 flag": "αγγίξτε 1 σημαία" + }, + "gameNames": { + "Assault": "Έφοδος", + "Capture the Flag": "Κατάκτηση Σημαίας", + "Chosen One": "Εκλεκτός", + "Conquest": "Κυριαρχία", + "Death Match": "Μάχη Θανάτου", + "Easter Egg Hunt": "Κυνήγι Πασχαλινών Αυγών", + "Elimination": "Εξόντωση", + "Football": "Ποδόσφαιρο", + "Hockey": "Χόκεϋ", + "Keep Away": "Απομάκρυνση", + "King of the Hill": "Βασιλιάς του Λόφου", + "Meteor Shower": "Βροχή Μετεωριτών", + "Ninja Fight": "Μάχη Νίντζα", + "Onslaught": "Επίθεση", + "Race": "Αγώνας Ταχύτητας", + "Runaround": "Περίγυρος", + "Target Practice": "Εξάσκηση Στόχου", + "The Last Stand": "Το Τελευταίο Ανάστημα" + }, + "inputDeviceNames": { + "Keyboard": "Πληκτρολόγιο", + "Keyboard P2": "Πληκτρολόγιο Π2" + }, + "languages": { + "Arabic": "Αραβικά", + "Belarussian": "Λευκορώσικα", + "Chinese": "Απλοποιημένα κινέζικα", + "ChineseTraditional": "Παραδοσιακά κινέζικα", + "Croatian": "Κροάτικα", + "Czech": "Τσέχικα", + "Danish": "Δανικά", + "Dutch": "Ολλανδικά", + "English": "Αγγλικά", + "Esperanto": "Εσπαράντο", + "Finnish": "Φινλανδικά", + "French": "Γαλλικά", + "German": "Γερμανικά", + "Gibberish": "Ασυνάρτητα", + "Greek": "Ελληνικά", + "Hindi": "Ινδικά", + "Hungarian": "Ουγγρικά", + "Indonesian": "Ινδονησιακά", + "Italian": "Ιταλικά", + "Japanese": "Ιαπωνέζικα", + "Korean": "Κορεάτικα", + "Persian": "Πέρσικα", + "Polish": "Πολωνικά", + "Portuguese": "Πορτογαλικά", + "Romanian": "Ρουμάνικα", + "Russian": "Ρώσικα", + "Serbian": "Σέρβικα", + "Slovak": "Σλοβακικά", + "Spanish": "Ισπανικά", + "Swedish": "Σουηδικά", + "Turkish": "Τούρκικα", + "Ukrainian": "Ουκρανικά", + "Venetian": "Ενετικά", + "Vietnamese": "Βιετναμέζικα" + }, + "leagueNames": { + "Bronze": "Χάλκινο", + "Diamond": "Διαμαντένιο", + "Gold": "Χρυσό", + "Silver": "Ασημένιο" + }, + "mapsNames": { + "Big G": "Μεγάλο G", + "Bridgit": "Ψηφιογέφυρα", + "Courtyard": "Αυλή", + "Crag Castle": "Βραχώδες Κάστρο", + "Doom Shroom": "Μανιτάρι του Χαμού", + "Football Stadium": "Στάδιο Ποδοσφαίρου", + "Happy Thoughts": "Χαρούμενες Σκέψεις", + "Hockey Stadium": "Στάδιο Χόκεϋ", + "Lake Frigid": "Καταψυγμένη Λίμνη", + "Monkey Face": "Φάτσα Μαϊμούς", + "Rampage": "Ράμπα", + "Roundabout": "Κυκλική Διασταύρωση", + "Step Right Up": "Σκαλοπάτια", + "The Pad": "Η Ταμπλέτα", + "Tip Top": "Κορυφή Υπόδειξης", + "Tower D": "Αμυντικός Πύργος", + "Zigzag": "ΖιγκΖαγκ" + }, + "playlistNames": { + "Just Epic": "Μόνο Επικό", + "Just Sports": "Μόνο Αγωνίσματα" + }, + "scoreNames": { + "Flags": "Σημαίες", + "Goals": "Γκολ", + "Score": "Βαθμοί", + "Survived": "Επιβίωση", + "Time": "Χρόνος", + "Time Held": "Χρόνος Κατοχής" + }, + "serverResponses": { + "A code has already been used on this account.": "Ένας κωδικός έχει ήδη χρησιμοποιηθεί σε αυτόν το λογαριασμό.", + "A reward has already been given for that address.": "Μια ανταμοιβή έχει ήδη δοθεί γι' αυτή την διεύθυνση.", + "Account linking successful!": "Δέσμευση Λογαριασμών Επιτυχής!", + "Account unlinking successful!": "Αποδέσμευση Λογαριασμών Επιτυχής!", + "Accounts are already linked.": "Οι λογαριασμοί είναι ήδη δεσμευμένοι.", + "An error has occurred; (${ERROR})": "Προέκυψε Σφάλμα. (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Προέκυψε Σφάλμα. Παρακαλώ επικοινωνήστε με την υποστήριξη. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Προέκυψε Σφάλμα. Παρακαλώ επικοινωνήστε με το support@froemling.net.", + "An error has occurred; please try again later.": "Προέκυψε Σφάλμα. Παρακαλώ ξαναπροσπαθήστε αργότερα.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Είστε σίγουροι πως θέλετε να δεσμεύσετε μεταξύ τους αυτούς τους λογαριασμούς;\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nΗ ενέργεια είναι μη αντιστρέψιμη!", + "BombSquad Pro unlocked!": "Το BombSquad Pro ξεκλειδώθηκε!", + "Can't link 2 accounts of this type.": "Αδύνατη η δέσμευση 2 λογαριασμών αυτού του τύπου.", + "Can't link 2 diamond league accounts.": "Αδύνατη η δέσμευση 2 λογαριασμών διαμαντένιας κατηγορίας.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Αδύνατη δέσμευση. Θα ξεπερνούσε το όριο των ${COUNT} δεσμευμένων λογαριασμών.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Εντοπίστηκε ζαβολιά. Οι βαθμολογίες και τα βραβεία θα ανασταλλούν για ${COUNT} μέρες.", + "Could not establish a secure connection.": "Δεν μπόρεσε να ιδρυθεί ασφαλής σύνδεση.", + "Daily maximum reached.": "Ημερήσιο μέγιστο επετεύχθη.", + "Entering tournament...": "Είσοδος στο τουρνουά...", + "Invalid code.": "Μη έγκυρος κωδικός.", + "Invalid payment; purchase canceled.": "Μη έγκυρη πληρωμή. Η αγορά ακυρώθηκε.", + "Invalid promo code.": "Μη έγκυρος κωδικός.", + "Invalid purchase.": "Μη έγκυρη αγορά.", + "Invalid tournament entry; score will be ignored.": "Μη έγκυρη είσοδος στο τουρνουά. Η βαθμολογία θα αγνοηθεί.", + "Item unlocked!": "Το αντικείμενο ξεκλειδώθηκε!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "ΑΡΝΗΣΗ ΔΕΣΜΕΥΣΗΣ. Ο ${ACCOUNT} περιέχει\nσημαντικά δεδομένα τα οποία θα ΧΑΝΟΝΤΑΝ.\nΜπορείτε να δεσμεύσετε με την αντίστροφη σειρά αν θα θέλατε\n(έτσι θα χάσετε τα δεδομένα ΑΥΤΟΥ του λογαριασμού)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Δέσμευση του λογαριασμού ${ACCOUNT} σε αυτόν τον λογαριασμό;\nΌλα τα δεδομένα που υπάρχουν στον ${ACCOUNT} θα χαθούν.\nΕνέργεια μη αντιστρέψιμη. Είστε σίγουροι;", + "Max number of playlists reached.": "Μέγιστος αριθμός λιστών επετεύχθη.", + "Max number of profiles reached.": "Μέγιστος αριθμός προφίλ επετεύχθη.", + "Maximum friend code rewards reached.": "Μέγιστος αριθμός ανταμοιβών κωδικού φίλων επετεύχθη.", + "Message is too long.": "Το μήνυμα είναι πολύ μεγάλο.", + "Profile \"${NAME}\" upgraded successfully.": "Το Προφίλ \"${NAME}\" αναβαθμίστηκε επιτυχώς.", + "Profile could not be upgraded.": "Το προφίλ δεν μπόρεσε να αναβαθμιστεί.", + "Purchase successful!": "Επιτυχής αγορά!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Παραλάβατε ${COUNT} εισιτήρια επειδή συνδεθήκατε.\nΕλάτε πάλι αύριο για να παραλάβετε ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Η λειτουργικότητα των διακομιστών δεν υποστηρίζεται πλέον σε αυτή την έκδοση του παιχνιδιού.\nΠαρακαλώ ενημερώστε σε μια νεότερη έκδοση.", + "Sorry, there are no uses remaining on this code.": "Συγνώμμη, δεν απομένουν άλλες χρήσεις αυτού του κωδικού.", + "Sorry, this code has already been used.": "Συγνώμη, αυτός ο κωδικός έχει ήδη χρησιμοποιηθεί.", + "Sorry, this code has expired.": "Συγνώμη, αυτός ο κωδικός έχει λήξει.", + "Sorry, this code only works for new accounts.": "Συγνώμη, αυτός ο κωδικός λειτουργεί μονάχα για νέους λογαριασμούς.", + "Temporarily unavailable; please try again later.": "Προς το παρόν μη διαθέσιμο. Παρακαλώ ξαναπροσπαθήστε αργότερα.", + "The tournament ended before you finished.": "Το τουρνουά έληξε πριν τερματίσετε.", + "This account cannot be unlinked for ${NUM} days.": "Αυτός ο λογαριασμός δεν μπορεί να αποδεσμευτεί για ${NUM} μέρες.", + "This code cannot be used on the account that created it.": "Αυτός ο κωδικός δεν μπορεί να χρησιμοποιηθεί από τον λογαριασμό που τον δημιούργησε.", + "This requires version ${VERSION} or newer.": "Αυτό απαιτεί έκδοση ${VERSION} ή νεότερη.", + "Tournaments disabled due to rooted device.": "Τα τουρνουά απενεργοποιήθηκαν λόγω rooted συσκευής.", + "Tournaments require ${VERSION} or newer": "Τα τουρνουά απαιτούν έκδοση ${VERSION} ή νεότερη", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Αποδέσμευση του ${ACCOUNT} από αυτόν τον λογαριασμό;\nΌλα τα δεδομένα στον ${ACCOUNT} θα υποστούν επαναφορά.\n(εκτός από τα επιτεύγματα σε ορισμένες περιπτώσεις)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Παράπονα παραβίασης (hacking) σημειώθηκαν εναντίον αυτού του λογαριασμού.\nΛογαριασμοί που παραβιάζουν το παιχνίδι θα αποκλείονται. Παρακαλώ παίξτε δίκαια.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Θα θέλατε να δεσμεύσετε τον λογαριασμό της συσκεύης σας με αυτόν εδώ;\n\nΟ λογαριασμός της συσκευής σας είναι ο ${ACCOUNT1}\nΑυτός ο λογαριασμός είναι ο ${ACCOUNT2}\n\nΑυτό θα σας επιτρέψει να κρατήσετε την υπάρχουσα πρόοδό σας.\nΠροσοχή: Ενέργεια μη αντιστρέψιμη!", + "You already own this!": "Αυτό σας ανήκει ήδη!", + "You can join in ${COUNT} seconds.": "Μπορείτε να ενταχθείτε σε ${COUNT} δευτ.", + "You don't have enough tickets for this!": "Δεν έχετε αρκετά εισιτήρια για να αγοράσετε αυτό το αντικείμενο!", + "You don't own that.": "Αυτό το αντικείμενο δεν σας ανήκει.", + "You got ${COUNT} tickets!": "Παραλάβατε ${COUNT} εισιτήρια!", + "You got a ${ITEM}!": "Παραλάβατε ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Έχετε προαχθεί σε μια νέα κατηγορία. Συγχαρητήρια!", + "You must update to a newer version of the app to do this.": "Πρέπει να ενημερώσετε σε μια νεότερη έκδοση της εφαρμογής γι' αυτή την ενέργεια.", + "You must update to the newest version of the game to do this.": "Πρέπει να ενημερώσετε στη νεότερη έκδοση του παιχνιδιού γι' αυτή την ενέργεια.", + "You must wait a few seconds before entering a new code.": "Πρέπει να περιμένετε μερικά δευτερόλεπτα πριν εισάγετε έναν νέο κωδικό.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Καταταχθήκατε #${RANK} στο τελευταίο τουρνουά. Ευχαριστώ για τη συμμετοχή!", + "Your account was rejected. Are you signed in?": "Ο λογαριασμός σας απορρίφθηκε. Έχετε συνδεθεί;", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Το αντίγραφο του παιχνιδιού σας έχει τροποποιηθεί.\nΠαρακαλώ κάντε επαναφορά όλων των αλλαγών και ξαναπροσπαθήστε.", + "Your friend code was used by ${ACCOUNT}": "Ο κωδικός φίλων σας χρησιμοποιήθηκε από τον λογαριασμό ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Λεπτό", + "1 Second": "1 Δευτερόλεπτο", + "10 Minutes": "10 Λεπτά", + "2 Minutes": "2 Λεπτά", + "2 Seconds": "2 Δευτερόλεπτα", + "20 Minutes": "20 Λεπτά", + "4 Seconds": "4 Δευτερόλεπτα", + "5 Minutes": "5 Λεπτά", + "8 Seconds": "8 Δευτερόλεπτα", + "Allow Negative Scores": "Να Επιτρέπονται Αρνητικές Βαθμολογίες", + "Balance Total Lives": "Να Εξισορροπούνται οι Συνολικές Ζωές", + "Bomb Spawning": "Δημιουργία Βομβών", + "Chosen One Gets Gloves": "Ο Εκλεκτός Παίρνει Γάντια", + "Chosen One Gets Shield": "Ο Εκλεκτός Παίρνει Ασπίδα", + "Chosen One Time": "Χρόνος Εκλεκτού", + "Enable Impact Bombs": "Ενεργοποίηση Βομβών Ερεθίσματος", + "Enable Triple Bombs": "Ενεργοποίηση Τριπλών Βομβών", + "Entire Team Must Finish": "Ολόκληρη Η Ομάδα Πρέπει να Τερματίσει", + "Epic Mode": "Επική Λειτουργία", + "Flag Idle Return Time": "Χρόνος Επιστροφής Αφημένης Σημαίας", + "Flag Touch Return Time": "Χρόνος Εξ'επαφούς Επιστροφής Σημαίας", + "Hold Time": "Χρόνος Κατοχής", + "Kills to Win Per Player": "Φόνοι Ανά Παίκτη για τη Νίκη", + "Laps": "Γύροι", + "Lives Per Player": "Ζωές Ανά Παίκτη", + "Long": "Διαρκές", + "Longer": "Εκτεταμένο", + "Mine Spawning": "Δημιουργία Ναρκών", + "No Mines": "Χωρίς Νάρκες", + "None": "Καθόλου", + "Normal": "Κανονικό", + "Pro Mode": "Επαγγελματική Λειτουργία", + "Respawn Times": "Χρόνος Επαναδημιουργίας", + "Score to Win": "Βαθμολογία για τη Νίκη", + "Short": "Βραχύ", + "Shorter": "Σύντομο", + "Solo Mode": "Λειτουργία Σόλο", + "Target Count": "Αριθμός Στόχων", + "Time Limit": "Χρονικό Όριο" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "Η ομάδα ${TEAM} αποκλείστηκε επειδή ο παίκτης ${PLAYER} εγκατέλλειψε", + "Killing ${NAME} for skipping part of the track!": "Θάνατος στον παίκτη ${NAME} επειδή παρέλειψε ένα μέρος της πίστας!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Προειδοποίηση στον παίκτη ${NAME}: το turbo / κατάχρηση κουμπιού σας τιμωρεί." + }, + "teamNames": { + "Bad Guys": "Κακοί Τύποι", + "Blue": "Μπλε", + "Good Guys": "Καλοί Τύποι", + "Red": "Κόκκινο" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Μια τέλεια συγχρονισμένη γροθιά τρεξίματος-άλματος-στροφής μπορεί να σκοτώσει\nμε ένα χτύπημα και να σας διασφαλίσει σεβασμό ευ' φόρου ζωής από τους φίλους σας.", + "Always remember to floss.": "Πάντα να θυμάστε το οδοντικό νήμα.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Δημιουργήστε προφίλ παικτών για εσάς και τους φίλους σας με τα\nονόματα και τις εμφανίσεις που προτιμάτε αντί να χρησιμοποιείτε τυχαία.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Τα κουτιά κατάρας σας μετατρέπουν σε ορολογιακή βόμβα.\nΗ μόνη θεραπεία είναι η άμεση χρήση ιατρικού.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Παρά την εμφάνισή τους, οι ιδιότητες όλων των χαρακτήρων είναι ίδιες,\nοπότε επιλέξτε αυτόν που σας αντιπροσοπεύει καλύτερα.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Μην παίρνετε πολύ θάρρος με το πεδίο ισχύος. Μπορείτε ακόμα να πέσετε από γκρεμό.", + "Don't run all the time. Really. You will fall off cliffs.": "Μην τρέχετε όλη την ώρα. Σοβαρά. Θα πέσετε από κανά γκρεμό.", + "Don't spin for too long; you'll become dizzy and fall.": "Μην περιστρέφεστε για πολλή ώρα. Θα ζαλιστείτε και θα πέσετε.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Κρατήστε πατημένο οποιοδήποτε κουμπί για να τρέξετε. (Το κουμπιά διακόπτες λειτουργούν πολύ καλά αν τα διαθέτετε)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Πατήστε κρατημένο οποιδήποτε κουμπί για να τρέξετε. Θα φτάνετε στον προορισμό σας\nταχύτερα αλλά θα είναι δύσκολο να στρίψετε, γι' αυτό λοιπόν προσοχή στους γκρεμούς.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Οι βόμβες πάγου δεν είναι και τόσο ισχυρές, αλλά παγώνουν\nοποιονδήποτε πετυχαίνουν, αφήνοντάς τον ευάλωτο στο θραύσιμο.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Αν κάποιος σας σηκώσει, χτυπήστε τον και θα σας\nαφήσει.Αυτό λειτουργεί και στην πραγματικότητα.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Αν ξεμείνατε από χειριστήρια, εγκαταστήστε την εφαρμογή '${REMOTE_APP_NAME}'\nστις φορητές σας συσκευές για να τις χρησιμοποιήσετε ως χειριστήρια.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Αν μια κολλώδη βόμβα κολλήσει πάνω σας, αρχίστε να πηδάτε και να τρέχετε σε κύκλους.\nΕνδέχεται να την ξεκολλήσετε, αλλιώς οι τελευταίες σας στιγμές θα είναι ψυχαγωγικές.", + "If you kill an enemy in one hit you get double points for it.": "Άμα σκοτώσετε έναν εχθρό με ένα χτύπημα κερδίζετε διπλάσιους πόντους.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Άμα μαζέψετε κατάρα, η μόνη ελπίδα σας για επιβίωση είναι\nνα βρείτε ιατρικό κουτί στα ακόλουθα δευτερόλεπτα.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Αν στέκεστε σε ένα σημείο, θα γίνετε ψητός. Τρέξτε και αποφύγετε για να επιβιώσετε..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Αν έχετε πολλούς παίκτες να εισέρχονται και να εξέρχονται, ενεργοποιήστε την 'αυτόματη-\nαποβολή-άπραγων-παικτών' στις ρυθμίσεις σε περίπτωση που κάποιος ξεχάσει να αποχωρήσει", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Αν η συσκευή σας ζεσταίνεται υπερβολικά ή θα θέλατε να διατηρήσετε την ενέργεια\nτης μπαταρίας, μειώστε τα \"Οπτικά\" ή την \"Ανάλυση\" στις Ρυθμίσεις->Γραφικά", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Αν το framerate σας είναι ασταθές, δοκιμάστε να μειώσετε την\nανάλυση ή τα οπτικά στις ρυθμίσεις γραφικών του παιχνιδιού.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "Στην Κατάκτηση-Σημαίας, η δική σας σημαία πρέπει να είναι στη βάση της για να σκοράρετε. Αν η άλλη\nομάδα πάει να σκοράρει, κλέβοντας την σημαία της είναι ένας αποτελεσματικός τρόπος για να τη σταματήσετε.", + "In hockey, you'll maintain more speed if you turn gradually.": "Στο χόκεϋ, θα διατηρήσετε περισσότερη ταχύτητα αν στρίβετε σταδιακά.", + "It's easier to win with a friend or two helping.": "Είναι ευκολότερο να νικήσετε με τη βοήθεια κάνα-δυο φίλων.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Πηδήξτε για να πετάξετε βόμβες ψηλότερα.", + "Land-mines are a good way to stop speedy enemies.": "Οι νάρκες εδάφους είναι ένας αποτελεσματικός τρόπος για να σταματήσετε γρήγορους εχθρούς.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Μπορείτε να σηκώσετε και να πετάξετε διάφορα, συμπεριλαμβανομένου και άλλους παίκτες. Ρίχνοντας τους εχθρούς σας\nαπό το χείλος του γκρεμού μπορεί να είναι μια αποτελεσματική και συγχρόνως συναισθηματικά φορτισμένη στρατηγική.", + "No, you can't get up on the ledge. You have to throw bombs.": "Όχι, δεν μπορείτε να ανεβείτε από το περβάζι. Πρέπει να ρίξετε βόμβες.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Οι παίκτες μπορούν να ενταχθούν και να αποχωρήσουν στην μέση των περισσότερων παιχνιδιών,\nκαι μπορείτε ακόμα να συνδέσετε και να αποσυνδέσετε χειριστήρια καθ' όλη τη διάρκειά τους.", + "Practice using your momentum to throw bombs more accurately.": "Εξασκηθείτε χρησιμοποιώντας την ορμή σας για να ρίξετε βόμβες με περισσότερη ακρίβεια.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Οι γροθιές προκαλούν περισσότερη ζημιά με βάση την ταχύτητα με τη\nοποία κινούνται, γι' αυτό τρέξτε, πηδήξτε και στρίψτε σαν τρελός.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Κινηθείτε μπρος και πίσω πριν ρίξετε μια\nβόμβα για να την εκσφενδονήσετε μακρύτερα.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Εξοντώστε πολλαπλούς εχθρούς\nρίχνοντας βόμβα δίπλα σε κουτί TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Το κεφάλι είναι η πιο ευαίσθητη περιοχή, οπότε, μια\nκολλώδη βόμβα πάνω του συνήθως σημαίνει τέλος-παιχνιδιού.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Αυτό το επίπεδο δεν τελειώνει ποτέ, αλλά μία υψηλή βαθμολογία\nεδώ θα σας αποκαταστήσει την υστεροφημία σας παγκοσμίως.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Η δύναμη ριπής είναι βασισμένη στην κατεύθυνση που έχετε κρατημένη.\nΓια να ρίξετε κάτι απαλά μπροστά σας, μην στοχεύετε κατεύθυνση.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Βαρεθήκατε την ηχητική υπόκρουση; Αντικαταστήστε την με\nμια δικιά σας! Βλέπε Ρυθμίσεις->Ήχος->Ηχητική Υπόκρουση", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Δοκιμάστε να \"Μαγειρέψετε\" τις βόμβες για κάνα-δυο δευτερόλεπτα πριν τις πετάξετε.", + "Try tricking enemies into killing eachother or running off cliffs.": "Δοκιμάστε να ξεγελάσετε τους εχθρούς κάνοντάς τους να αλληλοσκοτωθούν ή να πέσουν από τον γκρεμό.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Χρησιμοποιήστε το κουμπί αρπαγής για να σηκώσετε τη σημαία < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Κινηθείτε μπρος και πίσω για να κερδίσετε απόσταση στις ρίψεις σας..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Μπορείτε να \"στοχεύσετε\" τις γροθιές σας στρίβοντας αριστερά ή δεξιά. Αυτό είναι\nχρήσιμο για να πετάτε τους κακούς από τις άκρες ή να σκοράρετε στο χόκεϋ.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Μπορείτε να μαντέψετε πότε μια βόμβα θα εκραγεί με βάση το χρώμα\nτων σπινθήρων στο φυτίλι: κίτρινο..πορτοκαλί..κόκκινο..ΜΠΟΥΜ.", + "You can throw bombs higher if you jump just before throwing.": "Μπορείτε να ρίξετε τις βόμβες ψηλότερα αν πηδήξετε ακριβώς πριν τη ρίψη.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Δέχεστε ζημιές όταν κουτουλάτε το κεφάλι σας οπουδήποτε, οπότε\nπροσπαθήστε να μην κουτουλάτε το κεφάλι σας οπουδήποτε.", + "Your punches do much more damage if you are running or spinning.": "Οι γροθιές σας προκαλούν περισσότερη ζημιά όταν τρέχετε ή περιστρέφεστε." + } + }, + "trophiesRequiredText": "Αυτό απαιτεί τουλάχιστον ${NUMBER} βραβεία.", + "trophiesText": "Βραβεία", + "trophiesThisSeasonText": "Βραβεία Αυτής Της Σεζόν", + "tutorial": { + "cpuBenchmarkText": "Εκτέλεση του εκπαιδευτικού βίντεο σε γελοίες ταχύτητες (έλεγχος κυρίως της ταχύτητας του CPU)", + "phrase01Text": "Γειά σας!", + "phrase02Text": "Καλωσήρθατε στο ${APP_NAME}!", + "phrase03Text": "Ορίστε μερικές συμβουλές για τον χειρισμό του χαρακτήρα σας:", + "phrase04Text": "Πολλά πράγματα στο ${APP_NAME} είναι βασισμένα στους ΝΟΜΟΥΣ της ΦΥΣΙΚΗΣ.", + "phrase05Text": "Για παράδειγμα, όταν βαράτε,..", + "phrase06Text": "..η ζημιά είναι βασισμένη στην ορμή των γροθιών σας.", + "phrase07Text": "Είδατε; Είμασταν ακίνητοι, οπότε αυτό ίσα που πλήγωσε τον ${NAME}.", + "phrase08Text": "Τώρα ας πηδήξουμε και ας στρίψουμε για να δώσουμε περισσότερη ταχύτητα.", + "phrase09Text": "Αχ, καλύτερα έτσι.", + "phrase10Text": "Το τρέξιμο επίσης βοηθάει.", + "phrase11Text": "Κρατήστε πατημένο ΟΠΟΙΟΔΗΠΟΤΕ κουμπί για να τρέξετε.", + "phrase12Text": "Για καλύτερες γροθιές, δοκιμάστε να τρέξετε ΚΑΙ να πηδήξετε.", + "phrase13Text": "Ουπς. Συγνώμη γι' αυτό ${NAME}.", + "phrase14Text": "Μπορείτε να σηκώσετε διάφορα, όπως σημαίες.. ή τον ${NAME}.", + "phrase15Text": "Τέλος, έχουμε τις βόμβες.", + "phrase16Text": "Η σωστή ρίψη βόμβας χρειάζεται εξάσκηση.", + "phrase17Text": "Άουτς! Όχι και πολύ καλή ρίψη.", + "phrase18Text": "Η κίνηση σας επιτρέπει να ρίχνετε μακρύτερα.", + "phrase19Text": "Το άλμα σας επιτρέπει να ρίχνετε ψηλότερα.", + "phrase20Text": "Τινάξτε τις βόμβες σας με ώθηση για ακόμα καλύτερες βολές.", + "phrase21Text": "Ο σωστός συγχρονισμός με την βόμβα μπορεί να είναι δύσκολος.", + "phrase22Text": "Συμφορά.", + "phrase23Text": "Δοκιμάστε να \"Μαγειρέψετε\" το φυτίλι για κάνα-δυο δευτερόλεπτα.", + "phrase24Text": "Ζήτω! Καλόμαγειρεμένο.", + "phrase25Text": "Λοιπόν, με αυτό ολοκληρώνουμε.", + "phrase26Text": "Όρμα τους, τίγρη!", + "phrase27Text": "Θυμηθείτε την εκπαίδευσή σας, και ΘΑ επιστρέψετε ζωντανοί!", + "phrase28Text": "...πιθανόν δηλαδή...", + "phrase29Text": "Καλή τύχη!", + "randomName1Text": "Φρεντ", + "randomName2Text": "Χάρρυ", + "randomName3Text": "Μπίλι", + "randomName4Text": "Τσακ", + "randomName5Text": "Φιλ", + "skipConfirmText": "Παράλειψη του εκπαιδευτικού βίντεο; Πατήστε ή πιέστε για επιβεβαίωση.", + "skipVoteCountText": "${COUNT}/${TOTAL} ψήφοι παράλειψης", + "skippingText": "παράλειψη εκπαιδευτικού βίντεο...", + "toSkipPressAnythingText": "(πατήστε ή πιέστε οτιδήποτε για να παραλείψετε το εκπαιδευτικό βίντεο)" + }, + "twoKillText": "ΔΙΠΛΟΣ ΦΟΝΟΣ!", + "unavailableText": "μη διαθέσιμο", + "unconfiguredControllerDetectedText": "Εντοπίστηκε μη διαμορφωμένο χειριστήριο:", + "unlockThisInTheStoreText": "Αυτό πρέπει να ξεκλειδωθεί στο κατάστημα.", + "unlockThisProfilesText": "Για να δημιουργήσετε περισσότερα από ${NUM} προφίλ, χρειάζεστε:", + "unlockThisText": "Για να το ξεκλειδώσετε, χρειάζεστε:", + "unsupportedHardwareText": "Συγνώμη, αυτό το υλισμικό δεν υποστηρίζεται από αυτή την έκδοση του παιχνιδιού.", + "upFirstText": "Για αρχή:", + "upNextText": "Στη συνέχεια, παιχνίδι ${COUNT}:", + "updatingAccountText": "Ενημέρωση του λογαριασμού σας...", + "upgradeText": "Αναβάθμιση", + "upgradeToPlayText": "Ξεκλειδώστε το \"${PRO}\" στο κατάστημα του παιχνιδιού για να μπορείτε να το παίξετε.", + "useDefaultText": "Χρήση Προκαθορισμένων", + "usesExternalControllerText": "Αυτό το παιχνίδι χρησιμοποιεί ένα εξωτερικό χειριστήριο για είσοδο.", + "usingItunesText": "Χρήση εφαρμογής μουσικής για ηχητική υπόκρουση...", + "validatingTestBuildText": "Επικύρωση Δοκιμαστικής Έκδοσης...", + "victoryText": "Νίκη!", + "voteDelayText": "Δεν μπορείτε να ξαναξεκινήσετε ψηφοφορία για ${NUMBER} δευτ.", + "voteInProgressText": "Μια ψηφοφορία βρίσκεται ήδη σε εξέλιξη.", + "votedAlreadyText": "Ψηφίσατε ήδη", + "votesNeededText": "Χρειάζονται ${NUMBER} ψήφοι", + "vsText": "εναντίον", + "waitingForHostText": "(αναμονή συνέχισης από τον χρήστη ${HOST})", + "waitingForPlayersText": "αναμονή για ένταξη παικτών...", + "waitingInLineText": "Αναμονή στην ουρά (η συγκέντρωση είναι πλήρης)...", + "watchAVideoText": "Παρακολουθήστε ένα Βίντεο", + "watchAnAdText": "Παρακολουθήστε μια Διαφήμιση", + "watchWindow": { + "deleteConfirmText": "Διαγραφή \"${REPLAY}\";", + "deleteReplayButtonText": "Διαγραφή\nΕπανάληψης", + "myReplaysText": "Οι Επαναλήψεις Μου", + "noReplaySelectedErrorText": "Δεν Επιλέχθηκε Επανάληψη", + "playbackSpeedText": "Ταχύτητα Αναπαραγωγής: ${SPEED}", + "renameReplayButtonText": "Μετονομασία\nΕπανάληψης", + "renameReplayText": "Μετονομασία \"${REPLAY}\" σε:", + "renameText": "Μετονομασία", + "replayDeleteErrorText": "Σφάλμα κατά τη διαγραφή της επανάληψης.", + "replayNameText": "Όνομα Επανάληψης", + "replayRenameErrorAlreadyExistsText": "Μια επανάληψη με το ίδιο όνομα υπάρχει ήδη.", + "replayRenameErrorInvalidName": "Αδύνατη η μετονομασία. Μη έγκυρο όνομα.", + "replayRenameErrorText": "Σφάλμα κατά την μετονομασία της επανάληψης.", + "sharedReplaysText": "Κοινόχρηστες Επαναλήψεις", + "titleText": "Προβολή", + "watchReplayButtonText": "Προβολή\nΕπανάληψης" + }, + "waveText": "Κύμα", + "wellSureText": "Βεβαίως!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Ακρόαση Για Wiimotes...", + "pressText": "Πιέστε τα κουμπιά 1 και 2 στο Wiimote ταυτοχρόνως.", + "pressText2": "Σε νεότερα Wiimotes με ενσωματωμένο Motion Plus, πατήστε αντίθετα το κόκκινο κουμπί 'sync' στο πίσω μέρος." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "listenText": "Ακρόαση", + "macInstructionsText": "Σιγουρευτείτε ότι το Wii είναι κλειστό και το Bluetooth ενεργοποιημένο\nστον Mac σας, τότε πατήστε 'Ακρόαση'. Η υποστήριξη Wiimote μπορεί\nνα είναι λίγο προβληματική, άρα μπορεί να χρειαστεί\nνα προσπαθήσετε μερικές φορές πριν να γίνει σύνδεση.\n\nΤο Bluetooth θα μπορεί να αντέξει έως 7 συνδεδεμένες συσκευές,\nαν και η απόσταση μπορεί να διαφέρει.\n\nΤο BombSquad υποστηρίζει τα αυθεντικά Wiimotes, Nunchuks\nκαι το κλασσικό χειριστήριο.\nΤο νεότερο Wii Remote Plus τώρα επίσης δουλεύει\nαλλά όχι με επιπρόσθετα εξαρτήματα.", + "thanksText": "Ευχαριστούμε την ομάδα DarwiinRemote\nπου έκανε αυτό εφικτό.", + "titleText": "Εγκατάσταση Wiimote" + }, + "winsPlayerText": "Ο Παίκτης ${NAME} Νίκησε!", + "winsTeamText": "Η Ομάδα ${NAME} Νίκησε!", + "winsText": "${NAME} Νίκησε!", + "worldScoresUnavailableText": "Παγκόσμιες βαθμολογίες μη διαθέσιμες.", + "worldsBestScoresText": "Καλύτερες Βαθμολογίες Παγκοσμίως", + "worldsBestTimesText": "Καλύτεροι Χρόνοι Παγκοσμίως", + "xbox360ControllersWindow": { + "getDriverText": "Αποκτήστε Οδηγό", + "macInstructions2Text": "Για να χρησιμοποιήσετε χειριστήρια ασύρματα, θα χρειαστείτε επίσης\nέναν δέκτη που έρχεται μαζί με το 'Xbox 360 Ασύρματο Χειριστήριο για\nWindows'. Ένας δέκτης σας επιτρέπει να συνδέσετε έως και 4 χειριστήρια.\n\nΣημαντικό: οι δέκτες 3ου-μέλους δεν θα λειτουργήσουν με αυτόν τον οδηγό.\nΣιγουρευτείτε πως ο δέκτης αναγράφει 'Microsoft' και όχι 'XBOX 360'.\nΗ Microsoft δεν τους πωλεί πλέον ξεχωριστά, οπότε θα χρειαστεί να λάβετε\nέναν ως πακέτο μαζί με το χειριστήριο ή αλλιώς να κάνετε αναζήτηση στο ebay.\n\nΑν αυτό το βρήκατε χρήσιμο, παρακαλώ σκεφτείτε να κάνετε\nδωρεά στον προγραμματιστή οδηγών σε αυτή τη σελίδα.", + "macInstructionsText": "Για να χρησιμοποιήσετε χειριστήρια Xbox 360, θα χρειαστεί\nνα εγκαταστήσετε τον οδηγό Mac διαθέσιμο στον παρακάτω σύνδεσμο.\nΛειτουργεί και με ενσύρματα και ασύρματα χειριστήρια.", + "macInstructionsTextScale": 0.7, + "ouyaInstructionsText": "Για να χρησιμοποιήσετε ενσύρματα Xbox 360 χειριστήρια με το BombSquad,\nαπλούστατα συνδέστε τα στη θύρα USB της συσκευής σας. Μπορείτε να\nχρησιμοποιήσετε έναν κόμβο USB για να συνδέσετε πολλαπλά χειριστήρια.\n\nΓια να χρησιμοποιήσετε ασύρματα χειριστήρια θα χρειαστείτε έναν ασύρματο\nδέκτη, διαθέσιμο ως μέρος του πακέτου \"Xbox 360 ασύρματο Χειριστήριο για\nWindows\" ή πωλούμενο ξεχωριστά. Κάθε δέκτης συνδέεται μέσα σε μια θύρα USB\nκαι σας επιτρέπει να συνδέσετε έως και 4 ασύρματα χειριστήρια.", + "titleText": "Χρήση Xbox 360 Χειριστηρίων με το ${APP_NAME}:" + }, + "yesAllowText": "Ναι, Να Επιτρέπεται!", + "yourBestScoresText": "Οι Καλύτερές σας Βαθμολογίες", + "yourBestTimesText": "Οι Καλύτεροί σας Χρόνοι" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/hindi.json b/dist/ba_data/data/languages/hindi.json new file mode 100644 index 0000000..d15be64 --- /dev/null +++ b/dist/ba_data/data/languages/hindi.json @@ -0,0 +1,1832 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "खाता नाम में इमोजी अथवा अन्य विशेष अक्षर नहीं हो सकते।", + "accountProfileText": "(खाते प्रोफाइल )", + "accountsText": "खातें", + "achievementProgressText": "उपलब्धियां: ${TOTAL} में से ${COUNT}", + "campaignProgressText": "अभियान प्रगति [कठिन]: ${PROGRESS}", + "changeOncePerSeason": "आप केवल प्रति सीजन इसे एक बार बदल सकते हैं।", + "changeOncePerSeasonError": "आपको इसे फिर से बदलने के लिए अगले सीज़न तक इंतजार करना होगा (${NUM} दिन)", + "customName": "अनुकूल नाम", + "linkAccountsEnterCodeText": "कोड डालीए", + "linkAccountsGenerateCodeText": "काेड उत्पन्न करे", + "linkAccountsInfoText": "(प्रगति को अलग अलग यंत्रो पर बाटिए)", + "linkAccountsInstructionsNewText": "दो खातों को लिंक करने के लिए, पहले खाते पर एक कोड उत्पन्न करें \nऔर उस कोड को दूसरे पर दर्ज करें. \nदूसरा खाता डेटा तब दोनों के बीच साझा किया जाएगा. \n(पहले खाते से डेटा खो जाएगा) \n\nआप ${COUNT} खाते तक लिंक कर सकते हैं \n\nमहत्वपूर्ण: केवल आपके खाते लिंक करें; \nयदि आप दोस्तों के खातों के साथ लिंक करते हैं \nतो आप एक ही समय में ऑनलाइन खेलने में सक्षम नहीं हो पाएंगे.", + "linkAccountsInstructionsText": "दो खातों को जोड़ने के लिए एक में कोड उत्पन्न\nकरें एयर दुसरे खाते में दर्ज करें | प्रगति और\nवस्तुसूची जोड़ दी जायेगी | आप अधिकतम\n${COUNT} खातों को जोड़ सकते हैं |\n\nध्यान रखिये, इसे वापस नहीं लिया जा सकता !", + "linkAccountsText": "खाते को जोडिए", + "linkedAccountsText": "जुड़े हुए खाते:", + "nameChangeConfirm": "अपने खाता का नाम ईस नाम ${NAME} से बदले?", + "resetProgressConfirmNoAchievementsText": "यह आपकी को-ऑप प्रगति और \nस्थानीय उच्च स्कोर रीसेट कर देगा (आपके टिकट रीसेट नहीं होंगे) | \nइसे पूर्ववत नहीं किया जा सकता | इस बात की पुष्टि करें कि आप यह करना चाहते हैं |", + "resetProgressConfirmText": "यह आपकी को-ऑप प्रगति और\n स्थानीय उच्च स्कोर रीसेट कर देगा\n (आपके टिकट रीसेट नहीं होंगे) | \nइसे पूर्ववत नहीं किया जा सकता | इस बात की पुष्टि करें कि आप यह करना चाहते हैं |", + "resetProgressText": "प्रगति रीसेट करें", + "setAccountName": "खाता नाम रखे", + "setAccountNameDesc": "अपने खाते के लिए प्रदर्शित करने के लिए नाम चुनें। \nआप अपने लिंक किए गए किसी एक से नाम का उपयोग कर सकते हैं \nखातों या अनन्य कस्टम नाम बनाएं", + "signInInfoText": "सभी यंत्रों पर अपनी प्रगति को संग्रहीत करने के लिए, \nटिकट कमाने, टूर्नामेंट में प्रतिस्पर्धा करने के लिए साइन इन करें |", + "signInText": "साइन इन", + "signInWithDeviceInfoText": "(एक स्वचालित खता जिसका सिर्फ इस यंत्र से प्रयोग किया जा सकता है)", + "signInWithDeviceText": "इस डिवाइस के साथ साइन इन करें", + "signInWithGameCircleText": "Game circle के साथ प्रवेश करे", + "signInWithGooglePlayText": "गूगल प्ले से साईन ईन करे", + "signInWithTestAccountInfoText": "(पुराना खाते का प्ररूप; आगे के लिए यंत्र खाते का प्रयोग करें)", + "signInWithTestAccountText": "परीक्षण के खाते से साइन इन करें", + "signOutText": "साइन आउट", + "signingInText": "साइन इन हो रहा है...", + "signingOutText": "साइन आउट हो रहा है...", + "testAccountWarningCardboardText": "चेतावनी: आप एक \"परीक्षा\" खाते से साइन इन कर रहे हैं |\nयह आपके खाते से बदल दिया जायेगा एक बार कार्डबोर्ड एप्प समर्थित हो जाये |\n\nअभी के लिए आपको टिकेट गेम के अन्दर से ही कमाने होंगे |\n(आपको बम स्क्वाड प्रो का उन्नयन मुफ्त में दिया जाएगा |)", + "testAccountWarningOculusText": "चेतावनी: आप एक \"परीक्षा\" खाते से साइन इन कर रहे हैं |\nयह आपके औक्युलस खाते से बदल दिया जायेगा इस साल के अंत तक जब टिकेट खरीदने व आदि विशेषताएं इस गेम में जोड़ दी जायेंगी |\n\nअभी के लिए आपको टिकेट गेम के अन्दर से ही कमाने होंगे |\n(आपको बम स्क्वाड प्रो का उन्नयन मुफ्त में दिया जाएगा |)", + "testAccountWarningText": "चेतावनी: आप एक \"परीक्षा\" खाते से साइन इन कर रहे हैं |\nयह खाता इस विशिष्ट यंत्र से जुड़ा हुआ है और\nसमय-समय पर रीसेट हो सकता है. (तथा इसी लिए ज्यादा समय सामन खोलने/ खरीदने व इकट्ठा करने में न लगायें ) |\n\nएक खुदरा संस्करण का प्रयोग करें एक 'सम्पूर्ण' खाते ( गेम सेण्टर, गूगल प्लस, आदि) के लिए | इससे आप अपनी प्रगति को बचा कर रख सकते हैं क्लाउड में तथा दुसरे यंत्रों पे भी अपनी प्रगति वहीँ से जारी रख सकते हैं | ", + "ticketsText": "टिकट: ${COUNT}", + "titleText": "खाता", + "unlinkAccountsInstructionsText": "अनलिंक करने के लिए एक खाता चुनें", + "unlinkAccountsText": "खाते अनलिंक करें", + "viaAccount": "(खाता ${NAME} के माध्यम से)", + "youAreSignedInAsText": "आप इस खाते से साइनड इन हो: " + }, + "achievementChallengesText": "उपलब्धि की चुनौतियां", + "achievementText": "उपलब्धि", + "achievements": { + "Boom Goes the Dynamite": { + "description": "टी•न•टी से ३ बुरे लोगों को मार डालो |", + "descriptionComplete": "टी•न•टी से ३ बुरे लोगों को मार डाला |", + "descriptionFull": "टी•न•टी से ३ बुरे लोगों को मार डालो स्तर ${LEVEL} पर |", + "descriptionFullComplete": "टी•न•टी से ३ बुरे लोगों को मार डाला स्तर ${LEVEL}  पर |", + "name": "बूम !! डायनामाइट फटा !" + }, + "Boxer": { + "description": "बिना बम इस्तेमाल करे जीतें |", + "descriptionComplete": "बिना बम इसतमाल करे जीते |", + "descriptionFull": "बिना बम इस्तेमाल करे स्तर ${LEVEL} पार करें |", + "descriptionFullComplete": "बिना बम इस्तेमाल करे स्तर ${LEVEL} पार किया |", + "name": "मुक्केबाज़" + }, + "Dual Wielding": { + "descriptionFull": "दो कंट्रोलर (हार्डवेयर या ऍप) जोड़िये", + "descriptionFullComplete": "नियंतरण से जुणा", + "name": "दो हथियार" + }, + "Flawless Victory": { + "description": "एक भी बार चोट लगे बिना जीतें |", + "descriptionComplete": "एक भी बार चोट लगे बिना जीते |", + "descriptionFull": "एक भी बार चोट लगे बिना स्तर ${LEVEL} जीतें |", + "descriptionFullComplete": "एक भी बार चोट लगे बिना स्तर ${LEVEL} जीते |", + "name": "निष्कलंक विजय" + }, + "Free Loader": { + "descriptionFull": "फ्री-फॉर-ऑल गेम दो से ज्यादा खिलाड़ियोंके साथ शुरू कीजिए", + "descriptionFullComplete": "दो से जयादा खिलाडी़यों के साथ सबके-लिये-फृी खेल की शुरुआत हो चुकी है", + "name": "मुफत लोडर" + }, + "Gold Miner": { + "description": "विस्फोटक थल बम से ६ बुरे लोगों को मारें |", + "descriptionComplete": "विस्फोटक थल बम से ६ बुरे लोगों को मारा |", + "descriptionFull": "विस्फोटक थल बम से ६ बुरे लोगों को मारें स्तर ${LEVEL} पर |", + "descriptionFullComplete": "विस्फोटक थल बम से ६ बुरे लोगों को मार दिया स्तर ${LEVEL} पर |", + "name": "सोने का खनिक" + }, + "Got the Moves": { + "description": "घूंसे या बमों का इस्तेमाल बिना किये जीतें |", + "descriptionComplete": "घूंसे या बमों का इस्तेमाल बिना किये जीत गए |", + "descriptionFull": "घूंसे या बमों का इस्तेमाल बिना किये स्तर ${LEVEL} जीतें |", + "descriptionFullComplete": "घूंसे या बमों का इस्तेमाल बिना किये स्तर ${LEVEL} जीत गए |", + "name": "चालें में निपुण" + }, + "In Control": { + "descriptionFull": "एक नियंत्रक कनेक्ट करें (हार्डवेयर या ऐप)", + "descriptionFullComplete": "एक नियंत्रक जोड़ा गया(हार्डवेयर या ऐप)", + "name": "नियंत्रण मे" + }, + "Last Stand God": { + "description": "१००० अंक स्कोर करे |", + "descriptionComplete": "१००० स्कोर कर लिया |", + "descriptionFull": "${LEVEL} स्तर पर १००० अंक स्कोर करे |", + "descriptionFullComplete": "${LEVEL} स्तर पर १००० अंक स्कोर किया |", + "name": "${LEVEL} भगवान" + }, + "Last Stand Master": { + "description": "२५० स्कोर करें |", + "descriptionComplete": "२५० स्कोर कर लिया |", + "descriptionFull": "${LEVEL} स्तर पर २५० अंक स्कोर करें |", + "descriptionFullComplete": "${LEVEL} स्तर पर २५० अंक स्कोर किया |", + "name": "${LEVEL} गुरु" + }, + "Last Stand Wizard": { + "description": "५०० अंक स्कोर करें |", + "descriptionComplete": "५०० स्कोर कर लिया |", + "descriptionFull": "${LEVEL} स्तर पर ५०० अंक स्कोर करें |", + "descriptionFullComplete": "${LEVEL} स्तर पर ५०० अंक स्कोर किया |", + "name": "${LEVEL} जादूगर" + }, + "Mine Games": { + "description": "विस्फोटक थल बम से ३ बुरे लोगों को मारें |", + "descriptionComplete": "विस्फोटक थल बम से ३ बुरे लोगों को मारा |", + "descriptionFull": "विस्फोटक थल बम से स्तर ${LEVEL} पे ३ बुरे लोगों को मारें |", + "descriptionFullComplete": "विस्फोटक थल बम से स्तर ${LEVEL} पे ३ बुरे लोगों को मारा |", + "name": "विस्फोटक थल बम के खेल" + }, + "Off You Go Then": { + "description": "३ बुरे लोगों को नक्शे के बाहर फेंकें |", + "descriptionComplete": "३ बुरे लोगों को नक्शे के बाहर फेंका |", + "descriptionFull": "३ बुरे लोगों को नक्शे के बाहर स्तर ${LEVEL} पर फेंकें |", + "descriptionFullComplete": "३ बुरे लोगों को नक्शे के बाहर स्तर ${LEVEL} पर फेंका |", + "name": "जाओ यहाँ से तुम" + }, + "Onslaught God": { + "description": "५००० अंक स्कोर करें |", + "descriptionComplete": "५००० स्कोर कर लिया |", + "descriptionFull": "${LEVEL} स्तर पर ५००० अंक स्कोर करें |", + "descriptionFullComplete": "${LEVEL} स्तर पर ५००० अंक स्कोर किया |", + "name": "${LEVEL} भगवान" + }, + "Onslaught Master": { + "description": "५०० अंक स्कोर करें |", + "descriptionComplete": "५०० स्कोर कर लिया |", + "descriptionFull": "${LEVEL} स्तर पर ५०० अंक स्कोर करें |", + "descriptionFullComplete": "${LEVEL} स्तर पर ५०० अंक स्कोर किया |", + "name": "${LEVEL} गुरु" + }, + "Onslaught Training Victory": { + "description": "सारी लहरों को पार करें |", + "descriptionComplete": "सारी लहरों को पार किया |", + "descriptionFull": "साड़ी लहरों को पार करें स्तर ${LEVEL} में |", + "descriptionFullComplete": "साड़ी लहरों को पार किया स्तर ${LEVEL} में |", + "name": "${LEVEL} पर विजय" + }, + "Onslaught Wizard": { + "description": "१००० अंक स्कोर करें |", + "descriptionComplete": "१००० स्कोर कर लिया |", + "descriptionFull": "${LEVEL} स्तर पर १००० अंक स्कोर करें |", + "descriptionFullComplete": "${LEVEL} स्तर पर १००० अंक स्कोर किया |", + "name": "${LEVEL} जादूगर" + }, + "Precision Bombing": { + "description": "किसी भी शक्तिप्रापक का इस्तेमाल किये बिना जीतें |", + "descriptionComplete": "किसी भी शक्तिप्रापक का इस्तेमाल किये बिना जीते |", + "descriptionFull": "किसी भी शक्तिप्रापक का इस्तेमाल किये बिना स्तर ${LEVEL} जीतें |", + "descriptionFullComplete": "किसी भी शक्तिप्रापक का इस्तेमाल किये बिना स्तर ${LEVEL} जीते |", + "name": "सटीक बमबारी" + }, + "Pro Boxer": { + "description": "बिना बम का प्रयोग करे जीतें |", + "descriptionComplete": "बिना बम का प्रयोग करे जीते |", + "descriptionFull": "बिना बम का प्रयोग करे स्तर ${LEVEL} पार करें |", + "descriptionFullComplete": "बिना बम का प्रयोग करे स्तर ${LEVEL} पार किया |", + "name": "पेशेवर बॉक्सर" + }, + "Pro Football Shutout": { + "description": "बिना बुरे लोगों के अंक लिए हुए जीतें |", + "descriptionComplete": "बिना बुरे लोगों के अंक लिए हुए जीते |", + "descriptionFull": "बिना बुरे लोगों के अंक लिए हुए स्तर ${LEVEL} जीतें |", + "descriptionFullComplete": "बिना बुरे लोगों के अंक लिए हुए स्तर ${LEVEL} जीते |", + "name": "${LEVEL} शट आउट" + }, + "Pro Football Victory": { + "description": "गेम जीतें |", + "descriptionComplete": "गेम जीत गए |", + "descriptionFull": "गेम जीतें स्तर ${LEVEL} पर |", + "descriptionFullComplete": "गेम जीत गए स्तर ${LEVEL} पर |", + "name": "${LEVEL} पर विजय" + }, + "Pro Onslaught Victory": { + "description": "सारी लहरों को पार करें |", + "descriptionComplete": "सारी लहरों को पार किया |", + "descriptionFull": "साड़ी लहरों को पार करें स्तर ${LEVEL} में |", + "descriptionFullComplete": "साड़ी लहरों को पार किया स्तर ${LEVEL} में |", + "name": "${LEVEL} पर विजय" + }, + "Pro Runaround Victory": { + "description": "सारी लहरों को पार करें |", + "descriptionComplete": "सारी लहरों को पार किया |", + "descriptionFull": "साड़ी लहरों को पार करें स्तर ${LEVEL} में |", + "descriptionFullComplete": "साड़ी लहरों को पार किया स्तर ${LEVEL} में |", + "name": "${LEVEL} पर विजय" + }, + "Rookie Football Shutout": { + "description": "बिना बुरे लोगों के अंक लिए हुए जीतें |", + "descriptionComplete": "बिना बुरे लोगों के अंक लिए हुए जीते |", + "descriptionFull": "बिना बुरे लोगों के अंक लिए हुए स्तर ${LEVEL} जीतें |", + "descriptionFullComplete": "बिना बुरे लोगों के अंक लिए हुए स्तर ${LEVEL} जीते |", + "name": "${LEVEL} शट आउट" + }, + "Rookie Football Victory": { + "description": "गेम जीतें |", + "descriptionComplete": "गेम जीत गए |", + "descriptionFull": "गेम जीतें स्तर ${LEVEL} पर |", + "descriptionFullComplete": "गेम जीत गए स्तर ${LEVEL} पर |", + "name": "${LEVEL} पर विजय" + }, + "Rookie Onslaught Victory": { + "description": "सारी लहरों को पार करें |", + "descriptionComplete": "सारी लहरों को पार किया |", + "descriptionFull": "सारी लहरों को पार करें स्तर ${LEVEL} में |", + "descriptionFullComplete": "सारी लहरों को पार किया स्तर ${LEVEL} में |", + "name": "${LEVEL} पर विजय" + }, + "Runaround God": { + "description": "२०० अंक स्कोर करें |", + "descriptionComplete": "२०० स्कोर कर लिया |", + "descriptionFull": "${LEVEL} स्तर पर २०० अंक स्कोर करें |", + "descriptionFullComplete": "${LEVEL} स्तर पर २०० अंक स्कोर किया |", + "name": "${LEVEL} भगवान" + }, + "Runaround Master": { + "description": "५०० अंक स्कोर करें |", + "descriptionComplete": "५०० स्कोर कर लिया |", + "descriptionFull": "${LEVEL} स्तर पर ५०० अंक स्कोर करें |", + "descriptionFullComplete": "${LEVEL} स्तर पर ५०० अंक स्कोर किया |", + "name": "${LEVEL} गुरु" + }, + "Runaround Wizard": { + "description": "१००० अंक स्कोर करें |", + "descriptionComplete": "१००० स्कोर कर लिया |", + "descriptionFull": "${LEVEL} स्तर पर १००० अंक स्कोर करें |", + "descriptionFullComplete": "${LEVEL} स्तर पर १००० अंक स्कोर किया |", + "name": "${LEVEL} जादूगर" + }, + "Sharing is Caring": { + "descriptionFull": "सफलतापूवृक शेयर", + "descriptionFullComplete": "मित्र के साथ खेल को सफलतापूर्वक साझा किया गया", + "name": "साझा करना ही देखभाल है" + }, + "Stayin' Alive": { + "description": "बिना मरे जीतें |", + "descriptionComplete": "बिना मरे जीते |", + "descriptionFull": "बिना मरे स्तर ${LEVEL} जीतें |", + "descriptionFullComplete": "बिना मरे स्तर ${LEVEL} जीते |", + "name": "जिन्दा हूँ मैं !" + }, + "Super Mega Punch": { + "description": "एक घूँसा में १००% नुक्सान दें दुश्मन को |", + "descriptionComplete": "एक घूँसा में १००% नुक्सान दिया दुश्मन को |", + "descriptionFull": "एक घूँसा में १००% नुक्सान दें दुश्मन को स्तर ${LEVEL} पर |", + "descriptionFullComplete": "एक घूँसा में १००% नुक्सान दें दुश्मन को स्तर ${LEVEL} पर |", + "name": "धमाकेदार घूँसा !" + }, + "Super Punch": { + "description": "एक घूँसा में ५०% नुक्सान दें दुश्मन को |", + "descriptionComplete": "एक घूँसा में ५०% नुक्सान दिया दुश्मन को |", + "descriptionFull": "एक घूँसा में ५०% नुक्सान दें दुश्मन को स्तर ${LEVEL} पर |", + "descriptionFullComplete": "एक घूँसा में ५०% नुक्सान दें दुश्मन को स्तर ${LEVEL} पर |", + "name": "धुआँधार घूँसा !" + }, + "TNT Terror": { + "description": "टी•न•टी से ६ बुरे लोगों को मार डालो |", + "descriptionComplete": "टी•न•टी से ६ बुरे लोगों को मार डाला |", + "descriptionFull": "टी•न•टी से ६ बुरे लोगों को मार डालो स्तर ${LEVEL} पर |", + "descriptionFullComplete": "टी•न•टी से ६ बुरे लोगों को मार डाला स्तर ${LEVEL} पर |", + "name": "टी•न•टी का आतंक !" + }, + "Team Player": { + "descriptionFull": "4+ खिलाड़ियों के साथ एक टीम गेम शुरू करें", + "descriptionFullComplete": "4+ खिलाड़ियों के साथ एक टीम गेम शुरू किया", + "name": "टीम के खिलाड़ी" + }, + "The Great Wall": { + "description": "हर एक बुरे व्यक्ति को रोकें |", + "descriptionComplete": "हर एक बुरे व्यक्ति को रोका |", + "descriptionFull": "हर एक बुरे व्यक्ति को रोकें स्तर ${LEVEL} पर |", + "descriptionFullComplete": "हर एक बुरे व्यक्ति को रोका स्तर ${LEVEL} पर |", + "name": "अटूट दीवार !" + }, + "The Wall": { + "description": "हर एक बुरे व्यक्ति को रोकें |", + "descriptionComplete": "हर एक बुरे व्यक्ति को रोका |", + "descriptionFull": "हर एक बुरे व्यक्ति को रोकें स्तर ${LEVEL} पर |", + "descriptionFullComplete": "हर एक बुरे व्यक्ति को रोका स्तर ${LEVEL} पर |", + "name": "दीवार !" + }, + "Uber Football Shutout": { + "description": "बिना बुरे लोगों के अंक लिए हुए जीतें |", + "descriptionComplete": "बिना बुरे लोगों के अंक लिए हुए जीते |", + "descriptionFull": "बिना बुरे लोगों के अंक लिए हुए स्तर ${LEVEL} जीतें |", + "descriptionFullComplete": "बिना बुरे लोगों के अंक लिए हुए स्तर ${LEVEL} जीते |", + "name": "${LEVEL} शट आउट" + }, + "Uber Football Victory": { + "description": "गेम जीतें |", + "descriptionComplete": "गेम जीत गए |", + "descriptionFull": "गेम जीतें स्तर ${LEVEL} पर |", + "descriptionFullComplete": "गेम जीत गए स्तर ${LEVEL} पर |", + "name": "${LEVEL} पर विजय" + }, + "Uber Onslaught Victory": { + "description": "सारी लहरों को पार करें |", + "descriptionComplete": "सारी लहरों को पार किया |", + "descriptionFull": "सारी लहरों को पार करें स्तर ${LEVEL} पर |", + "descriptionFullComplete": "सारी लहरों को पार किया स्तर ${LEVEL} पर |", + "name": "${LEVEL} पर विजय" + }, + "Uber Runaround Victory": { + "description": "सारी लहरों को पार करें |", + "descriptionComplete": "सारी लहरों को पार किया |", + "descriptionFull": "सारी लहरों को पार करें स्तर ${LEVEL} पर |", + "descriptionFullComplete": "सारी लहरों को पार किया स्तर ${LEVEL} पर |", + "name": "${LEVEL} पर विजय" + } + }, + "achievementsRemainingText": "उप्लाब्धियाँ बाकी:", + "achievementsText": "उप्लाब्धियाँ", + "achievementsUnavailableForOldSeasonsText": "माफ़ करें उपलब्धियों कि जानकारी पुराने सीजन से नहीं है", + "addGameWindow": { + "getMoreGamesText": "और गेम्स कि जानकारी पायें", + "titleText": "गेम जोड़ें" + }, + "allowText": "अनुमति दें", + "alreadySignedInText": "आपका खाता किसी अन्य डिवाइस से साइन किया गया है; \nकृपया खातों को स्विच करें या अपने गेम को अन्य डिवाइस \nपर बंद करें और फिर से प्रयास करें", + "apiVersionErrorText": "${NAME} मौड्यूल लोड नहीं हो पाया ; यह एपीआई - संस्करण ${VERSION_USED} पे काम करने का प्रयास कर रहा है ; हमें संस्करण ${VERSION_REQUIRED} चाहिए |", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"स्वयं\" इसे तभी शुरू करेगा जब हैडफ़ोन लगें हों)", + "headRelativeVRAudioText": "सर के सापेक्ष वीआर ध्वनि", + "musicVolumeText": "गाने की आवाज़", + "soundVolumeText": "गाने कि ध्वनि का स्तर", + "soundtrackButtonText": "गाने", + "soundtrackDescriptionText": "(गेम के दौरान चलने के लिए अपना गाना निर्दिष्ट करें)", + "titleText": "ध्वनि" + }, + "autoText": "अपने आप उत्तम चुने", + "backText": "वापस", + "banThisPlayerText": "इस खिलाड़ी को प्रतिबंधित करें", + "bestOfFinalText": "${COUNT}-का-सबसे-अच्छा आखिरी", + "bestOfSeriesText": "${COUNT} सीरीज़ का सर्वोच्च", + "bestRankText": "आपका सबसे अच्छा पद है #${RANK}", + "bestRatingText": "आपका सबसे अच्छा मूल्यांकन है ${RATING}", + "bombBoldText": "बम", + "bombText": "बम", + "boostText": "प्रोत्साहन", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} को एप्लीकेशन के अन्दर से ही कॉन्फ़िगर करें |", + "buttonText": "बटन", + "canWeDebugText": "क्या आप बोम्ब-स्क्वाड को अपने आप खराबियों व \nआधारभूत उपयोग कि जानकारी भेजना चाहते हैं ? \n\nइस जानकारी में कुछ भी व्यक्तिगत नहीं होता है व \nयह गेम को सुचारू रूप से बिना खराबियों के चलने में सहायता करता है |", + "cancelText": "रद्द करें", + "cantConfigureDeviceText": "माफ़ करें  ${DEVICE} कांफिग्युरेब्ल नहीं है |", + "challengeEndedText": "यह चुनौती समाप्त हो चूकि हैं", + "chatMuteText": "बातचीत मौन करें", + "chatMutedText": "बातचीत मौन हो गई है", + "chatUnMuteText": "बातचीत दोबारा शुरू करें", + "choosingPlayerText": "<खिलाड़ी चुना जा रहा है>", + "completeThisLevelToProceedText": "आपको यह पड़ाव पार करना पड़ेगा आगे बढ़ने के लिए !", + "completionBonusText": "पूर्णता पुरस्कार", + "configControllersWindow": { + "configureControllersText": "नियंत्रक को कॉन्फ़िगर करें", + "configureKeyboard2Text": "कीबोर्ड पी २ को कॉन्फ़िगर करें", + "configureKeyboardText": "कीबोर्ड को कॉन्फ़िगर करें", + "configureMobileText": "मोबाइल यंत्रों को नियंत्रक के रूप में", + "configureTouchText": "टच स्क्रीन को कॉन्फ़िगर करें", + "ps3Text": "पी-एस-३ नियंत्रक", + "titleText": "नियंत्रक", + "wiimotesText": "वाईमोत्स", + "xbox360Text": "एक्स्बोक्स ३६० नियंत्रक" + }, + "configGamepadSelectWindow": { + "androidNoteText": "टिप्पणी- नियंत्रक के लिए सहारा यंत्र से यंत्र व एंड्राइड के संस्करणों से अलग अलग होता है |", + "pressAnyButtonText": "जिस भी नियंत्रक को आप कॉन्फ़िगर करना चाहते हैं उस पर कोई भी बटन दबाएँ ...", + "titleText": "नियंक्त्रकों को कॉन्फ़िगर करें" + }, + "configGamepadWindow": { + "advancedText": "उन्नत सेटिंग्स", + "advancedTitleText": "उन्नत सेटिंग्स नियंत्रक सेटअप", + "analogStickDeadZoneDescriptionText": "(इसको बढ़ाएं अगर आपकी प्रकृति नियंत्रण छोड़ने पर भी हिलती है )", + "analogStickDeadZoneText": "अनुरूप छड़ी का मृत क्षेत्र", + "appliesToAllText": "(इस प्रकार के सभी नियंत्रकों को प्रभावित करता है)", + "autoRecalibrateDescriptionText": "(इसे सक्षम करें यदि आपकी प्रकृति पूरी गति से नहीं चलती है )", + "autoRecalibrateText": "अनुरूप छड़ी का पुनः व्यास मापन करें", + "axisText": "धुरी", + "clearText": "खाली करें", + "dpadText": "डी-पैड", + "extraStartButtonText": "अतिरिक्त प्रारंभ करने का बटन", + "ifNothingHappensTryAnalogText": "अगर कुछ नहीं होता है तो अनुरूप छड़ी को निर्दिष्ट करने का प्रयास करें", + "ifNothingHappensTryDpadText": "अगर कुछ नहीं होता है तो डी-पैड को निर्दिष्ट करने का प्रयास करें", + "ignoreCompletelyDescriptionText": "(इस नियंत्रक को खेल या मेनू को प्रभावित करने से रोकें)", + "ignoreCompletelyText": "पूरी तरह से अनदेखा करें", + "ignoredButton1Text": "उपेक्षित बटन १", + "ignoredButton2Text": "उपेक्षित बटन २", + "ignoredButton3Text": "उपेक्षित बटन ३", + "ignoredButton4Text": "उपेक्षित बटन 4", + "ignoredButtonDescriptionText": "(होम व सम्न्यवित के बटन के प्रभाव से बचने के लिए इसका प्रयोग करें)", + "pressAnyAnalogTriggerText": "किसी भी अनुरुप संकेत को दबाएँ..", + "pressAnyButtonOrDpadText": "किसी भी बटन या डी-पैड को दबाएँ..", + "pressAnyButtonText": "कोई बटन दबाये..", + "pressLeftRightText": "दायें या बाएं दबाएँ..", + "pressUpDownText": "ऊपर या नीचे दबाएँ..", + "runButton1Text": "दोड़ने का पहला बटन", + "runButton2Text": "दोड़ने का दूसरा बटन", + "runTrigger1Text": "दोड़ने का पहला संकेत", + "runTrigger2Text": "दोड़ने का दूसरा संकेत", + "runTriggerDescriptionText": "(अनुरूप संकेत आपको परिवर्तनशील गति से दोड़ने दे सकता है)", + "secondHalfText": "इसका प्रयोग करके एक में दो वाले \nयंत्र को कॉन्फ़िगर करें जो एक यंत्र \nकि तरह भांपा जा रहा हो |", + "secondaryEnableText": "सक्षम करें", + "secondaryText": "द्वितीय नियंत्रक", + "startButtonActivatesDefaultDescriptionText": "(इसे बंद कर दें यदि आपका प्रारंभ करने का बटन मेन्यू बटन ज्यादा है)", + "startButtonActivatesDefaultText": "प्रारंभ करने का बटन, पहले से प्रस्थापित विजेट को सक्रिय कर देता है", + "titleText": "नियंत्रक का सेटअप", + "twoInOneSetupText": "एक में दो कंट्रोलर का सेटअप", + "uiOnlyDescriptionText": "(इस नियंत्रक को वास्तव में एक गेम में शामिल होने से रोकें)", + "uiOnlyText": "मेनू उपयोग की सीमा", + "unassignedButtonsRunText": "सरे निर्दिष्ट न किये हुए बटन को चलाना", + "unsetText": "<नियत न किये हुए>", + "vrReorientButtonText": "वीआर पुनर्योजी बटन" + }, + "configKeyboardWindow": { + "configuringText": "${DEVICE} को कॉन्फ़िगर किया जा रहा है", + "keyboard2NoteText": "टिपण्णी: अधिकतर कीबोर्ड एक बार में कुछ ही \nबटन के दबने को पह्चान सकता है, \nइसलिए दूसरा कीबोर्ड होना बेहतर रह सकता है |0\nपण्णी: आपको दुसरे खिलाड़ी के लिए \nफिर भी अलग बटन निर्दिष्ट करने पड़ेंगे |" + }, + "configTouchscreenWindow": { + "actionControlScaleText": "कार्य करने के बटनों का माप", + "actionsText": "कार्य", + "buttonsText": "बटन", + "dragControlsText": "< नियंत्रण बटन को पुनः स्थापित करने के लिए पकड़ के हिलाएं >", + "joystickText": "जोस्टिक", + "movementControlScaleText": "संचालन के बटनों का माप", + "movementText": "संचालन", + "resetText": "रीसेट", + "swipeControlsHiddenText": "स्वाइप आइकन को छुपा दें", + "swipeInfoText": "स्वाइप से खेलना सिखने में थोडा टाइम लगता है परन्तु\n उसके बाद बिना नियंत्रण बटन देखे खेलना बहुत आसन होता है", + "swipeText": "स्वाइप", + "titleText": "टच स्क्रीन को कॉन्फ़िगर करें" + }, + "configureItNowText": "अभी कॉन्फ़िगर करें ?", + "configureText": "कॉन्फ़िगर", + "connectMobileDevicesWindow": { + "amazonText": "अमेज़न का एप्लीकेशन भंडार", + "appStoreText": "गूगल का एप्लीकेशन हंदर", + "bestResultsText": "सबसे अच्छे परिणामों के लये आपको विलंबन रहित नेटवर्क चाहिए होगा | \nआप विलंबन कम कर सकते हैं बाकी वायरलेस यंत्रों को बंद कर के, \nराऊटर के पास खेल के, \nव गेम मेज़बान को नेटवर्क से ईथरनेट से सीधे जोड़ के |", + "explanationText": "किसी स्मार्ट फ़ोन या टेबलेट का वायरलेस नयन्त्रक के रूप में प्रयोग करने के लिए \nउसपे \"${REMOTE_APP_NAME}\" एप्लीकेशन डालें | \nकितने भी यंत्र ${APP_NAME} गेम से वाई-फी से जुड़ सकते हैं, और यह मुफ्त है !", + "forAndroidText": "एंड्राइड के लिए:", + "forIOSText": "आई-ओ-एस के लिए:", + "getItForText": "${REMOTE_APP_NAME} पायें आई-ओ-एस के लिए एप्पल एप्लीकेशन भंडार से \nव एंड्राइड के लिए गूगल प्ले भंडार या अमेज़न एप्लीकेशन भंडार से |", + "googlePlayText": "गूगल प्ले स्टोर", + "titleText": "मोबाइल यंत्र का नियंत्रक के रूप में प्रयोग करते हुए: " + }, + "continuePurchaseText": "${PRICE} के खर्चे पर जारी रखें ?", + "continueText": "जारी रखें", + "controlsText": "नियंत्रण", + "coopSelectWindow": { + "activenessAllTimeInfoText": "यह सम्पूर्ण काल के पद पे लागू नहीं होता है", + "activenessInfoText": "यह गुणक उन दिनों बढ़ता है जब आप खेलते हैं \nऔर उन दिनों घटता है जिन दिन आप नहीं खेलते", + "activityText": "सक्रियता", + "campaignText": "अभियान", + "challengesInfoText": "छोटे गेम खेल के पुरस्कार जीतें | \n\nकिसी स्तर को पार करने पर पुरस्कार \nव कठिनाई बढती है | \nकिसी स्तर पर हारने पर कठिनाई व पुरस्कार कम होता है |", + "challengesText": "चुनौतियाँ", + "currentBestText": "अभी तक का सबसे अच्छा", + "customText": "अपने हिसाब से", + "entryFeeText": "भाग लेने कि कीमत", + "forfeitConfirmText": "यह चुनौती छोड़ दें ?", + "forfeitNotAllowedYetText": "यह चुनौती इस वक्त समाप्त नहीं हो सकती", + "forfeitText": "हार मानें", + "multipliersText": "गुणक", + "nextChallengeText": "अगली चुनौती", + "nextPlayText": "अगली बार खेलने का अवसर", + "ofTotalTimeText": "${TOTAL} में से", + "playNowText": "अभी खेलें", + "pointsText": "अंक", + "powerRankingFinishedSeasonUnrankedText": "(ख़तम किया बिना पद के)", + "powerRankingNotInTopText": "(सर्वोच्च ${NUMBER} में से नहीं)", + "powerRankingPointsEqualsText": "= ${NUMBER} pts", + "powerRankingPointsMultText": "(x ${NUMBER} अंक)", + "powerRankingPointsText": "${NUMBER} अंक", + "powerRankingPointsToRankedText": "(${REMAINING} में से ${CURRENT} अंक बाकी)", + "powerRankingText": "सत्ता पद", + "prizesText": "पुरस्कार", + "proMultInfoText": "${PRO} उन्नयन वाले खिलाड़ियों को \n${PERCENT}% अंकों का बढ़ावा मिलता है", + "seeMoreText": "और..", + "skipWaitText": "प्रतीक्षा ख़तम करें", + "timeRemainingText": "समय बाकी", + "toRankedText": "अभी पद मिलना है", + "totalText": "सम्पूर्ण", + "tournamentInfoText": "सम्पूर्ण अंकों में अपने संघ के बाकी \nखिलाड़ियों के साथ प्रतिस्पर्धा करें | \n\nप्रतियोगिता के ख़तम होने पर सबसे ज्यादा अंकों वाले \nखिलाड़ियों को पुरस्कार दिया जायेगा |", + "welcome1Text": "${LEAGUE} में आपका स्वागत है | \nआप आपनी संघ में पैड को स्टार रेटिंग कमाने से, \nउप्लाब्धियाँ पार करने से व  ट्रॉफी जीतने से बढ़ा सकते हैं |", + "welcome2Text": "आप टिकेट उन्ही गतिविधियाओं से भी कमा सकते हैं | \nटिकेट नए रूप, नक़्शे व छोटे गेम खोलने तथा \nप्रतियोगिता में भाग लेने आदि में काम आ सकते हैं |", + "yourPowerRankingText": "आपका सत्ता पद :" + }, + "copyOfText": "${NAME} दूसरा", + "createEditPlayerText": "<प्लेयर बनाएँ / संपादित करें>", + "createText": "बनायें", + "creditsWindow": { + "additionalAudioArtIdeasText": "${NAME} कि अतिरिक्त ध्वनि, अकालघटित कलाकृति, और विचार", + "additionalMusicFromText": "${NAME} कि अतिरिक्त ध्वनि", + "allMyFamilyText": "मेरे सारे परिवार के सदस्य व दोस्त जिन्होंने गेम खेला", + "codingGraphicsAudioText": "कोडिंग, ग्राफ़िक्स और ध्वनि ${NAME} के द्वारा", + "languageTranslationsText": "भाषा के अनुवाद:", + "legalText": "क़ानूनी:", + "publicDomainMusicViaText": "सार्वजनिक-डोमेन संगीत ${NAME} के द्वारा", + "softwareBasedOnText": "यह सॉफ्टवेयर ${NAME} के कुछ अंश के काम पर आधारित है", + "songCreditText": "${TITLE} ${PERFORMER} के द्वारा, ${COMPOSER} के द्वारा रचा हुआ,\n ${ARRANGER} के द्वारा सहेजा हुआ, \n ${PUBLISHER} द्वारा प्रकाशित, ${SOURCE} का सौजन्य |", + "soundAndMusicText": "ध्वनि व गाने:", + "soundsText": "गाने (${SOURCE}):", + "specialThanksText": "विशेष धन्यवाद:", + "thanksEspeciallyToText": "खासकर धन्यवाद ${NAME}", + "titleText": "${APP_NAME} कि आभार सूचि", + "whoeverInventedCoffeeText": "जिसने भी कॉफ़ी ढूंढी!" + }, + "currentStandingText": "आपका वर्तमान पैड है #${RANK}", + "customizeText": "अपने हिसाब से समायोजित करें...", + "deathsTallyText": "${COUNT} बार मौत", + "deathsText": "मौत", + "debugText": "डीबग", + "debugWindow": { + "reloadBenchmarkBestResultsText": "टिप्पणी: इसकी जांच करते समय सेटिंग->ग्राफ़िक्स->तेक्स्चर्स को 'उच्च' पे रखें", + "runCPUBenchmarkText": "सीपीयू बेंचमार्क चलायें", + "runGPUBenchmarkText": "जीपीयु बेंचमार्क चलायें", + "runMediaReloadBenchmarkText": "मीडिया लादने का बेंचमार्क चलायें", + "runStressTestText": "तनाव परीक्षा करें", + "stressTestPlayerCountText": "खिलाड़ियों कि संख्या", + "stressTestPlaylistDescriptionText": "तनाव परीक्षा कि प्लेलिस्ट", + "stressTestPlaylistNameText": "प्लेलिस्ट का नाम", + "stressTestPlaylistTypeText": "प्लेलिस्ट का प्ररूप", + "stressTestRoundDurationText": "दौर कि अवधि", + "stressTestTitleText": "तनाव परीक्षा", + "titleText": "बेंचमार्क व तनाव परीक्षाएं", + "totalReloadTimeText": "पूरा पुनः लादने का समय: ${TIME} (अधिक जानकारी के लिए लॅाग देखें)" + }, + "defaultGameListNameText": "पहले से प्रस्थापित ${PLAYMODE} प्लेलिस्ट", + "defaultNewGameListNameText": "मेरी ${PLAYMODE} प्लेलिस्ट", + "deleteText": "हटाना", + "demoText": "डेमो", + "denyText": "अस्वीकृत करें", + "desktopResText": "डेस्कटॉप रेज़ोल्यूशन", + "difficultyEasyText": "आसन", + "difficultyHardOnlyText": "केवल कठिन", + "difficultyHardText": "कठिन", + "difficultyHardUnlockOnlyText": "इस स्तर को सिर्फ कठिन में खोला जा सकता है | \nक्या आपमें वोह है इसे पार करने के लिए !", + "directBrowserToURLText": "कृपया अपने वेब ब्राउज़र को इस यूआरएल पे भेजें", + "disableRemoteAppConnectionsText": "रिमोट के ऐप्प कनेक्शन्स को बंद करे", + "disableXInputDescriptionText": "4 नियंत्रकों से अधिक की अनुमति देता है लेकिन साथ ही साथ काम नहीं कर सकते", + "disableXInputText": "Xinput अक्षम करें", + "doneText": "हो गया", + "drawText": "बराबर", + "duplicateText": "प्रतिलिपि", + "editGameListWindow": { + "addGameText": "गेम जोड़ें", + "cantOverwriteDefaultText": "पहले से प्रस्थापित किया गया प्लेलिस्ट ओवरराईट नहीं कर सकते !", + "cantSaveAlreadyExistsText": "इस नाम कि प्लेलिस्ट पहले से ही मौजूद है |", + "cantSaveEmptyListText": "खली प्लेलिस्ट को सेव नहीं किया जा सकता है", + "editGameText": "गेम को संपादित करें", + "listNameText": "प्लेलिस्ट का नाम", + "nameText": "नाम", + "removeGameText": "गेम को हटायें", + "saveText": "सूचि को सेव करें", + "titleText": "प्लेलिस्ट संपादक" + }, + "editProfileWindow": { + "accountProfileInfoText": "यह एक ख़ास पार्श्वचित्र ( एक नाम और आइकॉन के साथ) है \nआपके साइन इन करे हुए खाते के आधार पर | \n\n${ICONS} \n\nअपना पार्श्वचित्र बनायें अगर आपको अलग नाम \nअपने आइकॉन का प्रयोग करना है |", + "accountProfileText": "(खाते का पार्श्वचित्र)", + "availableText": "नाम \"${NAME}\" उपलब्ध है", + "changesNotAffectText": "टिपण्णी: गेम के अन्दर पहले से जो पात्र है उनपे परिवर्तन का असर नहीं होगा", + "characterText": "पात्र", + "checkingAvailabilityText": "${NAME} कि उपलब्धि जाँची जा रही है...", + "colorText": "रंग", + "getMoreCharactersText": "और पात्र पायें...", + "getMoreIconsText": "और आइकॉन पायें...", + "globalProfileInfoText": "खिलाड़ियों के वैश्विक पार्श्वचित्र का अनोखा नाम होगा | \nवैश्विक पार्श्वचित्र वाले खिलाड़ी अपना आइकॉन भी लगा सकते हैं |", + "globalProfileText": "(वैश्विक पार्श्वचित्र)", + "highlightText": "उभार", + "iconText": "आइकॉन", + "localProfileInfoText": "स्थानिक खिलाड़ी पार्श्वचित्र का कोई आइकॉन नहीं होता है \nव यह भी ज़रूरी नहीं है कि उनका नाम अनोखा हो | \nवैश्विक पार्श्वचित्र में उन्नयन करें अनोखे नाम व आइकॉन के लिए |", + "localProfileText": "(स्थानिक पार्श्वचित्र)", + "nameDescriptionText": "खिलाड़ी का नाम", + "nameText": "नाम", + "randomText": "यादृच्छिक", + "titleEditText": "पार्श्वचित्र को संपादित करें", + "titleNewText": "नया पार्श्वचित्र बनायें", + "unavailableText": "\"${NAME}\" उपलब्ध नहीं है; कृपया दूसरा नाम चुनें |", + "upgradeProfileInfoText": "यह आपके नाम को विश्व भर में निग्रह कर देगा \nव आप अपना आइकॉन चुन सकते हैं |", + "upgradeToGlobalProfileText": "वैश्विक पार्श्वचित्र में उन्नयन करें" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "पहले से प्रस्थापित गाने को डिलीट नहीं कर सकते |", + "cantEditDefaultText": "पहले से प्रस्थापित गाने को संपादित नहीं किया जा सकता है | इसकी छवि बनायें या नया बनायें |", + "cantOverwriteDefaultText": "पहले से प्रस्थापित गाने को अधिलेखित नहीं किया जा सकता है", + "cantSaveAlreadyExistsText": "इस नाम का गाना पहले से है !", + "defaultGameMusicText": "<पहले से प्रस्थापित गाना>", + "defaultSoundtrackNameText": "पहले से प्रस्थापित गाना", + "deleteConfirmText": "गाने को हटायें ?\n\n${NAME}", + "deleteText": "गाना \nहटायें", + "duplicateText": "गाने कि \nछवि बनायें", + "editSoundtrackText": "गाना संपादक", + "editText": "गाने को \nसंपादित करें", + "fetchingITunesText": "आपके गाने वाले एप्लिकेशन कि प्लेलिस्ट ली जा रही है...", + "musicVolumeZeroWarning": "चेतावनी: गाने कि ध्वनि का स्तर ० पर है", + "nameText": "नाम", + "newSoundtrackNameText": "मेरा गाना ${COUNT}", + "newSoundtrackText": "नया गाना:", + "newText": "नया \nगाना:", + "selectAPlaylistText": "पल्य्लिस्ट चुनें:", + "selectASourceText": "गाने का स्रोत", + "testText": "परीक्षा करें", + "titleText": "गानें", + "useDefaultGameMusicText": "पहले से प्रस्थापित गेम का गाना", + "useITunesPlaylistText": "गाने एप्लिकेशन कि प्लेलिस्ट", + "useMusicFileText": "गाने कि फाइल (एम्-पी-३, आदि)", + "useMusicFolderText": "गाने कि फाइल्स का फोल्डर" + }, + "editText": "संपादित करें", + "endText": "समाप्त", + "enjoyText": "मज़ा लें !", + "epicDescriptionFilterText": "${DESCRIPTION} उत्कृष्ट धीमे गति में।", + "epicNameFilterText": "उत्कृष्ट ${NAME}", + "errorAccessDeniedText": "अभिगम वर्जित", + "errorOutOfDiskSpaceText": "डिस्क पे जगह ख़तम", + "errorText": "त्रुटी", + "errorUnknownText": "अज्ञात त्रुटी", + "exitGameText": "${APP_NAME} से निकास करें ?", + "exportSuccessText": "'${NAME}' निर्यात हुआ", + "externalStorageText": "बाहरी संचयन", + "failText": "असफल", + "fatalErrorText": "माफ़ करें कुछ लुप्त है या गड़बड़ है | \nकृपया फिर से इनस्टॉल करने का प्रयास करें या \n${EMAIL} पर सहयता के लिए संपर्क करें |", + "fileSelectorWindow": { + "titleFileFolderText": "कोई फाइल या फोल्डर चुनें", + "titleFileText": "फाइल चुनें", + "titleFolderText": "फोल्डर चुनें", + "useThisFolderButtonText": "इस फोल्डर को चुनें" + }, + "filterText": "निस्पंदन", + "finalScoreText": "समापक अंक", + "finalScoresText": "समापक अंक", + "finalTimeText": "समापक समय", + "finishingInstallText": "इनस्टॉल ख़तम हो रहा है, सब्र रखें...", + "fireTVRemoteWarningText": "और बेहतर अनुभव के लिए \nगेम नियंत्रक का प्रयोग करें, \nया '${REMOTE_APP_NAME}' \nएप्लीकेशन इन्स्टाल करें अपने फ़ोन या टेबलेट पे |", + "firstToFinalText": "${COUNT} तक पहले पहुँचने कि प्रतियोगिता का आखिरी दौर", + "firstToSeriesText": "${COUNT} तक पहले पहुँचने कि प्रतियोगिता", + "fiveKillText": "पांच हत्या !!!", + "flawlessWaveText": "त्रुटिरहित लहर !", + "fourKillText": "चार हत्या !!!", + "friendScoresUnavailableText": "दोस्तों के अंक उनुप्लाब्ध हैं ", + "gameCenterText": "गेम-सेण्टर", + "gameCircleText": "गेम-सर्किल", + "gameLeadersText": "गेम ${COUNT} के सरदार", + "gameListWindow": { + "cantDeleteDefaultText": "पहले से प्रस्थापित प्लेलिस्ट को डिलीट नहीं कर सकते |", + "cantEditDefaultText": "पहले से प्रस्थापित प्लेलिस्ट को संपादित नहीं किया जा सकता है | इसकी छवि बनायें या नया बनायें |", + "cantShareDefaultText": "आप डिफ़ॉल्ट प्लेलिस्ट साझा नहीं कर सकते", + "deleteConfirmText": "${LIST} को हटायें ?", + "deleteText": "प्लेलिस्ट \nको हटायें", + "duplicateText": "प्लेलिस्ट कि \nछवि बनायें", + "editText": "प्लेलिस्ट को \nसंपादित करें", + "newText": "नयी \nप्लेलिस्ट", + "showTutorialText": "ट्युटोरियल दिखाएँ", + "shuffleGameOrderText": "गेम के क्रमांक को मिलाएं", + "titleText": "${TYPE} प्लेलिस्ट को अपने हिसाब से बदलें" + }, + "gameSettingsWindow": { + "addGameText": "गेम जोड़ें" + }, + "gamesToText": "${WINCOUNT} जीते और ${LOSECOUNT} हारे", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "ध्यान रखें: किसी भी यंत्र पे एक से ज्यादा खिलाड़ी हो सकते हैं \nअगर आपके पास पर्याप्त नियंत्रक हैं", + "aboutDescriptionText": "पार्टी इकट्ठा करने के लिए इन टैब्स का प्रयोग करें | \n\nपार्टी में आप गेम व \nप्रतियोगिता अपने दोस्तों के साथ \nअलग यंत्रो पे भी खेल सकते हैं | \n\nऊपरी-दायें हाथ कोने में उपस्थित ${PARTY} बटन का प्रयोग करके \nआप अपनी पार्टी से बात कर सकते हैं | (नियंत्रक पे ${BUTTON} दबाएँ मेनू में) ", + "aboutText": "इसके बारे में", + "addressFetchErrorText": "<पता पाने में त्रुटी>", + "appInviteInfoText": "दोस्तों को बोम्ब-स्क्वाड खेलने के लिए आमंत्रित करें\nऔर आपको ${COUNT} टिकेट मुफ्त मिलेंगे | हर\nदोस्त के आपको ${YOU_COUNT} टिकेट मिलेंगे |", + "appInviteMessageText": "${NAME} ने आपको ${COUNT} टिकेट ${APP_NAME} में भेजे हैं", + "appInviteSendACodeText": "एक कोड भेजें", + "appInviteTitleText": "${APP_NAME} एप्लीकेशन का आमंत्रण", + "bluetoothAndroidSupportText": "(किसी भी ब्लूटूथ वाले एंड्राइड यंत्र पे चलता है)", + "bluetoothDescriptionText": "किसी पार्टी के मेज़बान बनें या किसी पार्टी में जुडें ब्लूटूथ से", + "bluetoothHostText": "मेज़बान बनें ब्लूटूथ पर", + "bluetoothJoinText": "ब्लूटूथ से जुडें", + "bluetoothText": "ब्लूटूथ", + "checkingText": "जांचा जा रहा है...", + "dedicatedServerInfoText": "श्रेष्ठ परिणामों के लिए, एक समर्पित सर्वर सेट करें. कैसे जानने के लिए bombsquadgame.com/server देखें.", + "disconnectClientsText": "यह आपके पार्टी में ${COUNT} \nखिलाड़ियों का सम्बन्ध तोड़ देगा", + "earnTicketsForRecommendingAmountText": "यदि वे गेम को आज़माते हैं तो दोस्तों को ${COUNT} \nटिकट मिलेंगे (और आप प्रत्येक के लिए ${YOU_COUNT} प्राप्त करेंगे)", + "earnTicketsForRecommendingText": "मुफ्त के टिकेट \nके लिए गेम को बाटें...", + "emailItText": "मेल करें", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} टिकेट मिले ${NAME} से", + "friendPromoCodeAwardText": "आपको ${COUNT} टिकेट मिलेंगे हर बार इसका प्रयोग किया जाता है", + "friendPromoCodeExpireText": "आपका कोड सिर्क नयें खिलाड़ियों के लिए चलेगा और ${EXPIRE_HOURS} घंटों के बाद काम करना बंद कर देगा", + "friendPromoCodeInstructionsText": "इसका प्रयोग करने के लिए, ${APP_NAME} खोले और फिर \"सेटिंग->उन्नत सेटिंग->कोड डालें\" में जाएँ \nअधिक जानकारी के लिए bombsquadgame.com व डाउनलोड करने के लिए पे जाएँ |", + "friendPromoCodeRedeemLongText": "यह ${COUNT} मुफ्त के टिकेट के लिए प्रयोग किया जा सकता है अधकतम  ${MAX_USES} लोगों के द्वारा |", + "friendPromoCodeRedeemShortText": "यह ${COUNT} मुफ्त के टिकेट के लिए प्रयोग किया जा सकता है गेम के अन्दर |", + "friendPromoCodeWhereToEnterText": "(\"सेटिंग->उन्नत सेटिंग->कोड डालें\" में जाएँ )", + "getFriendInviteCodeText": "दोस्त आमंत्रण कोड पायें", + "googlePlayDescriptionText": "गूगल प्ले के खिलाड़ियों को अपनी पार्टी में आमंत्रण दें:", + "googlePlayInviteText": "आमंत्रण दें", + "googlePlayReInviteText": "आपकी पार्टी में ${COUNT} गूगल प्ले के खिलाड़ी हैं \nजिनका सम्बन्ध टूट जाएगा अगर आप नया आमंत्रण शुरू करते हैं |\n उनको नए आमंत्रण में जोड़ें फिर से पार्टी में जोड़ने के लिए |", + "googlePlaySeeInvitesText": "आमंत्रण देखें", + "googlePlayText": "गूगल प्ले", + "googlePlayVersionOnlyText": "(एंड्राइड / गूगल प्ले का संस्करण)", + "hostPublicPartyDescriptionText": "एक सार्वजनिक पार्टी की मेजबानी करें", + "inDevelopmentWarningText": "टिपण्णी: \n\nनेटवर्क पे खेल अभी नया है और अभी भी इसका विकास हो रहा है | \nअभी के लिए यह सलाह डी जाती है\n कि सभी खिलाड़ी एक ही वाई-फाई नेटवर्क पे हों |", + "internetText": "इंटरनेट", + "inviteAFriendText": "दोस्तों के पास यह गेम नहीं है ? उन्हें आमंत्रित करें खेलने के लिए | \nउन्हें ${COUNT} टिकेट मुफ्त मिलेंगे |", + "inviteFriendsText": "आमंत्रित करें", + "joinPublicPartyDescriptionText": "सार्वजनिक दल से जुड़ें:", + "localNetworkDescriptionText": "अपने नेटवर्क के दल से जुड़ें:", + "localNetworkText": "स्थानिक नेटवर्क", + "makePartyPrivateText": "मेरा दल निजी करें", + "makePartyPublicText": "मेरा दल सार्वजनिक करें", + "manualAddressText": "पता", + "manualConnectText": "जुड़ें", + "manualDescriptionText": "पते के द्वारा किसी दल से जुडें", + "manualJoinableFromInternetText": "क्या आप इन्टरनेट से जुड़ सकते हैं ?:", + "manualJoinableNoWithAsteriskText": "नहीं*", + "manualJoinableYesText": "हाँ", + "manualRouterForwardingText": "*इसे ठीक करने के लिए अपने राऊटर के यु-डी-पी पोर्ट ${PORT} को अपने स्थानिक पते पर भेजने के अनुकूल करें", + "manualText": "खुद से", + "manualYourAddressFromInternetText": "आपका पता इन्टरनेट से:", + "manualYourLocalAddressText": "आपका स्थानीय पता:", + "noConnectionText": "<कोई कनेक्शन नहीं है>", + "otherVersionsText": "(कोई और संस्करण)", + "partyInviteAcceptText": "स्वीकार करें", + "partyInviteDeclineText": "अस्वीकार करें", + "partyInviteGooglePlayExtraText": "('गूगल-प्ले' टैब 'इकट्ठा' विंडो में देखें)", + "partyInviteIgnoreText": "नज़रंदाज़ करें", + "partyInviteText": "${NAME} ने आपको अपनी पार्टी में \nजुड़ने के लिए आमंत्रित करा है !", + "partyNameText": "दल का नाम", + "partySizeText": "पार्टी साइज", + "partyStatusCheckingText": "स्थिति की जांच ...", + "partyStatusJoinableText": "आपकी पार्टी इंटरनेट से अब जुड़ने योग्य है", + "partyStatusNoConnectionText": "सर्वर से संपर्क करने में असमर्थ", + "partyStatusNotJoinableText": "आपकी पार्टी इंटरनेट से जुड़ने योग्य नहीं है", + "partyStatusNotPublicText": "आपकी पार्टी सार्वजनिक नहीं है", + "pingText": "पिंग", + "portText": "पॉर्ट", + "requestingAPromoCodeText": "एक कोड के लिए अनुरोध किया जा रहा है...", + "sendDirectInvitesText": "सीधे आमंत्रण भेजें", + "shareThisCodeWithFriendsText": "इस कोड को अपने दोस्तों के साथ बाटें:", + "showMyAddressText": "मेरा पता दिखाएं", + "titleText": "इकट्ठा", + "wifiDirectDescriptionBottomText": "यदि सारे यंत्रों में 'वाई-फाई डायरेक्ट' का पैनल है \nतो यंत्र उससे आराम से एक दुसरे से जुड़ सकते हैं |\nजुड़ने के बाद आप 'स्थानीय नेटवर्क' टैब से पार्टी बना सकते है, सामान्य 'वाई-फाई' कि तरह | \n\n सबसे अच्छे परिणामों के लिए 'वाई-फाई डायरेक्ट' के मेज़बान को ही ${APP_NAME} पार्टी का मेज़बान होना चाहिए |", + "wifiDirectDescriptionTopText": "वाई-फाई डायरेक्ट' से एंड्राइड यंत्रों को बिना वाई-फाई नेटवर्क के भी जोड़ा जा सकता है | \nयह एंड्राइड ४.२ और ओसके बाद के सभी संस्करणों के साथ सबसे अच्छा चलता है | \n\nइसका प्रयोग करने के लिए वाई-फाई सेटिंग खोलें और फिर 'वाई-फाई डायरेक्ट' मेनू में ढूंढे |", + "wifiDirectOpenWiFiSettingsText": "वाई-फाई सेटिंग खोलें", + "wifiDirectText": "वाई-फाई डायरेक्ट", + "worksBetweenAllPlatformsText": "(हर प्लेटफार्म के बीच काम करता है)", + "worksWithGooglePlayDevicesText": "(गूगल प्ले का गेम का संस्करण चलने वाले यंत्रों पे चलता है)", + "youHaveBeenSentAPromoCodeText": "आपको बोम्बस्क्वॉड का प्रोत्साहक सन्देश भेजा गया है:" + }, + "getTicketsWindow": { + "freeText": "मुफ्त !!", + "freeTicketsText": "मुफ्त टिकेट", + "inProgressText": "एक सौदा अभी चल रहा है; कृपया थोड़ी देर बाद प्रयास करें", + "purchasesRestoredText": "खरीदारी वापस ला दी गयी है", + "receivedTicketsText": "${COUNT} टिकेट मिले !!", + "restorePurchasesText": "खरीदारी वापस लायें", + "ticketDoublerText": "टिकेट को दुगना करें", + "ticketPack1Text": "छोटा टिकेट का संग्रह", + "ticketPack2Text": "ठीक-ठाक टिकेट का संग्रह", + "ticketPack3Text": "बड़ा टिकेट का संग्रह", + "ticketPack4Text": "बहुत बड़ा टिकेट का संग्रह", + "ticketPack5Text": "महान टिकेट संग्रह", + "ticketPack6Text": "महाकाय टिकेट संग्रह", + "ticketsFromASponsorText": "किसी प्रायोजक \nसे ${COUNT} पायें", + "ticketsText": "${COUNT} टिकेट", + "titleText": "टिकेट पायें", + "unavailableLinkAccountText": "माफ़ करें इस प्लेटफार्म पर खरीदारी नहीं कि जा सकती है |\nएक वैकल्पिक हल के रूप में आप इस खाते को किसी \nऔर प्लेटफार्म के खाते से जोड़ कर खरीदारी कर सकते हैं |", + "unavailableTemporarilyText": "यह अभी उपलब्ध नहीं है; कृपया थोड़ी देर बाद पुनः प्रयास करें |", + "unavailableText": "मांफ करें, यह अभी उपलब्ध नहीं है |", + "versionTooOldText": "माफ़ कारें यह गेम का संस्करण बहुत ही पुराना है; कृपया नया संस्करण डालें", + "youHaveShortText": "आपके पास ${COUNT} हैं", + "youHaveText": "आपके पास ${COUNT} टिकेट हैं" + }, + "googleMultiplayerDiscontinuedText": "क्षमा करें, गूगल की एक साथ खेलने की सेवा अब उपलब्ध नहीं है। \nमैं जितनी जल्दी हो सके एक प्रतिस्थापन पर काम कर रहा हूं।\nतब तक, कृपया दूसरी जुडने की विधि आज़माएँ। \n-Eric", + "googlePlayText": "गूगल प्ले", + "graphicsSettingsWindow": { + "alwaysText": "हमेशा", + "fullScreenCmdText": "संपूर्ण स्क्रीन में चलायें (सी-एम्-डी + ऍफ़)", + "fullScreenCtrlText": "संपूर्ण स्क्रीन में चलायें (सी-टी-आर-एल + ऍफ़)", + "gammaText": "चमक", + "highText": "ज्यादा", + "higherText": "और ज्यादा", + "lowText": "कम", + "mediumText": "ठीक ठाक", + "neverText": "कभी नहीं", + "resolutionText": "रेज़ोल्यूशन", + "showFPSText": "ऍफ़-पी-एस दिखाएँ", + "texturesText": "बनावटें", + "titleText": "ग्राफ़िक्स", + "tvBorderText": "टी-वि कि सीमा", + "verticalSyncText": "लंबरूप समकालीकरण", + "visualsText": "दृश्य" + }, + "helpWindow": { + "bombInfoText": "- बम - \nमुक्कों से ज्यादा शक्तिशाली परंतू \nखुद को भी नुक्सान पहुंचा सकते हैं | \nसबसे अच्छे परिणामों के लिए दुश्मन \nकि तरफ फूटने से पहले फेंके |", + "canHelpText": "${APP_NAME} मदद कर सकता है |", + "controllersInfoText": "आप नेटवर्क पे दोस्तों के साथ ${APP_NAME} खेल सकते हैं, या आप एक ही यंत्र पे भी खेल सकते हैं \nअगर आपके पास पर्याप्त नियंत्रक हैं | \n${APP_NAME} विविध नियंत्रकों को चला सकता है; \nआप फ़ोन का भी नियंत्रक के रूप में प्रयोग कर सकते है मुफ्त कि \n'${REMOTE_APP_NAME}' एप्लीकेशन द्वारा | अधिक जानकारी के लिए सेटिंग->नियंत्रक देखें |", + "controllersText": "नियंत्रक", + "controlsSubtitleText": "आपका ${APP_NAME} पात्र कुछ बुनियादी कार्य कर सकता है", + "controlsText": "नियंत्रण", + "devicesInfoText": "${APP_NAME} गेम का वी-आर संस्करण आम संस्करण के \nसाथ नेटवर्क पे खेला जा सकता है, \nतो निकलित्ये अपने फ़ोन और टेबलेट और खेलने लग जाइए !\nएक आम संस्करण को वी-आर संस्करण से भी जोड़ सकते हैं \nताकि आपके दुसरे दोस्त भी देखके मज़ा ले सकें |", + "devicesText": "यंत्र", + "friendsGoodText": "इनका होना नहुत अच्छा है | ${APP_NAME} को कई खिलाड़ियों (अधिकतम ८ खिलाड़ी) \nके साथ खेलने में मज़ा आता है |यह हमें पहुंचाता है:", + "friendsText": "दोस्त", + "jumpInfoText": "- कूदें - \nछोटे फासलें पार करने के लिए कूदें, \nचीजों को ऊँचा फेकने के लिए कूदें, \nऔर ख़ुशी ज़ाहिर करने के लिये कूदें !", + "orPunchingSomethingText": "या मुक्केबाजी के लिए, बहार फेकने के लिए और चिपकू बम्ब से उसे रास्ते में फोड़ने के लिए", + "pickUpInfoText": "- उठाना - \nझंडे, दुश्मन या कुछ भी उठायें \nजो जमीन से न जुड़ा हो फेकने \nके लिए फिर से दबाएँ", + "powerupBombDescriptionText": "एक बार में एक कि जगह तीन \nबम फेकने कि क्षमता दे देता है |", + "powerupBombNameText": "तीगुना बम", + "powerupCurseDescriptionText": "आप इनसे शायद बचना चाहेंगे,.. \n......या नहीं ??", + "powerupCurseNameText": "श्राप", + "powerupHealthDescriptionText": "आपके स्वास्थ्य को पूरा कर देती है | \nयह आपने सोचा भी न होगा |", + "powerupHealthNameText": "स्वास्थ्य का संकुल", + "powerupIceBombsDescriptionText": "साधारण बम से कमज़ोर \nमगर दुश्मन को जमा के \nबहुत नाज़ुक कर देते हैं |", + "powerupIceBombsNameText": "बर्फ़-बम", + "powerupImpactBombsDescriptionText": "साधारण बम से कमज़ोर \nमगर छुते ही फुट जाते हैं |", + "powerupImpactBombsNameText": "ट्रिगर-बम", + "powerupLandMinesDescriptionText": "यह एक साथ तिन मिलती हैं; \nइसका प्रयोग अपने अड्डे \nकि सुरक्षा के लिए व तेज़ दुश्मनों को रोकने के लिए करें |", + "powerupLandMinesNameText": "विस्फोटक थल बम", + "powerupPunchDescriptionText": "आपके मुक्कों को और दमदार, \nतेज़ और बेहतर करता है |", + "powerupPunchNameText": "मुक्केबाज़ी के दस्ताने", + "powerupShieldDescriptionText": "थोडा सा नुक्सान \nअपने में ले लेता है |", + "powerupShieldNameText": "सुरक्षा कवच", + "powerupStickyBombsDescriptionText": "जिस भी चीज़ को छुएँगे उससे चिपक जायेंगे .. \nइसका मज़ा लें !", + "powerupStickyBombsNameText": "चिपकू-बम", + "powerupsSubtitleText": "बेशक कोई भी गेम शक्तिप्रापकों के बिना पूरा नहीं है;", + "powerupsText": "शक्तिप्रापक", + "punchInfoText": "- मुक्के - \nमुक्के ज्यादा नुक्सान करते हैं। \nजितनी तेजी से मुक्के चलेंगे उतने ज़्यादा नुकसान होंगे। \nअतः पागल जैसे दौड़ो और घूमो।", + "runInfoText": "- दोडें - \nदोड़ने के लिए किसी भी बटन को दबाये रखें | \nदोड़ने से आप जल्दी पहुँच सकते हैं लेकिन मुड़ने में दिक्कत हो सकती है, इसलिए किनारों पे सावधान रहें |", + "someDaysText": "कुछ दिन आपका सिर्फ मुक्के मारने का या कुछ फोड़ने का मन करता है", + "titleText": "${APP_NAME} सहायता", + "toGetTheMostText": "इस गेम से सबसे ज्यादा पाने के लिए आपको इनकी आवश्यकता होगी:", + "welcomeText": "${APP_NAME} में आपका स्वागत है !" + }, + "holdAnyButtonText": "<कोई भी बटन दबाएँ रखें>", + "holdAnyKeyText": "<कोई भी की दबाएँ रखें>", + "hostIsNavigatingMenusText": "- ${HOST} मेनू में सरदार कि तरह घूम रहा है -", + "importPlaylistCodeInstructionsText": "इस प्लेलिस्ट को कहीं और आयात करने के लिए निम्न कोड का उपयोग करें:", + "importPlaylistSuccessText": "आयातित ${TYPE} प्लेलिस्ट '${NAME}'", + "importText": "आयात करें", + "importingText": "आयात कर रहा है...", + "inGameClippedNameText": "खेल में होगा \n\"${NAME}\"", + "installDiskSpaceErrorText": "त्रुटी: इनस्टॉल ख़तम नहीं कर पाया | \nआपके यंत्र पे जगह ख़तम हो सकती है | \nजगह खली कर के फिर से प्रयास करें |", + "internal": { + "arrowsToExitListText": "सूचि से बाहर निकलने केलिए ${LEFT} या ${RIGHT} दबाएँ", + "buttonText": "बुतों", + "cantKickHostError": "आप होस्ट को किक नहीं कर सकते।", + "chatBlockedText": "${NAME} ${TIME} सेकंड के लिए अवरुद्ध चैट है।", + "connectedToGameText": "${NAME}' में शामिल हो गए", + "connectedToPartyText": "${NAME} कि पार्टी में जुड़ गए हैं !", + "connectingToPartyText": "जुड़ रहे हैं ...", + "connectionFailedHostAlreadyInPartyText": "जुड़ना असफल रहा; मेज़बान किसी और पार्टी में है |", + "connectionFailedPartyFullText": "कनेक्शन विफल; पार्टी भरी हुई है।", + "connectionFailedText": "जुड़ना असफल रहा |", + "connectionFailedVersionMismatchText": "जुड़ना असफल रहा; मेज़बान गेम का अलग संस्करण चला रहा है | \nयह सुनिश्चित करें कि आप दोनों के पास एक ही संस्करण है और फिर प्रयास करें |", + "connectionRejectedText": "जुड़ने के अनुरोध को अस्वीकार कर दिया गया |", + "controllerConnectedText": "${CONTROLLER} जुड़ गया है |", + "controllerDetectedText": "1 नियंत्रक का पता चला है |", + "controllerDisconnectedText": "${CONTROLLER} अब जुदा नहीं है |", + "controllerDisconnectedTryAgainText": "${CONTROLLER} अब जुदा नहीं है | पुनः जोड़ने का प्रयास करें |", + "controllerForMenusOnlyText": "इस नियंत्रक को खेलने के लिए इस्तेमाल नहीं किया जा सकता है; केवल मेनू नेविगेट करने के लिए।", + "controllerReconnectedText": "${CONTROLLER} पुनः जुड़ गया है |", + "controllersConnectedText": "${COUNT} नियंत्रक जुड़े हैं |", + "controllersDetectedText": "${COUNT} नियंत्रक का पता चला है |", + "controllersDisconnectedText": "${COUNT} नियंत्रक अब जुड़े नहीं है |", + "corruptFileText": "फाइलों में कुछ गड़बड़ी है | पुनः इनस्टॉल करने का प्रयास करें या ${EMAIL} पे मेल करें", + "errorPlayingMusicText": "${MUSIC} गाना चलने में दिक्कत", + "errorResettingAchievementsText": "ऑनलाइन उपलब्धियों को रिसेट करने में दिक्कत; कृपया थोड़ी देर बाद पुनः प्रयास करें |", + "hasMenuControlText": "${NAME} के पास मेनू का नियंत्रण है|", + "incompatibleNewerVersionHostText": "होस्ट खेल का एक नया संस्करण चल रहा है। \nनवीनतम संस्करण में अपडेट करें और पुनः प्रयास करें।", + "incompatibleVersionHostText": "मेज़बान गेम का अलग संस्करण चला रहा है | \nयह सुनिश्चित करें कि आप दोनों के पास एक ही संस्करण है और फिर प्रयास करें |", + "incompatibleVersionPlayerText": "${NAME} गेम का अलग संस्करण चला रहा है | \nयह सुनिश्चित करें कि आप दोनों के पास एक ही संस्करण है और फिर प्रयास करें |", + "invalidAddressErrorText": "त्रुटी: आमान्य पता |", + "invalidNameErrorText": "त्रुटि: गलत नाम", + "invalidPortErrorText": "त्रुटि: गलत पोर्ट।", + "invitationSentText": "आमंत्रण भेज दिया गया है |", + "invitationsSentText": "${COUNT} आमंत्रण भेज दिए गए हैं |", + "joinedPartyInstructionsText": "कोई आपकी पार्टी में जुड़ गया है | \nगेम शुरू करने के लिए 'खेलें' पे जाएँ |", + "keyboardText": "कीबोर्ड", + "kickIdlePlayersKickedText": "आलसी होने के कारण ${NAME} को बहार निकाल जा रहा है", + "kickIdlePlayersWarning1Text": "${NAME} को ${COUNT} सेकंड में निकल दिया जाएगा अगर अआलसी ही रहता है |", + "kickIdlePlayersWarning2Text": "(आप इसे सेटिंग -> उन्नत सेटिंग में जा के बंद कर सकते हैं)", + "leftGameText": "${NAME}' ने छोड़ दिया।", + "leftPartyText": "${NAME} कि पार्टी को छोड़ दिया है |", + "noMusicFilesInFolderText": "इस फोल्डर में कोई भी गाना नहीं है |", + "playerJoinedPartyText": "${NAME} पार्टी में जुड़ गया है !", + "playerLeftPartyText": "${NAME} ने पार्टी छोड़ दी है |", + "rejectingInviteAlreadyInPartyText": "आमंत्रण को अस्वीकार किया जा रहा है (पहले से ही पार्टी में हैं) |", + "serverRestartingText": "परिसेवक पुन: प्रारंभ हो रहा है। कृपया एक पल में फिर से जुड़ें...", + "serverShuttingDownText": "परिसेवक बंद हो रहा है...", + "signInErrorText": "साइन इन करने में त्रुटी", + "signInNoConnectionText": "साइन इन नहीं कर पा रहे | (इन्टरनेट कनेक्शन है ना ?)", + "telnetAccessDeniedText": "त्रुटी: उपयोगकर्ता ने टेलनेट कि अनुमति नहीं डी है |", + "timeOutText": "(समय ख़तम होने में ${TIME} सेकंड)", + "touchScreenJoinWarningText": "आपने टच स्क्रीन से जोड़ा है | \nअगर यह गलती से हुआ है तो 'मेनू -> गेम छोड़ें' से बाहर निकल जाएँ |", + "touchScreenText": "टच स्क्रीन", + "unableToResolveHostText": "त्रुटि: होस्ट को हल करने में असमर्थ हैं।", + "unavailableNoConnectionText": "यह अभी उपलब्ध नहीं है | (इन्टरनेट कनेक्शन है ना ?)", + "vrOrientationResetCardboardText": "वी-आर अनुस्थापन रिसेट करने के लिए इसका प्रयोग करें | \nगेम खेलने के लिए आपको एक बाहरी नियंत्रक चाहिए होगा |", + "vrOrientationResetText": "वी-आर अनुस्थापन रिसेट", + "willTimeOutText": "(समय ख़तम हो जायेगा अगर आलसी रहेंगे)" + }, + "jumpBoldText": "कूदें", + "jumpText": "कूदें", + "keepText": "रखें", + "keepTheseSettingsText": "इन सेटिंग को रखें ?", + "keyboardChangeInstructionsText": "कीबोर्ड को बदलने के लिए दो बार अन्तरक दबाएँ।", + "keyboardNoOthersAvailableText": "ओर कीबोर्ड उपलब्ध नहीं हैं।", + "keyboardSwitchText": "\"${NAME}\" से कीबोर्ड बदला जा रहा है।", + "kickOccurredText": "${NAME} को बाहर निकाल दिया गया।", + "kickQuestionText": "${NAME} को बाहर निकाल दें?", + "kickText": "बाहर निकाल दें", + "kickVoteCantKickAdminsText": "परिसेवक मालिक को निकाल नहीं सकते।", + "kickVoteCantKickSelfText": "खुद को निकाल नहीं कर सकते।", + "kickVoteFailedNotEnoughVotersText": "वोट के लिए पर्याप्त खिलाड़ी नहीं हैं।", + "kickVoteFailedText": "बाहर निकालने का वोट असफल रहा।", + "kickVoteStartedText": "${NAME} के लिए एक बाहर निकलने का वोट शुरू किया गया है।", + "kickVoteText": "बाहर निकालने के लिए वोट करें", + "kickVotingDisabledText": "निकालने का चुनाव बंद है।", + "kickWithChatText": "हां के लिए चैट में ${YES} टाइप करें और ना के लिए ${NO}।", + "killsTallyText": "${COUNT} हत्याएं", + "killsText": "हत्याएं", + "kioskWindow": { + "easyText": "आसान", + "epicModeText": "अद्भुत मोड", + "fullMenuText": "पूरा मेनू", + "hardText": "कठिन", + "mediumText": "मध्यम", + "singlePlayerExamplesText": "अकेले/दुकेले के अदाहरण", + "versusExamplesText": "विरुद्ध के उदहारण" + }, + "languageSetText": "भाषा अब \"${LANGUAGE}\" है |", + "lapNumberText": "${TOTAL} में से ${CURRENT} चक्कर", + "lastGamesText": "(आखिरी ${COUNT} गेम)", + "leaderboardsText": "लीडरबोर्ड", + "league": { + "allTimeText": "सबसे अच्छे शुरुआत से", + "currentSeasonText": "वर्तमान सीज़न (${NUMBER})", + "leagueFullText": "${NAME} संघ", + "leagueRankText": "संघ में पद", + "leagueText": "संघ", + "rankInLeagueText": "#${RANK}, ${NAME} संघ ${SUFFIX}", + "seasonEndedDaysAgoText": "सीज़न ख़तम हो गया ${NUMBER} दिन पहले |", + "seasonEndsDaysText": "सीज़न ख़तम होगा ${NUMBER} दिनों में |", + "seasonEndsHoursText": "सीज़न ख़तम होगा ${NUMBER} घंटों में |", + "seasonEndsMinutesText": "सीज़न ख़तम होगा ${NUMBER} मिनटों में |", + "seasonText": "सीज़न संख्या ${NUMBER}", + "tournamentLeagueText": "इस प्रतियोगिता में हिस्सा लेने के लिए आपको ${NAME} संघ तक पहले पहुंचना होगा", + "trophyCountsResetText": "ट्रॉफी कि संख्या अगले सीज़न में रिसेट हो जायेगी" + }, + "levelBestScoresText": "${LEVEL} पर सर्वश्रेष्ठ स्कोर", + "levelBestTimesText": "${LEVEL} पर सबसे अच्छे समय", + "levelFastestTimesText": "सबसे तेज़ ${LEVEL} स्तर पे", + "levelHighestScoresText": "सबसे जादा अंक ${LEVEL} स्तर पे", + "levelIsLockedText": "${LEVEL} अभी नहीं खेल सकते |", + "levelMustBeCompletedFirstText": "${LEVEL} को पहले पार करना होगा |", + "levelText": "स्तर ${NUMBER}", + "levelUnlockedText": "स्तर खुल गया है", + "livesBonusText": "प्राण का अधिलाभ", + "loadingText": "लोड हो रहा है", + "loadingTryAgainText": "लोड हो रहा है; एक क्षण में फिर से प्रयास करें...", + "macControllerSubsystemBothText": "दोनों (सिफारिश नहीं की गई)", + "macControllerSubsystemClassicText": "क्लासिक", + "macControllerSubsystemDescriptionText": "(यदि आपका नियंत्रक काम नहीं कर रहे हैं तो इसे बदलने का प्रयास करें)", + "macControllerSubsystemMFiNoteText": "आईओएस/मैक के लिए बना हुआ नियंत्रक का पता चला; \nआप सेटिंग -> नियंत्रकों में इन्हें सक्षम करना चाह सकते हैं", + "macControllerSubsystemMFiText": "आईओएस/मैक के लिए बना हुआ", + "macControllerSubsystemTitleText": "नियंत्रक समर्थन", + "mainMenu": { + "creditsText": "आभार सूचि", + "demoMenuText": "डेमो मेनू", + "endGameText": "गेम ख़तम करें", + "exitGameText": "गेम से बहार जाएँ", + "exitToMenuText": "ख़तम कर के मेनू में जाएं ?", + "howToPlayText": "कैसे खेलें", + "justPlayerText": "(केवल ${NAME})", + "leaveGameText": "गेम छोड़ें", + "leavePartyConfirmText": "सही में पार्टी छोड़ें ?", + "leavePartyText": "पार्टी छोड़ें", + "quitText": "बंद कर दें", + "resumeText": "पुनः शुरू करें", + "settingsText": "सेटिंग" + }, + "makeItSoText": "लागू करें", + "mapSelectGetMoreMapsText": "और नक़्शे पायें....", + "mapSelectText": "चुनें....", + "mapSelectTitleText": "${GAME} नक़्शे", + "mapText": "नक्शा", + "maxConnectionsText": "अधिकतम कनेक्शन", + "maxPartySizeText": "अधिकतम पार्टी का आकार", + "maxPlayersText": "अधिकतम खिलाड़ी", + "modeArcadeText": "आर्केड मोड", + "modeClassicText": "क्लासिक मोड", + "modeDemoText": "डेमो मोड", + "mostValuablePlayerText": "सबसे ज्यादा कीमती खिलाड़ी", + "mostViolatedPlayerText": "सबसे ज्यादा उल्लंघित खिलाड़ी", + "mostViolentPlayerText": "सबसे ज्यादा हिंसात्मक खिलाड़ी", + "moveText": "हिलें", + "multiKillText": "${COUNT}-हत्याएं!!!", + "multiPlayerCountText": "${COUNT} खिलाड़ी", + "mustInviteFriendsText": "टिपण्णी: आपको अपने दोस्तों को आमंत्रण \nभेजना होगा \"${GATHER}\" पैनल में, \nया दोस्तों के साथ खेलने के लिए नियंत्रक जोड़ें |", + "nameBetrayedText": "${NAME} ने ${VICTIM} को धोखा दिया |", + "nameDiedText": "${NAME} मर गया |", + "nameKilledText": "${NAME} ने ${VICTIM} को मार डाला |", + "nameNotEmptyText": "नाम खाली नहीं हो सकता है !", + "nameScoresText": "${NAME} को अंक मिला !", + "nameSuicideKidFriendlyText": "${NAME} गलती से मर गया |", + "nameSuicideText": "${NAME} ने आत्म-हत्या कर ली |", + "nameText": "नाम", + "nativeText": "मूलभूत", + "newPersonalBestText": "नया निजी उत्तम अंक !", + "newTestBuildAvailableText": "एक नया परीक्षा का संस्करण उपलब्ध है ! \n(${VERSION} बिल्ड ${BUILD}). इसे ${ADDRESS} से पायें |", + "newText": "नया", + "newVersionAvailableText": "${APP_NAME} का एक नया संस्करण उपलब्ध है ! (${VERSION})", + "nextAchievementsText": "अगली उपलब्धियां:", + "nextLevelText": "अगला स्तर", + "noAchievementsRemainingText": "- कुछ नहीं", + "noContinuesText": "(कोई जारी रखना नहीं)", + "noExternalStorageErrorText": "कोई बाहरी संचयन करने कि जगह नहीं मिली", + "noGameCircleText": "त्रुटी: गेम-सर्किल में लॉग-इन नहीं हैं |", + "noProfilesErrorText": "आपकी कोई खिलाड़ी पार्श्वचित्र नहीं है, इसलिए आप '${NAME}' नाम के साथ फंसे हैं |\nसेटिंग -> खिलाड़ी पार्श्वचित्र में जाके अपने लिए पार्श्वचित्र बनायें |", + "noScoresYetText": "अभी तक कोई स्कोर नहीं है |", + "noThanksText": "नहीं धन्यवाद", + "noTournamentsInTestBuildText": "चेतावनी: इस टेस्ट बिल्ड से खेलकूद-प्रतियोगिता के खेल के अंकों को नजरअंदाज कर दिया जाएगा।", + "noValidMapsErrorText": "इस गेम ढंग के लिए कोई नक्शा नहीं मिला", + "notEnoughPlayersRemainingText": "पर्याप्त खिलाड़ी बाकी नहीं हैं; बंद कर के नया गेम शुरू करें |", + "notEnoughPlayersText": "आपको कम से कम ${COUNT} खिलाड़ी चाहिए गेम शुरू करने के लिए !", + "notNowText": "अभी नहीं", + "notSignedInErrorText": "यह करने के लिए आपको अपने खाते में साइन इन करना पड़ेगा", + "notSignedInGooglePlayErrorText": "आपको गूगल प्ले से साइन इन करना पड़ेगा यह करने के लिए |", + "notSignedInText": "साइन इन नहीं हैं", + "nothingIsSelectedErrorText": "कुछ चुना नहीं है !", + "numberText": "#${NUMBER}", + "offText": "बंद करें", + "okText": "ठीक है", + "onText": "चालू करें", + "onslaughtRespawnText": "${PLAYER} फिर से जिन्दा होगा लहर ${WAVE} में", + "orText": "${A} या ${B}", + "otherText": "अन्य", + "outOfText": "(${ALL} में से #${RANK})", + "ownFlagAtYourBaseWarning": "आपका अपना झंडा अंक पाने के लिए आपके अड्डे पर होना चाहिए", + "packageModsEnabledErrorText": "स्थानिक मोड्स के साथ नेटवर्क पे खेलना वर्जित है (सेटिंग->उन्नत सेटिंग देखें)", + "partyWindow": { + "chatMessageText": "गपशप ख़त", + "emptyText": "आपकी पार्टी खाली है", + "hostText": "(मेज़बान)", + "sendText": "भेजें", + "titleText": "आपकी पार्टी" + }, + "pausedByHostText": "(मेज़बान ने रोका है)", + "perfectWaveText": "निष्कलंक लहर !", + "pickUpText": "उठायें", + "playModes": { + "coopText": "एक साथ", + "freeForAllText": "सभी अकेले-अकेले", + "multiTeamText": "कई समूह", + "singlePlayerCoopText": "अकेले/ एक साथ", + "teamsText": "समूह" + }, + "playText": "खेलें", + "playWindow": { + "oneToFourPlayersText": "१-४ खिलाड़ी", + "titleText": "खेलें", + "twoToEightPlayersText": "२-८ खिलाड़ी" + }, + "playerCountAbbreviatedText": "${COUNT} व्यक्ति", + "playerDelayedJoinText": "${PLAYER} अगले दौर कि शुरुआत में आएगा।", + "playerInfoText": "खिलाड़ी कि जानकारी", + "playerLeftText": "${PLAYER} ने गेम छोड़ दिया है", + "playerLimitReachedText": "खिलाड़ी कि अधिकतम संख्या ${COUNT} तक पहुँच गयी है; कोई और अब नहीं जुड़ सकता है |", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "आप अपने खाते के पार्श्वचित्र को हटा नहीं सकते |", + "deleteButtonText": "पार्श्वचित्र \nहटायें", + "deleteConfirmText": "${PROFILE}' हटायें?", + "editButtonText": "पार्श्वचित्र को \nसंपादित करें", + "explanationText": "(इस खाते के लिए कस्टम प्लेयर नाम और वेश-भूषा)", + "newButtonText": "नया \nपार्श्वचित्र", + "titleText": "खिलाड़ी के पार्श्वचित्र" + }, + "playerText": "खिलाड़ी", + "playlistNoValidGamesErrorText": "इस प्लेलिस्ट में कोई खुला हुआ गेम नहीं है |", + "playlistNotFoundText": "प्लेलिस्ट नहीं मिली", + "playlistsText": "प्लेलिस्ट", + "pleaseRateText": "अगर आपको ${APP_NAME} में मज़ा आ रहा है, \nतो एक क्षण ले कर इसका मूल्यांकन करें | \nयह इस गेम के आगे के विकास का एक बहुत अहम् अंश है | \n\nधन्यवाद !\n-एरिक", + "pleaseWaitText": "कृपया प्रतीक्षा करें...", + "pluginsDetectedText": "नए प्लगइन्स पता चले। उन्हें सेटिंग्स में चालू / कॉन्फ़िगर करें।", + "pluginsText": "प्लगइन्स", + "practiceText": "अभ्यास", + "pressAnyButtonPlayAgainText": "दोबारा खेलने के लिए कोई भी बटन दबाएँ...", + "pressAnyButtonText": "जारी रखने के लिए कोई भी बटन दबाएँ...", + "pressAnyButtonToJoinText": "भाग लेने के लिए कोई भी बटन दबाएँ...", + "pressAnyKeyButtonPlayAgainText": "दोबारा खेलने के लिए कोई भी की / बटन दबाएँ...", + "pressAnyKeyButtonText": "जारी रखने के लिए कोई भी की / बटन दबाएँ...", + "pressAnyKeyText": "कोई भी की दबाएँ...", + "pressJumpToFlyText": "** उड़ने के लिए कूदने वाला बटन बार बार दबाएँ **", + "pressPunchToJoinText": "जुड़ने के लिए मुक्के वाला बटन दबाएँ...", + "pressToOverrideCharacterText": "${BUTTONS} दबाएँ अपना चरित्र बदलने के लिए", + "pressToSelectProfileText": "खिलाड़ी चुनने के लिए ${BUTTONS} दबाएं", + "pressToSelectTeamText": "एक समूह चुनने के लिए ${BUTTONS} दबाएं", + "promoCodeWindow": { + "codeText": "कोड", + "codeTextDescription": "प्रोमो कोड", + "enterText": "घुसें" + }, + "promoSubmitErrorText": "कोड जमा करने में त्रुटि: अपना इंटरनेट कनेक्शन जांच लें।", + "ps3ControllersWindow": { + "macInstructionsText": "अपने पी.एस.३ के पीछे कि बिजली को काटें, \nयह सुनिश्चित करें कि ब्लूटूथ खुला है आपके मैक पे और \nफिर अपने नियंत्रक को अपने मैक से जोड़ें एक यु.एस.बी तार से | \nइसके बाद से आप अपने नियंत्रक के होम बटन से मैक से जुड़ सकते है दोनों तार के साथ और \nबिना तार के साथ भी | कुछ मैक पे आपको पास्कोद के लिए अनुरोध किया जा सकता है | \n\n\n\nअगर यह होता है तो सहायता के \nलिए टुटोरिअल देखें या गूगल करें | \n\n\nपी.एस.३ के नियंत्रक जो बिना तार के जुड़े हैं वोह यंत्रों कि सूचि में दिखने चाहिए | \nआपको उन्हें हटाना पड़ सकता है उस सूचि से जब \nआपको उनका पुनः पी.एस.३ के साथ प्रयोग करना हो | \n\nऔर यह भी ध्यान रखें कि जब वोह प्रयोग में ना हों तो \nवोह जुड़े ना हों अन्यथा उनकी बैटरी ख़तम होती रहेगी | \n\nब्लूटूथ पे अधिकतम ७ यंत्र चल जाने चाहिए, \nहालाकि उनमें थोड़ी देरी आ सकती है |", + "ouyaInstructionsText": "पी.एस.३ नियंत्रक को ऊया के सस्थ चलने के लिए सिर्फ उसे एक बार यु.एस.बी तार से जोड़ दें |\nयह करने पे आपके बाकी नियंत्रकों का जोड़ टूट सकता है इसलिए आपको अपने ऊया को पुनः शुरू\nकर के यु.एस.बी तार निकाल देना चाहिए | \n\nइसके बाद से आप नियंत्रक का 'होम' बटन दबा के बिना तार के जुड़ सकते हैं | \nजब आपका खेलना ख़तम हो जाए तो \n'होम' बटन १० सेकंड तक दबा के \nनियंत्रक को बंद कर दें अन्यथा वोह बैटरी ख़तम कर सकता है |", + "pairingTutorialText": "शैक्षणिक वीडियो जोड़ा जा रहा है", + "titleText": "${APP_NAME} के साथ पी.एस.३ नियंत्रक का प्रयोग किया जा रहा है:" + }, + "punchBoldText": "मुक्का", + "punchText": "मुक्का", + "purchaseForText": "${PRICE} के भाव पे खरीदें", + "purchaseGameText": "गेम खरीदें", + "purchasingText": "खरीद रहे हैं...", + "quitGameText": "${APP_NAME} को बंद कर दें ?", + "quittingIn5SecondsText": "५ सेकंड में बंद कर रहे हैं...", + "randomPlayerNamesText": "किशोर, यश, ख्याति, शालिनी, आदित्य, उमेस, निश्चिंत, भाविक, रिशव, तुषार, राहुल, अमोल, मिशेल, प्रियंका, कल्याण, रियान, दिविज, हरी, आदर्श, कौस्तुभ, ज़ोया, सीनू, प्रतीक, ज़ारा, रुक्सार, शकील, पूजा, शबनम, शेरा, चेतन, समीर, टोनी, अजय, आकाश, पंकज, आरती, शबाना, मुमताज़, शुभम, शिवम्, लकेव, सचिन, दीपक, अक्षय, अर्जुन, किशन, राधा, विश्वनाथ, शालू, विमल, शिवा, पप्पू, नरेंद्र, आज़म, अनमोल, काजल, संध्या, दिनेश, प्रिंस, आनंद, अज़हर, पवन, अभिषेक, विवेक", + "randomText": "अनियमित", + "rankText": "पद", + "ratingText": "मूल्यांकन", + "reachWave2Text": "पद पाने के लिए पेहली लहर को पार करें", + "readyText": "तैयार", + "recentText": "नए", + "remoteAppInfoShortText": "${APP_NAME} सबसे मजेदार है जब परिवार और दोस्तों के साथ खेला जाता है। \nएक या अधिक हार्डवेयर नियंत्रक से कनेक्ट करें \nया स्थापित करें ${REMOTE_APP_NAME} एप्लिकेशन उन्हें फोन या \nटेबलेट पर उपयोग करने के लिए नियंत्रकों के रूप में।", + "remote_app": { + "app_name": "बोम्ब-स्क्वाड दूरस्थ", + "app_name_short": "बी.एस दूरस्थ", + "button_position": "बटन कि जगह", + "button_size": "बटन का माप", + "cant_resolve_host": "मेज़बान से जुड़ नहीं पा रहे...", + "capturing": "पकड़ा जा रहा है...", + "connected": "जुड़ गए हैं", + "description": "अपने फ़ोन या टेबलेट का नियंत्रिक के रूप में बोम्ब-स्क्वाड के लिए प्रयोग करें | \nअधिकतम ८ यंत्र जुड़ सकते है एक साथ एक ही टेबलेट या दूरदर्शन में अपना अलग अलग खेलने के लिए |", + "disconnected": "परिसेवक से संपर्क टूट गया है", + "dpad_fixed": "स्थायी", + "dpad_floating": "हिलता हुआ", + "dpad_position": "हिलने के बटन कि जगह", + "dpad_size": "हिलने के बटन का माप", + "dpad_type": "हिलने के बटन का प्ररूप", + "enter_an_address": "पता डालें", + "game_full": "यह गेम भरा हुआ है और संपर्क नहीं स्वीकार कर रहा है |", + "game_shut_down": "गेम बंद हो गया है |", + "hardware_buttons": "हार्डवेयर बटन", + "join_by_address": "पते से जुडें...", + "lag": "देरी: ${SECONDS} सेकंड", + "reset": "पहले से प्रस्थापित सेटिंग्स को लागू करें", + "run1": "दोडें १", + "run2": "दोडें २", + "searching": "बोम्ब-स्क्वाड के गेम को ढूँढा जा रहा है", + "searching_caption": "जुड़ने के लिए किसी गेम के नाम को दबाएँ | \nये ध्यान रखे कि आप भी उसी नेटवर्क पे हों जिस पे गेम चल रहा है |", + "start": "शुरू करें", + "version_mismatch": "संस्करण मेल नहीं खा रहे हैं | \nयह सुनिश्चित कर लें कि बोम्ब-स्क्वाड गेम और \nरिमोट पर नवीनतम संस्करण है और पुनः प्रयास करें |" + }, + "removeInGameAdsText": "${PRO} को भंडार में खरीदें गेम के बीच आने वाले एड्स को हटाने के लिए |", + "renameText": "नाम बदलें", + "replayEndText": "रीप्ले बंद करें", + "replayNameDefaultText": "आखिरी गेम का रीप्ले", + "replayReadErrorText": "रीप्ले फाइल को पढने में त्रुटी", + "replayRenameWarningText": "अगर आप \"${REPLAY}\" को रखना चाहते हैं तो उसका नाम बदल दें वरना उसे अगली बार मिटा दिया जाएगा |", + "replayVersionErrorText": "माफ़ करें, यह रीप्ले किसी और संस्करण में बना था \nइसलिए इसका प्रयोग नहीं किया जा सकता है", + "replayWatchText": "रीप्ले देखें", + "replayWriteErrorText": "रीप्ले को फाइल में लिखने में त्रुटी |", + "replaysText": "रीप्ले", + "reportPlayerExplanationText": "इस ईमेल का प्रयोग करके धोकाधड़ी या अनुचित शब्दों के प्रयोग या \nकिसी और चीज़ कि आख्या दें | कृपया निचे विवरण दीजिये :", + "reportThisPlayerCheatingText": "धोकाधड़ी", + "reportThisPlayerLanguageText": "अनुचित शब्दों का प्रयोग", + "reportThisPlayerReasonText": "आप इस खिलाडी कि आख्या करना चाहते हैं ?", + "reportThisPlayerText": "खिलाडी कि आख्या करें", + "requestingText": "अनुरोध किया जा रहा है...", + "restartText": "पुनः शुरू करें", + "retryText": "पुनः प्रयास करें", + "revertText": "पूर्व स्तिथि में लायें", + "runText": "दोडें", + "saveText": "सुरक्षित करें", + "scanScriptsErrorText": "स्क्रिप्ट्स को स्कैन करने में त्रुटि; विवरण के लिए लॉग देखें।", + "scoreChallengesText": "अंकों कि चुनौतियाँ", + "scoreListUnavailableText": "अंकों कि सूचि अभी उपस्थित नहीं है", + "scoreText": "अंक", + "scoreUnits": { + "millisecondsText": "मिलीसेकेंड", + "pointsText": "अंक", + "secondsText": "सेकंड" + }, + "scoreWasText": "(${COUNT} था)", + "selectText": "चुनें", + "seriesWinLine1PlayerText": "विजयी", + "seriesWinLine1TeamText": "विजयी", + "seriesWinLine1Text": "विजयी", + "seriesWinLine2Text": "शृंखला !", + "settingsWindow": { + "accountText": "खाता", + "advancedText": "उन्नत", + "audioText": "ध्वनि", + "controllersText": "नियंत्रक", + "graphicsText": "ग्राफिक्स", + "playerProfilesMovedText": "टिपण्णी: खिलाडी के पार्श्वचित्र को मुख्या मेनू में खिसका दिया गया है |", + "titleText": "सेटिंग" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(एक साधारण, नियंत्रक से आसानी इ प्रयोग हो जाने वाला कीबोर्ड)", + "alwaysUseInternalKeyboardText": "हमेशा गेम के कीबोर्ड का प्रयोग करें", + "benchmarksText": "बेंचमार्क व तनाव परीक्षण", + "disableCameraGyroscopeMotionText": "कैमरा गायरोस्कोप गतिवान बंद करे", + "disableCameraShakeText": "कैमरा की हलचल बंद करे", + "disableThisNotice": "(आप उन्नत सेटिंग में इस नोटिस को अक्षम कर सकते हैं)", + "enablePackageModsDescriptionText": "(अतिरिक्त परिवर्तन करने कि क्षमताएं देता है, लेकिन फिर आप नेट पर नहीं खेल सकते हैं)", + "enablePackageModsText": "स्थानीय परिवर्तनों को लागू करें", + "enterPromoCodeText": "कोड डालें", + "forTestingText": "टिपण्णी: यह संख्याएं सिर्फ परीक्षण के लिए हैं, और एप्लीकेशन के बंद होने पर खो जायेंगी |", + "helpTranslateText": "${APP_NAME} के अनुवाद एक सामुदायिक प्रयास का परिणाम हैं | \nअगर आप इसमें अपना योगदान देना चाहते हैं \nया कुछ ठीक करना चाहते हैं तो निचे दी गयी लिंक को देखें | धन्यवाद !", + "kickIdlePlayersText": "निष्क्रिय खिलाडियों को बाहर निकाल दें", + "kidFriendlyModeText": "बच्चों के अनुकूल करें (कम हिंसा, आदि)", + "languageText": "भाषा", + "moddingGuideText": "परिवर्तन करने कि गाइड", + "mustRestartText": "इसके लागू होने के लिए आपको गेम को पुनः शुरू करना पड़ेगा |", + "netTestingText": "नेटवर्क पर परीक्षण", + "resetText": "रीसेट", + "showBombTrajectoriesText": "बोम्ब का पथ दिखाएँ", + "showPlayerNamesText": "खिलाड़ी का नाम दिखाएँ", + "showUserModsText": "परिवर्तनों का फोल्डर दिखाएँ", + "titleText": "उन्नत", + "translationEditorButtonText": "${APP_NAME} अनुवाद संपादक", + "translationFetchErrorText": "अनुवाद कि स्तिथि अभी उपलब्ध नहीं है", + "translationFetchingStatusText": "अनुवाद कि स्तिथि का पता करा जा रहा है...", + "translationInformMe": "मुझे सूचित करें जब मेरी भाषा में अपडेट की आवश्यकता होती है", + "translationNoUpdateNeededText": "यह भाषा अद्यतन है !!", + "translationUpdateNeededText": "** इस भाषा का अद्यतन करने कि जरूरत है !! **", + "vrTestingText": "वी-आर परीक्षण" + }, + "shareText": "शेयर", + "sharingText": "शेयर कर रहे हैं...", + "showText": "दिखाएँ", + "signInForPromoCodeText": "कोड प्रभावी होने के लिए आपको किसी खाते में साइन इन करना होगा।", + "signInWithGameCenterText": "गेम केंद्र खाते का उपयोग करने के लिए, \nगेम सेंटर एप के साथ साइन इन करें।", + "singleGamePlaylistNameText": "केवल ${GAME}", + "singlePlayerCountText": "1 खिलाड़ी", + "soloNameFilterText": "अकेले ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "कैरेक्टर का चयन", + "Chosen One": "चुना हुआ", + "Epic": "उत्कृष्ट प्रकार के खेल", + "Epic Race": "उत्कृष्ट दौड़", + "FlagCatcher": "झंडे पर कब्जा", + "Flying": "सुखद कल्पना", + "Football": "फ़ुटबॉल", + "ForwardMarch": "आक्रमण", + "GrandRomp": "जय", + "Hockey": "हॉकी", + "Keep Away": "दूर रहो", + "Marching": "रनअराउंड़", + "Menu": "मुख्य मेनू", + "Onslaught": "हमला", + "Race": "दौड़", + "Scary": "पहाड़ी का राजा", + "Scores": "स्कोर स्क्रीन", + "Survival": "निकाल देना", + "ToTheDeath": "मौत का खेल", + "Victory": "अंतिम स्कोर स्क्रीन" + }, + "spaceKeyText": "स्पेस", + "statsText": "स्टैट्‍स", + "storagePermissionAccessText": "इसमें संग्रहण एक्सेस की आवश्यकता है", + "store": { + "alreadyOwnText": "आपके पास ${NAME} पहले से ही है!", + "bombSquadProNameText": "${APP_NAME} प्रो", + "bombSquadProNewDescriptionText": "• इन-गेम विज्ञापनों और नैग-स्क्रीन को निकाल देता है \n• अधिक गेम सेटिंग्स को खोलता है \n• यें भी शामिल है:", + "buyText": "खरीदें", + "charactersText": "पात्र", + "comingSoonText": "जल्द आ रहा है...", + "extrasText": "अतिरिक्त", + "freeBombSquadProText": "बम्ब्सक्वाड अब नि:शुल्क है, लेकिन जब से आपने मूल रूप से इसे खरीदा था \nतब आप हैं बम्ब्सक्वाड प्रो उन्नयन और ${COUNT} टिकट आपको धन्यवाद के रूप में दिए जा रहे हैं। \nनई सुविधाओं का आनंद लें, और अपने समर्थन के लिए धन्यवाद! \n-एरिक", + "holidaySpecialText": "छुट्टी विशेष", + "howToSwitchCharactersText": "(पात्र असाइन और कस्टमाइज़ करने के लिए जाएं \"${SETTINGS} -> ${PLAYER_PROFILES}\" पर)", + "howToUseIconsText": "(इन का उपयोग करने के लिए वैश्विक खिलाड़ी प्रोफाइल (खाता विंडो में) बनाएं)", + "howToUseMapsText": "(इन नक्शों का उपयोग अपनी खुद की टीमों/फ्री-फाॅर-आॅल प्लेलिस्टों में करें)", + "iconsText": "आइकनस", + "loadErrorText": "पृष्ठ लोड करने में असमर्थ। \nअपना इंटरनेट संपर्क जांचे।", + "loadingText": "लोड हो रहा है", + "mapsText": "नक्शे", + "miniGamesText": "छोटे खेल", + "oneTimeOnlyText": "(सिर्फ़ एक बार)", + "purchaseAlreadyInProgressText": "इस आइटम की खरीद पहले से ही प्रगति पर है।", + "purchaseConfirmText": "${ITEM} खरीदें?", + "purchaseNotValidError": "खरीद मान्य नहीं है। \nअगर यह एक त्रुटि है तो ${EMAIL} से संपर्क करें।", + "purchaseText": "खरीदें", + "saleBundleText": "बंडल बिक्री!", + "saleExclaimText": "छूट!", + "salePercentText": "(${PERCENT}% छूट)", + "saleText": "छूट", + "searchText": "खोजें", + "teamsFreeForAllGamesText": "टीमों / फ्री-फाॅर-आॅल खेल", + "totalWorthText": "*** ${TOTAL_WORTH} मूल्य! ***", + "upgradeQuestionText": "उन्नयन करें?", + "winterSpecialText": "शीतकालीन विशेष", + "youOwnThisText": "- आप इसके मालिक हो -" + }, + "storeDescriptionText": "8 खिलाड़ी पार्टी खेल पागलपन! \n\nकैप्चर-द-फ्लैग, बॉम्बर-हॉकी, और एपिक-स्लो-मोशन-डेथ-मैच जैसे विस्फोटक मिनी-गेम्स के टूर्नामेंट में अपने दोस्तों (या कंप्यूटर) को उड़ाएं! \n\nसरल नियंत्रण और व्यापक नियंत्रक समर्थन कार्रवाई पर 8 लोगों तक पहुंचना आसान बनाता है; आप अपने मोबाइल उपकरणों को मुफ्त 'BombSquad Remote' ऐप के माध्यम से नियंत्रकों के रूप में भी उपयोग कर सकते हैं! \n\nबॉम्ब्स अवे! \n\nअधिक जानकारी के लिए www.froemling.net/bombsquad देखें।", + "storeDescriptions": { + "blowUpYourFriendsText": "अपने दोस्तों को उड़ाएं।", + "competeInMiniGamesText": "रेसिंग से उड़ान तक लेकर मिनी-गेम में प्रतिस्पर्धा करें।", + "customize2Text": "पात्र, मिनी-गेम्स और ध्वनि तक को अपने अनुकूल करें।", + "customizeText": "पात्र को अनुकूल करें और अपनी खुद की मिनी-गेम प्लेलिस्ट बनाएं।", + "sportsMoreFunText": "विस्फोटकों के साथ खेल अधिक मज़ेदार हैं।", + "teamUpAgainstComputerText": "कंप्यूटर के खिलाफ टीम" + }, + "storeText": "दुकान", + "submitText": "जमा करें", + "submittingPromoCodeText": "संहिता जमा कर रहा है ...", + "teamNamesColorText": "टीम के नाम / रंग ...", + "telnetAccessGrantedText": "टेलनेट एक्सेस सक्षम है", + "telnetAccessText": "टेलनेट पहुंच का पता चला; अनुमति देते हैं?", + "testBuildErrorText": "यह परीक्षण बिल्ड अब सक्रिय नहीं है; कृपया एक नए संस्करण की जांच करें", + "testBuildText": "टेस्ट बिल्ड", + "testBuildValidateErrorText": "परीक्षण निर्माण मान्य करने में असमर्थ। (कोई शुद्ध कनेक्शन नहीं है?)", + "testBuildValidatedText": "परीक्षण निर्माण मान्य; आनंद लें!", + "thankYouText": "आपके सहयोग के लिए धन्यवाद! खेल का लुफ्त उठाओ!!", + "threeKillText": "तिहरा हत्या!!", + "timeBonusText": "समय बोनस", + "timeElapsedText": "समय बीता", + "timeExpiredText": "समय समाप्त", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "टिप", + "titleText": "बमस्क्वाड", + "titleVRText": "बमस्क्वाड व.र.", + "topFriendsText": "अच्छे दोस्त", + "tournamentCheckingStateText": "प्रतियोगिता की जांच हो रही है; कृपया प्रतीक्षा करें...", + "tournamentEndedText": "यह प्रतियोगिता समाप्त हो गया है। एक नया जल्द ही शुरू होगा।", + "tournamentEntryText": "प्रतियोगिता प्रवेश", + "tournamentResultsRecentText": "हालिया प्रतियोगिता परिणाम", + "tournamentStandingsText": "प्रतियोगिता स्टैंडिंग्स", + "tournamentText": "प्रतियोगिता", + "tournamentTimeExpiredText": "प्रतियोगिता समय समाप्त", + "tournamentsText": "प्रतियोगिता", + "translations": { + "characterNames": { + "Agent Johnson": "एजेंट जॉनसन", + "B-9000": "बी-9000", + "Bernard": "बर्नार्ड", + "Bones": "हड्डि", + "Butch": "बुच", + "Easter Bunny": "ईस्टर बनी", + "Flopsy": "फ्लॉप्सी", + "Frosty": "फ्रॉस्टी", + "Gretel": "ग्रेटेल", + "Grumbledorf": "ग्रुम्बलडोर्फ़", + "Jack Morgan": "जैक मॉर्गन", + "Kronk": "क्रोंक", + "Lee": "ली", + "Lucky": "सौभाग्यशाली", + "Mel": "मेल", + "Middle-Man": "मध्य-व्यक्ति", + "Minimus": "कनिष्ठा", + "Pascal": "पास्कल", + "Pixel": "पिक्सेल", + "Sammy Slam": "सैमी स्लैम", + "Santa Claus": "सांता क्लॉज़", + "Snake Shadow": "सांप छाया", + "Spaz": "स्पॅज", + "Taobao Mascot": "ताओबाओ मास्कॉट", + "Todd McBurton": "टोड मैकबर्टन", + "Zoe": "झो", + "Zola": "ज़ोला" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} प्रशिक्षण", + "Infinite ${GAME}": "अनंत ${GAME}", + "Infinite Onslaught": "अनंत आक्रमण", + "Infinite Runaround": "अनंत भागम-भाग", + "Onslaught Training": "आक्रमण प्रशिक्षण", + "Pro ${GAME}": "उत्तम ${GAME}", + "Pro Football": "उत्तम फुटबॉल", + "Pro Onslaught": "उत्तम हमला", + "Pro Runaround": "उत्तम भागम-भाग", + "Rookie ${GAME}": "प्रथम ${GAME}", + "Rookie Football": "प्रथम फुटबॉल", + "Rookie Onslaught": "प्रथम हमला", + "The Last Stand": "एक अंतिम मोर्चा", + "Uber ${GAME}": "महा ${GAME}", + "Uber Football": "महा फुटबॉल", + "Uber Onslaught": "महा हमला", + "Uber Runaround": "महा रनराउंड" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "जीतने के लिए लंबे समय तक चुने गए व्यक्ति बनें। \nइसे चुनने के लिए चुने हुए को मार डालो।", + "Bomb as many targets as you can.": "जितना संभव हो उतने लक्ष्य पर बम मारें ।", + "Carry the flag for ${ARG1} seconds.": "${ARG1} सेकंड के लिए ध्वज पकड़ें", + "Carry the flag for a set length of time.": "समय की एक निश्चित लंबाई के लिए झंडा ले लो।", + "Crush ${ARG1} of your enemies.": "अपने ${ARG1} दुश्मनों को क्रश करें।", + "Defeat all enemies.": "सभी दुश्मनों को हराएं।", + "Dodge the falling bombs.": "गिरने वाले बम चकमा दें।", + "Final glorious epic slow motion battle to the death.": "मृत्यु के लिए अंतिम गौरवशाली महाकाव्य धीमी गति लड़ाई।", + "Gather eggs!": "अंडे इकट्ठा करो!", + "Get the flag to the enemy end zone.": "दुश्मन अंत क्षेत्र में ध्वज प्राप्त करें।", + "How fast can you defeat the ninjas?": "आप निंजा को कितनी तेजी से पराजित कर सकते हैं?", + "Kill a set number of enemies to win.": "जीतने के लिए दुश्मनों की एक निश्चित संख्या को मार डालो।", + "Last one standing wins.": "आखिरी खड़े की जीत", + "Last remaining alive wins.": "आखिरी शेष जीवित की जीत।", + "Last team standing wins.": "आखिरी टीम विजयी", + "Prevent enemies from reaching the exit.": "दुश्मनों को बाहर निकलने से रोकें।", + "Reach the enemy flag to score.": "स्कोर करने के लिए दुश्मन ध्वज तक पहुंचें।", + "Return the enemy flag to score.": "स्कोर करने के लिए दुश्मन ध्वज लौटें।", + "Run ${ARG1} laps.": "${ARG1} अंतराल दौड़ेँ", + "Run ${ARG1} laps. Your entire team has to finish.": "${ARG1} अंतराल दौड़ेँ। आपकी पूरी टीम को खत्म करना है।", + "Run 1 lap.": "1 अंतराल दौड़ो।", + "Run 1 lap. Your entire team has to finish.": "१ अंतराल दौड़ेँ। आपकी पूरी टीम को भी दौड़ना है।", + "Run real fast!": "असली तेजी से भागो!", + "Score ${ARG1} goals.": "${ARG1} गोल्स स्कोर करें।", + "Score ${ARG1} touchdowns.": "${ARG1} टचडाउन्स स्कोर करें।", + "Score a goal.": "एक गोल करो", + "Score a touchdown.": "एक टचडाउन स्कोर करें", + "Score some goals.": "कुछ स्कोर करें।", + "Secure all ${ARG1} flags.": "सभी ${ARG1} झंडे सुरक्षित करें।", + "Secure all flags on the map to win.": "जीतने के लिए मानचित्र पर सभी झंडे सुरक्षित करें।", + "Secure the flag for ${ARG1} seconds.": "${ARG1} सेकंड के लिए ध्वज सुरक्षित करें।", + "Secure the flag for a set length of time.": "समय की एक निश्चित लंबाई के लिए ध्वज सुरक्षित करें।", + "Steal the enemy flag ${ARG1} times.": "दुश्मन ध्वज ${ARG1} बार चुराओ।", + "Steal the enemy flag.": "दुश्मन का झंडा चोरी करो।", + "There can be only one.": "वहां केवल एक हो सकता है।", + "Touch the enemy flag ${ARG1} times.": "${ARG1} बार दुश्मन ध्वज स्पर्श करें।", + "Touch the enemy flag.": "दुश्मन का झंडा स्पर्श करें।", + "carry the flag for ${ARG1} seconds": "${ARG1} सेकंड के लिए ध्वज पकड़ें", + "kill ${ARG1} enemies": "${ARG1} दुश्मनों को मार डालो", + "last one standing wins": "आखिरी खड़े जीत", + "last team standing wins": "आखिरी टीम विजयी", + "return ${ARG1} flags": "${ARG1} झंडे वापस करें", + "return 1 flag": "1 झंडा वापस करें", + "run ${ARG1} laps": "${ARG1} अंतराल दौड़ेँ", + "run 1 lap": "1 अंतराल दौड़ो", + "score ${ARG1} goals": "${ARG1} गोल्स स्कोर करे।", + "score ${ARG1} touchdowns": "${ARG1} गोल्स स्कोर करें।", + "score a goal": "एक गोल करो", + "score a touchdown": "एक टचडाउन स्कोर करें", + "secure all ${ARG1} flags": "सभी ${ARG1} झंडे सुरक्षित करें", + "secure the flag for ${ARG1} seconds": "${ARG1} सेकंड के लिए ध्वज सुरक्षित करें", + "touch ${ARG1} flags": "${ARG1} झंडे स्पर्श करें", + "touch 1 flag": "1 ध्वज स्पर्श करें" + }, + "gameNames": { + "Assault": "आक्रमण", + "Capture the Flag": "झंडा कब्जा", + "Chosen One": "एक चुना हुआ", + "Conquest": "विजय", + "Death Match": "मौत का खेल", + "Easter Egg Hunt": "ईस्टर अंडा खोजना", + "Elimination": "निकाल देना", + "Football": "फ़ुटबॉल", + "Hockey": "हॉकी", + "Keep Away": "दूर रखना", + "King of the Hill": "पहाड़ी के राजा", + "Meteor Shower": "उल्का बौछार", + "Ninja Fight": "निंजा लड़ाई", + "Onslaught": "हमला", + "Race": "दौड़", + "Runaround": "चारो और दौड़ें", + "Target Practice": "लक्ष्य अभ्यास", + "The Last Stand": "अंतिम स्टैंड" + }, + "inputDeviceNames": { + "Keyboard": "कीबोर्ड", + "Keyboard P2": "कीबोर्ड प.२" + }, + "languages": { + "Arabic": "अरबी", + "Belarussian": "बेलारूसी", + "Chinese": "सरलीकृत चीनी", + "ChineseTraditional": "चीनी पारंपरिक", + "Croatian": "क्रोएशियाई", + "Czech": "चेक", + "Danish": "डेनिश", + "Dutch": "डच", + "English": "अंग्रेजी", + "Esperanto": "एस्पेरांतो", + "Finnish": "फिनिश", + "French": "फ्रेंच", + "German": "जर्मन", + "Gibberish": "जिबरिश", + "Greek": "ग्रीक", + "Hindi": "हिंदी", + "Hungarian": "हंगेरी", + "Indonesian": "इन्डोनेशियाई", + "Italian": "इतालवी", + "Japanese": "जापानी", + "Korean": "कोरियाई", + "Persian": "फ़ारसी", + "Polish": "पोलिश", + "Portuguese": "पुर्तगाली", + "Romanian": "रोमानियाई", + "Russian": "रूसी", + "Serbian": "सर्बियाई", + "Slovak": "स्लोवाक", + "Spanish": "स्पेनिश", + "Swedish": "स्वीडिश", + "Turkish": "तुर्की", + "Ukrainian": "यूक्रेनी", + "Venetian": "वेनेशियन", + "Vietnamese": "वियतनामी" + }, + "leagueNames": { + "Bronze": "कांस्य", + "Diamond": "हीरक", + "Gold": "स्वर्ण", + "Silver": "रजत" + }, + "mapsNames": { + "Big G": "बड़ा जी", + "Bridgit": "ब्रिजिट", + "Courtyard": "आंगन", + "Crag Castle": "क्रैग कैसल", + "Doom Shroom": "डूम शोरूम", + "Football Stadium": "फ़ुटबॉल स्टेडियम", + "Happy Thoughts": "सुखद ख़याल", + "Hockey Stadium": "हॉकी स्टेडियम", + "Lake Frigid": "उदासीन झील", + "Monkey Face": "वानर मुख", + "Rampage": "हंगामा", + "Roundabout": "फेरदार", + "Step Right Up": "वर्धक सीढी", + "The Pad": "तकती", + "Tip Top": "सर्वोच्चता", + "Tower D": "डी मीनार", + "Zigzag": "टेढ़ा-मेढ़ा" + }, + "playlistNames": { + "Just Epic": "सिर्फ उत्कृष्ट", + "Just Sports": "सिर्फ्र खेल" + }, + "scoreNames": { + "Flags": "झण्डें", + "Goals": "लक्ष्य", + "Score": "स्कोर", + "Survived": "जीवित रह गए", + "Time": "समय", + "Time Held": "आयोजित समय" + }, + "serverResponses": { + "A code has already been used on this account.": "इस खाते पर एक कोड पहले से ही इस्तेमाल किया जा चुका है।", + "A reward has already been given for that address.": "उस पते के लिए एक इनाम पहले ही दिया जा चुका है।", + "Account linking successful!": "खाता का जुड़ाव सफल!", + "Account unlinking successful!": "खाता अलगाव सफल!", + "Accounts are already linked.": "खाते पहले ही जुड़े हुए हैं।", + "An error has occurred; (${ERROR})": "एक गलती हुई है; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "एक गलती हुई है; कृपया समर्थन से संपर्क करें। (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "एक गलती हुई है; कृपया support@froemling.net से संपर्क करें।", + "An error has occurred; please try again later.": "एक गलती हुई है; बाद में पुन: प्रयास करें।", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "क्या आप वाकई इन खातों को जोड़ना चाहते हैं? \n\n${ACCOUNT1} \n${ACCOUNT2} \n\nइसे असंपादित नहीं किया जा सकता है!", + "BombSquad Pro unlocked!": "बमस्क्वाड प्रो खुला है!", + "Can't link 2 accounts of this type.": "इस प्रकार के २ खातों को लिंक नहीं कर सकता।", + "Can't link 2 diamond league accounts.": "२ डायमंड लीग खातों को लिंक नहीं कर सकता।", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "लिंक नहीं किआ जा सकता; अधिकतम ${COUNT} खातों का जुड़ाव पार होने की सम्भावना है।", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "धोखाधड़ी का पता चला; स्कोर और पुरस्कार ${COUNT} दिनों के लिए निलंबित कर दिए गए हैं।", + "Could not establish a secure connection.": "एक सुरक्षित कनेक्शन स्थापित नहीं कर सका।", + "Daily maximum reached.": "दैनिक अधिकतम पहुंच गया।", + "Entering tournament...": "टूर्नामेंट में प्रवेश ...", + "Invalid code.": "अमान्य कोड।", + "Invalid payment; purchase canceled.": "अमान्य भुगतान; खरीद रद्द।", + "Invalid promo code.": "अमान्य प्रोमो कोड।", + "Invalid purchase.": "अवैध खरीदी", + "Invalid tournament entry; score will be ignored.": "अवैध टूर्नामेंट प्रविष्टि; स्कोर को नजरअंदाज कर दिया जाएगा।", + "Item unlocked!": "सामग्री अनलॉक!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "जुड़ाव अस्वीकृत। ${ACCOUNT} में महत्वपूर्ण डेटा शामिल है \nजो सभी खो जाएंगे। यदि आप चाहते हैं \nतो आप विपरीत क्रम में लिंक कर सकते हैं \n(और इसके बजाय इस खाते का डेटा खो दें)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "इस खाते में ${ACCOUNT} खाता लिंक करें? \n${ACCOUNT} पर मौजूद सभी डेटा खो जाएंगे। \nइसे असंपादित नहीं किया जा सकता है। क्या आपको यकीन है?", + "Max number of playlists reached.": "प्लेलिस्ट की अधिकतम संख्या तक पहुंच गई।", + "Max number of profiles reached.": "प्रोफाइल की अधिकतम संख्या तक पहुंच गया।", + "Maximum friend code rewards reached.": "अधिकतम मित्र कोड पुरस्कार पहुंचे।", + "Message is too long.": "संदेश बहुत लंबा है।", + "Profile \"${NAME}\" upgraded successfully.": "प्रोफाइल \"${NAME}\" सफलतापूर्वक अपग्रेड किया गया।", + "Profile could not be upgraded.": "प्रोफ़ाइल को अपग्रेड नहीं किया जा सका।", + "Purchase successful!": "खरीद सफल!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "साइन इन करने के लिए ${COUNT} टिकट प्राप्त हुए। \n${TOMORROW_COUNT} प्राप्त करने के लिए कल वापस आएं।", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "सर्वर कार्यक्षमता अब खेल के इस संस्करण में समर्थित नहीं है; \nकृपया एक नए संस्करण में अपडेट करें।", + "Sorry, there are no uses remaining on this code.": "क्षमा करें, इस कोड पर कोई उपयोग शेष नहीं है।", + "Sorry, this code has already been used.": "क्षमा करें, यह कोड पहले ही इस्तेमाल हो चुका है।", + "Sorry, this code has expired.": "क्षमा करें, यह कोड समाप्त हो गया है।", + "Sorry, this code only works for new accounts.": "क्षमा करें, यह कोड केवल नए खातों के लिए काम करता है।", + "Temporarily unavailable; please try again later.": "अस्थाई रूप से अनुपलब्ध; बाद में पुन: प्रयास करें।", + "The tournament ended before you finished.": "टूर्नामेंट समाप्त होने से पहले समाप्त हो गया।", + "This account cannot be unlinked for ${NUM} days.": "यह खाता ${NUM} दिनों के लिए अनलिंक नहीं किया जा सकता है।", + "This code cannot be used on the account that created it.": "इस कोड का उपयोग उस खाते पर नहीं किया जा सकता है जिसने इसे बनाया है।", + "This requires version ${VERSION} or newer.": "इसके लिए संस्करण ${VERSION} या नए की आवश्यकता है।", + "Tournaments disabled due to rooted device.": "रूट डिवाइस के कारण टूर्नामेंट अक्षम", + "Tournaments require ${VERSION} or newer": "टूर्नामेंटों को ${VERSION} या नए की आवश्यकता होती है", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "इस खाते से ${ACCOUNT} अनलिंक करें? ${ACCOUNT} \nपर मौजूद सभी डेटा रीसेट हो जाएंगे। \n(कुछ मामलों में उपलब्धियों को छोड़कर)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "चेतावनी: आपके खाते के खिलाफ हैकिंग की शिकायतें जारी की गई हैं। \nहैकिंग के लिए पाए गए खातों पर प्रतिबंध लगा दिया जाएगा। कृपया ईमानदार से खेलें।", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "क्या आप अपने डिवाइस खाते को इस से लिंक करना चाहते हैं? \n\nआपका डिवाइस खाता ${ACCOUNT1} है \nयह खाता ${ACCOUNT2} है \n\nयह आपको अपनी मौजूदा प्रगति को रखने की अनुमति देगा। \nचेतावनी: इसे पूर्ववत नहीं किया जा सकता है", + "You already own this!": "आप पहले से ही इसे खरीद चुके है!", + "You can join in ${COUNT} seconds.": "आप ${COUNT} सेकंड में शामिल हो सकते हैं।", + "You don't have enough tickets for this!": "आपके पास इसके लिए पर्याप्त टिकट नहीं हैं!", + "You don't own that.": "आप उसका स्वामित्व नहीं रखते हैं।", + "You got ${COUNT} tickets!": "आपको ${COUNT} टिकट मिल गए हैं!", + "You got a ${ITEM}!": "आपको ${ITEM} मिला है!", + "You have been promoted to a new league; congratulations!": "आपको एक नए लीग में पदोन्नत किया गया है; बधाई!", + "You must update to a newer version of the app to do this.": "ऐसा करने के लिए आपको ऐप के एक नए संस्करण में अपडेट करना होगा।", + "You must update to the newest version of the game to do this.": "ऐसा करने के लिए आपको गेम के नवीनतम संस्करण में अपडेट करना होगा।", + "You must wait a few seconds before entering a new code.": "नया कोड दर्ज करने से पहले आपको कुछ सेकंड इंतजार करना होगा।", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "आपने पिछले टूर्नामेंट में #${RANK} रैंक किया था। खेलने के लिए शुक्रिया!", + "Your account was rejected. Are you signed in?": "आपका खाता अस्वीकृत कर दिया गया है। क्या आप हस्ताक्षरित हैं?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "खेल की आपकी प्रति संशोधित कर दी गई है। \nकृपया किसी भी बदलाव को वापस करें और पुनः प्रयास करें।", + "Your friend code was used by ${ACCOUNT}": "आपका मित्र कोड ${ACCOUNT} द्वारा उपयोग किया गया था" + }, + "settingNames": { + "1 Minute": "१ मिनट", + "1 Second": "१ सेकंड", + "10 Minutes": "१० मिनट", + "2 Minutes": "२ मिनट", + "2 Seconds": "२ सेकंड", + "20 Minutes": "२० मिनट", + "4 Seconds": "४ सेकंड", + "5 Minutes": "५ मिनटें", + "8 Seconds": "८ सेकंड", + "Allow Negative Scores": "नकारात्मक स्कोर की अनुमति दें", + "Balance Total Lives": "कुल जीवनों का संतुलन करें", + "Bomb Spawning": "बम की पैदावार", + "Chosen One Gets Gloves": "चुनिंदा को दस्ताना प्राप्त", + "Chosen One Gets Shield": "चुनिंदा को कवच प्राप्त", + "Chosen One Time": "एक चुनिंदा का समय", + "Enable Impact Bombs": "प्रभाव बम सक्षम करें", + "Enable Triple Bombs": "तिहरा बम सक्षम करें", + "Entire Team Must Finish": "पूरी टीम द्वारा पूर्ण होनी चाहिए", + "Epic Mode": "उत्कृष्ट प्रकार", + "Flag Idle Return Time": "झंडा निष्क्रिय का वापसी समय", + "Flag Touch Return Time": "फ्लैग स्पर्श का वापसी समय", + "Hold Time": "पकड़ रहने का समय", + "Kills to Win Per Player": "प्रति खिलाड़ी जीतने के लिए हत्या", + "Laps": "चक्कर", + "Lives Per Player": "प्रति खिलाड़ी जीवन", + "Long": "लम्बा", + "Longer": "अधिक लम्बा", + "Mine Spawning": "भूमिगत बम का अंतर", + "No Mines": "कोई भूमिगत बम नहीं", + "None": "कोई नहीं", + "Normal": "साधारण", + "Pro Mode": "प्रो प्रकार", + "Respawn Times": "पैदावार की अवधि", + "Score to Win": "जितने का अंक", + "Short": "लघु", + "Shorter": "अधिक लघु", + "Solo Mode": "एकल प्रकार", + "Target Count": "लक्ष्य संख्या", + "Time Limit": "समय अवधि" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} अयोग्य है क्योंकि ${PLAYER} ने छोड़ दिया।", + "Killing ${NAME} for skipping part of the track!": "रास्ते के कुछ भाग छोड़ने के लिए ${NAME} को मृत्यु दंड !", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "${NAME} को चेतावनी: टर्बो / बटन-अवांछनीयता आपको बाहर निकलता है।" + }, + "teamNames": { + "Bad Guys": "बुरे लोग", + "Blue": "नीला", + "Good Guys": "अच्छे लोग", + "Red": "लाल" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "एक परिपूर्ण समय से किआ गया दौड़-कूद-स्पिन-पंच एक ही बार में मार सकता है\nऔर आपको आपके दोस्तों से वह-वाही भी मिलती है।", + "Always remember to floss.": "हमेशा फ्लॉस करना याद रखें।", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "यादृच्छिक के बजाये अपने पसंदीदा नाम और भेष का उपयोग करके \nअपने और अपने दोस्तों के लिए प्लेयर प्रोफाइल बनाएं।", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "अभिशाप बक्से आपको एक टिकिंग टाइम बम में बदल देते हैं।\nइसका एकमात्र इलाज स्वास्थ्य पैक को जल्दी से पकड़ना है।", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "उनके दिक्खावट के अलावा, सभी पात्रों की क्षमता समान हैं,\nतो बस जो भी आप सबसे करीब मिलते हैं उसे चुनें।", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "उस ऊर्जा ढाल को पाकर खुदको एकदम से सुरक्षित मत समझो ; आप अभी भी एक चट्टान से फेंक जा सकते हैं।", + "Don't run all the time. Really. You will fall off cliffs.": "हर समय मत भागो। सचमे। आप चट्टानों से गिर जाओगे।", + "Don't spin for too long; you'll become dizzy and fall.": "बहुत लंबे समय तक स्पिन मत करो; आप चक्कर खाकर गिर जाओगे।", + "Hold any button to run. (Trigger buttons work well if you have them)": "दौड़ने के लिए कोई भी बटन दबाए रखें। (यदि आपके पास है तो ट्रिगर बटन अच्छी तरह से काम करते हैं)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "दौड़ने के लिए किसी भी बटन को दबाए रखें। आप स्थानों को तेजी से प्राप्त करेंगे\nलेकिन बहुत अच्छी तरह से नहीं मुड़ पाएंगे, तो चट्टानों ज़रा संभल कर ।", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "बर्फ बम बहुत शक्तिशाली नहीं हैं, लेकिन जो भी उसके चपेट में अत है जम जाता है, \nऔर मरने की स्तिथि में रह जाता है ।", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "अगर कोई आपको उठाता है, तो उन्हें मुक्का मारें और वे जाने देंगे।\nयह वास्तविक जीवन में भी काम करता है।", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "यदि आपके पास नियंत्रकों की कमी है, तो अपने मोबाइल उपकरणों पर '${REMOTE_APP_NAME}' ऐप ​​इंस्टॉल करें\nऔर उन्हें नियंत्रकों के रूप में उपयोग करने के लिए।", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "यदि आप चिपचिपा-बम के शिकार हो गए , तो चारों ओर कूदें और गोल-गोल घूमे। आप शायद\nबम को हिलाकर हटा सकेंगे, या यदि कुछ और नहीं तो आपके आखिरी क्षण मनोरंजन होंगे।", + "If you kill an enemy in one hit you get double points for it.": "यदि आप एक बार में दुश्मन को मार देते हैं तो आपको इसके लिए डबल अंक मिलते हैं।", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "यदि आप एक अभिशाप उठाते हैं, तो अस्तित्व के लिए आपकी एकमात्र आशा है\nअगले कुछ सेकंड में एक स्वास्थ्य पावरअप पाएं।", + "If you stay in one place, you're toast. Run and dodge to survive..": "यदि आप एक ही स्थान पर रहते हैं, तो आप एक टोस्ट की तरह जल जायेंगे। जीवित रहने के लिए भागो और चकमा दो..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "यदि आपके पास बहुत से खिलाड़ी आ रहे हैं और जा रहे हैं, तो सेटिंग्स के तहत 'ऑटो-किक-निष्क्रिय-प्लेयर्स' चालू करें\nयदि कोई भी गेम छोड़ना भूल जाता है।", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "यदि आपका डिवाइस बहुत गर्म हो जाता है या आप बैटरी पावर को संरक्षित करना चाहते हैं,\nसेटिंग-> ग्राफिक्स में \"विजुअल\" या \"रेज़ोल्यूशन\" को बंद करें", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "यदि आपका फ्रेमरेट चकाचौंध है, \nतो रेसोलुशन या खेल की ग्राफिक्स सेटिंग्स में दृश्य को बंद करने का प्रयास करें।", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "कैप्चर-द-फ्लैग में, आपका खुद का झंडा स्कोर करने के लिए आपके आधार पर होना चाहिए, यदि दूसरा\nटीम स्कोर करने वाली है, अपने ध्वज चोरी करना उन्हें रोकने का एक अच्छा तरीका हो सकता है।", + "In hockey, you'll maintain more speed if you turn gradually.": "हॉकी में, यदि आप धीरे-धीरे मुड़ते हैं तो आप अधिक गति बनाए रखेंगे।", + "It's easier to win with a friend or two helping.": "एक दोस्त या दो मदद के साथ जीतना आसान है।", + "Jump just as you're throwing to get bombs up to the highest levels.": "जैसे ही आप उच्चतम स्तर तक बम प्राप्त करने के लिए फेंक रहे हैं, कूदो।", + "Land-mines are a good way to stop speedy enemies.": "तेजी से दुश्मनों को रोकने के लिए भूमि-खान एक अच्छा तरीका है।", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "अन्य खिलाड़ियों सहित कई चीजों को उठाया और फेंक दिया जा सकता है। पटकना\nचट्टानों से आपके दुश्मन एक प्रभावी और भावनात्मक रूप से पूरा करने वाली रणनीति हो सकते हैं।", + "No, you can't get up on the ledge. You have to throw bombs.": "नहीं, आप किनारे पर नहीं उठ सकते हैं। आपको बम फेंकना है।", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "खिलाड़ी अधिकांश खेलों के बीच में शामिल हो सकते हैं और छोड़ सकते हैं,\nऔर आप फ्लाई पर नियंत्रकों को प्लग और अनप्लग भी कर सकते हैं", + "Practice using your momentum to throw bombs more accurately.": "बम फेंकने के लिए अपनी गति का उपयोग करके अधिक सटीक रूप से अभ्यास करें।", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "पंच आपके मुट्ठी तेजी से बढ़ रहे हैं, और अधिक नुकसान पहुंचाते हैं,\nइसलिए craz की तरह दौड़ना, कूदना और कताई करने का प्रयास करें", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "एक बम फेंकने से पहले आगे और पीछे भागो\nइसे 'whiplash' करने के लिए और इसे आगे फेंक दें।", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "दुश्मनों का एक समूह बाहर ले लो\nएक टीएनटी बॉक्स के पास एक बम की स्थापना।", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "सिर सबसे कमजोर क्षेत्र है, इसलिए एक चिपचिपा-बम\nनोगिन के लिए आम तौर पर गेम-ओवर का मतलब है", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "यह स्तर कभी खत्म नहीं होता है, लेकिन यहां एक उच्च स्कोर है\nआपको दुनिया भर में अनन्त सम्मान अर्जित करेगा।", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "फेंक ताकत आपके द्वारा धारित दिशा पर आधारित है।\nआप के सामने धीरे-धीरे कुछ टॉस करने के लिए, कोई दिशा न रखें।", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "साउंडट्रैक से थक गए? अपनी स्वयं की वस्तु से इसे प्रतिस्थापित करें!\nसेटिंग्स-> ऑडियो-> साउंडट्रैक देखें", + "Try 'Cooking off' bombs for a second or two before throwing them.": "उन्हें फेंकने से पहले एक या दो के लिए 'पाक कला बंद' बम आज़माएं।", + "Try tricking enemies into killing eachother or running off cliffs.": "एक दूसरे को मारने या चट्टानों से भागने में दुश्मनों को धोखा देने का प्रयास करें।", + "Use the pick-up button to grab the flag < ${PICKUP} >": "ध्वज <${PICKUP}> को पकड़ने के लिए पिक-अप बटन का उपयोग करें", + "Whip back and forth to get more distance on your throws..": "अपने फेंकने पर अधिक दूरी पाने के लिए आगे और पीछे चाबुक करें ..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "आप बाएं या दाएं कताई करके अपने पेंच 'लक्ष्य' कर सकते हैं।\nबुजुर्गों को किनारों से बाहर करने या हॉकी में स्कोर करने के लिए यह उपयोगी है।", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "जब आप एक बम विस्फोट करने जा रहे हैं तो आप न्याय कर सकते हैं\nअपने फ्यूज से स्पार्क्स का रंग: पीला..रेंज..रेड..बॉम।", + "You can throw bombs higher if you jump just before throwing.": "यदि आप फेंकने से पहले कूदते हैं तो आप बम को अधिक फेंक सकते हैं।", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "जब आप चीजों पर अपना सिर फटकारते हैं तो आप नुकसान लेते हैं,\nतो चीजों पर अपने सिर को मारने की कोशिश मत करो।", + "Your punches do much more damage if you are running or spinning.": "यदि आप दौड़ रहे हैं या चक्रण कर रहे हैं तो आपके मुक्के अधिक नुकसान करते हैं।" + } + }, + "trophiesRequiredText": "इसके लिए कम से कम ${NUMBER} वैजयन्तीयों की आवश्यकता होती है।", + "trophiesText": "वैजयन्तीयां", + "trophiesThisSeasonText": "इस सीज़न की वैजयन्तीयां", + "tutorial": { + "cpuBenchmarkText": "ऊटपटांग-गति पर शिक्षण चलाना (मुख्य रूप से सीपीयू की गति का परीक्षण करता है)", + "phrase01Text": "नमस्ते!", + "phrase02Text": "${APP_NAME} में आपका स्वागत है!", + "phrase03Text": "अपने पात्र को नियंत्रित करने के लिए यहां कुछ युक्तियां दी गई हैं:", + "phrase04Text": "${APP_NAME} में कई चीजें भौतिक विज्ञान पर आधारित हैं।", + "phrase05Text": "उदाहरण के लिए, जब आप मुक्के मारते हैं,..", + "phrase06Text": "..क्षति आपके मुट्ठी की गति पर आधारित है।", + "phrase07Text": "देखा? हम एक ही जगह पर थे, जिससे मुश्किल से ${NAME} को चोट पहुंची।", + "phrase08Text": "अब कूदो और अधिक गति पाने के लिए घूमो ।", + "phrase09Text": "वाह! यह बेहतर है।", + "phrase10Text": "दौड़ना से भी मदद मिलती है।", + "phrase11Text": "दौड़ने के लिए किसी भी को बटन दबाए रखें।", + "phrase12Text": "अतिरिक्त-विस्मयकारी मुक्के के लिए, दौड़कर घूमने का प्रयास करें।", + "phrase13Text": "ओह, इसके लिए क्षमा करें '${NAME}।", + "phrase14Text": "झंडे जैसे चीजों को आप उठा सकते हैं और फेंक सकते हैं .. और ${NAME} को भी ।", + "phrase15Text": "अंत में, बम।", + "phrase16Text": "बम फेंकने के लिए अभ्यास की आवश्यकता है।", + "phrase17Text": "आउच! बहुत अच्छा फेंक नहीं है।", + "phrase18Text": "विचलन से आपको आगे फेंकने में मदद मिलती है।", + "phrase19Text": "कूदकर आप बम को ऊँचा फेंक सकते हैं।", + "phrase20Text": "ज़्यादा दूर फेकने के लिए बम को \"घुमा\" कर फेकें।", + "phrase21Text": "अपने बमों का समय निर्धारित करना मुश्किल हो सकता है।", + "phrase22Text": "फुस्स...!", + "phrase23Text": "एक या दो सेकंड के लिए सुतली को \"जलने\" देने का प्रयास करें।", + "phrase24Text": "हुर्रे! अच्छी तरह से जला है।", + "phrase25Text": "खैर, इसके बारे में बस इतना ही ।", + "phrase26Text": "अब जाओ शेर, उसका शिकार करलो ।", + "phrase27Text": "अपना प्रशिक्षण याद रखें, और आप जीवित वापस आ जाएंगे!", + "phrase28Text": "...शायद, नहीं भी...", + "phrase29Text": "शुभकामनाएँ", + "randomName1Text": "किशोर", + "randomName2Text": "हरी", + "randomName3Text": "यश", + "randomName4Text": "ख्याति", + "randomName5Text": "कल्याण", + "skipConfirmText": "सच में ट्यूटोरियल छोड़ना चाहते हैं? पुष्टि करने के लिए स्पर्श करें या दबाएं।", + "skipVoteCountText": "${COUNT}/${TOTAL} मतदान छोड़ना", + "skippingText": "शिक्षण छोड़ा जा रहा है...", + "toSkipPressAnythingText": "(शिक्षण छोड़ने के लिए कुछ भी स्पर्श करे या दबाएं)" + }, + "twoKillText": "दोहरी हत्या", + "unavailableText": "उपलब्ध नहीं", + "unconfiguredControllerDetectedText": "बिना विन्यास वाले नियंत्रक का पता चला:", + "unlockThisInTheStoreText": "यह स्टोर में अनलॉक होना चाहिए।", + "unlockThisProfilesText": "${NUM} प्रोफ़ाइल बनाने के लिए, आपको इसकी आवश्यकता है:", + "unlockThisText": "इसे अनलॉक करने के लिए, आपको इसकी आवश्यकता है:", + "unsupportedHardwareText": "क्षमा करें, यह हार्डवेयर गेम के इस निर्माण द्वारा समर्थित नहीं है।", + "upFirstText": "सर्व प्रथम", + "upNextText": "गेम ${COUNT} में अगला:", + "updatingAccountText": "आपका खाता \nअद्यतन हो रहा है ...", + "upgradeText": "अभ्युत्थान", + "upgradeToPlayText": "इन-गेम स्टोर में इसे चलाने के लिए \"${PRO}\" अनलॉक करें।", + "useDefaultText": "पूर्व निर्धारित उपयोग करें", + "usesExternalControllerText": "यह गेम इनपुट के लिए बाहरी नियंत्रक का उपयोग करता है।", + "usingItunesText": "गाने के लिए संगीत ऐप का उपयोग कर रहे है ...", + "validatingTestBuildText": "परीक्षण निर्माण मान्य ...", + "victoryText": "विजय!", + "voteDelayText": "आप ${NUMBER} सेकंड के लिए एक और वोट शुरू नहीं कर सकते हैं", + "voteInProgressText": "एक वोट पहले ही प्रगति पर है।", + "votedAlreadyText": "तुमने पहले ही मतदान कर दिया", + "votesNeededText": "${NUMBER} वोटों की आवश्यकता है", + "vsText": "बनाम", + "waitingForHostText": "(जारी रखने के लिए ${HOST} की प्रतीक्षा कर रहा है)", + "waitingForPlayersText": "प्लेयर्स के जुड़ने की प्रतीक्षा करे।", + "waitingInLineText": "लाइन में प्रतीक्षा (पार्टी पूर्ण है) ...", + "watchAVideoText": "वीडियो देखो", + "watchAnAdText": "एक विज्ञापन देखें", + "watchWindow": { + "deleteConfirmText": "\"${REPLAY}\" हटाएं?", + "deleteReplayButtonText": "रीप्ले \nहटाएं", + "myReplaysText": "मेरा रीप्ले", + "noReplaySelectedErrorText": "कोई रीप्ले चयनित नहीं है", + "playbackSpeedText": "रीप्ले की गति", + "renameReplayButtonText": "रीप्ले का \nपुनः नामकारन", + "renameReplayText": "\"${REPLAY}\" का नाम बदलें।", + "renameText": "नाम बदलूँ", + "replayDeleteErrorText": "रीप्ले हटाने में त्रुटि", + "replayNameText": "रीप्ले का नाम", + "replayRenameErrorAlreadyExistsText": "उस नाम के साथ एक रीप्ले पहले से मौजूद है।", + "replayRenameErrorInvalidName": "रीप्ले का नाम बदल नहीं सकते; गलत नाम।", + "replayRenameErrorText": "रीप्ले का नाम बदलने में त्रुटि।", + "sharedReplaysText": "साझा रीप्ले", + "titleText": "देखें", + "watchReplayButtonText": "रीप्ले \nदेखें" + }, + "waveText": "लहर", + "wellSureText": "हाँ ज़रूर !", + "wiimoteLicenseWindow": { + "titleText": "डार्विन रिमोट कॉपीराइट" + }, + "wiimoteListenWindow": { + "listeningText": "Wiimotes के लिए सुन रहा है ...", + "pressText": "एक साथ Wiimote बटन 1 और 2 दबाएं।", + "pressText2": "मोशन प्लस के साथ निर्मित नए वाईमोट्स पर, इसके बजाय लाल 'सिंक' बटन दबाएं।" + }, + "wiimoteSetupWindow": { + "copyrightText": "डार्विनरिमोट कॉपीराइट", + "listenText": "सुनें", + "macInstructionsText": "सुनिश्चित करें कि आपके मैक पर वाईआई बंद है और ब्लूटूथ सक्षम है, \nफिर 'Listen' दबाएं। विमोट सम्रथन थोड़ा सा चंचल हो सकता है, \nतो कनेक्शन मिलने से पहले आपको कुछ \nबार कोशिश करनी पड़ सकती है। \n\nब्लूटूथ ७ कनेक्टेड डिवाइसों को संभालना चाहिए, \nहालांकि आपका माइलेज भिन्न हो सकता है। \n\nबमस्क्वाड मूल विमोट, ननचक्स और पारम्परिक \nनियंत्रक का समर्थन करता है। \nनया वाईआई रिमोट प्लस अब भी काम करता है \nलेकिन संलग्नक के साथ नहीं।", + "thanksText": "इसे संभव बनाने के लिए \nDarwiinRemote को धन्यवाद।", + "titleText": "विमोट सेटअप" + }, + "winsPlayerText": "${NAME} विजयी!", + "winsTeamText": "${NAME} विजयी!", + "winsText": "${NAME} विजयी!", + "worldScoresUnavailableText": "वैश्विक अंक उपलब्ध नहीं", + "worldsBestScoresText": "जागतिक सर्वोत्तम स्कोर्स", + "worldsBestTimesText": "विश्व का सबसे अधिक समय", + "xbox360ControllersWindow": { + "getDriverText": "ड्राइवर प्राप्त करें।", + "macInstructions2Text": "", + "macInstructionsText": "Xbox 360 नियंत्रकों का उपयोग करने के लिए, \nआपको इंस्टॉल करने की आवश्यकता होगी मैक ड्राइवर नीचे दिए गए लिंक पर उपलब्ध है। \nयह वायर्ड और वायरलेस नियंत्रकों दोनों के साथ काम करता है।", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "BombSquad के साथ वायर्ड Xbox 360 नियंत्रकों का उपयोग करने के लिए, \nबस उन्हें अपने डिवाइस के यूएसबी पोर्ट में प्लग करें। \nएकाधिक नियंत्रकों को जोड़ने के लिए आप एक यूएसबी हब का उपयोग कर सकते हैं । \n\nवायरलेस नियंत्रकों का उपयोग करने के लिए आपको एक वायरलेस रिसीवर की आवश्यकता होगी, \n\"विंडोज़ के लिए Xbox 360 वायरलेस नियंत्रक\" के हिस्से के रूप में उपलब्ध पैकेज या अलग से बेचा गया। \nप्रत्येक रिसीवर यूएसबी पोर्ट में प्लग करता है \nऔर आपको 4 वायरलेस नियंत्रकों तक कनेक्ट करने की अनुमति देता है।", + "titleText": "${APP_NAME}:के साथ Xbox 360 नियंत्रकों का उपयोग करना" + }, + "yesAllowText": "हाँ, आज्ञा दें", + "yourBestScoresText": "आपका बेस्ट स्कोर।", + "yourBestTimesText": "आपका सर्वोत्तम समय" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/hungarian.json b/dist/ba_data/data/languages/hungarian.json new file mode 100644 index 0000000..0ba5b1f --- /dev/null +++ b/dist/ba_data/data/languages/hungarian.json @@ -0,0 +1,1834 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "A fiók neve nem tartalmazhat emojikat és más speciális karaktereket", + "accountProfileText": "(Felhasználó profilok)", + "accountsText": "Fiókok", + "achievementProgressText": "Eredmènyek: ${COUNT} a(z) ${TOTAL}-ból/ből.", + "campaignProgressText": "Kampány haladás(nehéz mód): ${PROGRESS}", + "changeOncePerSeason": "Ebben a szezonban ezt csak egyszer változtathatod meg.", + "changeOncePerSeasonError": "Kell várjál a következő szezonig, hogy ezt megint megváltoztasd (${NUM} nap)", + "customName": "Egyedi név", + "deviceSpecificAccountText": "Csak erről az eszközről lehet elérni ezt a profilt: ${NAME}", + "linkAccountsEnterCodeText": "Írd Be A Kódot", + "linkAccountsGenerateCodeText": "Kód Generálása", + "linkAccountsInfoText": "(vidd át előrehaladásodat akár több eszközre is)", + "linkAccountsInstructionsNewText": "Két fiók összekapcsolásához először hozzon létre egy kódot\nés írja be a második kódot. Adatok a\na második számlát majd megosztják egymással.\n(Az első fiókból származó adatok elveszhetnek)\n\nÖsszesen akár ${COUNT} fiókot is összekapcsolhat.\n\nFONTOS: csak az Ön tulajdonában lévő fiókokat kapcsolja össze;\nHa kapcsolatba lépsz a barátok fiókjaival, akkor nem fogsz\negyszerre tud online játszani.", + "linkAccountsInstructionsText": "Hogy párosíts két profilt, generálj egy kódot\naz egyiken majd írd be a kapott kódot a másikon.\nAz előrehaladás és a megvásárolt dolgok is párosításra kerülnek .\nÖsszesen ${COUNT} profilt tudsz összehangolni.\n\nFONTOS:Csak olyan profilt csatlakoztass ami a tiéd!\nHogyha profilt csatlakoztatsz a barátaiddal\nakkor nem fogsz tudni játszani azonos időben!\n\nLégy óvatos!Ezt a lépést nem lehet visszavonni!", + "linkAccountsText": "Profilok Párosítása", + "linkedAccountsText": "Párosított Profilok:", + "nameChangeConfirm": "Megváltoztatod a fiókod nevét erre: ${NAME}?", + "resetProgressConfirmNoAchievementsText": "Ez visszaállítja a co-op haladásodat és \nhelyi legmagasabb pontszámaidat (de a jegyeidet nem). \nEz nem vissza vonható. Biztos vagy benne?", + "resetProgressConfirmText": "Ez visszaállítja a co-op haladásodat,\nteljesítményeidet és helyi legmagasabb pontszámaidat\n(de a jegyeidet nem). Ez nem vissza vonható.\nBiztos vagy benne?", + "resetProgressText": "Haladás visszaállítása", + "setAccountName": "Állítsa be a fiók nevét", + "setAccountNameDesc": "Válassza ki a megjeleníteni kívánt fiók nevét.\nHasználhatja a nevét az egyik kapcsoltól\nfiókokat, vagy egyedi egyedi nevet hozhat létre.", + "signInInfoText": "Lépj be, hogy tudj jegyeket gyűjteni, online versenyezni\nés elérni az eredményeidet akár több eszközről is.", + "signInText": "Bejelentkezés", + "signInWithDeviceInfoText": "(Autómatikus fiók csak ezen az eszközön elérhető)", + "signInWithDeviceText": "Bejelentkezés az eszköz felhasználójával.", + "signInWithGameCircleText": "Lépj be a Game Circle fiókodba", + "signInWithGooglePlayText": "Belépés Google fiókkal", + "signInWithTestAccountInfoText": "(Egy régi típusú felhasználó; Mostantól a készülék felhasználóját használd.)", + "signInWithTestAccountText": "Bejelentkezés teszt felhasználóval", + "signOutText": "Kijelentkezés", + "signingInText": "Bejelentkezés...", + "signingOutText": "Kijelentkezés...", + "testAccountWarningCardboardText": "Vigyázat:egy \"teszt\" fiókkal lépsz be.\nEzek helyettesítve lesznek egy Google\nfiókkal, amint támogatottá válnak.\n\nMost minden jegyet meg kell szerezned a játékban.\n(viszont megkapod a BombSquad Pro-t ingyen)", + "testAccountWarningOculusText": "Figyelmeztetés: egy \"teszt\" felhasználóval jelentkezel be.\nEz \"valós\" felhasználókkal lesz helyettesítve később\nidén ami jegyek és egyéb jellemzők vásárlását fogja ajánlani.\n\nEgyenlőre minden jegyet játékon belül kell megszerezned.\n(Viszont, ha megteszed, ingyen megkaphatod a BombSquad Pro frissítést)", + "testAccountWarningText": "Figyelmeztetés: egy \"teszt\" felhasználóval jelentkezel be.\nEz a felhasználó ehez az egy telefonhoz van kötve és\nlehet időnként visszaáll. (Tehát kérjük ne tölts\nsok időt a cuccok gyüjtésével/felnyitásával)\n\nFuss egy kereskedelmi verziót a játékból, hogy egy \"valós\" \nfelhasználót használj (Game-Center, Google Plus, stb.)\nEz engedi a haladások tárolását a felhőben és megosztását\nmás eszközök közt is.\n", + "ticketsText": "Jegyek: ${COUNT}", + "titleText": "Felhasználó", + "unlinkAccountsInstructionsText": "Válassz ki egy fiókot a leválasztáshoz", + "unlinkAccountsText": "Fiókok leválasztása", + "viaAccount": "(számlán keresztül ${NAME})", + "youAreSignedInAsText": "Be vagy jelentkezve, mint:" + }, + "achievementChallengesText": "Teljesítmény kihívások", + "achievementText": "Teljesítmény", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Ölj meg 3 rosszfiút TNT-vel", + "descriptionComplete": "Megöltél 3 rosszfiút TNT-vel", + "descriptionFull": "Ölj meg 3 rosszfiút TNT-vel a(z) ${LEVEL}-n", + "descriptionFullComplete": "Megöltél 3 rosszfiút TNT-vel a(z) ${LEVEL}-n", + "name": "Bumm Megy a Dinamit" + }, + "Boxer": { + "description": "Nyerj bombák használata nélkül", + "descriptionComplete": "Bombák használata nélkül nyertél", + "descriptionFull": "Teljesítsd a(z) ${LEVEL} szintet bombák használata nélkül", + "descriptionFullComplete": "Teljesítetteda a(z) ${LEVEL} szintet bombák használata nélkül", + "name": "Boxoló" + }, + "Dual Wielding": { + "descriptionFull": "Csatlakoztass 2 kontrollert(hardvert vagy applikáció)", + "descriptionFullComplete": "2 kontroller csatlakoztatva (hardver vagy applikáció)", + "name": "Dupla Hadonászás" + }, + "Flawless Victory": { + "description": "Nyerj anélkül hogy megütnének", + "descriptionComplete": "Nyertél anélkül hogy megütöttek volna", + "descriptionFull": "Nyerj a(z) ${LEVEL} pályán anélkül, hogy megütnének", + "descriptionFullComplete": "Nyertél a(z) ${LEVEL} pályán anélkül hogy megütöttek volna", + "name": "Csodálatos győzelem" + }, + "Free Loader": { + "descriptionFull": "Kezdj el egy mindenki mindenki ellen meccset több mint 2 játékossal", + "descriptionFullComplete": "Egy mindenki mindenki ellen meccs elkezdve több mint 2 játékossal", + "name": "A Vezető" + }, + "Gold Miner": { + "description": "Ölj meg 6 rosszfiút taposóaknákkal", + "descriptionComplete": "Megöltél 6 rosszfiút taposóaknákkal", + "descriptionFull": "Ölj meg 6 rosszfiút taposóaknákkal a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Megöltél 6 rosszfiút taposóaknákkal a(z) ${LEVEL} pályán", + "name": "Aranyásó" + }, + "Got the Moves": { + "description": "Nyerj anélkül, hogy ütéseket vagy bombákat alkalmaznál", + "descriptionComplete": "Nyertél anélkül, hogy ütéseket vagy bombákat alkalmaztál volna", + "descriptionFull": "Nyerj a(z) ${LEVEL} pályán anélkül, hogy ütéseket vagy bombákat alkalmaznál", + "descriptionFullComplete": "Nyertél a(z) ${LEVEL}-n anélkül, hogy ütéseket vagy bombákat alkalmaztál volna", + "name": "Megvan a mozgás" + }, + "In Control": { + "descriptionFull": "Csatlakoztass egy kontrollert (hardver vagy applikáció)", + "descriptionFullComplete": "Egy kontrollert csatlakoztatva (hardver vagy applikáció)", + "name": "Kontrollálva" + }, + "Last Stand God": { + "description": "Szerezz 1000 pontot", + "descriptionComplete": "Szereztél 1000 pontot", + "descriptionFull": "Szerezz 1000 pontot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Szereztél 1000 pontot a(z) ${LEVEL}-n", + "name": "${LEVEL} Isten" + }, + "Last Stand Master": { + "description": "Szerezz 250 pontot", + "descriptionComplete": "Szereztél 250 pontot", + "descriptionFull": "Szerezz 250 pontot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Szereztél 250 pontot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Mester" + }, + "Last Stand Wizard": { + "description": "Szerezz 500 pontot", + "descriptionComplete": "Szereztél 500 pontot", + "descriptionFull": "Szerezz 500 pontot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Szereztél 500 pontot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Varázsló" + }, + "Mine Games": { + "description": "Ölj meg 3 rosszfiút taposóaknákkal", + "descriptionComplete": "Megöltél 3 rosszfiút taposóaknákkal", + "descriptionFull": "Ölj meg 3 rosszfiút taposóaknákkal a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Megöltél 3 rosszfiút taposóaknákkal a(z) ${LEVEL} pályán", + "name": "Akna Játékok" + }, + "Off You Go Then": { + "description": "Lökj ki 3 rosszfiút a pályáról", + "descriptionComplete": "Kilöktél 3 rosszfiút a pályáról", + "descriptionFull": "Lökj ki 3 rosszfiút a pályáról a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Kilöktél 3 rosszfiút a pályáról a(z) ${LEVEL} pályán", + "name": "Kifele mész majd" + }, + "Onslaught God": { + "description": "Szerezz 5000 pontot", + "descriptionComplete": "Szereztél 5000 pontot", + "descriptionFull": "Szerezz 5000 pontot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Szereztél 5000 pontot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Isten" + }, + "Onslaught Master": { + "description": "Szerezz 500 pontot", + "descriptionComplete": "Szereztél 500 pontot", + "descriptionFull": "Szerezz 500 pontot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Szereztél 500 pontot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Mester" + }, + "Onslaught Training Victory": { + "description": "Győzz le minden hullámot", + "descriptionComplete": "Minden hullámot legyőztél", + "descriptionFull": "Győzz le minden hullámot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Legyőztél minden hullámot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Győzelem" + }, + "Onslaught Wizard": { + "description": "Szerezz 1000 pontot", + "descriptionComplete": "Szereztél 1000 pontot", + "descriptionFull": "Szerezz 1000 pontot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Szereztél 1000 pontot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Varázsló" + }, + "Precision Bombing": { + "description": "Nyerj mindenféle erőnövelő nélkül", + "descriptionComplete": "Nyertél mindenféle erőnövelő nélkül", + "descriptionFull": "Nyerj a(z) ${LEVEL}-n mindenféle erőnövelő nélkül", + "descriptionFullComplete": "Nyertél a(z) ${LEVEL}-n mindenféle erőnövelő nélkül", + "name": "Precíziós Bombák" + }, + "Pro Boxer": { + "description": "Nyerj bombák használata nélkül", + "descriptionComplete": "Bombák használata nélkül nyertél", + "descriptionFull": "Teljesítsd a(z) ${LEVEL} pályát bombák használata nélkül", + "descriptionFullComplete": "Teljesítetted a(z) ${LEVEL}-t bombák használata nélkül", + "name": "Profi Boxoló" + }, + "Pro Football Shutout": { + "description": "Nyerj anélkül hogy a rosszfiúk pontot szerezzenek.", + "descriptionComplete": "Nyertél anélkül hogy a rosszfiúk pontot szereztek volna", + "descriptionFull": "Nyerd meg a(z) ${LEVEL} pályát anélkül, hogy a rosszfiúk pontot szereznének", + "descriptionFullComplete": "Megnyerted a(z) ${LEVEL} pályát anélkül, hogy a rosszfiúk pontot szereztek volna", + "name": "${LEVEL} Kirekesztve" + }, + "Pro Football Victory": { + "description": "Nyerd meg a játékot", + "descriptionComplete": "Megnyerted a játékot", + "descriptionFull": "Nyerdmeg a játékot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Megnyerted a játékot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Győzelem" + }, + "Pro Onslaught Victory": { + "description": "Győzz le minden hullámot", + "descriptionComplete": "Minden hullámot túléltél!", + "descriptionFull": "Győzz le minden hullámot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Legyőztél minden hullámot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Győzelem" + }, + "Pro Runaround Victory": { + "description": "Teljesítsd az összes hullámot", + "descriptionComplete": "Összes hullám teljesítve", + "descriptionFull": "Teljesítsd az összes hullámot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Teljesítetted az összes hullámot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Győzelem" + }, + "Rookie Football Shutout": { + "description": "Nyerj anélkül, hogy hagynád a rosszfiúkat pontot szerezni", + "descriptionComplete": "Nyertél anélkül, hogy a rosszfiúk pontot szereztek volna", + "descriptionFull": "Nyerj a(z) ${LEVEL}-n anélkül, hogy hagynád a rosszfiúkat pontot szerezni", + "descriptionFullComplete": "Megnyerted a ${LEVEL} pályát, anélkül, hogy a rosszfiúk pontot szereztek volna", + "name": "${LEVEL} Kirekesztve" + }, + "Rookie Football Victory": { + "description": "Nyerd meg a játékot", + "descriptionComplete": "Játék megnyerve", + "descriptionFull": "Nyerd meg a ${LEVEL} pályát", + "descriptionFullComplete": "${LEVEL} pálya megnyerve", + "name": "${LEVEL} Győzelem" + }, + "Rookie Onslaught Victory": { + "description": "Győzz le minden hullámot", + "descriptionComplete": "Minden hullámot túléltél!", + "descriptionFull": "Győzz le minden hullámot a(z) ${LEVEL}-ban/ben", + "descriptionFullComplete": "Legyőztél minden hullámot a(z) ${LEVEL}-ban/ben", + "name": "${LEVEL} Győzelem" + }, + "Runaround God": { + "description": "Szerezz 2000 pontot", + "descriptionComplete": "Szereztél 2000 pontot", + "descriptionFull": "Szerezz 2000 pontot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Szereztél 2000 pontot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Isten" + }, + "Runaround Master": { + "description": "Szerezz 500 pontot", + "descriptionComplete": "Szereztél 500 pontot", + "descriptionFull": "Szerezz 500 pontot a ${LEVEL} pályán", + "descriptionFullComplete": "Szereztél 500 pontot a ${LEVEL} pályán", + "name": "${LEVEL} Mester" + }, + "Runaround Wizard": { + "description": "Szerezz 1000 pontot", + "descriptionComplete": "Szereztél 1000 pontot", + "descriptionFull": "Szerezz 1000 pontot a(z) ${LEVEL} pályán", + "descriptionFullComplete": "Szereztél 1000 pontot a(z) ${LEVEL} pályán", + "name": "${LEVEL} Varázsló" + }, + "Sharing is Caring": { + "descriptionFull": "Oszd meg a játékot egy barátoddal", + "descriptionFullComplete": "A játék sikeresen megosztva egy baráttal", + "name": "Oszd meg és Uralkodj" + }, + "Stayin' Alive": { + "description": "Nyerj halál nélkül", + "descriptionComplete": "Nyerj anélkül hogy meghalnál.", + "descriptionFull": "Nyerd meg a ${LEVEL} pályát halál nélkül", + "descriptionFullComplete": "${LEVEL} pálya megnyerve halál nélkül", + "name": "Maradj életben!" + }, + "Super Mega Punch": { + "description": "Okozz 100%-os sebzést egyetlen ütéssel", + "descriptionComplete": "100%-os sebzés kiosztva", + "descriptionFull": "Okozz 100%-os sebzést egyetlen ütéssel a ${LEVEL} pályán", + "descriptionFullComplete": "100%-os sebzés kiosztva a ${LEVEL} pályán", + "name": "Szuper Mega Ütés" + }, + "Super Punch": { + "description": "Okozz 50%-os sebzést egyetlen ütéssel", + "descriptionComplete": "50%-os sebzés kiosztva", + "descriptionFull": "Okozz 50%-os sebzést egyetlen ütéssel a ${LEVEL} pályán", + "descriptionFullComplete": "50%-os sebzés kiosztva a ${LEVEL} pályán", + "name": "Szuper ütés" + }, + "TNT Terror": { + "description": "Ölj meg 6 rossz fiút TNT-vel", + "descriptionComplete": "6 rossz fiú megölve TNT-vel", + "descriptionFull": "Ölj meg 6 rossz fiút TNT-vel itt:${LEVEL}", + "descriptionFullComplete": "6 rossz fiú megölve TNT-vel itt:${LEVEL}", + "name": "TNT Terror" + }, + "Team Player": { + "descriptionFull": "Indíts el egy Csapatos mérkőzést több mint 4 játékossal", + "descriptionFullComplete": "Csapatos mérkőzés sikeresen elkezdve több mint 4 játékossal", + "name": "Csapatjátékos" + }, + "The Great Wall": { + "description": "Állítsd meg as összes rosszfiút", + "descriptionComplete": "Összes rosszfiú megállítva", + "descriptionFull": "Állítsd meg az összes rossz fiút itt:${LEVEL}", + "descriptionFullComplete": "Az összes rossz fiú megállítva itt:${LEVEL}", + "name": "A Nagy Fal" + }, + "The Wall": { + "description": "Állítsd meg as összes rosszfiút", + "descriptionComplete": "Megállítittad az összes rosszfiút", + "descriptionFull": "Állítsd meg az összes rossz fiút itt:${LEVEL}", + "descriptionFullComplete": "Az összes rossz fiú megállítva itt:${LEVEL}", + "name": "A Fal" + }, + "Uber Football Shutout": { + "description": "Nyerj, anélkül, hogy a rosszfiúk pontot szereznének", + "descriptionComplete": "Nyertél ,anélkül ,hogy a rossz fiúk pontot szereztek volna", + "descriptionFull": "Nyerj, anélkül, hogy a rossz fiúk pontot szereznének itt:${LEVEL}", + "descriptionFullComplete": "Nyertél ,anélkül ,hogy a rossz fiúk pontot szereztek volna itt:${LEVEL}", + "name": "${LEVEL} Kiütés" + }, + "Uber Football Victory": { + "description": "Nyerd meg a játékot", + "descriptionComplete": "Megnyerted a játékot", + "descriptionFull": "Nyerd meg a játékot a(z) ${LEVEL} pályán.", + "descriptionFullComplete": "Játék megnyerve a ${LEVEL} pályán", + "name": "${LEVEL} Győzelem" + }, + "Uber Onslaught Victory": { + "description": "Győzz le minden hullámot", + "descriptionComplete": "Minden hullámot legyőztél", + "descriptionFull": "Győzz le minden hullámot a(z) ${LEVEL}-ban/ben", + "descriptionFullComplete": "Legyőztél minden hullámot a(z) ${LEVEL}-ban/ben", + "name": "${LEVEL} Győzelem" + }, + "Uber Runaround Victory": { + "description": "Győzd le az összes hullámot", + "descriptionComplete": "Az összes hullám legyőzve", + "descriptionFull": "Győzd le az összes hullámot itt:${LEVEL}", + "descriptionFullComplete": "Az összes hullám legyőzve itt:${LEVEL}", + "name": "${LEVEL} Győzelem" + } + }, + "achievementsRemainingText": "Hátralévő Teljesítmények:", + "achievementsText": "Teljesítmények", + "achievementsUnavailableForOldSeasonsText": "Bocsi, de a teljesítmények nem elérhetőek a régi szezonban.", + "addGameWindow": { + "getMoreGamesText": "Több Játékmód...", + "titleText": "Játék Hozzáadása" + }, + "allowText": "Engedélyezés", + "alreadySignedInText": "A fiókoddal be vagy jelentkezve egy másik eszközről;\nkérlek cserélj fiókot vagy zárd be a játékot \na másik eszközön és próbáld újra", + "apiVersionErrorText": "Nem lehet betölteni a ${NAME} modult jelenlegi verzió:${VERSION_USED}; szükséges verió:${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Automatikus\" csak akkor érhető el ha csatlakoztatva van egy fejhallgató)", + "headRelativeVRAudioText": "Head-Relative hangzás", + "musicVolumeText": "Zene Hangereje", + "soundVolumeText": "Játék Hangereje", + "soundtrackButtonText": "Zenék", + "soundtrackDescriptionText": "(hallgasd a kedvenc zenéidet a játék betétdalaként)", + "titleText": "Hang" + }, + "autoText": "Automatikus", + "backText": "Vissza", + "banThisPlayerText": "Kitagadod ezt a játékost", + "bestOfFinalText": "A ${COUNT} Finálé legjobbja", + "bestOfSeriesText": "A ${COUNT} sorozat legjobbja:", + "bestRankText": "Legjobb helyezésed:#${RANK}", + "bestRatingText": "Legjobb értékelésed:${RATING}", + "bombBoldText": "BOMBA", + "bombText": "Bomba", + "boostText": "Boost", + "bsRemoteConfigureInAppText": "A(z) ${REMOTE_APP_NAME} a saját alkalmazásában konfigurálódik.", + "buttonText": "Gomb", + "canWeDebugText": "Szeretnéd, ha a BombSquad automatikusan jelentené a hibákat, \ncrash-eléseket, és általános használati infókat a fejlesztőknek?\n\nEz az adat nem tartalmaz személyes információkat és segít\na játék sima és hibamentes futásának megtartásában.", + "cancelText": "Mégse", + "cantConfigureDeviceText": "Bocs, ${DEVICE} nem beállítható.", + "challengeEndedText": "Ez a kihívás már végét ért.", + "chatMuteText": "Chat némítása", + "chatMutedText": "Chat némítva", + "chatUnMuteText": "Chat némítás feloldása", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Teljesítened kell ezt\na szintet a folytatáshoz!", + "completionBonusText": "Befejezési Bónusz", + "configControllersWindow": { + "configureControllersText": "Vezérlők beállítása", + "configureKeyboard2Text": "2. játékos billentyűzetének beállítása", + "configureKeyboardText": "Billentyűzet beállítása", + "configureMobileText": "Mobil eszközök, mint vezérlők", + "configureTouchText": "Érintőkijelző beállítása", + "ps3Text": "PS3 Vezérlők", + "titleText": "Kontrollerek", + "wiimotesText": "Wiimote-ok", + "xbox360Text": "Xbox 360 Vezérlők" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Megjegyzés: a vezérlő támogatás függ az eszköztől és Android verziójától.", + "pressAnyButtonText": "Nyomj akármilyen gombot a vezérlőn\namit be akarsz állítani...", + "titleText": "Vezérlők Beállítása" + }, + "configGamepadWindow": { + "advancedText": "Haladó", + "advancedTitleText": "Haladó Vezérlő Felépítések", + "analogStickDeadZoneDescriptionText": "(kapcsold ezt fel, ha a karaktered 'kifarol' amikor elengeded a joystick-et)", + "analogStickDeadZoneText": "Analóg Joystick Holtpont", + "appliesToAllText": "(alkalmazza minden ilyen típusú vezérlőnek)", + "autoRecalibrateDescriptionText": "(engedélyezze ezt, ha karaktere nem fut teljes sebességen)", + "autoRecalibrateText": "Automatikus Analóg kar Újrakaribrálás", + "axisText": "tengely", + "clearText": "tiszta", + "dpadText": "dpad", + "extraStartButtonText": "Extra Start Gomb", + "ifNothingHappensTryAnalogText": "Ha semmi sem történt,próbál meg az analóg kart használni helyette.", + "ifNothingHappensTryDpadText": "Ha semmi sem történt,próbáld meg a d-padot használni helyette.", + "ignoreCompletelyDescriptionText": "(megelőzi hogy ez a kontroller befolyásolja a játékot vagy a menüket)", + "ignoreCompletelyText": "Teljes Letiltás", + "ignoredButton1Text": "Letiltott gomb 1", + "ignoredButton2Text": "Letiltott gomb 2", + "ignoredButton3Text": "Letiltott gomb 3", + "ignoredButton4Text": "Letiltott Gomb 4", + "ignoredButtonDescriptionText": "(használd ezeket a gombokat,hogy megelőzd ,hogy a 'home' és a 'sync' gombok befolyásolják az UI-t)", + "pressAnyAnalogTriggerText": "Érintsd meg az analógot...", + "pressAnyButtonOrDpadText": "Nyomj meg egy gombot vagy a D-padot...", + "pressAnyButtonText": "Nyomj meg egy gombot...", + "pressLeftRightText": "Érintsd meg a jobb oldalt vagy a balt...", + "pressUpDownText": "Nyomd meg a felfelét vagy a lefelét...", + "runButton1Text": "Futás gomb 1", + "runButton2Text": "Futás gomb 2", + "runTrigger1Text": "Futás ravasz 1", + "runTrigger2Text": "Futás ravasz 2", + "runTriggerDescriptionText": "(az analóg ravaszok lehetővé teszik a variálható futási sebességet)", + "secondHalfText": "Használd ezt ,hogy beállítsd a második felét \na 2 az 1-ben kontrollernek \n,így a játék egy kontrollert fog mutatni.", + "secondaryEnableText": "Engedélyezés", + "secondaryText": "Másodlagos Vezérlő", + "startButtonActivatesDefaultDescriptionText": "(kapcsold ezt ki ha a start gombod több mint egy 'menü' gomb)", + "startButtonActivatesDefaultText": "A Start gomb létrehoz egy widgetet", + "titleText": "Kontroller Beállítása", + "twoInOneSetupText": "2 az 1-ben Kontroller Beállítása", + "uiOnlyDescriptionText": "(Akadályozd meg, hogy ez a vezérlő csatlakozzon a játszmába)", + "uiOnlyText": "Menü használatra korlátozás", + "unassignedButtonsRunText": "Minden nem használt gomb fut", + "unsetText": "", + "vrReorientButtonText": "VR Nézet Visszaállítás Gomb" + }, + "configKeyboardWindow": { + "configuringText": "Konfigurálás: ${DEVICE}", + "keyboard2NoteText": "A legtöbb billentyűzet csak pár gombnyomást érzékel egyszerre,\nszóval jobb ha használtok két játékos esetén jobb, \nhogy ha egy második billentyűzetet is használtok.\nFontos, hogy ebben az esetben is be kell állítani \naz egyéni vezérlést." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Akció Panel Átmérőja", + "actionsText": "Akciók", + "buttonsText": "gombok", + "dragControlsText": "", + "joystickText": "joystick", + "movementControlScaleText": "Mozgás Panel Átmérője", + "movementText": "Mozgás", + "resetText": "Visszaállítás", + "swipeControlsHiddenText": "Nyilak elrejtése", + "swipeInfoText": "A 'Nyilak' stílushoz hozzá kell szokni \n,viszont játék közben nem kell folyamatosan az ikonokat nézni.", + "swipeText": "nyilak", + "titleText": "Érintőképernyő Konfigurálása" + }, + "configureItNowText": "Most Konfigurálod?", + "configureText": "Konfigurálás", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "A legjobb eredményért szükséged lesz egy lag-mentes Wi-fi hálózatra.\nCsökkentheted a wi-fi lag esélyét azzal, hogy kikapcsolsz más vezeték nélküli\neszközt, hogy közel játszol a router-hez, és hogy egyszerűen\ncsatlakoztatod a kiszolgálót ethernet segitségével.", + "explanationText": "Hogy az okos telefonodat vagy a tableted használni tudd mint vezeték nélküli kontroller,\nnem kell mást tenned csak telepíteni a \"${REMOTE_APP_NAME}\" alkalmazást.\nBármennyi eszköz csatlakozhat a ${APP_NAME}-hoz Wi-Fi-n keresztül méghozzá teljesen ingyen!", + "forAndroidText": "Androidhoz:", + "forIOSText": "iOS-hez:", + "getItForText": "Töltsd le a ${REMOTE_APP_NAME} alkalmazást iOS-hez, az Apple\nApp Store-ból, Androidhoz a Google Play Áruházból, vagy az Amazon Appstore-ból", + "googlePlayText": "Google Play", + "titleText": "Mobil eszközök használata vezérlőként" + }, + "continuePurchaseText": "Folytatod ${PRICE}-t?", + "continueText": "Folytatás", + "controlsText": "Irányítás", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Ez nem vonatkozik a minden idők rangsorra.", + "activenessInfoText": "Ez a szorzó növekszik mikor naponta játszol,\nde kihagyott napok után viszont csökkenni fog.", + "activityText": "Aktivitás", + "campaignText": "Kampány", + "challengesInfoText": "Nyerj nyereményeket mini-játékok megcsinálásával.\n\nA nyeremény és a nehézségi szint\nmindig nő, amikor egy játékot teljesítesz\nés csökken ha egy nem sikerül.", + "challengesText": "Kihívások", + "currentBestText": "Jelenlegi Legjobb", + "customText": "Egyediség", + "entryFeeText": "Belépő", + "forfeitConfirmText": "Felhagysz ezzel a kihívással?", + "forfeitNotAllowedYetText": "Ezt a kihívást most még nem adhatod fel.", + "forfeitText": "Feladás", + "multipliersText": "Szorzók", + "nextChallengeText": "Következő kihívás", + "nextPlayText": "Következő játék", + "ofTotalTimeText": "${TOTAL}-ból/ből", + "playNowText": "Játssz most", + "pointsText": "Pontok", + "powerRankingFinishedSeasonUnrankedText": "(szezon befejezve rang nélkül)", + "powerRankingNotInTopText": "(Nincs benne a top ${NUMBER} -ban/-ben)", + "powerRankingPointsEqualsText": "= ${NUMBER} pont", + "powerRankingPointsMultText": "(x ${NUMBER} pont)", + "powerRankingPointsText": "${NUMBER} pont", + "powerRankingPointsToRankedText": "(${CURRENT} a ${REMAINING} pontból)", + "powerRankingText": "Értékelés", + "prizesText": "Nyeremények", + "proMultInfoText": "Játékosok a ${PRO} verzióval\n${PERCENT}%-kal több pontot kapnak.", + "seeMoreText": "További...", + "skipWaitText": "Kihagyás", + "timeRemainingText": "Hátralévő idő", + "toRankedText": "A helyezésig", + "totalText": "végeredmény", + "tournamentInfoText": "Versenyez a legjobb eredményeiddel\nmás játékosokkal a ligádban.\n\nA legjobb eredményeket elérő játékosok\na torna végén nyereményt kapnak.", + "welcome1Text": "Üdv a ${LEAGUE} Ligában. Fejlesztheted a liga \nhelyezésedet azzal, hogy csillagokat, teljesítményeket \nszerzel vagy akár megnyered a mérkőzéseket.", + "welcome2Text": "Jegyeket is szerezhetsz. A jegyekkel beléphetsz mérkőzésekbe,\nilletve betudod váltani új karakterekre, pályákra, mini-játékokra\nés még sok másra.", + "yourPowerRankingText": "Helyezésed:" + }, + "copyOfText": "${NAME} másolata", + "createEditPlayerText": "", + "createText": "Készíts", + "creditsWindow": { + "additionalAudioArtIdeasText": "További Audio, Korai Művek, és Ötletek ${NAME} által", + "additionalMusicFromText": "További zenék ${NAME}-tól/től", + "allMyFamilyText": "Minden barátomnak és a családomnak akik segítettek a tesztelésben", + "codingGraphicsAudioText": "Kódolás, Grafika, és Audio ${NAME} által", + "languageTranslationsText": "Nyelvi fordítások:", + "legalText": "Jog:", + "publicDomainMusicViaText": "Nyilvános-domain zene ${NAME} által", + "softwareBasedOnText": "Ez a szoftver részben ${NAME} munkáján alapszik", + "songCreditText": "${PERFORMER} előadásával a(z) ${TITLE}\nKomponálta ${COMPOSER}, Rendezte ${ARRANGER}, Kiadta ${PUBLISHER},\n${SOURCE} Udvariasságából", + "soundAndMusicText": "Hang és Zene:", + "soundsText": "Hangok (${SOURCE}):", + "specialThanksText": "Külön Köszönet:", + "thanksEspeciallyToText": "Kűlönösképpen köszönet ${NAME}-nak/nek", + "titleText": "${APP_NAME} Közreműködők", + "whoeverInventedCoffeeText": "Akárki, aki meghívott egy kávéra" + }, + "currentStandingText": "A jelenlegi álláspontod: #${RANK}", + "customizeText": "Személyre szabás...", + "deathsTallyText": "${COUNT} halál", + "deathsText": "Halál", + "debugText": "Hibajavítás", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Megjegyzés: ajánlott a Beállítások->Grafika->Textúrákat 'Eros'-re állítani ennek leteszteléséhez.", + "runCPUBenchmarkText": "CPU Benchmark futtatása", + "runGPUBenchmarkText": "GPU Benchmark futtatása", + "runMediaReloadBenchmarkText": "Média-Újratöltési Benchmark futtatása", + "runStressTestText": "Feszültség teszt futtatása", + "stressTestPlayerCountText": "Játékos szám", + "stressTestPlaylistDescriptionText": "Stressz Teszt lista", + "stressTestPlaylistNameText": "Lista neve", + "stressTestPlaylistTypeText": "Lista típusa", + "stressTestRoundDurationText": "Kör időtartama", + "stressTestTitleText": "Stressz Teszt", + "titleText": "Tesztfeladat és Stressz Teszt", + "totalReloadTimeText": "Újratöltési idő: ${TIME} (lásd a naplót)" + }, + "defaultGameListNameText": "Alapértelmezett ${PLAYMODE} lista", + "defaultNewGameListNameText": "Saját ${PLAYMODE} listám", + "deleteText": "Törlés", + "demoText": "Demo", + "denyText": "Megtagad", + "desktopResText": "Asztali Felbontás", + "difficultyEasyText": "Könnyű", + "difficultyHardOnlyText": "Csak Nehéz Mód", + "difficultyHardText": "Nehéz", + "difficultyHardUnlockOnlyText": "Ezt a szintet csak nehéz módban lehet feloldani.\nSzerinted megvan benned ami kell!?!?!", + "directBrowserToURLText": "Kérlek irányíts egy web-böngészőt a következő URL-re:", + "disableRemoteAppConnectionsText": "Irányító Alkalmazások Letiltása", + "disableXInputDescriptionText": "Engedélyezi ,hogy 4-nél több kontroller is csatlakozhasson ,viszont nem biztos a hibátlan működés.", + "disableXInputText": "XInput kikapcsolása", + "doneText": "Elvégezve", + "drawText": "Döntetlen", + "duplicateText": "Másolás", + "editGameListWindow": { + "addGameText": "Játék\nHozzáadása", + "cantOverwriteDefaultText": "Nem lehet felülírni az alap lejátszási listát!", + "cantSaveAlreadyExistsText": "Egy lejátszási lista már létezik ilyen néven!", + "cantSaveEmptyListText": "Egy üres lejátszási listát nem lehet elmenteni!", + "editGameText": "Játék\nSzerkesztése", + "listNameText": "Lejátszási lista név", + "nameText": "Név", + "removeGameText": "Játék\nEltávolítása", + "saveText": "Lista Mentése", + "titleText": "Lejátszási lista szerkesztő" + }, + "editProfileWindow": { + "accountProfileInfoText": "Ez a különleges profil rendelkezik névvel,\nés ikonnal a fiókod alapján.\n\n${ICONS}\n\nKészíts egyéni profilt, hogy használhass\nkülönböző neveket vagy egyéni ikonokat.", + "accountProfileText": "(fiók profil)", + "availableText": "A \"${NAME}\" név elérhető.", + "changesNotAffectText": "Megjegyzés: a változások nem érintik a játékban lévő karaktereket", + "characterText": "karakter", + "checkingAvailabilityText": "\"${NAME}\" név ellenőrzése...", + "colorText": "szín", + "getMoreCharactersText": "Több Karakter Szerzése...", + "getMoreIconsText": "Szerezz több ikont...", + "globalProfileInfoText": "A teljes körű játékos profilok garantálják az egyedi \nnevet a játékban, és feloldják az ikonokat is.", + "globalProfileText": "(teljes körű profil)", + "highlightText": "fénypont", + "iconText": "ikon", + "localProfileInfoText": "Alkalmi játékos profilok nem rendelkeznek ikonokkal és lehet,\nhogy a nevüket már használja más a játékban.Fejleszd a profilodat\n teljes körű profilra, hogy egyedülálló neved legyen és, hogy felold az ikonokat.", + "localProfileText": "(alkalmi profil)", + "nameDescriptionText": "Játékos Név", + "nameText": "Név", + "randomText": "véletlen", + "titleEditText": "Profil Szerkesztése", + "titleNewText": "Új Profil", + "unavailableText": "A \"${NAME}\" név nem elérhető;próbálj ki egy másikat.", + "upgradeProfileInfoText": "Ezzel a frissítéssel egyedülálló neved lesz\n és engedélyezi az ikonok használatát is.", + "upgradeToGlobalProfileText": "Frissítés teljes körű profillá" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Nem törölheted az alap betétdalt.", + "cantEditDefaultText": "Nem lehet szerkeszteni az alap betétdalt. Duplázd meg vagy csinálj egy újat.", + "cantOverwriteDefaultText": "Nem lehet felülírni az alap betétdalt", + "cantSaveAlreadyExistsText": "Egy betétdal már létezik ilyen névvel!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Alapértelmezett Betétdal", + "deleteConfirmText": "Betétdal Törlése:\n\n'${NAME}'?", + "deleteText": "Betétdal\nTörlése", + "duplicateText": "Betétdal\nDuplázása", + "editSoundtrackText": "Betétdal Szerkesztő", + "editText": "Betétdal\nSzerkesztése", + "fetchingITunesText": "A(z) iTunes lejátszási listák megszerzése...", + "musicVolumeZeroWarning": "Figyelem: a zene hangerő 0-ra van állítva", + "nameText": "Név", + "newSoundtrackNameText": "Saját Betétdal ${COUNT}", + "newSoundtrackText": "Új Betétdal:", + "newText": "Új\nBetétdal", + "selectAPlaylistText": "Válassz Egy Lejátszási Listát", + "selectASourceText": "Zene Forrás", + "testText": "Teszt", + "titleText": "Betétdalok", + "useDefaultGameMusicText": "Alapértelmezett Játék Zene", + "useITunesPlaylistText": "Zene App Lejátszási Lista", + "useMusicFileText": "Zene Fájl (mp3, stb)", + "useMusicFolderText": "Zene Fájlok Mappája" + }, + "editText": "Szerkesztés", + "endText": "Befejezés", + "enjoyText": "Jó Játékot!", + "epicDescriptionFilterText": "${DESCRIPTION} A hatalmas lassú mozgásban", + "epicNameFilterText": "Hatalmas ${NAME}", + "errorAccessDeniedText": "hozzáférés megtagadva", + "errorOutOfDiskSpaceText": "Kifogyott a szabadhelyből", + "errorText": "Hiba", + "errorUnknownText": "ismeretlen hiba", + "exitGameText": "Kilépsz a ${APP_NAME}-ból?", + "exportSuccessText": "'${NAME}' áthelyezve.", + "externalStorageText": "Külső Tárhely", + "failText": "Bukta", + "fatalErrorText": "Ajajj; valami hiányzik vagy megsérült.\nPróbáld meg újratelepíteni a játékot vagy,\nírj az alábbi e-mail címünkre: ${EMAIL}.", + "fileSelectorWindow": { + "titleFileFolderText": "Válaszd ki a fájlt vagy a mappát", + "titleFileText": "Válasz ki egy fájlt", + "titleFolderText": "Válasz ki egy mappát", + "useThisFolderButtonText": "Használd ezt a mappát" + }, + "finalScoreText": "Végeredmény", + "finalScoresText": "Végeredmények", + "finalTimeText": "Végső idő", + "finishingInstallText": "Telepítés befejezése...", + "fireTVRemoteWarningText": "A jobb játékélményért, használj\nkontrollert, vagy telepítsd fel a\n'${REMOTE_APP_NAME}' alkalmazást az\nokostelefonodra vagy táblagépedre.", + "firstToFinalText": "Első ${COUNT} végleges", + "firstToSeriesText": "Első ${COUNT} sorozat", + "fiveKillText": "SOROZAT GYILKOS!!!", + "flawlessWaveText": "Hibátlan Hullám!", + "fourKillText": "CSOPORT GYILKOS!!!", + "friendScoresUnavailableText": "A barátok eredményei elérhetetlenek.", + "gameCenterText": "Játék Központ", + "gameCircleText": "GameCircle", + "gameLeadersText": "${COUNT}. játék vezetői", + "gameListWindow": { + "cantDeleteDefaultText": "Az alapértelmezett listát nem lehet törölni.", + "cantEditDefaultText": "Az alapértelmezett listát nem lehet szerkezteni.Másold vagy csinálj egy újat.", + "cantShareDefaultText": "Nem oszthatod meg az alap lejátszási listát.", + "deleteConfirmText": "Törli ezt: \"${LIST}\"?", + "deleteText": "Lista\nTörlése", + "duplicateText": "Lista\nMásolása", + "editText": "Lista\nSzerkesztése", + "newText": "Új\nLista", + "showTutorialText": "Oktató végignézése", + "shuffleGameOrderText": "Játék Rendelés Megkeverése", + "titleText": "Szabd személyre a ${TYPE} Lejátszási listát" + }, + "gameSettingsWindow": { + "addGameText": "Játék Hozzáadása" + }, + "gamesToText": "${WINCOUNT} játék a ${LOSECOUNT}-hez/hoz/höz", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Emlékezz: néhány eszköz a társaságban, több mint\negy játékost is kezelhet, ha van elég vezérlő hozzá.", + "aboutDescriptionText": "Használd ezeket a füleket, hogy összegyűjts egy társaságot.\n\nTársaságokban játszhatsz játékokat, mérkőzéseket\nbarátaiddal különböző eszközökön keresztül.\n\nHasználd a ${PARTY} gombot a felső jobb sarokban, hogy\ncsevegj és közölj valamit társaságoddal.\n(a vezérlőn, nyomd meg a ${BUTTON}-t amikor a menübe vagy)", + "aboutText": "Információ", + "addressFetchErrorText": "", + "appInviteInfoText": "Hívj meg barátokat hogy kipróbálják a BombSquad-ot és ők\nkapnak ${COUNT} ingyen jegyeket. Te kapsz\n${YOU_COUNT} minden barátért aki játszik.", + "appInviteMessageText": "${NAME} küldött neked ${COUNT} jegyeket a ${APP_NAME}-ban", + "appInviteSendACodeText": "Küldj neki egy Kódot", + "appInviteTitleText": "${APP_NAME} Játékba Meghívás", + "bluetoothAndroidSupportText": "(működik néhány Android eszközzel Bluetooth támogatással)", + "bluetoothDescriptionText": "Szolgálj ki/csatlakozz egy társasághoz Bluetooth-on keresztül:", + "bluetoothHostText": "Kiszolgálás Bluetooth-on keresztül", + "bluetoothJoinText": "Csatlakozás Bluetooth-on keresztül", + "bluetoothText": "Bluetooth", + "checkingText": "ellenőrzés...", + "dedicatedServerInfoText": "A legjobb eredményért, csinálj dedikált szerver.Útmutató:bombsquadgame.com/server", + "disconnectClientsText": "Ez le fogja csatlakoztatni a ${COUNT} játékost\na társaságodból. Biztos vagy benne?", + "earnTicketsForRecommendingAmountText": "A barátaid kapni fognak ${COUNT} jegyet ha kipróbálják a játékot\n(és te is kapni fogsz ${YOU_COUNT} jegyet minden barátok általi kipróbálás után)", + "earnTicketsForRecommendingText": "Terjeszd a játékot\ningyenes jegyekért...", + "emailItText": "Küldés", + "friendHasSentPromoCodeText": "${COUNT} db ${APP_NAME} jegyet kaptál ${NAME}-tól", + "friendPromoCodeAwardText": "${COUNT} jegyet fogsz kapni minden egyes használásnál.", + "friendPromoCodeExpireText": "Ez a kód ${EXPIRE_HOURS} óra múlva lejár és csak új játékosok használhatják.", + "friendPromoCodeInfoText": "Ez a kód ${COUNT} jegyet ér.\n\nMenj a \"Beállítások->Haladó->Promóciós Kód Beírása\" a játékban, hogy aktiváld.\nLátogasd meg a bombsquadgame.com nevű weboldalt, a letöltési linkek eléréséhez.\nEz a kód csak ${EXPIRE_HOURS} óráig érvényes és csak új játékosok számára elérhető.", + "friendPromoCodeInstructionsText": "Hogy használhasd, nyisd meg a ${APP_NAME}-ot, menj a \"Beállítások->Haladó>Promóciós Kód Beírása\" menüpontba.\nÍrd be a kódod majd kattints a \"Küldés\" gombra. Látogasd meg a bombsquadgame.com weboldalt letöltési linkekért, és a támogatott eszközök listájáért.", + "friendPromoCodeRedeemLongText": "${COUNT} jegyet ér és maximum ${MAX_USES} ember használhatja.", + "friendPromoCodeRedeemShortText": "${COUNT} jegyet ér a játékon belül.", + "friendPromoCodeWhereToEnterText": "(itt \"Beállítások->Haladó->Promóciós Kód Beírása\")", + "getFriendInviteCodeText": "Promóciós Kód Lekérése", + "googlePlayDescriptionText": "Hívj meg Google Play játékosokat a társaságodba:", + "googlePlayInviteText": "Meghívás", + "googlePlayReInviteText": "Van ${COUNT} Google Play játékos a társaságodban\naki le lesz csatlakoztatva, ha egy másik meghívást kezdeményezel.\nVedd be őket is az új meghívásba, hogy visszakapd őket.", + "googlePlaySeeInvitesText": "Lásd a Meghívásokat", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play-es verzió)", + "hostPublicPartyDescriptionText": "Nyilvános Játék Indítása:", + "inDevelopmentWarningText": "Megjegyzés:\n\nHálózati játszás egy új és még mindig-fejlődő részleg.\nEgyenlőre, erősen ajánlott, hogy az összes játékos\nlegyen egy Wi-Fi hálózaton.", + "internetText": "Internet", + "inviteAFriendText": "Nincs meg a barátoknak a játék? Hívd meg őket,\nhogy játszanak és ${COUNT} jegy ütheti a markukat.", + "inviteFriendsText": "Barát Meghívása", + "joinPublicPartyDescriptionText": "Belépés Nyilvános Játékba:", + "localNetworkDescriptionText": "Csatlakozz egy társasághoz, a hálózatodon:", + "localNetworkText": "Helyi Hálózat", + "makePartyPrivateText": "Privát Parti", + "makePartyPublicText": "Nyilvános Parti", + "manualAddressText": "Cím", + "manualConnectText": "Csatlakozás", + "manualDescriptionText": "Csatlakozz egy társasághoz cím alapján:", + "manualJoinableFromInternetText": "Tudnak más játékosok csatlakozni hozzád?", + "manualJoinableNoWithAsteriskText": "NEM*", + "manualJoinableYesText": "IGEN", + "manualRouterForwardingText": "*hogy ezt kijávítsd, próbáld meg beállítani a router-edet UDP port átengedése szempontjából a helyi hálózatra ezen: ${PORT}", + "manualText": "Kézi", + "manualYourAddressFromInternetText": "Publikus IP címed:", + "manualYourLocalAddressText": "Lokális IP címed:", + "noConnectionText": "", + "otherVersionsText": "(másik verziók)", + "partyInviteAcceptText": "Elfogad", + "partyInviteDeclineText": "Elutasít", + "partyInviteGooglePlayExtraText": "(lásd a 'Google Play' fület a 'Toborzás' ablakban)", + "partyInviteIgnoreText": "Figyelmen kívül hagy", + "partyInviteText": "${NAME} meg lett hívva hogy \ncsatlakozzon a te társaságodba!", + "partyNameText": "Parti Neve:", + "partySizeText": "parti mérete", + "partyStatusCheckingText": "Ellenőrzés...", + "partyStatusJoinableText": "a partidba most már mások is csatlakozhatnak az internetről", + "partyStatusNoConnectionText": "a szerver nem elérhető", + "partyStatusNotJoinableText": "a partidba nem tudnak mások csatlakozni az internetről", + "partyStatusNotPublicText": "a partid nem nyilvános", + "pingText": "ping", + "portText": "Port", + "requestingAPromoCodeText": "Kód lekérése....", + "sendDirectInvitesText": "Meghívás Küldése", + "sendThisToAFriendText": "Küld el ezt a kódot egy barátodnak:", + "shareThisCodeWithFriendsText": "Küld el ezt a kódot barátaidnak:", + "showMyAddressText": "Címem", + "startAdvertisingText": "Parti Indítása", + "stopAdvertisingText": "Parti Leállítása", + "titleText": "Toborzás", + "wifiDirectDescriptionBottomText": "Ha minden eszköznek van 'Wi-Fi Direct' panelje, akkor meg kellene találniuk és \ncsatlakozniuk egymáshoz. Ha mindegyik eszköz csatlakoztatva van, akkor tudtok alkotni\na társaságot a 'Helyi Hálózatok' fülnél, csak úgy mint a sima Wi-Fi hálózattal.\n\nA legjobb eredményért, a Wi-Fi Direkt hálózat készítőjének kellene lennie a ${APP_NAME} társaság készítőjének is.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct-t arra is lehet használni, hogy Android eszközön közvetlenül, Wi-Fi hálózat\nigénye nélkül is csatlakozhassanak. A legjobban Android 4.2-n és újabbon működik.\n\nHogy ezt használd, nyisd meg a Wi-Fi beállításokat és keress 'Wi-Fi Direct'-et a menüben.", + "wifiDirectOpenWiFiSettingsText": "Wi-Fi Beállítások Megnyitása", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(minden platform közt működik)", + "worksWithGooglePlayDevicesText": "(Google Play-t futtató (android) verziós játékkal működik)", + "youHaveBeenSentAPromoCodeText": "Küldtek neked egy ${APP_NAME} promóciós kódot:" + }, + "getTicketsWindow": { + "freeText": "INGYENES!", + "freeTicketsText": "Ingyen Jegyek", + "inProgressText": "Egy tranzakció már folyamatban van; kérlek próbáld ismét egy pillanat múlva.", + "purchasesRestoredText": "Vásárlások helyreállítva.", + "receivedTicketsText": "${COUNT} jegy megkapva!", + "restorePurchasesText": "Vásárlások Helyreállítása", + "ticketDoublerText": "Jegy Duplázó", + "ticketPack1Text": "Kis Jegy Csomag", + "ticketPack2Text": "Közepes Jegy Csomag", + "ticketPack3Text": "Nagy Jegy Csomag", + "ticketPack4Text": "Jumbo Jegy Csomag", + "ticketPack5Text": "Mammoth Jegy Csomag", + "ticketPack6Text": "Végső Jegy Csomag", + "ticketsFromASponsorText": "Szerezz ${COUNT} jegyet\negy szponzortól", + "ticketsText": "${COUNT} Jegy", + "titleText": "Szerezz Jegyeket", + "unavailableLinkAccountText": "Sajnálom, a vásárlások ezen az eszközön nem elérhetőek.\nHa szeretnéd, ezt a felhasználót csatlakoztathatod egy másik\neszközre és ott vásárolhatsz.", + "unavailableTemporarilyText": "Ez jelenleg elérhetetlen; kérlek próbáld meg később ismét.", + "unavailableText": "Bocs, ez nem elérhetó.", + "versionTooOldText": "Bocs, a játék ezen verziója túl régi; kérlek frissítsd fel egy újabbra.", + "youHaveShortText": "${COUNT} jegyed van", + "youHaveText": "Neked ${COUNT} jegyed van." + }, + "googleMultiplayerDiscontinuedText": "Bocsánat, A Google-nek a többjátékos szervisze többé nem elérhető.\nÉn most egy cserén dolgozok.\nAddig, Kérlek Válasz egy Másik Kapcsolódási lehetőséget.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Mindig", + "fullScreenCmdText": "Teljes képernyő (Cmd-F)", + "fullScreenCtrlText": "Teljes képernyő (Ctrl-F)", + "gammaText": "Gamma", + "highText": "Magas", + "higherText": "Magasabb", + "lowText": "Alacsony", + "mediumText": "Közepes", + "neverText": "Soha", + "resolutionText": "Felbontás", + "showFPSText": "FPS Mutatása", + "texturesText": "Textúrák", + "titleText": "Grafika", + "tvBorderText": "TV Keret", + "verticalSyncText": "Vertikális Szinkron", + "visualsText": "Látvány" + }, + "helpWindow": { + "bombInfoText": "- Bomba -\nErősebb, mint az ütés, de\neredményezhet komoly ön-sértést.\nA legjobb eredményért, dobj közel\naz elenség elé, mielőtt a kanóc kiég.", + "canHelpText": "A ${APP_NAME} tud segíteni.", + "controllersInfoText": "Játszhatsz ${APP_NAME}-ot a barátaiddal egy hálózaton keresztül, vagy\nmind tudtok játszani ugyanazon az eszközön, ha van elég kontrolleretek.\nA ${APP_NAME} támogat különböző fajtákat is; használhatod akár telefonod is\nmint kontroller az ingyenes '${REMOTE_APP_NAME}' alkalmazás által.\nLásd \"Beállítások->Vezérlők\" menüpontot további infókért.", + "controllersText": "Vezérlők", + "controlsSubtitleText": "A barátságos ${APP_NAME} karakterednek van pár alap képessége:", + "controlsText": "Irányítás", + "devicesInfoText": "A VR verziós ${APP_NAME} játszható hálózaton keresztül a sima verzióval,\ntehát nincs szükséged az extra telefonjaidra, tabletedre, és számítógépedre és \nszállj be a játékba. Ez még hasznos is lehet, hogy csatlakozhatsz a\nsima verziós játékkal a VR verzióshoz hogy lehetőséget adj\na nem játszó embereknek, hogy kívülről nézhessék az eseményeket.", + "devicesText": "Eszközök", + "friendsGoodText": "Jó ha vannak. A ${APP_NAME} akkor a legszórakoztatóbb, hogy ha másokkal játszol,\na játék 8 játékost támogat egyszerre, ami ide vezet:", + "friendsText": "Barátok", + "jumpInfoText": "- Ugrás -\nUgorj keresztül kis hasadásokon,\nhogy távolabbra dobj dolgokat, és\nhogy sürgesd az öröm érzetét.", + "orPunchingSomethingText": "Vagy megütni valamit, ledobni egy hegyről, és felrobbantani a lefele úton egy ragadós bombával.", + "pickUpInfoText": "- Felvétel -\nRagadd meg a zászlót, ellenfelet,\nvagy akármi mást ami nincs rögzítve.\nNyomd meg még egyszer az eldobáshoz.", + "powerupBombDescriptionText": "Hagyja, hogy egyszerre három\nbombát is el tudj dobni.", + "powerupBombNameText": "Tripla-Bomba", + "powerupCurseDescriptionText": "Talán ezeket elakarod kerülni.\n...vagy nem?", + "powerupCurseNameText": "Átok", + "powerupHealthDescriptionText": "Helyreállítja a teljes egészségedet.\nSose gondoltad volna.", + "powerupHealthNameText": "Gyógyító-Csomag", + "powerupIceBombsDescriptionText": "Gyengébb mint a normál bombák,\nde hülten hagyja ellenfeleidet\nés különösen törékenyen.", + "powerupIceBombsNameText": "Jég-Bombák", + "powerupImpactBombsDescriptionText": "Némileg gyengébb, mint az eredeti\nbombák, de becsapódásra robannak.", + "powerupImpactBombsNameText": "Ravasz-Bombák", + "powerupLandMinesDescriptionText": "Ezek 3-asával jönnek egy csomagba;\nHasznos a bázis védelemre vagy\na gyors ellenfelek megállítására.", + "powerupLandMinesNameText": "Taposó-akna", + "powerupPunchDescriptionText": "Felerősíti, gyorsítja, jobbá,\nés keményebbé teszi ütéseidet.", + "powerupPunchNameText": "Box-Kesztyűk", + "powerupShieldDescriptionText": "Elnyeli a sérülés egy részét,\nszóval neked nem kell.", + "powerupShieldNameText": "Energia-Pajzs", + "powerupStickyBombsDescriptionText": "Ragadnak mindenhez amit megütnek.\nVidámság következik.", + "powerupStickyBombsNameText": "Ragadós-Bombák", + "powerupsSubtitleText": "Természetesen, nincs is játék erőfokozók nélkül:", + "powerupsText": "Erőfokozók", + "punchInfoText": "- Ütés -\nÜtések nagyobb sérülést okoznak az\nöklöd mozgási sebességével, tehát\nfuss és pörgj mint egy őrült.", + "runInfoText": "- Futás -\nTarts lenyomva AKÁRMILYEN gombot a futáshoz. Adagolós gombok jól működnek, már ha van olyanod (vezérlők tetején \nLT,LB,RT,RB vagy L1,L2,R1,R2) A futás gyorsabban juttat el helyekre, de megnehezíti a fordulást, szóval vigyázz hova lépsz.", + "someDaysText": "Néha úgy érzed, hogy bele vernél valamibe. Vagy csak felrobbantanál valamit.", + "titleText": "${APP_NAME} Segítség", + "toGetTheMostText": "Hogy mindent kihozz ebből a játékből, szükséged lesz:", + "welcomeText": "Üdvözlet a ${APP_NAME}-ban!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} navigálja most a menüt -", + "importPlaylistCodeInstructionsText": "Használd ezt a kódot, hogy áthelyezd ezt a lejátszási listát:", + "importPlaylistSuccessText": "A ${TYPE} típusú '${NAME}' nevű lejátszási lista sikeresen áthelyezve", + "importText": "Importálás", + "importingText": "Bemásolás...", + "inGameClippedNameText": "játékbeli alak:\n\"${NAME}\"", + "installDiskSpaceErrorText": "Hiba: Nem lehet teljesíteni a telepítést.\nTalán kifogytál a szabadhelyből az eszközödön.\nTakaríts egy kis helyet, és próbáld újra.", + "internal": { + "arrowsToExitListText": "nyomj ${LEFT}-t vagy ${RIGHT}-t a lista bezárásához", + "buttonText": "gomb", + "cantKickHostError": "Nem rúghatod ki a host-ot.", + "chatBlockedText": "${NAME} ki lett tiltva a chatből ${TIME} mp.-re.", + "connectedToGameText": "Beléptél ide: ${NAME}", + "connectedToPartyText": "Csatlakoztál ${NAME} társaságába!", + "connectingToPartyText": "Csatlakozás...", + "connectionFailedHostAlreadyInPartyText": "Csatlakozás nem sikerült; a kiszolgáló másik társaságban van.", + "connectionFailedPartyFullText": "Sikertelen csatlakozás;a parti tele van.", + "connectionFailedText": "Csatlakozás nem sikerült.", + "connectionFailedVersionMismatchText": "Csatlakozás nem sikerült; a kiszolgáló a játék másik verzióját futtatja.\nBizonyosodj meg róla, mindkettőtöknek naprakész és próbáljátok meg újra.", + "connectionRejectedText": "Csatlakozás elutasítva.", + "controllerConnectedText": "${CONTROLLER} csatlakoztatva.", + "controllerDetectedText": "1 vezérlő észlelve.", + "controllerDisconnectedText": "${CONTROLLER} lecsatlakoztatva.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} lecsatlakoztatva. Kérlek próbáld meg újra csatlakoztatni.", + "controllerForMenusOnlyText": "Ezt a típusú kontrollert nem használhatod játékra; csak navigálásra a menüpontokon.", + "controllerReconnectedText": "${CONTROLLER} újracsatlakoztatva.", + "controllersConnectedText": "${COUNT} vezérlő csatlakoztatva.", + "controllersDetectedText": "${COUNT} vezérlő észlelve", + "controllersDisconnectedText": "${COUNT} vezérlő lecsatlakoztatva.", + "corruptFileText": "Korrupt fájl(ok) észlelve. Kérlek próbáld meg újra telepíteni, vagy küld el a ${EMAIL}-ra", + "errorPlayingMusicText": "Hiba a zene lejátszása közben: ${MUSIC}", + "errorResettingAchievementsText": "Nem lehet visszaállítani az achievementeket, próbálkozz később.", + "hasMenuControlText": "${NAME} vezérli a menüt.", + "incompatibleNewerVersionHostText": "A hosztoló a játék újabb verzióját használja.\nFrissítsd az appot, malyd próbáld újra.", + "incompatibleVersionHostText": "A kiszolgáló más virzió számú játékot futtat.\nBizonyosodj megróla hogy mimdkettőtöké naprakész és próbáld újra.", + "incompatibleVersionPlayerText": "${NAME} más BombSquad verziót használ. Győződjetek meg\nróla, hogy mindkettőtöknek a legfrissebb verzió van meg.", + "invalidAddressErrorText": "Hiba: érvénytelen cím.", + "invalidNameErrorText": "Hiba: helytelen név.", + "invalidPortErrorText": "Hiba:érvénytelen port.", + "invitationSentText": "Meghívó elküldve.", + "invitationsSentText": "${COUNT} db meghívó elküldve.", + "joinedPartyInstructionsText": "Valaki csatlakozott a partidhoz.\nMenj a 'Játék'-ra hogy elkezd a játékot.", + "keyboardText": "Billentyűzet", + "kickIdlePlayersKickedText": "${NAME} kirúgva tétlenség miatt.", + "kickIdlePlayersWarning1Text": "${NAME} ki lesz rúgva ${COUNT} másodpercen beül ha továbbra is tétlen marad.", + "kickIdlePlayersWarning2Text": "(ezt ki tudod kapcsolni itt: Beállítások->Haladó)", + "leftGameText": "Kiléptél innen: ${NAME}", + "leftPartyText": "Elhagytad ${NAME} társaságát.", + "noMusicFilesInFolderText": "A mappa nem tartalmaz zenei fájlokat.", + "playerJoinedPartyText": "${NAME} csatlakozott a partihoz!", + "playerLeftPartyText": "${NAME} kilépett a partiból.", + "rejectingInviteAlreadyInPartyText": "Meghívó elutasítva (már benne van a partiban).", + "serverRestartingText": "A szerver Ujraindul. Kérlek csatlakozz vissza egy pillanat után...", + "serverShuttingDownText": "A Szerver lekapcsol...", + "signInErrorText": "Hiba a bejelentkezésnél.", + "signInNoConnectionText": "Nem lehet bejelentkezni. (nincs internet kapcsolat?)", + "telnetAccessDeniedText": "HIBA: a felhasználónak nincs telnet hozzáférése", + "timeOutText": "(Kifagyás ${TIME} másodpercen belül)", + "touchScreenJoinWarningText": "Az érintőképernyővel csatlakoztál.\nHa ez egy hiba, akkor menj ide: Menü->Játék Elhagyása.", + "touchScreenText": "Érintőképernyő", + "unableToResolveHostText": "Hiba: a host nem elérhető.", + "unavailableNoConnectionText": "Ez a funkció nem elérhető (Nincs internet kapcsolat?).", + "vrOrientationResetCardboardText": "Használd ezt, hogy visszaállíts a VR orientációkat.\nHogy játszhass csatlakoztatnod kell egy kontrollert.", + "vrOrientationResetText": "VR orientáció újraindítása.", + "willTimeOutText": "(lejár az idő, ha tétlen)" + }, + "jumpBoldText": "UGORJ", + "jumpText": "Ugorj", + "keepText": "Tartsd", + "keepTheseSettingsText": "Megtartja ezeket a beállításokat?", + "kickOccurredText": "${NAME} ki lett rúgva.", + "kickQuestionText": "Kirúgod őt:${NAME}?", + "kickText": "Kirúgás", + "kickVoteCantKickAdminsText": "Adminokat Nem lehet Kidobni.", + "kickVoteCantKickSelfText": "Nem tudod magadat kirugni.", + "kickVoteFailedNotEnoughVotersText": "Nincs elég játékos a szavazáshoz.", + "kickVoteFailedText": "Kirúgási-indítvány sikertelen.", + "kickVoteStartedText": "Kirúgási-szavazás indult ${NAME} ellen.", + "kickVoteText": "Kirúgási indítvány", + "kickVotingDisabledText": "Kirugási szavazás ki van kapcsolva.", + "kickWithChatText": "Írd be a chatbe, hogy:${YES},ha igennel szavazol vagy azt, hogy:${NO}, ha nemmel.", + "killsTallyText": "${COUNT} ölés", + "killsText": "Ölések", + "kioskWindow": { + "easyText": "Könnyű", + "epicModeText": "Hatalmas mód", + "fullMenuText": "Teljes menü", + "hardText": "Nehéz", + "mediumText": "Közepes", + "singlePlayerExamplesText": "Egyszemélyes / Co-op példák", + "versusExamplesText": "Egymás elleni példák" + }, + "languageSetText": "A most használt nyelv:\"${LANGUAGE}\".", + "lapNumberText": "Kör ${CURRENT}/${TOTAL}", + "lastGamesText": "(utolsó ${COUNT} játék)", + "leaderboardsText": "Ranglisták", + "league": { + "allTimeText": "Minden Idő", + "currentSeasonText": "Jelenlegi szezon (${NUMBER})", + "leagueFullText": "${NAME} Liga", + "leagueRankText": "Liga Helyezés", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga${SUFFIX}", + "seasonEndedDaysAgoText": "A liga befejeződött ${NUMBER} napja.", + "seasonEndsDaysText": "A Szezonból hátra van még:${NUMBER} nap.", + "seasonEndsHoursText": "Szezonból hátralévő idő:${NUMBER} óra.", + "seasonEndsMinutesText": "Szezonból hátralévő idő:${NUMBER} perc.", + "seasonText": "Szezon ${NUMBER}", + "tournamentLeagueText": "Hogy ebbe a tornába be tudj nevezni,el kell érned ezt a ligát:${NAME}", + "trophyCountsResetText": "A trófeák a következő szezonban lenullázódnak." + }, + "levelBestScoresText": "Legjobb eredmények a ${LEVEL}-es szinten.", + "levelBestTimesText": "Legjobb időeredmények a ${LEVEL}-es szinten", + "levelFastestTimesText": "Legjobb idők a ${LEVEL}-n", + "levelHighestScoresText": "Legmagasabb pontszám a ${LEVEL}-n", + "levelIsLockedText": "${LEVEL} le van zárva.", + "levelMustBeCompletedFirstText": "Előszőr a ${LEVEL}-t kell teljesítened.", + "levelText": "Szint:${NUMBER}", + "levelUnlockedText": "Szint Feloldva!", + "livesBonusText": "Élet Bónusz", + "loadingText": "betöltés", + "loadingTryAgainText": "Próbáld újra később...", + "macControllerSubsystemBothText": "Mindkettő(nem ajánlott)", + "macControllerSubsystemClassicText": "Klasszikus", + "macControllerSubsystemDescriptionText": "(Próbáld megváltoztatni ezt, ha a kontroller nem működik)", + "macControllerSubsystemMFiNoteText": "iOS/Mac kontroller észlelve;\nEngedélyezd őket itt: Beállítások->Kontrollerek", + "macControllerSubsystemMFiText": "iOS/Mac", + "macControllerSubsystemTitleText": "Kontroller Támogatás", + "mainMenu": { + "creditsText": "Közreműködők", + "demoMenuText": "Demo Menü", + "endGameText": "Játék Vége", + "exitGameText": "Kilépés a Játékból", + "exitToMenuText": "Kilépés a menübe?", + "howToPlayText": "Tippek", + "justPlayerText": "(Csak ${NAME})", + "leaveGameText": "Játék Elhagyása", + "leavePartyConfirmText": "Tényleg elhagyod a társaságot?", + "leavePartyText": "Társaság Elhagyása", + "quitText": "Kilépés", + "resumeText": "Folytatás", + "settingsText": "Beállítások" + }, + "makeItSoText": "Tedd meg annak", + "mapSelectGetMoreMapsText": "Szerezz Több Pályát", + "mapSelectText": "Válassz...", + "mapSelectTitleText": "${GAME} Pályák", + "mapText": "Pálya", + "maxConnectionsText": "Max. Eszközök", + "maxPartySizeText": "Maximum Parti Méret", + "maxPlayersText": "Max. Játékosok", + "mostValuablePlayerText": "Legértékesebb Játékos", + "mostViolatedPlayerText": "Legtöbbször Megölt Játékos", + "mostViolentPlayerText": "Legerőszakosabb Játékos", + "moveText": "Mozgás", + "multiKillText": "${COUNT}-ÖLÉS!!!", + "multiPlayerCountText": "${COUNT} játékos", + "mustInviteFriendsText": "Jegyezd meg: meg kell hívnod a barátaidat \na \"${GATHER}\" panelbenvagy csatlakoztatni\negy vezérlőt a többjátékos mód játszásához.", + "nameBetrayedText": "${NAME} elárulta ${VICTIM}-t.", + "nameDiedText": "${NAME} meghalt.", + "nameKilledText": "${NAME} megölte ${VICTIM}-t.", + "nameNotEmptyText": "A név nem lehet üres!", + "nameScoresText": "${NAME} Pontot Szerzett!", + "nameSuicideKidFriendlyText": "${NAME} véletlenül meghalt", + "nameSuicideText": "${NAME} öngyilkosságot követett el.", + "nameText": "Név", + "nativeText": "Eredeti", + "newPersonalBestText": "Új személyi rekord!", + "newTestBuildAvailableText": "Egy újabb teszt szerkezet elérhető! (${VERSION} build ${BUILD}).\nSzerezd meg a ${ADDRESS}-n", + "newText": "Új", + "newVersionAvailableText": "A ${APP_NAME} újabb verziója elérhető! (${VERSION})", + "nextAchievementsText": "Következő Teljesítmények:", + "nextLevelText": "Következő Szint", + "noAchievementsRemainingText": "- semmi", + "noContinuesText": "(újraéledés nélkül)", + "noExternalStorageErrorText": "Nincs megtalálható külső tárhely ezen az eszközön", + "noGameCircleText": "Hiba: nincs bejelentkezve egy JátékKörbe", + "noProfilesErrorText": "Nincs játékos profilod, tehát te most '${NAME}'-val nyomulsz.\nMenj a Beállítások->Játékos Profilok-ba hogy csinálj magadnak egy profilt-", + "noScoresYetText": "Nincs még pontod.", + "noThanksText": "Nem Köszönöm", + "noTournamentsInTestBuildText": "FIGYELEM: A tournament pontok ebből a teszt épitésből ki lesznek hagyva.", + "noValidMapsErrorText": "Nem található pálya ehhez a játéktípushoz.", + "notEnoughPlayersRemainingText": "Nincs elég játékos. Lépj ki és kezdj egy új játékot.", + "notEnoughPlayersText": "A játék elindításához, minimum ${COUNT} játékos szükséges!", + "notNowText": "Most Nem", + "notSignedInErrorText": "Be kell jelentkezned a művelethez.", + "notSignedInGooglePlayErrorText": "Be kell jelentkezned Google Play-el a művelethez.", + "notSignedInText": "nem vagy bejelentkezve", + "nothingIsSelectedErrorText": "Nem választottál ki semmit!", + "numberText": "#${NUMBER}", + "offText": "Ki", + "okText": "Ok", + "onText": "Be", + "onslaughtRespawnText": "${PLAYER} újraéled a következő körben (${WAVE})", + "orText": "${A} vagy ${B}", + "otherText": "Egyéb...", + "outOfText": "(#${RANK} a ${ALL}-ból/-ből)", + "ownFlagAtYourBaseWarning": "A saját zászlódnak a bázisodon\nkell tartózkodnia, a pontszerzéshez.", + "packageModsEnabledErrorText": "A hálózati játék nem engedélyezett amíg a Helyi modok be vannak kapcsolva (lásd Beállítások->Haladó)", + "partyWindow": { + "chatMessageText": "Üzenet:", + "emptyText": "Senki sincs a partiban", + "hostText": "(házigazda)", + "sendText": "Küld", + "titleText": "A Partid" + }, + "pausedByHostText": "(szüneteltetve a házigazda által)", + "perfectWaveText": "Tökéletes Hullám!", + "pickUpText": "Felvétel", + "playModes": { + "coopText": "Co-op", + "freeForAllText": "Mindenki mindenki ellen", + "multiTeamText": "Multi-csapat", + "singlePlayerCoopText": "Egy Játékos / Co-op", + "teamsText": "Csapat" + }, + "playText": "Játék", + "playWindow": { + "oneToFourPlayersText": "1-4 játékos", + "titleText": "Játék", + "twoToEightPlayersText": "2-8 játékos" + }, + "playerCountAbbreviatedText": "${COUNT}játékos", + "playerDelayedJoinText": "${PLAYER} a következő körben fog csatlakozni a játékhoz.", + "playerInfoText": "Játékos Információ", + "playerLeftText": "${PLAYER} elhagyta a játékot.", + "playerLimitReachedText": "A játékos limit elérte a határt: ${COUNT}. Többen nem csatlakozhatnak.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Nem lehet törölni a főprofilt.", + "deleteButtonText": "Profil\nTörlése", + "deleteConfirmText": "Törlöd a '${PROFILE}' profilt?", + "editButtonText": "Profil\nSzerkesztése", + "explanationText": "(egyedi karakter nevek és kinézetek ezen a fiókon)", + "newButtonText": "Új \nProfil", + "titleText": "Profilok" + }, + "playerText": "Játékos", + "playlistNoValidGamesErrorText": "Ez a lista tartalmaz még nem feloldott játékokat.", + "playlistNotFoundText": "A lista nem található", + "playlistsText": "Listák", + "pleaseRateText": "Ha élvezed a játékot, kérlek értékeld, vagy írj egy\nvéleményt a ${APP_NAME}-ról. Ez mások számára hasznos \nlehet, vagy akár segítheti a játék fejlődését is a jövőben.\n\nKöszi!\n-eric", + "pleaseWaitText": "Kérljek várj...", + "pluginsDetectedText": "Új Plugin találva. Kapcsold/Configuráld őket a beálitásokba.", + "pluginsText": "Pluginok", + "practiceText": "Gyakorlás", + "pressAnyButtonPlayAgainText": "Nyomj meg egy gombot, az újrakezdéshez...", + "pressAnyButtonText": "Nyomj meg egy gombot a folytatáshoz...", + "pressAnyButtonToJoinText": "nyomj meg egy gombot a csatlakozáshoz...", + "pressAnyKeyButtonPlayAgainText": "Nyomj meg bármilyen billenyűt/gombot az újrajátszáshoz...", + "pressAnyKeyButtonText": "Nyomj meg bármilyen gombot a folytatáshoz...", + "pressAnyKeyText": "Nyomj meg bármilyen gombot....", + "pressJumpToFlyText": "**Nyomd az ugrást ismételten a repüléshez**", + "pressPunchToJoinText": "Nyomd meg az ütést a csatlakozáshoz...", + "pressToOverrideCharacterText": "Nyomd meg a ${BUTTONS} gombot hogy megváltoztasd a karaktered", + "pressToSelectProfileText": "Nyomd meg a ${BUTTONS}-ot karakterválasztáshoz", + "pressToSelectTeamText": "Nyomd meg a ${BUTTONS}-ot csapatválasztáshoz", + "promoCodeWindow": { + "codeText": "Kód", + "codeTextDescription": "Promóciós Kód", + "enterText": "Küldés" + }, + "promoSubmitErrorText": "Hiba a promóciós kód aktiválásánál; ellenőrizd az internetkapcsolatodat", + "ps3ControllersWindow": { + "macInstructionsText": "Kapcsold ki a PS3-dat majd ellenőrizd ,hogy a MAC-eden \nbiztosan be van-e kapcsolva a Bluetooth, majd \ncsatlakoztasd a kontrollert egy USB kábellel a MAC-hez.\nMost már használhatod a kontroller 'home' gombját hogy \npárosítsd a MAC-et USB-n vagy Bluetooth-on.\n\nNéhány MAC-en meg kell adni a párosítási kódot.\nHa ez történt próbáld meg követni a leírást \nvagy használd a google-t.\n\nHa a PS3 kontrollert vezeték nélkül párosítod akkor meg kell jelennie a\nlistában itt:Preferenciák->Bluetooth.El kell a listából távolítanod \n,hogyha a PS3-dal szeretnéd újra használni a kontrollert.\n\nGyőződj meg róla ,hogy leválasztottad a \nbluetooth-ról ha nem használod mivel \nha ezt nem teszed meg az \nakkumulátor tovább fog merülni.\n\nA bluetooth 7 eszközt tud kezelni,\nde ez a szám változhat.", + "ouyaInstructionsText": "Ha PS3 kontrollert szeretnél használni az OUYA-hoz egyszerűen \ncsak csatlakoztasd a kontrollert egy USB kábellel.\nMiközben ezt csinálod lehet ,hogy a többi kontrollered lecsatlakozik \nígy majd újra kell indítanod az OUYA-át és kihúzni az USB-t.\n\nHogy ha vezeték nélkül szeretnél játszani használd a kontrollert \n'home' gombját.Ha már nem játszol tartsd lenyomva 10 másodpercig a \n'home' gombot ,hogy kikapcsold azt.", + "pairingTutorialText": "párosítást bemutató videó", + "titleText": "PS3 kontroller használata a ${APP_NAME}-ban:" + }, + "punchBoldText": "ÜTÉS", + "punchText": "Ütés", + "purchaseForText": "Megvétel ennyiért:${PRICE}", + "purchaseGameText": "Játék megvásárlása", + "purchasingText": "Vásárlás folyamatban...", + "quitGameText": "Kilépsz a ${APP_NAME}-ból?", + "quittingIn5SecondsText": "Kilépés 5 másodpercen belül...", + "randomPlayerNamesText": "ALAP_NEVEK András, Béla, Csaba, Dániel, Elemér, Ferenc, Gábor, Herold, Imre, János, Károly, László, Márkó, Nándor, Ottó, Péter, Roland, Simon, Tamás, Ubul, Vajk, Walter, Zoltán", + "randomText": "Véletlenszerű", + "rankText": "Helyezés", + "ratingText": "Értékelés", + "reachWave2Text": "Érd el a 2. hullámot a helyezésért.", + "readyText": "kész", + "recentText": "Legutóbbi", + "remoteAppInfoShortText": "A ${APP_NAME} akkor a legjobb hogyha a barátaiddal játszod.\nCsatlakoztass kontrollereket vagy telepítsd a ${REMOTE_APP_NAME}\nnevű alkalmazást a telefonodra vagy tabletedre és használd ezeket\nkontrollerként.", + "remote_app": { + "app_name": "BombSquad Irányító", + "app_name_short": "BsIrányító", + "button_position": "Gomb helye", + "button_size": "Gomb mérete", + "cant_resolve_host": "Nem elemezhető hálózat.", + "capturing": "Foglalás...", + "connected": "Kapcsolódva.", + "description": "Használd a telefonod vagy tableted egy irányítóként a BombSqaud-hez.\nAkár 8 eszköz tud kapcsolódni egyszerre egy epikus többjátékos őrülethez TV-n vagy Tableten.", + "disconnected": "Lecsatlakozva a szerver miatt.", + "dpad_fixed": "megoldva", + "dpad_floating": "repülés", + "dpad_position": "D-Pad Helye", + "dpad_size": "D-pad Mérete", + "dpad_type": "D-Pad Típusa", + "enter_an_address": "Belépés címről.", + "game_full": "Ez a játék tele van vagy nem fogad kapcsolatokat.", + "game_shut_down": "Ez a játék leállt.", + "hardware_buttons": "Hardver Gombok.", + "join_by_address": "Cím alapján való csatlakozás...", + "lag": "Lagg: ${SECONDS} másodpercig.", + "reset": "Visszaállítás alap értelmezettre.", + "run1": "1 Futtatása", + "run2": "2 Futtatása", + "searching": "Keresés BombSquad játékra...", + "searching_caption": "Nyomj annak a játéknak a nevére amelynez csatlakozni akarsz.\nLegyél biztos abba hogy ugyan azon a wifi hálozaton van a játék.", + "start": "Kezdés", + "version_mismatch": "Verzió hiba.\nLégy biztos abban hogy a BombSqad Irányító\na legújabb verzióval rendelkezik, és próbáld újra." + }, + "removeInGameAdsText": "Old fel a \"${PRO}\"-t az áruházból hogy eltávolítsd a játékbeli reklámokat.", + "renameText": "Átnevezés", + "replayEndText": "Visszajátszás Befejezése", + "replayNameDefaultText": "Utolsó Játék visszajátszás", + "replayReadErrorText": "Hiba a visszajátszási fájlok olvasásában.", + "replayRenameWarningText": "Nevezd át \"${REPLAY}\"-t egy játék után ha megakarod tartani; máskülönben felül lesz írva.", + "replayVersionErrorText": "Hiba: Ez a visszajátszás még a régebbi verzióval\nkészült, tehát nem használható.", + "replayWatchText": "Visszajátszás visszanézése", + "replayWriteErrorText": "Hiba a fájl írása során.", + "replaysText": "Visszajátszások", + "reportPlayerExplanationText": "Használd ezt az emailt hogy feljelents csalást, trágár nyelhasználatot , vagy más szabálysért.\nKérlek ide írd:", + "reportThisPlayerCheatingText": "Csalás", + "reportThisPlayerLanguageText": "Trágár Nyelvhasználat", + "reportThisPlayerReasonText": "Miért akarsz feljelentést tenni?", + "reportThisPlayerText": "Játékos Jelentése", + "requestingText": "Lekérés...", + "restartText": "Újraindítás", + "retryText": "Újrapróbálás", + "revertText": "Visszateker", + "runText": "Futás", + "saveText": "Mentés", + "scanScriptsErrorText": "Hiba a szkriptek szkennelésében; részletek a naplóban.", + "scoreChallengesText": "Pontszám Kihívások", + "scoreListUnavailableText": "A pontszám lista nem elérhető.", + "scoreText": "Pont", + "scoreUnits": { + "millisecondsText": "Milliszekundum", + "pointsText": "Pontok", + "secondsText": "Másodperc" + }, + "scoreWasText": "(${COUNT} volt)", + "selectText": "Válassz", + "seriesWinLine1PlayerText": "NYERTE MEG A", + "seriesWinLine1TeamText": "NYERTE MEG A", + "seriesWinLine1Text": "NYERTE MEG A", + "seriesWinLine2Text": "SOROZATOT", + "settingsWindow": { + "accountText": "Felhasználó", + "advancedText": "Haladó", + "audioText": "Hang", + "controllersText": "Vezérlők", + "graphicsText": "Grafika", + "playerProfilesMovedText": "Megjegyzés: A felhasználói profilok át lettek helyezve a fiókok ablakhoz a főmenübe.", + "playerProfilesText": "Játékos Profil", + "titleText": "Beállítások" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(kontroller barát billentyűzet a szöveg szerkesztéséhez)", + "alwaysUseInternalKeyboardText": "Használjon Belső Billentyűzetet", + "benchmarksText": "Teljesítmény és Stressz Teszt", + "disableCameraGyroscopeMotionText": "Kamera Gyroscope kikapcsolása", + "disableCameraShakeText": "Kamera rázást Kikapcsolni", + "disableThisNotice": "(kikapcsolhatod ezt a beállítások menüben)", + "enablePackageModsDescriptionText": "(engedélyez a plusz helyeket a modoknak, viszont letiltja a hálózati játékot)", + "enablePackageModsText": "Helyi Modok Engedélyezése", + "enterPromoCodeText": "Kód beírása", + "forTestingText": "Megj.:ezek az értékek csak tesztek és az alkalmazás bezárásával együtt törlődnek.", + "helpTranslateText": "A ${APP_NAME} nem Angol fordításait a közösség végzi.\nHa szeretnél fordítani vagy hibát javítani akkor \nhasználd ezt a linket. Előre is köszönöm!", + "kickIdlePlayersText": "Tétlen Játékosok Kirúgása", + "kidFriendlyModeText": "Gyerekbarát Mód (erőszak csökkentése,stb.)", + "languageText": "Nyelv", + "moddingGuideText": "Modolási Útmutató", + "mustRestartText": "Újra kell indítanod a játékot a módosítások érvénybe lépéséhez.", + "netTestingText": "Hálózat Tesztelése", + "resetText": "Visszaállítás", + "showBombTrajectoriesText": "Bomba Pályájának Mutatása", + "showPlayerNamesText": "Játékos Név Mutatása", + "showUserModsText": "Mod Mappák Mutatása", + "titleText": "Haladó", + "translationEditorButtonText": "${APP_NAME} Fordítás Szerkesztő", + "translationFetchErrorText": "fordítási státusz elérhetetlen", + "translationFetchingStatusText": "Fordítási státusz ellenőrzése...", + "translationInformMe": "Értesítsen, ha a nyelvem fordítását frissíteni kell", + "translationNoUpdateNeededText": "A jelenlegi nyelv naprakész; woohoo!", + "translationUpdateNeededText": "**a jelenlegi nyelv frissítésre szorul!!**", + "vrTestingText": "VR Tesztelése" + }, + "shareText": "Megosztás", + "sharingText": "Megosztás...", + "showText": "Mutatott Liga:", + "signInForPromoCodeText": "A promóciós kódok csak akkor érvényesíthetőek, hogy ha be vagy lépve egy profilba.", + "signInWithGameCenterText": "Hogy használd a Játék Központ felhasználódat\njelentkezz be a Játék Központban.", + "singleGamePlaylistNameText": "Csak ${GAME}", + "singlePlayerCountText": "1 játékos", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Karakter Választás", + "Chosen One": "A kiválasztott", + "Epic": "Hatalmas Módú Játékok", + "Epic Race": "Hatalmas Verseny", + "FlagCatcher": "Szerezd meg a Zászlót", + "Flying": "Boldog Gondolatok", + "Football": "Football", + "ForwardMarch": "Roham", + "GrandRomp": "Hóditás", + "Hockey": "Hoki", + "Keep Away": "Tartsd Távol", + "Marching": "Szaladgálás", + "Menu": "Főmenü", + "Onslaught": "Támadás", + "Race": "Verseny", + "Scary": "A Hegy Királya", + "Scores": "Ponttáblázat", + "Survival": "Kirekesztés", + "ToTheDeath": "Death Match", + "Victory": "Végső Ponttáblázat" + }, + "spaceKeyText": "space", + "statsText": "Statisztikák", + "storagePermissionAccessText": "Ehhez tárhelyhozzáférés szükséges", + "store": { + "alreadyOwnText": "Már birtoklod ezt:${NAME}", + "bombSquadProDescriptionText": "-Megduplázza a jegyek szerzését az eredményekért\n-Eltávolítja a hírdetéseket\n-Tartalmaz ${COUNT} ajándék jegyet\n-+${PERCENT}% liga pont bónusz\n-Feloldja a '${INF_ONSLAUGHT}' és\na '${INF_RUNAROUND}' co-op pályákat", + "bombSquadProFeaturesText": "Jellemzők:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "Eltávolítja a reklámokat.\nTöbb játék beállítást nyit meg.\nÉs tartalmaz még:", + "buyText": "Megvesz", + "charactersText": "Karakterek", + "comingSoonText": "Hamarosan...", + "extrasText": "Extrák", + "freeBombSquadProText": "A BombSquad most ingyenes, de mivel te eredetileg megvetted, ezért \nmegkapod a BombSquad Pro frissítést és ${COUNT} jegyet hála jeléül.\nÉlvezd az új jellemzőket, és kösz a támogatást!\n-Eric", + "gameUpgradesText": "Játék Frissítések", + "holidaySpecialText": "Ünnepi Különlegességek", + "howToSwitchCharactersText": "(menj a \"${SETTINGS} -> ${PLAYER_PROFILES}\"-ba, hogy kijelölj és testre szabj karaktereket)", + "howToUseIconsText": "(készíts egy teljes körű profilt (a fiók ablakban) hogy használhasd ezeket)", + "howToUseMapsText": "(használd ezeket a pályákat a saját játék listáidban)", + "iconsText": "Ikonok", + "loadErrorText": "Nem lehet betölteni az oldalt.\nEllenőrizd internet kapcsolatodat.", + "loadingText": "betöltés", + "mapsText": "Pályák", + "miniGamesText": "MiniJátékok", + "oneTimeOnlyText": "(csak egyszer)", + "purchaseAlreadyInProgressText": "Egy vásárlása ezen tárgynak, már folyamatban van.", + "purchaseConfirmText": "Megveszed ${ITEM}?", + "purchaseNotValidError": "Vásárlás nem érvényes\nLépj kapcsolatba a ${EMAIL} címmel, ha ez egy hiba", + "purchaseText": "Vásárlás", + "saleBundleText": "Csomag Akció!", + "saleExclaimText": "Akció!", + "salePercentText": "(${PERCENT}%-os leárazás)", + "saleText": "AKCIÓ", + "searchText": "Keresés", + "teamsFreeForAllGamesText": "Csapatok / Mindenki-Mindenki-ellen Játékok", + "totalWorthText": "*** ${TOTAL_WORTH} Érték! ***", + "upgradeQuestionText": "Fejelszti?", + "winterSpecialText": "Téli Különlegesség", + "youOwnThisText": "- rendelkezel ezzel -" + }, + "storeDescriptionText": "8 Játékos Parti Játék Őrület!\n\nRobbantsd fel a barátaidat (vagy a gépet) a kirobbanó jó mini-játékokkal rendelkező tornákon mint pl: Szerezd meg a Zászlót,Bombázó Jéghoki és Látványos Lassított Death Match!\n\nEgyszerű irányítás és csatlakoztatható kontrollerek teszik lehetővé akár 8-an is csatlakozhassatok a játékhoz.Akár a telefonodat is használhatod mint kontroller a BomSquad Remote ingyenes alkalmazással.\n\nBombára Fel!\n\nA további információkért látogass el ide-> www.froemling.net/bombsquad", + "storeDescriptions": { + "blowUpYourFriendsText": "Robbantsd fel barátaidat.", + "competeInMiniGamesText": "Teljesíts a mini-játékokban a versenyzéstől a repülésig.", + "customize2Text": "Szabd testre a karaktereket, mini-játékokat, és még a betétdalokat is.", + "customizeText": "Szabj testre karaktereket és alkosd meg saját mini-játék lejátszási listádat.", + "sportsMoreFunText": "A sportok még mókásabbak robbanószerekkel.", + "teamUpAgainstComputerText": "Alkoss csapatot a számítógép ellen." + }, + "storeText": "Bolt", + "submitText": "Érvényesítsd", + "submittingPromoCodeText": "Promóciós Kód Érvényesítése...", + "teamNamesColorText": "Csapatnevek/csapatszínek...", + "telnetAccessGrantedText": "Telnet hozzáférés engedélyezve.", + "telnetAccessText": "Telnet hozzáférés észlelve. Engedélyezed?", + "testBuildErrorText": "Ez a teszt verzió többé nem elérhető;kérlek nézz utána egy újabb verziónak.", + "testBuildText": "Teszt Verzió", + "testBuildValidateErrorText": "A Teszt Verzió nem lehet ellenőrizni.(nincs internet?)", + "testBuildValidatedText": "A Teszt Verzió Naprakész;Élvezd!", + "thankYouText": "Köszi a támogatást. Jó játékot!", + "threeKillText": "TRIPLA ÖLÉS!", + "timeBonusText": "Idő Bónusz", + "timeElapsedText": "Eltelt Idő", + "timeExpiredText": "Lejárt az idő", + "timeSuffixDaysText": "${COUNT}n", + "timeSuffixHoursText": "${COUNT}ó", + "timeSuffixMinutesText": "${COUNT}p", + "timeSuffixSecondsText": "${COUNT}mp", + "tipText": "Tipp", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Top Barátok", + "tournamentCheckingStateText": "Torna állapotának ellenőrzése;Kérlek várj...", + "tournamentEndedText": "Ez a torna befejeződött.Az új torna hamarosan kezdődik.", + "tournamentEntryText": "Belépés a Tornába", + "tournamentResultsRecentText": "Legutóbbi Tornában elért helyezésed", + "tournamentStandingsText": "Torna Állása", + "tournamentText": "Bajnokság", + "tournamentTimeExpiredText": "Mérkőzési Idő Lejárt", + "tournamentsText": "Bajnokságok", + "translations": { + "characterNames": { + "Agent Johnson": "Johnson Ügynök", + "B-9000": "B-9000", + "Bernard": "Bernard", + "Bones": "Csonti", + "Butch": "Billy", + "Easter Bunny": "Húsvéti Nyúl", + "Flopsy": "Bolyhos", + "Frosty": "Fagyos", + "Gretel": "Gréta", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Morgan Jack", + "Kronk": "Kronk", + "Lee": "Boróka", + "Lucky": "Manó", + "Mel": "Mel", + "Middle-Man": "Pannónia Kapitány", + "Minimus": "Minimusz", + "Pascal": "Pingu", + "Pixel": "Pixi", + "Sammy Slam": "Sam", + "Santa Claus": "Mikulás", + "Snake Shadow": "Kígyó Árny", + "Spaz": "Spaz", + "Taobao Mascot": "Taobao Mascot", + "Todd McBurton": "Termi Nándor", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Edzés", + "Infinite ${GAME}": "Végtelen ${GAME}", + "Infinite Onslaught": "Végtelen Támadás", + "Infinite Runaround": "Végtelen Szaladgálás", + "Onslaught Training": "Támadás edzés", + "Pro ${GAME}": "Profi ${GAME}", + "Pro Football": "Profi Football", + "Pro Onslaught": "Profi Támadás", + "Pro Runaround": "Profi Szaladgálás", + "Rookie ${GAME}": "Újonc ${GAME}", + "Rookie Football": "Újonc Football", + "Rookie Onslaught": "Újonc Támadás", + "The Last Stand": "Az Utolsó Kiállás", + "Uber ${GAME}": "Über ${GAME}", + "Uber Football": "Über Football", + "Uber Onslaught": "Über Támadás", + "Uber Runaround": "Über Szaladgálás" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Légy a kiválasztott egy ideig hogy nyerj.\nÖld meg a kiválasztottat, hogy te légy az.", + "Bomb as many targets as you can.": "Bombázz annyi célpontot, amennyit csak tudsz.", + "Carry the flag for ${ARG1} seconds.": "Cipeld a zászlót ${ARG1} másodpercig.", + "Carry the flag for a set length of time.": "Cipeld a zászlót egy adott ideig.", + "Crush ${ARG1} of your enemies.": "Zúzz szét ${ARG1}-t az ellenségeidből.", + "Defeat all enemies.": "Győzz le minden ellenséget.", + "Dodge the falling bombs.": "Térj ki minden hulló bomba elől.", + "Final glorious epic slow motion battle to the death.": "A végső dicső hatalmas lassú mozgásos csata a halálig.", + "Gather eggs!": "Gyűjts tojásokat!", + "Get the flag to the enemy end zone.": "Juttasd el a zászlót az ellenséges zóna végére.", + "How fast can you defeat the ninjas?": "Milyen gyorsan győzöd le a ninja-kat?", + "Kill a set number of enemies to win.": "Öllj meg fix mennyiségű ellenséget a nyeréshez.", + "Last one standing wins.": "Utolsó ember nyer.", + "Last remaining alive wins.": "Az utolsó életben lévő nyer.", + "Last team standing wins.": "Utolsó csapat nyer.", + "Prevent enemies from reaching the exit.": "Ne engedd át az ellenséget a kijáraton.", + "Reach the enemy flag to score.": "Érd el az ellenfél zászlóját ,hogy pontot szerezz.", + "Return the enemy flag to score.": "Vidd haza az ellenség zászlóját, hogy pontot szerezz.", + "Run ${ARG1} laps.": "Fuss ${ARG1} kört.", + "Run ${ARG1} laps. Your entire team has to finish.": "Fuss ${ARG1} kört. A csapatod összes tagjának végeznie kell.", + "Run 1 lap.": "Fuss 1 kört.", + "Run 1 lap. Your entire team has to finish.": "Fuss 1 kört. A csapatod összes tagjának végeznie kell.", + "Run real fast!": "Fuss nagyon gyorsan!", + "Score ${ARG1} goals.": "szerezz ${ARG1} gólt.", + "Score ${ARG1} touchdowns.": "Szerezz touchdowns-t.", + "Score a goal.": "Szerezz egy gólt.", + "Score a touchdown.": "Szerezz egy touchdown-t.", + "Score some goals.": "Szerezz pár gólt.", + "Secure all ${ARG1} flags.": "Védd meg mid a ${ARG1} zászlót.", + "Secure all flags on the map to win.": "Védd meg az összes zászlót a pályán ,hogy nyerj.", + "Secure the flag for ${ARG1} seconds.": "Védd a zászlót ${ARG1} másodpercig.", + "Secure the flag for a set length of time.": "Őrizd a zászlót egy adott ideig.", + "Steal the enemy flag ${ARG1} times.": "Lopd el az ellenség zászlóját ${ARG1} alkalommal.", + "Steal the enemy flag.": "Lopd el az ellenség zászlóját.", + "There can be only one.": "Csak egy lehet.", + "Touch the enemy flag ${ARG1} times.": "Érintsd meg az ellenség zászlóját ${ARG1} alkalommal.", + "Touch the enemy flag.": "Érintsd meg az ellenség zászlóját.", + "carry the flag for ${ARG1} seconds": "Cipeld a zászlót ${ARG1} másodpercig", + "kill ${ARG1} enemies": "Ölj meg ${ARG1} ellenséget", + "last one standing wins": "Utolsó ember nyer", + "last team standing wins": "utolsó csapat nyer", + "return ${ARG1} flags": "juttasd vissza a zászlót ${ARG1} alkalommal.", + "return 1 flag": "juttasd vissza a zászlót 1 alkalommal", + "run ${ARG1} laps": "fuss ${ARG1} kört", + "run 1 lap": "fuss 1 kört", + "score ${ARG1} goals": "szerezz ${ARG1} gólt", + "score ${ARG1} touchdowns": "szerezz ${ARG1} touchdowns-t", + "score a goal": "szerezz egy gólt", + "score a touchdown": "szerezz egy touchdown-t", + "secure all ${ARG1} flags": "véd meg mind a ${ARG1} zászlót", + "secure the flag for ${ARG1} seconds": "védd a zászlót ${ARG1} másodpercig", + "touch ${ARG1} flags": "Érints meg ${ARG1} zászlót", + "touch 1 flag": "Érints meg 1 zászlót" + }, + "gameNames": { + "Assault": "Támadás", + "Capture the Flag": "Szerezd meg a zászlót", + "Chosen One": "Kiválasztott", + "Conquest": "Hódítás", + "Death Match": "Death Match", + "Easter Egg Hunt": "Húsvéti tojásvadászat", + "Elimination": "Kiesés", + "Football": "Football", + "Hockey": "Hockey", + "Keep Away": "Tartsd Távol", + "King of the Hill": "A Hegyek Ura", + "Meteor Shower": "Meteor Zuhany", + "Ninja Fight": "Ninja Harc", + "Onslaught": "Támadás", + "Race": "Verseny", + "Runaround": "Szaladgálás", + "Target Practice": "Célzás gyakorlás", + "The Last Stand": "Az utolsó kiállás" + }, + "inputDeviceNames": { + "Keyboard": "Billentyűzet", + "Keyboard P2": "Billentyűzet P2" + }, + "languages": { + "Arabic": "Arab", + "Belarussian": "Fehér Orosz", + "Chinese": "Kínai Simplább", + "ChineseTraditional": "Kínai Tradicionális", + "Croatian": "Horvát", + "Czech": "Cseh", + "Danish": "Dán", + "Dutch": "Holland", + "English": "Angol", + "Esperanto": "Eszperanto", + "Finnish": "Finn", + "French": "Francia", + "German": "Német", + "Gibberish": "Értelmetlen beszéd", + "Greek": "Görög", + "Hindi": "Hindi", + "Hungarian": "Magyar", + "Indonesian": "Indonéz", + "Italian": "Olasz", + "Japanese": "Japán", + "Korean": "Koreai", + "Persian": "Perzsa", + "Polish": "Lengyel", + "Portuguese": "Portugál", + "Romanian": "Román", + "Russian": "Orosz", + "Serbian": "Szerb", + "Slovak": "Szlovák", + "Spanish": "Spanyol", + "Swedish": "Svéd", + "Turkish": "Török", + "Ukrainian": "Ukrán", + "Vietnamese": "Vietnám" + }, + "leagueNames": { + "Bronze": "Bronz", + "Diamond": "Gyémánt", + "Gold": "Arany", + "Silver": "Ezüst" + }, + "mapsNames": { + "Big G": "Nagy G", + "Bridgit": "Bridgit", + "Courtyard": "Udvar", + "Crag Castle": "Szikla Vár", + "Doom Shroom": "A Végzet Gombája", + "Football Stadium": "Football Stadion", + "Happy Thoughts": "Vidám Gondolatok", + "Hockey Stadium": "Hockey Stadion", + "Lake Frigid": "Fagyos Tó", + "Monkey Face": "Majom Arc", + "Rampage": "Rámpa", + "Roundabout": "Kerülő", + "Step Right Up": "Fél Piramis", + "The Pad": "A Pad", + "Tip Top": "Tip Top", + "Tower D": "D Torony", + "Zigzag": "Zigzag" + }, + "playlistNames": { + "Just Epic": "Csak Hatalmas", + "Just Sports": "Csak Sportok" + }, + "scoreNames": { + "Flags": "Zászlók", + "Goals": "Gólok", + "Score": "Pontok", + "Survived": "Túlélve", + "Time": "Idő", + "Time Held": "Megmaradt Idő" + }, + "serverResponses": { + "A code has already been used on this account.": "Ez a kód már fel volt használva ezen a profilon.", + "A reward has already been given for that address.": "Ez a kód egyszer már használva volt.", + "Account linking successful!": "A fiók összekötése sikeres!", + "Account unlinking successful!": "A fiók leválasztása sikeres", + "Accounts are already linked.": "A fiókok már össze vannak kötve.", + "An error has occurred; (${ERROR})": "Egy valamiféle hiba történt; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Hiba történt; kérlek vedd fel a kapcsolatot az ügyfélszolgálattal. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Valamiféle hiba történt; kérlek lépj kapcsolatba velünk: support@froemling.net.", + "An error has occurred; please try again later.": "Egy hiba lépett; kérlek próbáld újra később.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Össze akarod kötni ezt a két fiókot?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nEzt nem lehet visszavonni!", + "BombSquad Pro unlocked!": "BombSquad Pro feloldva!", + "Can't link 2 accounts of this type.": "Nem lehet 2 ilyen típusú fiókot összekötni.", + "Can't link 2 diamond league accounts.": "Nem lehet összekötni 2 gyémánt ligabeli fiókot.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Nem lehet összekötni; túllépné a maximum ${COUNT} összeköthető fiókok mennyiségét.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Csalás észlelve; a pontok és nyeremények felfüggesztve ${COUNT} napig.", + "Could not establish a secure connection.": "Nem lehet megbízható kapcsolatot létesíteni.", + "Daily maximum reached.": "Napi maximum elérve.", + "Entering tournament...": "Belépés a tornába...", + "Invalid code.": "Hibás kód.", + "Invalid payment; purchase canceled.": "Érvénytelen fizetési mód; fizetés visszavonva.", + "Invalid promo code.": "Érvénytelen promóciós kód.", + "Invalid purchase.": "Érvénytelen vásárlás.", + "Invalid tournament entry; score will be ignored.": "Érvénytelen verseny belépés; az eredményeid figyelmen kívül lesznek hagyva.", + "Item unlocked!": "Elem feloldva!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "KAPCSOLAT FENNTARTVA. ${ACCOUNT} tartalmaz\njelentős adatok, amelyek MINDEN elvesznének.\nÖsszekapcsolhatja az ellenkező sorrendben, ha szeretné\n(és ehelyett elveszíti a fiók adatait)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Összekötöd a/az ${ACCOUNT} fiókot ehhez a fiókhoz?\nMinden létező adat a/az ${ACCOUNT} fiókon elveszik.\nEzt nem tudod majd visszavonni. Biztos vagy benne?", + "Max number of playlists reached.": "Maximum listaszám elérve.", + "Max number of profiles reached.": "Maximum profilszám elérve.", + "Maximum friend code rewards reached.": "Maximum barát kód elérve.", + "Message is too long.": "Az üzenet túl hosszú.", + "Profile \"${NAME}\" upgraded successfully.": "A \"${NAME}\" profil teljes körűvé fejlesztve.", + "Profile could not be upgraded.": "A profilt nem lehet frissíteni.", + "Purchase successful!": "Vásárlás sikeres!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "${COUNT} jegy megkapva a bejelentkezés miatt.\nTérj vissza holnap és kapsz újabb${TOMORROW_COUNT} jegyet.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "A szerver nem működik a játék jelenlegi verziójával; \nKérlek frissítsd egy új verzióra.", + "Sorry, there are no uses remaining on this code.": "Ez a kód már elérte a használati limitet.", + "Sorry, this code has already been used.": "Ezt a kódot már használták.", + "Sorry, this code has expired.": "Ez a kód már lejárt.", + "Sorry, this code only works for new accounts.": "Ezt a kódot csak új játékosok használhatják.", + "Temporarily unavailable; please try again later.": "Jelenleg elérhetetlen; kérlek próbáld újra később.", + "The tournament ended before you finished.": "A torna lezárult mielőtt te befejezted volna.", + "This account cannot be unlinked for ${NUM} days.": "Ezt a fiókot nem tudod leválasztani ${NUM} napig.", + "This code cannot be used on the account that created it.": "Ez a kód nem használható fel, mivel te alkottad.", + "This requires version ${VERSION} or newer.": "Ehhez szükséges verzió:${VERSION} vagy újabb.", + "Tournaments disabled due to rooted device.": "A versenyek tiltva vannak rootolt eszköz miatt.", + "Tournaments require ${VERSION} or newer": "A versenyhez ${VERSION} verzió vagy újabb szükséges.", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Leválasztod ${ACCOUNT} erről a fiókról?\nAz összes adat innen: ${ACCOUNT} törölve lesz. \n(Kivéve a mérföldkövek egyes esetekben)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "FIGYELMEZTETÉS: fiókod ellen panaszok érkeztek csalásért.\nA csaláson kapott fiókok tiltva lesznek. Kérlek játsz tisztességesen.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Szeretnéd hogy a készülék felhasználója ez legyen?\n\nA készülék mostani felhasználója: ${ACCOUNT1}\nEz a felhasználó: ${ACCOUNT2}\n\nEz megtartja a mostani teljesítményed.\nVigyázat: ez nem visszavonható!", + "You already own this!": "Már birtoklod ezt!", + "You can join in ${COUNT} seconds.": "Beléphetsz ${COUNT} másodperc múlva.", + "You don't have enough tickets for this!": "Nincs elég jegyed a vásárláshoz!", + "You don't own that.": "Te nem rendelkezel ezzel.", + "You got ${COUNT} tickets!": "Kaptál ${COUNT} jegyet!", + "You got a ${ITEM}!": "Kaptál egy ${ITEM}-t!", + "You have been promoted to a new league; congratulations!": "Feljutottál egy új ligába; Gratulálunk!", + "You must update to a newer version of the app to do this.": "Frissítened kell a játékot az újabb verzióra, hogy megtehesd ezt.", + "You must update to the newest version of the game to do this.": "Frissítened kell a játék legújabb verziójára hogy ezt meg tudd tenni.", + "You must wait a few seconds before entering a new code.": "Várnod kell pár másodpercet egy új kód beírásához.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Az előző tornában elért helyezésed:#${RANK}.Köszönjük a játékot!", + "Your account was rejected. Are you signed in?": "Fiókod elutasítva. Be vagy jelentkezve?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "A játékod módosítva lett.\nÁllíts vissza minden módosítást és próbáld újra.", + "Your friend code was used by ${ACCOUNT}": "A promóciós kódod használva volt ${ACCOUNT} által" + }, + "settingNames": { + "1 Minute": "1 Perc", + "1 Second": "1 Másodperc", + "10 Minutes": "10 Perc", + "2 Minutes": "2 Perc", + "2 Seconds": "2 Másodperc", + "20 Minutes": "20 Perc", + "4 Seconds": "4 Másodperc", + "5 Minutes": "5 Perc", + "8 Seconds": "8 Másodperc", + "Allow Negative Scores": "Negatív Eredmények Engedélyezése", + "Balance Total Lives": "Teljes Életek Kiegyensúlyozása", + "Bomb Spawning": "Random Bomba", + "Chosen One Gets Gloves": "A Kiválasztott Kesztyűket Kap", + "Chosen One Gets Shield": "A Kiválaszott Pajzsot Kap", + "Chosen One Time": "Kiválasztott Idő", + "Enable Impact Bombs": "Ragadós Bomba engedélyezve", + "Enable Triple Bombs": "Háromszoros Bomba engedélyezve", + "Entire Team Must Finish": "Az Egész csapatnak be kell érnie", + "Epic Mode": "Hatalmas Mód", + "Flag Idle Return Time": "Zászló Tétlen Visszatérés Idő", + "Flag Touch Return Time": "Zászló Érintés Visszatérési Idő", + "Hold Time": "Kitartási Idő", + "Kills to Win Per Player": "Ölések a Győzelemhez Per Játékos", + "Laps": "Körök", + "Lives Per Player": "Életek Per Játékos", + "Long": "Hosszú", + "Longer": "Hosszabb", + "Mine Spawning": "Akna Lerakás", + "No Mines": "Nincsenek Aknák", + "None": "Semmi", + "Normal": "Normál", + "Pro Mode": "Profi mód", + "Respawn Times": "Újraéledési Idő", + "Score to Win": "Pontszám a Győzelemért", + "Short": "Rövid", + "Shorter": "Rövidebb", + "Solo Mode": "Solo Mód", + "Target Count": "Célpontok Száma", + "Time Limit": "Idő Korlát" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "A/az ${TEAM} csapat diszkvalifikálva mivel kilépett az alábbi játékos:${PLAYER}", + "Killing ${NAME} for skipping part of the track!": "${NAME} megölése pálya rész átugrás miatt!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Figyelem ${NAME} dollárnak: a turbó / gomb spammelés kiüti Önt." + }, + "teamNames": { + "Bad Guys": "Rosszfiúk", + "Blue": "Kék", + "Good Guys": "Jó Fiúk", + "Red": "Vörös" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Egy tökéletesen időzített futás-ugrás-pörgés-ütés kombináció azonnal képes ölni\nés emellett egy életen át tartó tisztelet is jár mellé ,talán.", + "Always remember to floss.": "Ne felejts el fogselymet használni!", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Csinálj játékos profilokat magadnak és a barátaidnak,\n hogy ne kelljen véletlenszerű neveket használnotok.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Az átok doboz visszaszámlálós bombává tesz téged.\nAz egyetlen gyógymód ha gyorsan felveszel egy gyógyító dobozt.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Annak ellenére ,hogy a karakterek másképp néznek ki a képességük azonosak,\ntehát válaszd ki a legszimpatikusabbat közölük.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Le legyél elszállva magadtól ha van egy energia pajzsod,ennek ellenére is ugyan úgy lehajíthatnak.", + "Don't run all the time. Really. You will fall off cliffs.": "Ne csak szaladgálj, még a végén leesel a pályáról.", + "Don't spin for too long; you'll become dizzy and fall.": "ne pörögj sokáig, mivel elfogsz esni.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Nyomj meg egy gombot a futáshoz. (A \"Ravasz\" gombok ugyanúgy működnek, ha vannak)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "A futáshoz, nyomj meg egy gombot. Gyorsabb leszel,\nde nehezebb lesz kanyarodnod. Figyelj oda, nehogy leess a pályáról.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Jégbombák nem a legerősebbek, de ha valakit eltalál\naz megfagy. Ilyenkor az illető sebezhető.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Ha valaki felvesz a hátára, csak üsd meg és elfog engedni.\nEz a való életben is így működik.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Ha nincs elég kontrollered, töltsd le a '${REMOTE_APP_NAME}' alkalmazást\na mobil eszközödre hogy kontrollerként használhasd őket.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "HA nincs kontrollered ,telepítsd a BombSquad Remote alkalmazást \na iOS vagy Android készülékedre és használd őket kontrollerként.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Ha egy ragadós bomba rád ragadna,ugrálj és forogj \nkörbe-körbe így talán sikerül lerázni magadról a bombát.", + "If you kill an enemy in one hit you get double points for it.": "Ha megölsz egy ellenséget egyetlen egy sebzéssel, akkor dupla pontot kapsz érte.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Ha felveszel egy átok dobozt csak egy esélyed van a túlélésre \nha felveszel egy gyógyító doboz pár másodpercen belül.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Ha egy helyben maradsz megpörkölnek a bombák. Szaladj, és kerüld ki őket.", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Ha sok egy játékos csatlakozik le és fel kapcsold be a 'Tétlen Játékosok Kirúgása'-t\n a beállításokba így senki sem felejt el majd kilépni.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Ha a telefonod híján van az energiának és szeretnél több energiát megtartani \naz akkumulátorban akkor vedd lejjebb a 'Grafikát' a 'Beállításokban'.", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Ha az FPS-ed túl kicsi lenne próbáld meg lejjebb venni\n a felbontást vagy a grafikát a beállításokban.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "A Szerezd meg a Zászlót játékban, ahhoz, hogy pontot szerezz a zászlódnak a bázisodon kell lennie,\nígy egy jó módszer az ellenfél pontszerzésének megakadályozására a zászlójuk ellopása.", + "In hockey, you'll maintain more speed if you turn gradually.": "Hokiban több sebességet tudsz szerezni, ha fokozatosan elfordulsz.", + "It's easier to win with a friend or two helping.": "Sokkal könnyebb nyerni a barátok segítségével.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Ugorj fel, ha magasabb helyekre szeretnél feldobni bombát.", + "Land-mines are a good way to stop speedy enemies.": "A gyors ellenségek ellen a leghatásosabbak a taposóaknák.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Rengeteg dolog fogható meg és dobható el még a játékos.\nDobd le az ellenfeleidet ,hisz így biztosan meghal.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nem tudsz felmenni a létrát. A bombát kell használod.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "A játékosok a legtöbb játékmódban akár a meccs közepén \nis becsatlakozhat, és ez igaz a kontrollerekre is.", + "Practice using your momentum to throw bombs more accurately.": "Használd az időt is, ha egy kicsit vársz a bombával, hatásosabb lehet.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Az ütések hatékonyabbak ha az öklöd mozog,\nszóval próbálj keg futni,ugrani vagy pörögni közben.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Menj vissza és fuss neki a bomba dobásnak\n így a bomba sokkal messzebre repül majd.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Iktass egyszerre egy egész csapatnyi ellenfelet\n úgy ,hogy a TNT közelébe dobsz egy bombát.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "A fej a legsebezhetőbb rész, szóval egy fejre ragadt bomba \nlegtöbbször csak egyet jelenthet: GAME OVER.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Ennek a pályának nincs vége, viszont a \nlegmagasabb pontszám nagy tiszteletet adhat neked.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Az erős dobás egyik alapja az irány nyomva tartása.\nHa gyengén szeretnél eldobni valamit annak a legjobb módja, hogy nem nyomsz semmilyen mozgási gombot.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Unalmasak az alap zenék? Rakj be sajátot!\nBővebben: Beállítások->Hang->Zenék", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Próbáld meg megtartani a bombát olyan 1-2 másodpercig mielőtt eldobnád.", + "Try tricking enemies into killing eachother or running off cliffs.": "Próbáld meg kicselezni az ellenséget úgy ,hogy megöljék egymást vagy leszaladjanak a pályáról.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Használd a felemelő gombot, hogy felemeld a zászlót. < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Próbálj meg neki futni a bombadobásnak így az messzebbre repül...", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "'Becélozhatod' űtésed azzal, hogy forogsz balra vagy jobbra.\nHasznos hogy kiüsd a rosszfiúkat a pályaszélről, vagy a hokiban való pont szerzésre.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Megtudod állapítani, hogy a bomba mikor fog robbani, a kanócon lévő\nszikrák színére való tekintettel: sárga..narancs..piros..BUMM.", + "You can throw bombs higher if you jump just before throwing.": "Magasabbra is tudod dobni a bombát, ha épp a dobás előtt csak ugrassz egyet.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Sérülést szerzel, amikor beütöd a fejedet a dolgokba,\nszóval próbád nem megfejelni a dolgokat.", + "Your punches do much more damage if you are running or spinning.": "Az ütéseid nagyobb sérülést okoznak, ha futsz vagy pörögsz." + } + }, + "trophiesRequiredText": "Ehhez szükség van minimum ${NUMBER} kupára.", + "trophiesText": "Trófeák", + "trophiesThisSeasonText": "Trófeák ebben a ligában", + "tutorial": { + "cpuBenchmarkText": "Futó oktatás a nevetséges-sebességen (elsősorban CPU sebesség teszt)", + "phrase01Text": "Szevasz!", + "phrase02Text": "Üdvözöllek a ${APP_NAME}-ban!", + "phrase03Text": "Itt van pár tipp, hogy irányítsd a karaktered:", + "phrase04Text": "Sok tárgy a ${APP_NAME}-ban FIZIKÁRA alapszik.", + "phrase05Text": "Például, ha ütsz...", + "phrase06Text": "..a sérülés az őkleid gyorsaságán múlik.", + "phrase07Text": "Látod? Nem mozdultunk, szóval ez alig sebezte meg ${NAME}-t.", + "phrase08Text": "Most ugorjunk és pörögjünk hogy sebességre tegyünk szert.", + "phrase09Text": "Áh, így jobb.", + "phrase10Text": "A futás is segít.", + "phrase11Text": "Tartsd lenyomva AKÀRMELYIK gombot a futáshoz.", + "phrase12Text": "Extra-király ütésért, próbálj futni és pörögni.", + "phrase13Text": "Hopsz; bocs ezért ${NAME}", + "phrase14Text": "Feltudsz venni és eldobni dolgokat, csak úgy mint a zászlót... vagy ${NAME}-t.", + "phrase15Text": "Végül, van bomba.", + "phrase16Text": "A bomba dobás gyakorlatot vesz igénybe.", + "phrase17Text": "Áú! Nem nagyon jódobás!", + "phrase18Text": "Mozgás segít távolabbra dobni.", + "phrase19Text": "Az ugrás segít magasabbra dobni.", + "phrase20Text": "\"Ostorozz\" a bombáddal, hogy még messzebbre is dobd.", + "phrase21Text": "Beidőzíteni a bombádat, trükkös lehet.", + "phrase22Text": "Beng.", + "phrase23Text": "Próbáld meg \"kisütni\" a kanócot egy vagy két másodpercre.", + "phrase24Text": "Hurrá! Jól megsült.", + "phrase25Text": "Jólvan, ez csak erről szól.", + "phrase26Text": "Kapd el őket, tigris!", + "phrase27Text": "Emlékezz az edzésedre, és élve jössz vissza!", + "phrase28Text": "...jah, talán...", + "phrase29Text": "Sok szerencsét!", + "randomName1Text": "Fred", + "randomName2Text": "Harry", + "randomName3Text": "Bill", + "randomName4Text": "Chuck", + "randomName5Text": "Phil", + "skipConfirmText": "Tényleg átléped az oktatót? Tapints a belegyezéshez.", + "skipVoteCountText": "${COUNT}/${TOTAL} szavazat az átlépésre", + "skippingText": "Oktató átlépése...", + "toSkipPressAnythingText": "(tapints vagy nyomj meg akármit az oktató átlépéséhez)" + }, + "twoKillText": "DUPLA ÖLÉS!", + "unavailableText": "elérhetetlen", + "unconfiguredControllerDetectedText": "Konfigurált vezérlő felismerve:", + "unlockThisInTheStoreText": "Ennek feloldva kell lennie az áruházban.", + "unlockThisProfilesText": "Hogy készíts több mint ${NUM} profilt, kelleni fog:", + "unlockThisText": "Hogy ezt felold, szükséged van ezekre:", + "unsupportedHardwareText": "Bocs, ez a hárdver nem támogatott a játék ezen szerkezete által.", + "upFirstText": "Elsőként:", + "upNextText": "A következő a ${COUNT}. játékban:", + "updatingAccountText": "Felhasználó frissítése...", + "upgradeText": "Frissítés", + "upgradeToPlayText": "Old fel a \"${PRO}\"-t az áruházba, hogy játszhass ezzel.", + "useDefaultText": "Alapértelmezett Használata", + "usesExternalControllerText": "Ez a játék egy külső vezérlőt használ bemenet gyanánt.", + "usingItunesText": "Itunes használása a zenéhez...", + "usingItunesTurnRepeatAndShuffleOnText": "Kérlek bizonyosodj meg arról, hogy a keverés BE van kapcsolva és az ismétlés MINDEN-re van állítva az iTunes-on.", + "validatingTestBuildText": "Teszt Szerkezet Érvényesítése...", + "victoryText": "Győzelem!", + "voteDelayText": "Még nem indíthatsz szavazást újabb ${NUMBER} másodpercig", + "voteInProgressText": "Már egy szavazás folyamatban van...", + "votedAlreadyText": "Már szavaztál", + "votesNeededText": "${NUMBER} szavazatra van szükség", + "vsText": "ellen", + "waitingForHostText": "(várakozás ${HOST}-ra, hogy folytassuk)", + "waitingForPlayersText": "várakozás a csatlakozó játékosokra...", + "waitingInLineText": "Várakozás (a parti tele van)...", + "watchAVideoText": "Videó Nézése", + "watchAnAdText": "Reklám Nézése", + "watchWindow": { + "deleteConfirmText": "\"${REPLAY}\" Törlése?", + "deleteReplayButtonText": "Visszajátszás\nTörlése", + "myReplaysText": "Az én visszajátszásaim", + "noReplaySelectedErrorText": "Nincs Visszajátszás Kiválasztva", + "playbackSpeedText": "Lejátszási sebesség: ${SPEED}", + "renameReplayButtonText": "Visszajátszás\nÁtnevezése", + "renameReplayText": "\"${REPLAY}\" átnevezése erre:", + "renameText": "Átnevezés", + "replayDeleteErrorText": "Hiba a visszajátszás törlése során.", + "replayNameText": "Visszajátszási Név", + "replayRenameErrorAlreadyExistsText": "Egy visszajátszás már létezik ezzel a névvel.", + "replayRenameErrorInvalidName": "Nem lehet átnevezni a visszajátszást; érvénytelen név.", + "replayRenameErrorText": "Hiba a visszajátszás átnevezése során.", + "sharedReplaysText": "Megosztott Visszajátszások", + "titleText": "Visszajátszás", + "watchReplayButtonText": "Visszajátszás\nMegnézése" + }, + "waveText": "Hullám", + "wellSureText": "Rendben!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Szerzői jog" + }, + "wiimoteListenWindow": { + "listeningText": "Wiimote-ra való figyelés...", + "pressText": "Nyomd meg a Wiimote 1-es és 2-es gombokat egyidejűleg.", + "pressText2": "Az újabb Wiimote-okon a Motion Plus beépítéssel, nyomd meg a piros 'sync' gombot a hátul lévő helyett." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Szerzői jog", + "listenText": "Figyelj", + "macInstructionsText": "Bizonyosodj meg arról, hogy a Wii-d ki van kapcsolva és a bluetooth engedélyezve van\na te Mac-eden, azután nyomd meg a 'Listen'-t. Wiimote támogatás lehet\nkicsit labilis, szóval lehet párszor próbálgatnod kell\nmielőtt csatlakoztathatnád.\n\nA Bluetooth-nak kezelnie kell felmenőleg 7 csatlakoztatott eszközt,\nbár a távolság változhat.\n\nBombSquad támogatja az eredeti Wiimote-okat, Nunchuk-okat,\nés a klasszikus vezérlőt.\nMost már az újabb Wii Remote Plus is működik\nde nem mellékletekkel.", + "thanksText": "Köszönet a DarwiinRemote csapatnak,\nhogy lehetővé tették.", + "titleText": "Wiimote beállítás" + }, + "winsPlayerText": "${NAME} nyert!", + "winsTeamText": "${NAME} nyert!", + "winsText": "${NAME} nyert!", + "worldScoresUnavailableText": "Világ eredmények elérhetetlenek", + "worldsBestScoresText": "Világ legjobb eredményei", + "worldsBestTimesText": "Világ legjobb idejei", + "xbox360ControllersWindow": { + "getDriverText": "Szerezz illesztőprogramot", + "macInstructions2Text": "Hogy használj vezérlőket vezetéknélkül, szükséged lesz egy vevőre (reciever),\nami a(z) 'Xbox 360 Wireless Controller for Windows'-hoz jár.\nEgy vevő 4 vezérlőnek engedélyezi a csatlakozást.\n\nFontos: 3rd-party vevők nem fognak működni ezzel az illesztőprogrammal;\nbizonyosodj meg róla, hogy a te vevődön van 'Microsoft' felirat, nem 'Xbox 360'.\nMicrosoft többé már nemadja ezeket külön, tehát szerezned kell\negy olyan csomagot a vezérlővel vagy keress mást ebay-en.\n\nHa hasznosnak találod ezt, kérlek fontolgasd meg az adományozást\naz illesztőprogram fejlesztőinek az ő oldalukon.", + "macInstructionsText": "Hogy használj xbox 360 vezérlőket, fel kell telepítened\na Mac illesztő programot, ami elérhető a fenti link-en.\nMűködik vezetékes, és vezetéknélküli vezérlőkkel is.", + "ouyaInstructionsText": "Hogy használj xbox 360 vezérlőket a BombSquad-dal, szimplán\ndugd őket be az eszköz USB port-jába. Használhatsz egy USB hub-ot\nhogy csatlakoztass több vezérlőket.\n\nHogy használj vezetéknélküli vezérlőket, szükséged lesz vezetéknélküli vevőre, (wireless receiver)\nelérhető, mint a(z) \"Xbox 360 wireless Controller for Windows\" csomag\nalkatrésze vagy külön eladva. Minden vevő egy USB port-ba megy és\nengedélyezi a felcsatlakozást 4 vezetéknélküli vezérlő számára.", + "titleText": "Xbox 360 kontroller használata a ${APP_NAME}-ban:" + }, + "yesAllowText": "Igen, Engedélyez!", + "yourBestScoresText": "Te legjobb pontjaid", + "yourBestTimesText": "Te legjobb időid" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/indonesian.json b/dist/ba_data/data/languages/indonesian.json new file mode 100644 index 0000000..6414b41 --- /dev/null +++ b/dist/ba_data/data/languages/indonesian.json @@ -0,0 +1,1854 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Emoji atau karakter spesial lainnya tidak dapat digunakan untuk nama akun", + "accountProfileText": "(profil akun)", + "accountsText": "Akun", + "achievementProgressText": "Penghargaan: ${COUNT} dari ${TOTAL}", + "campaignProgressText": "Kemajuan Ekspedisi [Sulit]: ${PROGRESS}", + "changeOncePerSeason": "Kamu hanya dapat mengganti ini sekali per musim.", + "changeOncePerSeasonError": "Kamu harus menunggu sampai musim berikutnya untuk menggantinya lagi (${NUM} hari)", + "customName": "Nama Khusus", + "linkAccountsEnterCodeText": "Masukkan kode", + "linkAccountsGenerateCodeText": "Buat Kode", + "linkAccountsInfoText": "(bagikan kemajuan permainan dengan platform lain)", + "linkAccountsInstructionsNewText": "untuk menghubungkan dua akun,buat kode pada \nperangkat pertama dan masukan kode ke perangkat kedua.\nData dari kedua akun akan di bagi ke kedua perangkat.\n(Data dari akun pertama akan hilang)\n\nKamu dapat menghubungkan ke ${COUNT} akun.\n\nPENTING:menghubungkan akun hanya bekerja untuk akun milikmu sendiri;\nJika kamu menghubungkannya dengan akun milik temanmu kamu tidak akan\ndapat bermain daring pada waktu yang bersamaan.", + "linkAccountsInstructionsText": "Untuk menghubungkan dua akun, buat kode di salah satu\ndari mereka dan masukkan kode itu di sisi lain.\nKemajuan dan persediaan akan digabungkan.\nAnda dapat menghubungkan hingga ${COUNT} akun.\n\nPENTING: Hanya hubungkan akun yang anda miliki!\nJika Anda menghubungkan akun dengan teman anda, anda\ntidak akan bisa bermain bersamaan!\n\nJuga: ini tidak dapat dibatalkan, jadi berhati-hatilah!", + "linkAccountsText": "Hubungkan Akun", + "linkedAccountsText": "Akun yang Terhubung:", + "nameChangeConfirm": "Ganti namamu menjadi ${NAME}?", + "resetProgressConfirmNoAchievementsText": "Tindakan ini akan mengatur ulang kemajuan co-op dan\nskor tertinggi Kamu (kecuali tiket).\nTidak dapat dibatalkan. Apakah Kamu yakin?", + "resetProgressConfirmText": "Ini akan mengatur ulang kemajuan co-op,\npencapaian, dan skor tertinggi Kamu\n(kecuali tiket). Ini tidak dapat\ndibatalkan. Kamu yakin?", + "resetProgressText": "Atur Ulang kemajuan", + "setAccountName": "Pasang Nama Akun", + "setAccountNameDesc": "Pilih nama yang ditampilkan untuk akun Kamu.\nGunakan nama dari akun yang tersambung atau\nbuatlah nama khusus baru.", + "signInInfoText": "Masuk untuk mendapatkan tiket, bermain daring,\ndan membagikan kemajuan permaian antar perangkat.", + "signInText": "Masuk", + "signInWithDeviceInfoText": "(akun otomatis hanya tersedia untuk perangkat ini)", + "signInWithDeviceText": "Masuk menggunakan akun perangkat", + "signInWithGameCircleText": "Masuk dengan LingkarPermainan", + "signInWithGooglePlayText": "Masuk menggunakan Google Play", + "signInWithTestAccountInfoText": "(akun tipe lama; gunakan akun device untuk kedepannya)", + "signInWithTestAccountText": "Masuk menggunakan akun percobaan", + "signOutText": "Keluar", + "signingInText": "Sedang masuk...", + "signingOutText": "Sedang keluar...", + "ticketsText": "Tiket:${COUNT}", + "titleText": "Akun", + "unlinkAccountsInstructionsText": "Pilih akun yang akan diputuskan.", + "unlinkAccountsText": "Memutus Akun.", + "viaAccount": "(melalui akun ${NAME})", + "youAreSignedInAsText": "Kamu terdaftar sebagai:" + }, + "achievementChallengesText": "Tantangan Pencapaian", + "achievementText": "Pencapaian", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Bunuh 3 musuh menggunakan TNT", + "descriptionComplete": "Membunuh 3 musuh menggunakan TNT", + "descriptionFull": "Bunuh 3 musuh menggunakan TNT pada ${LEVEL}", + "descriptionFullComplete": "Membunuh 3 musuh menggunakan TNT pada ${LEVEL}", + "name": "Duar, Peledak Diledakkan" + }, + "Boxer": { + "description": "Menangkan pertandingan tanpa menggunakan bom", + "descriptionComplete": "Memenangkan pertandingan tanpa bom", + "descriptionFull": "Selesaikan ${LEVEL} tanpa menggunakan bom sama sekali", + "descriptionFullComplete": "Menyelesaikan ${LEVEL} tanpa menggunakan bom", + "name": "Petinju" + }, + "Dual Wielding": { + "descriptionFull": "hubungkan 2 pengontrol (perangkat keras atau aplikasi)", + "descriptionFullComplete": "2 pengontrol terhubung (perangkat keras atau aplikasi)", + "name": "Dua Tangan" + }, + "Flawless Victory": { + "description": "Menangkan pertandingan tanpa terkena serangan", + "descriptionComplete": "Menang Tanpa terkena Serangan", + "descriptionFull": "Menangkan ${LEVEL} tanpa terkena serangan", + "descriptionFullComplete": "Memenangkan ${LEVEL} tanpa terkena serangan", + "name": "Kemenangan Mutlak" + }, + "Free Loader": { + "descriptionFull": "Mulai permainan Bebas-Untuk-Semua dengan 2+ pemain", + "descriptionFullComplete": "Memulai Bebas-Untuk-Semua permainan dengan 2 + pemain", + "name": "Tukang Angkut" + }, + "Gold Miner": { + "description": "Bunuh 6 musuh dengan menggunakan ranjau", + "descriptionComplete": "Membunuh 6 musuh menggunakan ranjau", + "descriptionFull": "Bunuh 6 musuh menggunakan ranjau pada ${LEVEL}", + "descriptionFullComplete": "Membunuh 6 musuh dengan ranjau pada ${LEVEL}", + "name": "Penambang Emas" + }, + "Got the Moves": { + "description": "Menang tanpa memukul atau menggunakan bom", + "descriptionComplete": "Memenangkan pertandingan tanpa memukul dan bom", + "descriptionFull": "Menangkan ${LEVEL} tanpa memukul dan tanpa menggunakan bom", + "descriptionFullComplete": "Memenangkan ${LEVEL} tanpa memukul dan tanpa bom", + "name": "Got the Moves" + }, + "In Control": { + "descriptionFull": "hubungkan pengontrol (perangkat keras atau aplikasi)", + "descriptionFullComplete": "pengontrol terhubung (perangkat keras atau aplikasi)", + "name": "Terkendali" + }, + "Last Stand God": { + "description": "Cetak skor 1000 poin", + "descriptionComplete": "Mencetak skor 1000 poin", + "descriptionFull": "Cetak skor 1000 poin pada ${LEVEL}", + "descriptionFullComplete": "Mencetak skor 1000 poin pada ${LEVEL}", + "name": "Rajanya ${LEVEL}" + }, + "Last Stand Master": { + "description": "Cetak skor 250 poin", + "descriptionComplete": "Mencetak skor 250 poin", + "descriptionFull": "Cetak skor 250 poin pada ${LEVEL}", + "descriptionFullComplete": "Mencetak skor 250 poin pada ${LEVEL}", + "name": "Masternya ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Cetak skor 500 poin", + "descriptionComplete": "Mencetak skor 500 poin", + "descriptionFull": "Cetak skor 500 poin pada ${LEVEL}", + "descriptionFullComplete": "Mencetak skor 500 poin pada ${LEVEL}", + "name": "Penyihir ${LEVEL}" + }, + "Mine Games": { + "description": "Bunuh 3 musuh menggunakan ranjau", + "descriptionComplete": "Membunuh 3 musuh menggunakan ranjau", + "descriptionFull": "Bunuh 3 musuh menggunakan ranjau pada ${LEVEL}", + "descriptionFullComplete": "Membunuh 3 musuh menggunakan ranjau pada ${LEVEL}", + "name": "Permainan Ranjau" + }, + "Off You Go Then": { + "description": "Lempar 3 musuh keluar arena", + "descriptionComplete": "Melempar 3 musuh keluar arena", + "descriptionFull": "Lempar 3 musuh keluar arena pada ${LEVEL}", + "descriptionFullComplete": "Melempar 3 musuh keluar arena pada ${LEVEL}", + "name": "Oke, Maju!" + }, + "Onslaught God": { + "description": "Cetak skor 5000 poin", + "descriptionComplete": "Mencetak skor 5000 poin", + "descriptionFull": "Cetak skor 5000 poin pada ${LEVEL}", + "descriptionFullComplete": "Mencetak skor 5000 poin pada ${LEVEL}", + "name": "Rajanya ${LEVEL}" + }, + "Onslaught Master": { + "description": "Cetak skor 500 poin", + "descriptionComplete": "Mencetak skor 500 poin", + "descriptionFull": "Cetak skor 500 poin pada ${LEVEL}", + "descriptionFullComplete": "Mencetak skor 500 poin pada ${LEVEL}", + "name": "Masternya ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Kalahkan semua gelombang", + "descriptionComplete": "Mengalahkan semua gelombang", + "descriptionFull": "Kalahkan semua gelombang pada ${LEVEL}", + "descriptionFullComplete": "Mengalahkan semua gelombang pada ${LEVEL}", + "name": "Juara ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Cetak skor 1000 poin", + "descriptionComplete": "Mencetak skor 1000 poin", + "descriptionFull": "Cetak skor 1000 poin pada ${LEVEL}", + "descriptionFullComplete": "Mencetak skor 1000 poin pada ${LEVEL}", + "name": "Penyihir ${LEVEL}" + }, + "Precision Bombing": { + "description": "Menang tanpa mengambil kekuatan tambahan", + "descriptionComplete": "Memenangkan permainan tanpa mengambil kekuatan tambahan", + "descriptionFull": "Menangkan ${LEVEL} tanpa mengambil power-up", + "descriptionFullComplete": "Memenangkan ${LEVEL} tanpa mengambil power-up", + "name": "Pengebom Jitu" + }, + "Pro Boxer": { + "description": "Menang tanpa menggunakan bom", + "descriptionComplete": "Menang tanpa menggunakan bom", + "descriptionFull": "Menangkan ${LEVEL} tanpa menggunakan bom", + "descriptionFullComplete": "Memenangkan ${LEVEL} tanpa menggunakan bom", + "name": "Petinju Handal" + }, + "Pro Football Shutout": { + "description": "Menang tanpa memperbolehkan musuh mencetak skor", + "descriptionComplete": "Menang tanpa memperbolehkan musuh mencetak skor", + "descriptionFull": "Menangkan ${LEVEL} tanpa memperbolehkan musuh mencetak skor", + "descriptionFullComplete": "Memenangkan ${LEVEL} tanpa memperbolehkan musuh mencetak skor", + "name": "${LEVEL} Beres" + }, + "Pro Football Victory": { + "description": "Menangkan permainan", + "descriptionComplete": "Memenangkan permainan", + "descriptionFull": "Menangkan permainan pada ${LEVEL}", + "descriptionFullComplete": "Memenangkan permainan pada ${LEVEL}", + "name": "Juara ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Kalahkan semua gelombang", + "descriptionComplete": "Mengalahkan semua gelombang", + "descriptionFull": "Kalahkan semua gelombang pada ${LEVEL}", + "descriptionFullComplete": "Mengalahkan semua gelombang pada ${LEVEL}", + "name": "Juara ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Selesaikan semua gelombang", + "descriptionComplete": "Menyelesaikan semua gelombang", + "descriptionFull": "Selesaikan semua gelombang pada ${LEVEL}", + "descriptionFullComplete": "Menyelesaikan semua gelombang pada ${LEVEL}", + "name": "Juara ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Menang tanpa memperbolehkan musuh mencetak skor", + "descriptionComplete": "Menang tanpa memperbolehkan musuh mencetak skor", + "descriptionFull": "Menangkan ${LEVEL} tanpa memperbolehkan musuh mencetak skor", + "descriptionFullComplete": "Memenangkan ${LEVEL} tanpa memperbolehkan musuh mencetak skor", + "name": "${LEVEL} Beres" + }, + "Rookie Football Victory": { + "description": "Menangkan permainan", + "descriptionComplete": "Memenangkan permainan", + "descriptionFull": "Menangkan permainan pada ${LEVEL}", + "descriptionFullComplete": "Memenangkan permainan pada ${LEVEL}", + "name": "Juara ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Kalahkan semua gelombang", + "descriptionComplete": "Mengalahkan semua gelombang", + "descriptionFull": "Kalahkan semua gelombang pada ${LEVEL}", + "descriptionFullComplete": "Mengalahkan semua gelombang pada ${LEVEL}", + "name": "Juara ${LEVEL}" + }, + "Runaround God": { + "description": "Cetak skor 2000 poin", + "descriptionComplete": "Mencetak skor 2000 poin", + "descriptionFull": "Cetak skor 2000 poin pada ${LEVEL}", + "descriptionFullComplete": "Mencetak skor 2000 poin pada ${LEVEL}", + "name": "Rajanya ${LEVEL}" + }, + "Runaround Master": { + "description": "Cetak skor 500 poin", + "descriptionComplete": "Mencetak skor 500 poin", + "descriptionFull": "Cetak skor 500 poin pada ${LEVEL}", + "descriptionFullComplete": "Mencetak skor 500 poin pada ${LEVEL}", + "name": "Masternya ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Cetak skor 1000 poin", + "descriptionComplete": "Mencetak skor 1000 poin", + "descriptionFull": "Cetak skor 1000 poin pada ${LEVEL}", + "descriptionFullComplete": "Mencetak skor 1000 poin pada ${LEVEL}", + "name": "Penyihir ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Berhasil berbagi permainan dengan teman", + "descriptionFullComplete": "Berhasil berbagi permainan dengan teman", + "name": "Berbagi adalah Peduli" + }, + "Stayin' Alive": { + "description": "Menangkan permainan tanpa mati", + "descriptionComplete": "Memenangkan permainan tanpa mati", + "descriptionFull": "Menangkan ${LEVEL} tanpa mati", + "descriptionFullComplete": "Memenangkan ${LEVEL} tanpa mati", + "name": "Bertahan Hidup" + }, + "Super Mega Punch": { + "description": "Buat 100% damage dengan satu pukulan", + "descriptionComplete": "Membuat 100% damage dengan satu pukulan", + "descriptionFull": "Buat 100% damage dengan satu pukulan pada ${LEVEL}", + "descriptionFullComplete": "Membuat 100% damage dengan satu pukulan pada ${LEVEL}", + "name": "Tinjuan Mega Super" + }, + "Super Punch": { + "description": "Buat 50% damage dengan satu pukulan", + "descriptionComplete": "Membuat 50% damage dengan satu pukulan", + "descriptionFull": "Buat 50% damage dengan satu pukulan pada ${LEVEL}", + "descriptionFullComplete": "Membuat 50% damage dengan satu pukulan pada ${LEVEL}", + "name": "Tinjuan Super" + }, + "TNT Terror": { + "description": "Bunuh 6 musuh menggunakan TNT", + "descriptionComplete": "Membunuh 6 musuh menggunakan TNT", + "descriptionFull": "Bunuh 6 musuh menggunakan TNT pada ${LEVEL}", + "descriptionFullComplete": "Membunuh 6 musuh menggunakan TNT pada ${LEVEL}", + "name": "Teror TNT" + }, + "Team Player": { + "descriptionFull": "Mulai permainan tim dengan 4+ pemain", + "descriptionFullComplete": "Memulai permainan tim dengan 4+ pemain", + "name": "Pemain tim" + }, + "The Great Wall": { + "description": "Hentikan semua musuh", + "descriptionComplete": "Menghentikan semua musuh", + "descriptionFull": "Hentikan semua musuh pada ${LEVEL}", + "descriptionFullComplete": "Menghentikan semua musuh pada ${LEVEL}", + "name": "Si Tembok Baja" + }, + "The Wall": { + "description": "Hentikan semua musuh", + "descriptionComplete": "Menghentikan semua musuh", + "descriptionFull": "Hentikan semua musuh pada ${LEVEL}", + "descriptionFullComplete": "Menghentikan semua musuh pada ${LEVEL}", + "name": "Si Tembok" + }, + "Uber Football Shutout": { + "description": "Menang tanpa memperbolehkan musuh mencetak skor", + "descriptionComplete": "Menang tanpa memperbolehkan musuh mencetak skor", + "descriptionFull": "Menangkan ${LEVEL} tanpa memperbolehkan musuh mencetak skor", + "descriptionFullComplete": "Memenangkan ${LEVEL} tanpa memperbolehkan musuh mencetak skor", + "name": "${LEVEL} Beres" + }, + "Uber Football Victory": { + "description": "Menangkan permainan", + "descriptionComplete": "Memenangkan permainan", + "descriptionFull": "Menangkan permainan pada ${LEVEL}", + "descriptionFullComplete": "Memenangkan permainan pada ${LEVEL}", + "name": "Juara ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Kalahkan semua gelombang", + "descriptionComplete": "Mengalahkan semua gelombang", + "descriptionFull": "Kalahkan semua gelombang pada ${LEVEL}", + "descriptionFullComplete": "Mengalahkan semua gelombang pada ${LEVEL}", + "name": "Juara ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Selesaikan semua gelombang", + "descriptionComplete": "Menyelesaikan semua gelombang", + "descriptionFull": "Selesaikan semua gelombang pada ${LEVEL}", + "descriptionFullComplete": "Menyelesaikan semua gelombang pada ${LEVEL}", + "name": "Juara ${LEVEL}" + } + }, + "achievementsRemainingText": "Achievement Tersisa:", + "achievementsText": "Achievement", + "achievementsUnavailableForOldSeasonsText": "Maaf, spesifik achievement tidak tersedia untuk musim lama.", + "addGameWindow": { + "getMoreGamesText": "Game Lain...", + "titleText": "Tambah Game" + }, + "allowText": "Izinkan", + "alreadySignedInText": "Akunmu telah masuk di perangkat lain;\nSilakan beralih akun atau menutup permainanmu \ndi perangkat lain dan coba lagi.", + "apiVersionErrorText": "Modul ${NAME} gagal dimuat; Menarget api-version ${VERSION_USED}; kami membutuhkan ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" nyalakan ini hanya jika menggunakan headphone)", + "headRelativeVRAudioText": "VR Audio Head-Relative", + "musicVolumeText": "Volume Musik", + "soundVolumeText": "Volume Suara", + "soundtrackButtonText": "Pengiring lagu", + "soundtrackDescriptionText": "(masukkan musik Kamu untuk diputar saat permainan)", + "titleText": "Audio" + }, + "autoText": "Otomatis", + "backText": "Kembali", + "banThisPlayerText": "Melarang Pemain Ini", + "bestOfFinalText": "Terbaik dari ${COUNT} Final", + "bestOfSeriesText": "Terbaik dari ${COUNT}:", + "bestRankText": "Urutan terbaikmu #${RANK}", + "bestRatingText": "Rating terbaikmu ${RATING}", + "bombBoldText": "BOM", + "bombText": "Bom", + "boostText": "Dorongan", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} dikonfigurasi di aplikasinya sendiri.", + "buttonText": "tombol", + "canWeDebugText": "Bolehkah BombSquad mengirim informasi kerusakan\ndan info penggunaan ke pengembang secara otomatis? \n\nData pribadi tidak akan dikirim dan\nmembantu game berjalan lebih baik.", + "cancelText": "Batal", + "cantConfigureDeviceText": "Maaf, ${DEVICE} tidak dapat dikonfigurasi.", + "challengeEndedText": "Tantangan selesai", + "chatMuteText": "Abaikan Percakapan", + "chatMutedText": "Percakapan Diabaikan", + "chatUnMuteText": "Menampilkan kembali percakapan", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Kamu harus menyelesaikan\nlevel ini untuk dapat lanjut!", + "completionBonusText": "Bonus Kelengkapan", + "configControllersWindow": { + "configureControllersText": "Atur Pengontrol", + "configureKeyboard2Text": "Atur Keyboard Player 2", + "configureKeyboardText": "Atur Keyboard", + "configureMobileText": "Perangkat ponsel sebagai pengontrol", + "configureTouchText": "Atur Kontrol Layar", + "ps3Text": "Pengontrol PS3", + "titleText": "Pengontrol", + "wiimotesText": "Wiimote", + "xbox360Text": "Pengontrol Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Catatan: pengontrol dapat digunakan berdasarkan versi Android Kamu.", + "pressAnyButtonText": "Tekan tombol kontroller yang\n ingin Kamu atur,,,", + "titleText": "Konfigurasi Pengontrol" + }, + "configGamepadWindow": { + "advancedText": "Lanjutan", + "advancedTitleText": "Pengaturan Pengontrol Lanjutan", + "analogStickDeadZoneDescriptionText": "(nyalakan ini jika karaktermu ngedrift saat kamu melepaskan analog stik)", + "analogStickDeadZoneText": "Analog Stick Zona Mati", + "appliesToAllText": "(Berlakukan ke semua kontrol tipe ini)", + "autoRecalibrateDescriptionText": "(nyalakan ini jika karaktermu tidak bergerak dengan kecepatan penuh)", + "autoRecalibrateText": "Otomatis kalibrasi kembali stik analog", + "axisText": "axis", + "clearText": "hapus", + "dpadText": "dpad", + "extraStartButtonText": "Tambahan tombol start", + "ifNothingHappensTryAnalogText": "Jika tidak terjadi apa-apa,coba berlakukan stik analog langsung", + "ifNothingHappensTryDpadText": "Jika tidak terjadi apa-apa,coba berlakukan ke d-pad langsung", + "ignoreCompletelyDescriptionText": "Cegah pengontrol ini untuk mempengaruhi game atau menu", + "ignoreCompletelyText": "Abaikan Sepenuhnya", + "ignoredButton1Text": "Abaikan tombol 1", + "ignoredButton2Text": "Abaikan tombol 2", + "ignoredButton3Text": "Abaikan tombol 3", + "ignoredButton4Text": "Abaikan tombol 4", + "ignoredButtonDescriptionText": "(Gunakan ini untuk mencegah tombol 'home' atau 'sync' yang mempengaruhi UI)", + "pressAnyAnalogTriggerText": "Tekan Tombol analog....", + "pressAnyButtonOrDpadText": "Tekan Tombol dpad", + "pressAnyButtonText": "Tekan tombol", + "pressLeftRightText": "Tekan Tombol kiri atau kanan", + "pressUpDownText": "Tekan tombol atas atau bawah..", + "runButton1Text": "Jalankan tombol 1", + "runButton2Text": "Jalankan tombol 2", + "runTrigger1Text": "Jalankan pelatuk 1", + "runTrigger2Text": "Jalankan pelatuk 2", + "runTriggerDescriptionText": "(Analog memungkinkan kamu untuk lari pada berbagai kecepatan)", + "secondHalfText": "Gunakan ini untuk pengontrol kedua dari\n2-pengontrol-dalam-1 alat yang\nditunjukan sebagai pengontrol tunggal", + "secondaryEnableText": "Aktifkan", + "secondaryText": "Pengontrol kedua", + "startButtonActivatesDefaultDescriptionText": "(Matikan jika tombol start Kamu lebih dari tombol 'menu')", + "startButtonActivatesDefaultText": "Tombol start mengaktifkan widget biasanya", + "titleText": "Pengaturan Pengontrol", + "twoInOneSetupText": "Pengaturan Pengontrol 2-dalam-1", + "uiOnlyDescriptionText": "(Cegah kontrol ini dari mengikuti permainan yang asli)", + "uiOnlyText": "Batas untuk menu guna", + "unassignedButtonsRunText": "Tidak berlakukan semua tombol lari", + "unsetText": "", + "vrReorientButtonText": "Tombol reorientasi VR" + }, + "configKeyboardWindow": { + "configuringText": "Mengaturi ${DEVICE}", + "keyboard2NoteText": "Catatan: beberapa keyboard hanya dapat memasukan beberapa tombol yang ditekan pada satu waktu,\njadi memiliki keyboard pemain kedua mungkin bekerja lebih baik\njika ada pemisah keyboard yang terpasang untuk digunakan\nPerlu dicatat bahwa Kamu masih butuh memasukan tombol unik untuk\nkedua pemain di kasus ini" + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Ukuran kontrol aksi", + "actionsText": "Aksi", + "buttonsText": "tombol", + "dragControlsText": "< geser kontrol untuk memposisikannya >", + "joystickText": "Joystick", + "movementControlScaleText": "Skala kontrol penggerak", + "movementText": "Pergerakan", + "resetText": "Kembalikan ke awal", + "swipeControlsHiddenText": "Sembunyikan ikon geser", + "swipeInfoText": "Model kontrol 'geser' membutuhkan penggunaan sedikit \nnamun membuat mudah untuk bermain tanpa melihat pengontrol", + "swipeText": "Geser", + "titleText": "Atur layar sentuh" + }, + "configureItNowText": "Atur sekarang?", + "configureText": "Konfigurasi", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App store", + "bestResultsText": "Untuk hasil yang lebih baik, Kamu membutuhkan jaringan Wi-Fi yang bebas lag.\nKamu dapat menurunkan lag Wi-Fi dengan cara mematikan alat wireless lainnya,\ndengan bermain dekat dengan router Wi-Fi dan dengan menyambungkan ke host\ngame langsung ke jaringan via ethernet", + "explanationText": "Untuk menggunakan smartphone atau tablet sebagai pengontrol,\npasang \"${REMOTE_APP_NAME}\" app. Alat apapun dapat tersambung ke\n${APP_NAME} game melalui Wi-Fi, dan ini gratis!", + "forAndroidText": "Untuk Android:", + "forIOSText": "Untuk iOS:", + "getItForText": "Dapatkan ${REMOTE_APP_NAME} untuk iOS di Apple App Store\natau untuk Android di Google Play Store atau Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Gunakan Perangkat untuk kntroler:" + }, + "continuePurchaseText": "Lanjutkan untuk ${PRICE}?", + "continueText": "Lanjutkan", + "controlsText": "Kontrol", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Ini akan tidak berlaku ke ranking semua waktu.", + "activenessInfoText": "Kelipatan ini naik di hari ketika Kamu bermain\ndan turun di haru ketika Kamu tidak bermain", + "activityText": "Aktivitas", + "campaignText": "Ekspedisi", + "challengesInfoText": "Dapatkan hadiah dengan menyelesaikan mini games\n\nHadiah dan tingkat kesulitan level bertambah\nsaat tantangan terselesaikan \ndan berkurang jika waktu habis atau menyerah", + "challengesText": "Tantangan", + "currentBestText": "Terbaik saat ini", + "customText": "Kustom", + "entryFeeText": "Masuk", + "forfeitConfirmText": "Kehilangan tantangan ini??", + "forfeitNotAllowedYetText": "Tantangan ini belum dapat hiang", + "forfeitText": "Kehilangan", + "multipliersText": "Kelipatan", + "nextChallengeText": "Tantangan berikutnya", + "nextPlayText": "Permainan berikutnya", + "ofTotalTimeText": "Dari ${TOTAL}", + "playNowText": "Main sekarang", + "pointsText": "Poin", + "powerRankingFinishedSeasonUnrankedText": "(Sesi berakhir tidak terangking)", + "powerRankingNotInTopText": "(tidak di atas ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} poin", + "powerRankingPointsMultText": "(x ${NUMBER} poin)", + "powerRankingPointsText": "${NUMBER} poin", + "powerRankingPointsToRankedText": "(${CURRENT} dari ${REMAINING} poin)", + "powerRankingText": "Rangking power", + "prizesText": "Hadiah", + "proMultInfoText": "Pemain dengan peningkatan ${PRO}\nmendapatkan sebuah ${PERCENT}% poin tambahan disini.", + "seeMoreText": "Lebih...", + "skipWaitText": "Lewati Tunggu", + "timeRemainingText": "Waktu tersisa", + "toRankedText": "untuk Peringkat", + "totalText": "Total", + "tournamentInfoText": "Bersaing untuk skor tinggi dengan\npemain lain di liga Kamu.\n\nHadiah diberikan ke atas\npapan skor", + "welcome1Text": "Selamat datang di ${LEAGUE}.Kamu dapat tingkatkan \nranking liga dengan dapatkan peringkat berlian, selesaikan \npenghargaan dan menenangkan piala di turnamen", + "welcome2Text": "Kamu juga dapat mendapatkan tiket dari aktivitas yang sama.\nTiket dapat digunakan untuk membuka karakter baru, peta, dan\nmini games,untuk masuk liga, dan lainnya", + "yourPowerRankingText": "Peringkat Kekuatan Kamu:" + }, + "copyOfText": "Salinan ${NAME}", + "createEditPlayerText": "", + "createText": "Buat", + "creditsWindow": { + "additionalAudioArtIdeasText": "Audio tambahan,Early Artwork, dan ide dari ${NAME}", + "additionalMusicFromText": "Musik Tambahan Dari ${NAME}", + "allMyFamilyText": "Semua teman saya dan keluarga saya yang menolong untuk memainkan test", + "codingGraphicsAudioText": "Koding, Grafik, Dan audio dari ${NAME}", + "languageTranslationsText": "Penerjemah bahasa:", + "legalText": "Legal:", + "publicDomainMusicViaText": "Musik publik dominan via ${NAME}", + "softwareBasedOnText": "Software ini berasal dari bagian kerja dari ${NAME}", + "songCreditText": "${TITLE} Dimainkan oleh ${PERFORMER}\nKomposisi oleh ${COMPOSER}, Aransemen oleh ${ARRANGER}, Publikasi oleh ${PUBLISHER},\nSumber dari ${SOURCE}", + "soundAndMusicText": "Suara & Musik:", + "soundsText": "Suara (${SOURCE}):", + "specialThanksText": "Terima Kasih Khusus:", + "thanksEspeciallyToText": "dan Terima Kasih juga kepada ${NAME}", + "titleText": "${APP_NAME} Kredit", + "whoeverInventedCoffeeText": "Siapapun itu yang menyajikan Kopi" + }, + "currentStandingText": "Posisimu sekarang: #${RANK}", + "customizeText": "Ubah...", + "deathsTallyText": "${COUNT} kematian", + "deathsText": "Kematian", + "debugText": "debug", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Penting!: disarankan untuk mengatur Pengaturan->Grafis->Tekstur ke 'Tinggi' saat mencoba ini.", + "runCPUBenchmarkText": "Menjalankan CPU Benchmark", + "runGPUBenchmarkText": "Jalankan GPU Benchmark", + "runMediaReloadBenchmarkText": "Menjalankan Media-Reload Benchmark", + "runStressTestText": "Menjalankan test stress", + "stressTestPlayerCountText": "Jumlah Pemain", + "stressTestPlaylistDescriptionText": "Stress Test Playlist", + "stressTestPlaylistNameText": "Nama PLaylist", + "stressTestPlaylistTypeText": "Tipe Playlist", + "stressTestRoundDurationText": "Durasi Permainan", + "stressTestTitleText": "Uji Stress", + "titleText": "Uji Benchmarks % Stress", + "totalReloadTimeText": "Total waktu memuat: ${TIME} (lihat log untuk selengkapnya)" + }, + "defaultGameListNameText": "Playlist ${PLAYMODE} Semula", + "defaultNewGameListNameText": "Playlist ${PLAYMODE} Ku", + "deleteText": "Hapus", + "demoText": "Demo", + "denyText": "Tolak", + "desktopResText": "Desktop Res", + "difficultyEasyText": "Mudah", + "difficultyHardOnlyText": "Khusus Mode Sulit", + "difficultyHardText": "Sulit", + "difficultyHardUnlockOnlyText": "Level ekspedisi ini khusus untuk mode sulit. \nKamu pikir kamu bisa!?!?!", + "directBrowserToURLText": "dimohon langsung ke web-browser untuk URL:", + "disableRemoteAppConnectionsText": "Matikan Koneksi App-Remot", + "disableXInputDescriptionText": "Izinkan lebih dari 4 pengontrol tapi mungkin agak lemot.", + "disableXInputText": "Blokir XInput", + "doneText": "Selesai", + "drawText": "Seri", + "duplicateText": "Duplikat", + "editGameListWindow": { + "addGameText": "Tambah\nGame", + "cantOverwriteDefaultText": "Tidak dapat mengubah playlist semula!", + "cantSaveAlreadyExistsText": "Playlist dengan nama ini sudah ada!", + "cantSaveEmptyListText": "Tidak dapat menyimpan playlist kosong!", + "editGameText": "Ubah\nPermainan", + "listNameText": "Nama Playlist", + "nameText": "Nama", + "removeGameText": "Hapus\nPermainan", + "saveText": "Simpan Daftar", + "titleText": "Pengaturan Playlist" + }, + "editProfileWindow": { + "accountProfileInfoText": "Profil spesial ini mengikuti nama\ndan icon sesuai akun Kamu.\n\n${ICONS}\n\nBuat profil lain untuk menggunakan\nnama dan icon yang berbeda.", + "accountProfileText": "(Profil Akun)", + "availableText": "Nama ini \"${NAME}\" tersedia.", + "characterText": "Karakter", + "checkingAvailabilityText": "Memeriksa Ketersediaan \"${NAME}\"...", + "colorText": "warna", + "getMoreCharactersText": "Dapatkan karakter lain...", + "getMoreIconsText": "Dapatkan icon lain...", + "globalProfileInfoText": "profil pemain global dijamin untuk memiliki nama unik\ndi seluruh dunia. Termasuk juga icon lain.", + "globalProfileText": "(Profil Global)", + "highlightText": "highlight", + "iconText": "icon", + "localProfileInfoText": "Profile lokal tidak mempunyai ikon dan nama tidak terjamin unik.\nTingkatkan ke profil dunia untuk mendapatkan nama unik dan pemain dapat tambahkan ikon kustom", + "localProfileText": "(Profil lokal)", + "nameDescriptionText": "Nama Pemain", + "nameText": "Nama", + "randomText": "Acak", + "titleEditText": "Ubah Profil", + "titleNewText": "Profil Baru", + "unavailableText": "\"${NAME}\" tidak tersedia; coba nama lain.", + "upgradeProfileInfoText": "Ini akan jadi nama Kamu dalam game ini\ndan memungkinkan Kamu untuk menetapkan ikon kustom.", + "upgradeToGlobalProfileText": "Tingkatkan ke Global Profil" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Kamu tidak dapat menghapus soundtrack asal.", + "cantEditDefaultText": "tidak dapat mengubah soundtrack asal. Gandakan atau buat soundtrack baru.", + "cantOverwriteDefaultText": "tidak dapat menimpa soundtrack asal", + "cantSaveAlreadyExistsText": "Soundtrack dengan nama ini telah digunakan!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Soundtrack asal", + "deleteConfirmText": "Hapus Soundtrack:\n\n'${NAME}'?", + "deleteText": "Hapus\nSoundtrack", + "duplicateText": "Gandakan\nSoundtrack", + "editSoundtrackText": "Pengaturan Soundtrack", + "editText": "Ubah\nSoundtrack", + "fetchingITunesText": "Mengambil playlist Music App...", + "musicVolumeZeroWarning": "Perhatian: suara musik menjadi 0", + "nameText": "Nama", + "newSoundtrackNameText": "Soundtrack saya ${COUNT}", + "newSoundtrackText": "Soundtrack Baru:", + "newText": "Buat\nSoundtrack", + "selectAPlaylistText": "Pilih Playlist", + "selectASourceText": "Sumber Musik", + "testText": "Test", + "titleText": "Soundtrack", + "useDefaultGameMusicText": "Musik Game Asal", + "useITunesPlaylistText": "Music App Playlist", + "useMusicFileText": "Data Musik (mp3, dll)", + "useMusicFolderText": "berkas dari Data Musik" + }, + "editText": "Edit", + "endText": "Akhiri", + "enjoyText": "Nikmati!", + "epicDescriptionFilterText": "${DESCRIPTION} dalam slow-motion yang epik.", + "epicNameFilterText": "${NAME} Epik", + "errorAccessDeniedText": "akses ditolak", + "errorOutOfDiskSpaceText": "media penyimpanan tidak cukup", + "errorText": "Error", + "errorUnknownText": "kesalahan tak teridentifikasi", + "exitGameText": "Keluar dari ${APP_NAME}?", + "exportSuccessText": "'${NAME}' TEREXPORT", + "externalStorageText": "Penyimpanan Eksternal", + "failText": "Gagal", + "fatalErrorText": "O-ow, sesuatu hilang atau rusak. \nCoba install ulang BombSquad atau kontak\n${EMAIL} untuk bantuan.", + "fileSelectorWindow": { + "titleFileFolderText": "Pilih File atau Folder", + "titleFileText": "Ambil File", + "titleFolderText": "Pilih Folder", + "useThisFolderButtonText": "Gunakan Folder" + }, + "filterText": "Filter", + "finalScoreText": "Skor Final", + "finalScoresText": "Skor Final", + "finalTimeText": "Waktu Final", + "finishingInstallText": "Selesai menginstall, tunggu sebentar...", + "fireTVRemoteWarningText": "* Untuk mempermudah, gunakan\npengontrol permainan atau pasang\naplikasi '${REMOTE_APP_NAME}'\ndi HP atau tabletmu.", + "firstToFinalText": "Final Pertama Mencapai ${COUNT}", + "firstToSeriesText": "Pertama Mencapai ${COUNT}", + "fiveKillText": "MATI LIMA!!", + "flawlessWaveText": "Gelombang Mulus!", + "fourKillText": "MATI EMPAT!!", + "friendScoresUnavailableText": "Skor teman tidak tersedia.", + "gameCenterText": "PusatGame", + "gameCircleText": "LingkaranGame", + "gameLeadersText": "Pemimpin Game ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "Kamu tidak dapat menghapus daftar putar default.", + "cantEditDefaultText": "tidak dapat mengubah Playlist Awal! Gandakan atau buat baru.", + "cantShareDefaultText": "Kamu tidak dapat bagikan playlist semula", + "deleteConfirmText": "Hapus \"${LIST}\"?", + "deleteText": "Hapus\nPlaylist", + "duplicateText": "Gandakan\nPlaylist", + "editText": "Ubah\nPlaylist", + "newText": "Buat\nPlaylist", + "showTutorialText": "Lihat Panduan", + "shuffleGameOrderText": "Acak Urutan Game", + "titleText": "Ubah ${TYPE} Playlists" + }, + "gameSettingsWindow": { + "addGameText": "Tambah Game" + }, + "gamesToText": "${WINCOUNT} menang lawan ${LOSECOUNT} menang", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Catatan: tiap perangkat dapat punya lebih dari\nsatu pemain jika memang ada pengontrol yang cukup.", + "aboutDescriptionText": "Gunakan tab ini untuk mengadakan acara.\n\nDengan adanya acara, Kamu dapat bermain\ndengan teman di perangkat yang berbeda.\n\nGunakan tombol ${PARTY} di kanan atas\nuntuk chat dan berinteraksi di acara. \n(pada pengontrol, tekan ${BUTTON} saat di menu)", + "aboutText": "Perihal", + "addressFetchErrorText": "", + "appInviteMessageText": "${NAME} mengirim ${COUNT} tiket ke ${APP_NAME}", + "appInviteSendACodeText": "Kirimkan Kode", + "appInviteTitleText": "Undangan Aplikasi ${APP_NAME}", + "bluetoothAndroidSupportText": "(bekerja dengan semua Android yang punya Bluetooth)", + "bluetoothDescriptionText": "Buat/ikut acara lewat Bluetooth:", + "bluetoothHostText": "Adakan acara!", + "bluetoothJoinText": "Ikut acara!", + "bluetoothText": "Bluetooth", + "checkingText": "sedang memeriksa...", + "copyCodeConfirmText": "Kode disalin ke papan klip.", + "copyCodeText": "Salin Kode", + "dedicatedServerInfoText": "Untuk hasil terbaik, buatlah server yang bagus. lihat bombsquadgame.com/server untuk membuatnya", + "disconnectClientsText": "${COUNT} pemain akan putus\nhubungan. Yakin?", + "earnTicketsForRecommendingAmountText": "Teman Kamu akan mendapatkan ${COUNT} tiket jika mereka memainkan game ini\n(dan Kamu akan mendapatkan ${YOU_COUNT} untuk setiap mereka memainkan game ini)", + "earnTicketsForRecommendingText": "Bagikan permainan\nuntuk tiket gratis...", + "emailItText": "Lewat Surel", + "favoritesSaveText": "Simpan Sebagai Favorit", + "favoritesText": "Favorit", + "freeCloudServerAvailableMinutesText": "Server cloud gratis berikutnya tersedia dalam ${MINUTES} menit.", + "freeCloudServerAvailableNowText": "Server cloud gratis tersedia!", + "freeCloudServerNotAvailableText": "Tidak ada server cloud gratis yang tersedia.", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} tiket dari ${NAME}", + "friendPromoCodeAwardText": "Kamu akan mendapatkan ${COUNT} tiket setiap kode ini digunakan.", + "friendPromoCodeExpireText": "kode ini akan berakhir dalam ${EXPIRE_HOURS} jam dan hanya berlaku untuk pemain baru.", + "friendPromoCodeInstructionsText": "Untuk menggunakannya, buka ${APP_NAME} dan buka \"Pengaturan-> Lanjutan-> Masukkan Kode\".\nLihat bombsquadgame.com untuk tautan unduhan untuk semua platform yang didukung.", + "friendPromoCodeRedeemLongText": "ini dapat digunakan untuk ${COUNT} tiket gratis hingga batas maksimal ${MAX_USES} orang.", + "friendPromoCodeRedeemShortText": "ini dapat digunkanan untuk mendapatkan ${COUNT} tiket.", + "friendPromoCodeWhereToEnterText": "(di \"Pengaturan-> Lanjutan-> Masukkan Kode\")", + "getFriendInviteCodeText": "Dapatkan Kode Undangan Teman", + "googlePlayDescriptionText": "Undang pemain Google Play ke acaramu:", + "googlePlayInviteText": "Undang", + "googlePlayReInviteText": "Ada ${COUNT} pemain Google Play di acaramu\nyang akan terputus jika Kamu undang ulang. \nJangan lupa masukan mereka ke undangan.", + "googlePlaySeeInvitesText": "Lihat Undangan", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(versi Android / Google Play)", + "hostPublicPartyDescriptionText": "Selenggarakan Acara Publik", + "hostingUnavailableText": "Hosting Tidak Tersedia", + "inDevelopmentWarningText": "Catatan:\n\nBermain dalam jaringan masih baru dan dalam\nperkembangan. Sementara, sangat disarankan para\npemain ada di jaringan Wi-Fi yang sama.", + "internetText": "Internet", + "inviteAFriendText": "Teman Kamu belum memainkan ini? undang mereka sekarang\ndan mereka akan mendapatkan ${COUNT} tiket gratis.", + "inviteFriendsText": "Undang Teman", + "joinPublicPartyDescriptionText": "Gabung dengan Acara Publik", + "localNetworkDescriptionText": "Bergabunglah dengan Acara Terdekat (LAN, Bluetooth, dll.)", + "localNetworkText": "Jaringan Lokal", + "makePartyPrivateText": "Buat acaraku pribadi", + "makePartyPublicText": "Buat acaraku publik", + "manualAddressText": "Alamat", + "manualConnectText": "Hubungkan", + "manualDescriptionText": "Ikut acara di alamat:", + "manualJoinSectionText": "Gabung Berdasarkan Alamat", + "manualJoinableFromInternetText": "Apakah Kamu dapat bergabung internet?:", + "manualJoinableNoWithAsteriskText": "TIDAK*", + "manualJoinableYesText": "IYA", + "manualRouterForwardingText": "*perbaiki dengan mengonfigurasi router ke UDP port ${PORT} ke alamat lokal Kamu", + "manualText": "Manual", + "manualYourAddressFromInternetText": "Alamat Kamu dari internet:", + "manualYourLocalAddressText": "Alamat lokal Kamu:", + "nearbyText": "Dekat", + "noConnectionText": "", + "otherVersionsText": "(Versi lain)", + "partyCodeText": "Kode Pesta", + "partyInviteAcceptText": "Terima", + "partyInviteDeclineText": "Tolak", + "partyInviteGooglePlayExtraText": "(Lihat tab 'Google Play' di jendela 'Berkumpul')", + "partyInviteIgnoreText": "Cuekin", + "partyInviteText": "${NAME} mengundangmu\nke acaranya!", + "partyNameText": "Nama acara", + "partyServerRunningText": "Server pesta Anda sedang berjalan.", + "partySizeText": "ukuran", + "partyStatusCheckingText": "memeriksa status...", + "partyStatusJoinableText": "sekarang orang lain dapat gabung ke acaramu dari internet", + "partyStatusNoConnectionText": "Gak dapat nyambung ke server", + "partyStatusNotJoinableText": "orang lain gak dapat gabung ke acaramu lewat internet", + "partyStatusNotPublicText": "acaramu bukan acara publik", + "pingText": "Ping", + "portText": "Port", + "privatePartyCloudDescriptionText": "Pihak swasta dijalankan di server cloud khusus; tidak diperlukan konfigurasi router.", + "privatePartyHostText": "Selenggarakan Pesta Pribadi", + "privatePartyJoinText": "Bergabunglah dengan Pesta Pribadi", + "privateText": "Pribadi", + "publicHostRouterConfigText": "Ini mungkin memerlukan konfigurasi penerusan port di router Anda. Untuk opsi yang lebih mudah, selenggarakan pesta pribadi.", + "publicText": "Publik", + "requestingAPromoCodeText": "Memesan Kode...", + "sendDirectInvitesText": "Kirim Undangan", + "shareThisCodeWithFriendsText": "Bagikan kode ini ke teman-teman mu!", + "showMyAddressText": "Liatin Alamatku", + "startHostingPaidText": "Host Sekarang Dengan ${COST}", + "startHostingText": "Tuan rumah", + "startStopHostingMinutesText": "Anda dapat memulai dan menghentikan hosting gratis untuk ${MINUTES} menit berikutnya.", + "stopHostingText": "Hentikan Hosting", + "titleText": "Gabung", + "wifiDirectDescriptionBottomText": "Jika semua perangkat punya tab 'Wi-Fi Direct', maka seharusnya semua dapat saling\nkoneksi. Ketika sudah konek semua, buat team\ndi tab 'Jaringan Lokal', seperti Wi-Fi biasa.\n\nUntuk hasil maksimal, host Wi-Fi Direct juga harus sebagai host team di ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct dapat digunakan untuk koneksi Android tanpa hotspot.\nBekerja paling bagus pada Android 4.2 lebih.\n\nUntuk itu, buka Pengaturan Wi-Fi dan pilih 'Wi-Fi Direct' di menu.", + "wifiDirectOpenWiFiSettingsText": "Buka Pengaturan Wi-Fi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(bekerja antar platform)", + "worksWithGooglePlayDevicesText": "(bekerja antar perangkat yang menggunakan versi Android)", + "youHaveBeenSentAPromoCodeText": "Kamu telah mengirim sebuah promo code ${APP_NAME}!" + }, + "getTicketsWindow": { + "freeText": "Gratis!", + "freeTicketsText": "Tiket Gratis", + "inProgressText": "Transaksi sedang dalam proses; sabar, coba lagi nanti.", + "purchasesRestoredText": "Pembelian dipulihkan.", + "receivedTicketsText": "Mendapatkan ${COUNT} tiket!", + "restorePurchasesText": "Memulihkan Pembelian", + "ticketPack1Text": "Paket Tiket Kecil", + "ticketPack2Text": "Paket Tiket Sedang", + "ticketPack3Text": "Paket Tiket Besar", + "ticketPack4Text": "Paket Tiket Jumbo", + "ticketPack5Text": "Paket Tiket Raksasa", + "ticketPack6Text": "Paket Tiket Berlimpah", + "ticketsFromASponsorText": "Dapatkan ${COUNT} tiket\ndari iklan", + "ticketsText": "${COUNT} tiket", + "titleText": "Dapatkan Tiket", + "unavailableLinkAccountText": "Maaf, pembelian tidak dapat dilakukan di perangkat ini.\nsebagai antisipasi, kamu dapat menautkan akun ini ke perangkat\nlain dan melakukan pembelian di sana.", + "unavailableTemporarilyText": "mohon maaf, saat ini layanan tidak tersedia; coba lagi lain waktu.", + "unavailableText": "maaf, tidak tersedia.", + "versionTooOldText": "Maaf, versi permainan ini terlalu usang; silakan perbarui dengan yang terbaru.", + "youHaveShortText": "kamu memiliki ${COUNT}", + "youHaveText": "kamu memiliki ${COUNT} tiket" + }, + "googleMultiplayerDiscontinuedText": "Maaf, Google's multiplayer service tidak lagi tersedia.\nSaya sedang bekerja pada penggantian secepat mungkin.\nHingga saat itu, silakan coba metode koneksi lainnya.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Selalu", + "fullScreenCmdText": "Layar Penuh (Cmd-F)", + "fullScreenCtrlText": "Layar Penuh (Ctrl+F)", + "gammaText": "Gamma", + "highText": "Tinggi", + "higherText": "Tertinggi", + "lowText": "Rendah", + "mediumText": "Sedang", + "neverText": "Tak Pernah", + "resolutionText": "Resolusi", + "showFPSText": "Tampilkan FPS", + "texturesText": "Tekstur", + "titleText": "Grafik", + "tvBorderText": "Perbatasan TV", + "verticalSyncText": "Vertical Sync", + "visualsText": "Visual" + }, + "helpWindow": { + "bombInfoText": "- Bomb -\nLebih kuat dari Tinju, tapi\ndapat menjadi bom bunuh diri.\ncoba untuk melempar sebelum\nsumbu akan habis.", + "canHelpText": "${APP_NAME} Solusinya!", + "controllersInfoText": "Kamu dapat bermain ${APP_NAME} dengan temanmu melalui sebuah\nJaringan, atau kamu dapat bermain dalam perangkat yang sama\njika kamu memiliki kontrol yang cukup. ${APP_NAME} menyediakan\npengontrol digital melalui aplikasi '${REMOTE_APP_NAME}'.\nlihat di Pengaturan -> Kontrol untuk info lebih lanjut.", + "controllersText": "Kontrol", + "controlsSubtitleText": "karakter ${APP_NAME} Memiliki beberapa gerakan dasar:", + "controlsText": "Kontrol", + "devicesInfoText": "Versi VR dari ${APP_NAME} dapat dimainkan lewat jaringan dengan\nversi reguler. Jadi siapkan ponsel, Tablet atau Komputermu\ndan Mainkan Game ini! Fitur ini juga dapat menjadi menyenangkan\ndengan cara mengizinkan orang lain melihat permainan Kamu saat\nmenggunakan VR!", + "devicesText": "Perangkat", + "friendsGoodText": "Sangat diperlukan. ${APP_NAME} Sangat menyenangkan dengan beberapa\npemain (maksimal 8 pemain) dalam sekali permainan.", + "friendsText": "Teman", + "jumpInfoText": "- Lompat -\nLompat untuk melewati gundukan,\nmelempar lebih jauh, dan\nmengekspresikan kebehagiaan Kamu.", + "orPunchingSomethingText": "Atau menghajar sesuatu, melemparnya ke Jurang, dan meledakannya dengan beberapa bom mematikan.", + "pickUpInfoText": "- Angkat -\nMengangkat Bendera, Musuh, atau\napapun yang tidak melekat di tanah.\nTekan sekali lagi untuk melempar.", + "powerupBombDescriptionText": "Memberikan Kamu 3 bom sekaligus.\nLebih baik dari pada 1.", + "powerupBombNameText": "Bomb Beruntun", + "powerupCurseDescriptionText": "Hindari ini segera jika kamu \ntidak ingin mati!", + "powerupCurseNameText": "Kutukan", + "powerupHealthDescriptionText": "Mengembalikan darah Kamu\nseperti semula.", + "powerupHealthNameText": "Kotak Medis", + "powerupIceBombsDescriptionText": "Lebih lemah dari bom biasa\ntapi membuat musuh Kamu beku,\npanik, gelisah, dan rapuh.", + "powerupIceBombsNameText": "Bom Beku", + "powerupImpactBombsDescriptionText": "sedikit lebih lemah dar bom\nbiasa, tapi akan meledak saat terbentur.", + "powerupImpactBombsNameText": "Bom Pemicu", + "powerupLandMinesDescriptionText": "berisi 3 paket; berguna untuk\nbertahan atau menghentikan\nlangkah musuhmu.", + "powerupLandMinesNameText": "Ranjau", + "powerupPunchDescriptionText": "Membuat tinjumu lebih kuat,\nlebih cepat, bahkan lebih baik.", + "powerupPunchNameText": "Sarung Tinju", + "powerupShieldDescriptionText": "menahan beberapa serangan\nsehingga darah Kamu tidak berkurang.", + "powerupShieldNameText": "Energi Pelindung", + "powerupStickyBombsDescriptionText": "lengket ke apapun yang tersentuh.\nSungguh Menjijikan..", + "powerupStickyBombsNameText": "Bom Lengket", + "powerupsSubtitleText": "Jelas sekali, tidak ada game yang bakal seru tanpa Kekuatan Tambahan:", + "powerupsText": "Kekuatan Tambahan", + "punchInfoText": "- Tinju -\nakan lebih berguna saat\nKamu bergerak cepat. jadi lari\ndan berputarlah seperti maddog.", + "runInfoText": "- Lari -\nSEMUA tombol dapat digunakan untuk lari. Kecuali tombol pusar Kamu, haha. Lari\ndapat membuat Kamu cepat tapi sulit untuk berbelok, jadi hati-hati dengan jurang.", + "someDaysText": "Terkadang, kamu ingin sekali menghajar sesuatu atau menghancurkan sesuatu.", + "titleText": "Bantuan ${APP_NAME}", + "toGetTheMostText": "Untuk menikmati game ini, kamu perlu menyiapkan:", + "welcomeText": "Selamat datang di ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} sedang merenung untuk memilih menu -", + "importPlaylistCodeInstructionsText": "Masukan kode untuk mengimpor playlist :", + "importPlaylistSuccessText": "Terimpor ${TYPE} playlist '${NAME}'", + "importText": "Impor", + "importingText": "Mengimpor...", + "inGameClippedNameText": "dalam game akan\n\"${NAME}\"", + "installDiskSpaceErrorText": "ERROR: Gagal menginstall. \nMungkin penyimpanan Kamu terlalu penuh. \nMohon hapus beberapa file dan coba lagi.", + "internal": { + "arrowsToExitListText": "tekan ${LEFT} atau ${RIGHT} untuk keluar", + "buttonText": "Tombol", + "cantKickHostError": "Kamu tak dapat mengeluarkan host", + "chatBlockedText": "${NAME} di blokir chatnya selama ${TIME} detik.", + "connectedToGameText": "'${NAME}' Bergabung", + "connectedToPartyText": "Bergabung ke team ${NAME}!", + "connectingToPartyText": "Menyambung...", + "connectionFailedHostAlreadyInPartyText": "Sambungan Gagal, Host sedang dalam team lain.", + "connectionFailedPartyFullText": "Koneksi gagal; acara sudah penuh", + "connectionFailedText": "Gagal Menghubungkan.", + "connectionFailedVersionMismatchText": "Gagal menghubungkan; Host sedang menjalankan versi lain dari game ini.\nPastikan kamu memperbarui game lalu coba lagi.", + "connectionRejectedText": "Sambungan ditolak.", + "controllerConnectedText": "${CONTROLLER} tersambung.", + "controllerDetectedText": "1 pengontrol terdeteksi.", + "controllerDisconnectedText": "${CONTROLLER} terputus.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} terputus. Silahkan menghubungkan kembali.", + "controllerForMenusOnlyText": "Pengontrol ini tidak dapat digunakan untuk bermain, hanya untuk memilih menu.", + "controllerReconnectedText": "${CONTROLLER} terhubung kembali.", + "controllersConnectedText": "${COUNT} pengontrol terhubung.", + "controllersDetectedText": "${COUNT} pengontrol terdeteksi.", + "controllersDisconnectedText": "${COUNT} pengontrol terputus.", + "corruptFileText": "Data rusak terdeteksi. dimohon untuk pasang ulang, atau kirimkan surel ke ${EMAIL}", + "errorPlayingMusicText": "Gagal memutar musik: ${MUSIC}", + "errorResettingAchievementsText": "Tidak dapat mengulang Online Achievments; dimohon coba lagi.", + "hasMenuControlText": "${NAME} mempunyai kendali menu.", + "incompatibleNewerVersionHostText": "Host menggunakan versi baru dari game ini\nSilakan perbarui dan coba lagi", + "incompatibleVersionHostText": "Host berjalan dengan versi yang berbeda dengan game.\nPastikan Kamu up-to-date dan coba lagi", + "incompatibleVersionPlayerText": "${NAME} menjalankan versi game yang berbeda.\nPastikan versi game kalian sama dan coba lagi.", + "invalidAddressErrorText": "Error: alamat tidak jelas.", + "invalidNameErrorText": "Error: nama tidak jelas.", + "invalidPortErrorText": "Error: port tidak jelas.", + "invitationSentText": "Undangan terkirim.", + "invitationsSentText": "${COUNT} undangan terkirim.", + "joinedPartyInstructionsText": "Seseorang bergabung di acaramu.\nTekan 'Main' untuk mulai permainan.", + "keyboardText": "Keyboard", + "kickIdlePlayersKickedText": "Mengeluarkan ${NAME} karena diam.", + "kickIdlePlayersWarning1Text": "${NAME} akan dikeluarkan dalam ${COUNT} detik jika masih diam.", + "kickIdlePlayersWarning2Text": "(Kamu dapat mematikan ini di Pengaturan -> Lanjutan)", + "leftGameText": "Keluar '${NAME}'.", + "leftPartyText": "Keluar dari acara ${NAME}'", + "noMusicFilesInFolderText": "Folder tidak memiliki file musik.", + "playerJoinedPartyText": "${NAME} bergabung ke acara!", + "playerLeftPartyText": "${NAME} keluar dari acara.", + "rejectingInviteAlreadyInPartyText": "Membatalkan undangan (sudah ada di acara).", + "serverRestartingText": "Memulai ulang. Silakan masuk dalam beberapa saat lagi", + "serverShuttingDownText": "Server sedang menutup...", + "signInErrorText": "Gagal masuk", + "signInNoConnectionText": "Gagal masuk. (apa jaringanmu tidak terkoneksi internet?)", + "telnetAccessDeniedText": "ERROR: pengguna tidak mengizinkan akses telnet.", + "timeOutText": "(waktu akan habis dalam ${TIME} detik lagi)", + "touchScreenJoinWarningText": "Kamu telah bergabung dengan layar sentuh.\nJika ini kesalahan. tekan 'Menu->Mode Penonton' saja", + "touchScreenText": "Layar Sentuh", + "unableToResolveHostText": "Error:Tidak dapat menyambung pada server", + "unavailableNoConnectionText": "Maaf, layanan tidak tersedia (apa jaringanmu tidak terkoneksi internet?)", + "vrOrientationResetCardboardText": "Gunakan ini untuk mengulang orientasi VR.\nUntuk memainkan game ini Kamu harus mempunyai kontroller eksternal.", + "vrOrientationResetText": "Atur ulang orientasi VR", + "willTimeOutText": "(waktu habis jika diam)" + }, + "jumpBoldText": "LOMPAT", + "jumpText": "Lompat", + "keepText": "Simpan", + "keepTheseSettingsText": "Simpan pengaturan ini?", + "keyboardChangeInstructionsText": "Tekan spasi dua kali untuk mengubah keyboard.", + "keyboardNoOthersAvailableText": "Tidak ada keyboard lain tersedia.", + "keyboardSwitchText": "Mengalihkan keyboard ke \"${NAME}\".", + "kickOccurredText": "${NAME} dikeluarkan.", + "kickQuestionText": "Keluarkan ${NAME}?", + "kickText": "Keluarkan", + "kickVoteCantKickAdminsText": "Admin tidak dapat dikeluarkan", + "kickVoteCantKickSelfText": "Kamu tidak dapat mengeluarkan dirimu sendiri", + "kickVoteFailedNotEnoughVotersText": "Tidak cukup pemain untuk pengambilan suara.", + "kickVoteFailedText": "Pengambilan suara untuk mengeluarkan pemain gagal", + "kickVoteStartedText": "Pengambilan suara untuk mengeluarkan ${NAME} sudah dimulai.", + "kickVoteText": "Tentukan suara untuk mengeluarkan", + "kickVotingDisabledText": "Pengambilan suara untuk mengeluarkan pemain di nonaktifkan", + "kickWithChatText": "Ketik ${YES} di kolom obrolan jika setuju dan ${NO} jika tidak setuju.", + "killsTallyText": "${COUNT} pembunuhan", + "killsText": "Pembunuhan", + "kioskWindow": { + "easyText": "Mudah", + "epicModeText": "Mode Epik", + "fullMenuText": "Semua Menu", + "hardText": "Sulit", + "mediumText": "Sedang", + "singlePlayerExamplesText": "Coontoh Main Sendiri / Berteman", + "versusExamplesText": "Contoh Versus" + }, + "languageSetText": "Bahasa yang sedang digunakan adalah \"${LANGUAGE}\".", + "lapNumberText": "Putaran ${CURRENT}/${TOTAL}", + "lastGamesText": "(${COUNT} game terakhir)", + "leaderboardsText": "Papan Juara", + "league": { + "allTimeText": "Keseluruhan", + "currentSeasonText": "Musim Sekarang (${NUMBER})", + "leagueFullText": "Liga ${NAME}", + "leagueRankText": "Peringkat Liga", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga${SUFFIX}", + "seasonEndedDaysAgoText": "Musim berakhir ${NUMBER} hari yang lalu", + "seasonEndsDaysText": "Musim berakhir ${NUMBER} hari lagi", + "seasonEndsHoursText": "Musim berakhir ${NUMBER} jam lagi", + "seasonEndsMinutesText": "Musim berakhir ${NUMBER} menit lagi", + "seasonText": "Musim ${NUMBER}", + "tournamentLeagueText": "Kamu harus berada di liga ${NAME} untuk memasukinya", + "trophyCountsResetText": "Hitungan trofimu akan diulang di musim berikutnya" + }, + "levelBestScoresText": "Skor terbaik di ${LEVEL}", + "levelBestTimesText": "Waktu terbaik di ${LEVEL}", + "levelFastestTimesText": "Waktu tercepat pada ${LEVEL}", + "levelHighestScoresText": "Skor tertinggi pada ${LEVEL}", + "levelIsLockedText": "${LEVEL} terbuka.", + "levelMustBeCompletedFirstText": "Selesaikan ${LEVEL} dulu.", + "levelText": "Level ${NUMBER}", + "levelUnlockedText": "Level Terbuka!", + "livesBonusText": "Nyawa Tambahan", + "loadingText": "memuat", + "loadingTryAgainText": "Memuat; Coba lagi nanti.. sabar ya..", + "macControllerSubsystemBothText": "Keduanya (Tidak direkomendasikan)", + "macControllerSubsystemClassicText": "Klasik", + "macControllerSubsystemDescriptionText": "Coba ubah ini jika pengontrol Kamu tidak bekerja", + "macControllerSubsystemMFiNoteText": "Pengontrol yang dibuat-untuk-ios/Mac terdeteksi;\nKamu mungkin ingin mengaktifkanya melalui Pengaturan -> Pengontrol", + "macControllerSubsystemMFiText": "Dibuat-untuk-iOS/Mac", + "macControllerSubsystemTitleText": "Dukungan pengontrol", + "mainMenu": { + "creditsText": "Kredit", + "demoMenuText": "Menu Demo", + "endGameText": "Akhiri Permainan", + "exitGameText": "Keluar dari Permainan", + "exitToMenuText": "Ke Menu?", + "howToPlayText": "Cara bermain", + "justPlayerText": "(Hanya ${NAME})", + "leaveGameText": "Mode Penonton", + "leavePartyConfirmText": "Yakin Ingin Keluar?", + "leavePartyText": "Keluar", + "quitText": "Berhenti", + "resumeText": "Lanjutkan", + "settingsText": "Pengaturan" + }, + "makeItSoText": "Jadilah Demikian", + "mapSelectGetMoreMapsText": "Arena Lainnya", + "mapSelectText": "Pilih", + "mapSelectTitleText": "Arena ${GAME}", + "mapText": "Arena", + "maxConnectionsText": "Koneksi maksimal", + "maxPartySizeText": "Besar Ukuran Maksimal", + "maxPlayersText": "Jumlah Pemain Maksimal", + "modeArcadeText": "Mode Arcade", + "modeClassicText": "Mode Klasik", + "modeDemoText": "Mode Demo", + "mostValuablePlayerText": "Pemain Terunggul", + "mostViolatedPlayerText": "Pemain Teraniaya", + "mostViolentPlayerText": "Pemain Terkejam", + "moveText": "Gerak", + "multiKillText": "${COUNT} MATI!!", + "multiPlayerCountText": "${COUNT} pemain", + "mustInviteFriendsText": "CATATAN: kamu harus mengundang teman di\nopsi \"${GATHER}\" atau pasang\npengontrol untuk bermain di mode multi-pemain", + "nameBetrayedText": "${NAME} membunuh rekannya ${VICTIM}", + "nameDiedText": "${NAME} meninggal.", + "nameKilledText": "${NAME} membunuh ${VICTIM}", + "nameNotEmptyText": "Nama harus diisi!", + "nameScoresText": "${NAME} mencetak poin!", + "nameSuicideKidFriendlyText": "${NAME} meninggal tiba-tiba.", + "nameSuicideText": "${NAME} bunuh diri.", + "nameText": "Nama", + "nativeText": "Asli", + "newPersonalBestText": "Rekor Pribadi Baru!", + "newTestBuildAvailableText": "Uji coba baru tersedia! (${VERSION} bangun ${BUILD}).\nDapatkan di ${ADDRESS}", + "newText": "Baru", + "newVersionAvailableText": "Versi ${APP_NAME} terbaru tersedia", + "nextAchievementsText": "Pencapaian berikutnya:", + "nextLevelText": "Level Berikutnya", + "noAchievementsRemainingText": "- tidak ada", + "noContinuesText": "(tidak dapat melanjutkan)", + "noExternalStorageErrorText": "Tidak ada penyimpanan eksternal", + "noGameCircleText": "Kesalahan: tidak masuk ke LingkaranGame", + "noProfilesErrorText": "Kamu tidak punya profil pemain, jadi '${NAME}' dipakai. \nMasuk Pengaturan->Profil Pemain untuk membuat profil. ", + "noScoresYetText": "Belum ada skor.", + "noThanksText": "Tidak, Terima kasih", + "noTournamentsInTestBuildText": "PERHATIAN: Skor turnamen dari build tes ini akan di abaikan", + "noValidMapsErrorText": "Tidak ada arena valid untuk game ini.", + "notEnoughPlayersRemainingText": "Tidak ada pemain tersisa; keluar dan main ulang.", + "notEnoughPlayersText": "Kamu butuh sedikitnya ${COUNT} pemain untuk memulai permainan!", + "notNowText": "Jangan Sekarang", + "notSignedInErrorText": "Kamu harus masuk untuk lakukan ini.", + "notSignedInGooglePlayErrorText": "Kamu harus masuk pakai Google Play untuk lakukan ini.", + "notSignedInText": "belum masuk", + "nothingIsSelectedErrorText": "Tidak ada yang dipilih!", + "numberText": "#${NUMBER}", + "offText": "Mati", + "okText": "Baik", + "onText": "Nyala", + "oneMomentText": "Sebentar...", + "onslaughtRespawnText": "${PLAYER} akan bangkit pada gelombang ${WAVE}", + "orText": "${A} atau ${B}", + "otherText": "Lainnya...", + "outOfText": "(#${RANK} dari ${ALL})", + "ownFlagAtYourBaseWarning": "Benderamu harus\nberada di basismu!", + "packageModsEnabledErrorText": "Game yang melalui jaringan tidak diperbolehkan ketika mod-paket-lokal diaktifkan (lihat Pengaturan->Lanjutan)", + "partyWindow": { + "chatMessageText": "Pesan Obrolan", + "emptyText": "acaramu kosong", + "hostText": "(pembuat)", + "sendText": "Kirim", + "titleText": "acaramu" + }, + "pausedByHostText": "(terhenti oleh pemilik)", + "perfectWaveText": "Gelombang Sempurna!", + "pickUpText": "Ambil", + "playModes": { + "coopText": "Koloni", + "freeForAllText": "Saling bunuh", + "multiTeamText": "Multi-Tim", + "singlePlayerCoopText": "Pemain Tunggal / Lawan komputer", + "teamsText": "Tim" + }, + "playText": "Main", + "playWindow": { + "oneToFourPlayersText": "1-4 pemain", + "titleText": "Main", + "twoToEightPlayersText": "2-8 pemain" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerDelayedJoinText": "${PLAYER} akan masuk pada ronde berikutnya.", + "playerInfoText": "Info Pemain", + "playerLeftText": "${PLAYER} meninggalkan game.", + "playerLimitReachedText": "Batas pemain ${COUNT} tercapai; tidak dapat bergabung lagi.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Kamu tidak dapat menghapus akun profilmu.", + "deleteButtonText": "Hapus\nProfil", + "deleteConfirmText": "Hapus '${PROFILE}'?", + "editButtonText": "Ubah\nProfil", + "explanationText": "(setiap nama pemain dan penampilan kustom untuk akun ini)", + "newButtonText": "Profil\nBaru", + "titleText": "Profil Pemain" + }, + "playerText": "Pemain", + "playlistNoValidGamesErrorText": "Playlist ini mempunyai game terbuka yang tidak valid.", + "playlistNotFoundText": "Daftar Putar tidak ditemukan", + "playlistText": "Daftar Putar", + "playlistsText": "Daftar Putar", + "pleaseRateText": "Jika Kamu menyukai ${APP_NAME}, yuk luangkan waktu sejenak untuk menilai dan membubuhkan komentar. Ini akan membantu kami untuk menyempurnakan permainan yang akan datang.\n\nterima kasih!\n-eric", + "pleaseWaitText": "Mohon tunggu...", + "pluginsDetectedText": "Plugin baru terdeteksi. Aktifkan/konfigurasikan di pengaturan.", + "pluginsText": "Plugin", + "practiceText": "Latihan", + "pressAnyButtonPlayAgainText": "Tekan tombol apa saja untuk kembali bermain...", + "pressAnyButtonText": "Tekan tombol apa saja untuk lanjut...", + "pressAnyButtonToJoinText": "tekan apa saja untuk bergabung...", + "pressAnyKeyButtonPlayAgainText": "Tekan apa saja untuk kembali bermain...", + "pressAnyKeyButtonText": "Tekan apa saja untuk lanjut...", + "pressAnyKeyText": "Tekan apa saja...", + "pressJumpToFlyText": "** Tekan tombol lompat terus menerus untuk terbang **", + "pressPunchToJoinText": "tekan PUKUL untuk bergabung..", + "pressToOverrideCharacterText": "tekan ${BUTTONS} untuk menimpa karaktermu.", + "pressToSelectProfileText": "tekan ${BUTTONS} untuk memilih pemain", + "pressToSelectTeamText": "tekan ${BUTTONS} untuk memilih tim", + "promoCodeWindow": { + "codeText": "Kode", + "enterText": "Masuk" + }, + "promoSubmitErrorText": "Kesalahan saat mengirim kode; periksa koneksi internet Kamu", + "ps3ControllersWindow": { + "macInstructionsText": "Matikan daya pada bagian belakang PS3-mu, pastikan\nBluetooth aktif pada Mac-mu, lalu hubungkan kontrollermu\nke Mac-mu dengan kabel USB untuk memasangkannya. Sekarang, Kamu\ndapat menggunakan kontrollermu dengan kabel USB atau Bluetooth.\n\nPada beberapa perangkat Mac, Kamu mungkin akan dimintai kata sandi ketika memasangkannya.\nJika ini terjadi, lihat beberapa petunjuk atau gunakan google untuk meminta bantuan.\n\n\n\n\n\nKontroller PS3 yang terhubung dengan jaringan akan terlihat pada daftar perangkat\ndalam System Preferences->Bluetooth. Kamu mungkin harus menghapusnya\ndalam daftar tersebut ketika Kamu ingin menggunakan kontroller PS3mu kembali.\n\nDan pastikan untuk memutuskannya dari Bluetooth ketika sedang tidak\ndigunakan atau baterainya akan secara otomatis terkuras.\n\nBluetooth biasanya menangani sampai 7 perangkat yang terhubung,\nmeski jarak tempuhmu berbeda.", + "ouyaInstructionsText": "Untuk menggunakan kontroller PS3mu dengan OUYA, hubungkan saja dengan kabel USB\nsekali untuk memasangkannya. Melakukan ini akan memutuskan kontrollermu yang lain, jadi\nKamu harus mengulang OUYA-mu dan cabut kabel USB.\n\nSekarang Kamu seharusnya dapat menggunakan kontrollermu untuk\nmenghubungkannya dengan jaringan. Ketika Kamu sudah selesai bermain, tekan tombol HOME\nselama 10 detik untuk mematikan kontroller; jika itu tetap menyala\nmaka bateraimu akan terkuras habis.", + "pairingTutorialText": "memasangkan video petunjuk", + "titleText": "Menggunakan Kontroller PS3 dengan ${APP_NAME}:" + }, + "punchBoldText": "PUKUL", + "punchText": "Pukul", + "purchaseForText": "Membeli dengan ${PRICE}", + "purchaseGameText": "Membeli Game", + "purchasingText": "Membeli...", + "quitGameText": "Keluar ${APP_NAME}?", + "quittingIn5SecondsText": "Keluar dalam 5 detik...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Acak", + "rankText": "Peringkat", + "ratingText": "Nilai", + "reachWave2Text": "Raih gelombang 2 untuk dapat peringkat", + "readyText": "Siap", + "recentText": "Terbaru", + "remoteAppInfoShortText": "${APP_NAME} akan menyenangkan ketika dimainkan dengan keluarga & teman.\nHubungkan satu atau lebih kontroller atau install app\n${REMOTE_APP_NAME} pada ponsel atau tablet untuk menggunakannya\nsebagai kontroller.", + "remote_app": { + "app_name": "Remot BombSquad", + "app_name_short": "RemotBS", + "button_position": "Posisi Tombol", + "button_size": "Ukuran Tombol", + "cant_resolve_host": "Tidak dapat menemukan pemilik.", + "capturing": "Menangkap…", + "connected": "Terhubung.", + "description": "Gunakan ponsel atau tabletmu sebagai pengontrol BombSquad.\nLebih dari 8 perangkat dapat terhubung sekaligus untuk keseruan multipemain lokal yang epik di sebuah TV atau tablet", + "disconnected": "Diputus server", + "dpad_fixed": "tetap", + "dpad_floating": "mengambang", + "dpad_position": "Posisi D-Pad", + "dpad_size": "Ukuran D-Pad", + "dpad_type": "Tipe D-Pad", + "enter_an_address": "Masukkan Alamat", + "game_full": "Permainan sudah penuh atau tidak menerima koneksi.", + "game_shut_down": "Permainan sudah berakhir", + "hardware_buttons": "Tombol Perangkat keras", + "join_by_address": "Masuk pakai Alamat...", + "lag": "Lag: ${SECONDS} detik", + "reset": "Ubah ke awal", + "run1": "Lari 1", + "run2": "Lari 2", + "searching": "Mencari BombSquad...", + "searching_caption": "Ketuk nama permainan yang kamu inginkan.\nPastikan Kamu berada di jaringan wifi yang sama dengan permainan tersebut.", + "start": "Mulai", + "version_mismatch": "Versi tidak sama.\nPastikan kamu menggunakan BombSquad dan Remot BombSquad\ndengan versi terbaru dan coba lagi." + }, + "removeInGameAdsText": "Buka \"${PRO}\" di toko untuk menghilangkan iklan game", + "renameText": "Mengubah Nama", + "replayEndText": "Akhiri Replay", + "replayNameDefaultText": "Replay Game Terakhir", + "replayReadErrorText": "Error membaca file replay.", + "replayRenameWarningText": "Ubah nama \"${REPLAY}\" setelah game jika Kamu ingin menyimpannya; jika tidak itu akan ditimpa.", + "replayVersionErrorText": "Maaf, replay ini dibuat dalam\nversi game yang berbeda dan tidak dapat digunakan", + "replayWatchText": "Nonton Replay", + "replayWriteErrorText": "Error menulis file replay", + "replaysText": "Replay", + "reportPlayerExplanationText": "Gunakan alamat surel ini untuk melaporkan tindakan curang, kata-kata yang tidak pantas, atau kelakuan buruk lainnya.\nTolong jelaskan dibawah ini:", + "reportThisPlayerCheatingText": "Curang", + "reportThisPlayerLanguageText": "Bahasa Yang Tidak Pantas", + "reportThisPlayerReasonText": "Apa yang ingin Kamu laporkan?", + "reportThisPlayerText": "Laporkan pemain ini", + "requestingText": "Meminta...", + "restartText": "Ulangi", + "retryText": "Ulangi", + "revertText": "Kembali", + "runText": "Lari", + "saveText": "Simpan", + "scanScriptsErrorText": "Galat saat memindai skrip; lihat log untuk detailnya.", + "scoreChallengesText": "Tantangan Skor", + "scoreListUnavailableText": "Daftar skor tidak ada", + "scoreText": "Skor", + "scoreUnits": { + "millisecondsText": "Milidetik", + "pointsText": "Poin", + "secondsText": "Detik" + }, + "scoreWasText": "(adalah ${COUNT})", + "selectText": "Pilih", + "seriesWinLine1PlayerText": "MEMENANGKAN", + "seriesWinLine1TeamText": "MEMENANGKAN", + "seriesWinLine1Text": "MEMENANGKAN", + "seriesWinLine2Text": "SERINYA!", + "settingsWindow": { + "accountText": "Akun", + "advancedText": "Lanjutan", + "audioText": "Suara", + "controllersText": "pengontrol", + "graphicsText": "Grafik", + "playerProfilesMovedText": "NB: Profil-Profil Pemain sudah dipindahkan di jendela Akun di menu utama.", + "playerProfilesText": "Profil Pemain", + "titleText": "Pengaturan" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(keyboard dari BombSquad)", + "alwaysUseInternalKeyboardText": "Gunakan Keyboard Internal", + "benchmarksText": "Tes Stres dan Benchmark", + "disableCameraGyroscopeMotionText": "Nonaktikan Gerakkan Kamera Giroskop", + "disableCameraShakeText": "Nonaktifkan Gerakkan Kamera", + "disableThisNotice": "(Kamu dapat matikan peringatan ini di pengaturan tambahan)", + "enablePackageModsDescriptionText": "(menyalakan kapabilitas modding ekstra menyebabkan kamu tidak dapat bermain di internet)", + "enablePackageModsText": "Izinkan Paket Mod Lokal", + "enterPromoCodeText": "Masukkan Kode", + "forTestingText": "NB: jumlah ini hanya untuk tes dan akan hilang saat keluar", + "helpTranslateText": "Translasi ${APP_NAME} selain Bahasa Inggris adalah bantuan \nkomunitas. Jika Kamu ingin membantu atau mengoreksi berkas\ntranslasi, silakan masuk ke situs berikut. Terima kasih!", + "kickIdlePlayersText": "Keluarkan Pemain Diam", + "kidFriendlyModeText": "Mode Dibawah Umur (kekerasan rendah, dll)", + "languageText": "Bahasa", + "moddingGuideText": "Cara Me-Modding", + "mustRestartText": "Kamu harus memulai ulang permainan untuk menerapkan perubahan.", + "netTestingText": "Tes Jaringan", + "resetText": "Atur ulang", + "showBombTrajectoriesText": "Lihat Lintasan Bom", + "showPlayerNamesText": "Tunjukkan Nama Pemain", + "showUserModsText": "Lihat Folder Mod", + "titleText": "Lanjutan", + "translationEditorButtonText": "Penyunting Translasi ${APP_NAME}", + "translationFetchErrorText": "status translasi tidak tersedia.", + "translationFetchingStatusText": "memeriksa status translasi", + "translationInformMe": "Beritahu saya jika bahasa yang saya gunakan harus diperbarui", + "translationNoUpdateNeededText": "bahasa ini sudah terbaharukan; Horeee !", + "translationUpdateNeededText": "** bahasa ini perlu diperbaharui! **", + "vrTestingText": "Tes VR" + }, + "shareText": "Bagikan", + "sharingText": "Membagikan...", + "showText": "Lihat", + "signInForPromoCodeText": "Kamu harus masuk ke akun agar kode berlaku.", + "signInWithGameCenterText": "Untuk menggunakan akun Game Center,\nmasuk ke Game Center dulu", + "singleGamePlaylistNameText": "Hanya ${GAME}", + "singlePlayerCountText": "1 pemain", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Pemilihan Karakter", + "Chosen One": "Yang Terpilih", + "Epic": "Game Mode Epik", + "Epic Race": "Balap Epik", + "FlagCatcher": "Tangkap Bendera", + "Flying": "Pikiran Bahagia", + "Football": "Rugby", + "ForwardMarch": "Serangan", + "GrandRomp": "Penaklukan", + "Hockey": "Hoki", + "Keep Away": "Menjauh !", + "Marching": "Bolak-Balik", + "Menu": "Menu Utama", + "Onslaught": "Pembantaian", + "Race": "Balapan", + "Scary": "Raja Bukit", + "Scores": "Layar Skor", + "Survival": "Eliminasi", + "ToTheDeath": "Pertarungan Mematikan", + "Victory": "Layar Skor Akhir" + }, + "spaceKeyText": "spasi", + "statsText": "Statistik", + "storagePermissionAccessText": "Ini membutuhkan akses penyimpanan", + "store": { + "alreadyOwnText": "Kamu sudah punya ${NAME}!", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Menghapus iklan dalam permainan dan omelan-layar\n• Membuka lebih banyak pengaturan permainan\n• Juga termasuk:", + "buyText": "Beli", + "charactersText": "Karakter", + "comingSoonText": "Segera Hadir...", + "extrasText": "Extra", + "freeBombSquadProText": "BombSquad sekarang gratis, karena dulu Kamu membelinya\nKamu mendapat tingkatan BombSquad Pro dan ${COUNT} tiket sebagai ucapan terima kasih.\nNikmati fitur barunya, dan terima kasih dukugannya!\n-Eric", + "holidaySpecialText": "Spesial Liburan", + "howToSwitchCharactersText": "pergi ke \"${SETTINGS} -> ${PLAYER_PROFILES}\" untuk mengubah karakter", + "howToUseIconsText": "(Buatlah profil pemain global (dalam jendela akun) untuk menggunakan ini)", + "howToUseMapsText": "(gunakan peta ini di tim/playlist bebasmu", + "iconsText": "Simbol", + "loadErrorText": "Tidak dapat memuat halaman.\nCek koneksi internetmu.", + "loadingText": "memuat", + "mapsText": "Peta", + "miniGamesText": "Mini Game", + "oneTimeOnlyText": "(sekali saja)", + "purchaseAlreadyInProgressText": "Pembelian barang ini sedang dalam proses.", + "purchaseConfirmText": "Beli ${ITEM}?", + "purchaseNotValidError": "Pembelian tidak valid.\nHubungi ${EMAIL} jika ini adalah error.", + "purchaseText": "Membeli", + "saleBundleText": "Paket Promo!", + "saleExclaimText": "Dijual!", + "salePercentText": "(Diskon ${PERCENT}%)", + "saleText": "DIJUAL", + "searchText": "Cari", + "teamsFreeForAllGamesText": "Permainan Tim / Bebas-untuk-Semua", + "totalWorthText": "*** ${TOTAL_WORTH} nilai! ***", + "upgradeQuestionText": "Tingkatkan?", + "winterSpecialText": "Spesial Musim Dingin", + "youOwnThisText": "- Kamu sudah memiliki ini -" + }, + "storeDescriptionText": "Permainan dengan 8 pemain!\n\nLedakkan temanmu (atau komputer) dalam turnamen ledakkan seperti Ambil-Bendera, Bom-Hoki, dan Pertarungan-Mematikan-Epik-Gerakkan-Lambat\n\nDukungan pengontrol Sederhana dan pengontrol luas membuatnya mudah untuk 8 orang untuk beraksi; Kamu juga dapat menggunakan perangkat ponselmu sebagai pengontrol dengan aplikasi ‘BombSquad Remote’!\n\nAwas Bom!\n\nKunjungi www.froemling.net/bombsquad untuk informasi lebih lanjut.", + "storeDescriptions": { + "blowUpYourFriendsText": "Meledakkan temanmu.", + "competeInMiniGamesText": "Bersaing dalam mini-game mulai dari balapan sampai terbang.", + "customize2Text": "Sesuaikan karakter, mini-game, dan juga soundtrack.", + "customizeText": "Sesaikan karakter dan buat daftar mini-gamemu sendiri.", + "sportsMoreFunText": "Olahraga lebih menyenangkan dengan peledak.", + "teamUpAgainstComputerText": "Satu tim melawan komputer." + }, + "storeText": "Toko", + "submitText": "Serahkan", + "submittingPromoCodeText": "Menyerahkan Kode ...", + "teamNamesColorText": "Nama Tim / Warna ...", + "telnetAccessGrantedText": "Akses telnet aktif.", + "telnetAccessText": "Akses telnet terdekteksi; izinkan?", + "testBuildErrorText": "Percobaan ini tidak aktif lagi; tolong cek versi barunya.", + "testBuildText": "Percobaan", + "testBuildValidateErrorText": "Tidak dapat mengesahkan percobaan. (Tidak tersedia koneksi internet?)", + "testBuildValidatedText": "Percobaan Sah; Nikmati!", + "thankYouText": "Terima kasih atas dukungan mu! Nikmati gamenya!!", + "threeKillText": "KEJAM ! 3 TERBUNUH", + "timeBonusText": "Bonus Waktu", + "timeElapsedText": "Waktu Berlalu", + "timeExpiredText": "Berakhir", + "timeSuffixDaysText": "${COUNT}h", + "timeSuffixHoursText": "${COUNT}j", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}d", + "tipText": "Petunjuk", + "titleText": "BomSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Teman Terbaik", + "tournamentCheckingStateText": "Memeriksa keadaan turnamen; harap tunggu...", + "tournamentEndedText": "Turnamen sudah berakhir. Turnamen yang baru akan mulai segera.", + "tournamentEntryText": "Biaya Masuk Turnamen", + "tournamentResultsRecentText": "Hasil Turnamen Terbaru", + "tournamentStandingsText": "Hasil Terbaik Turnamen", + "tournamentText": "Turnamen", + "tournamentTimeExpiredText": "Waktu Turnamen Berakhir", + "tournamentsText": "Turnamen", + "translations": { + "characterNames": { + "Agent Johnson": "Agen Jhonson", + "B-9000": "B-9000", + "Bernard": "Beruang", + "Bones": "Jerangkong", + "Butch": "Koboi", + "Easter Bunny": "Kelinci Paskah", + "Flopsy": "Kelinci", + "Frosty": "Manusia Salju", + "Gretel": "Gretel", + "Grumbledorf": "Penyihir", + "Jack Morgan": "Jack Morgan", + "Kronk": "Ade Rai", + "Lee": "Yudu", + "Lucky": "Lucky", + "Mel": "Yani", + "Middle-Man": "Manusia Super", + "Minimus": "Minimus", + "Pascal": "Pinguin", + "Pixel": "Peri", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Santa Klaus", + "Snake Shadow": "Ninja", + "Spaz": "Reno", + "Taobao Mascot": "Maskot Taobao", + "Todd": "Todd", + "Todd McBurton": "Rambo", + "Xara": "Xara", + "Zoe": "Putri", + "Zola": "Zola" + }, + "coopLevelNames": { + "${GAME} Training": "Latihan ${GAME}", + "Infinite ${GAME}": "${GAME} Tanpa Batas", + "Infinite Onslaught": "Pembantaian Tanpa Batas", + "Infinite Runaround": "Penjaga Tanpa Batas", + "Onslaught Training": "Latihan Pembantaian", + "Pro ${GAME}": "${GAME} Profesional", + "Pro Football": "Rugby Ahli", + "Pro Onslaught": "Pembantaian Ahli", + "Pro Runaround": "Penjaga Ahli", + "Rookie ${GAME}": "${GAME} Pemula", + "Rookie Football": "Rugby Pemula", + "Rookie Onslaught": "Pembantaian Pemula", + "The Last Stand": "Usaha Terakhir", + "Uber ${GAME}": "${GAME} Master", + "Uber Football": "Rugby Uber", + "Uber Onslaught": "Pembantaian Uber", + "Uber Runaround": "Master Penjaga" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Jadilah 'yang terpilih' dalam waktu yang ditentukan.\nBunuh 'yang terpilih' untuk menjadi 'yang terpilih'.", + "Bomb as many targets as you can.": "Bom target sebanyak mungkin.", + "Carry the flag for ${ARG1} seconds.": "Bawa bendera selama ${ARG1} detik.", + "Carry the flag for a set length of time.": "Bawa bendera dalam waktu yang ditentukan.", + "Crush ${ARG1} of your enemies.": "Hancurkan ${ARG1} musuh.", + "Defeat all enemies.": "Hancurkan semua musuh.", + "Dodge the falling bombs.": "Yang bersih ya.", + "Final glorious epic slow motion battle to the death.": "Pertarungan slow motion epik hingga kematian menjemput.", + "Gather eggs!": "Kumpulkan telur!", + "Get the flag to the enemy end zone.": "Bawa bendera sampai ujung lapangan.", + "How fast can you defeat the ninjas?": "Mampukah Kamu menjadi Hokage?", + "Kill a set number of enemies to win.": "Hancurkan sejumlah musuh.", + "Last one standing wins.": "Terakhir hidup menang.", + "Last remaining alive wins.": "Terakhir hidup menang.", + "Last team standing wins.": "Habisi tim lawan.", + "Prevent enemies from reaching the exit.": "Tahan musuh jangan sampai finish.", + "Reach the enemy flag to score.": "Sentuh bendera lawan untuk skor.", + "Return the enemy flag to score.": "Kembalikan bendera musuh untuk menyekor.", + "Run ${ARG1} laps.": "Lari ${ARG1} putaran.", + "Run ${ARG1} laps. Your entire team has to finish.": "Lari ${ARG1} putaran. Seluruh tim harus mencapai finish.", + "Run 1 lap.": "Lari 1 putaran.", + "Run 1 lap. Your entire team has to finish.": "Lari 1 putaran. Seluruh tim harus mencapai finish.", + "Run real fast!": "Lari! Ada Anjing!", + "Score ${ARG1} goals.": "Masukan ${ARG1} gol.", + "Score ${ARG1} touchdowns.": "Cetak ${ARG1} touchdown.", + "Score a goal.": "Cetak 1 Gol.", + "Score a touchdown.": "Cetak 1 touchdown.", + "Score some goals.": "Cetak beberapa gol.", + "Secure all ${ARG1} flags.": "Amankan ${ARG1} bendera.", + "Secure all flags on the map to win.": "Amankan semua bendera di arena untuk menang.", + "Secure the flag for ${ARG1} seconds.": "Amankan bendera selama ${ARG1} detik.", + "Secure the flag for a set length of time.": "Amankan bendera selama waktu yang ditentukan.", + "Steal the enemy flag ${ARG1} times.": "Curi bendera musuh ${ARG1} kali.", + "Steal the enemy flag.": "Curi bendera musuh.", + "There can be only one.": "Dunia ini tidak cukup luas untuk kita.", + "Touch the enemy flag ${ARG1} times.": "Sentuh bendera musuh ${ARG1} kali.", + "Touch the enemy flag.": "Sentuh bendera musuh.", + "carry the flag for ${ARG1} seconds": "bawa bendera selama ${ARG1} detik", + "kill ${ARG1} enemies": "Bunuh ${ARG1} musuh.", + "last one standing wins": "terakhir hidup menang", + "last team standing wins": "habisi tim lawan", + "return ${ARG1} flags": "kembalikan ${ARG1} bendera", + "return 1 flag": "kembalikan 1 bendera", + "run ${ARG1} laps": "lari ${ARG1} putaran", + "run 1 lap": "lari 1 putaran", + "score ${ARG1} goals": "masukan ${ARG1} gol", + "score ${ARG1} touchdowns": "Cetak ${ARG1} touchdown", + "score a goal": "cetak 1 gol", + "score a touchdown": "cetak 1 touchdown", + "secure all ${ARG1} flags": "amankan ${ARG1} bendera", + "secure the flag for ${ARG1} seconds": "amankan bendera selama ${ARG1} detik", + "touch ${ARG1} flags": "sentuh ${ARG1} bendera", + "touch 1 flag": "sentuh 1 bendera" + }, + "gameNames": { + "Assault": "Penyerbuan", + "Capture the Flag": "Curi Bendera", + "Chosen One": "Yang Terpilih", + "Conquest": "Penguasaan", + "Death Match": "Pertarungan Kematian", + "Easter Egg Hunt": "Memburu Easter Egg", + "Elimination": "Eliminasi", + "Football": "Rugby", + "Hockey": "Hoki", + "Keep Away": "Kejar-kejaran", + "King of the Hill": "Penguasa Bendera", + "Meteor Shower": "Hujan Meteor", + "Ninja Fight": "Perang Ninja", + "Onslaught": "Pembantaian", + "Race": "Balap Liar", + "Runaround": "Jangan Kabur", + "Target Practice": "Sasaran Tembak", + "The Last Stand": "Penyintas Terakhir" + }, + "inputDeviceNames": { + "Keyboard": "Keyboard", + "Keyboard P2": "Keyboard P2" + }, + "languages": { + "Arabic": "Arab", + "Belarussian": "Belarusia", + "Chinese": "Mandarin (disederhanakan)", + "ChineseTraditional": "Cina tradisional", + "Croatian": "Kroasia", + "Czech": "Ceko", + "Danish": "Denmark", + "Dutch": "Belanda", + "English": "Inggris", + "Esperanto": "Esperanto", + "Finnish": "Finlandia", + "French": "Prancis", + "German": "Jerman", + "Gibberish": "Rahasia", + "Greek": "Yunani", + "Hindi": "Hindi", + "Hungarian": "Hongaria", + "Indonesian": "Indonesia", + "Italian": "Italia", + "Japanese": "Jepang", + "Korean": "Korea", + "Persian": "Persia", + "Polish": "Polandia", + "Portuguese": "Portugis", + "Romanian": "Romania", + "Russian": "Rusia", + "Serbian": "Serbia", + "Slovak": "Slovakia", + "Spanish": "Spanyol", + "Swedish": "Swedia", + "Turkish": "Turki", + "Ukrainian": "Ukraina", + "Venetian": "Venesia", + "Vietnamese": "Vietnam" + }, + "leagueNames": { + "Bronze": "Perunggu", + "Diamond": "Berlian", + "Gold": "Emas", + "Silver": "Perak" + }, + "mapsNames": { + "Big G": "G Besar", + "Bridgit": "Jembatan", + "Courtyard": "Tempat Pengadilan", + "Crag Castle": "Kastil karang", + "Doom Shroom": "Jamur Gelap", + "Football Stadium": "Stadion Rugby", + "Happy Thoughts": "Pikiran Bahagia", + "Hockey Stadium": "Stadion Hoki", + "Lake Frigid": "Danau Beku", + "Monkey Face": "Wajah Monyet", + "Rampage": "Timbangan", + "Roundabout": "Luncuran", + "Step Right Up": "Tangga Penentuan", + "The Pad": "Tablet", + "Tip Top": "Bukit Bayangan", + "Tower D": "Menara D", + "Zigzag": "Lika Liku" + }, + "playlistNames": { + "Just Epic": "Hanya Epik", + "Just Sports": "Hanya Olahraga" + }, + "scoreNames": { + "Flags": "Bendera", + "Goals": "Gol", + "Score": "Skor", + "Survived": "Bertahan Hidup", + "Time": "Waktu", + "Time Held": "Lama Kuasa" + }, + "serverResponses": { + "A code has already been used on this account.": "Sebuah kode sudah digunakan pada aku ini.", + "A reward has already been given for that address.": "Hadiah telah diberikan ke alamat tersebut.", + "Account linking successful!": "Berhasil menghubungkan akun!", + "Account unlinking successful!": "Pemutusan akun berhasil!", + "Accounts are already linked.": "Akun sudah dihubungkan.", + "An error has occurred; (${ERROR})": "Sebuah kesalahan telah terjadi; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Sebuah kesalahan telah terjadi; tolong hubungi dukungan. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Sebuah error telah terjadi; tolong hubungi support@froemling.net.", + "An error has occurred; please try again later.": "Sebuah error telah terjadi; mohon coba lagi nanti.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Kamu yakin ingin menghubungkan akun ini?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nIni tidak dapat ditunda!", + "BombSquad Pro unlocked!": "BombSquad Pro terbuka!", + "Can't link 2 accounts of this type.": "Tidak dapat menghubungkan 2 tipe akun ini.", + "Can't link 2 diamond league accounts.": "Tidak dapat menghubungkan 2 akun liga berlian.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Tidak dapat menghubungkan; akan melampaui batas maksimum ${COUNT} akun yang dihubungkan.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Kecurangan terdeteksi; skor dan hadiah ditahan selama ${COUNT} hari", + "Could not establish a secure connection.": "Tidak dapat menyeimbangkan koneksi yang aman.", + "Daily maximum reached.": "Batas maksimal tercapai.", + "Entering tournament...": "Memasuki turnamen...", + "Invalid code.": "Kode Salah", + "Invalid payment; purchase canceled.": "Pembayaran tidak valid; pembelian dibatalkan.", + "Invalid promo code.": "Promo kode salah.", + "Invalid purchase.": "Pembelian tidak valid.", + "Invalid tournament entry; score will be ignored.": "Masukkan turnamen tidak valid; skor akan diabaikan.", + "Item unlocked!": "Item dibuka!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "PENAUTAN DITOLAK. ${ACCOUNT} berisi\ndata penting yang SEMUANYA AKAN HILANG.\nKamu dapat menautkannya dalam urutan yang berlawanan jika Kamu mau\n(dan kehilangan data akun INI sebagai gantinya)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Menghubungkan akun ${ACCOUNT} ke akun ini?\nSemua data yang ada di ${ACCOUNT} akan hilang.\nIni tidak dapat dibatalkan. Apa kamu yakin?", + "Max number of playlists reached.": "Batas maksimum daftar putar tercapai.", + "Max number of profiles reached.": "Batas maksimum profil tercapai.", + "Maximum friend code rewards reached.": "Batas maksimum hadiah kode teman tercapai.", + "Message is too long.": "Pesan terlalu panjang.", + "Profile \"${NAME}\" upgraded successfully.": "Profil \"${NAME}\" berhasil ditingkatkan.", + "Profile could not be upgraded.": "Profile tidak dapat di tingkatkan.", + "Purchase successful!": "Pembelian sukses!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Menerima ${COUNT} tiket untuk masuk.\nSilahkan kembali besok untuk menerima ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Server tidak lagi mendukung permainan dengan versi ini;\nSilahkan perbarui ke versi yang lebih baru.", + "Sorry, there are no uses remaining on this code.": "Maaf, kode ini tidak dapat dipakai lagi", + "Sorry, this code has already been used.": "Maaf,kode ini sudah digunakan", + "Sorry, this code has expired.": "Maaf, kode ini sudah kadaluarsa.", + "Sorry, this code only works for new accounts.": "Maaf, kode ini hanya berlaku untuk akun baru.", + "Temporarily unavailable; please try again later.": "Sedang tidak ada; mohon coba lagi nanti.", + "The tournament ended before you finished.": "Turnamen berakhir sebelum Kamu selesai.", + "This account cannot be unlinked for ${NUM} days.": "Akun ini tidak dapat diputuskan untuk ${NUM} hari.", + "This code cannot be used on the account that created it.": "Kode ini tidak dapat dipakai di akun yang membuatnya.", + "This is currently unavailable; please try again later.": "Ini saat ini tidak tersedia; silahkan coba lagi nanti.", + "This requires version ${VERSION} or newer.": "Ini membutuhkan versi ${VERSION} atau versi yang baru.", + "Tournaments disabled due to rooted device.": "Turnamen dinonaktifkan karena perangkat yang di-rooting.", + "Tournaments require ${VERSION} or newer": "Turnamen membutuhkan ${VERSION} atau lebih baru", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Memutus ${ACCOUNT} dari akun ini?\nSemua data dari ${ACCOUNT} akan disetel ulang.\n(kecuali penghargaan di beberapa kasus)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "PERINGATAN: keluhan tentang kecurangan telah diajukan terhadap akun Kamu.\nAkun yang ditemukan curang akan dilarang. Silakan bermain adil.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Apakah Kamu mau menghubungkan akun perangkatmu dengan yang ini>\n\nAkun perangkatmu adalah ${ACCOUNT1}\nAkun ini adalah ${ACCOUNT2}\n\nIni akan mengizinkanmu untuk menyimpan datamu.\nPeringatan: ini tidak dapat dibatalkan!", + "You already own this!": "Kamu sudah mempunyai ini!", + "You can join in ${COUNT} seconds.": "Kamu dapat bergabung lagi pada ${COUNT} detik", + "You don't have enough tickets for this!": "Kamu tidak mempunyai cukup tiket untuk ini!", + "You don't own that.": "Kamu tidak memiliki itu.", + "You got ${COUNT} tickets!": "Kamu dapat ${COUNT} tiket!", + "You got a ${ITEM}!": "Kamu dapat ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Kamu dipromosikan ke liga yang baru; selamat!", + "You must update to a newer version of the app to do this.": "Kamu harus perbarui app ke versi yang baru untuk melakukan ini.", + "You must update to the newest version of the game to do this.": "Kamu harus memperbaharui ke versi permainan yang lebih baru untuk melakukan ini.", + "You must wait a few seconds before entering a new code.": "Kamu harus menunggu beberapa detik sebelum memasukkan kode yang baru.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Kamu peringkat ke #${RANK} pada turnament terakhir. Terima kasih sudah bermain!", + "Your account was rejected. Are you signed in?": "Akunmu ditolak. Apakah Kamu terdaftar?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Salinan gamemu telah dimodifikasi.\nMohon kembalikan perubahan apapun dan coba lagi.", + "Your friend code was used by ${ACCOUNT}": "Kode temanmu digunakan oleh ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Menit", + "1 Second": "1 Detik", + "10 Minutes": "10 Menit", + "2 Minutes": "2 Menit", + "2 Seconds": "2 Detik", + "20 Minutes": "20 Menit", + "4 Seconds": "4 Detik", + "5 Minutes": "5 Menit", + "8 Seconds": "8 detik", + "Allow Negative Scores": "Izinkan Skor Bernilai Negatif", + "Balance Total Lives": "Total Nyawa Seimbang", + "Bomb Spawning": "Bom Muncul", + "Chosen One Gets Gloves": "Yang Terpilih Mendapat Sarung Tinju", + "Chosen One Gets Shield": "Terpilih akan mendapatkan perisai", + "Chosen One Time": "Waktu Yang Terpilih", + "Enable Impact Bombs": "Izinkan Dampak Bom", + "Enable Triple Bombs": "Izinkan Bom Beruntun", + "Entire Team Must Finish": "Seluruh Tim Harus Melewati Finish", + "Epic Mode": "Mode Epik", + "Flag Idle Return Time": "Waktu Bendera Diam Kembali", + "Flag Touch Return Time": "Waktu Bendera Sentuh Kembali", + "Hold Time": "Waktu Tunggu", + "Kills to Win Per Player": "Pembunuhan untuk Para Pemain", + "Laps": "Lap", + "Lives Per Player": "Nyawa Tiap Pemain", + "Long": "Lama", + "Longer": "Lama Sekali", + "Mine Spawning": "Ranjau Muncul", + "No Mines": "Tidak Ada Ranjau", + "None": "Tak Satupun", + "Normal": "Sedang", + "Pro Mode": "Mode Ahli", + "Respawn Times": "Jeda Hingga Kembali", + "Score to Win": "Skor menang", + "Short": "Pendek", + "Shorter": "Pendek sekali", + "Solo Mode": "Mode Solo", + "Target Count": "Jumlah Target", + "Time Limit": "Batas waktu" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} didiskualifikasi karena ${PLAYER} meninggalkan permainan", + "Killing ${NAME} for skipping part of the track!": "Membunuh ${NAME} Karena melewati bagian permainan ini!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Peringatan ke ${NAME}: turbo / tombol-spamming menjatuhkan Kamu." + }, + "teamNames": { + "Bad Guys": "Si Jahat", + "Blue": "Biru", + "Good Guys": "Si Baik", + "Red": "Merah" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Lari-lompat-putar-danpukul yang sempurna dan pada waktu yang tepat dapat\nmembunuh hanya dengan sekali serangan dan dapatkan penghargaan dari temanmu.", + "Always remember to floss.": "Selalu ingat untuk buang air.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Buat profil pemain untuk teman dan dirimu sendiri dengan\nnama dan penampilan yang kamu sukai daripada menggunakan yang acak.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Kotak Terkutuk membuatmu menjadi bom waktu.\nSatu-satunya obat adalah mencari kotak medis.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Selain penampilannya, semua kemampuan karakter sama,\npilih saja yang cocok untukmu.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Jangan terlalu sombong dengan perisai energi; Kamu masih dapat dibuang ke dalam jurang.", + "Don't run all the time. Really. You will fall off cliffs.": "Jangan selalu berlari atau Kamu akan jatuh ke jurang.", + "Don't spin for too long; you'll become dizzy and fall.": "Jangan putar terlalu lama; kamu akan pusing dan jatuh.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Tahan tombol apa saja untuk lari.", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Tahan tombol apa saja untuk lari. Kamu akan menjadi lebih cepat\ntapi tidak dapat berbelok dengan lancar, hati-hati dengan jurang.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Bom es tidak cukup kuat, tetapi mereka membekukan\napapun yang mereka kena dan membuat orang mudah dihancurkan.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Jika seseorang mengangkatmu, pukul dia dan dia akan melepaskanmu.\nIni bekerja didunia nyata juga.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Jika Kamu tidak mempunyai kontroller, pasang app '${REMOTE_APP_NAME}\npada perangkat handphonemu untuk menggunakannya sebagai kontroller.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "", + "If you kill an enemy in one hit you get double points for it.": "Jika Kamu membunuh musuh dengan sekali serangan maka Kamu akan mendapat poin ganda.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Jika Kamu mengambil kutukan, satu-satunya harapanmu untuk hidup adalah\nmenemukan kotak medis dalam 5 detik.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Jika Kamu berdiam diri saja, maka Kamu akan tamat. Lari dan menghindar untuk tetap hidup..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Jika Kamu mendapatkan banyak pemain yang masuk dan bergabung dalam permainan, aktifkan 'keluarkan pemain diam'\npada pengaturan untuk jaga-jaga jika seseorang lupa meninggalkan permainan.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Jika perangkatmu mulai panas atau Kamu mau menghemat baterai,\nturunkan \"Visual\" atau \"Resolusi\" pada Pengaturan->Grafis", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Jika framerate-mu lemah, coba turunkan resolusi\natau visual dalam pengaturan grafis permainan.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "Dalam Ambil-Benderanya, benderamu sendiri harus ada pada bentengmu untuk mencetak skor, jika tim lain\nakan mencetak skor, mengambil benderanya adalah cara yang bagus untuk menghentikannnya.", + "In hockey, you'll maintain more speed if you turn gradually.": "Dalam hoki, Kamu dapat mempertahankan kecepatanmu jika Kamu berbelok secara bertahap.", + "It's easier to win with a friend or two helping.": "Lebih mudah menang jika dibantu teman.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Lompat sambil melempar bom untuk membuat bom melambung tinggi.", + "Land-mines are a good way to stop speedy enemies.": "Ranjau adalah cara yang bagus untuk menghentikan musuh yang cepat.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Banyak hal yang dapat diambil dan dilempar, termasuk pemain lain. Membuang\nmusuhmu ke jurang akan lebih efektif.", + "No, you can't get up on the ledge. You have to throw bombs.": "Tidak, Kamu tidak dapat melewati pembatas. Kamu harus melempar bom.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Pemain dapat masuk dan keluar ditengah-tengah permainan,\ndan Kamu juga dapat menyambung dan memutuskan kontroller.", + "Practice using your momentum to throw bombs more accurately.": "Latihan menggunakan momentum-mu untuk melempar bom lebih akurat.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Pukulan mendapatkan lebih banyak serangan ketika Kamu\nberlari, melompat, dan berputar.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Lari maju mundur untuk melempar bom\nsangat jauh.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Hancurkan sekumpulan musuh dengan\nmemasang TNT dan meledakkannya dengan bom.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Kepala adalah tempat yang paling rapuh, jadi menempelkannya dengan bom-lengket\nakan membuat musuhmu \"GAME OVER\".", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Level ini tidak akan pernah berakhir, tetapi skor\nakan membuat semua takjub didunia ini.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Kekuatan lemparan tergantung dimana Kamu melempar.\nJika Kamu ingin melempar lebih lembut, jangan terlalu banyak bergerak.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Bosan dengan soundtrack lama? Ganti dengan milikmu sendiri!\nLihat Pengaturan->Suara->Soundtrack", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Cobalah menunggu sumbu bom-mu mati lalu lempar.", + "Try tricking enemies into killing eachother or running off cliffs.": "Cobalah menipu musuhmu dengan membuatnya saling membunuh atau lari ke jurang.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Gunakkan tombol ambil untuk mengambil bendera < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Jalan maju mundur untuk melempar lebih jauh..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Kamu dapat 'mengarahkan' tinjuanmu dengan memutar ke kiri atau ke kanan.\nIni berguna untuk memukul musuh di tepian atau membuat gol di permainan hoki.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Kamu dapat memperkirakan bom yang akan meledak dengan melihat warna percikan bom itu sendiri:\nKuning..Jingga..Merah..DUARRR.", + "You can throw bombs higher if you jump just before throwing.": "Kamu dapat melempar bom lebih tinggi jika kamu melompat sebelum melemparnya.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Kamu akan kesakitan jika kamu menabrakan kepalamu ke benda lain,\nJadi,Jangan tabrakan kepalamu ke benda lain.", + "Your punches do much more damage if you are running or spinning.": "Pukulanmu akan lebih sakit jika kamu lari atau berputar-putar." + } + }, + "trophiesRequiredText": "Ini membutuhkan setidaknya ${NUMBER} piala.", + "trophiesText": "Piala", + "trophiesThisSeasonText": "Piala Musim Ini", + "tutorial": { + "cpuBenchmarkText": "Proses tutorial dengan kecepatan super cepat(CPU test)", + "phrase01Text": "Hai, apa kabar?", + "phrase02Text": "Selamat Datang di ${APP_NAME}", + "phrase03Text": "Ada sedikit tips untuk mengontrol karaktermu:", + "phrase04Text": "Banyak hal di ${APP_NAME} yang didasarkan pada FISIKA", + "phrase05Text": "Misalnya, ketika kamu meninju....", + "phrase06Text": "... kecepatan lenganmu memengaruhi tinjuanmu.", + "phrase07Text": "Lihat? Kamu hanya diam, jadi ${NAME} hanya kegelian", + "phrase08Text": "Sekarang, cobalah lompat sembari berputar supaya lebih cepat.", + "phrase09Text": "Hmm, lebih baik", + "phrase10Text": "Berlari juga membantu", + "phrase11Text": "Tahan tombol APA SAJA untuk berlari.", + "phrase12Text": "Untuk tinjuan super maut, cobalah lari dan berputar.", + "phrase13Text": "Ups, maaf ya ${NAME}!", + "phrase14Text": "Kamu dapat mengangkat lalu melempar barang, bendera, atau mungkin.... ${NAME}", + "phrase15Text": "Yang terakhir, BOM.", + "phrase16Text": "Melempar bom butuh latihan.", + "phrase17Text": "Eh! Lemparanmu sangat buruk!", + "phrase18Text": "Lempar bom sambil lari juga membantu.", + "phrase19Text": "Lompat membuat lemparanmu makin tinggi.", + "phrase20Text": "Lempar sambil berputar untuk lemparan terjauh.", + "phrase21Text": "Mengatur waktu bom dapat jadi sulit.", + "phrase22Text": "Yah, meleset", + "phrase23Text": "Tunggu bom hingga matang, lalu lempar.", + "phrase24Text": "Horee! Kerja Bagus.", + "phrase25Text": "Yap, sepertinya itu saja.", + "phrase26Text": "Hantam mereka, Joe!", + "phrase27Text": "Ingat latihanmu, agar kamu dapat kembali hidup-hidup!", + "phrase28Text": "... em, mungkin sih...", + "phrase29Text": "Semoga beruntung kawan !", + "randomName1Text": "Ucup", + "randomName2Text": "Asep", + "randomName3Text": "Galang", + "randomName4Text": "Suci", + "randomName5Text": "Aldi", + "skipConfirmText": "Yakin ingin melompati pembelajaran? Tekan apa saja untuk konfirmasi.", + "skipVoteCountText": "${COUNT} dari ${TOTAL} suara untuk melewati pembelajaran", + "skippingText": "Melewati pembelajaran...", + "toSkipPressAnythingText": "(tekan apa saja untuk melompati pembelajaran)" + }, + "twoKillText": "PEMBUNUHAN GANDA!", + "unavailableText": "tidak tersedia", + "unconfiguredControllerDetectedText": "Pengontrol belum terkonfigurasi terdeteksi:", + "unlockThisInTheStoreText": "Tersedia di toko terdekat.", + "unlockThisProfilesText": "Untuk membuat lebih dari ${NUM} profil, Kamu memerlukan:", + "unlockThisText": "Untuk buka ini,kamu membutuhkan:", + "unsupportedHardwareText": "Maaf, perangkat ini tidak mendukung build permainan ini.", + "upFirstText": "Game pertama:", + "upNextText": "${COUNT} game berikutnya:", + "updatingAccountText": "Memperbaharui akun...", + "upgradeText": "Tingkatkan", + "upgradeToPlayText": "Buka \"${PRO}\" di toko game untuk bermain.", + "useDefaultText": "Gunakan Default", + "usesExternalControllerText": "Game ini menggunakan pengontrol external untuk input.", + "usingItunesText": "Menggunakan soundtrack dari aplikasi Musik", + "usingItunesTurnRepeatAndShuffleOnText": "Tolong pastikan lagu diacak dan diulang di iTunes. ", + "validatingTestBuildText": "Memvalidasi Bangunan Tes...", + "victoryText": "Menang!", + "voteDelayText": "Kamu tidak dapat memulai pemilihan suara dalam ${NUMBER} detik", + "voteInProgressText": "Pemilihan suara sedang dalam proses.", + "votedAlreadyText": "Kamu sudah memilih suara", + "votesNeededText": "${NUMBER} suara dibutuhkan", + "vsText": "lw.", + "waitingForHostText": "(menunggu ${HOST} untuk lanjut)", + "waitingForPlayersText": "menunggu pemain untuk bergabung...", + "waitingInLineText": "Menunggu antrian (acara penuh)...", + "watchAVideoText": "Tonton Video", + "watchAnAdText": "Tonton Iklan", + "watchWindow": { + "deleteConfirmText": "Hapus \"${REPLAY}\"?", + "deleteReplayButtonText": "Hapus\nReplay", + "myReplaysText": "Replayku", + "noReplaySelectedErrorText": "Tidak Ada Replay Terpilih", + "playbackSpeedText": "Kecepatan Putar Ulang: ${SPEED}", + "renameReplayButtonText": "Ganti\nNama", + "renameReplayText": "Mengubah nama\"${REPLAY}\" ke", + "renameText": "Ganti Nama", + "replayDeleteErrorText": "Galat menghapus replay", + "replayNameText": "Nama Replay", + "replayRenameErrorAlreadyExistsText": "Replay dengan nama yang sudah ada", + "replayRenameErrorInvalidName": "Tidak dapat menganti nama replay; nama invailid", + "replayRenameErrorText": "Error mengganti nama replay", + "sharedReplaysText": "Replay dibagikan", + "titleText": "Nonton", + "watchReplayButtonText": "Lihat\nReplay" + }, + "waveText": "Gelombang", + "wellSureText": "Sip!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Mencari Wiimotes....", + "pressText": "Tekan tombol Wilmote 1 dan 2 secara bersamaan", + "pressText2": "Pada Wiimotes baru dengan Motion Plus built in, tekan tombol merah 'sync' pada belakang sebagai gantinya." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "listenText": "Mendengarkan", + "macInstructionsText": "Pastika Wii-mu mati dan Bluetooth hidup\npada Macmu, lalu tekan 'Dengar'. Dukungan Wiimote\ndapat sedikit sulit, jadi Kamu harus mencoba beberapa kali\nsebelum Kamu mempunyai koneksi.\n\nBluetooth seharusnya dapat mengatasi sampai 7 perangkat yang terhubung\nmeski jaraknya bervariasi.\n\nBombSquad mendukung Wiimotes, Nunchuks,\ndan kontroller klasik.\nWii Remote yang baru sekarang bekerja juga\ntapi tanpa penghubung.", + "thanksText": "Terimakasih kepada DarwiinRemote team\nUntuk dukungan wiimote", + "titleText": "Pengaturan Wiimote" + }, + "winsPlayerText": "${NAME} Menang!", + "winsTeamText": "${NAME} Menang!", + "winsText": "${NAME} Menang!", + "worldScoresUnavailableText": "Skor Dunia tidak tersedia.", + "worldsBestScoresText": "Nilai Terbaik Dunia", + "worldsBestTimesText": "Waktu Terbaik Dunia", + "xbox360ControllersWindow": { + "getDriverText": "Dapatkan Driver", + "macInstructions2Text": "Untuk menggunakan stik nirkabel, Kamu juga memerlukan penerima jaringan nirkabel\natau wireless receiver yang termasuk bagian dari 'Xbox 360 Wireless Controller for Windows'.\nSatu receiver dapat digunakan untuk hingga 4 stik/pengontrol.\n\nPenting: receiver pihak ketiga biasanya tidak dapat digunakan dengan driver ini;\npastikan pada receiver Kamu tertulis logo 'Microsoft', bukan logo 'XBOX360'.\nMicrosoft tidak menjual receiver secara terpisah lagi, jadi Kamu harus\nmembeli stik yang termasuk receiver, atau cari di ebay/kaskus.\n\nJika Kamu berpikir ini berguna, tolong pertimbangkan untuk donasi kepada\npengembang/developer driver tersebut pada situsnya.", + "macInstructionsText": "Untuk menggunakan stik Xbox 360, Kamu perlu menginstall\ndriver untuk Mac, dapat didownload pada link di bawah.\nDapat digunakan untuk stik kabel maupun wireless.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "Untuk menggunakan stik Xbox 360 kabel, tancapkan\nkabel USB ke komputer Kamu. Kamu dapat menggunakan USB hub\nuntuk menancapkan banyak stik/pengontrol\n\nUntuk menggunakan stik wireless Kamu perlu wireless reciever,\ntersedia sebagai bagian dari \"Xbox 360 wireless Controller for Windows\"\ntermasuk ataupun dijual terpisah. Setiap reciever yang ditancapkan dapat\nmenyambunggkan sampai 4 buah stik/pengontrol.", + "titleText": "Menggunakan pengontrol Xbox 360 dengan ${APP_NAME}:" + }, + "yesAllowText": "Ya, Izinkan!", + "yourBestScoresText": "Skor terbaikmu", + "yourBestTimesText": "Waktu Terbaikmu" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/italian.json b/dist/ba_data/data/languages/italian.json new file mode 100644 index 0000000..0894769 --- /dev/null +++ b/dist/ba_data/data/languages/italian.json @@ -0,0 +1,1952 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "I nomi degli account non possono contenere emoji o altri caratteri speciali.", + "accountProfileText": "(profilo account)", + "accountsText": "Account", + "achievementProgressText": "Trofei: ${COUNT} su ${TOTAL}", + "campaignProgressText": "Progresso Campagna [Difficile]: ${PROGRESS}", + "changeOncePerSeason": "Puoi cambiare questa cosa solo una volta a stagione.", + "changeOncePerSeasonError": "Devi aspettare la prossima stagione per apportare delle modifiche (${NUM} days)", + "customName": "Nome Personalizzato", + "deviceSpecificAccountText": "Stai usando un account automaticamente generato: ${NAME}", + "linkAccountsEnterCodeText": "Inserisci il codice", + "linkAccountsGenerateCodeText": "Genera codice", + "linkAccountsInfoText": "(condividi i progressi sui vari dispositivi)", + "linkAccountsInstructionsNewText": "Per collegare due account, genera un codice nel primo dispositivo\ned inserisci il codice nel secondo. I dati del secondo account\nverranno condivisi in entrambi gli account.\n(I dati del primo account andranno persi)\n\nPuoi collegare fino a ${COUNT} account.\n\nIMPORTANTE: collega solo account che ti appartengono;\nSe colleghi account di amici non sarete in grado di\ngiocare online allo stesso momento.", + "linkAccountsInstructionsText": "Per collegare due account, crea un codice su uno\ndei dispositivi e inserisci quel codice negli altri. \nProgressi e inventario verranno combinati.\nPuoi collegare fino a ${COUNT} account.\n\nIMPORTANTE: Collega solo account che possiedi!\nSe colleghi un account con un tuo amico non potrete\ngiocare allo stesso momento!\n \nInoltre: questa operazione non può essere annullata, quindi stai attento!", + "linkAccountsText": "Collega account", + "linkedAccountsText": "Account Connessi:", + "nameChangeConfirm": "Confermi di voler modificare il tuo nome in ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Stai per cancellare i tuoi progressi in\nmodalità cooperativa e i tuoi punteggi\nlocali (ma non i tuoi ticket). L'opera-\nzione è irreversibile: continuare?", + "resetProgressConfirmText": "Stai per cancellare i tuoi progressi in\nmodalità cooperativa, i tuoi punteggi\nlocali e i trofei (ma non i tuoi ticket).\nL'operazione è irreversibile: continuare?", + "resetProgressText": "Cancella Progressi", + "setAccountName": "Inserisci un nome utente", + "setAccountNameDesc": "Seleziona quale nome visualizzare per il tuo account.\nPuoi usare quel nome per uno dei tuoi account collegati,\noppure creare un unico nome personalizzato.", + "signInInfoText": "Accedi per raccogliere biglietti, competere online,\ne condividere i progressi tra i vari dispositivi.", + "signInText": "Accedi", + "signInWithDeviceInfoText": "(Un solo account automatico è disponibile per questo dispositivo)", + "signInWithDeviceText": "Accedi con l'account del dispositivo", + "signInWithGameCircleText": "Accedi con Game Circle", + "signInWithGooglePlayText": "Accedi con Google Play", + "signInWithTestAccountInfoText": "(tipo di account obsoleto; usa gli account dispositivo d'ora in poi)", + "signInWithTestAccountText": "Accedi con un account di prova", + "signOutText": "Esci", + "signingInText": "Accesso in corso...", + "signingOutText": "Uscita in corso...", + "testAccountWarningCardboardText": "Attenzione: stai accedendo con un account di test.\n\nQuesto sarà sostituito con l'account Google quando diventeremo supportati nelle app per cardboard.\n\nPer ora puoi raccogliere tutti i biglietti in-game.\n(Però puoi prendere l'aggiornamento BombSquad Pro gratis)", + "testAccountWarningOculusText": "Attenzione. Stai eseguendo l'accesso con un account \"di prova\".\nQuesto sarà sostituito da un account Oculus l'anno prossimo \ne offrirà acquisti di biglietti e altre opzioni.\n\nAl momento, tutti i biglietti vanno guadagnati nel gioco.\n(Devi, comunque, prendere l'upgrade di BombSquad Pro gratuito)", + "testAccountWarningText": "Attenzione: stai per accedere usando un account \"di prova\".\nQuesto account è legato a questo specifico apparecchio e\npotrebbe essere resettato in alcune occasioni (perciò non\nperderci troppo tempo a raccogliere/sbloccare oggetti).\n\nUsa una versione completa del gioco per usare un profilo\n\"vero\" (Game-Center, Google Plus, ecc.). Questo ti permet-\nterà di salvare i tuoi progressi online e condividerlo fra\npiù apparecchi.", + "ticketsText": "Ticket: ${COUNT}", + "titleText": "Account", + "unlinkAccountsInstructionsText": "Seleziona un account da scollegare", + "unlinkAccountsText": "Scollega Account", + "viaAccount": "(tramite ${NAME})", + "youAreLoggedInAsText": "Accesso effettuato come:", + "youAreSignedInAsText": "Hai effettuato l'accesso come:" + }, + "achievementChallengesText": "Sfide trofeo", + "achievementText": "Trofeo", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Uccidi 3 cattivoni con la TNT", + "descriptionComplete": "Uccisi 3 cattivoni con la TNT", + "descriptionFull": "Uccidi 3 cattivoni con la TNT in ${LEVEL}", + "descriptionFullComplete": "Uccisi 3 cattivoni con la TNT in ${LEVEL}", + "name": "E la dinamite fa Boom!" + }, + "Boxer": { + "description": "Vinci senza lanciare bombe", + "descriptionComplete": "Hai vinto senza lanciare bombe", + "descriptionFull": "Completa ${LEVEL} senza lanciare bombe", + "descriptionFullComplete": "Hai completato ${LEVEL} senza lanciare bombe", + "name": "Pugile" + }, + "Dual Wielding": { + "descriptionFull": "Connetti 2 controller (hardware o app)", + "descriptionFullComplete": "2 controller connessi (hardware o app)", + "name": "Doppio maneggiamento" + }, + "Flawless Victory": { + "description": "Vinci senza essere colpito", + "descriptionComplete": "Hai vinto senza essere colpito", + "descriptionFull": "Vinci ${LEVEL} senza essere colpito", + "descriptionFullComplete": "Hai vinto ${LEVEL} senza essere colpito", + "name": "Vittoria schiacciante" + }, + "Free Loader": { + "descriptionFull": "Avvia una partita Tutti Contro Tutti con 2 o più giocatori", + "descriptionFullComplete": "Hai avviato una partita Tutti Contro Tutti con 2 o più giocatori", + "name": "Caricatore gratuito" + }, + "Gold Miner": { + "description": "Uccidi 6 cattivoni con le mine antiuomo", + "descriptionComplete": "Hai ucciso 6 cattivoni con le mine antiuomo", + "descriptionFull": "Uccidi 6 cattivoni con le mine in ${LEVEL}", + "descriptionFullComplete": "Hai ucciso 6 cattivoni con le mine antiuomo in ${LEVEL}", + "name": "Minatore" + }, + "Got the Moves": { + "description": "Vinci senza tirare pugni né bombe", + "descriptionComplete": "Hai vinto senza tirare pugni né bombe", + "descriptionFull": "Vinci ${LEVEL} senza tirare pugni né bombe", + "descriptionFullComplete": "Hai vinto il livello ${LEVEL} senza tirare pugni né bombe", + "name": "Ci sai fare!" + }, + "In Control": { + "descriptionFull": "Connetti un controller (hardware o app)", + "descriptionFullComplete": "Controller connesso (hardware o app)", + "name": "In controllo" + }, + "Last Stand God": { + "description": "Totalizza 1000 punti", + "descriptionComplete": "Hai totalizzato 1000 punti", + "descriptionFull": "Totalizza 1000 punti in ${LEVEL}", + "descriptionFullComplete": "Hai totalizzato 1000 punti in ${LEVEL}", + "name": "Dio di ${LEVEL}" + }, + "Last Stand Master": { + "description": "Totalizza 250 punti", + "descriptionComplete": "Hai totalizzato 250 punti", + "descriptionFull": "Totalizza 250 punti in ${LEVEL}", + "descriptionFullComplete": "Hai totalizzato 250 punti in ${LEVEL}", + "name": "Maestro di ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Fai 500 punti", + "descriptionComplete": "Hai fatto 500 punti", + "descriptionFull": "Totalizza 500 punti in ${LEVEL}", + "descriptionFullComplete": "Hai totalizzato 500 punti in ${LEVEL}", + "name": "Mago di ${LEVEL}" + }, + "Mine Games": { + "description": "Uccidi 3 cattivoni con le mine antiuomo", + "descriptionComplete": "Hai ucciso 3 cattivoni con le mine antiuomo", + "descriptionFull": "Uccidi 3 cattivoni con le mine antiuomo in ${LEVEL}", + "descriptionFullComplete": "Hai ucciso 3 cattivoni con le mine antiuomo in ${LEVEL}", + "name": "Campo minato" + }, + "Off You Go Then": { + "description": "Scaraventa 3 cattivoni fuori dalla mappa", + "descriptionComplete": "Hai scaraventato 3 cattivoni fuori dalla mappa", + "descriptionFull": "Scaraventa 3 cattivoni fuori dalla mappa in ${LEVEL}", + "descriptionFullComplete": "Hai scaraventato 3 cattivoni fuori dalla mappa in ${LEVEL}", + "name": "Cado dalle nubi" + }, + "Onslaught God": { + "description": "Totalizza 5000 punti", + "descriptionComplete": "Hai totalizzato 5000 punti", + "descriptionFull": "Totalizza 5000 punti in ${LEVEL}", + "descriptionFullComplete": "Hai totalizzato 5000 punti in ${LEVEL}", + "name": "Dio di ${LEVEL}" + }, + "Onslaught Master": { + "description": "Totalizza 500 punti", + "descriptionComplete": "Hai totalizzato 500 punti", + "descriptionFull": "Totalizza 500 punti in ${LEVEL}", + "descriptionFullComplete": "Hai totalizzato 500 punti in ${LEVEL}", + "name": "Maestro di ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Sconfiggi tutte le ondate", + "descriptionComplete": "Hai sconfitto tutte le ondate", + "descriptionFull": "Sconfiggi tutte le ondate in ${LEVEL}", + "descriptionFullComplete": "Hai sconfitto tutte le ondate in ${LEVEL}", + "name": "Vittoria in ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Totalizza 1000 punti", + "descriptionComplete": "Hai totalizzato 1000 punti", + "descriptionFull": "Totalizza 1000 punti in ${LEVEL}", + "descriptionFullComplete": "Hai totalizzato 1000 punti in ${LEVEL}", + "name": "Mago di ${LEVEL}" + }, + "Precision Bombing": { + "description": "Vinci senza usare potenziamenti", + "descriptionComplete": "Hai vinto senza usare potenziamenti", + "descriptionFull": "Vinci ${LEVEL} senza usare potenziamenti", + "descriptionFullComplete": "Hai vinto ${LEVEL} senza usare potenziamenti", + "name": "Bombardamento di precisione" + }, + "Pro Boxer": { + "description": "Vinci senza lanciare bombe", + "descriptionComplete": "Hai vinto senza lanciare bombe", + "descriptionFull": "Completa ${LEVEL} senza lanciare bombe", + "descriptionFullComplete": "Hai completato ${LEVEL} senza lanciare bombe", + "name": "Pugile Esperto" + }, + "Pro Football Shutout": { + "description": "Vinci senza che i cattivoni facciano gol", + "descriptionComplete": "Hai vinto senza che i cattivoni facessero gol", + "descriptionFull": "Vinci ${LEVEL} senza che i cattivoni facciano gol", + "descriptionFullComplete": "Hai vinto ${LEVEL} senza che i cattivoni facessero gol", + "name": "Saracinesca in ${LEVEL}" + }, + "Pro Football Victory": { + "description": "Vinci la partita", + "descriptionComplete": "Hai vinto la partita", + "descriptionFull": "Vinci la partita in ${LEVEL}", + "descriptionFullComplete": "Hai vinto la partita in ${LEVEL}", + "name": "Vittoria in ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Sconfiggi tutte le ondate", + "descriptionComplete": "Hai sconfitto tutte le ondate", + "descriptionFull": "Sconfiggi tutte le ondate di ${LEVEL}", + "descriptionFullComplete": "Hai sconfitto tutte le ondate di ${LEVEL}", + "name": "Vittoria in ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Completa tutte le ondate", + "descriptionComplete": "Hai completato tutte le ondate", + "descriptionFull": "Completa tutte le ondate su ${LEVEL}", + "descriptionFullComplete": "Hai completato tutte le ondate su ${LEVEL}", + "name": "Vittoria in ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Vinci senza che i cattivoni facciano gol", + "descriptionComplete": "Hai vinto senza che i cattivoni facessero gol", + "descriptionFull": "Vinci ${LEVEL} senza che i cattivoni segnino", + "descriptionFullComplete": "Hai vinto ${LEVEL} senza che i cattivoni segnassero", + "name": "Saracinesca in ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Vinci la partita", + "descriptionComplete": "Hai vinto la partita", + "descriptionFull": "Vinci la partita in ${LEVEL}", + "descriptionFullComplete": "Hai vinto la partita in ${LEVEL}", + "name": "Vittoria in ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Sconfiggi tutte le ondate", + "descriptionComplete": "Hai sconfitto tutte le ondate", + "descriptionFull": "Sconfiggi tutte le ondate in ${LEVEL}", + "descriptionFullComplete": "Hai sconfitto tutte le ondate in ${LEVEL}", + "name": "Vittoria in ${LEVEL}" + }, + "Runaround God": { + "description": "Totalizza 2000 punti", + "descriptionComplete": "Hai totalizzato 2000 punti", + "descriptionFull": "Totalizza 2000 punti in ${LEVEL}", + "descriptionFullComplete": "Hai totalizzato 2000 punti in ${LEVEL}", + "name": "Dio di ${LEVEL}" + }, + "Runaround Master": { + "description": "Totalizza 500 punti", + "descriptionComplete": "Hai totalizzato 500 punti", + "descriptionFull": "Totalizza 500 punti in ${LEVEL}", + "descriptionFullComplete": "Hai totalizzato 500 punti in ${LEVEL}", + "name": "Maestro di ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Totalizza 1000 punti", + "descriptionComplete": "Hai totalizzato 1000 punti", + "descriptionFull": "Totalizza 1000 punti in ${LEVEL}", + "descriptionFullComplete": "Hai totalizzato 1000 punti in ${LEVEL}", + "name": "Mago di ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Condividi con successo il gioco con un amico", + "descriptionFullComplete": "Gioco condiviso con successo con un amico", + "name": "Condividere significa tenerci" + }, + "Stayin' Alive": { + "description": "Vinci senza morire", + "descriptionComplete": "Hai vinto senza morire", + "descriptionFull": "Vinci ${LEVEL} senza morire", + "descriptionFullComplete": "Hai vinto ${LEVEL} senza morire", + "name": "Stayin' alive" + }, + "Super Mega Punch": { + "description": "Infliggi una percentuale del 100% di danno con un solo pugno", + "descriptionComplete": "Hai inflitto una percentuale del 100% di danno con un solo pugno", + "descriptionFull": "Infliggi il 100% di danno con un solo pugno in ${LEVEL}", + "descriptionFullComplete": "Hai inflitto il 100% di danno con un solo pugno in ${LEVEL}", + "name": "Super mega pugno" + }, + "Super Punch": { + "description": "Infliggi una percentuale del 50% di danno con un solo pugno", + "descriptionComplete": "Hai inflitto una percentuale del 50% di danno con un solo pugno", + "descriptionFull": "Infliggi il 50% di danno con un solo pugno in ${LEVEL}", + "descriptionFullComplete": "Hai inflitto il 50% di danno con un solo pugno in ${LEVEL}", + "name": "Super Pugno" + }, + "TNT Terror": { + "description": "Uccidi 6 cattivoni con la TNT", + "descriptionComplete": "Hai ucciso 6 cattivoni con la TNT", + "descriptionFull": "Uccidi 6 cattivoni con la TNT su ${LEVEL}", + "descriptionFullComplete": "Hai ucciso 6 cattivoni con la TNT su ${LEVEL}", + "name": "Il dinamitardo" + }, + "Team Player": { + "descriptionFull": "Avvia una partita Squadre con 4+ giocatori", + "descriptionFullComplete": "Avviato una partita Squadre con 4+ giocatori", + "name": "Giocatore di Squadra" + }, + "The Great Wall": { + "description": "Ferma ogni singolo cattivone", + "descriptionComplete": "Hai fermato ogni singolo cattivone", + "descriptionFull": "Ferma ogni singolo cattivone in ${LEVEL}", + "descriptionFullComplete": "Hai fermato ogni singolo cattivone in ${LEVEL}", + "name": "La grande muraglia" + }, + "The Wall": { + "description": "Ferma ogni singolo cattivone", + "descriptionComplete": "Hai fermato ogni singolo cattivone", + "descriptionFull": "Ferma ogni singolo cattivone in ${LEVEL}", + "descriptionFullComplete": "Hai fermato ogni singolo cattivone in ${LEVEL}", + "name": "Il muro" + }, + "Uber Football Shutout": { + "description": "Vinci senza che i cattivoni facciano gol", + "descriptionComplete": "Hai vinto senza che i cattivoni facessero gol", + "descriptionFull": "Vinci ${LEVEL} senza che i cattivoni segnino", + "descriptionFullComplete": "Hai vinto ${LEVEL} senza che i cattivoni segnassero", + "name": "Saracinesca in ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Vinci la partita", + "descriptionComplete": "Hai vinto la partita", + "descriptionFull": "Vini la partita in ${LEVEL}", + "descriptionFullComplete": "Hai vinto la partita in ${LEVEL}", + "name": "Vittoria in ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Sconfiggi tutte le ondate", + "descriptionComplete": "Hai sconfitto tutte le ondate", + "descriptionFull": "Sconfiggi tutte le ondate in ${LEVEL}", + "descriptionFullComplete": "Hai sconfitto tutte le ondate in ${LEVEL}", + "name": "Vittoria in ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Comleta tutte le ondate", + "descriptionComplete": "Hai completato tutte le ondate", + "descriptionFull": "Completa tutte le ondate in ${LEVEL}", + "descriptionFullComplete": "Completa tutte le ondate in ${LEVEL}", + "name": "Vittoria in ${LEVEL}" + } + }, + "achievementsRemainingText": "Trofei rimasti:", + "achievementsText": "Trofei", + "achievementsUnavailableForOldSeasonsText": "Mi dispiace, ma gli obbiettivi non sono disponibili per le stagioni vecchie.", + "addGameWindow": { + "getMoreGamesText": "Ottieni Giochi...", + "titleText": "Aggiunga Partita" + }, + "allowText": "Consenti", + "alreadySignedInText": "Il tuo account è collegato da un altro dispositivo;\ncambia account o chiudi il gioco nel tuo altro\ndispositivo e riprova.", + "apiVersionErrorText": "Impossibile caricare il modulo ${NAME}; sono installate le API versione ${VERSION_USED}; è richiesta la ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" abilitalo solo quando sono connesse delle cuffie)", + "headRelativeVRAudioText": "Audio VR Head-Relative", + "musicVolumeText": "Volume delle musica", + "soundVolumeText": "Volume dei suoni", + "soundtrackButtonText": "Colonna sonora", + "soundtrackDescriptionText": "(scegli la musica da ascoltare durante le partite)", + "titleText": "Audio" + }, + "autoText": "Automatico", + "backText": "Indietro", + "banThisPlayerText": "Banna questo giocatore", + "bestOfFinalText": "Finale Al-meglio-di-${COUNT}", + "bestOfSeriesText": "Serie al meglio di ${COUNT}:", + "bestRankText": "Il tuo miglior piazzamento: N.${RANK}", + "bestRatingText": "La tua valutazione massima è ${RATING}", + "betaErrorText": "Questa versione non è più attiva; per favore, cercane una più recente.", + "betaValidateErrorText": "Impossibile convalidare la beta. (nessuna connessione internet?)", + "betaValidatedText": "Versione beta convalidata; Divertiti!", + "bombBoldText": "BOMBA", + "bombText": "Bomba", + "boostText": "Potenziamento", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} è configurato nell'app stessa.", + "buttonText": "pulsante", + "canWeDebugText": "Vorresti che BombSquad segnalasse automaticamente bug, errori\ne informazioni sull'utilizzo direttamente allo sviluppatore?\n\nQuesti dati non contengono informazioni personali e aiutano a \nmantenere il gioco privo di bug e rallentamenti.", + "cancelText": "Annulla", + "cantConfigureDeviceText": "Spiacente, ${DEVICE} non è configurabile.", + "challengeEndedText": "Questa sfida è finita.", + "chatMuteText": "Silenzia Chat", + "chatMutedText": "Chat Silenziata", + "chatUnMuteText": "Smuta Chat", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Devi completare questo\nlivello per procedere!", + "completionBonusText": "Competizione bonus", + "configControllersWindow": { + "configureControllersText": "Configura Controller", + "configureGamepadsText": "Configura i gamepad", + "configureKeyboard2Text": "Configura la tastiera P2", + "configureKeyboardText": "Configura la tastiera", + "configureMobileText": "Smartphone/tablet come Controller", + "configureTouchText": "Configura il touchscreen", + "ps3Text": "Controller PS3", + "titleText": "Controller", + "wiimotesText": "Wiimotes", + "xbox360Text": "Controller Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Nota: il supporto ai gamepad varia a seconda del dispositivo e la versione di Android.", + "pressAnyButtonText": "Premi un pulsante qualsiasi sul\nController che vuoi configurare...", + "titleText": "Configura i Controller" + }, + "configGamepadWindow": { + "advancedText": "Avanzato", + "advancedTitleText": "Impostazioni Avanzate Controller", + "analogStickDeadZoneDescriptionText": "(aumentalo se il personaggio continua a camminare anche quando non tocchi nulla)", + "analogStickDeadZoneText": "Zona Neutrale Levetta Analogica", + "appliesToAllText": "(si applica a tutti i Controller di questo tipo)", + "autoRecalibrateDescriptionText": "(abilita se il tuo personaggio non si muove alla massima velocità)", + "autoRecalibrateText": "Autocalibra le levette analogiche", + "axisText": "Asse", + "clearText": "reimposta", + "dpadText": "dpad", + "extraStartButtonText": "Tasto Start Extra", + "ifNothingHappensTryAnalogText": "Se non succede nulla, prova invece ad assegnarlo alla levetta analogica", + "ifNothingHappensTryDpadText": "Se non succede nulla, prova invece ad assegnarlo al d-pad.", + "ignoreCompletelyDescriptionText": "(impedisci a questo controller di modificare il gioco o i menu)", + "ignoreCompletelyText": "Ignora Completamente", + "ignoredButton1Text": "Tasto Ignorato 1", + "ignoredButton2Text": "Tasto Ignorato 2", + "ignoredButton3Text": "Tasto Ignorato 3", + "ignoredButton4Text": "Pulsante sconosciuto 4", + "ignoredButtonDescriptionText": "(usa questo per prevenire che i pulsanti 'home' o 'sync' influenzino il gioco)", + "ignoredButtonText": "Pulsante ignorato", + "pressAnyAnalogTriggerText": "Premi un qualunque grilletto analogico...", + "pressAnyButtonOrDpadText": "Premi un qualsiasi pulsante o d-pad...", + "pressAnyButtonText": "Premi un pulsante qualsiasi...", + "pressLeftRightText": "Premi il pulsante destra o sinistra...", + "pressUpDownText": "Premi il pulsante su o giù...", + "runButton1Text": "Pulsante per Correre 1", + "runButton2Text": "Pulsante per Correre 2", + "runTrigger1Text": "Grilletto per Correre 1", + "runTrigger2Text": "Grilletto per Correre 2", + "runTriggerDescriptionText": "(i grilletti analogici ti permettono di variare la velocità di corsa)", + "secondHalfText": "Usa questo per configurare la seconda metà\ndi un dispositivo 2-controller-in-1 che\nviene mostrato come controller singolo.", + "secondaryEnableText": "Abilita", + "secondaryText": "Controller secondario", + "startButtonActivatesDefaultDescriptionText": "(disattivalo se il tuo pulsante Start è più che altro un pulsante \"menu/pausa\")", + "startButtonActivatesDefaultText": "Non usare il pulsante Start/Menu del controller (se presente) per correre", + "titleText": "Configurazione Controller", + "twoInOneSetupText": "Installazione controller 2-in-1", + "uiOnlyDescriptionText": "(non permettere a questo controller di essere usato durante il gioco)", + "uiOnlyText": "Limita l'uso al Menu", + "unassignedButtonsRunText": "Corri tenendo premuto qualsiasi pulsante", + "unsetText": "", + "vrReorientButtonText": "Pulsante Reset VR" + }, + "configKeyboardWindow": { + "configuringText": "Configurando ${DEVICE}", + "keyboard2NoteText": "Nota: la maggior parte delle tastiere può registrare solo un numero\nridotto di pulsanti premuti alla volta, quindi se c'è un secondo\ngiocatore alla tastiera potrebbe risultare meglio farlo giocare\nsu una tastiera a parte. Nota che dovrai assegnare tasti diversi\nai due giocatori anche in quel caso." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Dimensione controlli azioni", + "actionsText": "Azioni", + "buttonsText": "Pulsanti", + "dragControlsText": "< Trascina i comandi in modo da riposizionarli >", + "joystickText": "Joystick", + "movementControlScaleText": "Dimensione controlli movimento", + "movementText": "Movimento", + "resetText": "Reset", + "swipeControlsHiddenText": "Nascondi icone scivolati", + "swipeInfoText": "Ci vuole un po' per abituarsi ai comandi in stile 'Attacco',\nma rendono più facile giocare senza guardare i comandi.", + "swipeText": "Attacco", + "titleText": "Configura il Touchscreen", + "touchControlsScaleText": "Scala dei Comandi Touch" + }, + "configureItNowText": "Configurarlo adesso?", + "configureText": "Configura", + "connectMobileDevicesWindow": { + "amazonText": "Appstore di Amazon", + "appStoreText": "App store", + "bestResultsText": "Le prestazioni del gamepad dipendono dalla tua rete wifi.\nPuoi ridurre il ritardo (lag) avvicinandoti al router,\ncollegandoti tramite cavo di rete (dove possibile)\no disattivando altri dispositivi WiFi che non usi.", + "explanationText": "Per usare uno smartphone o un tablet come controller wireless,\ninstallaci l'app \"${REMOTE_APP_NAME}\". Puoi connettere quanti\ndispositivi vuoi a ${APP_NAME} tramite WiFi, ed è gratis!", + "forAndroidText": "Per Android:", + "forIOSText": "Per iOS:", + "getItForText": "Scarica ${REMOTE_APP_NAME} per iOS dall'AppStore Apple o\nper Android dal GooglePlay Store o dall'Appstore Amazon", + "googlePlayText": "Google Play", + "titleText": "Smartphone/Tablet come Controller:" + }, + "continuePurchaseText": "Proseguire per ${PRICE}?", + "continueText": "Proseguire", + "controlsText": "Comandi", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Questo non viene applicato al punteggio totale", + "activenessInfoText": "Questo moltiplicatore aumenta i giorni che giochi\n e diminuisce quando non giochi", + "activityText": "Attività", + "campaignText": "Campagna", + "challengesInfoText": "Ottieni premi completando mini-giochi.\n\nI premi e la difficoltà aumentano ogni volta\nche una sfida è completata e diminuiscono\nquando queste scadono o vengono abbandonate.", + "challengesText": "Sfide", + "currentBestText": "Attuale Migliore", + "customText": "Personalizzati", + "entryFeeText": "Ingresso", + "forfeitConfirmText": "Abbandonare questa sfida?", + "forfeitNotAllowedYetText": "Questa sfida non puó essere abbandonata.", + "forfeitText": "Abbandona", + "multipliersText": "Moltiplicatori", + "nextChallengeText": "Sfida successiva", + "nextPlayText": "Prossima partita", + "ofTotalTimeText": "su ${TOTAL}", + "playNowText": "Gioca ora", + "pointsText": "Punti", + "powerRankingFinishedSeasonUnrankedText": "(stagione finita senza essere classificato)", + "powerRankingNotInTopText": "(non nella top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} punti", + "powerRankingPointsMultText": "(x ${NUMBER} punti)", + "powerRankingPointsText": "${NUMBER} punti", + "powerRankingPointsToRankedText": "(${CURRENT} di ${REMAINING} punti)", + "powerRankingText": "Punteggi", + "prizesText": "Premi", + "proMultInfoText": "I giocatori con il ${PRO} upgrade\nricevono il ${PERCENT}% di punti in più.", + "seeMoreText": "Altri...", + "skipWaitText": "Salta attesa", + "timeRemainingText": "Tempo Rimanente", + "titleText": "Cooperativi", + "toRankedText": "Al prossimo livello", + "totalText": "Totale", + "tournamentInfoText": "Competi per punteggi alti con\naltri giocatori della tua lega.\n\nI premi sono dati ai giocatori con\ni punteggi più alti quando il tempo del torneo scade.", + "welcome1Text": "Benvenuto in ${LEAGUE}. Puoi migliorare il tuo\n livello lega guadagnando stelle, completando\n obbiettivi, e vincendo i trofei.", + "welcome2Text": "Puoi anche guadagnare biglietti con molte attività simili.\nI biglietti possono essere usato per sbloccare nuovi capitoli, mappe e\nmini-giochi, per entrare nei tornei ed altro.", + "yourPowerRankingText": "La tua posizione assoluta" + }, + "copyOfText": "${NAME} - Copia", + "copyText": "Copia", + "copyrightText": "© 2013 Eric Froemling", + "createAPlayerProfileText": "Creare un profilo giocatore?", + "createEditPlayerText": "", + "createText": "Crea", + "creditsWindow": { + "additionalAudioArtIdeasText": "Effetti sonori addizionali, disegni iniziali e idee di ${NAME}", + "additionalMusicFromText": "Musiche extra: ${NAME}", + "allMyFamilyText": "Tutti i miei amici e familiari che hanno aiutato a testare il gioco", + "codingGraphicsAudioText": "Programmazione, Grafica e Audio di ${NAME}", + "languageTranslationsText": "Traduzioni:", + "legalText": "Note legali:", + "publicDomainMusicViaText": "Musica di pubblico dominio via ${NAME}", + "softwareBasedOnText": "Questo software è basato in parte sul lavoro di ${NAME}", + "songCreditText": "${TITLE} Eseguita da ${PERFORMER}\nComposta da ${COMPOSER}, arrangiata da ${ARRANGER}, pubblicata da ${PUBLISHER},\nPer concessione di ${SOURCE}", + "soundAndMusicText": "Effetti sonori & Musica:", + "soundsText": "Effetti sonori (${SOURCE}):", + "specialThanksText": "Ringraziamenti Speciali:", + "thanksEspeciallyToText": "Grazie specialmente a ${NAME}", + "titleText": "Riconoscimenti per ${APP_NAME}", + "whoeverInventedCoffeeText": "Chiunque abbia inventato il caffè" + }, + "currentStandingText": "La tua attuale posizione è #${RANK}", + "customizeText": "Personalizza...", + "deathsTallyText": "${COUNT} morti", + "deathsText": "Morti", + "debugText": "Debug", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Nota: si raccomanda di impostare le texture 'Alte' in Impostazioni->Grafiche->Texture per questa prova.", + "runCPUBenchmarkText": "Esegui Benchmark CPU", + "runGPUBenchmarkText": "Esegui Benchmark GPU", + "runMediaReloadBenchmarkText": "Esegui Benchmark Media-Reload", + "runStressTestText": "Testa il gioco", + "stressTestPlayerCountText": "Numero Giocatori", + "stressTestPlaylistDescriptionText": "Stress Test Playlist", + "stressTestPlaylistNameText": "Nome Playlist", + "stressTestPlaylistTypeText": "Tipo Playlist", + "stressTestRoundDurationText": "Durata Round", + "stressTestTitleText": "Prova di Carico", + "titleText": "Benchmark e Stress Test", + "totalReloadTimeText": "Tempo totale di ricarica: ${TIME} (vedi log per i dettagli)", + "unlockCoopText": "Sblocca i livelli cooperativi" + }, + "defaultFreeForAllGameListNameText": "Partite Tutti Contro Tutti predefinite", + "defaultGameListNameText": "Scaletta ${PLAYMODE} predefinita", + "defaultNewFreeForAllGameListNameText": "Le mie partite Tutti Contro Tutti", + "defaultNewGameListNameText": "Le mie scalette ${PLAYMODE}", + "defaultNewTeamGameListNameText": "I miei giochi a squadre", + "defaultTeamGameListNameText": "Giochi a squadre predefiniti", + "deleteText": "Cancella", + "demoText": "Demo", + "denyText": "Nega", + "desktopResText": "Risoluzione Nativa", + "difficultyEasyText": "Facile", + "difficultyHardOnlyText": "Solo Modalità Difficile", + "difficultyHardText": "Difficile", + "difficultyHardUnlockOnlyText": "Questo livello può essere sbloccato solo in modalità difficile.\nPensi di avere la stoffa per farlo?!", + "directBrowserToURLText": "Visita quest'indirizzo sul tuo computer:", + "disableRemoteAppConnectionsText": "Disattiva connessione remota all'app", + "disableXInputDescriptionText": "Permette l'uso di più di 4 pulsantiere, ma potrebbe anche non funzionare.", + "disableXInputText": "Disabilita XInput", + "doneText": "Fatto", + "drawText": "Pareggio", + "duplicateText": "Duplicato", + "editGameListWindow": { + "addGameText": "Aggiungi\nGioco", + "cantOverwriteDefaultText": "Non puoi sovrascrivere la scaletta predefinita!", + "cantSaveAlreadyExistsText": "Una scaletta con quel nome esiste già!", + "cantSaveEmptyListText": "Non puoi salvare una scaletta vuota!", + "editGameText": "Modifica\nGioco", + "gameListText": "Serie di Partite", + "listNameText": "Nome Scaletta", + "nameText": "Nome", + "removeGameText": "Rimuovi\nGioco", + "saveText": "Salva la serie", + "titleText": "Editor della Scaletta" + }, + "editProfileWindow": { + "accountProfileInfoText": "Questo è uno speciale profilo giocatore con un\nnome e icona basati sul tuo account attualmente connesso.\n\n${ICONS}\n\nCrea profili personalizzati se vuoi usare\nnomi o icone differenti.", + "accountProfileText": "(profilo account)", + "availableText": "Il nome \"${NAME}\" è disponibile.", + "changesNotAffectText": "Nota: i cambiamenti non influiranno sui personaggi già in gioco", + "characterText": "personaggio", + "checkingAvailabilityText": "Controllo disponibilità per \"${NAME}\"...", + "colorText": "colore", + "getMoreCharactersText": "Ottieni altri personaggi...", + "getMoreIconsText": "Ottieni più icone...", + "globalProfileInfoText": "I profili globali del giocatore garantiscono di avere nomi\nunici in tutto il mondo. Includono anche delle icone personalizzate.", + "globalProfileText": "(profilo globale)", + "highlightText": "colore principale", + "iconText": "icona", + "localProfileInfoText": "I profili giocatori locali non hanno icone e i loro nomi\nnon sono garantiti che siano unici. Aggiorna a un profilo\nglobale per riservare un nome unico e per aggiungere un'icona personalizzata", + "localProfileText": "(profilo locale)", + "nameDescriptionText": "Nome del giocatore", + "nameText": "Nome", + "randomText": "casuale", + "titleEditText": "Modifica Profilo", + "titleNewText": "Nuovo Profilo", + "unavailableText": "\"${NAME}\" non è disponibile; prova un altro nome.", + "upgradeProfileInfoText": "Questo riserverà il tuo nome in tutto il mondo\nè ti permetterà di assegnare un'icona personalizzata ad esso.", + "upgradeToGlobalProfileText": "Aggiorna al Profilo Globale" + }, + "editProfilesAnyTimeText": "(puoi creare profili in qualiasi momento sotto la voce 'impostazioni')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Impossibile cancellare la colonna sonora predefinita.", + "cantEditDefaultText": "Impossibile modificare la colonna sonora predefinita. Duplicala o creane una nuova.", + "cantEditWhileConnectedOrInReplayText": "Impossibile modificare la colonna sonora mentre connesso ad un gruppo o in un replay. ", + "cantOverwriteDefaultText": "Impossibile sovrascrivere la colona sonora predefinita", + "cantSaveAlreadyExistsText": "Esiste già una colonna sonora con quel nome!", + "copyText": "Copia", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Colonna sonora predefinita", + "deleteConfirmText": "Cancellare la colonna sonora:\n\n'${NAME}'?", + "deleteText": "Elimina", + "duplicateText": "Duplica", + "editSoundtrackText": "Editor Colonna Sonora", + "editText": "Modifica", + "fetchingITunesText": "Recupero playlist musicale dall'app", + "musicVolumeZeroWarning": "Attenzione: il volume della musica è impostato a 0", + "nameText": "Nome", + "newSoundtrackNameText": "La mia Colonna Sonora ${COUNT}", + "newSoundtrackText": "Nuova colonna sonora:", + "newText": "Nuova", + "selectAPlaylistText": "Seleziona una playlist", + "selectASourceText": "Fonte Musica", + "soundtrackText": "Colonna sonora", + "testText": "test", + "titleText": "Colonne Sonore", + "useDefaultGameMusicText": "Musica Predefinita", + "useITunesPlaylistText": "Playlist di colonne sonore da app musicale", + "useMusicFileText": "File Musicale (mp3, ecc)", + "useMusicFolderText": "Cartella di Musica" + }, + "editText": "Modifica", + "endText": "Fine", + "enjoyText": "Buon Divertimento!", + "epicDescriptionFilterText": "${DESCRIPTION} a rallentatore leggendario.", + "epicNameFilterText": "${NAME} Leggendario", + "errorAccessDeniedText": "accesso negato", + "errorOutOfDiskSpaceText": "spazio su disco esaurito", + "errorText": "Errore", + "errorUnknownText": "errore sconosciuto", + "exitGameText": "Uscire da ${APP_NAME}?", + "exportSuccessText": "'${NAME}' esportato", + "externalStorageText": "Memoria Esterna", + "failText": "Perso", + "fatalErrorText": "Ops! qualcosa non va.\nPer favore provate a reinstallare l'app o\ncontattate ${EMAIL} per ricevere aiuto.", + "fileSelectorWindow": { + "titleFileFolderText": "Scegli un File o Cartella", + "titleFileText": "Scegli un File", + "titleFolderText": "Scegli una Cartella", + "useThisFolderButtonText": "Usa Questa Cartella" + }, + "filterText": "Filtro", + "finalScoreText": "Punteggio finale", + "finalScoresText": "Punteggi finali", + "finalTimeText": "Tempo finale", + "finishingInstallText": "Completamento installazione; un istante...", + "fireTVRemoteWarningText": "* Per una migliore esperienza,\nusa dei Controller o installa\nl'app '${REMOTE_APP_NAME}' sui\ntuoi telefoni o tablet.", + "firstToFinalText": "Finale First-to-${COUNT}", + "firstToSeriesText": "Serie First-to-${COUNT}", + "fiveKillText": "CINQUE UCCISIONI!!!", + "flawlessWaveText": "Ondata schiacciante", + "fourKillText": "QUATTRO UCCISIONI!!!", + "freeForAllText": "Tutti Contro Tutti", + "friendScoresUnavailableText": "Punteggi degli amici non disponibili.", + "gameCenterText": "Game Center", + "gameCircleText": "GameCircle", + "gameLeadersText": "Leader della partita ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "Non puoi eliminare la scaletta predefinita!", + "cantEditDefaultText": "Non puoi modificare la scaletta predefinita! Duplicala o creane una nuova.", + "cantShareDefaultText": "Non puoi condividere la scaletta predefinita", + "deleteConfirmText": "Eliminare \"${LIST}\"?", + "deleteText": "Elimina", + "duplicateText": "Duplica\nScaletta", + "editText": "Modifica\nScaletta", + "gameListText": "Lista delle serie di partite", + "newText": "Nuova\nScaletta", + "showTutorialText": "Mostra Guida", + "shuffleGameOrderText": "Ordine Partite Casuale", + "titleText": "Personalizza Scalette ${TYPE}" + }, + "gameSettingsWindow": { + "addGameText": "Aggiungi Partita" + }, + "gamepadDetectedText": "Un gamepad rilevato.", + "gamepadsDetectedText": "${COUNT} gamepad rilevati.", + "gamesToText": "${WINCOUNT} - ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Ricorda: qualunque dispositivo in un gruppo può ospitare \npiù di un giocatore se si hanno abbastanza controller.", + "aboutDescriptionText": "Usa queste schede per creare un gruppo-\n\nI gruppi ti permettono di partecipare a giochi e\ntornei con i tuoi amici usando diversi dispositivi.\n\nUsa il tasto ${PARTY} in alto a destra per chattare\ne interagire con il tuo gruppo.\n(su un controller, premi ${BUTTON} in un menu)", + "aboutText": "Informazioni", + "addressFetchErrorText": "", + "appInviteInfoText": "Invita i tuoi amici a provare BonvSquad e loro \nriceveranno ${COUNT} biglietti gratis. Tu guadagnerai\n${YOU_COUNT} per ogni amico che lo farà.", + "appInviteMessageText": "${NAME} ti ha mandato ${COUNT} biglietti per ${APP_NAME}", + "appInviteSendACodeText": "Inviagli un codice", + "appInviteTitleText": "Invito per l'app di ${APP_NAME}", + "bluetoothAndroidSupportText": "(funziona su dispositivi Android dotati di Bluetooth)", + "bluetoothDescriptionText": "Ospita/unisciti a un gruppo via Bluetooth:", + "bluetoothHostText": "Ospita via Bluetooth", + "bluetoothJoinText": "Unisciti via Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "controllo in corso...", + "copyCodeConfirmText": "Codice copiato negli appunti!", + "copyCodeText": "Copia il codice", + "dedicatedServerInfoText": "Per risultati migliori, crea un server dedicato. Vedi bombsquadgame.com/server per scoprire come.", + "disconnectClientsText": "Questo disconnetterà ${COUNT} giocatore/i\nnel tuo gruppo. Sei sicuro?", + "earnTicketsForRecommendingAmountText": "I tuoi amici riceveranno ${COUNT} biglietti se proveranno il gioco\n(anche tu riceverai ${YOU_COUNT} biglietti per ogni amico che lo farà)", + "earnTicketsForRecommendingText": "Condividi il gioco\nper biglietti gratis...", + "emailItText": "Manda Email", + "favoritesSaveText": "enregistrer comme favori", + "favoritesText": "Favoris", + "freeCloudServerAvailableMinutesText": "Il prossimo server gratuito sarà disponibile tra ${MINUTES} minuti!", + "freeCloudServerAvailableNowText": "Server gratuito disponibile!", + "freeCloudServerNotAvailableText": "Nessun server gratuito disponibile", + "friendHasSentPromoCodeText": "${COUNT} biglietti di ${APP_NAME} da ${NAME}", + "friendPromoCodeAwardText": "Riceverai ${COUNT} biglietti ogni volta che viene usato.", + "friendPromoCodeExpireText": "Questo codice scadrà in ${EXPIRE_HOURS} ore e funziona soltanto per nuovi giocatori.", + "friendPromoCodeInstructionsText": "Per usarlo, apri ${APP_NAME} e vai su \"Impostazioni->Avanzate->Inserisci Codice\".\nVisita bombsquadgame.com per i link di download per tutte le versioni supportate.", + "friendPromoCodeRedeemLongText": "Puó essere utilizzato per ${COUNT} biglietti gratis per un massimo di ${MAX_USES} persone.", + "friendPromoCodeRedeemShortText": "Puó essere utilizzato per ${COUNT} biglietti nel gioco.", + "friendPromoCodeWhereToEnterText": "(in \"Impostazioni->Avanzate->Inserisci Codice\")", + "getFriendInviteCodeText": "Ottieni un Codice di Invito", + "googlePlayDescriptionText": "Invita giocatori Google Play nel tuo gruppo:", + "googlePlayInviteText": "Invita", + "googlePlayReInviteText": "Ci sono ${COUNT} giocatori di Google Play nel tuo gruppo\nche saranno disconnessi se mandi un nuovo invito.\nIncludili nel nuovo invito per farli tornare.", + "googlePlaySeeInvitesText": "Visualizza inviti", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(solo nella versione Android / Google Play)", + "hostPublicPartyDescriptionText": "Ospita un Party pubblico:", + "hostingUnavailableText": "Hosting non disponibile", + "inDevelopmentWarningText": "Note:\n\nIl gioco in rete è una funzione ancora in fase di sviluppo. \nPer adesso, è altamente raccomandato che tutti\ni giocatori siano sulla stessa rete WiFi.", + "internetText": "Internet", + "inviteAFriendText": "I tuoi amici non hanno il gioco? Invitati a\nprovarlo e riceveranno ${COUNT} biglietti gratis.", + "inviteFriendsText": "Invita Amici", + "joinPublicPartyDescriptionText": "Unisciti a un party pubblico", + "localNetworkDescriptionText": "Unisciti ad un Party vicino (LAN, Bluetooth, etc.)", + "localNetworkText": "Rete locale", + "makePartyPrivateText": "Rendi privato il mio gruppo", + "makePartyPublicText": "Rendi pubblico il mio gruppo", + "manualAddressText": "Indirizzi", + "manualConnectText": "Connettiti", + "manualDescriptionText": "Unisciti a un gruppo all'indirizzo:", + "manualJoinSectionText": "se joindre à la direction", + "manualJoinableFromInternetText": "Sei raggiungibile da internet?", + "manualJoinableNoWithAsteriskText": "NO*", + "manualJoinableYesText": "SÌ", + "manualRouterForwardingText": "*come rimedio, prova a far inoltrare al router la porta UDP ${PORT} verso l'indirizzo locale", + "manualText": "Manuale", + "manualYourAddressFromInternetText": "Il tuo indirizzo da internet:", + "manualYourLocalAddressText": "Indirizzo locale:", + "nearbyText": "Proche", + "noConnectionText": "", + "otherVersionsText": "(altre versioni)", + "partyCodeText": "Code du parti", + "partyInviteAcceptText": "Accetta", + "partyInviteDeclineText": "Rifiuta", + "partyInviteGooglePlayExtraText": "(vedi la scheda 'Google Play' nella finestra 'Raduna')", + "partyInviteIgnoreText": "Ignora", + "partyInviteText": "${NAME} ti ha invitato\nad unirsi al suo gruppo!", + "partyNameText": "Nome gruppo", + "partyServerRunningText": "votre serveur de groupe est en cours d'exécution", + "partySizeText": "dimensione gruppo", + "partyStatusCheckingText": "controllo status in corso...", + "partyStatusJoinableText": "il tuo gruppo è ora raggiungibile da internet", + "partyStatusNoConnectionText": "impossibile connettersi al server", + "partyStatusNotJoinableText": "il tuo gruppo non è raggiungibile da internet", + "partyStatusNotPublicText": "il tuo gruppo non è pubblico", + "pingText": "ping", + "portText": "Porta", + "privatePartyCloudDescriptionText": "les parties privées s'exécutent sur des serveurs cloud dédiés; aucune configuration de routeur requise", + "privatePartyHostText": "Ospita un Party privato", + "privatePartyJoinText": "Entra in un Party privato", + "privateText": "Privé", + "publicHostRouterConfigText": "Questa opzione richiede la configurazione di port-forwarding sul tuo router. Per comodità, utilizza la funzionalità di hosting su server", + "publicText": "Pubblico", + "requestingAPromoCodeText": "Richiesta codice in corso...", + "sendDirectInvitesText": "Invia Inviti Diretti", + "shareThisCodeWithFriendsText": "Condividi questo codice con i tuoi amici:", + "showMyAddressText": "Mostra il mio IP", + "startHostingPaidText": "Hosting disponibile per ${COST}", + "startHostingText": "Ospita il party!", + "startStopHostingMinutesText": "Puoi avviare o terminare il tuo server gratuitamente entro ${MINUTES} minuti", + "stopHostingText": "Termina l'Hosting", + "titleText": "Raduna", + "wifiDirectDescriptionBottomText": "Se tutti i dispositivi hanno un pannello 'WiFi direct', dovrebbero poterlo usare\nper trovarsi e connettersi l'un l'altro. Una volta connessi, potrai formare gruppi\nqui utilizzando la scheda \"Rete Locale\", come fosse una rete WiFi.\n\nPer risultati migliori, l'host WiFi Direct dovrebbe essere anche l'host ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct può essere usato per connettere dispositivi Android direttamente\nsenza l'uso di una rete Wi-Fi. Funziona meglio con Android 4.2 o superiore.\n\nPer usarlo, apri le impostazioni Wi-Fi e cerca la voce 'Wi-Fi Direct' nel menu.", + "wifiDirectOpenWiFiSettingsText": "Apri Impostazioni Wi-Fi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(funziona fra tutte le piattaforme)", + "worksWithGooglePlayDevicesText": "(funziona con dispositivi che usano la versione Google Play (Android) del gioco)", + "youHaveBeenSentAPromoCodeText": "Ti è stato spedito un codice promozionale di ${APP_NAME}:" + }, + "getCoinsWindow": { + "coinDoublerText": "Raddoppia Monete", + "coinsText": "${COUNT} Monete", + "freeCoinsText": "Monete Gratuite", + "restorePurchasesText": "Ripristina Acquisti", + "titleText": "Ottieni Monete" + }, + "getTicketsWindow": { + "freeText": "GRATIS!", + "freeTicketsText": "Biglietti Gratuiti", + "inProgressText": "Una transazione è in corso; prova di nuovo in un secondo momento.", + "purchasesRestoredText": "Acquisti ripristinati.", + "receivedTicketsText": "Ricevuti ${COUNT} biglietti!", + "restorePurchasesText": "Ripristina Acquisti", + "ticketDoublerText": "Raddoppia Biglietti", + "ticketPack1Text": "Pacchetto Biglietti Piccolo", + "ticketPack2Text": "Pacchetto Biglietti Medio", + "ticketPack3Text": "Pacchetto Biglietti Grande", + "ticketPack4Text": "Pacchetto Biglietti Jumbo", + "ticketPack5Text": "Pacchetto Biglietti Mammuth", + "ticketPack6Text": "Pacchetto Biglietti Ultimate", + "ticketsFromASponsorText": "Ottieni ${COUNT} biglietti\nda uno sponsor", + "ticketsText": "${COUNT} Biglietti", + "titleText": "Ottieni Biglietti", + "unavailableLinkAccountText": "Scusa, gli acquisti non sono disponibili su questa piattaforma.\nCome soluzione, puoi collegare questo account ad un'altra \npiattaforma e fare l'acquisto lì.", + "unavailableTemporarilyText": "Non disponibile al momento: riprova più tardi.", + "unavailableText": "Spiacenti, non disponibile.", + "versionTooOldText": "Mi dispiace, questa versione del gioco è troppo vecchia; ti preghiamo di aggiornare.", + "youHaveShortText": "Hai ${COUNT}", + "youHaveText": "Hai ${COUNT} biglietti" + }, + "googleMultiplayerDiscontinuedText": "Mi dispiace, il servizio multiplayer di Google non è più disponibile.\nSto lavorando per sostituirlo il più velocemente possibile.\nFino a quando non troverò una soluzione, prova un altro metodo per connetterti.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Sempre", + "autoText": "Automatico", + "fullScreenCmdText": "Schermo intero (Cmd-F)", + "fullScreenCtrlText": "Schermo intero (Ctrl-F)", + "gammaText": "Gamma", + "highText": "Alto", + "higherText": "Più alto", + "lowText": "Basso", + "mediumText": "Medio", + "neverText": "Mai", + "resolutionText": "Risoluzione", + "showFPSText": "Mostra FPS", + "texturesText": "Textures", + "titleText": "Grafiche", + "tvBorderText": "Bordo della TV", + "verticalSyncText": "Sincronizzazione verticale", + "visualsText": "Visuali" + }, + "helpWindow": { + "bombInfoText": "- Bomba -\nPiù forte dei pugni, ma \npuò risultare autolesionistica.\nPer un risultato migliore, lanciala\nin direzione del nemico prima\nche la miccia si esaurisca.", + "canHelpText": "${APP_NAME} può aiutarti.", + "controllersInfoText": "Puoi giocare a ${APP_NAME} con gli amici attraverso la rete,\no mediante lo stesso dispositivo se hai abbastanza controller.\n${APP_NAME} ne supporta diversi; puoi usare perfino smartphones\ncome controller grazie all'app gratuita '${REMOTE_APP_NAME}'.\nVedi Impostazioni->Controller per ulteriori informazioni.", + "controllersInfoTextFantasia": "Puoi usare il telecomando per giocare, ma suggerisco\nvivamente di usare i gamepad. Puoi anche usare i tuoi cellulari e\ntablet come controller usando l'app gratuita \"BombSquad Remote\".\nVai su \"Impostazioni\" e poi \"Controller\" per più informazioni.", + "controllersInfoTextMac": "Uno o due giocatori possono usare la tastiera, ma BombSquad dà il meglio usando i Gamepad.\nPuoi controllare i personaggi usando Gamepad USB, controller PS3 o Xbox 360, Wiimote e \ndispositivi iOS/Android. Si spera che tu abbia alcuni di questi sotto mano. \nPer ulteriori informazioni vai su Impostazioni > Controller", + "controllersInfoTextOuya": "Con BombSquad puoi utilizzare controller OUYA, PS3 e Xbox 360, e tanti\naltri Gamepad USB e Bluetooth. Puoi anche utilizzare dispositivi iOS e\nAndroid come controller tramite l'app gratuita 'BombSquad Remote'.\nPer ulteriori informazioni vai su Impostazioni > Controller.", + "controllersText": "Controller", + "controlsSubtitleText": "Il tuo amichevole personaggio di ${APP_NAME} ha poche azioni di base:", + "controlsText": "Comandi", + "devicesInfoText": "Si può giocare online con la versione VR di ${APP_NAME} con la versione\ncompleta, perciò tirate fuori i telefonini, tablet, e computer e comin-\nciate a giocare. Può essere anche utile connettere una versione completa\ndel gioco con una VR solo per permettere alla gente fuori di seguire \nl'azione.", + "devicesText": "Dispositivi", + "friendsGoodText": "È sempre meglio averne. ${APP_NAME} è molto più divertente con tanti giocatori,\nfino ad un massimo di 8 per volta, che ci porta a poter usare:", + "friendsText": "Amici", + "jumpInfoText": "- Salto -\nSalta per aggirare piccoli ostacoli,\nlanciare oggetti più in alto, e\nesprimere la tua gioia.", + "orPunchingSomethingText": "O per prenderla a pugni, buttarla giù da un precipizio e farla saltare giù con una bomba appiccicosa..", + "pickUpInfoText": "- Raccogliere -\nRaccogli bandiere, nemici, o qualsiasi\naltra cosa non fissata sul terreno.\nPremi di nuovo per lanciare.", + "powerupBombDescriptionText": "Permette di lanciare tre bombe\ndi seguito anziché una sola.", + "powerupBombNameText": "Tripla bomba", + "powerupCurseDescriptionText": "Non credo che tu voglia prenderle.\n...o forse sì?", + "powerupCurseNameText": "Maledizione", + "powerupHealthDescriptionText": "Rigenera completamente la tua salute.\nNon l'avresti mai detto, eh?", + "powerupHealthNameText": "Pronto soccorso", + "powerupIceBombsDescriptionText": "Più deboli rispetto alle bombe normali\nma congelano i tuoi nemici\ne li rendono particolarmente fragili.", + "powerupIceBombsNameText": "Bombe ghiaccio", + "powerupImpactBombsDescriptionText": "Leggermente più deboli rispetto alle bombe\nnormali, ma esplodono all'impatto.", + "powerupImpactBombsNameText": "Bombe ad impatto", + "powerupLandMinesDescriptionText": "Sono disponibili in pratiche confezioni da 3;\nUtili per piazzare una difesa di base o\nper fermare nemici rapidi.", + "powerupLandMinesNameText": "Mine antiuomo", + "powerupPunchDescriptionText": "Rendono i tuoi pugni più tosti,\nveloci, migliori e forti.", + "powerupPunchNameText": "Guantoni da boxe", + "powerupShieldDescriptionText": "Assorbono un po' di danno\nal posto tuo.", + "powerupShieldNameText": "Scudo energetico", + "powerupStickyBombsDescriptionText": "Si appiccicano a qualunque cosa colpiscono.\nNe consegue una massiccia dose di ilarità.", + "powerupStickyBombsNameText": "Bombe caccola", + "powerupsSubtitleText": "Ovviamente, nessun gioco è davvero completo se non vi sono potenziamenti:", + "powerupsText": "Potenziamenti", + "punchInfoText": "- Pugno -\nI pugni infliggono molti\npiù danni quanto più velocemente ti muovi,\nquindi corri e scazzotta come un pazzo.", + "runInfoText": "- Corsa -\nTieni premuto un QUALSIASI tasto. Pulsanti dorsali e grilletti funzionano bene se ne hai.\nCorrere ti permette di arrivare prima ma rende difficile girare, perciò attento ai precipizi.", + "someDaysText": "Certi giorni vorresti prendere tutto a pugni. O far esplodere qualcosa.", + "titleText": "Manuale di ${APP_NAME}", + "toGetTheMostText": "Per divertirti al meglio in questo gioco avrai bisogno di:", + "welcomeText": "Benvenuti in ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} sta navigando fra i menu come un vero lupo di mare.", + "importPlaylistCodeInstructionsText": "Usa il codice seguente per importare questa scaletta da un'altra parte:", + "importPlaylistSuccessText": "Importata ${TYPE} la playlist '${NAME}'", + "importText": "Importa", + "importingText": "Importando...", + "inGameClippedNameText": "Nel gioco ci sarà\n\"${NAME}\"", + "installDiskSpaceErrorText": "ERRORE: Impossibile completare l'installazione. \nPotresti aver esaurito lo spazio sul tuo dispositivo. \nLibera un po' di spazio e prova di nuovo.", + "internal": { + "arrowsToExitListText": "Premi ${LEFT} o ${RIGHT} per uscire dalla serie", + "buttonText": "pulsante", + "cantKickHostError": "Non è possibile cacciare l'host.", + "chatBlockedText": "${NAME} è escluso dalla chat per ${TIME} secondi.", + "connectedToGameText": "Unito alla partita '${NAME}'", + "connectedToPartyText": "Unito al gruppo di ${NAME}!", + "connectingToPartyText": "Connessione in corso...", + "connectionFailedHostAlreadyInPartyText": "Connessione fallita; l'host è in un altro gruppo.", + "connectionFailedPartyFullText": "Connessione fallita; il gruppo è pieno.", + "connectionFailedText": "Connessione fallita.", + "connectionFailedVersionMismatchText": "Connessione fallita; l'host usa una versione diversa del gioco.\nAssicuratevi che BombSquad sia aggiornato e riprovate.", + "connectionRejectedText": "Connessione rifiutata.", + "controllerConnectedText": "${CONTROLLER} connesso.", + "controllerDetectedText": "Trovato 1 controller.", + "controllerDisconnectedText": "${CONTROLLER} disconnesso.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} si è disconnesso. Prova a riconnetterlo.", + "controllerForMenusOnlyText": "Questo controller non può essere usato per giocare; solo per la navigazione tra i menu.", + "controllerReconnectedText": "${CONTROLLER} riconnesso.", + "controllersConnectedText": "Trovati ${COUNT} controller.", + "controllersDetectedText": "Trovati ${COUNT} controller.", + "controllersDisconnectedText": "${COUNT} controller disconnessi.", + "corruptFileText": "Ci sono alcuni file corrotti. Si prega di reinstallare il gioco, o di inviare un email ${EMAIL}", + "errorPlayingMusicText": "Errore nel riprodurre la musica: ${MUSIC}", + "errorResettingAchievementsText": "Impossibile ripristinare i trofei online: riprova più tardi.", + "hasMenuControlText": "${NAME} ha il comando del menù.", + "incompatibleNewerVersionHostText": "L'host già giocando una versione più nuova del gioco\nAggiorna il gioco alla versione più recente e riprova", + "incompatibleVersionHostText": "L'host sta usando una versione diversa del gioco.\nAssicuratevi che BombSquad sia aggiornato e riprovate.", + "incompatibleVersionPlayerText": "${NAME} sta usando una versione diversa del gioco.\nAssicuratevi che BombSquad sia aggiornato e riprovate.", + "invalidAddressErrorText": "Errore: indirizzo non valido.", + "invalidNameErrorText": "Errore: nome non valido.", + "invalidPortErrorText": "Errore: porta non valida.", + "invitationSentText": "Invito spedito.", + "invitationsSentText": "${COUNT} inviti spediti.", + "joinedPartyInstructionsText": "Qualcuno si è unito al tuo gruppo.\nVai a 'Gioca' per cominciare una partita.", + "keyboardText": "Tastiera", + "kickIdlePlayersKickedText": "${NAME} cacciato per inattività.", + "kickIdlePlayersWarning1Text": "${NAME} sarà cacciato in ${COUNT} secondi se rimarrà inattivo.", + "kickIdlePlayersWarning2Text": "(puoi disattivarlo in Impostazioni -> Avanzate)", + "leftGameText": "Hai abbandonato '${NAME}'.", + "leftPartyText": "Gruppo di ${NAME} abbandonato.", + "noMusicFilesInFolderText": "La cartella non contiene file musicali.", + "playerJoinedPartyText": "${NAME} si è unito al gruppo!", + "playerLeftPartyText": "${NAME} è uscito dal gruppo.", + "rejectingInviteAlreadyInPartyText": "Rifiuta invito (già in un gruppo).", + "serverRestartingText": "Il server si sta riavviando. Per favore ricollegati tra poco...", + "serverShuttingDownText": "Il server si sta chiudendo...", + "signInErrorText": "Errore nell'accesso.", + "signInNoConnectionText": "Impossibile accedere. (nessuna connessione internet?)", + "teamNameText": "Team ${NAME}", + "telnetAccessDeniedText": "ERRORE: l'utente non ha concesso l'accesso a telnet.", + "timeOutText": "(prenderai il controllo tra ${TIME} secondi)", + "touchScreenJoinWarningText": "Ti sei unito al gioco con il touchscreen.\nSe è stato un errore, premi 'Menu->Lascia Gioco'.", + "touchScreenText": "TouchScreen", + "trialText": "prova", + "unableToResolveHostText": "Errore: impossibile connettersi all'host", + "unavailableNoConnectionText": "Non è al momento disponibile (dipende dalla tua connessione?)", + "vrOrientationResetCardboardText": "Usa questo per ripristinare l'orientamento del VR.\nPer giocare avrai bisogno di un controller esterno.", + "vrOrientationResetText": "Ripristina orientamento VR.", + "willTimeOutText": "(se inattivo potrai riprendere il controllo)" + }, + "jumpBoldText": "SALTO", + "jumpText": "Salta", + "keepText": "Mantieni", + "keepTheseSettingsText": "Mantenere queste impostazioni?", + "keyboardChangeInstructionsText": "Premi due volte Spazio per cambiare la tastiera.", + "keyboardNoOthersAvailableText": "Non sono disponibili altre tastiere.", + "keyboardSwitchText": "Cambiando la tastiera a \"${NAME}\".", + "kickOccurredText": "${NAME} è stato cacciato.", + "kickQuestionText": "Cacciare ${NAME}?", + "kickText": "Caccia", + "kickVoteCantKickAdminsText": "Gli amministratori non possono essere cacciati.", + "kickVoteCantKickSelfText": "Non puoi cacciare te stesso.", + "kickVoteFailedNotEnoughVotersText": "Non ci sono abbastanza giocatori per una votazione", + "kickVoteFailedText": "Voto di caccia fallito.", + "kickVoteStartedText": "Una votazione per l'espulsione di ${NAME} è già in corso", + "kickVoteText": "Vota per cacciare", + "kickVotingDisabledText": "Il voto di caccia è disabilitato.", + "kickWithChatText": "Scrivi ${YES} in chat per sì e ${NO} per no.", + "killsTallyText": "${COUNT} uccisioni", + "killsText": "Uccisioni", + "kioskWindow": { + "easyText": "Facile", + "epicModeText": "Modalità Epica", + "fullMenuText": "Menu completo", + "hardText": "Difficile", + "mediumText": "Medio", + "singlePlayerExamplesText": "Esempi Giocatore Singolo / Co-op", + "versusExamplesText": "Esempi Versus" + }, + "languageSetText": "Lingua impostata: \"${LANGUAGE}\".", + "lapNumberText": "Giro ${CURRENT} di ${TOTAL}", + "lastGamesText": "(ultime ${COUNT} partite)", + "leaderboardsText": "Classifiche", + "league": { + "allTimeText": "Di sempre", + "currentSeasonText": "Stagione corrente (${NUMBER})", + "leagueFullText": "${NAME} Lega", + "leagueRankText": "Punteggio Lega", + "leagueText": "Lega", + "rankInLeagueText": "#${RANK}, ${NAME} Lega${SUFFIX}", + "seasonEndedDaysAgoText": "La stagione è finita da ${NUMBER} giorni.", + "seasonEndsDaysText": "La stagione finirà tra ${NUMBER} giorni.", + "seasonEndsHoursText": "La stagione finisce tra ${NUMBER} ore.", + "seasonEndsMinutesText": "La stagione finisce tra ${NUMBER} minuti.", + "seasonText": "Stagione ${NUMBER}", + "tournamentLeagueText": "Devi raggiungere la lega ${NAME} per partecipare a questo torneo.", + "trophyCountsResetText": "Il numero dei trofei verra azzerato la prossima stagione." + }, + "levelBestScoresText": "Miglior punteggio in ${LEVEL}", + "levelBestTimesText": "Miglior tempo in ${LEVEL}", + "levelFastestTimesText": "Tempo migliore su ${LEVEL}", + "levelHighestScoresText": "Punteggio migliore su ${LEVEL}", + "levelIsLockedText": "${LEVEL} è bloccato.", + "levelMustBeCompletedFirstText": "Devi completare prima il livello ${LEVEL}.", + "levelText": "Livello ${NUMBER}", + "levelUnlockedText": "Livello Sbloccato!", + "livesBonusText": "Vite bonus", + "loadingText": "caricamento", + "loadingTryAgainText": "Caricamento; riprova tra un momento", + "macControllerSubsystemBothText": "Entrambi (non consigliato)", + "macControllerSubsystemClassicText": "Classica", + "macControllerSubsystemDescriptionText": "(Prova a modificare questo se il controller non funziona)", + "macControllerSubsystemMFiNoteText": "Rilevato iOS/Mac Controller\nPotresti voler attivare questi in Impostazioni -> Controller", + "macControllerSubsystemMFiText": "iOS/Mac controller", + "macControllerSubsystemTitleText": "Supporto controller", + "mainMenu": { + "creditsText": "Riconoscimenti", + "demoMenuText": "Menu Demo", + "endGameText": "Chiudi partita", + "exitGameText": "Esci dal gioco", + "exitToMenuText": "Tornare al menu?", + "howToPlayText": "Come Giocare", + "justPlayerText": "(solo ${NAME})", + "leaveGameText": "Lascia Gioco", + "leavePartyConfirmText": "Lasciare davvero il gruppo?", + "leavePartyText": "Lascia Gruppo", + "leaveText": "Espelli giocatore", + "quitText": "Esci", + "resumeText": "Riprendi", + "settingsText": "Impostazioni" + }, + "makeItSoText": "Va Bene Così", + "mapSelectGetMoreMapsText": "Ottieni altre Mappe...", + "mapSelectText": "Cambia...", + "mapSelectTitleText": "Mappe per ${GAME}", + "mapText": "Mappa", + "maxConnectionsText": "Connessioni massime", + "maxPartySizeText": "Dimensione massima gruppo", + "maxPlayersText": "Giocatori massimi", + "modeArcadeText": "Modalità Arcade", + "modeClassicText": "Modalità Classica", + "modeDemoText": "Modalità Demo", + "mostValuablePlayerText": "Giocatore Più Bravo", + "mostViolatedPlayerText": "Giocatore Più Ucciso", + "mostViolentPlayerText": "Giocatore Più Violento", + "moveText": "Muovi", + "multiKillText": "${COUNT} UCCISIONI!!!", + "multiPlayerCountText": "${COUNT} giocatori", + "mustInviteFriendsText": "Nota: dovete invitare degli amici nel\npannello \"${GATHER}\" o collegare\ndei controller per giocare in multiplayer.", + "nameBetrayedText": "${NAME} ha tradito ${VICTIM}.", + "nameDiedText": "${NAME} è morto.", + "nameKilledText": "${NAME} ha ucciso ${VICTIM}.", + "nameNotEmptyText": "Il nome non può essere vuoto!", + "nameScoresText": "${NAME} ha segnato!", + "nameSuicideKidFriendlyText": "${NAME} è morto accidentalmente.", + "nameSuicideText": "${NAME} si è suicidato.", + "nameText": "Nome", + "nativeText": "Nativa", + "newPersonalBestText": "Nuovo record personale!", + "newTestBuildAvailableText": "Una nuova versione beta è disponibile (${VERSION} build ${BUILD}).\nScaricala da ${ADDRESS}", + "newText": "Nuovo", + "newVersionAvailableText": "Una nuova versione di ${APP_NAME} è disponibile! (${VERSION})", + "nextAchievementsText": "Prossimi Trofei:", + "nextLevelText": "Prossimo Livello", + "noAchievementsRemainingText": "- nessuno", + "noContinuesText": "(non continua)", + "noExternalStorageErrorText": "Nessun archiviatore esterno trovato su questo dispositivo", + "noGameCircleText": "Errore: non hai effettuato il login su GameCircle", + "noJoinCoopMidwayText": "Non puoi entrare in una partita cooperativa già iniziata.", + "noProfilesErrorText": "Non hai un profilo giocatore, quindi sei bloccato con '${NAME}'.\nVai su Impostazioni > Profili giocatore per creare un profilo personale.", + "noScoresYetText": "Ancora nessun punteggio.", + "noThanksText": "No Grazie", + "noTournamentsInTestBuildText": "I punteggi del torneo di questo test build verranno ignorati", + "noValidMapsErrorText": "Non sono state trovate mappe valide per questo tipo di gioco.", + "notEnoughPlayersRemainingText": "Non ci sono più abbastanza giocatori: esci e inizia un'altra partita.", + "notEnoughPlayersText": "Hai bisogno di almeno ${COUNT} giocatori per iniziare questa partita!", + "notNowText": "Non ora", + "notSignedInErrorText": "Per fare questo, devi accedere.", + "notSignedInGooglePlayErrorText": "Per fare questo devi accedere a Google Play.", + "notSignedInText": "non hai effettuato l'accesso", + "nothingIsSelectedErrorText": "Non hai selezionato nulla!", + "numberText": "N°${NUMBER}", + "offText": "Disattiva", + "okText": "Ok", + "onText": "Attiva", + "oneMomentText": "Un Momento...", + "onslaughtRespawnText": "${PLAYER} ritornerà all'ondata ${WAVE}", + "orText": "${A} o ${B}", + "otherText": "Altro", + "outOfText": "(#${RANK} su ${ALL})", + "ownFlagAtYourBaseWarning": "Per segnare un punto la tua bandiera\ndev'essere nella tua base!", + "packageModsEnabledErrorText": "Il gioco in rete non è consentito con i mod dei pacchetti locali abilitati (vedi Impostazioni->Avanzate)", + "partyWindow": { + "chatMessageText": "Messaggio Chat", + "emptyText": "Il tuo gruppo è vuoto", + "hostText": "(host)", + "sendText": "Invia", + "titleText": "Il tuo Gruppo" + }, + "pausedByHostText": "(in pausa da host)", + "perfectWaveText": "Ondata Perfetta!", + "pickUpBoldText": "RACCOGLI", + "pickUpText": "Raccogli", + "playModes": { + "coopText": "Cooperativa", + "freeForAllText": "Tutti Contro Tutti", + "multiTeamText": "Multi-Squadra", + "singlePlayerCoopText": "Giocatore singolo / Cooperativa", + "teamsText": "Squadre" + }, + "playText": "Gioca", + "playWindow": { + "coopText": "Cooperativi", + "freeForAllText": "Tutti Contro Tutti", + "oneToFourPlayersText": "1-4 giocatori", + "teamsText": "A Squadre", + "titleText": "Gioca", + "twoToEightPlayersText": "2-8 giocatori" + }, + "playerCountAbbreviatedText": "${COUNT}g", + "playerDelayedJoinText": "${PLAYER} si unirà al prossimo round.", + "playerInfoText": "Info Giocatore", + "playerLeftText": "${PLAYER} ha lasciato il gioco.", + "playerLimitReachedText": "Limite di ${COUNT} giocatori raggiunto; non può aggiungersi nessuno.", + "playerLimitReachedUnlockProText": "Aggiorna alla \"${PRO}\" sul negozio per giocare con più di ${COUNT} giocatori.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Non puoi eliminare il tuo profilo account.", + "deleteButtonText": "Elimina\nProfilo", + "deleteConfirmText": "Cancellare '${PROFILE}'?", + "editButtonText": "Modifica\nProfilo", + "explanationText": "(nomi e aspetti personalizzati per questo account)", + "newButtonText": "Nuovo\nProfilo", + "titleText": "Profili Giocatore" + }, + "playerText": "Giocatore", + "playlistNoValidGamesErrorText": "Questa scaletta non contiene giochi sbloccati validi.", + "playlistNotFoundText": "playlist non trovata", + "playlistText": "Lista", + "playlistsText": "Scalette", + "pleaseRateText": "Se ti sta piacendo ${APP_NAME}, prenditi\nun momento per valutarlo o scriverci su una recensione.\nQuesto aiuterà a supportare futuri sviluppi.\n\nGrazie!\n-eric", + "pleaseWaitText": "Attendi...", + "pluginsDetectedText": "Nuovo/i Plugin rilevato/i. Abilitali/configurali nelle impostazioni.", + "pluginsText": "Plugin", + "practiceText": "Allenamento", + "pressAnyButtonPlayAgainText": "Premi un pulsante qualunque per rigiocare...", + "pressAnyButtonText": "Premi un pulsante qualunque per continuare...", + "pressAnyButtonToJoinText": "Premi un pulsante qualunque per partecipare...", + "pressAnyKeyButtonPlayAgainText": "Premi un tasto/pulsante qualunque per giocare ancora...", + "pressAnyKeyButtonText": "Premi un tasto/pulsante qualunque per continuare...", + "pressAnyKeyText": "Premi un tasto qualunque...", + "pressJumpToFlyText": "** Premi ripetutamente 'salto' per volare **", + "pressPunchToJoinText": "premi PUGNO per partecipare...", + "pressToOverrideCharacterText": "premi ${BUTTONS} per sovrascrivere il tuo personaggio", + "pressToSelectProfileText": "premi ${BUTTONS} per selezionare il giocatore", + "pressToSelectTeamText": "premi ${BUTTONS} per selezionare una squadra", + "profileInfoText": "Crea profili per te e i tuoi amici\nper personalizzare nomi, personaggi e colori.", + "promoCodeWindow": { + "codeText": "Codice", + "codeTextDescription": "Codice Promozionale", + "enterText": "Usa" + }, + "promoSubmitErrorText": "Errore nell' immissione del codice; controlla la tua connessione internet", + "ps3ControllersWindow": { + "macInstructionsText": "Spegni l'alimentazione sul retro della tua PS3, assicurati\nche il Bluetooth sia abilitato sul tuo Mac, poi connetti i tuoi controller\nal tuo Mac tramite un cavo USB per appaiarli. Da qui in poi, puoi usare\nil tasto home del tuo controller per connetterlo al tuo Mac\nsia in modalità cavo (USB) o senza fili (Bluetooth).\n\nSu alcuni Mac potrebbe esserci la richiesta di una password per l'appaiamento.\nSe dovesse succedere, da' un occhiata al seguente tutorial o cerca su google.\n\n\n\n\nI controller PS3 connessi senza filo dovrebbero essere mostrati \nnella lista dispositivi su Preferenze di Sistema > Bluetooth. Potrebbe essere\nnecessario rimuoverli dalla lista quando vorrai usarli di nuovo sulla tua PS3.\n\nAssicurati inoltre di disconnetterli dal Bluetooth quando \nnon sono in uso altrimenti la loro batteria continuerà a scaricarsi.\n\nIl Bluetooth dovrebbe riuscire a gestire fino a 7 dispositivi,\nanche se il numero potrebbe variare.", + "ouyaInstructionsText": "Per usare un controller PS3 con la tua OUYA, semplicemente collegalo con un\ncavo USB in modo da accoppiarlo. Fare questo potrebbe disconnettere gli\naltri controller, quindi dovresti riavviare la OUYA e scollegare il cavo USB.\n\nDa qui in poi dovresti essere capace di usare il tasto HOME per connetterlo \nsenza fili. Una volta finito di giocare, tieni premuto il tasto HOME per 10\nsecondi per spegnere il controller; in caso contrario rimarrà acceso e \nsprecherà le batterie.", + "pairingTutorialText": "Video-tutorial per l'accoppiamento", + "titleText": "Come usare i controller PS3 con ${APP_NAME}:" + }, + "publicBetaText": "BETA PUBBLICA", + "punchBoldText": "PUGNO", + "punchText": "Pugno", + "purchaseForText": "Acquista per ${PRICE}", + "purchaseGameText": "Acquista il gioco", + "purchasingText": "Acquisto in corso...", + "quitGameText": "Uscire da ${APP_NAME}?", + "quittingIn5SecondsText": "Uscendo in 5 secondi...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Casuale", + "rankText": "Grado", + "ratingText": "Valutazione", + "reachWave2Text": "Raggiungi la seconda ondata per entrare in classifica.", + "readyText": "pronto", + "recentText": "Recenti", + "remainingInTrialText": "Rimani in prova", + "remoteAppInfoShortText": "${APP_NAME} è più divertente se giocato in famiglia o tra amici.\nConnetti uno o più controller hardware oppure installa la app \n${REMOTE_APP_NAME} su telefoni o tablet, per usarli\na mo' di controller.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Posizione del Tasto", + "button_size": "Dimensione del Tasto", + "cant_resolve_host": "Impossibile trovare l'host.", + "capturing": "Attesa Giocatori...", + "connected": "Connesso.", + "description": "Usa il tuo cellulare o tablet come controller per BombSquad.\nMassimo 8 dispositivi possono connettersi alla volta su una singola Tv o tablet.", + "disconnected": "Disconnesso dal Server.", + "dpad_fixed": "Fisso", + "dpad_floating": "Mobile", + "dpad_position": "Posizione del D-Pad", + "dpad_size": "Dimensione del D-Pad", + "dpad_type": "Modalità del D-Pad", + "enter_an_address": "Inserire un Indirizzo", + "game_full": "Il gioco è al completo o non accetta connessioni.", + "game_shut_down": "Il gioco si è spento.", + "hardware_buttons": "Tasto Hardware", + "join_by_address": "Accedi per Indirizzo...", + "lag": "Lag: ${SECONDS} secondi", + "reset": "Resetta", + "run1": "Corsa 1", + "run2": "Corsa 2", + "searching": "In cerca di partite...", + "searching_caption": "Tocca il nome della partita per aggiungertici.\nAssicurati di utilizzare la stessa rete wifi della partita.", + "start": "Start", + "version_mismatch": "Incompatibilità di versione.\nAssicurati che BombSquad e BombSquad Remote\nsiano alla stessa versione e riprova." + }, + "removeInGameAdsText": "Sblocca \"${PRO}\" nel negozio per rimuovere le pubblicità.", + "renameText": "Rinomina", + "replayEndText": "Fine Replay", + "replayNameDefaultText": "Replay Ultima Partita", + "replayReadErrorText": "Errore durante la lettura del replay.", + "replayRenameWarningText": "Rinomina ${REPLAY} dopo una partita per conservarlo, altrimenti verrà sovrascritto.", + "replayVersionErrorText": "Questo replay è stato creato con una versione\nnon compatibile del gioco e non può essere usato.", + "replayWatchText": "Guarda Replay", + "replayWriteErrorText": "Errore durante il salvataggio del replay.", + "replaysText": "Replay", + "reportPlayerExplanationText": "Usa questa email per segnalare imbrogli, linguaggio inappropriato o altri cattivi comportamenti.\nAggiungere una descrizione nello spazio sottostante:", + "reportThisPlayerCheatingText": "Scorrettezze", + "reportThisPlayerLanguageText": "Linguaggio inappropriato", + "reportThisPlayerReasonText": "Cosa vorresti segnalare?", + "reportThisPlayerText": "Segnala Questo Giocatore", + "requestingText": "Richiesta in corso...", + "restartText": "Ricomincia", + "retryText": "Riprova", + "revertText": "Ritorna", + "runText": "Corri", + "saveText": "Salva", + "scanScriptsErrorText": "Errore/i nella lettura degli script, guardare il log per i dettagli.", + "scoreChallengesText": "Punteggi sfida", + "scoreListUnavailableText": "Lista punteggi non disponibile.", + "scoreText": "Punteggio", + "scoreUnits": { + "millisecondsText": "Millisecondi", + "pointsText": "Punti", + "secondsText": "Secondi" + }, + "scoreWasText": "(era ${COUNT})", + "selectText": "Seleziona", + "seriesWinLine1PlayerText": "VINCE LA", + "seriesWinLine1TeamText": "VINCE LA", + "seriesWinLine1Text": "VINCE LA", + "seriesWinLine2Text": "SERIE!", + "settingsWindow": { + "accountText": "Account", + "advancedText": "Avanzate", + "audioText": "Audio", + "controllersText": "Controller", + "graphicsText": "Grafiche", + "playerProfilesMovedText": "Nota: Il profilo del giocatore è stato spostato nella finestra \"Account\" nella schermata principale.", + "playerProfilesText": "Profili giocatore", + "titleText": "Impostazioni" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(una semplice tastiera sullo schermo per editare testi)", + "alwaysUseInternalKeyboardText": "Usa Sempre La Tastiera Interna", + "benchmarksText": "Benchmarks & Prove Di Stress", + "disableCameraGyroscopeMotionText": "Disabilita il movimento della visuale tramite giroscopio", + "disableCameraShakeText": "Disabilita il tremolio della visuale", + "disableThisNotice": "(puoi disattivare questo messaggio su Impostazioni>Avanzate)", + "enablePackageModsDescriptionText": "(abilita la possibilità di modding aggiuntivo ma disabilita il gioco in rete)", + "enablePackageModsText": "Abilita Mod Dei Pacchetti Locali", + "enterPromoCodeText": "Inserisci il Codice", + "forTestingText": "Note: questi valori sono solo di prova e saranno cancellati uscendo dalla app.", + "helpTranslateText": "Le traduzioni di ${APP_NAME} sono un contributo\ndella comunità. Se vuoi contribuire o correggere una\ntraduzione, segui il link qui sotto. Grazie in anticipo!", + "kickIdlePlayersText": "Caccia i giocatori inattivi", + "kidFriendlyModeText": "Modalità per bambini (violenza ridotta e altro)", + "languageText": "Lingua", + "moddingGuideText": "Manuale di personalizzazione", + "mustRestartText": "Devi riavviare il gioco per apportare questa modifica.", + "netTestingText": "Collaudo Rete", + "resetText": "Reset", + "showBombTrajectoriesText": "Mostra le traiettorie delle bombe", + "showPlayerNamesText": "Mostra i nomi dei giocatori", + "showUserModsText": "Apri cartella personalizzazioni", + "titleText": "Avanzato", + "translationEditorButtonText": "Modifica la traduzione di ${APP_NAME}", + "translationFetchErrorText": "stato traduzione non disponibile", + "translationFetchingStatusText": "controllo stato traduzione...", + "translationInformMe": "Informami quando la mia lingua ha bisogno di aggiornamenti", + "translationNoUpdateNeededText": "la traduzione italiana è completa; woohoo!", + "translationUpdateNeededText": "** ci sono testi da tradurre!! **", + "vrTestingText": "Collaudo VR" + }, + "shareText": "Condividi", + "sharingText": "Condividendo...", + "showText": "Mostra", + "signInForPromoCodeText": "Devi accedere ad un account, per poter far funzionare i codici.", + "signInWithGameCenterText": "Per utilizzare un account Game Center,\naccedi utilizzando l'app Game Center.", + "singleGamePlaylistNameText": "Solo ${GAME}", + "singlePlayerCountText": "Un giocatore", + "soloNameFilterText": "${NAME} Testa a Testa", + "soundtrackTypeNames": { + "CharSelect": "Selezione del personaggio", + "Chosen One": "Il prescelto", + "Epic": "Giochi in Modalità Leggendaria", + "Epic Race": "Corsa Leggendaria", + "FlagCatcher": "Cattura la bandiera", + "Flying": "Pensieri beati", + "Football": "Football", + "ForwardMarch": "Attacco", + "GrandRomp": "Conquista", + "Hockey": "Hockey", + "Keep Away": "Tieni lontano", + "Marching": "Girotondo", + "Menu": "Menu principale", + "Onslaught": "Assalto", + "Race": "Corsa", + "Scary": "Re della collina", + "Scores": "Schermata punteggi", + "Survival": "Eliminazione", + "ToTheDeath": "Death match", + "Victory": "Schermata punteggi finali" + }, + "spaceKeyText": "spazio", + "statsText": "statistiche", + "storagePermissionAccessText": "Richiede l'accesso alla memoria locale", + "store": { + "alreadyOwnText": "Hai già acquistato ${NAME}!", + "bombSquadProDescriptionText": "• Guadagni doppi per gli obiettivi\n• Rimuovi la pubblicità nel gioco\n• Include ${COUNT} biglietti bonus\n• +${PERCENT}% di bonus sul punteggio di lega\n• Sblocca '${INF_ONSLAUGHT}' e\n '${INF_RUNAROUND}' livelli co-op", + "bombSquadProFeaturesText": "Caratteristiche:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Rimuove la pubblicità e nag screens\n• Sblocca più impostazioni del gioco\n• Include inoltre:", + "buyText": "Acquista", + "charactersText": "Personaggi", + "comingSoonText": "In Arrivo...", + "extrasText": "Extra", + "freeBombSquadProText": "BombSquad adesso è gratuito, ma dato che lo avevi già comprato\nriceverai l'aggiornamento a BombSquad Pro e ${COUNT} biglietti come ringraziamento.\nGustati le novità, e grazie per il supporto!\n-Eric", + "gameUpgradesText": "Aggiornamenti Gioco", + "getCoinsText": "Ottieni monete", + "holidaySpecialText": "Speciale Vacanze", + "howToSwitchCharactersText": "(vai a \"${SETTINGS} -> ${PLAYER_PROFILES}\" per assegnare o cambiare i personaggi.)", + "howToUseIconsText": "(crea profili giocatori globali (nella finestra \"Account\") per poterle usare)", + "howToUseMapsText": "(usa queste mappe nelle tue playlist a squadre/tutti contro tutti)", + "iconsText": "Icone", + "loadErrorText": "Impossibile caricare la pagina.\nControlla la tua connessione a internet.", + "loadingText": "caricamento", + "mapsText": "Mappe", + "miniGamesText": "MiniGiochi", + "oneTimeOnlyText": "(solo una volta)", + "purchaseAlreadyInProgressText": "Un acquisto per questo oggetto è già in corso.", + "purchaseConfirmText": "Vuoi comprare ${ITEM}?", + "purchaseNotValidError": "Acquisto non valido.\nContatta ${EMAIL} se questo è un errore.", + "purchaseText": "Compra", + "saleBundleText": "Pacchetto in sconto!", + "saleExclaimText": "Saldi!", + "salePercentText": "(${PERCENT}% di meno)", + "saleText": "SALDI", + "searchText": "Cerca", + "teamsFreeForAllGamesText": "Giochi a Squadre e/o Tutti Contro Tutti", + "totalWorthText": "*** Valore di ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Aggiornare?", + "winterSpecialText": "Speciale Inverno", + "youOwnThisText": "- è già tuo -" + }, + "storeDescriptionText": "Festa Caotica a 8 Giocatori\n\nFai saltare in aria i tuoi amici (o il computer) in un torneo di mini-giochi esplosivi come Cattura la bandiera, Hockeybomba, e l'Epic-Slow-Motion-Death-Match!\n\nI comandi semplici e l'ampio supporto di controller rendono più facile e divertente il gioco fino a 8 persone; puoi anche usare il tuo cellulare o tablet come controller tramite l'App gratuita 'BombSquad Remote'!\n\nBombardali tutti!\n\nVai su www.froemling.net/bombsquad per più informazioni.", + "storeDescriptions": { + "blowUpYourFriendsText": "Fai saltare in aria i tuoi amici.", + "competeInMiniGamesText": "Competi in mini-giochi che vanno dalla corsa al volo.", + "customize2Text": "Personalizza personaggi, mini-giochi, e persino la colonna sonora.", + "customizeText": "Personalizza i personaggi e crea la tua playlist personale di mini-giochi.", + "sportsMoreFunText": "Gli sport sono più divertenti con gli esplosivi.", + "teamUpAgainstComputerText": "Alleati contro il computer." + }, + "storeText": "Negozio", + "submitText": "Inoltra", + "submittingPromoCodeText": "Inoltrando il Codice...", + "teamNamesColorText": "Colore/Nome Team", + "teamsText": "Squadre", + "telnetAccessGrantedText": "Accesso a telnet abilitato", + "telnetAccessText": "Rilevato accesso a telnet; consentirlo?", + "testBuildErrorText": "Questa build di prova non è più attiva; per favore controlla se c'è una nuova versione.", + "testBuildText": "Build Di Prova", + "testBuildValidateErrorText": "Incapace di validare la build di prova. (sei connesso alla rete?)", + "testBuildValidatedText": "Build Di Prova Validata; Divertiti!", + "thankYouText": "Grazie per il supporto! Goditi il gioco!!", + "threeKillText": "TRIPLA UCCISIONE!!", + "timeBonusText": "Bonus Tempo", + "timeElapsedText": "Tempo trascorso", + "timeExpiredText": "Tempo Scaduto", + "timeSuffixDaysText": "${COUNT}gg", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Consiglio", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Gli amici migliori", + "tournamentCheckingStateText": "Controllo stato torneo in corso; attendere...", + "tournamentEndedText": "Questo torneo è finito. Ne partirà un altro a breve.", + "tournamentEntryText": "Partecipa al Torneo", + "tournamentResultsRecentText": "Risultati recenti del torneo", + "tournamentStandingsText": "Classifica del torneo", + "tournamentText": "Torneo", + "tournamentTimeExpiredText": "Tempo del torneo esaurito", + "tournamentsText": "Tornei", + "translations": { + "characterNames": { + "Agent Johnson": "Agente Johnson", + "B-9000": "B-9000", + "Bernard": "Bernardo", + "Bones": "Ossicino", + "Butch": "Butch", + "Easter Bunny": "Coniglio Pasquale", + "Flopsy": "Flopsy", + "Frosty": "Gelido", + "Gretel": "Gretel", + "Grumbledorf": "Brontolone", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Lucky", + "Mel": "Mel", + "Middle-Man": "Middle-Man", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Babbo Natale", + "Snake Shadow": "Snake Shadow", + "Spaz": "Spaz", + "Taobao Mascot": "Mascotte Tabao", + "Todd": "Todd", + "Todd McBurton": "Todd McBurton", + "Xara": "Xara", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Assalto\nInfinito", + "Infinite\nRunaround": "Girotondo\nInfinito", + "Onslaught\nTraining": "Assalto\nAllenamento", + "Pro\nFootball": "Football\nEsperto", + "Pro\nOnslaught": "Assalto\nEsperto", + "Pro\nRunaround": "Girotondo\nEsperto", + "Rookie\nFootball": "Football\nNovellino", + "Rookie\nOnslaught": "Assalto\nNovellino", + "The\nLast Stand": "L'ultimo\nSopravvissuto", + "Uber\nFootball": "Football\nUltra", + "Uber\nOnslaught": "Assalto\nUltra", + "Uber\nRunaround": "Girotondo\nUltra" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Allenamento", + "Infinite ${GAME}": "${GAME} Infinito", + "Infinite Onslaught": "Assalto Infinito", + "Infinite Runaround": "Girotondo Infinito", + "Onslaught": "Assalto - Infinito", + "Onslaught Training": "Allenamento Assalto", + "Pro ${GAME}": "${GAME} Pro", + "Pro Football": "Football Esperto", + "Pro Onslaught": "Assalto Esperto", + "Pro Runaround": "Girotondo Esperto", + "Rookie ${GAME}": "${GAME} Principiante", + "Rookie Football": "Football Facile", + "Rookie Onslaught": "Assalto Facile", + "Runaround": "Girotondo - Infinito", + "The Last Stand": "L'ultimo sopravvissuto", + "Uber ${GAME}": "${GAME} Impossibile", + "Uber Football": "Football Ultra", + "Uber Onslaught": "Assalto Ultra", + "Uber Runaround": "Girotondo Ultra" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Per vincere, sii il prescelto per un determinato tempo.\nUccidi il prescelto per diventarlo.", + "Bomb as many targets as you can.": "Bombarda più bersagli che puoi.", + "Carry the flag for ${ARG1} seconds.": "Tieni la bandiera per ${ARG1} secondi.", + "Carry the flag for a set length of time.": "Tieni la bandiera per una determinata quantità di tempo.", + "Crush ${ARG1} of your enemies.": "Distruggi ${ARG1} dei tuoi nemici.", + "Defeat all enemies.": "Sconfiggi tutti i nemici.", + "Dodge the falling bombs.": "Evita le bombe cadenti.", + "Final glorious epic slow motion battle to the death.": "Gloriosa battaglia finale fino alla morte a rallentatore leggendario.", + "Gather eggs!": "Raccogli le uova!", + "Get the flag to the enemy end zone.": "Prendi la bandiera dalla zona del nemico.", + "How fast can you defeat the ninjas?": "Quanto velocemente riuscirete a sconfiggere i ninja?", + "Kill a set number of enemies to win.": "Uccidi un determinato numero di nemici per vincere.", + "Last one standing wins.": "L'ultimo giocatore a rimanere in piedi vince.", + "Last remaining alive wins.": "L'ultimo giocatore a rimanere vivo vince.", + "Last team standing wins.": "L'ultima squadra a rimanere in piedi vince.", + "Prevent enemies from reaching the exit.": "Non lasciare che i nemici raggiungano l'uscita.", + "Reach the enemy flag to score.": "Raggiungi la bandiera nemica per vincere.", + "Return the enemy flag to score.": "Riporta la bandiera nemica per vincere.", + "Run ${ARG1} laps.": "Corri per ${ARG1} giri.", + "Run ${ARG1} laps. Your entire team has to finish.": "Corri per ${ARG1} giri. Tutta la tua squadra deve completarli.", + "Run 1 lap.": "Corri per un giro.", + "Run 1 lap. Your entire team has to finish.": "Corri per un giro. Tutta la tua squadra deve completarlo.", + "Run real fast!": "Corri velocissimo!", + "Score ${ARG1} goals.": "Segna ${ARG1} gol.", + "Score ${ARG1} touchdowns.": "Segna ${ARG1} touchdown.", + "Score a goal": "Segna un gol", + "Score a goal.": "Segna un gol.", + "Score a touchdown.": "Segna un touchdown.", + "Score some goals.": "Segna alcuni gol.", + "Secure all ${ARG1} flags.": "Assicurati tutte e ${ARG1} le bandiere.", + "Secure all flags on the map to win.": "Assicurati tutte le bandiere sulla mappa per vincere.", + "Secure the flag for ${ARG1} seconds.": "Difendi la bandiera per ${ARG1} secondi.", + "Secure the flag for a set length of time.": "Difendi la bandiera per un determinato periodo di tempo.", + "Steal the enemy flag ${ARG1} times.": "Ruba la bandiera nemica ${ARG1} volte.", + "Steal the enemy flag.": "Ruba la bandiera nemica.", + "There can be only one.": "Ne resterà soltanto uno.", + "Touch the enemy flag ${ARG1} times.": "Tocca la bandiera nemica ${ARG1} volte.", + "Touch the enemy flag.": "Tocca la bandiera nemica.", + "carry the flag for ${ARG1} seconds": "tieni la bandiera per ${ARG1} secondi", + "kill ${ARG1} enemies": "Uccidi ${ARG1} nemici", + "last one standing wins": "L'ultimo a rimanere in piedi vince", + "last team standing wins": "L'ultima squadra a rimanere in piedi vince", + "return ${ARG1} flags": "Riporta ${ARG1} bandiere", + "return 1 flag": "Riporta 1 bandiera", + "run ${ARG1} laps": "corri per ${ARG1} giri", + "run 1 lap": "corri per un giro", + "score ${ARG1} goals": "Segna ${ARG1} gol", + "score ${ARG1} touchdowns": "Segna ${ARG1} touchdowns", + "score a goal": "Segna un gol", + "score a touchdown": "segna un touchdown", + "secure all ${ARG1} flags": "Assicurati tutte e ${ARG1} bandiere", + "secure the flag for ${ARG1} seconds": "Difendi la bandiera per ${ARG1} secondi", + "touch ${ARG1} flags": "tocca ${ARG1} bandiere", + "touch 1 flag": "tocca 1 bandiera" + }, + "gameNames": { + "Assault": "Attacco", + "Capture the Flag": "Cattura la bandiera", + "Chosen One": "Il Prescelto", + "Conquest": "Conquista", + "Death Match": "Death Match", + "Easter Egg Hunt": "Caccia alle Uova", + "Elimination": "Eliminazione", + "Football": "Football", + "Hockey": "Hockey", + "Keep Away": "Tieni lontano", + "King of the Hill": "Re della collina", + "Meteor Shower": "Tempesta di meteoriti", + "Ninja Fight": "Ninja Fight", + "Onslaught": "Assalto", + "Race": "Corsa", + "Runaround": "Girotondo", + "Target Practice": "Tiro al Bersaglio", + "The Last Stand": "L'ultimo sopravvissuto" + }, + "inputDeviceNames": { + "Keyboard": "Tastiera", + "Keyboard P2": "Tastiera G2" + }, + "languages": { + "Arabic": "Arabo", + "Belarussian": "Bielorusso", + "Chinese": "Cinese Semplificato", + "ChineseTraditional": "Cinese Tradizionale", + "Croatian": "Croato", + "Czech": "Ceco", + "Danish": "Danese", + "Dutch": "Olandese", + "English": "Inglese", + "Esperanto": "Esperanto", + "Finnish": "Finlandese", + "French": "Francese", + "German": "Tedesco", + "Gibberish": "Insensato", + "Greek": "Greco", + "Hindi": "Hindi", + "Hungarian": "Ungherese", + "Indonesian": "Indonesiano", + "Italian": "Italiano", + "Japanese": "Giapponese", + "Korean": "Koreano", + "Persian": "Persiano", + "Polish": "Polacco", + "Portuguese": "Portoghese", + "Romanian": "Rumeno", + "Russian": "Russo", + "Serbian": "Serbo", + "Slovak": "Slovacco", + "Spanish": "Spagnolo", + "Swedish": "Svedese", + "Turkish": "Turco", + "Ukrainian": "Ucraino", + "Venetian": "Veneto", + "Vietnamese": "Vietnamita" + }, + "leagueNames": { + "Bronze": "Bronzo", + "Diamond": "Diamante", + "Gold": "Oro", + "Silver": "Argento" + }, + "mapsNames": { + "Big G": "La grande G", + "Bridgit": "Pontile", + "Courtyard": "Cortile", + "Crag Castle": "Castello arroccato", + "Doom Shroom": "Il fungo del destino", + "Football Stadium": "Campo da football", + "Happy Thoughts": "Pensieri beati", + "Hockey Stadium": "Campo da Hockey", + "Lake Frigid": "Lago Ghiacciato", + "Monkey Face": "Faccia da scimmia", + "Rampage": "La furia", + "Roundabout": "Trampolini", + "Step Right Up": "Su e giù per le scale", + "The Pad": "L'altura", + "Tip Top": "Tip Top", + "Tower D": "Torre D", + "Zigzag": "Zig Zag" + }, + "playlistNames": { + "Just Epic": "Epico!", + "Just Sports": "Facciamo Sport!" + }, + "promoCodeResponses": { + "invalid promo code": "codice promozionale non valido" + }, + "scoreNames": { + "Flags": "Bandiere", + "Goals": "Gol", + "Score": "Punteggio", + "Survived": "Sopravvissuto", + "Time": "Tempo", + "Time Held": "Tempo di possesso" + }, + "serverResponses": { + "A code has already been used on this account.": "Un codice è già stato usato su questo account.", + "A reward has already been given for that address.": "Una ricompensa è già stata data a quell'indirizzo", + "Account linking successful!": "Account collegato correttamente!", + "Account unlinking successful!": "Account scollegato con successo!", + "Accounts are already linked.": "Questi account sono già collegati.", + "An error has occurred; (${ERROR})": "C'è stato un errore; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "C'è stato un errore; contatta il supporto. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Si è verificato un errore; per favore contattare support@froemling.net.", + "An error has occurred; please try again later.": "Qualcosa non va; per favore riprova più tardi.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Sei sicuro di voler collegare questi account?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nL'operazione non può essere annullata!", + "BombSquad Pro unlocked!": "BombSquad Pro sbloccato!", + "Can't link 2 accounts of this type.": "Impossibile collegare 2 account di questo tipo.", + "Can't link 2 diamond league accounts.": "Impossibile collegare 2 account lega diamante.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Impossibile collegare; supererebbe il massimo di ${COUNT} account collegabili.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Individuato imbroglio: punteggi e premi sospesi per ${COUNT} giorni.", + "Could not establish a secure connection.": "Impossibile stabilire una connessione sicura.", + "Daily maximum reached.": "Limite massimo giornaliero raggiunto.", + "Entering tournament...": "Entrando nel torneo...", + "Invalid code.": "Codice non valido.", + "Invalid payment; purchase canceled.": "Pagamento non valido; acquisto annullato.", + "Invalid promo code.": "Codice promozionale non valido.", + "Invalid purchase.": "Acquisto non valido.", + "Invalid tournament entry; score will be ignored.": "Entrata in gioco non valida; il punteggio sarà ignorato.", + "Item unlocked!": "Oggetto sbloccato!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "Collegamento rifiutato. ${ACCOUNT} contiene\ndati importanti che verranno TUTTI PERSI.\nPuoi collegarlo nell'ordine opposto se preferisci\n(e perdere i dati di QUESTO account)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Vuoi davvero collegare l'account ${ACCOUNT} a questo account?\nTutti i dati presenti in ${ACCOUNT} andranno persi.\nLa procedura non può essere annullata. Sei sicuro?", + "Max number of playlists reached.": "Numero massimo di playlist raggiunto.", + "Max number of profiles reached.": "Numero massimo di profili raggiunto.", + "Maximum friend code rewards reached.": "Numero massimo di richieste d'amicizia inviate", + "Message is too long.": "Il messaggio è troppo lungo.", + "Profile \"${NAME}\" upgraded successfully.": "Il profilo \"${NAME}\" è stato aggiornato con successo.", + "Profile could not be upgraded.": "Il profilo non è potuto essere aggiornato.", + "Purchase successful!": "Acquistato con successo!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Ricevi ${COUNT} biglietti accedendo.\nTorna domani per ricevere ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "La funzionalità server non è più supportata in questa versione del gioco;\nAggiornalo ad una nuova versione.", + "Sorry, there are no uses remaining on this code.": "Spiacente, non ci sono altri utilizzi rimasti su questo codice.", + "Sorry, this code has already been used.": "Spiacente, questo codice è già stato usato.", + "Sorry, this code has expired.": "Spiacente, questo codice è scaduto.", + "Sorry, this code only works for new accounts.": "Spiacente, questo codice funziona solo per nuovi account.", + "Temporarily unavailable; please try again later.": "Al momento non disponibile; prova più tardi.", + "The tournament ended before you finished.": "Il torneo è terminato prima che tu abbia finito.", + "This account cannot be unlinked for ${NUM} days.": "Questo account non può essere scollegato per ${NUM} giorni.", + "This code cannot be used on the account that created it.": "Questo codice non può essere usato sull'account che l'ha creato.", + "This is currently unavailable; please try again later.": "Al momento non è disponibile. Riprova più tardi!", + "This requires version ${VERSION} or newer.": "Richiede la versione ${VERSION} o più recente.", + "Tournaments disabled due to rooted device.": "Tornei disabilitati a causa del dispositivo rooted.", + "Tournaments require ${VERSION} or newer": "I tornei richiedono ${VERSION} o superiore", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Scollegare l'account ${ACCOUNT} da questo account?\nTutti i dati su ${ACCOUNT} andranno cancellati.\n(eccetto per i trofei in alcuni casi)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ATTENZIONE: tentativi di hacking sono stati segnalati per il tuo account.\nGli account scoperti in tentativi di hacking saranno bannati. Gioca pulito.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Vuoi collegare il tuo account di dispositivo a questo?\n\nIl tuo account di dispositivo è ${ACCOUNT1}\nQuesto account è ${ACCOUNT2}\n\nQuesta operazione ti permetterà di conservare i tuoi progressi esistenti.\nAttenzione: l'operazione non può essere annullata!", + "You already own this!": "È già tuo!", + "You can join in ${COUNT} seconds.": "Potrai entrare in ${COUNT} secondi", + "You don't have enough tickets for this!": "Non hai abbastanza biglietti per questo!", + "You don't own that.": "Non lo possiedi", + "You got ${COUNT} tickets!": "Hai ${COUNT} biglietti!", + "You got a ${ITEM}!": "Hai ricevuto un ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Sei stato promosso a una nuova lega, Congratulazioni!", + "You must update to a newer version of the app to do this.": "Devi aggiornare a una versione nuova dell'app per poter fare questo.", + "You must update to the newest version of the game to do this.": "Devi aggiornare ad una nuova versione del gioco per fare ciò.", + "You must wait a few seconds before entering a new code.": "Devi aspettare qualche secondo prima di inserire un nuovo codice.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Sei il numero ${RANK} dell'ultimo torneo. Grazie per aver giocato!", + "Your account was rejected. Are you signed in?": "Il tuo account è stato rifiutato. Sei già iscritto?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "La tua copia del gioco è stata modificata.\nPer favore, annulla le modifiche e riprova.", + "Your friend code was used by ${ACCOUNT}": "Il tuo codice amico è stato usato da ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 minuto", + "1 Second": "1 Secondo", + "10 Minutes": "10 minuti", + "2 Minutes": "2 minuti", + "2 Seconds": "2 Secondi", + "20 Minutes": "20 minuti", + "4 Seconds": "4 Secondi", + "5 Minutes": "5 minuti", + "8 Seconds": "8 Secondi", + "Allow Negative Scores": "Abilita Punteggio Negativo", + "Balance Total Lives": "Bilancia le vite totali", + "Bomb Spawning": "Apparizione bombe", + "Chosen One Gets Gloves": "Il prescelto ottiene i guantoni", + "Chosen One Gets Shield": "Il prescelto ottiene lo scudo", + "Chosen One Time": "Tempo del prescelto", + "Enable Impact Bombs": "Abilita bombe ad impatto", + "Enable Triple Bombs": "Abilita bombe triple", + "Entire Team Must Finish": "Tutta la squadra deve finire", + "Epic Mode": "Modalità Leggendaria", + "Flag Idle Return Time": "Tempo di ritorno della bandiera inattiva", + "Flag Touch Return Time": "Tempo di ritorno della bandiera toccata", + "Hold Time": "Tempo di possesso", + "Kills to Win Per Player": "Uccisioni per giocatore per vincere", + "Laps": "Giri", + "Lives Per Player": "Vite per giocatore", + "Long": "Lunga", + "Longer": "Più lunga", + "Mine Spawning": "Apparizione Mine", + "No Mines": "Nessuna mina", + "None": "No", + "Normal": "Normale", + "Pro Mode": "Modalità Pro", + "Respawn Times": "Attesa per riapparire", + "Score to Win": "Punteggio per vincere", + "Short": "Breve", + "Shorter": "Più breve", + "Solo Mode": "Modalità Testa a Testa", + "Target Count": "Numero dì obbiettivi", + "Time Limit": "Tempo limite" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} è squalificato perché ${PLAYER} ha abbandonato", + "Killing ${NAME} for skipping part of the track!": "${NAME} è stato ucciso per aver saltato parte del tracciato!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Avviso per ${NAME}: turbo / turbo-spam ti ha espulso." + }, + "teamNames": { + "Bad Guys": "Cattivoni", + "Blue": "Blu", + "Good Guys": "Buoni", + "Red": "Rossa" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Un pugno dato in perfetta sincronia di corsa, salto e rotazione può uccidere\nin un colpo solo e farti guadagnare la stima e il rispetto dei tuoi amici.", + "Always remember to floss.": "Ricorda sempre di usare il filo interdentale.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Crea profili giocatore per te e i tuoi amici con i vostri\nnomi e aspetti preferiti invece di usarne uno casuale.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Le scatole maledizione ti trasformano in una bomba ad orologeria.\nL'unico antidoto è di prendere velocemente un kit di pronto soccorso.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Malgrado il loro aspetto, le abilità di ogni personaggio sono identiche, \nquindi scegline uno qualunque nel quale ti ci rivedi.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Non darti troppe arie con quello scudo di energia; puoi sempre essere scaraventato giù da un dirupo.", + "Don't run all the time. Really. You will fall off cliffs.": "Non correre sempre. Davvero. Cadrai sicuramente.", + "Don't spin for too long; you'll become dizzy and fall.": "Non girare troppo a lungo altrimenti diverrai pazzo e cadrai", + "Hold any button to run. (Trigger buttons work well if you have them)": "Tieni premuto un pulsante qualsiasi per correre. (I grilletti sono ideali, se li hai)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Tieni premuto un pulsante qualsiasi. Andrai più veloce\nma sarà più difficile fare manovre, quindi attento a non cadere.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Le bombe ghiaccio non sono molto potenti, ma congelano\nchiunque colpiscono, rendendoli vulnerabili alla frantumazione.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Se qualcuno ti afferra, colpiscilo e ti lascerà andare.\nQuesto funziona anche nella vita di tutti i giorni.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Se non hai nessun controller, installa l'app ${REMOTE_APP_NAME}'\nsul tuo dispositivo mobile per usarlo come controller.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Se sei a corto di controller, installa l'app 'BombSquad Remote' \nsul tuo dispositivo iOS o Android per usarli come controller. ", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Se vieni colpito da una bomba caccola, salta, corri in cerchi e dimenati come un matto.\nDovresti riuscire a staccartene, o almeno a rendere più divertente i tuoi ultimi istanti di vita.", + "If you kill an enemy in one hit you get double points for it.": "Se uccidi un nemico in un colpo solo riceverai il doppio dei punti.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Se acchiappi una maledizione, la tua unica speranza di salvezza \nè quella di prendere un kit di pronto soccorso nei pochi secondi successivi.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Se resti fermo, sei fritto. Corri e schiva per sopravvivere.", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Se hai diversi giocatori che vanno e vengono, seleziona 'Caccia i giocatori inattivi'\nnel menù Impostazioni nel caso qualcuno dimentichi di lasciare il gioco.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Se il tuo dispositivo si scaldasse eccessivamente e tu volessi conservare batteria,\ncala le impostazioni \"Visuali\" o \"Risoluzione\" in Impostazioni->Grafiche", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Se i frame sono discontinui, prova ad abbassare la risoluzione\no la visuale del gioco nel menu Impostazioni grafiche", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "In Cattura la bandiera, per segnare un punto la tua bandiera dev'essere nella tua base;\nse il team avversario sta per segnare, rubare la loro bandiera può essere un modo per fermarli.", + "In hockey, you'll maintain more speed if you turn gradually.": "Nell'hockey, per manterere più velocità, ti conviene girare gradualmente.", + "It's easier to win with a friend or two helping.": "È più facile vincere con uno o più amici che ti aiutano.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Salta nel momento in cui lanci le bombe per farle arrivare più in alto possibile.", + "Land-mines are a good way to stop speedy enemies.": "Le mine antiuomo sono un'ottimo modo di fermare nemici veloci.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Molte cose possono essere raccolte e lanciate, compresi altri giocatori. Scaraventare\ni tuoi nemici giù dai precipizi può essere un efficiente ed appagante strategia.", + "No, you can't get up on the ledge. You have to throw bombs.": "No, non puoi salire sulla sporgenza. Devi lanciare le bombe.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "I giocatori possono entrare ed uscire durante la maggior parte delle partite,\ne puoi anche collegare e scollegare le periferiche al volo.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "I giocatori possono entrare e uscire nel mezzo della maggior parte dei giochi,\ne inoltre puoi collegare e scollegare i gamepad al volo.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "I potenziamenti sono a tempo solo nei giochi cooperativi.\nNei giochi a squadre e nei Tutti Contro Tutti sono tuoi finché non muori.", + "Practice using your momentum to throw bombs more accurately.": "Fai pratica usando la spinta per lanciare le bombe con più precisione.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "I pugni infliggono molti più danni quanto più velocemente ti muovi,\nquindi prova a correre, saltare e girare come un pazzo.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Prendi la rincorsa prima di lanciare una bomba\ncosì da farla andare più lontano.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Fai fuori un gruppo di nemici\nlanciando una bomba vicino a una scatola di TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "La testa è l'area più vulnerabile, quindi una bomba caccola\nsulla zucca di solito vuol dire game over.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Questo livello non finisce mai, ma un punteggio elevato qui\nti farà guadagnare eterno rispetto in tutto il mondo.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "La potenza di lanci è basata sulla direzione che stai premendo.\nPer lanciare delicatamente qualcosa davanti a te, non premere nessun pulsante direzionale.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Stanco della colonna sonora? Sostituiscila con una tua!\nVai in Settings->Audio->Soundtrack", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Prova a 'lasciar cuocere' le bombe per un secondo o due prima di lanciarle.", + "Try tricking enemies into killing eachother or running off cliffs.": "Prova ad ingannare i nemici in modo da farli uccidere l'un l'altro o di farli cadere dalle sporgenze.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Usa il pulsante raccogli per afferrare la bandiera < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Prendi la rincorsa per lanciare più lontano.", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Puoi 'direzionare' i tuoi pugni girando a destra o a sinistra.\nQuesto è utile per far cadere giù i nemici dalle sporgenze o segnare un gol nell'hockey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Puoi capire quando una bomba sta per esplodere basandoti\nsul colore della miccia: giallo... arancione... rosso... BOOOM.", + "You can throw bombs higher if you jump just before throwing.": "Se salti appena prima di lanciarle, le bombe arrivano più in alto.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Non c'è bisogno di creare un profilo per cambiare personaggio; basta premere \nil pulsante superiore (afferra) quando prendi parte ad un gioco.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Vieni danneggiato quando sbatti la testa contro le cose.\nQuini? Quindi cerca di non sbattere la testa contro le cose.", + "Your punches do much more damage if you are running or spinning.": "I pugni fanno molti più danni se stai correndo o roteando." + } + }, + "trialPeriodEndedText": "Il periodo di prova è finito. Ti andrebbe di\ncomprare BombSquad e continuare a giocare?", + "trophiesRequiredText": "Questo richiede almeno ${NUMBER} trofei.", + "trophiesText": "Trofei", + "trophiesThisSeasonText": "Trofei Questa Stagione", + "tutorial": { + "cpuBenchmarkText": "Esecuzione tutorial a velocità assurda (principalmente analizza velocità CPU)", + "phrase01Text": "Ciao!", + "phrase02Text": "Benvenuto in ${APP_NAME}!", + "phrase03Text": "Ecco alcuni consigli su come muovere il tuo personaggio:", + "phrase04Text": "Molte cose in ${APP_NAME} sono basate sulla FISICA.", + "phrase05Text": "Ad esempio, quando tiri un pugno...", + "phrase06Text": "...il danno è calcolato in base alla velocità dei tuoi pugni.", + "phrase07Text": "Visto? Non ci stavamo muovendo, perciò ${NAME} lo ha a malapena sentito.", + "phrase08Text": "Ora saltiamo e roteiamo per ottenere più velocità.", + "phrase09Text": "Eh sì, così va meglio.", + "phrase10Text": "Anche correre aiuta.", + "phrase11Text": "Tieni premuto un tasto QUALUNQUE per correre.", + "phrase12Text": "Per pugni ancora più tosti, prova a correre E roteare.", + "phrase13Text": "Ooops! Non volevo, ${NAME}.", + "phrase14Text": "Puoi raccogliere e lanciare oggetti come le bandiere... o ${NAME}", + "phrase15Text": "Infine, ci sono le bombe.", + "phrase16Text": "Ci vuole pratica per lanciare le bombe.", + "phrase17Text": "Ahia. Non era granché come lancio.", + "phrase18Text": "Muoversi aiuta a lanciare più lontano.", + "phrase19Text": "Saltare aiuta a lanciare più in alto.", + "phrase20Text": "Fai girare la tua bomba per lanci ancora più lunghi.", + "phrase21Text": "Capire quando lanciare può essere difficile.", + "phrase22Text": "Uff...", + "phrase23Text": "Prova a far consumare la miccia per un secondo o due.", + "phrase24Text": "Così si fa! Cotto a puntino.", + "phrase25Text": "Be', questo è tutto.", + "phrase26Text": "Ora va' e colpisci, campione!", + "phrase27Text": "Ricorda il tuo addestramento, e tornerai indietro vivo!", + "phrase28Text": "...be', forse...", + "phrase29Text": "Buona fortuna!", + "randomName1Text": "Fred", + "randomName2Text": "Harry", + "randomName3Text": "Bill", + "randomName4Text": "Chuck", + "randomName5Text": "Phil", + "skipConfirmText": "Sicuro di voler saltare il tutorial? Tocca o premi per confermare.", + "skipVoteCountText": "${COUNT} su ${TOTAL} vogliono saltare la guida", + "skippingText": "guida saltata", + "toSkipPressAnythingText": "(tocca o premi qualsiasi pulsante per saltare la guida)" + }, + "twoKillText": "DOPPIA UCCISIONE!", + "unavailableText": "non disponibile", + "unconfiguredControllerDetectedText": "Rilevato controller non configurato:", + "unlockThisInTheStoreText": "Deve essere sbloccato nel negozio", + "unlockThisProfilesText": "Per creare più di ${NUM} profili, ti serve:", + "unlockThisText": "Per sbloccare questo, hai bisogno di:", + "unsupportedHardwareText": "Purtroppo, questo hardware non è supportato da questa versione del gioco.", + "upFirstText": "Per primo:", + "upNextText": "Fra poco nel ${COUNT}:", + "updatingAccountText": "Sto aggiornando il tuo account...", + "upgradeText": "Aggiorna", + "upgradeToPlayText": "Sblocca \"${PRO}\" nel negozio in-game per poter giocare a questo.", + "useDefaultText": "Usa Predefinito", + "usesExternalControllerText": "Questo gioco utilizza un controller esterno come input.", + "usingItunesText": "Sto usando una app musicale per la colonna sonora...", + "usingItunesTurnRepeatAndShuffleOnText": "Per favore, assicurati che la riproduzione casuale sia ATTIVA e che la ripetizione sia su TUTTO su iTunes.", + "validatingBetaText": "Sto convalidando la beta...", + "validatingTestBuildText": "Convalida Build Di Prova...", + "victoryText": "Vittoria!", + "voteDelayText": "Non puoi cominciare un'altra votazione per ${NUMBER} secondi", + "voteInProgressText": "C'è già una votazione in corso.", + "votedAlreadyText": "Hai già votato", + "votesNeededText": "Servono ${NUMBER} voti", + "vsText": "vs.", + "waitingForHostText": "(in attesa di ${HOST} per continuare)", + "waitingForLocalPlayersText": "in attesa di giocatori...", + "waitingForPlayersText": "in attesa di giocatori...", + "waitingInLineText": "Aspettando in coda (il party è pieno)..", + "watchAVideoText": "Guarda un video", + "watchAnAdText": "Guarda una pubblicità", + "watchWindow": { + "deleteConfirmText": "Vuoi cancellare \"${REPLAY}\"?", + "deleteReplayButtonText": "Cancella\nReplay", + "myReplaysText": "I Miei Replay", + "noReplaySelectedErrorText": "Nessun Replay Selezionato", + "playbackSpeedText": "Velocità Replay: ${SPEED}", + "renameReplayButtonText": "Rinomina\nReplay", + "renameReplayText": "Rinomina \"${REPLAY}\" in:", + "renameText": "Rinomina", + "replayDeleteErrorText": "Errore cancellazione replay.", + "replayNameText": "Nome Replay", + "replayRenameErrorAlreadyExistsText": "Esiste già un replay con questo nome.", + "replayRenameErrorInvalidName": "Impossibile rinominare replay; nome invalido.", + "replayRenameErrorText": "Errore rinominazione replay", + "sharedReplaysText": "Replays Condivisi", + "titleText": "Guarda", + "watchReplayButtonText": "Guarda\nReplay" + }, + "waveText": "Ondata", + "wellSureText": "Beh, certo!", + "wiimoteLicenseWindow": { + "licenseText": "Copyright (c) 2007, DarwiinRemote Team\nAll rights reserved.\n\n Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n3. Neither the name of this project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.", + "licenseTextScale": 0.62, + "titleText": "Copyright DarwiinRemote" + }, + "wiimoteListenWindow": { + "listeningText": "In attesa dei Wiimote...", + "pressText": "Premi contemporaneamente i tasti 1 e 2 sul Wiimote.", + "pressText2": "Nei nuovi Wiimote con il Motion Plus integrato, premi invece il tasto rosso 'sync' sul retro.", + "pressText2Scale": 0.55, + "pressTextScale": 1.0 + }, + "wiimoteSetupWindow": { + "copyrightText": "Copyright DarwiinRemote", + "copyrightTextScale": 0.6, + "listenText": "Ascolta", + "macInstructionsText": "Assicurati che la tua Wii sia spenta e il Bluetooth acceso\nsul tuo Mac, poi premi 'Ascolta'. Il supporto a Wiimote può\nessere un po' traballante, quindi prova ad aspettare qualche\nminuto prima di ottenere una connessione.\n\nVia Bluetooth dovresti riuscire a gestire fino a 7 dispositivi connessi,\nanche se le prestazioni potrebbero variare.\n\nBombSquad supporta i controller originali Wiimote, Nunchuk,\ne il Controller Classico.\nAnche i nuovi controller Wii Remote Plus funzionano, ma\nnon con gli accessori.", + "macInstructionsTextScale": 0.7, + "thanksText": "Grazie al team DarwiiRemote\nper aver reso possibile tutto questo.", + "thanksTextScale": 0.8, + "titleText": "Configurazione Wiimote" + }, + "winsPlayerText": "${NAME} Ha Vinto!", + "winsTeamText": "${NAME} Ha Vinto!", + "winsText": "${NAME} vince!", + "worldScoresUnavailableText": "Punteggi mondiali non disponibili.", + "worldsBestScoresText": "I punteggi migliori del mondo", + "worldsBestTimesText": "I tempi migliori del mondo", + "xbox360ControllersWindow": { + "getDriverText": "Scarica il driver", + "macInstructions2Text": "Per usare i controller in modalità wireless, avrai anche bisogno del\nricevitore contenuto nel pacchetto 'Xbox 360 controller wireless per\nWindows'. Un ricevitore permette di collegare fino a 4 controller.\n\nImportante: i ricevitori di terze parti non funzioneranno con questo driver;\nassicurati di avere un ricevitore 'Microsoft', non 'XBOX 360'.\nMicrosoft non li vende più separatamente, quindi avrai bisogno di quello\nfornito con il controller, oppure cerca su ebay.\n\nSe lo hai trovato utile, considera una donazione allo sviluppatore del\ndriver al suo sito.", + "macInstructions2TextScale": 0.76, + "macInstructionsText": "Per usare il controller dell'Xbox 360, avrai bisogno di installare\ni driver per MAC disponibili cliccando sul link qui sotto.\nFunziona sia con i controller senza fili che con i via cavo.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "Per usare i controller dell'Xbox 360 con Bombsquad, semplicemente\ncollegali alla porta USB. Puoi usare un hub USB per connettere\npiù controller.\n\nPer usare i controller wireless hai bisogno di un ricevitore\nwireless, disponibile come parte del pacchetto \"Xbox 360 controller\nwireless per Windows\", venduto separatamente. Ogni ricevitore va\ninserito nella porta USB e permette di connettere fino a 4 controller.", + "ouyaInstructionsTextScale": 0.8, + "titleText": "Come usare i controller dell'Xbox 360 con ${APP_NAME}:" + }, + "yesAllowText": "Sì, permetti!", + "yourBestScoresText": "I tuoi punteggi migliori", + "yourBestTimesText": "I tuoi tempi migliori" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/korean.json b/dist/ba_data/data/languages/korean.json new file mode 100644 index 0000000..6fb8298 --- /dev/null +++ b/dist/ba_data/data/languages/korean.json @@ -0,0 +1,1789 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "계정 이름은 이모티콘이나 특수문자를 포함할수 없습니다.", + "accountProfileText": "계정 프로파일", + "accountsText": "계정", + "achievementProgressText": "업적: ${TOTAL} 중 ${COUNT}", + "campaignProgressText": "캠페인 진행 상황 [어려움]: ${PROGRESS}", + "changeOncePerSeason": "한 시즌마다 바꿀수 있습니다.", + "changeOncePerSeasonError": "다음 시즌으로 바뀔때까지 (${NUM} 일)기다려야 계정 이름을 바꿀수 있습니다.", + "customName": "이름 맞춤설정", + "linkAccountsEnterCodeText": "코드 입력", + "linkAccountsGenerateCodeText": "코드 생성", + "linkAccountsInfoText": "(여러 플랫폼에서 진행 상황을 공유합니다)", + "linkAccountsInstructionsText": "두 개의 계정을 연동하려면 한 곳에서 코드를\n생성한 후 다른 곳에서 해당 코드를 입력합니다.\n진행 상황 및 소지품은 결합됩니다.\n최대 ${COUNT}개의 계정을 연동할 수 있습니다.\n\n중요:당신의 계정만 연동하십시오!\n만약에 당신이 친구의 계정과 연동하면 친구가 게임\n하는 동안 당신은 게임 할수 없게 됩니다!\n\n이 작업은 취소할 수 없으므로 주의하세요!", + "linkAccountsText": "계정 연동", + "linkedAccountsText": "연동된 계정", + "nameChangeConfirm": "계정이름을 ${NAME}으로 바꾸시겠습니까?", + "resetProgressConfirmNoAchievementsText": "이 옵션은 협동 모드 진행 상황 및 로컬\n최고 점수를 초기화합니다 (티켓 제외).\n이 작업은 취소할 수 없습니다. 괜찮습니까?", + "resetProgressConfirmText": "이 옵션은 협동 모드 진행 상황, 업적 및\n로컬 최고 점수를 초기화합니다\n(티켓 제외). 이 작업은 취소할 수 없습니다.\n괜찮습니까?", + "resetProgressText": "진행 상황 초기화", + "setAccountName": "계정 이름 설정", + "signInInfoText": "로그인해서 티켓을 수집하고 온라인으로 겨루며\n여러 기기에서 진행 상황을 공유하세요.", + "signInText": "로그인", + "signInWithDeviceInfoText": "(자동 계정은 이 기기에서만 이용할 수 있습니다)", + "signInWithDeviceText": "기기 계정으로 로그인", + "signInWithGameCircleText": "Game Circle로 로그인", + "signInWithGooglePlayText": "Google Play로 로그인", + "signInWithTestAccountInfoText": "(이전 계정 유형, 앞의 기기 계정을 이용하세요)", + "signInWithTestAccountText": "테스트 계정으로 로그인", + "signOutText": "로그아웃", + "signingInText": "로그인 중...", + "signingOutText": "로그아웃 중...", + "testAccountWarningText": "테스트 계정은 초기화될 염려가 있습니다.\n(이 계정을 쓸때는 제발 많은 시간이나 상품을 구입하지 말아주세요).\n\n진짜 계정을 쓰시면( Game-Center, 구글 플러스 Google Plus, 기타등등)\n계정을 다른 기기에서 불러올 수 있습니다).", + "ticketsText": "티켓: ${COUNT}", + "titleText": "계정", + "unlinkAccountsInstructionsText": "계정 연동을 해제할 계정을 선택하세요", + "unlinkAccountsText": "계정 연동해제", + "viaAccount": "(계정 종류 ${NAME})", + "youAreSignedInAsText": "현재 로그인된 사용자 이름:" + }, + "achievementChallengesText": "업적 챌린지", + "achievementText": "업적", + "achievements": { + "Boom Goes the Dynamite": { + "description": "TNT로 악당 3명을 처치합니다", + "descriptionComplete": "TNT로 악당 3명을 처치했습니다", + "descriptionFull": "${LEVEL}에서 TNT로 악당 3명을 처치합니다", + "descriptionFullComplete": "${LEVEL}에서 TNT로 악당 3명을 처치했습니다", + "name": "다이너마이트 대폭발" + }, + "Boxer": { + "description": "폭탄을 사용하지 않고 승리합니다", + "descriptionComplete": "폭탄을 사용하지 않고 승리했습니다", + "descriptionFull": "폭탄을 사용하지 않고 ${LEVEL} 레벨을 완료합니다", + "descriptionFullComplete": "폭탄을 사용하지 않고 ${LEVEL} 레벨을 완료했습니다", + "name": "권투 선수" + }, + "Dual Wielding": { + "descriptionFull": "컨트롤러를 두개 연결합니다. (하드웨어 밎 앱)", + "descriptionFullComplete": "컨트롤러를 두개 연결했습니다. (하드웨어 밎 앱)", + "name": "콩까는 컨트롤러" + }, + "Flawless Victory": { + "description": "피해를 입지 않고 승리합니다", + "descriptionComplete": "피해를 입지 않고 승리했습니다", + "descriptionFull": "${LEVEL}에서 피해를 입지 않고 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 피해를 입지 않고 승리했습니다", + "name": "완벽한 승리" + }, + "Free Loader": { + "descriptionFull": "개인전을 두명 이상으로 시작합니다.", + "descriptionFullComplete": "개인전을 두명 이상으로 시작했습니다.", + "name": "격투가" + }, + "Gold Miner": { + "description": "지뢰로 악당 6명을 처치합니다", + "descriptionComplete": "지뢰로 악당 6명을 처치했습니다", + "descriptionFull": "${LEVEL}에서 지뢰로 악당 6명을 처치합니다", + "descriptionFullComplete": "${LEVEL}에서 지뢰로 악당 6명을 처치했습니다", + "name": "금광 광부" + }, + "Got the Moves": { + "description": "펀치 또는 폭탄을 사용하지 않고 승리합니다", + "descriptionComplete": "펀치 또는 폭탄을 사용하지 않고 승리했습니다", + "descriptionFull": "${LEVEL}에서 펀치 또는 폭탄을 사용하지 않고 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 펀치 또는 폭탄을 사용하지 않고 승리했습니다", + "name": "뛰어난 움직임" + }, + "In Control": { + "descriptionFull": "컨트롤러를 연결합니다. (하드웨어 밎 앱)", + "descriptionFullComplete": "컨트롤러를 연결했습니다. (하드웨어 밎 앱)", + "name": "에네미 컨트롤러!" + }, + "Last Stand God": { + "description": "1000점을 기록합니다", + "descriptionComplete": "1000점을 기록했습니다", + "descriptionFull": "${LEVEL}에서 1000점을 기록합니다", + "descriptionFullComplete": "${LEVEL}에서 1000점을 기록했습니다", + "name": "${LEVEL}의 신" + }, + "Last Stand Master": { + "description": "250점을 기록합니다", + "descriptionComplete": "250점을 기록했습니다", + "descriptionFull": "${LEVEL}에서 250점을 기록합니다", + "descriptionFullComplete": "${LEVEL}에서 250점을 기록했습니다", + "name": "${LEVEL}의 달인" + }, + "Last Stand Wizard": { + "description": "500점을 기록합니다", + "descriptionComplete": "500점을 기록했습니다", + "descriptionFull": "${LEVEL}에서 500점을 기록합니다", + "descriptionFullComplete": "${LEVEL}에서 500점을 기록했습니다", + "name": "${LEVEL}의 현인" + }, + "Mine Games": { + "description": "지뢰로 악당 3명을 처치합니다", + "descriptionComplete": "지뢰로 악당 3명을 처치했습니다", + "descriptionFull": "${LEVEL}에서 지뢰로 악당 3명을 처치합니다", + "descriptionFullComplete": "${LEVEL}에서 지뢰로 악당 3명을 처치했습니다", + "name": "지뢰 유희" + }, + "Off You Go Then": { + "description": "지도 밖으로 악당 3명을 내던집니다", + "descriptionComplete": "지도 밖으로 악당 3명을 내던졌습니다", + "descriptionFull": "${LEVEL}에서 지도 밖으로 악당 3명을 내던집니다", + "descriptionFullComplete": "${LEVEL}에서 지도 밖으로 악당 3명을 내던졌습니다", + "name": "먼저 나가" + }, + "Onslaught God": { + "description": "5000점을 기록합니다", + "descriptionComplete": "5000점을 기록했습니다", + "descriptionFull": "${LEVEL}에서 5000점을 기록합니다", + "descriptionFullComplete": "${LEVEL}에서 5000점을 기록했습니다", + "name": "${LEVEL}의 신" + }, + "Onslaught Master": { + "description": "500점을 기록합니다", + "descriptionComplete": "500점을 기록했습니다", + "descriptionFull": "${LEVEL}에서 500점을 기록합니다", + "descriptionFullComplete": "${LEVEL}에서 500점을 기록했습니다", + "name": "${LEVEL}의 달인" + }, + "Onslaught Training Victory": { + "description": "모든 웨이브를 물리칩니다", + "descriptionComplete": "모든 웨이브를 물리쳤습니다", + "descriptionFull": "${LEVEL}에서 모든 웨이브를 물리칩니다", + "descriptionFullComplete": "${LEVEL}에서 모든 웨이브를 물리쳤습니다", + "name": "${LEVEL} 승리" + }, + "Onslaught Wizard": { + "description": "1000점을 기록합니다", + "descriptionComplete": "1000점을 기록했습니다", + "descriptionFull": "${LEVEL}에서 1000점을 기록합니다", + "descriptionFullComplete": "${LEVEL}에서 1000점을 기록했습니다", + "name": "${LEVEL}의 현인" + }, + "Precision Bombing": { + "description": "파워업을 사용하지 않고 승리합니다", + "descriptionComplete": "파워업을 사용하지 않고 승리했습니다", + "descriptionFull": "${LEVEL}에서 파워업을 사용하지 않고 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 파워업을 사용하지 않고 승리했습니다", + "name": "정밀 폭격" + }, + "Pro Boxer": { + "description": "폭탄을 사용하지 않고 승리합니다", + "descriptionComplete": "폭탄을 사용하지 않고 승리했습니다", + "descriptionFull": "폭탄을 사용하지 않고 ${LEVEL} 레벨을 완료합니다", + "descriptionFullComplete": "폭탄을 사용하지 않고 ${LEVEL} 레벨을 완료했습니다", + "name": "프로 권투 선수" + }, + "Pro Football Shutout": { + "description": "악당들에게 점수를 주지 않고 승리합니다", + "descriptionComplete": "악당들에게 점수를 주지 않고 승리했습니다", + "descriptionFull": "${LEVEL}에서 악당들에게 점수를 주지 않고 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 악당들에게 점수를 주지 않고 승리했습니다", + "name": "${LEVEL} 완봉승" + }, + "Pro Football Victory": { + "description": "게임을 승리합니다", + "descriptionComplete": "게임을 승리했습니다", + "descriptionFull": "${LEVEL}에서 게임을 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 게임을 승리했습니다", + "name": "${LEVEL} 승리" + }, + "Pro Onslaught Victory": { + "description": "모든 웨이브를 물리칩니다", + "descriptionComplete": "모든 웨이브를 물리쳤습니다", + "descriptionFull": "${LEVEL}의 모든 웨이브를 물리칩니다", + "descriptionFullComplete": "${LEVEL}의 모든 웨이브를 물리쳤습니다", + "name": "${LEVEL} 승리" + }, + "Pro Runaround Victory": { + "description": "모든 웨이브를 완료합니다", + "descriptionComplete": "모든 웨이브를 완료했습니다", + "descriptionFull": "${LEVEL}에서 모든 웨이브를 완료합니다", + "descriptionFullComplete": "${LEVEL}에서 모든 웨이브를 완료했습니다", + "name": "${LEVEL} 승리" + }, + "Rookie Football Shutout": { + "description": "악당들에게 점수를 주지 않고 승리합니다", + "descriptionComplete": "악당들에게 점수를 주지 않고 승리했습니다", + "descriptionFull": "${LEVEL}에서 악당들에게 점수를 주지 않고 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 악당들에게 점수를 주지 않고 승리했습니다", + "name": "${LEVEL} 완봉승" + }, + "Rookie Football Victory": { + "description": "게임을 승리합니다", + "descriptionComplete": "게임을 승리했습니다", + "descriptionFull": "${LEVEL}에서 게임을 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 게임을 승리했습니다", + "name": "${LEVEL} 승리" + }, + "Rookie Onslaught Victory": { + "description": "모든 웨이브를 물리칩니다", + "descriptionComplete": "모든 웨이브를 물리쳤습니다", + "descriptionFull": "${LEVEL}에서 모든 웨이브를 물리칩니다", + "descriptionFullComplete": "${LEVEL}에서 모든 웨이브를 물리쳤습니다", + "name": "${LEVEL} 승리" + }, + "Runaround God": { + "description": "2000점을 기록합니다", + "descriptionComplete": "2000점을 기록했습니다", + "descriptionFull": "${LEVEL}에서 2000점을 기록합니다", + "descriptionFullComplete": "${LEVEL}에서 2000점을 기록했습니다", + "name": "${LEVEL}의 신" + }, + "Runaround Master": { + "description": "500점을 기록합니다", + "descriptionComplete": "500점을 기록했습니다", + "descriptionFull": "${LEVEL}에서 500점을 기록합니다", + "descriptionFullComplete": "${LEVEL}에서 500점을 기록했습니다", + "name": "${LEVEL}의 달인" + }, + "Runaround Wizard": { + "description": "1000점을 기록합니다", + "descriptionComplete": "1000점을 기록했습니다", + "descriptionFull": "${LEVEL}에서 1000점을 기록합니다", + "descriptionFullComplete": "${LEVEL}에서 1000점을 기록했습니다", + "name": "${LEVEL}의 현인" + }, + "Sharing is Caring": { + "descriptionFull": "게임을 친구와 성공적으로 공유합니다", + "descriptionFullComplete": "게임을 친구와 성공적으로 공유했습니다", + "name": "백지장도 맞들면 낫다" + }, + "Stayin' Alive": { + "description": "죽지 않고 승리합니다", + "descriptionComplete": "죽지 않고 승리했습니다", + "descriptionFull": "${LEVEL}에서 죽지 않고 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 죽지 않고 승리했습니다", + "name": "생존 본능" + }, + "Super Mega Punch": { + "description": "한 번의 펀치로 100% 대미지를 가합니다", + "descriptionComplete": "한 번의 펀치로 100% 대미지를 가했습니다", + "descriptionFull": "${LEVEL}에서 한 번의 펀치로 100% 대미지를 가합니다", + "descriptionFullComplete": "${LEVEL}에서 한 번의 펀치로 100% 대미지를 가했습니다", + "name": "슈퍼 메가 펀치" + }, + "Super Punch": { + "description": "한 번의 펀치로 50% 대미지를 가합니다", + "descriptionComplete": "한 번의 펀치로 50% 대미지를 가했습니다", + "descriptionFull": "${LEVEL}에서 한 번의 펀치로 50% 대미지를 가합니다", + "descriptionFullComplete": "${LEVEL}에서 한 번의 펀치로 50% 대미지를 가했습니다", + "name": "슈퍼 펀치" + }, + "TNT Terror": { + "description": "TNT로 악당 6명을 처치합니다", + "descriptionComplete": "TNT로 악당 6명을 처치했습니다", + "descriptionFull": "${LEVEL}에서 TNT로 악당 6명을 처치합니다", + "descriptionFullComplete": "${LEVEL}에서 TNT로 악당 6명을 처치했습니다", + "name": "TNT 테러" + }, + "Team Player": { + "descriptionFull": "4명 이상과 같이 팀전을 합니다", + "descriptionFullComplete": "4명 이상과 같이 팀전을 하였습니다.", + "name": "팀전 플레이어" + }, + "The Great Wall": { + "description": "모든 악당을 저지합니다", + "descriptionComplete": "모든 악당을 저지했습니다", + "descriptionFull": "${LEVEL}에서 모든 악당을 저지합니다", + "descriptionFullComplete": "${LEVEL}에서 모든 악당을 저지했습니다", + "name": "만리장성" + }, + "The Wall": { + "description": "모든 악당을 저지합니다", + "descriptionComplete": "모든 악당을 저지했습니다", + "descriptionFull": "${LEVEL}에서 모든 악당을 저지합니다", + "descriptionFullComplete": "${LEVEL}에서 모든 악당을 저지했습니다", + "name": "벽" + }, + "Uber Football Shutout": { + "description": "악당들에게 점수를 주지 않고 승리합니다", + "descriptionComplete": "악당들에게 점수를 주지 않고 승리했습니다", + "descriptionFull": "${LEVEL}에서 악당들에게 점수를 주지 않고 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 악당들에게 점수를 주지 않고 승리했습니다", + "name": "${LEVEL} 완봉승" + }, + "Uber Football Victory": { + "description": "게임을 승리합니다", + "descriptionComplete": "게임을 승리했습니다", + "descriptionFull": "${LEVEL}에서 게임을 승리합니다", + "descriptionFullComplete": "${LEVEL}에서 게임을 승리했습니다", + "name": "${LEVEL} 승리" + }, + "Uber Onslaught Victory": { + "description": "모든 웨이브를 물리칩니다", + "descriptionComplete": "모든 웨이브를 물리쳤습니다", + "descriptionFull": "${LEVEL}에서 모든 웨이브를 물리칩니다", + "descriptionFullComplete": "${LEVEL}에서 모든 웨이브를 물리쳤습니다", + "name": "${LEVEL} 승리" + }, + "Uber Runaround Victory": { + "description": "모든 웨이브를 완료합니다", + "descriptionComplete": "모든 웨이브를 완료했습니다", + "descriptionFull": "${LEVEL}에서 모든 웨이브를 완료합니다", + "descriptionFullComplete": "${LEVEL}에서 모든 웨이브를 완료했습니다", + "name": "${LEVEL} 승리" + } + }, + "achievementsRemainingText": "남은 업적:", + "achievementsText": "업적", + "achievementsUnavailableForOldSeasonsText": "죄송합니다만 이전 시즌에 대해서는 업적 정보가 제공되지 않습니다.", + "addGameWindow": { + "getMoreGamesText": "다른 게임 보기...", + "titleText": "게임 추가" + }, + "allowText": "허용", + "alreadySignedInText": "귀하의 계정은 다른 기기에서 로그인되었습니다. \n계정을 전환하거나 다른 기기에서 게임을 종료하고 \n다시 시도하십시오.", + "apiVersionErrorText": "${NAME} 모듈을 불러올 수 없습니다; ${VERSION_USED} api 버전입니다; ${VERSION_REQUIRED} 버전이 필요합니다.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(헤드폰이 연결된 때에만 '자동'이 이 옵션을 활성화합니다)", + "headRelativeVRAudioText": "머리 비례 VR 오디오", + "musicVolumeText": "음악 볼륨", + "soundVolumeText": "사운드 볼륨", + "soundtrackButtonText": "사운드트랙", + "soundtrackDescriptionText": "(게임 중 재생할 사용자의 음악을 배정합니다)", + "titleText": "오디오" + }, + "autoText": "자동", + "backText": "뒤로", + "banThisPlayerText": "이 플레이어를 밴한다", + "bestOfFinalText": "베스트 ${COUNT} 최종 점수", + "bestOfSeriesText": "Best of ${COUNT} series:", + "bestRankText": "귀하의 최고 순위: #${RANK}", + "bestRatingText": "귀하의 최고 등급: ${RATING}", + "bombBoldText": "폭탄", + "bombText": "폭탄", + "boostText": "증가", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME}(은)는 앱 자체에 구성되어 있습니다.", + "buttonText": "버튼", + "canWeDebugText": "BombSquad가 버그, 충돌 및 기본 사용 정보를\n자동으로 개발자에게 신고하도록 하시겠습니까?\n\n이 데이터에 개인 정보는 포함되지 않으며 게임이\n버그 없이 원활하게 실행되도록 하는 데 도움이 됩니다.", + "cancelText": "취소", + "cantConfigureDeviceText": "죄송합니다만 ${DEVICE}(은)는 구성할 수 없습니다.", + "challengeEndedText": "이 챌린지는 종료되었습니다.", + "chatMuteText": "채팅 음소거", + "chatMutedText": "채팅 음소거됨.", + "chatUnMuteText": "채팅 음소거 해제", + "choosingPlayerText": "<플레이어 선택>", + "completeThisLevelToProceedText": "계속 진행하려면 이 레벨을\n완료해야 합니다!", + "completionBonusText": "완료 보너스", + "configControllersWindow": { + "configureControllersText": "컨트롤러 구성", + "configureKeyboard2Text": "키보드 P2 구성", + "configureKeyboardText": "키보드 구성", + "configureMobileText": "모바일 기기를 컨트롤러로 사용", + "configureTouchText": "터치스크린 구성", + "ps3Text": "플레이스테이션 3 컨트롤러", + "titleText": "컨트롤러", + "wiimotesText": "Wii 리모컨", + "xbox360Text": "Xbox 360 컨트롤러" + }, + "configGamepadSelectWindow": { + "androidNoteText": "참고: 기기 및 안드로이드 버전에 따라 컨트롤러 지원이 다릅니다.", + "pressAnyButtonText": "구성하고 싶은 컨트롤러의\n 버튼을 아무거나 누르세요...", + "titleText": "컨트롤러 구성" + }, + "configGamepadWindow": { + "advancedText": "고급", + "advancedTitleText": "고급 컨트롤러 설정", + "analogStickDeadZoneDescriptionText": "(스틱을 놓을 때 캐릭터가 '표류'할 경우 이 옵션을 켭니다)", + "analogStickDeadZoneText": "아날로그 스틱 데드 존", + "appliesToAllText": "(이 유형의 모든 컨트롤러에 적용됩니다)", + "autoRecalibrateDescriptionText": "(캐릭터가 전속력으로 움직이지 않으면 이 옵션을 사용합니다)", + "autoRecalibrateText": "아날로그 스틱 자동 재보정", + "axisText": "축", + "clearText": "지우기", + "dpadText": "D-패드", + "extraStartButtonText": "보조 시작 버튼", + "ifNothingHappensTryAnalogText": "아무 일도 발생하지 않으면 아날로그 스틱을 대신 배정해보세요.", + "ifNothingHappensTryDpadText": "아무 일도 발생하지 않으면 D-패드를 대신 배정해보세요.", + "ignoreCompletelyDescriptionText": "(이 컨트롤러를 게임 또는 메뉴에 영향을 주지 않게 함)", + "ignoreCompletelyText": "완전히 무시하기", + "ignoredButton1Text": "무시된 버튼 1", + "ignoredButton2Text": "무시된 버튼 2", + "ignoredButton3Text": "무시된 버튼 3", + "ignoredButton4Text": "4번 버튼을 무시함", + "ignoredButtonDescriptionText": "('홈' 또는 '동기화' 버튼이 사용자 인터페이스에 영향을 주지 않도록 하려면 이 옵션을 사용합니다)", + "pressAnyAnalogTriggerText": "아무 아날로그 트리거나 누르세요...", + "pressAnyButtonOrDpadText": "아무 버튼 또는 D-패드를 누르세요...", + "pressAnyButtonText": "아무 버튼이나 누르세요...", + "pressLeftRightText": "왼쪽 또는 오른쪽을 누르세요...", + "pressUpDownText": "위쪽 또는 아래쪽을 누르세요...", + "runButton1Text": "달리기 버튼 1", + "runButton2Text": "달리기 버튼 2", + "runTrigger1Text": "달리기 트리거 1", + "runTrigger2Text": "달리기 트리거 2", + "runTriggerDescriptionText": "(다양한 속도에서 달릴 수 있게 하는 아날로그 트리거)", + "secondHalfText": "이 옵션을 사용해서 단일 컨트롤러로\n표시되는 투-인-원 컨트롤러\n장치의 두 번째 설정을 구성합니다.", + "secondaryEnableText": "활성화", + "secondaryText": "보조 컨트롤러", + "startButtonActivatesDefaultDescriptionText": "(시작 버튼이 '메뉴' 버튼 기능 이상일 경우 이 옵션을 끕니다)", + "startButtonActivatesDefaultText": "시작 버튼은 기본 위젯을 활성화합니다", + "titleText": "컨트롤러 설정", + "twoInOneSetupText": "투-인-원 컨트롤러 설정", + "uiOnlyDescriptionText": "(이 컨트롤러를 게임에 참여하지 못하게 하기)", + "uiOnlyText": "메뉴에만 사용", + "unassignedButtonsRunText": "배정되지 않은 모든 버튼으로 달리기", + "unsetText": "<미설정>", + "vrReorientButtonText": "VR 방향 재설정 버튼" + }, + "configKeyboardWindow": { + "configuringText": "${DEVICE} 구성 중", + "keyboard2NoteText": "주의\n대부분의 키보드는 한번에 많은 양의 키를 인식하지 못하므로\n2인 플레이시 다른 키보드를 연결하여 플레이하는 것이 더 나을 수도 있습니다.\n단, 이 경우에도 각 플레이어에게 서로 다른 키배치를 배정해야\n게임이 정상적으로 됩니다." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "액션 컨트롤 눈금", + "actionsText": "액션", + "buttonsText": "버튼", + "dragControlsText": "< 재배치하려면 컨트롤을 드래그하세요 >", + "joystickText": "조이스틱", + "movementControlScaleText": "이동 컨트롤 눈금", + "movementText": "이동", + "resetText": "재설정", + "swipeControlsHiddenText": "스와이프 아이콘 숨기기", + "swipeInfoText": "'스와이프' 스타일 컨트롤은 익숙해지는 데 시간이 약간\n걸리지만 컨트롤을 보지 않고 플레이할 때 더 편합니다.", + "swipeText": "스와이프", + "titleText": "터치스크린 구성" + }, + "configureItNowText": "지금 구성하시겠습니까?", + "configureText": "구성", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "최선의 결과를 얻으려면 랙이 없는 Wi-Fi 네트워크가 필요합니다. \n다른 무선 장치들을 끄거나 Wi-Fi 라우터 근처에서 플레이하거나\n게임 호스트를 이더넷 네트워크에 직접 연결해서\nWi-Fi 랙을 줄일 수 있습니다.", + "explanationText": "스마트폰 또는 태블릿을 무선 컨트롤러로 사용하려면\n해당 기기에 \"${REMOTE_APP_NAME}\" 앱을 설치합니다. Wi-Fi를 통해서\n여러 기기를 ${APP_NAME} 게임에 무료로 연결할 수 있습니다!", + "forAndroidText": "안드로이드:", + "forIOSText": "iOS:", + "getItForText": "${REMOTE_APP_NAME} 앱의 iOS 버전은 Apple App Store에서, Android\n버전은 Google Play Store 또는 Amazon Appstore에서 다운로드할 수 있습니다", + "googlePlayText": "Google Play", + "titleText": "모바일 기기를 컨트롤러로 사용하기:" + }, + "continuePurchaseText": "${PRICE}에 계속하시겠습니까?", + "continueText": "계속", + "controlsText": "컨트롤", + "coopSelectWindow": { + "activenessAllTimeInfoText": "이 옵션은 역대 랭킹에는 적용되지 않습니다.", + "activenessInfoText": "이 증배율은 플레이할 때 올라가고\n플레이를 안 하면 내려갑니다.", + "activityText": "활동", + "campaignText": "캠페인", + "challengesInfoText": "미니 게임을 완료하고 상품을 받으세요.\n\n상품 및 난이도는 챌린지를 완료할 때마다 \n올라가고 만료되거나 포기하면\n내려갑니다.", + "challengesText": "챌린지", + "currentBestText": "현재 최고 점수", + "customText": "커스텀", + "entryFeeText": "참가", + "forfeitConfirmText": "이 챌린지를 포기하시겠습니까?", + "forfeitNotAllowedYetText": "이 챌린지는 아직 포기할 수 없습니다.", + "forfeitText": "포기", + "multipliersText": "증배율", + "nextChallengeText": "다음 챌린지", + "nextPlayText": "다음 플레이", + "ofTotalTimeText": "/ ${TOTAL}", + "playNowText": "지금 시작", + "pointsText": "점수", + "powerRankingFinishedSeasonUnrankedText": "(랭크 없이 완료된 시즌)", + "powerRankingNotInTopText": "(톱 ${NUMBER} 밖)", + "powerRankingPointsEqualsText": "= ${NUMBER}점", + "powerRankingPointsMultText": "(x ${NUMBER}점)", + "powerRankingPointsText": "${NUMBER}점", + "powerRankingPointsToRankedText": "(${CURRENT} / ${REMAINING}점)", + "powerRankingText": "파워 랭킹", + "prizesText": "상품", + "proMultInfoText": "${PRO} 업그레이드를 구입한 플레이어들은\n이곳에서 ${PERCENT}% 점수 보너스를 받습니다.", + "seeMoreText": "더 보기", + "skipWaitText": "대기 건너뛰기", + "timeRemainingText": "남은 시간", + "toRankedText": "랭킹까지", + "totalText": "합계", + "tournamentInfoText": "자신의 리그에서 다른 플레이어들과\n최고 점수를 겨루세요.\n\n토너먼트가 종료될 때 랭킹에 오른\n플레이어들에게 상품이 수여됩니다.", + "welcome1Text": "${LEAGUE}에 환영합니다. 별 등급을 획득하고\n업적을 완료하며 토너먼트에서 트로피를 획득해서\n리그 등급을 올릴 수 있습니다.", + "welcome2Text": "또한 많은 동일한 활동에서 티켓을 획득할 수 있습니다.\n티켓을 사용해서 새로운 캐릭터, 지도 및 미니 게임을\n잠금 해제하고 토너먼트에 참가할 수 있습니다.", + "yourPowerRankingText": "내 파워 랭킹:" + }, + "copyOfText": "${NAME} 사본", + "createEditPlayerText": "<플레이어 생성/편집>", + "createText": "생성", + "creditsWindow": { + "additionalAudioArtIdeasText": "추가 오디오, 초기 아트워크 및 아이디어: ${NAME}", + "additionalMusicFromText": "추가 음악: ${NAME}", + "allMyFamilyText": "게임 테스트를 도와준 모든 친구 및 가족", + "codingGraphicsAudioText": "코딩, 그래픽 및 오디오: ${NAME}", + "languageTranslationsText": "언어 번역:", + "legalText": "법무:", + "publicDomainMusicViaText": "공개 음악: ${NAME}", + "softwareBasedOnText": "이 소프트웨어는 ${NAME}의 작품에 일부 기반하고 있습니다", + "songCreditText": "${TITLE} 연주: ${PERFORMER}\n작곡: ${COMPOSER}, 편곡: ${ARRANGER}, 발표: ${PUBLISHER},\n제공: ${SOURCE}", + "soundAndMusicText": "사운드 및 음악:", + "soundsText": "사운드 (${SOURCE}):", + "specialThanksText": "특히 감사드리는 분들:", + "thanksEspeciallyToText": "${NAME}에게 특히 감사를 드립니다", + "titleText": "${APP_NAME} 개발진", + "whoeverInventedCoffeeText": "커피를 발명하신 분" + }, + "currentStandingText": "귀하의 현재 순위: #${RANK}", + "customizeText": "커스터마이징...", + "deathsTallyText": "${COUNT}번 사망", + "deathsText": "사망", + "debugText": "디버그", + "debugWindow": { + "reloadBenchmarkBestResultsText": "참고: 테스트를 할 때에 설정->그래픽->텍스처에서 '높음'으로 설정할 것을 권장합니다.", + "runCPUBenchmarkText": "CPU 벤치마크 실행", + "runGPUBenchmarkText": "GPU 벤치마크 실행", + "runMediaReloadBenchmarkText": "미디어-리로드 벤치마크 실행", + "runStressTestText": "스트레스 테스트 실행", + "stressTestPlayerCountText": "플레이어 수", + "stressTestPlaylistDescriptionText": "플레이 목록 스트레스 테스트", + "stressTestPlaylistNameText": "플레이 목록 이름", + "stressTestPlaylistTypeText": "플레이 목록 유형", + "stressTestRoundDurationText": "라운드 지속 시간", + "stressTestTitleText": "스트레스 테스트", + "titleText": "벤치마크 및 스트레스 테스트", + "totalReloadTimeText": "총 리로드 시간: ${TIME} (로그에서 자세한 정보 참고)" + }, + "defaultGameListNameText": "기본 ${PLAYMODE} 플레이 목록", + "defaultNewGameListNameText": "내 ${PLAYMODE} 플레이 목록", + "deleteText": "삭제", + "demoText": "체험판", + "denyText": "거부", + "desktopResText": "데스크톱 해상도", + "difficultyEasyText": "쉬움", + "difficultyHardOnlyText": "어려움 모드만", + "difficultyHardText": "어려움", + "difficultyHardUnlockOnlyText": "이 레벨은 어려움 모드에서만 잠금 해제할 수 있습니다.\n준비가 되셨습니까?", + "directBrowserToURLText": "웹 브라우저에서 다음 URL을 방문하십시오:", + "disableRemoteAppConnectionsText": "리모트 앱 연결 비활성화", + "disableXInputDescriptionText": "4개 이상의 컨트롤러를 허용하지만 아마 잘 작동하지 않을 것입니다.", + "disableXInputText": "엑스인풋 컨트롤러 비활성화", + "doneText": "완료", + "drawText": "무승부", + "duplicateText": "복사", + "editGameListWindow": { + "addGameText": "게임\n추가", + "cantOverwriteDefaultText": "기본 플레이 목록은 덮어쓸 수 없습니다!", + "cantSaveAlreadyExistsText": "같은 이름의 플레이 목록이 이미 존재합니다!", + "cantSaveEmptyListText": "빈 플레이 목록을 저장할 수 없습니다!", + "editGameText": "게임\n편집", + "listNameText": "플레이 목록 이름", + "nameText": "이름", + "removeGameText": "게임\n제거", + "saveText": "목록 저장", + "titleText": "플레이 목록 편집기" + }, + "editProfileWindow": { + "accountProfileInfoText": "이 특별 프로필은 사용자의 계정에\n기반한 이름 및 아이콘을 보유합니다.\n\n${ICONS}\n\n다른 이름 또는 맞춤형 아이콘을 사용하려면\n맞춤형 프로필을 만드십시오.", + "accountProfileText": "(계정 프로필)", + "availableText": "이름 \"${NAME}\"(은)는 이용할 수 있습니다.", + "characterText": "캐릭터", + "checkingAvailabilityText": "\"${NAME}\"의 이용 가능성 확인 중...", + "colorText": "색", + "getMoreCharactersText": "캐릭터 더 보기...", + "getMoreIconsText": "아이콘 더 보기...", + "globalProfileInfoText": "글로벌 플레이어 프로필은 반드시 전 세계적으로 고유한\n이름을 갖습니다. 또한 맞춤형 아이콘이 포함됩니다.", + "globalProfileText": "(글로벌 프로필)", + "highlightText": "하이라이트", + "iconText": "아이콘", + "localProfileInfoText": "로컬 플레이어 프로필은 아이콘이 없으며 이름은 반드시\n고유하지 않습니다. 고유한 이름을 갖고 맞춤형 아이콘을\n추가하려면 글로벌 프로필로 업그레이드하세요.", + "localProfileText": "(로컬 프로필)", + "nameDescriptionText": "플레이어 이름", + "nameText": "이름", + "randomText": "무작위", + "titleEditText": "프로필 편집", + "titleNewText": "새 프로필", + "unavailableText": "\"${NAME}\"(은)는 사용할 수 없습니다. 다른 이름을 시도하세요.", + "upgradeProfileInfoText": "플레이어 이름을 전 세계적으로 유지하고\n맞춤형 아이콘을 추가할 수 있습니다.", + "upgradeToGlobalProfileText": "글로벌 프로필로 업그레이드" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "기본 사운드트랙을 삭제할 수 없습니다.", + "cantEditDefaultText": "기본 사운드트랙을 편집할 수 없습니다. 복사하거나 새로 만드세요.", + "cantOverwriteDefaultText": "기본 사운드트랙을 덮어쓸 수 없습니다", + "cantSaveAlreadyExistsText": "같은 이름의 사운드트랙이 이미 존재합니다!", + "defaultGameMusicText": "<기본 게임 음악>", + "defaultSoundtrackNameText": "기본 사운드트랙", + "deleteConfirmText": "사운드트랙 삭제:\n\n'${NAME}'?", + "deleteText": "사운드트랙\n삭제", + "duplicateText": "사운드트랙\n복사", + "editSoundtrackText": "사운드트랙 편집기", + "editText": "사운드트랙\n편집", + "fetchingITunesText": "iTunes 재생 목록을 가져오는 중...", + "musicVolumeZeroWarning": "경고: 음악 볼륨은 0으로 설정됩니다", + "nameText": "이름", + "newSoundtrackNameText": "내 사운드트랙 ${COUNT}", + "newSoundtrackText": "새 사운드트랙:", + "newText": "새\n사운드트랙", + "selectAPlaylistText": "재생 목록 선택", + "selectASourceText": "음악 출처", + "testText": "테스트", + "titleText": "사운드트랙", + "useDefaultGameMusicText": "기본 게임 음악", + "useITunesPlaylistText": "iTunes 재생 목록", + "useMusicFileText": "음악 파일 (mp3 등)", + "useMusicFolderText": "음악 파일 폴더" + }, + "editText": "편집", + "endText": "종료", + "enjoyText": "즐기세요!", + "epicDescriptionFilterText": "(에픽 슬로 모션) ${DESCRIPTION}.", + "epicNameFilterText": "에픽 ${NAME}", + "errorAccessDeniedText": "액세스가 거부됨", + "errorOutOfDiskSpaceText": "디스크 공간 부족", + "errorText": "오류", + "errorUnknownText": "알 수 없는 오류", + "exitGameText": "${APP_NAME}를 종료하시겠습니까?", + "exportSuccessText": "'${NAME}' 를 내보냈습니다.", + "externalStorageText": "외부 저장소", + "failText": "실패", + "fatalErrorText": "무언가 누락되거나 잘못되었습니다.\n앱을 다시 설치하거나 ${EMAIL}에\n문의해주십시오.", + "fileSelectorWindow": { + "titleFileFolderText": "파일 또는 폴더 선택", + "titleFileText": "파일 선택", + "titleFolderText": "폴더 선택", + "useThisFolderButtonText": "이 폴더 사용" + }, + "finalScoreText": "최종 점수", + "finalScoresText": "최종 점수", + "finalTimeText": "최종 시간", + "finishingInstallText": "설치 완료 중, 잠시만 기다려주십시오...", + "fireTVRemoteWarningText": "* 더 나은 경험을 위해 게임 컨트롤러를\n사용하거나 스마트폰 또는 태블릿에서\n'${REMOTE_APP_NAME}' 앱을\n설치해주십시오.", + "firstToFinalText": "시작 ~ ${COUNT} 최종 결과", + "firstToSeriesText": "시작 ~ ${COUNT} 시리즈", + "fiveKillText": "펜타킬!!!", + "flawlessWaveText": "완벽한 웨이브!", + "fourKillText": "쿼드라킬!!!", + "friendScoresUnavailableText": "친구 점수를 이용할 수 없습니다.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "게임 ${COUNT} 리더", + "gameListWindow": { + "cantDeleteDefaultText": "기본 플레이 목록을 삭제할 수 없습니다.", + "cantEditDefaultText": "기본 플레이 목록을 편집할 수 없습니다! 복사하거나 새로 만드세요.", + "cantShareDefaultText": "기본 목록은 공유할 수 없습니다.", + "deleteConfirmText": "\"${LIST}\"(을)를 삭제합니까?", + "deleteText": "플레이 목록\n삭제", + "duplicateText": "플레이 목록\n복사", + "editText": "플레이 목록\n편집", + "newText": "새\n플레이 목록", + "showTutorialText": "튜토리얼 보기", + "shuffleGameOrderText": "게임 순서 섞기", + "titleText": "${TYPE} 플레이 목록 커스터마이징" + }, + "gameSettingsWindow": { + "addGameText": "게임 추가" + }, + "gamesToText": "${WINCOUNT} 대 ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "참고: 컨트롤러가 충분하면 파티 내의 어떤 기기든\n한 명 이상의 플레이어를 보유할 수 있습니다.", + "aboutDescriptionText": "이 탭을 이용해 파티를 구성합니다.\n\n파티를 통해서 다양한 기기를 사용하는 친구들과\n게임 및 토너먼트를 즐길 수 있습니다.\n\n우측 상단의 ${PARTY} 버튼을 이용해서\n파티원과 채팅하고 교류하세요.\n(컨트롤러의 경우 메뉴에서 ${BUTTON}을 누릅니다)", + "aboutText": "정보", + "addressFetchErrorText": "<주소 가져오기 오류>", + "appInviteMessageText": "${NAME} 님이 ${APP_NAME}에서 티켓 ${COUNT}장을 보냈습니다", + "appInviteSendACodeText": "코드 보내기", + "appInviteTitleText": "${APP_NAME} 앱 초대", + "bluetoothAndroidSupportText": "(블루투스를 지원하는 안드로이드 기기에서 작동합니다)", + "bluetoothDescriptionText": "블루투스를 통해 파티 호스트/가입:", + "bluetoothHostText": "블루투스를 통해 호스트", + "bluetoothJoinText": "블루투스를 통해 가입", + "bluetoothText": "블루투스", + "checkingText": "확인 중...", + "dedicatedServerInfoText": "최선의 결과를 위해 전용 서버를 구축하세요. 자세한 사항은 bombsquadgame.com/server를 참조해주십시오.", + "disconnectClientsText": "파티 내의 플레이어 ${COUNT}명의 연결이\n끊어집니다. 괜찮습니까?", + "earnTicketsForRecommendingAmountText": "친구들은 게임을 시도하면 티켓 ${COUNT}장을 받습니다\n(귀하는 각각의 친구에 대해서 ${YOU_COUNT}장을 받습니다)", + "earnTicketsForRecommendingText": "게임을 공유하고\n무료 티켓을 받으세요...", + "emailItText": "이메일 보내기", + "friendHasSentPromoCodeText": "${NAME} 님이 ${APP_NAME} 티켓 ${COUNT}장을 보냄", + "friendPromoCodeAwardText": "코드가 사용될 때마다 귀하는 티켓 ${COUNT}장을 받습니다.", + "friendPromoCodeExpireText": "이 코드는 ${EXPIRE_HOURS}시간 후 만료되며 신규 플레이어에게만 적용됩니다.", + "friendPromoCodeInstructionsText": "사용하려면 ${APP_NAME} 앱을 열고 '설정->고급->코드 입력'으로 이동합니다.\n지원되는 모든 플랫폼의 다운로드 링크는 bombsquadgame.com에서 확인하세요.", + "friendPromoCodeRedeemLongText": "최대 ${MAX_USES}명의 사람이 무료 티켓 ${COUNT}장과 교환할 수 있습니다.", + "friendPromoCodeRedeemShortText": "게임에서 티켓 ${COUNT}장과 교환할 수 있습니다.", + "friendPromoCodeWhereToEnterText": "('설정->고급->코드 입력')", + "getFriendInviteCodeText": "친구 초대 코드 받기", + "googlePlayDescriptionText": "Google Play 플레이어들을 파티에 초대하세요.", + "googlePlayInviteText": "초대", + "googlePlayReInviteText": "새 초대를 시작하면 파티 내의 Google Play 플레이어\n${COUNT}명의 연결이 끊깁니다. 그들을 다시 초대하려면\n새 초대에 포함시키세요.", + "googlePlaySeeInvitesText": "초대 보기", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(안드로이드 / Google Play 버전)", + "hostPublicPartyDescriptionText": "공개 파티 만들기:", + "inDevelopmentWarningText": "참고:\n\n네트워크 플레이는 계속 발전 중인 신규 기능입니다.\n당분간은 모든 플레이어가 같은 Wi-Fi 네트워크에\n접속할 것을 적극 권장합니다.", + "internetText": "인터넷", + "inviteAFriendText": "친구가 이 게임을 갖고 있지 않나요? 초대해서 함께\n플레이하세요. 친구들은 티켓 ${COUNT}장을 무료로 받습니다.", + "inviteFriendsText": "친구 초대", + "joinPublicPartyDescriptionText": "공개 파티 참가하기:", + "localNetworkDescriptionText": "네트워크에서 파티에 가입하기:", + "localNetworkText": "로컬 네트워크", + "makePartyPrivateText": "비공개 파티로 전환", + "makePartyPublicText": "공개 파티 만들기", + "manualAddressText": "주소", + "manualConnectText": "연결", + "manualDescriptionText": "주소로 파티에 가입하기:", + "manualJoinableFromInternetText": "인터넷을 통한 가입 가능 여부:", + "manualJoinableNoWithAsteriskText": "아니오*", + "manualJoinableYesText": "예", + "manualRouterForwardingText": "*이 문제를 해결하려면 UDP 포트 ${PORT}번을 로컬 주소로 전달하도록 라우터를 설정해보세요", + "manualText": "수동", + "manualYourAddressFromInternetText": "인터넷에서 귀하의 주소:", + "manualYourLocalAddressText": "귀하의 로컬 주소:", + "noConnectionText": "<연결 없음>", + "otherVersionsText": "(다른 버전)", + "partyInviteAcceptText": "수락", + "partyInviteDeclineText": "거절", + "partyInviteGooglePlayExtraText": "('파티 모집' 창에서 'Google Play' 탭을 확인하세요)", + "partyInviteIgnoreText": "무시", + "partyInviteText": "${NAME} 님이 귀하를\n파티에 초대했습니다!", + "partyNameText": "파티 이름", + "partySizeText": "인원", + "partyStatusCheckingText": "등급 확인 중...", + "partyStatusJoinableText": "당신의 파티는 이제 인터넷에 참가됩니다.", + "partyStatusNoConnectionText": "서버에 연결할 수 없습니다.", + "partyStatusNotJoinableText": "당신의 파티는 인터넷에 참가할 수 없습니다.", + "partyStatusNotPublicText": "당신의 파티는 비공개입니다.", + "pingText": "핑", + "portText": "포트", + "requestingAPromoCodeText": "코드 요청 중...", + "sendDirectInvitesText": "직접 초대 보내기", + "shareThisCodeWithFriendsText": "다음 코드를 친구들과 공유하세요.", + "showMyAddressText": "내 IP주소 보기", + "titleText": "파티 모집", + "wifiDirectDescriptionBottomText": "모든 장치에 'Wi-Fi 다이렉트' 패널이 있으면 이를 통해서 서로 찾고 연결할 수 있어야\n합니다. 모든 장치가 연결되면 일반 Wi-Fi 네트워크와 똑같이 '로컬 네트워크' 탭을\n이용해 이곳에서 파티를 결성할 수 있습니다.\n\n최선의 결과를 얻으려면 Wi-Fi 다이렉트 호스트가 ${APP_NAME} 파티 호스트이어야 합니다.", + "wifiDirectDescriptionTopText": "Wi-Fi 다이렉트는 Wi-Fi 네트워크 없이 직접 안드로이드 장치들을 연결하는 데\n사용될 수 있습니다. 이 기능은 안드로이드 버전 4.2 이상에서 가장 잘 작동합니다.\n\n이 기능을 사용하려면 Wi-Fi 설정을 열고 메뉴에서 'Wi-Fi 다이렉트'를 찾으세요.", + "wifiDirectOpenWiFiSettingsText": "Wi-Fi 설정 열기", + "wifiDirectText": "Wi-Fi 다이렉트", + "worksBetweenAllPlatformsText": "(모든 플랫폼 간에 작동합니다)", + "worksWithGooglePlayDevicesText": "(이 게임의 Google Play (안드로이드) 버전을 실행하는 장치들과 함께 작동합니다)", + "youHaveBeenSentAPromoCodeText": "${APP_NAME} 프로모션 코드가 도착했습니다." + }, + "getTicketsWindow": { + "freeText": "무료!", + "freeTicketsText": "무료 티켓", + "inProgressText": "거래가 진행 중입니다. 조금 후 다시 시도해주십시오.", + "purchasesRestoredText": "구매 항목이 복원되었습니다.", + "receivedTicketsText": "티켓 ${COUNT}장을 받았습니다!", + "restorePurchasesText": "구매 항목 복원", + "ticketPack1Text": "소형 티켓 팩", + "ticketPack2Text": "중형 티켓 팩", + "ticketPack3Text": "대형 티켓 팩", + "ticketPack4Text": "점보 티켓 팩", + "ticketPack5Text": "매머드 티켓 팩", + "ticketPack6Text": "궁극의 티켓 팩", + "ticketsFromASponsorText": "스폰서로부터 티켓\n${COUNT}장 받기", + "ticketsText": "티켓 ${COUNT}장", + "titleText": "티켓 구입", + "unavailableLinkAccountText": "죄송합니다만 이 플랫폼에서는 구매할 수 없습니다.\n해결책으로, 이 계정을 다른 플랫폼의 계정에 연동하여\n그곳에서 구매를 진행할 수 있습니다.", + "unavailableTemporarilyText": "이 옵션은 현재 이용할 수 없습니다. 나중에 다시 시도해주십시오.", + "unavailableText": "죄송합니다만 이 옵션은 이용할 수 없습니다.", + "versionTooOldText": "죄송합니다만 이 게임 버전은 너무 오래되었습니다. 최신 버전으로 업데이트해주십시오.", + "youHaveShortText": "티켓 보유량: ${COUNT}", + "youHaveText": "보유량: ${COUNT} 티켓" + }, + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "언제나", + "fullScreenCmdText": "전체 화면 (Cmd-F)", + "fullScreenCtrlText": "전체 화면 (Ctrl-F)", + "gammaText": "감마", + "highText": "높음", + "higherText": "매우 높음", + "lowText": "낮음", + "mediumText": "중간", + "neverText": "안 함", + "resolutionText": "해상도", + "showFPSText": "fps 수치 표시", + "texturesText": "질감", + "titleText": "그래픽", + "tvBorderText": "TV 테두리", + "verticalSyncText": "수직 동기화", + "visualsText": "비주얼" + }, + "helpWindow": { + "bombInfoText": "- 폭탄 -\n펀치보다 강력하지만 자신도\n심각한 부상을 입을 수 있습니다.\n도화선이 다 타기 전에 적들에게\n던지는 것이 가장 좋습니다.", + "canHelpText": "${APP_NAME}가 여러분을 도울 수 있습니다.", + "controllersInfoText": "네트워크를 통해 친구들과 함께 ${APP_NAME}를 즐기거나\n컨트롤러가 충분할 경우 동일한 기기에서 함께 플레이할 수 있습니다.\n${APP_NAME}는 다양한 기기를 지원합니다. \n심지어 무료로 '${REMOTE_APP_NAME}' 앱을 사용해 휴대폰을 컨트롤러로\n사용할 수도 있습니다. 자세한 사항은 설정->컨트롤러를 참고하세요.", + "controllersText": "컨트롤러", + "controlsSubtitleText": "당신의 ${APP_NAME} 캐릭터는 약간의 기본적인 행동이 가능합니다", + "controlsText": "컨트롤", + "devicesInfoText": "일반 버전을 이용해 네트워크에서 ${APP_NAME}의 VR 버전을 \n플레이할 수 있습니다. 그러므로 남는 스마트폰, 태블릿, 컴퓨터를\n꺼내서 게임을 즐겨보세요. 이 게임의 일반 버전을 VR 버전에\n연결시켜서 다른 사람들이 게임을 감상하게 할 때에도\n유용합니다.", + "devicesText": "기기", + "friendsGoodText": "친구들이 있으면 좋습니다. ${APP_NAME}는 여러 친구가 함께 즐길 때\n가장 재미있으며 최대 8명을 지원할 수 있습니다.", + "friendsText": "친구", + "jumpInfoText": "- 점프 -\n점프를 이용해 작은 틈을 건너고\n물건을 더 높이 던지며 기쁜\n감정을 표현할 수 있습니다.", + "orPunchingSomethingText": "또는 무언가를 절벽에서 던져버리거나 끈적한 폭탄으로 저 멀리 날려버리고 싶을 때도 있을 겁니다.", + "pickUpInfoText": "- 줍기 -\n깃발, 적 또는 땅에 고정되지 않은\n것을 움켜잡을 수 있습니다.\n다시 누르면 던집니다.", + "powerupBombDescriptionText": "한 개가 아니라 연속해서\n세 개의 폭탄을 꺼냅니다.", + "powerupBombNameText": "3연발 폭탄", + "powerupCurseDescriptionText": "이것은 피하고 싶을 겁니다.\n ...혹시 원하는 건가요?", + "powerupCurseNameText": "저주", + "powerupHealthDescriptionText": "체력을 완전히 회복합니다.\n생각도 못했을 걸요.", + "powerupHealthNameText": "치유 팩", + "powerupIceBombsDescriptionText": "일반 폭탄보다는 약하지만\n적들을 얼려버리고\n약하게 만듭니다.", + "powerupIceBombsNameText": "얼음 폭탄", + "powerupImpactBombsDescriptionText": "일반 폭탄보다 약간 더 약하지만\n충격을 받으면 폭발합니다.", + "powerupImpactBombsNameText": "반응 폭탄", + "powerupLandMinesDescriptionText": "3개가 든 팩으로 제공됩니다.\n기지 방어 또는 빠른 적들을\n저지하는 데 유용합니다.", + "powerupLandMinesNameText": "지뢰", + "powerupPunchDescriptionText": "당신의 펀치를 더 단단하고,\n빠르고, 더 강하게 만들어 줍니다.", + "powerupPunchNameText": "권투 장갑", + "powerupShieldDescriptionText": "상대방의 공격을 막아\n피해를 줄입니다.", + "powerupShieldNameText": "에너지 실드", + "powerupStickyBombsDescriptionText": "뭐든 맞으면 달라붙습니다.\n그 다음이 재미있어요.", + "powerupStickyBombsNameText": "끈적이 폭탄", + "powerupsSubtitleText": "다양한 파워업 아이템들이 있습니다", + "powerupsText": "파워업", + "punchInfoText": "- 펀치 -\n주먹을 움직이는 속도가 빠를수록\n펀치가 더 강력해집니다. 그러니\n미친 사람처럼 달리고 회전하세요.", + "runInfoText": "- 달리기 -\n아무 버튼이나 길게 누르면 달립니다. 트리거 또는 숄더 버튼이 있으면 잘 작동합니다.\n달리면 더 빠르게 우위를 차지할 수 있으나 회전하기 어려우므로 절벽을 주의하세요.", + "someDaysText": "그냥 뭔가를 주먹으로 치고 싶을 때가 있을 겁니다. 아니면 무언가 날려버리고 싶거나요.", + "titleText": "${APP_NAME} 도움말", + "toGetTheMostText": "이 게임을 최대한 즐기기 위해 필요한 준비물", + "welcomeText": "${APP_NAME}에 오신 것을 환영합니다!" + }, + "holdAnyButtonText": "<아무 버튼이나 길게 누르세요>", + "holdAnyKeyText": "<아무 키나 길게 누르세요>", + "hostIsNavigatingMenusText": "- ${HOST}가 메뉴를 탐색 중입니다 -", + "importPlaylistCodeInstructionsText": "다른 곳으로 이 플레이 목록을 불러오기 위해 다음 코드를 사용하세요:", + "importPlaylistSuccessText": "${TYPE} 플레이 목록 '${NAME}' 를 불러왔습니다", + "importText": "불러오기", + "importingText": "불러오는 중...", + "inGameClippedNameText": "게임 내에서는 다음과 같이 보여질 것입니다.\n\"${NAME}\"", + "installDiskSpaceErrorText": "오류: 설치를 완료할 수 없습니다.\n기기에 공간이 부족한 것 같습니다.\n공간을 확보한 후 다시 시도해보세요.", + "internal": { + "arrowsToExitListText": "목록에서 나가려면 ${LEFT} 또는 ${RIGHT}를 누르세요", + "buttonText": "버튼", + "cantKickHostError": "당신은 호스트를 추방할 수 없습니다.", + "chatBlockedText": "${TIME} 초 동안 ${NAME} 님의 채팅이 차단됩니다.", + "connectedToGameText": "'${NAME}' 에 참가했습니다.", + "connectedToPartyText": "${NAME} 님의 파티에 가입했습니다!", + "connectingToPartyText": "연결 중...", + "connectionFailedHostAlreadyInPartyText": "연결에 실패했습니다. 호스트가 다른 파티에 있습니다.", + "connectionFailedPartyFullText": "연결에 실패했습니다. 파티가 꽉 찼습니다.", + "connectionFailedText": "연결에 실패했습니다.", + "connectionFailedVersionMismatchText": "연결에 실패했습니다. 호스트가 다른 게임 버전을 실행하고 있습니다.\n모두 최신 버전인지 확인한 후 다시 시도해보세요.", + "connectionRejectedText": "연결이 거부되었습니다.", + "controllerConnectedText": "${CONTROLLER}(이)가 연결되었습니다.", + "controllerDetectedText": "1개의 컨트롤러가 검색되었습니다.", + "controllerDisconnectedText": "${CONTROLLER}의 연결이 끊김.", + "controllerDisconnectedTryAgainText": "${CONTROLLER}의 연결이 끊겼습니다. 다시 연결을 시도해주십시오.", + "controllerForMenusOnlyText": "이 컨트롤러는 메뉴 전용으로 게임에는 사용하실 수 없습니다.", + "controllerReconnectedText": "${CONTROLLER}(이)가 다시 연결되었습니다.", + "controllersConnectedText": "${COUNT}개의 컨트롤러가 연결되었습니다.", + "controllersDetectedText": "${COUNT}개의 컨트롤러가 검색되었습니다.", + "controllersDisconnectedText": "${COUNT}개의 컨트롤러의 연결이 끊겼습니다.", + "corruptFileText": "손상된 파일이 검색되었습니다. 다시 설치하거나 ${EMAIL}에 이메일을 보내주십시오", + "errorPlayingMusicText": "음악 재생 오류: ${MUSIC}", + "errorResettingAchievementsText": "온라인 업적을 재설정할 수 없습니다. 나중에 다시 시도해주십시오.", + "hasMenuControlText": "${NAME} 님이 메뉴 컨트롤을 보유합니다.", + "incompatibleNewerVersionHostText": "호스트는 게임의 최신버전을 실행중입니다.\n최신버전으로 업데이트 하고 다시 시도해주세요.", + "incompatibleVersionHostText": "호스트가 다른 게임 버전을 실행하고 있습니다.\n모두 최신 버전인지 확인한 후 다시 시도해보세요.", + "incompatibleVersionPlayerText": "${NAME} 님이 다른 게임 버전을 실행하고 있습니다.\n모두 최신 버전인지 확인한 후 다시 시도해보세요.", + "invalidAddressErrorText": "오류: 잘못된 주소.", + "invalidNameErrorText": "오류: 잘못된 이름.", + "invalidPortErrorText": "오류: 잘못된 포트.", + "invitationSentText": "초대를 보냈습니다.", + "invitationsSentText": "${COUNT}개의 초대를 보냈습니다.", + "joinedPartyInstructionsText": "누가 파티에 가입했습니다.\n'플레이'에서 게임을 시작하세요.", + "keyboardText": "키보드", + "kickIdlePlayersKickedText": "부재 중인 ${NAME} 님을 추방하는 중.", + "kickIdlePlayersWarning1Text": "${NAME} 님은 계속 부재 중이면 ${COUNT}초 후 추방됩니다.", + "kickIdlePlayersWarning2Text": "(설정->고급에서 이 옵션을 끌 수 있습니다)", + "leftGameText": "'${NAME}' 을 떠났습니다.", + "leftPartyText": "${NAME} 님의 파티를 떠났습니다.", + "noMusicFilesInFolderText": "폴더에 음악 파일이 없습니다.", + "playerJoinedPartyText": "${NAME} 님이 파티에 가입했습니다!", + "playerLeftPartyText": "${NAME} 님이 파티를 떠났습니다.", + "rejectingInviteAlreadyInPartyText": "초대를 거절하는 중 (이미 파티 중임).", + "serverRestartingText": "다시 시작하는 중입니다. 곧 다시 합류해주세요...", + "signInErrorText": "로그인 오류.", + "signInNoConnectionText": "로그인할 수 없습니다. (인터넷에 연결되지 않았습니까?)", + "telnetAccessDeniedText": "오류: 사용자가 텔넷 액세스를 부여하지 않았습니다.", + "timeOutText": "(${TIME}초 후 시간 초과)", + "touchScreenJoinWarningText": "터치스크린을 이용해 가입했습니다.\n실수로 그러신 경우 '메뉴->게임 나가기'를 누르세요.", + "touchScreenText": "터치스크린", + "unableToResolveHostText": "오류: 호스트를 확인할 수 없습니다.", + "unavailableNoConnectionText": "이 기능은 현재 이용할 수 없습니다 (인터넷에 연결되지 않았습니까?)", + "vrOrientationResetCardboardText": "이것을 이용해 VR 방향을 재설정합니다.\n게임을 플레이하려면 외부 컨트롤러가 필요합니다.", + "vrOrientationResetText": "VR 방향이 재설정되었습니다.", + "willTimeOutText": "(부재 중이면 시간 초과가 됩니다)" + }, + "jumpBoldText": "점프", + "jumpText": "점프", + "keepText": "유지", + "keepTheseSettingsText": "이 설정을 유지합니까?", + "kickOccurredText": "${NAME} 님이 추방당했습니다.", + "kickQuestionText": "${NAME} 님을 추방하시겠습니까?", + "kickText": "추방", + "kickVoteFailedNotEnoughVotersText": "투표 인원이 모자랍니다.", + "kickVoteFailedText": "추방 투표를 실패했습니다.", + "kickVoteStartedText": "${NAME} 님에 대한 추방 투표가 시작됐습니다.", + "kickVoteText": "추방 투표", + "kickWithChatText": "채팅 창에 ${YES} (을)를 치셔서 찬성 또는 ${NO} (을)를 치셔서 반대하십시오.", + "killsTallyText": "${COUNT}번 처치", + "killsText": "처치", + "kioskWindow": { + "easyText": "쉬움", + "epicModeText": "에픽 모드", + "fullMenuText": "전체 메뉴", + "hardText": "어려움", + "mediumText": "보통", + "singlePlayerExamplesText": "싱글 플레이어 / 협동 모드의 예", + "versusExamplesText": "대결 모드의 예" + }, + "languageSetText": "현재 언어는 '${LANGUAGE}'입니다.", + "lapNumberText": "랩 ${CURRENT}/${TOTAL}", + "lastGamesText": "(마지막 ${COUNT} 게임)", + "leaderboardsText": "순위표", + "league": { + "allTimeText": "역대", + "currentSeasonText": "현재 시즌 (${NUMBER})", + "leagueFullText": "${NAME} 리그", + "leagueRankText": "리그 순위", + "leagueText": "리그", + "rankInLeagueText": "#${RANK}, ${NAME} 리그${SUFFIX}", + "seasonEndedDaysAgoText": "시즌은 ${NUMBER}일 전에 종료되었습니다.", + "seasonEndsDaysText": "시즌은 ${NUMBER}일 후 종료됩니다.", + "seasonEndsHoursText": "시즌은 ${NUMBER}시간 후 종료됩니다.", + "seasonEndsMinutesText": "시즌은 ${NUMBER}분 후 종료됩니다.", + "seasonText": "시즌 ${NUMBER}", + "tournamentLeagueText": "이 토너먼트에 참가하려면 ${NAME} 리그에 도달해야 합니다.", + "trophyCountsResetText": "트로피 수는 다음 시즌에 초기화됩니다." + }, + "levelBestScoresText": "${LEVEL}의 최고 점수", + "levelBestTimesText": "${LEVEL}의 최고 시간", + "levelIsLockedText": "${LEVEL}(은)는 잠겼습니다.", + "levelMustBeCompletedFirstText": "${LEVEL}(을)를 먼저 완료해야 합니다.", + "levelText": "레벨 ${NUMBER}", + "levelUnlockedText": "레벨이 잠겼습니다!", + "livesBonusText": "생명력 보너스", + "loadingText": "불러오는 중", + "loadingTryAgainText": "로딩중입니다. 나중에 다시 시도하십시오...", + "macControllerSubsystemBothText": "둘 다 (권장하지 않음)", + "macControllerSubsystemClassicText": "클래식", + "macControllerSubsystemDescriptionText": "(컨트롤러가 작동하지 않는 경우 이것을 변경해보십시오)", + "macControllerSubsystemMFiNoteText": "Made-for-OS / Mac 컨트롤러가 감지되었습니다.\n설정 -> 컨트롤러에서 활성화 할 수 있습니다.", + "macControllerSubsystemMFiText": "iOS용/Mac용", + "macControllerSubsystemTitleText": "컨트롤러 지원", + "mainMenu": { + "creditsText": "개발진", + "demoMenuText": "데모 메뉴", + "endGameText": "게임 종료", + "exitGameText": "게임 종료", + "exitToMenuText": "메뉴로 나가시겠습니까?", + "howToPlayText": "게임 방법", + "justPlayerText": "(${NAME} 님만)", + "leaveGameText": "게임 나가기", + "leavePartyConfirmText": "정말로 파티를 떠나시겠습니까?", + "leavePartyText": "파티 떠나기", + "quitText": "끝내기", + "resumeText": "계속하기", + "settingsText": "설정" + }, + "makeItSoText": "적용", + "mapSelectGetMoreMapsText": "다른 지도 보기...", + "mapSelectText": "선택...", + "mapSelectTitleText": "${GAME} 지도", + "mapText": "지도", + "maxConnectionsText": "최대 연결", + "maxPartySizeText": "최대 인원", + "maxPlayersText": "최대 플레이어", + "mostValuablePlayerText": "가장 뛰어난 플레이어", + "mostViolatedPlayerText": "가장 비참한 플레이어", + "mostViolentPlayerText": "가장 난폭한 플레이어", + "moveText": "이동", + "multiKillText": "${COUNT}연속 처치!!!", + "multiPlayerCountText": "${COUNT}명의 플레이어", + "mustInviteFriendsText": "참고: 멀티 플레이어 게임을 하려면\n'${GATHER}' 패널에서 친구들을\n초대하거나 컨트롤러를 연결하세요.", + "nameBetrayedText": "${NAME} 님이 ${VICTIM} 님을 배반했습니다.", + "nameDiedText": "${NAME} 님이 죽었습니다.", + "nameKilledText": "${NAME} 님이 ${VICTIM} 님을 처치했습니다.", + "nameNotEmptyText": "이름은 비워둘 수 없습니다.", + "nameScoresText": "${NAME} 님 점수 획득!", + "nameSuicideKidFriendlyText": "${NAME} 님이 실수로 죽었습니다.", + "nameSuicideText": "${NAME} 님이 자살했습니다.", + "nameText": "이름", + "nativeText": "기본", + "newPersonalBestText": "새 개인 최고 기록!", + "newTestBuildAvailableText": "새 테스트 빌드가 나왔습니다! (${VERSION} 빌드 ${BUILD}).\n${ADDRESS}에서 다운로드 하세요", + "newText": "새", + "newVersionAvailableText": "${APP_NAME}의 새 버전이 나왔습니다! (${VERSION})", + "nextAchievementsText": "남은 도전과제:", + "nextLevelText": "다음 레벨", + "noAchievementsRemainingText": "- 없음", + "noContinuesText": "(계속 없음)", + "noExternalStorageErrorText": "이 기기에서 외부 저장소를 찾지 못했습니다.", + "noGameCircleText": "오류: GameCircle에 로그인되지 않았습니다", + "noScoresYetText": "아직 점수 없음.", + "noThanksText": "아니요", + "noValidMapsErrorText": "이 게임 유형에 유효한 지도를 찾지 못했습니다.", + "notEnoughPlayersRemainingText": "남은 플레이어가 충분하지 않습니다. 게임을 종료한 후 새로 시작하세요.", + "notEnoughPlayersText": "이 게임을 시작하려면 ${COUNT}명 이상의 플레이어가 필요합니다!", + "notNowText": "다음에", + "notSignedInErrorText": "이 작업을 하려면 로그인해야 합니다.", + "notSignedInGooglePlayErrorText": "이 작업을 하려면 Google Play로 로그인해야 합니다.", + "notSignedInText": "로그인하지 않음", + "nothingIsSelectedErrorText": "선택된 것이 없습니다!", + "numberText": "#${NUMBER}", + "offText": "끔", + "okText": "확인", + "onText": "켬", + "onslaughtRespawnText": "${PLAYER} 님은 ${WAVE} 웨이브에서 부활합니다", + "orText": "${A} 또는 ${B}", + "otherText": "기타...", + "outOfText": "(#${RANK} / ${ALL})", + "ownFlagAtYourBaseWarning": "자신의 깃발이 기지에 있어야\n점수를 획득할 수 있습니다!", + "packageModsEnabledErrorText": "로컬 패키지 모드를 사용하는 동안은 네트워크 플레이가 허용되지 않습니다 (설정->고급 참고)", + "partyWindow": { + "chatMessageText": "채팅 메시지", + "emptyText": "파티가 비어 있습니다", + "hostText": "(호스트)", + "sendText": "보내기", + "titleText": "내 파티" + }, + "pausedByHostText": "(호스트가 일시 중지함)", + "perfectWaveText": "완벽한 웨이브!", + "pickUpText": "줍기", + "playModes": { + "coopText": "협동", + "freeForAllText": "개인전", + "multiTeamText": "멀티 팀", + "singlePlayerCoopText": "싱글 플레이어 / 협동", + "teamsText": "팀전" + }, + "playText": "플레이", + "playWindow": { + "oneToFourPlayersText": "1-4 플레이어", + "titleText": "플레이", + "twoToEightPlayersText": "2-8 플레이어" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerDelayedJoinText": "${PLAYER} 님이 다음 라운드 시작 시 참가합니다.", + "playerInfoText": "플레이어 정보", + "playerLeftText": "${PLAYER} 님이 게임을 나갔습니다.", + "playerLimitReachedText": "${COUNT}명의 플레이어 한도에 도달함. 더 이상 가입할 수 없습니다.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "자신의 계정 프로필을 삭제할 수 없습니다.", + "deleteButtonText": "프로필\n삭제", + "deleteConfirmText": "'${PROFILE}' 삭제합니까?", + "editButtonText": "프로필\n편집", + "explanationText": "(이 계정에 대한 맞춤형 플레이어 이름 및 외관)", + "newButtonText": "새\n프로필", + "titleText": "플레이어 프로필" + }, + "playerText": "플레이어", + "playlistNoValidGamesErrorText": "이 플레이 목록에 포함된 잠금 해제된 유효한 게임이 없습니다.", + "playlistNotFoundText": "플레이 목록이 없음", + "playlistsText": "플레이 목록", + "pleaseRateText": "${APP_NAME} 앱이 마음에 드시면 잠시 시간을 내어\n평가를 하거나 리뷰를 남겨주세요. 저희가 유용한\n피드백을 얻을 수 있고 향후 개발에 도움이 됩니다.\n\n감사합니다!\n-eric", + "pleaseWaitText": "잠시만 기다려 주십시오...", + "practiceText": "연습", + "pressAnyButtonPlayAgainText": "다시 플레이하려면 아무 버튼이나 누르세요...", + "pressAnyButtonText": "계속하려면 아무 버튼이나 누르세요...", + "pressAnyButtonToJoinText": "가입하려면 아무 버튼이나 누르세요...", + "pressAnyKeyButtonPlayAgainText": "다시 플레이하려면 아무 키/버튼이나 누르세요...", + "pressAnyKeyButtonText": "계속하려면 아무 키/버튼이나 누르세요...", + "pressAnyKeyText": "아무 키나 누르세요...", + "pressJumpToFlyText": "** 비행하려면 점프를 반복해서 누르세요 **", + "pressPunchToJoinText": "펀치를 눌러서 가입합니다...", + "pressToOverrideCharacterText": "캐릭터를 덮어쓰려면 ${BUTTONS} 버튼을 누르세요", + "pressToSelectProfileText": "플레이어를 선택하려면 ${BUTTONS} 버튼을 누르세요", + "pressToSelectTeamText": "팀을 선택하려면 ${BUTTONS} 버튼을 누르세요", + "promoCodeWindow": { + "codeText": "코드", + "enterText": "입력" + }, + "promoSubmitErrorText": "코드 제출 오류. 인터넷 연결 상태를 확인하세요", + "ps3ControllersWindow": { + "macInstructionsText": "PS3의 전원을 끄고, 블루투스를 켠 뒤 컨트롤러를 USB로 연결하면 페어링이 됩니다.\n그 이후, 컨트롤러의 홈 버튼을 사용하면 유선 모드(USB)와 무선 모드(블루투스)를\n바꿀 수 있습니다.\n\n\n페어링 도중 패스워드를 입력해야 하는 경우가 있는데,\n이 경우 다음의 하는 방법 또는 구글링을 통하여 도움을 받을 수 있습니다.\n\n\n\n\n\n연결된 무선 컨트롤러의 리스트는\n시스템 환경설정->블루투스에서 확인이 가능합니다.\n만약 PS3에서의 사용을 원하신다면 블루투스에서 연결을 끊어야 쓸 수 있습니다.\n\n\n미사용중일시 블루투스에서 연결을 끊어야 배터리를 아낄 수 있습니다.\n\n\n차이가 있을 수 있으나, 블루투스는 최대 7개의 컨트롤러를 연결할 수 있습니다.", + "ouyaInstructionsText": "PS3 컨트롤러를 OUYA에서 사용하시려면, 우선 USB 케이블로 연결하여 페어링을 하면 됩니다.\n이 경우 다른 컨트롤러의 연결을 끊을 수도 있으므로 이 경우 OUYA를 재가동하신 후\nUSB 케이블을 빼면 됩니다.\n\n\n그 이후 홈 버튼을 누르면 무선 연결이 가능해집니다.\n그만 사용하고 싶다면 10초동안 홈 버튼을 눌러서 컨트롤러를 꺼야 합니다;\n하지 않으면 배터리가 낭비됩니다.", + "pairingTutorialText": "페어링 설명 동영상", + "titleText": "${APP_NAME} 앱과 함께 PS3 컨트롤러 사용하기:" + }, + "punchBoldText": "펀치", + "punchText": "펀치", + "purchaseForText": "${PRICE}에 구입", + "purchaseGameText": "게임 구입", + "purchasingText": "구입 중...", + "quitGameText": "${APP_NAME}를 종료하시겠습니까?", + "quittingIn5SecondsText": "5초 후 종료됩니다...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "무작위", + "rankText": "순위", + "ratingText": "등급", + "reachWave2Text": "순위에 들려면 웨이브 2에 도달하세요.", + "readyText": "준비", + "recentText": "최근", + "remoteAppInfoShortText": "${APP_NAME}는 친구 또는 가족과 같이 할 때 더 재미있습니다.\n컨트롤러 몇개를 연결하거나 또는\n${REMOTE_APP_NAME} 앱을 폰 또는 태블릿에\n설치하여 컨트롤러처럼 사용하실 수 있습니다.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "버튼 위치", + "button_size": "버튼 크기", + "cant_resolve_host": "호스트를 확인할 수 없습니다.", + "capturing": "캡처 중...", + "connected": "연결됨.", + "description": "스마트폰 또는 태블릿을 BombSquad의 컨트롤러로 사용하세요.\n하나의 TV 또는 태블릿에서 최대 8대의 기기를 동시에 연결해 멋진 로컬 멀티 플레이어 게임을 즐길 수 있습니다.", + "disconnected": "서버에서 연결이 끊겼습니다.", + "dpad_fixed": "고정", + "dpad_floating": "변동", + "dpad_position": "D-패드 위치", + "dpad_size": "D-패드 크기", + "dpad_type": "D-패드 종류", + "enter_an_address": "주소 입력", + "game_full": "게임이 만원이거나 연결을 받지 않습니다.", + "game_shut_down": "게임이 종료되었습니다.", + "hardware_buttons": "하드웨어 버튼", + "join_by_address": "주소로 가입...", + "lag": "랙: ${SECONDS}초", + "reset": "기본값으로 재설정", + "run1": "달리기 1", + "run2": "달리기 2", + "searching": "BombSquad 게임 검색 중...", + "searching_caption": "게임의 이름을 눌러서 가입하세요.\n게임과 같은 Wi-Fi 네트워크에 연결되어 있는지 확인하세요.", + "start": "시작", + "version_mismatch": "버전이 일치하지 않습니다.\nBombSquad 및 BombSquad Remote가\n최신 버전인지 확인한 후 다시 시도하세요." + }, + "removeInGameAdsText": "게임 내 광고를 제거하려면 상점에서 \"${PRO}\"를 잠금 해제하세요.", + "renameText": "이름 바꾸기", + "replayEndText": "다시 보기 종료", + "replayNameDefaultText": "마지막 게임 다시 보기", + "replayReadErrorText": "다시 보기 파일 읽기 오류.", + "replayRenameWarningText": "보관하고 싶으면 게임 후 \"${REPLAY}\"의 이름을 바꾸세요. 그렇지 않으면 덮어쓰게 됩니다.", + "replayVersionErrorText": "죄송합니다만 이 다시 보기는 다른 게임 버전에서\n만들어져서 사용할 수 없습니다.", + "replayWatchText": "다시 보기 감상", + "replayWriteErrorText": "다시 보기 파일 쓰기 오류.", + "replaysText": "다시 보기", + "reportPlayerExplanationText": "치팅, 부적절한 언어 또는 기타 나쁜 행위를 신고하려면 이 이메일을 사용하세요.\n아래에 기재해주십시오.", + "reportThisPlayerCheatingText": "치팅", + "reportThisPlayerLanguageText": "부적절한 언어", + "reportThisPlayerReasonText": "무엇을 신고하고 싶으세요?", + "reportThisPlayerText": "이 플레이어 신고", + "requestingText": "요청 중...", + "restartText": "다시 시작", + "retryText": "다시 시도", + "revertText": "되돌리기", + "runText": "달리기", + "saveText": "저장", + "scanScriptsErrorText": "스크립트를 검색하는 중 오류가 발생했습니다. 자세한 내용은 로그를 참조하십시오.", + "scoreChallengesText": "점수 챌린지", + "scoreListUnavailableText": "점수 목록을 이용할 수 없습니다.", + "scoreText": "점수", + "scoreUnits": { + "millisecondsText": "밀리초", + "pointsText": "점수", + "secondsText": "초" + }, + "scoreWasText": "(이전: ${COUNT})", + "selectText": "선택", + "seriesWinLine1PlayerText": "님이 시리즈에서", + "seriesWinLine1TeamText": "님이 시리즈에서", + "seriesWinLine1Text": "님이 시리즈에서", + "seriesWinLine2Text": "승리했습니다!", + "settingsWindow": { + "accountText": "계정", + "advancedText": "고급", + "audioText": "오디오", + "controllersText": "컨트롤러", + "graphicsText": "그래픽", + "playerProfilesMovedText": "참고: 플레이어 프로필은 메인 메뉴의 계정 창으로 이동했습니다.", + "titleText": "설정" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(간편하고 사용하기 쉬운 텍스트 편집용 온스크린 키보드)", + "alwaysUseInternalKeyboardText": "언제나 내부 키보드를 사용함", + "benchmarksText": "벤치마크 및 스트레스 테스트", + "disableThisNotice": "(고급설정에서 이 알림을 중지 할 수 있습니다)", + "enablePackageModsDescriptionText": "(추가 모드 기능을 활성화하지만 네트워크 플레이를 비활성화합니다)", + "enablePackageModsText": "로컬 패키지 모드 활성화", + "enterPromoCodeText": "코드 입력", + "forTestingText": "참고: 이 값들은 테스트용으로 앱을 종료하면 없어집니다.", + "helpTranslateText": "${APP_NAME}의 비영어권 번역은 커뮤니티에서 지원된\n결과입니다. 공헌하거나 번역을 교정하고 싶으면\n아래 링크를 이용하세요. 감사합니다!", + "kickIdlePlayersText": "부재 중 플레이어 추방", + "kidFriendlyModeText": "어린이 보호 모드 (폭력 순화 등)", + "languageText": "언어", + "moddingGuideText": "모딩 가이드", + "mustRestartText": "이 설정이 적용되려면 게임을 다시 시작해야 합니다.", + "netTestingText": "네트워크 테스트", + "resetText": "재설정", + "showBombTrajectoriesText": "폭탄 궤적 표시", + "showPlayerNamesText": "플레이어 이름 표시", + "showUserModsText": "모드 폴더 표시", + "titleText": "고급", + "translationEditorButtonText": "${APP_NAME} 번역 편집기", + "translationFetchErrorText": "번역 상태를 이용할 수 없습니다", + "translationFetchingStatusText": "번역 상태 확인 중...", + "translationInformMe": "언어 업데이트 알림 활성화", + "translationNoUpdateNeededText": "현재 언어는 최신 상태입니다. 만세!", + "translationUpdateNeededText": "** 현재 언어는 업데이트가 필요합니다!! **", + "vrTestingText": "VR 테스트" + }, + "shareText": "공유", + "sharingText": "공유 중...", + "showText": "표시", + "signInForPromoCodeText": "코드가 적용되려면 계정에 로그인해야 합니다.", + "signInWithGameCenterText": "Game Center 계정을 이용하려면\nGame Center 앱으로 로그인하세요.", + "singleGamePlaylistNameText": "${GAME}만", + "singlePlayerCountText": "1 플레이어", + "soloNameFilterText": "솔로 ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "캐릭터 선택", + "Chosen One": "선택된 자", + "Epic": "에픽 모드 게임", + "Epic Race": "에픽 레이스", + "FlagCatcher": "깃발 탈환", + "Flying": "행복한 생각", + "Football": "축구", + "ForwardMarch": "전진", + "GrandRomp": "정복", + "Hockey": "하키", + "Keep Away": "차단", + "Marching": "행군", + "Menu": "메인 메뉴", + "Onslaught": "맹습", + "Race": "레이스", + "Scary": "킹 오브 더 힐", + "Scores": "점수 화면", + "Survival": "제거", + "ToTheDeath": "데스 매치", + "Victory": "최종 점수 화면" + }, + "spaceKeyText": "스페이스", + "statsText": "전적", + "storagePermissionAccessText": "이 행위는 저장소 접근이 필요합니다.", + "store": { + "alreadyOwnText": "이미 ${NAME}(을)를 소유 중입니다!", + "bombSquadProDescriptionText": "• 업적 티켓 보상 두 배\n• 게임 내 광고 제거\n• 보너스 티켓 ${COUNT}장 포함\n• +${PERCENT}% 리그 점수 보너스\n• '${INF_ONSLAUGHT}' 및\n '${INF_RUNAROUND}' 협동 레벨 잠금 해제", + "bombSquadProNameText": "${APP_NAME} 프로", + "bombSquadProNewDescriptionText": "• 광고 제거와 게임 중 자성 화면 제거\n• 추가 게임 설정 사용 가능\n• 이것들도 제공됩니다:", + "buyText": "구입", + "charactersText": "캐릭터", + "comingSoonText": "향후 예정...", + "extrasText": "추가", + "freeBombSquadProText": "현재 봄스쿼드는 무료이지만, 당신은 무료로 전환되기 전 구매를 하였으므로\n봄스쿼드 프로판 업그레이드와 티켓 ${COUNT}를 감사의 의미로 드리겠습니다.\n새 기능을 즐기시고, 후원해 주셔서 감사합니다!\n-Eric", + "holidaySpecialText": "연휴 특가 상품", + "howToSwitchCharactersText": "(\"${SETTINGS} -> ${PLAYER_PROFILES}\"에서 캐릭터들을 할당 및 커스터마이징하세요)", + "howToUseIconsText": "(이것을 사용하려면 계정 창에서 글로벌 플레이어 프로필을 만드세요)", + "howToUseMapsText": "(이 지도들을 팀전/개인전 플레이 목록에서 사용하세요)", + "iconsText": "아이콘", + "loadErrorText": "페이지를 불러올 수 없습니다.\n인터넷 연결 상태를 확인하세요.", + "loadingText": "불러오는 중", + "mapsText": "지도", + "miniGamesText": "미니 게임", + "oneTimeOnlyText": "(마지막 기회)", + "purchaseAlreadyInProgressText": "이 아이템의 구매가 이미 진행 중입니다.", + "purchaseConfirmText": "${ITEM}(을)를 구매합니까?", + "purchaseNotValidError": "구매가 유효하지 않습니다.\n오류인 경우 ${EMAIL}에 문의해주십시오.", + "purchaseText": "구매", + "saleBundleText": "번들 세일!", + "saleExclaimText": "세일!", + "salePercentText": "(${PERCENT}% 할인)", + "saleText": "세일", + "searchText": "검색", + "teamsFreeForAllGamesText": "팀전 / 개인전", + "totalWorthText": "*** ${TOTAL_WORTH}만큼 가치가 있음! ***", + "upgradeQuestionText": "계정을 승급시키겠습니까?", + "winterSpecialText": "겨울 특가 상품", + "youOwnThisText": "- 보유 중 -" + }, + "storeDescriptionText": "광란의 8 플레이어 파티 게임!\n\n깃발-탈환, 폭탄-하키, 에픽-슬로-모션-데스-매치 등의 폭발성 미니 게임 토너먼트에서\n친구들 (또는 컴퓨터)을 날려버리세요!\n\n간단한 컨트롤과 폭넓은 컨트롤러 지원을 바탕으로 최대 8명의 플레이어가 손쉽게 액션을 즐길 수 있습니다. 무료 ‘BombSquad Remote’ 앱을 통해서 모바일 기기를 컨트롤러로서 사용할 수도 있습니다!\n\n폭탄으로 날려버리세요!\n\n자세한 사항은 www.froemling.net/bombsquad에서 확인하세요.", + "storeDescriptions": { + "blowUpYourFriendsText": "친구들을 날려버리세요.", + "competeInMiniGamesText": "레이싱에서 비행까지 다양한 미니 게임에서 경쟁하세요.", + "customize2Text": "캐릭터, 미니 게임 및 사운드트랙을 커스터마이징하세요.", + "customizeText": "캐릭터를 커스터마이징하고 자신만의 미니 게임 플레이 목록을 만드세요.", + "sportsMoreFunText": "폭탄이 있으면 스포츠가 더 재미있어요.", + "teamUpAgainstComputerText": "팀으로 컴퓨터에 맞서세요." + }, + "storeText": "상점", + "submitText": "제출", + "submittingPromoCodeText": "코드 제출 중...", + "teamNamesColorText": "팀 이름/색상...", + "telnetAccessGrantedText": "텔넷 액세스가 활성화됨.", + "telnetAccessText": "텔넷 액세스가 검색됨, 허용하시겠습니까?", + "testBuildErrorText": "이 테스트 빌드는 더 이상 유효하지 않습니다. 새 버전을 확인해주십시오.", + "testBuildText": "테스트 빌드", + "testBuildValidateErrorText": "테스트 빌드를 확인할 수 없습니다. (네트워크에 연결되지 않았습니까?)", + "testBuildValidatedText": "테스트 빌드 확인 완료. 즐거운 시간 되세요!", + "thankYouText": "지원해주셔서 감사합니다! 즐거운 시간 되세요!!", + "threeKillText": "트리플 킬!!", + "timeBonusText": "시간 보너스", + "timeElapsedText": "시간 경과", + "timeExpiredText": "시간 종료", + "timeSuffixDaysText": "${COUNT}일", + "timeSuffixHoursText": "${COUNT}시간", + "timeSuffixMinutesText": "${COUNT}분", + "timeSuffixSecondsText": "${COUNT}초", + "tipText": "팁", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "절친들", + "tournamentCheckingStateText": "토너먼트 상태를 확인 중입니다. 잠시 기다리세요...", + "tournamentEndedText": "이 토너먼트는 종료되었습니다. 새 토너먼트가 곧 시작됩니다.", + "tournamentEntryText": "토너먼트 참가", + "tournamentResultsRecentText": "최근 토너먼트 결과", + "tournamentStandingsText": "토너먼트 성적", + "tournamentText": "토너먼트", + "tournamentTimeExpiredText": "토너먼트 시간이 종료되었습니다", + "tournamentsText": "토너먼트", + "translations": { + "characterNames": { + "Agent Johnson": "존슨 요원", + "B-9000": "B-9000", + "Bernard": "버나드", + "Bones": "본즈", + "Butch": "부치", + "Easter Bunny": "부활절 토끼", + "Flopsy": "플롭시", + "Frosty": "프로스티", + "Gretel": "그레텔", + "Grumbledorf": "그럼블도프 마법사", + "Jack Morgan": "잭 모건", + "Kronk": "크롱크", + "Lee": "리", + "Lucky": "럭키", + "Mel": "멜", + "Middle-Man": "삼순신", + "Minimus": "미니무스", + "Pascal": "펭귄 파스칼", + "Pixel": "픽셀", + "Sammy Slam": "새미 슬램", + "Santa Claus": "산타클로스", + "Snake Shadow": "스네이크 셰도우", + "Spaz": "스파즈", + "Taobao Mascot": "타오바오 마스코트", + "Todd McBurton": "토드 맥버튼", + "Zoe": "조우", + "Zola": "졸라" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} 트레이닝", + "Infinite ${GAME}": "무한 ${GAME}", + "Infinite Onslaught": "무한 맹습", + "Infinite Runaround": "무한 행군", + "Onslaught Training": "맹습 트레이닝", + "Pro ${GAME}": "프로 ${GAME}", + "Pro Football": "프로 축구", + "Pro Onslaught": "프로 맹습", + "Pro Runaround": "프로 행군", + "Rookie ${GAME}": "루키 ${GAME}", + "Rookie Football": "루키 축구", + "Rookie Onslaught": "루키 맹습", + "The Last Stand": "최후의 저항", + "Uber ${GAME}": "슈퍼 ${GAME}", + "Uber Football": "슈퍼 축구", + "Uber Onslaught": "슈퍼 맹습", + "Uber Runaround": "슈퍼 행군" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "승리하려면 일정 시간 동안 선택된 자가 되세요.\n그 지위를 뺏으려면 선택된 자를 처치하세요.", + "Bomb as many targets as you can.": "가능한 한 많은 목표에 폭탄을 던지세요.", + "Carry the flag for ${ARG1} seconds.": "${ARG1}초 동안 깃발을 나르세요.", + "Carry the flag for a set length of time.": "일정 시간 동안 깃발을 나르세요.", + "Crush ${ARG1} of your enemies.": "적 ${ARG1}명을 분쇄하세요.", + "Defeat all enemies.": "모든 적을 물리치세요.", + "Dodge the falling bombs.": "떨어지는 폭탄을 피하세요.", + "Final glorious epic slow motion battle to the death.": "끝까지 싸우는 장렬하고 서사적인 마지막 슬로 모션 전투.", + "Gather eggs!": "달걀을 모으시오!", + "Get the flag to the enemy end zone.": "깃발을 적진 끝까지 가져가세요.", + "How fast can you defeat the ninjas?": "얼마나 빨리 닌자들을 물리칠 수 있어요?", + "Kill a set number of enemies to win.": "승리하려면 정해진 수의 적을 처치하세요.", + "Last one standing wins.": "최후까지 살아남은 자가 승리합니다.", + "Last remaining alive wins.": "마지막까지 살아남으면 승리합니다.", + "Last team standing wins.": "최후까지 살아남은 팀이 승리합니다.", + "Prevent enemies from reaching the exit.": "적들이 출구에 도달하지 못하게 하세요.", + "Reach the enemy flag to score.": "적의 깃발에 도달해서 점수를 획득하세요.", + "Return the enemy flag to score.": "적의 깃발을 되찾아서 점수를 획득하세요.", + "Run ${ARG1} laps.": "${ARG1} 랩을 달리세요.", + "Run ${ARG1} laps. Your entire team has to finish.": "${ARG1} 랩을 달리세요. 팀 전체가 완료해야 합니다.", + "Run 1 lap.": "1 랩을 달리세요.", + "Run 1 lap. Your entire team has to finish.": "1 랩을 달리세요. 팀 전체가 완료해야 합니다.", + "Run real fast!": "정말로 빨리 달리세요!", + "Score ${ARG1} goals.": "${ARG1} 골을 기록하세요.", + "Score ${ARG1} touchdowns.": "${ARG1} 터치다운을 기록하세요.", + "Score a goal.": "1 골을 기록하세요.", + "Score a touchdown.": "1 터치다운을 기록하세요.", + "Score some goals.": "골 몇 개를 기록하세요.", + "Secure all ${ARG1} flags.": "${ARG1}개의 깃발 전부를 확보하세요.", + "Secure all flags on the map to win.": "승리하려면 지도 상의 깃발 전부를 확보하세요.", + "Secure the flag for ${ARG1} seconds.": "${ARG1}초 동안 깃발을 확보하세요.", + "Secure the flag for a set length of time.": "일정 시간 동안 깃발을 확보하세요.", + "Steal the enemy flag ${ARG1} times.": "적 깃발을 ${ARG1}번 훔치세요.", + "Steal the enemy flag.": "적 깃발을 훔치세요.", + "There can be only one.": "한 명만 남을 수 있습니다.", + "Touch the enemy flag ${ARG1} times.": "적 깃발을 ${ARG1}번 터치하세요.", + "Touch the enemy flag.": "적 깃발을 터치하세요.", + "carry the flag for ${ARG1} seconds": "${ARG1}초 동안 깃발을 나르세요", + "kill ${ARG1} enemies": "적 ${ARG1}명을 처치하세요", + "last one standing wins": "최후까지 살아남은 자가 승리합니다", + "last team standing wins": "최후까지 살아남은 팀이 승리합니다", + "return ${ARG1} flags": "깃발 ${ARG1}개를 되찾으세요", + "return 1 flag": "깃발 1개를 되찾으세요", + "run ${ARG1} laps": "${ARG1} 랩을 달리세요", + "run 1 lap": "1 랩을 달리세요", + "score ${ARG1} goals": "${ARG1} 골을 기록하세요", + "score ${ARG1} touchdowns": "${ARG1} 터치다운을 기록하세요", + "score a goal": "1 골을 기록하세요", + "score a touchdown": "1 터치다운을 기록하세요", + "secure all ${ARG1} flags": "${ARG1}개의 깃발 전부를 확보하세요", + "secure the flag for ${ARG1} seconds": "${ARG1}초 동안 깃발을 확보하세요", + "touch ${ARG1} flags": "깃발 ${ARG1}개를 터치하세요", + "touch 1 flag": "깃발 1개를 터치하세요" + }, + "gameNames": { + "Assault": "전진", + "Capture the Flag": "깃발 탈환", + "Chosen One": "선택된 자", + "Conquest": "정복", + "Death Match": "데스 매치", + "Easter Egg Hunt": "부활절 달걀 찾기", + "Elimination": "제거", + "Football": "축구", + "Hockey": "하키", + "Keep Away": "차단", + "King of the Hill": "킹 오브 더 힐", + "Meteor Shower": "유성우", + "Ninja Fight": "닌자 전투", + "Onslaught": "맹습", + "Race": "레이스", + "Runaround": "행군", + "Target Practice": "투척 연습", + "The Last Stand": "최후의 저항" + }, + "inputDeviceNames": { + "Keyboard": "키보드", + "Keyboard P2": "키보드 P2" + }, + "languages": { + "Arabic": "아랍어", + "Belarussian": "벨로루시어", + "Chinese": "중국어", + "Croatian": "크로아티아어", + "Czech": "체코어", + "Danish": "덴마크어", + "Dutch": "네덜란드어", + "English": "영어", + "Esperanto": "에스페란토", + "Finnish": "핀란드어", + "French": "프랑스어", + "German": "독일어", + "Gibberish": "횡설수설", + "Greek": "그리스어", + "Hindi": "힌디어", + "Hungarian": "헝가리어", + "Indonesian": "인도네시아어", + "Italian": "이탈리아어", + "Japanese": "일본어", + "Korean": "한국어", + "Persian": "페르시아어", + "Polish": "폴란드어", + "Portuguese": "포르투갈어", + "Romanian": "루마니아어", + "Russian": "러시아어", + "Serbian": "세르비아어", + "Spanish": "스페인어", + "Swedish": "스웨덴어", + "Turkish": "터키어", + "Ukrainian": "우크라이나어" + }, + "leagueNames": { + "Bronze": "브론즈", + "Diamond": "다이아몬드", + "Gold": "골드", + "Silver": "실버" + }, + "mapsNames": { + "Big G": "빅 G", + "Bridgit": "브리짓", + "Courtyard": "코트야드", + "Crag Castle": "크래그 캐슬", + "Doom Shroom": "둠 쉬룸", + "Football Stadium": "풋볼 스타디움", + "Happy Thoughts": "행복한 생각", + "Hockey Stadium": "하키 스타디움", + "Lake Frigid": "레이크 프리짓", + "Monkey Face": "몽키 페이스", + "Rampage": "램페이지", + "Roundabout": "런어바웃", + "Step Right Up": "스텝 라잇 업", + "The Pad": "더 패드", + "Tip Top": "팁 톱", + "Tower D": "타워 D", + "Zigzag": "지그재그" + }, + "playlistNames": { + "Just Epic": "에픽 전용", + "Just Sports": "스포츠 전용" + }, + "scoreNames": { + "Flags": "깃발", + "Goals": "골", + "Score": "점수", + "Survived": "생존", + "Time": "시간", + "Time Held": "보유 시간" + }, + "serverResponses": { + "A code has already been used on this account.": "코드가 이 계정에 이미 사용되었습니다.", + "A reward has already been given for that address.": "보상이 이미 지급되었습니다.", + "Account linking successful!": "계정 연동 성공!", + "Account unlinking successful!": "계정 연동 해제 완료!", + "Accounts are already linked.": "계정들이 이미 연동되었습니다.", + "An error has occurred; (${ERROR})": "에러가 발생했습니다; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "심각한 에러가 발생했습니다.; 지원센터로 연락해주시오. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "오류가 발생했습니다. support@froemling.net으로 문의해주십시오.", + "An error has occurred; please try again later.": "에러가 발생하였습니다; 나중에 다시 시도하여 주시기 바랍니다.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "정말로 이 계정들을 연동하시겠습니까?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\n이 작업은 취소할 수 없습니다!", + "BombSquad Pro unlocked!": "BombSquad 프로 잠금 해제!", + "Can't link 2 accounts of this type.": "이 유형의 계정 2개를 연동할 수 없습니다.", + "Can't link 2 diamond league accounts.": "다이아몬드 리그 계정 2개를 연동할 수 없습니다.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "연동할 수 없습니다. 최대 연동 계정 수 ${COUNT}(을)를 초과합니다.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "부정행위 사용이 감지되었습니다. ${COUNT}일 간 점수 및 상품이 중지됩니다.", + "Could not establish a secure connection.": "보안 연결을 수립할 수 없습니다.", + "Daily maximum reached.": "일일 한도에 도달했습니다.", + "Entering tournament...": "토너먼트에 참가 중...", + "Invalid code.": "잘못된 코드.", + "Invalid payment; purchase canceled.": "구매 무효화; 구매가 취소되었습니다.", + "Invalid promo code.": "잘못된 프로모션 코드.", + "Invalid purchase.": "잘못된 구매.", + "Invalid tournament entry; score will be ignored.": "잘못된 토너먼트 참가. 점수는 무시됩니다.", + "Item unlocked!": "아이템 잠금해제!", + "Max number of playlists reached.": "최대 플레이 목록 수에 도달했습니다.", + "Max number of profiles reached.": "최대 프로필 수에 도달했습니다.", + "Maximum friend code rewards reached.": "최대의 친구 코드 보상에 도달했습니다.", + "Profile \"${NAME}\" upgraded successfully.": "프로필 \"${NAME}\" 업그레이드 성공.", + "Profile could not be upgraded.": "프로필을 업그레이드하지 못했습니다.", + "Purchase successful!": "구매 성공!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "로그인 선물로 티켓 ${COUNT}장을 받았습니다.\n내일 ${TOMORROW_COUNT}장을 받으세요.", + "Sorry, there are no uses remaining on this code.": "죄송합니다만 이 코드에 남은 사용 횟수가 없습니다.", + "Sorry, this code has already been used.": "죄송합니다만 이 코드는 이미 사용되었습니다.", + "Sorry, this code has expired.": "죄송합니다만 이 코드는 만료되었습니다.", + "Sorry, this code only works for new accounts.": "죄송합니다만 이 코드는 새 계정에만 유효합니다.", + "The tournament ended before you finished.": "귀하가 완료하기 전에 토너먼트가 종료되었습니다.", + "This code cannot be used on the account that created it.": "이 코드는 생성된 계정에 사용할 수 없습니다.", + "This requires version ${VERSION} or newer.": "${VERSION} 버전 이상이 필요합니다.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "귀하의 기기를 이 계정에 연동하시겠습니까?\n\n귀하의 기기 계정: ${ACCOUNT1}\n이 계정: ${ACCOUNT2}\n\n이로써 기존 진행 상황을 유지할 수 있습니다.\n경고: 이 작업은 취소할 수 없습니다!", + "You already own this!": "이미 소유 중입니다!", + "You can join in ${COUNT} seconds.": "${COUNT} 초 후에 참가할 수 있습니다.", + "You don't have enough tickets for this!": "티켓이 충분하지 않습니다!", + "You don't own that.": "이미 소유하지 않았습니다.", + "You got ${COUNT} tickets!": "티켓 ${COUNT}장을 받았습니다!", + "You got a ${ITEM}!": "${ITEM}(을)를 받았습니다!", + "You have been promoted to a new league; congratulations!": "새 리그로 승격되었습니다. 축하합니다!", + "You must update to a newer version of the app to do this.": "이 작업을 하려면 새 앱 버전으로 업데이트해야 합니다.", + "You must update to the newest version of the game to do this.": "이 작업을 하려면 새로운 게임 버전으로 업데이트해야 합니다.", + "You must wait a few seconds before entering a new code.": "새 코드를 입력하기 전에 수초 간 기다려야 합니다.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "마지막 토너먼트에서 #${RANK}위에 랭크되었습니다. 플레이해주셔서 감사합니다!", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "게임 사본이 수정되었습니다.\n변경 사항을 되돌린 후 다시 시도해주십시오.", + "Your friend code was used by ${ACCOUNT}": "${ACCOUNT} 님이 친구 코드를 사용했습니다" + }, + "settingNames": { + "1 Minute": "1분", + "1 Second": "1초", + "10 Minutes": "10분", + "2 Minutes": "2분", + "2 Seconds": "2초", + "20 Minutes": "20분", + "4 Seconds": "4초", + "5 Minutes": "5분", + "8 Seconds": "8초", + "Allow Negative Scores": "마이너스 점수 허용", + "Balance Total Lives": "총 생명력 같게 하기", + "Bomb Spawning": "폭탄 생성주기", + "Chosen One Gets Gloves": "선택된 자가 글러브 획득", + "Chosen One Gets Shield": "선택된 자가 실드 획득", + "Chosen One Time": "선택된 자의 시간", + "Enable Impact Bombs": "충격 폭탄 사용", + "Enable Triple Bombs": "3연발 폭탄 사용", + "Entire Team Must Finish": "전체의 팀이 통과해야 합니다.", + "Epic Mode": "에픽 모드", + "Flag Idle Return Time": "유휴 깃발 귀환 시간", + "Flag Touch Return Time": "터치 깃발 귀환 시간", + "Hold Time": "유지 시간", + "Kills to Win Per Player": "승리하기 위한 플레이어당 처치 수", + "Laps": "랩", + "Lives Per Player": "플레이어당 생명력", + "Long": "김", + "Longer": "매우 김", + "Mine Spawning": "지뢰 출현", + "No Mines": "지뢰 없음", + "None": "없음", + "Normal": "보통", + "Pro Mode": "프로 모드", + "Respawn Times": "출현 시간", + "Score to Win": "승리 점수", + "Short": "짧음", + "Shorter": "매우 짧음", + "Solo Mode": "솔로 모드", + "Target Count": "표적 수", + "Time Limit": "시간 제한" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${PLAYER} 님이 이탈하여 ${TEAM} 팀이 실격됬습니다.", + "Killing ${NAME} for skipping part of the track!": "트랙 일부를 건너뛴 ${NAME} 님을 처치하는 중!" + }, + "teamNames": { + "Bad Guys": "나쁜 녀석들", + "Blue": "블루", + "Good Guys": "좋은 녀석들", + "Red": "레드" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "완벽한 타이밍의 달리기-점프-회전-펀치로 한 번에 처치하고\n친구들로부터 평생 존경을 받을 수 있습니다.", + "Always remember to floss.": "꼭 치실질 하는 걸 기억하세요.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "무작위로 생성된 이름 및 외관 대신 원하는 이름과 외관으로\n자신과 친구들의 플레이어 프로필을 생성하세요.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "저주 상자는 사용자를 시한 폭탄으로 변하게 합니다.\n유일한 치료 방법은 체력 팩을 빨리 집는 것입니다.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "여러 외관에도 불구하고 모든 캐릭터의 능력은 동일합니다.\n그러므로 가장 닮은 캐릭터를 아무거나 선택하세요.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "에너지 실드가 있다고 너무 건방 떨지는 마세요. 여전히 절벽으로 떨어질 수 있거든요.", + "Don't run all the time. Really. You will fall off cliffs.": "언제나 뛰지는 마세요. 정말이에요. 그러다 절벽에서 떨어질 거예요.", + "Don't spin for too long; you'll become dizzy and fall.": "너무 오랫동안 회전하지 마십시오. 현기증이 나서 넘어 질 것입니다.", + "Hold any button to run. (Trigger buttons work well if you have them)": "달리려면 아무 버튼이나 길게 누르세요. (트리거 버튼이 있으면 더 잘 작동합니다)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "아무 버튼이나 길게 누르면 달립니다. 달리면 더 빨리 우위를\n차지할 수 있으나 잘 회전이 안 되므로 절벽을 주의하세요.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "얼음 폭탄은 아주 강력하지는 않지만 맞은 것은 무엇이든\n얼려서 부서지기 쉽게 만듭니다.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "누군가 당신을 잡으면 펀치를 날리세요. 그럼 풀어줄 거예요.\n현실에서도 효과가 있어요.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "컨트롤러가 부족하면 모바일 기기에 '${REMOTE_APP_NAME}' 앱을\n설치해서 컨트롤러로 사용하세요.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "끈적이 폭탄이 당신에게 붙으면 마구 점프하고 빙빙 도세요. 폭탄을 흔들어 떨어트릴 수\n있어요. 아니면, 적어도 마지막 순간은 재미있을 거예요.", + "If you kill an enemy in one hit you get double points for it.": "적을 한 번에 처치하면 점수를 두 배로 얻어요.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "저주에 걸리면 생존 가능한 유일한 희망은 다음\n수초 이내에 체력 파워업을 찾는 거예요.", + "If you stay in one place, you're toast. Run and dodge to survive..": "한 곳에 머물면 토스트가 되버릴 거예요. 달리고 피해서 생존하세요.", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "많은 플레이어가 들어왔다 나갔다 하면 설정에서 '부재 중-플레이어-자동-추방'을\n켜세요. 게임 나가기를 잊은 플레이어가 있을 수 있거든요.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "기기가 너무 뜨거워지거나 배터리를 절약하고 싶으면\n설정->그래픽에서 '비주얼' 또는 '해상도'를 낮게 하세요", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "프레임 속도가 고르지 않으면 게임의 그래픽 설정에서\n해상도 또는 비주얼을 낮게 해보세요.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "깃발-탈환에서 점수를 획득하려면 자신의 깃발이 자신의 기지에 있어야 해요.\n다른 팀이 점수를 얻으려고 할 때 깃발을 훔치는 것도 방해하는 좋은 방법이에요.", + "In hockey, you'll maintain more speed if you turn gradually.": "하키에서 점차적으로 회전하면 더 큰 스피드를 유지할 수 있어요.", + "It's easier to win with a friend or two helping.": "한 두 명의 도와주는 친구가 있으면 더 쉽게 이길 수 있어요.", + "Jump just as you're throwing to get bombs up to the highest levels.": "더 높은 곳으로 폭탄을 던지려면 던지면서 점프하세요.", + "Land-mines are a good way to stop speedy enemies.": "지뢰는 빠르게 움직이는 적들을 저지할 때 좋은 수단이에요.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "다른 플레이어를 포함해서 많은 것을 집고 던질 수 있어요. 적들을 절벽 밖으로\n던지는 것은 효과적이고 만족스러운 전략이 될 수 있어요.", + "No, you can't get up on the ledge. You have to throw bombs.": "아니요, 바위 턱에는 올라갈 수 없어요. 폭탄을 던져야 해요.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "대부분의 게임에서 플레이어들이 중간에 들어오고 나갈 수 있어요.\n그리고 언제든 컨트롤러를 연결하거나 분리할 수도 있어요.", + "Practice using your momentum to throw bombs more accurately.": "탄력을 이용해 더 정확히 폭탄을 던지는 법을 연습하세요.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "주먹을 빠르게 움직일수록 펀치로 더 많은 대미지를 가해요.\n그러니 미친 듯이 달리고 점프하고 회전하세요.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "폭탄을 던지기 전에 왔다 갔다 달리면 \n더 멀리 던질 수 있어요.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "TNT 상자 근처에서 폭탄을 터트려\n적의 무리를 처치하세요.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "목은 가장 약한 부분이어서 끈적이 폭탄이 머리에 붙으면\n보통은 게임 오버죠.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "이 레벨은 절대로 끝나지 않아요. 그러나 이곳에서 높은 점수를\n얻으면 전 세계에서 영원한 존경을 받게 될 거예요.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "던지는 힘은 길게 누르는 방향에 따라 달라져요.\n무언가 앞으로 가볍게 던지려면 어떤 방향도 길게 누르지 마세요.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "사운드트랙이 싫증나세요? 자신의 입맛대로 바꾸세요!\n설정->오디오->사운드트랙에서 변경하세요.", + "Try 'Cooking off' bombs for a second or two before throwing them.": "던지기 전에 1~2초 동안 폭탄을 자연 발화시켜보세요.", + "Try tricking enemies into killing eachother or running off cliffs.": "적들을 속여서 서로 죽이게 하거나 절벽에서 떨어지게 해보세요.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "깃발을 집으려면 줍기 버튼을 사용하세요 < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "더 멀리 던지려면 왔다 갔다 하세요.", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "왼쪽 또는 오른쪽으로 회전해서 펀치를 '조준'할 수 있어요.\n모서리에서 악당들을 밀쳐내거나 하키에서 점수를 딸 때 유용해요.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "도화선 불꽃의 색깔로 폭탄이 언제 폭발할지 판단할 수 있어요.\n노랑.. 오렌지.. 빨강.. 그리고 터져버리죠.", + "You can throw bombs higher if you jump just before throwing.": "던지기 바로 전에 점프해서 폭탄을 더 높이 던질 수 있어요.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "머리를 물체에 부딪히면 대미지를 입어요.\n그러니 머리를 물체에 부딪히지 않도록 하세요.", + "Your punches do much more damage if you are running or spinning.": "달리거나 회전하고 있으면 펀치로 더 많은 대미지를 가해요." + } + }, + "trophiesRequiredText": "이것은 최소 ${NUMBER} 개의 트로피가 필요합니다.", + "trophiesText": "트로피", + "trophiesThisSeasonText": "이번 시즌 트로피", + "tutorial": { + "cpuBenchmarkText": "말도 안되는 속도로 튜토리얼을 실행 중(우선 CPU 속도를 테스트합니다)", + "phrase01Text": "안녕하세요!", + "phrase02Text": "${APP_NAME}에 환영합니다!", + "phrase03Text": "캐릭터를 컨트롤하는 팁을 몇 가지 알려드릴 게요.", + "phrase04Text": "${APP_NAME}의 많은 것이 물리학에 바탕을 두고 있어요.", + "phrase05Text": "예를 들어, 펀치를 날릴 때...", + "phrase06Text": "...대미지는 주먹의 속도에 기반하죠.", + "phrase07Text": "보셨죠? 움직이지 않으니 ${NAME} 님에게 거의 피해를 안 줘요.", + "phrase08Text": "이제 점프 및 회전을 해서 스피드를 더 올려보세요.", + "phrase09Text": "네, 훨씬 더 낫군요.", + "phrase10Text": "달리는 것도 도움이 돼요.", + "phrase11Text": "달리려면 아무 버튼이나 길게 누르세요.", + "phrase12Text": "멋진 펀치를 날리려면 달리고 회전해보세요.", + "phrase13Text": "어이쿠, 저 ${NAME} 님에게 미안하군요.", + "phrase14Text": "깃발이나... 또는 ${NAME} 님 같은 물체를 집을 수 있어요.", + "phrase15Text": "마지막으로 폭탄이 있군요.", + "phrase16Text": "폭탄을 던지려면 연습이 필요해요.", + "phrase17Text": "윽! 멋지게 던지지 못했군요.", + "phrase18Text": "움직이면 더 멀리 던지는 데 도움이 돼요.", + "phrase19Text": "점프하면 더 높이 던지는 데 도움이 돼죠.", + "phrase20Text": "폭탄을 '왔다 갔다'하면 훨씬 더 멀리 던질 수 있어요.", + "phrase21Text": "폭탄의 타이밍을 맞추는 게 까다로울 수 있어요.", + "phrase22Text": "이런.", + "phrase23Text": "1, 2초 동안 도화선을 '자연 발화'시켜보세요.", + "phrase24Text": "만세! 멋지게 태웠군요.", + "phrase25Text": "그럼, 다 됐네요.", + "phrase26Text": "이제 출발하세요!", + "phrase27Text": "배운 걸 기억하면 살아서 돌아올 거예요!", + "phrase28Text": "...아마도, 그럴 거예요...", + "phrase29Text": "행운을 빌어요!", + "randomName1Text": "프레드", + "randomName2Text": "해리", + "randomName3Text": "빌", + "randomName4Text": "척", + "randomName5Text": "필", + "skipConfirmText": "정말로 튜토리얼을 건너뛰세요? 눌러서 확인하세요.", + "skipVoteCountText": "${COUNT}/${TOTAL} 건너뜀", + "skippingText": "튜토리얼을 건너뛰는 중...", + "toSkipPressAnythingText": "(튜토리얼을 건너뛰려면 아무거나 누르세요)" + }, + "twoKillText": "더블 킬!", + "unavailableText": "이용할 수 없음", + "unconfiguredControllerDetectedText": "구성되지 않은 컨트롤러가 검색됨:", + "unlockThisInTheStoreText": "상점에서 잠금 해제해야 합니다.", + "unlockThisProfilesText": "${NUM}개 이상의 프로필을 만들기 위해, 다음 사항이 필요합니다. :", + "unlockThisText": "잠금 해제 필요 사항:", + "unsupportedHardwareText": "죄송합니다만 이 하드웨어는 본 게임 빌드에 의해 지원되지 않습니다.", + "upFirstText": "첫 번째:", + "upNextText": "게임 ${COUNT}의 다음:", + "updatingAccountText": "계정 업데이트 중...", + "upgradeText": "업그레이드", + "upgradeToPlayText": "플레이하려면 상점에서 \"${PRO}\"를 잠금 해제하세요.", + "useDefaultText": "기본값 사용", + "usesExternalControllerText": "이 게임은 외부 컨트롤러를 입력용으로 사용합니다.", + "usingItunesText": "사운트트랙에 iTunes 사용 중...", + "usingItunesTurnRepeatAndShuffleOnText": "iTunes에서 임의 재생이 켜져있고 반복은 모두로 되어 있는지 확인해주십시오.", + "validatingTestBuildText": "테스트 빌드 확인 중...", + "victoryText": "승리!", + "voteDelayText": "${NUMBER} 초 동안 다른 투표를 시작할 수 없습니다.", + "voteInProgressText": "투표가 이미 진행 중입니다.", + "votedAlreadyText": "이미 투표했습니다.", + "votesNeededText": "${NUMBER} 표 필요", + "vsText": "대", + "waitingForHostText": "(${HOST}를 기다리는 중)", + "waitingForPlayersText": "플레이어들의 가입을 기다리는 중...", + "waitingInLineText": "줄 서서 기다리는 중 (파티가 꽉 찼습니다)...", + "watchAVideoText": "비디오 감상", + "watchAnAdText": "광고 감상", + "watchWindow": { + "deleteConfirmText": "\"${REPLAY}\"를 삭제하시겠습니까?", + "deleteReplayButtonText": "다시 보기\n삭제", + "myReplaysText": "내 다시 보기", + "noReplaySelectedErrorText": "선택된 다시 보기 없음", + "playbackSpeedText": "재생 속도: ${SPEED}", + "renameReplayButtonText": "다시 보기\n이름 바꾸기", + "renameReplayText": "\"${REPLAY}\" 이름 변경", + "renameText": "이름 바꾸기", + "replayDeleteErrorText": "다시 보기 삭제 오류.", + "replayNameText": "다시 보기 이름", + "replayRenameErrorAlreadyExistsText": "같은 이름의 다시 보기가 이미 존재합니다.", + "replayRenameErrorInvalidName": "다시 보기의 이름을 바꿀 수 없습니다. 잘못된 이름.", + "replayRenameErrorText": "다시 보기 이름 바꾸기 오류", + "sharedReplaysText": "공유된 다시 보기", + "titleText": "감상", + "watchReplayButtonText": "다시 보기\n감상" + }, + "waveText": "웨이브", + "wellSureText": "확인!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote 저작권" + }, + "wiimoteListenWindow": { + "listeningText": "Wii 리모컨 등록중...", + "pressText": "Wii 리모컨의 1과 2를 동시에 누르시오.", + "pressText2": "신형 Wii 리모컨 모션 플러스의 경우는 뒤의 빨간 등록 버튼을 누르시오." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote 저작권", + "listenText": "등록하기", + "macInstructionsText": "Wiii를 꺼 놓고 블루투스를 킨 후 '등록하기' 버튼을 누르시오.\n현재 Wii 리모컨으로 하는 경우는 다소 잘 안될 수 도 있으므로\n연결 전 테스트를 몇번 하시는 것을 추천합니다.\n\n기본적으로 블루투스는 총 7개의 Wii 리모컨을 지원하나\n실제 성능은 다를 수 있습니다.\n\n\nBombSquad는 Wii 리모컨, 눈차크,\n그리고 클래식 컨트롤러를 지원합니다.\n현재 신형 Wii 리모컨 모션 플러스도 지원하나\n눈차크 등 추가 컨트롤러는 지원하지 않습니다.", + "thanksText": "이 기능을 가능하게 해 주신\nDarwiinRemote 팀에게 감사드립니다.", + "titleText": "Wii 리모컨 설정" + }, + "winsPlayerText": "${NAME} 님 승리!", + "winsTeamText": "${NAME} 팀 승리!", + "winsText": "${NAME} 님 승리!", + "worldScoresUnavailableText": "세계 기록을 이용할 수 없습니다.", + "worldsBestScoresText": "세계 최고 점수", + "worldsBestTimesText": "세계 최고 시간", + "xbox360ControllersWindow": { + "getDriverText": "드라이버 받기", + "macInstructions2Text": "컨트롤러를 무선으로 이용하기 위해선 윈도우즈용 Xbox 360 무선 컨트롤러와\n같이 있는 무선 리시버가 필요합니다.\n리시버 하나당 최대 4개의 컨트롤러를 연결할 수 있습니다.\n\n중요: 마이크로소프트 외의 회사에서 개발한 리시버의 경우 해당 드라이버와의\n사용이 불가능 합니다; 무선 리시버에 'XBOX 360'이 아니라 'Microsoft'가\n적혀 있어야 합니다. 현재 마이크로소프트는 이 무선 리시버를 단품으로 팔지 않으므로\n무선 리시버가 있는 컨트롤러를 구매거나 온라인 쇼핑몰에서 찾아야 합니다.\n\n이 드라이버가 유용하다면 드라이버 개발진에게\n기부를 하시는 것을 추천합니다.", + "macInstructionsText": "Xbox 360 컨트롤러를 사용하기 위해선 하단 링크의 맥용 드라이버를\n설치하신 후 사용하실 수 있습니다.\n유선 컨트롤러와 무선 컨트롤러 둘다 지원합니다.", + "ouyaInstructionsText": "Xbox 360 컨트롤러를 BombSquad에 사용하기 위해선 기기의\nUSB 포트에 연결하시면 됩니다. 여러개의 컨트롤러를 연결하시려면\nUSB 허브를 사용하시면 됩니다.\n\n무선 컨트롤러를 사용하기 위해선 윈도우즈용 시중에 판매중인\nXbox 360 무선 컨트롤러용 무선 리시버를 사용하시면 됩니다.\n각 리시버를 USB 포트에 연결하시면\n각 리시버당 4개의 컨트롤러를 연결 할 수 있습니다.", + "titleText": "${APP_NAME} 앱과 함께 Xbox 360 컨트롤러 사용하기:" + }, + "yesAllowText": "예, 허용합니다!", + "yourBestScoresText": "내 최고 점수", + "yourBestTimesText": "내 최고 시간" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/persian.json b/dist/ba_data/data/languages/persian.json new file mode 100644 index 0000000..73dcefb --- /dev/null +++ b/dist/ba_data/data/languages/persian.json @@ -0,0 +1,1853 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "نام نمی‌تواند اموجی (شکلک) یا نویسه‌های ویژه داشته باشد", + "accountProfileText": "(مشخصات حساب)", + "accountsText": "حساب‌ها", + "achievementProgressText": "${TOTAL} از‎ ${COUNT} :دستاوردها", + "campaignProgressText": "${PROGRESS} :[سخت]‎ پیشروی در بازی اصلی", + "changeOncePerSeason": ".فقط یک‌بار در هر فصل می‌توانید این مورد را تغییر دهید", + "changeOncePerSeasonError": "(روز تا فصل بعد‎ ${NUM}) برای تغییر این گزینه باید تا فصل بعد صبر کنید", + "customName": "نام سفارشی", + "linkAccountsEnterCodeText": "کد را وارد کنید", + "linkAccountsGenerateCodeText": "ایجاد کد", + "linkAccountsInfoText": "(به اشتراک گذاری پیشروی بین دستگاه‌های مختلف)", + "linkAccountsInstructionsNewText": "برای اتصال دو حساب کاربری، ابتدا یک کد در حساب اول بسازید\nسپس آن را در حساب دوم وارد کنید. پس از این کار \n.اطلاعات حساب دوم بین دو حساب به اشتراک گذاشته می‌شود\n(اطلاعات حساب اول از بین خواهد رفت)\n\n.حساب را به هم متصل کنید‎ ${COUNT} شما می‌توانید تا\n\nتوجه: تنها حساب‌هایی که متعلق به خودتان است را\nبه هم متصل کنید! اگر به حساب دوستتان متصل\n.شوید، نمی‌توانید همزمان آنلاین بازی کنید", + "linkAccountsInstructionsText": "برای اتصال دو حساب، در یکی از\nآن ها کدی ایجاد کرده \nو آن را در دیگری وارد کنید.\nپیشرفت ها و موجودی ترکیب خواهد شد.\nحساب را وصل کنید ${COUNT} شما می توانید.\n!توجه : فقط حساب هایی را وصل کنید که برای\n شماست\nاگر شما حساب دیگری را وصل کنید، شما توانایی این را ندارید که در یک زمان بازی کنید!\nاین عمل برگشت پذیر نیست، پس \nدقت کنید!", + "linkAccountsText": "متصل کردن حساب‌ها", + "linkedAccountsText": "حساب‌های متصل‌شده:‎", + "nameChangeConfirm": "تغییر کند؟‎ ${NAME} آیا نام شما به", + "resetProgressConfirmNoAchievementsText": "همهٔ پیشروی‌های شما در بخش همکاری و بالاترین امتیازات\nشما پاک خواهد شد. (به استثنای بلیت‌های شما)\nاین کار برگشت‌پذیر نیست. آیا مطمئنید؟", + "resetProgressConfirmText": "همهٔ پیشروی‌ها در بخش همکاری، دستاوردها\n.و امتیازات بالای شما پاک خواهد شد\n(به استثنای بلیت‌های شما)\nاین کار برگشت‌پذیر نیست. آیا مطمئنید؟", + "resetProgressText": "بازنشانی پیشروی", + "setAccountName": "تنظیم نام حساب", + "setAccountNameDesc": "نامی که می‌خواهید برای این حساب نمایش داده شود را انتخاب کنید\nمی‌توانید از یکی از نام‌های حساب‌های وصل‌شده استفاده کنید\nیا یک نام جدید بسازید", + "signInInfoText": "به حسابتان وصل شوید تا بلیت جمع کنید، آنلاین رقابت کنید \n.و پیشرفت خود را با دیگران به اشتراک بگذارید", + "signInText": "ورود به حساب", + "signInWithDeviceInfoText": "(یک حساب خودکار فقط از این دستگاه در دسترس می‌باشد)", + "signInWithDeviceText": "وصل شدن با حساب دستگاه", + "signInWithGameCircleText": "وصل شدن از طریق حوزهٔ بازی", + "signInWithGooglePlayText": "ورود با حساب بازی‌های گوگل", + "signInWithTestAccountInfoText": "(حساب میراثی؛ از حساب‌های دستگاه برای پیشروی استفاده می‌کند)", + "signInWithTestAccountText": "ورود با حساب آزمایشی", + "signOutText": "خروج از حساب", + "signingInText": "در حال اتصال…", + "signingOutText": "در حال خروج…", + "ticketsText": "${COUNT} :بلیت‌ها", + "titleText": "حساب", + "unlinkAccountsInstructionsText": "یک حساب را برای جداسازی انتخاب کنید", + "unlinkAccountsText": "جداسازی حساب‌ها", + "viaAccount": "(${NAME} از طریق حساب)", + "youAreSignedInAsText": ":با این حساب وصل شده‌اید" + }, + "achievementChallengesText": "چالش‌های دستاورددار", + "achievementText": "دستاورد", + "achievements": { + "Boom Goes the Dynamite": { + "description": ".نابود کن TNT سه حریف را با", + "descriptionComplete": "!نابود کردی TNT سه حریف را با", + "descriptionFull": "از بین ببر TNT با ${LEVEL} سه حریف را در", + "descriptionFullComplete": "از بین بردی TNT با ${LEVEL} سه حریف را در", + "name": "!ترکوندی" + }, + "Boxer": { + "description": "بدون استفاده از هیچ بمبی برنده شو", + "descriptionComplete": "بدون استفاده از بمب برنده شدی", + "descriptionFull": "را بدون استفاده از هیچ بمبی کامل کن ${LEVEL} مرحلهٔ", + "descriptionFullComplete": "را بدون استفاده از بمب کامل کردی ${LEVEL} مرحلهٔ", + "name": "مشت‌زن" + }, + "Dual Wielding": { + "descriptionFull": "(‏دو دستهٔ بازی وصل کن ‏(سخت‌افزاری یا نرم‌افزاری", + "descriptionFullComplete": "(دو دستهٔ بازی وصل کردی (سخت‌افزار یا نرم‌افزار", + "name": "دوگانه اداره کردن" + }, + "Flawless Victory": { + "description": "بدون ضربه خوردن برنده شو", + "descriptionComplete": "بدون ضربه خوردن برنده شدی", + "descriptionFull": "را بدون ضربه خوردن برنده شو ${LEVEL} مرحلهٔ", + "descriptionFullComplete": "را بدون ضربه خوردن برنده شدی ${LEVEL} مرحلهٔ", + "name": "پیروزی بی‌نقص" + }, + "Free Loader": { + "descriptionFull": "یک بازی به سبک تک به تک را با ۲+ بازیکن شروع کن", + "descriptionFullComplete": "یک بازی تک به تک را با ۲+ بازیکن شروع کردی", + "name": "بارگذار رایگان" + }, + "Gold Miner": { + "description": "شش حریف را با مین زمینی نابود کن", + "descriptionComplete": "شش حریف را با مین زمینی نابود کردی", + "descriptionFull": "با مین زمینی از بین ببر ${LEVEL} شش حریف را در مرحلهٔ", + "descriptionFullComplete": "با مین زمینی نابود کردی ${LEVEL} شش حریف را در مرحلهٔ", + "name": "مین‌گذار حرفه‌ای" + }, + "Got the Moves": { + "description": "بدون استفاده از مشت یا بمب برنده شو", + "descriptionComplete": "بدون استفاده از هیچ مشت یا بمبی برنده شدی", + "descriptionFull": "را بدون مشت یا بمب برنده شو ${LEVEL} بازی", + "descriptionFullComplete": "را بدون مشت یا بمب برنده شدی ${LEVEL} بازی", + "name": "عجب حرکتی" + }, + "In Control": { + "descriptionFull": "(یک دستهٔ بازی وصل کن (سخت‌افزاری یا نرم‌افزاری", + "descriptionFullComplete": "(یک دستهٔ بازی وصل کردی (سخت‌افزار یا نرم‌افزار", + "name": "تحت کنترل" + }, + "Last Stand God": { + "description": "‏۱۰۰۰ امتیاز بگیر", + "descriptionComplete": "‏۱۰۰۰ امتیاز گرفتی‏", + "descriptionFull": "‏۱۰۰۰ امتیاز بگیر ${LEVEL} در مرحلهٔ", + "descriptionFullComplete": "‏۱۰۰۰ امتیاز گرفتی‏ ${LEVEL} در مرحلهٔ", + "name": "${LEVEL} سَرور" + }, + "Last Stand Master": { + "description": "‏۲۵۰ امتیاز بگیر", + "descriptionComplete": "‏۲۵۰ امتیاز گرفتی", + "descriptionFull": "‏۲۵۰ امتیاز بگیر ${LEVEL} در مرحلهٔ", + "descriptionFullComplete": "‏۲۵۰ امتیاز گرفتی ${LEVEL} در مرحلهٔ", + "name": "${LEVEL} استاد" + }, + "Last Stand Wizard": { + "description": "‏۵۰۰ امتیاز بگیر", + "descriptionComplete": "‏۵۰۰ امتیاز گرفتی", + "descriptionFull": "‏۵۰۰ امتیاز بگیر ${LEVEL} در مرحلهٔ", + "descriptionFullComplete": "‏۵۰۰ امتیاز گرفتی ${LEVEL} در مرحلهٔ", + "name": "${LEVEL} جادوگر" + }, + "Mine Games": { + "description": "سه حریف را با مین زمینی از بین ببر", + "descriptionComplete": "سه حریف را با مین زمینی از بین بردی", + "descriptionFull": "با مین از بین ببر ${LEVEL} سه حریف را در مرحلهٔ", + "descriptionFullComplete": "با مین از بین بردی ${LEVEL} سه حریف را در مرحلهٔ", + "name": "بازی با مین" + }, + "Off You Go Then": { + "description": "سه حریف رو از نقشه بنداز پایین", + "descriptionComplete": "سه حریف رو از نقشه انداختی پایین", + "descriptionFull": "از نقشه بنداز پایین${LEVEL}سه حریف رو در مرحله ی", + "descriptionFullComplete": "از نقشه انداختی پایین ${LEVEL} سه حریف رو در مرحله ی", + "name": "حالا میتونی بری" + }, + "Onslaught God": { + "description": "پنج هزار امتیاز بگیر", + "descriptionComplete": "پنج هزار امتیاز گرفتی", + "descriptionFull": "بگیر${LEVEL}پنج هزار امتیاز در مرحله ی", + "descriptionFullComplete": "گرفتی${LEVEL}پنج هزار امتیاز در مرحله ی", + "name": "${LEVEL} سَرور" + }, + "Onslaught Master": { + "description": "پونصد امتیاز بگیر", + "descriptionComplete": "پونصد امتیاز گرفتی", + "descriptionFull": "بگیر ${LEVEL} پونصد امتیاز در مرحله ی", + "descriptionFullComplete": "گرفتی ${LEVEL} پونصد امتیاز در مرحله ی", + "name": "${LEVEL} استاد" + }, + "Onslaught Training Victory": { + "description": "تمام موج ها را بگذران", + "descriptionComplete": "تمام موج ها را گذراندی", + "descriptionFull": "بگذران ${LEVEL} تمام موج ها رو در مرحله ی", + "descriptionFullComplete": "گذراندی ${LEVEL} تمام موج ها رو در مرحله ی", + "name": "${LEVEL} پیروزی" + }, + "Onslaught Wizard": { + "description": "هزار امتیاز بگیر", + "descriptionComplete": "هزار امتیاز گرفتی", + "descriptionFull": "بگیر ${LEVEL} هزار امتیاز در مرحله ی", + "descriptionFullComplete": "گرفتی ${LEVEL} هزار امتیاز در مرحله ی", + "name": "${LEVEL} جادوگر" + }, + "Precision Bombing": { + "description": "بدون گرفتن هیچ جعبه ای برنده شو", + "descriptionComplete": "بدون گرفتن هیچ جعبه ای برنده شدی", + "descriptionFull": "رو بدون گرفتن جعبه برنده شو ${LEVEL} مرحله ی", + "descriptionFullComplete": "رو بدون گرفتن جعبه برنده شدی ${LEVEL} مرحله ی", + "name": "بمب اندازی دقیق" + }, + "Pro Boxer": { + "description": "بدون استفاده از هر بمبی برنده شو", + "descriptionComplete": "بدون استفاده از هر بمبی برنده شدی", + "descriptionFull": "رو بدون استفاده از هر بمبی تمام کن ${LEVEL} مرحله ی", + "descriptionFullComplete": "رو بدون استفاده از هر بمبی تمام کردی ${LEVEL} مرحله", + "name": "مشت‌زن حرفه‌ای" + }, + "Pro Football Shutout": { + "description": "بدون اینکه اجازه بدی حریف امتیاز بگیره برنده شو", + "descriptionComplete": "بدون اینکه اجازه بدی حریف امتیاز بگیره برنده شدی", + "descriptionFull": "اجازه نده حریف امتیاز بگیره و برنده شو ${LEVEL} در مرحله", + "descriptionFullComplete": "اجازه ندادی حریف امتیاز بگیره و برنده شدی ${LEVEL} در مرحله", + "name": "${LEVEL} امتیاز ندادن" + }, + "Pro Football Victory": { + "description": "برنده شو", + "descriptionComplete": "! برنده شدی", + "descriptionFull": "برنده شو ${LEVEL} در مرحله", + "descriptionFullComplete": "برنده شدی ${LEVEL} در مرحله", + "name": "${LEVEL} پیروزی" + }, + "Pro Onslaught Victory": { + "description": "همه ی موج ها را بگذران", + "descriptionComplete": "همه ی موج ها را گذراندی", + "descriptionFull": "همه ی موج ها را بگذران ${LEVEL} در مرحله", + "descriptionFullComplete": "همه ی موج ها را گذراندی ${LEVEL} در مرحله", + "name": "${LEVEL} پیروزی" + }, + "Pro Runaround Victory": { + "description": "همه ی موج ها را بگذران", + "descriptionComplete": "همه ی موج ها را گذراندی", + "descriptionFull": "همه ی موج ها را بگذران ${LEVEL} در مرحله", + "descriptionFullComplete": "همه ی موج ها را گذراندی ${LEVEL} در مرحله", + "name": "${LEVEL} پیروزی" + }, + "Rookie Football Shutout": { + "description": "برنده شو و اجازه نده حریف امتیاز بگیره", + "descriptionComplete": "برنده شدی و اجازه ندادی حریف امتیاز بگیره", + "descriptionFull": "برنده شو و اجازه نده حریف امتیاز بگیره ${LEVEL} در مرحله", + "descriptionFullComplete": "برنده شدی و اجازه ندادی حریف امتیاز بگیره ${LEVEL} در مرحله", + "name": "${LEVEL} بدون امتیاز دادن" + }, + "Rookie Football Victory": { + "description": "برنده شو", + "descriptionComplete": "برنده شدی", + "descriptionFull": "برنده شو ${LEVEL} در مرحله", + "descriptionFullComplete": "برنده شدی ${LEVEL} در مرحله", + "name": "${LEVEL} پیروزی" + }, + "Rookie Onslaught Victory": { + "description": "همه ی موج ها را بگذران", + "descriptionComplete": "همه ی موج ها را گذراندی", + "descriptionFull": "همه ی موج ها را بگذران ${LEVEL} در مرحله", + "descriptionFullComplete": "همه ی موج ها را گذراندی ${LEVEL} در مرحله", + "name": "${LEVEL} پیروزی" + }, + "Runaround God": { + "description": "دو هزار امتیاز بگیر", + "descriptionComplete": "دو هزار امتیاز گرفتی", + "descriptionFull": "دو هزار امتیاز بگیر ${LEVEL} در مرحله", + "descriptionFullComplete": "دو هزار امتیاز گرفتی ${LEVEL} در مرحله", + "name": "${LEVEL} سَرور" + }, + "Runaround Master": { + "description": "پانصد امتیاز بگیر", + "descriptionComplete": "پانصد امتیاز گرفتی", + "descriptionFull": "پونصد امتیاز بگیر ${LEVEL} در مرحله", + "descriptionFullComplete": "پونصد امتیاز گرفتی ${LEVEL} در مرحله", + "name": "${LEVEL} استاد" + }, + "Runaround Wizard": { + "description": "هزار امتیاز بگیر", + "descriptionComplete": "هزار امتیاز گرفتی", + "descriptionFull": "هزار امتیاز بگیر ${LEVEL} در مرحله", + "descriptionFullComplete": "هزار امتیاز گرفتی ${LEVEL} در مرحله", + "name": "${LEVEL} جادوگر" + }, + "Sharing is Caring": { + "descriptionFull": "بازی را با یک دوست به اشتراک بگذار", + "descriptionFullComplete": "بازی را با یک دوست به اشتراک گذاشتی", + "name": "به اشتراک‌گذاشتن یه‌جور مراقبت است" + }, + "Stayin' Alive": { + "description": "بدون از بین‌رفتن برنده‌شو", + "descriptionComplete": "بدون از بین‌رفتن برنده شدی", + "descriptionFull": "بدون از بین رفتن شو ${LEVEL} در مرحله", + "descriptionFullComplete": "بدون از بین رفتن برنده شدی ${LEVEL} در مرحله", + "name": "زنده ماندن" + }, + "Super Mega Punch": { + "description": "فقط با یک مشت، یکی رو نابود کن", + "descriptionComplete": "فقط با یک مشت، یه نفرو کشتی", + "descriptionFull": "فقط با یک مشت، یه نفر رو نابود کن ${LEVEL} در مرحله", + "descriptionFullComplete": "فقط با یک مشت، یه نفر رو کشتی ${LEVEL} در مرحله", + "name": "مشت فوق‌العاده" + }, + "Super Punch": { + "description": "با یک مشت، نصف جون یه نفر رو ببر", + "descriptionComplete": "با یک مشت، نصف جون یه نفر رو بردی", + "descriptionFull": "با یک مشت، نصف جون یه نفر رو ببر ${LEVEL} در مرحله", + "descriptionFullComplete": "با یک مشت، نصف جون یه نفر رو بردی ${LEVEL} در مرحله", + "name": "مشت فوق‌العاده" + }, + "TNT Terror": { + "description": "نابود کنTNT شش حریف رو با", + "descriptionComplete": "نابود کردی TNT شش حریف رو با", + "descriptionFull": "نابود کن TNT شش حریف رو با ${LEVEL} در مرحله", + "descriptionFullComplete": "نابود کردی TNT شش حریف رو با ${LEVEL} در مرحله", + "name": "TNT وحشت" + }, + "Team Player": { + "descriptionFull": "یه بازی تیمی با چهارتا از دوستات شروع کن", + "descriptionFullComplete": "یه بازی تیمی با چهارتا از دوستات شروع کردی", + "name": "بازیکن تیم" + }, + "The Great Wall": { + "description": "جلوی تک تک حریف ها رو بگیر", + "descriptionComplete": "جلوی تک تک حریف ها رو گرفتی", + "descriptionFull": "جلوی تک تک حریف ها رو بگیر ${LEVEL} در مرحله", + "descriptionFullComplete": "جلوی تک تک حریف ها رو گرفتی ${LEVEL} در مرحله", + "name": "دیوار بزرگ" + }, + "The Wall": { + "description": "جلوی تک تک حریف ها رو بگیر", + "descriptionComplete": "جلوی تک تک حریف ها رو گرفتی", + "descriptionFull": "جلوی تک تک حریف ها رو بگیر ${LEVEL} در مرحله", + "descriptionFullComplete": "جلوی تک تک حریف ها رو بگیر ${LEVEL} در مرحله", + "name": "دیوار" + }, + "Uber Football Shutout": { + "description": "برنده شو و اجازه نده حریف امتیاز بگیره", + "descriptionComplete": "برنده شدی و اجازه ندادی حریف امتیاز بگیره", + "descriptionFull": "برنده شو و اجازه نده حریف امتیاز بگیره ${LEVEL} در مرحله", + "descriptionFullComplete": "برنده شدی و اجازه ندادی حریف امتیاز بگیره ${LEVEL} در مرحله", + "name": "${LEVEL} بدون امتیاز دادن" + }, + "Uber Football Victory": { + "description": "برنده شو", + "descriptionComplete": "برنده شدی", + "descriptionFull": "برنده شو ${LEVEL} در مرحله", + "descriptionFullComplete": "برنده شدی ${LEVEL} در مرحله", + "name": "${LEVEL} پیروزی" + }, + "Uber Onslaught Victory": { + "description": "تمام موج ها رو بگذران", + "descriptionComplete": "تمام موج ها رو گذراندی", + "descriptionFull": "تمام موج ها رو بگذران ${LEVEL} در مرحله", + "descriptionFullComplete": "تمام موج ها رو گذراندی ${LEVEL} در مرحله", + "name": "${LEVEL} پیروزی" + }, + "Uber Runaround Victory": { + "description": "همه ی موج ها رو بگذران", + "descriptionComplete": "همه ی موج ها رو گذرونی", + "descriptionFull": "همه ی موج ها رو بگذران ${LEVEL} در مرحله", + "descriptionFullComplete": "همه ی موج ها رو گذروندی ${LEVEL} در مرحله", + "name": "${LEVEL} پیروزی" + } + }, + "achievementsRemainingText": "دستاورد های باقیمانده:", + "achievementsText": "دستاوردها", + "achievementsUnavailableForOldSeasonsText": "ببخشید، دستاوردهای مخصوص فصل گذشته در دسترس نیستند", + "addGameWindow": { + "getMoreGamesText": "...بازی های بیشتر", + "titleText": "افزودن بازی" + }, + "allowText": "اجازه دادن", + "alreadySignedInText": "این حساب کاربری توسط یک دستگاه دیگر در حال استفاده می باشد.\nلطفا از حساب کاربری دیگری استفاده کنید یا بازی را \nدر بقیه دستگاه هایتان ببندید و دوباره امتحان کنید.", + "apiVersionErrorText": "نیاز داریم ${VERSION_REQUIRED} است. به ورژن ${VERSION_USED} بالا نمی آید. هدفش ${NAME} مدل", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(به صورت اتوماتیک فعال شود وقتی که هدفون متصل است)", + "headRelativeVRAudioText": "(صدای واقعیت مجازی(مخصوص هدفون", + "musicVolumeText": "صدای موسیقی", + "soundVolumeText": "صدای بازی", + "soundtrackButtonText": "تراک های موسیقی", + "soundtrackDescriptionText": "(موسیقی مورد نظر خود را برای هنگام بازی تعیین کنید)", + "titleText": "صدا" + }, + "autoText": "خودکار", + "backText": "بازگشت", + "banThisPlayerText": "این بازیکن را محروم کن", + "bestOfFinalText": "${COUNT} برترین های نهایی از", + "bestOfSeriesText": "${COUNT} برترین های مجموعه از", + "bestRankText": "است #${RANK} برترین رتبه ی شما", + "bestRatingText": "است ${RATING} بهترین امتیاز شما", + "bombBoldText": "بمب", + "bombText": "بمب", + "boostText": "تقویت", + "bsRemoteConfigureInAppText": "در خود برنامه تنظیم شده است ${REMOTE_APP_NAME} برنامه", + "buttonText": "دکمه", + "canWeDebugText": "آیا مایلید که بازی، اتوماتیک خرابی ها \nو باگ ها را به نویسنده ی بازی گزارش دهد ؟\n\nداده ای که فرستاده میشود حاوی هیچ یک از\n اطلاعات شخصی شما نیست و باعث میشود بازی روان تر شود", + "cancelText": "لغو", + "cantConfigureDeviceText": "قابل تنظیم نیست ${DEVICE} متاسفانه دستگاه", + "challengeEndedText": "این چالش به پایان رسیده است", + "chatMuteText": "گفتگو رو بیصدا کن", + "chatMutedText": "گفتگو بیصدا شد", + "chatUnMuteText": "گفتگو رو صدادار کن", + "choosingPlayerText": "<انتخاب بازیکن>", + "completeThisLevelToProceedText": "برای ادامه باید این مرحله را تمام کنید", + "completionBonusText": "پاداش به اتمام رساندن", + "configControllersWindow": { + "configureControllersText": "تنظیم دسته ها", + "configureKeyboard2Text": "تنظیمات کیبورد بازیکن دوم", + "configureKeyboardText": "تنظیمات کیبورد", + "configureMobileText": "گوشی مبایل به عنوان دسته بازی", + "configureTouchText": "تنظیمات صفحه لمسی", + "ps3Text": "PS3 دسته", + "titleText": "دسته ها", + "wiimotesText": "Wii دسته", + "xbox360Text": "Xbox 360 دسته" + }, + "configGamepadSelectWindow": { + "androidNoteText": "تذکر: پشتیبانی از دسته با توجه به دستگاه و نسخه ی اندرویدش متفاوت است", + "pressAnyButtonText": "یکی از دکمه های دسته ای که میخواهید\n تنظیم کنید را فشار دهید", + "titleText": "تنظیم دسته" + }, + "configGamepadWindow": { + "advancedText": "پیشرفته", + "advancedTitleText": "تنظیمات پیشرفته ی دسته", + "analogStickDeadZoneDescriptionText": "این گزینه را فعال کنید اگر بازیکن شما بی خودی حرکت میکند", + "analogStickDeadZoneText": "دکمه ی آنالوگ منطقه ی مرگ و میر", + "appliesToAllText": "(بر همه دسته ها از این نوع اعمال میشود)", + "autoRecalibrateDescriptionText": "(این گزینه را فعال کنید اگر بازیکن شما با تمام سرعت نمیدود)", + "autoRecalibrateText": "کالیبره ی اتوماتیک مجدد دکمه ی آنالوگ", + "axisText": "محور", + "clearText": "پاک کردن", + "dpadText": "dpad", + "extraStartButtonText": "یه دکمه ی شروع اضافی", + "ifNothingHappensTryAnalogText": "اگر اتفاقی نمی‌افتد، سعی کنید به دکمه‌ی آنالوگ ارجاع دهید", + "ifNothingHappensTryDpadText": "اگر اتفاقی نمیفتد، سعی کنید به دیپَد ازجاع دهید در عوض", + "ignoreCompletelyDescriptionText": "(از این دسته در برابر اثر گذاشتن بر بازی یا منوها جلوگیری کنید)", + "ignoreCompletelyText": "کاملاً نادیده گرفتن", + "ignoredButton1Text": "نادیده گرفتن دکمه 1", + "ignoredButton2Text": "نادیده گرفتن دکمه 2", + "ignoredButton3Text": "نادیده گرفتن دکمه 3", + "ignoredButton4Text": "نادیده گرفتن دکمه 4", + "ignoredButtonDescriptionText": "از این استفاده کنید تا از تاثیر گذاشتن دکمه ی \"خانه\" و \" همگامسازی\" بر واسط کاربر جلوگیری شود", + "pressAnyAnalogTriggerText": "دکمه ی آنالوگ را تکان دهید", + "pressAnyButtonOrDpadText": "هر دکمه ای یا دیپَد را فشار دهید", + "pressAnyButtonText": "یک دکمه ای را فشار بده", + "pressLeftRightText": "چپ یا راست را فشار بده", + "pressUpDownText": "بالا یا پایین را فشار بده", + "runButton1Text": "دکمه ی دویدن ۱", + "runButton2Text": "دکمه ی دویدن ۲", + "runTrigger1Text": "آنالوگ دویدن ۱", + "runTrigger2Text": "آنالوگ دویدن ۲", + "runTriggerDescriptionText": "دکمه های آنالوگ به شما این امکان را میدهند که در سرعت های مختلف بدویید", + "secondHalfText": "از این برای تنظیم نیمه ی دوم \"دو دسته\nدر یک دستگاه\" استفاده کنید و به صورت\nدسته ی جداگانه نمایش داده میشود", + "secondaryEnableText": "فعال", + "secondaryText": "دسته ی دوم", + "startButtonActivatesDefaultDescriptionText": "این گزینه را غیر فعال کنید اگر دکمه ی شروع شما بیشتر از بالا آوردن منو کاری انجام میدهد", + "startButtonActivatesDefaultText": "دکمه ی شروع حالت پیشفرض را فعال می کند", + "titleText": "تنظیمات دسته", + "twoInOneSetupText": "\"تنظیمات \"دو دسته در یک", + "uiOnlyDescriptionText": "(از این دسته برای وصل شدن به بازی جلوگیری کنید)", + "uiOnlyText": "به استفاده ی منو محدود شود", + "unassignedButtonsRunText": "تمام دکمه های تنظیم نشده برای دویدن", + "unsetText": "<تعیین نشده>", + "vrReorientButtonText": "دکمه ی تنظیم واقعیت مجازی" + }, + "configKeyboardWindow": { + "configuringText": "${DEVICE} پیکربندی", + "keyboard2NoteScale": 0.7, + "keyboard2NoteText": "تذکر:بیشتر کیبوردها فقط چند دکمه را همزمان میپذیرند\nپس داشتن یه کیبورد دیگه ممکنه مفید باشه\nاگر کیبورد متصل و جدای دیگه هم برای استفاده باشه\nتوجه کنید که هنوز هم لازمه دکمه های خاصی رو \nبرای دو کیبورد تنظیم کنید" + }, + "configTouchscreenWindow": { + "actionControlScaleText": "اندازه ی دکمه ها", + "actionsText": "اعمال", + "buttonsText": "کلید ها", + "dragControlsText": "<دکمه ها را بکشید و موقعیتشان را تعیین کنید>", + "joystickText": "دکمه ی حرکت", + "movementControlScaleText": "اندازه ی دکمه ی حرکت", + "movementText": "حرکت", + "resetText": "بازگرداندن", + "swipeControlsHiddenText": "مخفی کردن دکمه ی حرکت", + "swipeInfoText": "کمی طول میکشد به این نوع حرکت عادت کنید\nولی راحت باشید و بدون نگاه کردن به آن بازی کنید", + "swipeText": "حرکت جاروبی", + "titleText": "پیکربندی صفحه لمسی" + }, + "configureItNowText": "همین الآن تنظیم شود ؟", + "configureText": "پیکربندی", + "connectMobileDevicesWindow": { + "amazonText": "فروشگاه برنامه آمازون", + "appStoreText": "فروشگاه برنامه", + "bestResultsScale": 0.65, + "bestResultsText": "برای بهترین نتایج شما به یک شبکه وای‌فای بدون لَگ نیاز دارید\nبرای رفع لگ می‌توانید بقیه دستگاه‌های متصل به وای‌فای را خاموش کنید\nیا نزدیک مودم بازی کنید و یا با شبکه محلی به شبکه وصل شوید و میزبان\nبازی را مستقیماً وصل کنید", + "explanationText": "برای استفاده کردن از یک گوشی هوشمند یا تبلت به عنوان دسته ی بی سیم\nرا بر روی آن نصب کنید. هر تعداد دلخواه گوشی ${REMOTE_APP_NAME} برنامه\nتوسط وای فای به صورت رایگان وصل شوند ${APP_NAME} به برنامه", + "forAndroidText": "برای اندروید", + "forIOSText": "iOS برای", + "getItForText": "بگیرید فروشگاه برنامه اپل را برای اپلی‌ها از ${REMOTE_APP_NAME} برنامه\nیا برای اندروید از فروشگاه گوگل پلی و یا فروشگاه برنامه آمازون.", + "googlePlayText": "گوگل پلی", + "titleText": "استفاده از گوشی هوشمند به عنوان دسته" + }, + "continuePurchaseText": "ادامه میدهید؟ ${PRICE} با قیمت", + "continueText": "ادامه", + "controlsText": "دکمه ها", + "coopSelectWindow": { + "activenessAllTimeInfoText": "بر روی رده بندی کلی اِعمال نمی شود", + "activenessInfoText": "این افزاینده امتیاز بیشتر می‌شود در روزهایی که\nبازی میکنید و در روزهایی که بازی نمی‌کنید", + "activityText": "فعالیت", + "campaignText": "عملیات", + "challengesInfoText": ".برای پیروزی در مینی بازی ها جایزه بگیرید\n\nجایزه ها و سختی مراحل افزایش می یابد هرگاه\nکه چالشی را انجام میدهید و کاهش می یابد\n.هرگاه چالشی تمام شود یا هدر رود", + "challengesText": "چالش ها", + "currentBestText": "بهترین امتیاز کنونی", + "customText": "سفارشی", + "entryFeeText": "ورود", + "forfeitConfirmText": "هدر دادن این چالش ؟", + "forfeitNotAllowedYetText": "این چالش هنوز نمی تواند از بین برود.", + "forfeitText": "هدر دادن", + "multipliersText": "افزاینده ها", + "nextChallengeText": "چالش بعدی", + "nextPlayText": "بازی بعدی", + "ofTotalTimeText": "${TOTAL} از", + "playNowText": "بازی همین حالا", + "pointsText": "امتیازات", + "powerRankingFinishedSeasonUnrankedText": "(فصل پایان یافته بدون رده بندی)", + "powerRankingNotInTopText": "(نفر برتر نیستید ${NUMBER} بین)", + "powerRankingPointsEqualsText": "=امتیاز ${NUMBER}", + "powerRankingPointsMultText": "(xامتیاز ${NUMBER})", + "powerRankingPointsText": "امتیاز ${NUMBER}", + "powerRankingPointsToRankedText": "(امتیاز ${REMAINING} از ${CURRENT})", + "powerRankingText": "رتبه بندی قدرت", + "prizesText": "جایزه ها", + "proMultInfoText": "ارتقا داده اند ${PRO} بازیکنانی که بازی را به\nافزاینده ی امتیاز دریافت میکنند ${PERCENT}%", + "seeMoreText": "...بیشتر", + "skipWaitText": "عبور کردن از وقفه", + "timeRemainingText": "زمان باقی مانده", + "toRankedText": "تا رتبه بندی شوید", + "totalText": "در مجموع", + "tournamentInfoText": "بر سر امتیاز بیشتر با بازیکنان در\n.لیگ خود رقابت کنید\n\nهنگامی که زمان مسابقه تمام شود، جایزه به\nنفرات برتر با امتیازهای بالا داده میشود", + "welcome1Text": "خوش آمدید. شما میتوانید ${LEAGUE} به لیگ\nبا گرفتن امتیاز، کامل کردن دستاوردها یا گرفتن جام\n.در مسابقات رتبه ی خود را بهبود بخشید", + "welcome2Text": ".همچنین میتوانید از راه های مشابه بلیط جمع آوری کنید\nبا بلیط میتوانید بازیکن جدید، نقشه و مینی-بازی باز کنید\n...یا در مسابقه ها شرکت کنید و", + "yourPowerRankingText": "رتبه بندی قدرت شما:" + }, + "copyOfText": "${NAME} کپی", + "createEditPlayerText": "<ایجاد/ویرایش بازیکن>", + "createText": "ساختن", + "creditsWindow": { + "additionalAudioArtIdeasText": "${NAME} صداهای افزوده، کارهای هنری و ایده های ابتدایی توسط", + "additionalMusicFromText": "${NAME} موسیقی های افزوده از", + "allMyFamilyText": "همه ی دوستانم و اعضای خانواده که با بازی نسخه آزمایشی کمک کردند", + "codingGraphicsAudioText": "${NAME} کد گذاری، گرافیک و صدا توسط", + "languageTranslationsText": "مترجمان زبان ها :", + "legalText": "حقوقی:", + "publicDomainMusicViaText": "${NAME} موسیقیِ خاصه ی مردم از", + "softwareBasedOnText": "میباشد ${NAME} این نرم افزار در بخش هایی الهام گرفته از", + "songCreditText": "اجرا شده و ${PERFORMER} توسط ${TITLE} آهنگ\n.می باشد ${COMPOSER}تنظیم شده و نوشته ی${ARRANGER}انتشار یافته، توسط${PUBLISHER}توسط\n${SOURCE} ادب و مهربانی", + "soundAndMusicText": "صدا & آهنگ:", + "soundsText": "(${SOURCE})صداها :", + "specialThanksText": "تشکر ویژه:", + "thanksEspeciallyToText": "${NAME} تشکر مخصوص از", + "titleText": "${APP_NAME} درباره", + "whoeverInventedCoffeeText": "هر کسی که قهوه را اختراع کرد!" + }, + "currentStandingText": "است #${RANK} رتبه ی کنونی شما", + "customizeText": "...سفارشی کردن", + "deathsTallyText": "مرگ ${COUNT}", + "deathsText": "مرگ و میرها", + "debugText": "رفع اشكال", + "debugWindow": { + "reloadBenchmarkBestResultsText": "تذکر: توصیه میشود که در قسمت تنظیمات>گرافیک، کیفیت بافت را آخر ببرید در هنگام تست این", + "runCPUBenchmarkText": "را بسنجید CPU عملکرد", + "runGPUBenchmarkText": "را بسنجید GPU عملکرد", + "runMediaReloadBenchmarkText": "بارگذاری رسانه را بسنجید", + "runStressTestText": "اجرای تست استرس", + "stressTestPlayerCountText": "شمارش بازیکن", + "stressTestPlaylistDescriptionText": "لیست بازی تست استرس", + "stressTestPlaylistNameText": "نام لیست بازی", + "stressTestPlaylistTypeText": "نوع لیست بازی", + "stressTestRoundDurationText": "مدت زمان دور", + "stressTestTitleText": "تست استرس", + "titleText": "معیارها و تست‌های استرس", + "totalReloadTimeText": "مجموع زمان بارگذاری: ${TIME} (برای جزئیات گزارش را مشاهده کنید)" + }, + "defaultGameListNameText": "به صورت پیشفرض ${PLAYMODE} لیست بازی", + "defaultNewGameListNameText": "من ${PLAYMODE} لیست بازی", + "deleteText": "پاک کن", + "demoText": "نسخه آزمایشی", + "denyText": "نپذیرفتن", + "desktopResText": "رزولوشن دسکتاپ", + "difficultyEasyText": "آسان", + "difficultyHardOnlyText": "فقط حالت سخت", + "difficultyHardText": "سخت", + "difficultyHardUnlockOnlyText": ".این مرحله فقط در حالت سخت باز میشود\nفکر میکنی توان انجام دادنشو داری ؟", + "directBrowserToURLText": "لطفا آدرس زیر رو توی مرورگر خود باز کنید:", + "disableRemoteAppConnectionsText": "غیر فعال کردن ارتباطات از راه دور برنامه", + "disableXInputDescriptionText": "اجازه می‌دهد به بیش از 4 کنترل کننده اما ممکن است کار نکند.", + "disableXInputText": "غیرفعال کردن ورودی ایکس", + "doneText": "انجام شد", + "drawText": "مساوی", + "duplicateText": "تکراری", + "editGameListWindow": { + "addGameText": "افزودن\nبازی", + "cantOverwriteDefaultText": "نمیشه لیست پیشفرض رو بازنویسی کرد", + "cantSaveAlreadyExistsText": "یک لیست بازی با همین نام وجود دارد", + "cantSaveEmptyListText": "نمیشه یه لیست خالی رو ذخیره کرد", + "editGameText": "ویرایش\nبازی", + "listNameText": "نام لیست بازی", + "nameText": "نام", + "removeGameText": "حذف\nبازی", + "saveText": "ذخیره لیست", + "titleText": "ویرایشگر لیست بازی" + }, + "editProfileWindow": { + "accountProfileInfoText": "این نمایۀ ویژه دارای یک نام\n.و تندیس بر اساس حسابتان است\n\n${ICONS}\n\nنمایه‌های سفارشی بسازید تا از نام‌ها\n.و تندیس‌های مختلف استفاده کرده باشید", + "accountProfileText": "(نمایهٔ حساب)", + "availableText": ".در دسترس میباشد \"${NAME}\" نام", + "characterText": "بازیکن", + "checkingAvailabilityText": "...\"${NAME}\" بررسی برای در دسترس بودن نام", + "colorText": "رنگ", + "getMoreCharactersText": "...بازیکن های بیشتر", + "getMoreIconsText": "... تندیس های بیشتر", + "globalProfileInfoText": "ضمانت می‌شود که نمایه‌های جهانی بازیکنان، نام‌های یکتا\n.دارند. همچنین یک تندیس سفارشی ضمیمهٔ آن‌ها است", + "globalProfileText": "(نمایهٔ جهانی)", + "highlightText": "بخش درخشان", + "iconText": "تندیس", + "localProfileInfoText": "نمایه‌های محلی بازیکنان تندیس ندارند و ضمانت نمی‌شود\nکه نام آن‌ها یکتا باشد. نمایه را جهانی کنید تا\n.نامی یکتا و تندیسی سفارشی داشته باشید", + "localProfileText": "(نمایهٔ محلی)", + "nameDescriptionText": "نام بازیکن", + "nameText": "نام", + "randomText": "تصادفی", + "titleEditText": "ویرایش نمایه", + "titleNewText": "نمایهٔ جدید", + "unavailableText": ".در دسترس نمی‌باشد؛ نامی دیگر امتحان کنید «${NAME}» نام", + "upgradeProfileInfoText": "این کار نام بازیکن شما را در جهان ذخیره میکند\n.و اجازه میدهد که تندیسی سفارشی به آن دهید", + "upgradeToGlobalProfileText": "ارتقا به نمایهٔ جهانی" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "نمیتوانید صدای پیشفرض را حذف کنید", + "cantEditDefaultText": ".نمیتوانید صدای پیشفرض رو دست کاری کنید. آن را کپی کنید یا یه جدید بسازید", + "cantOverwriteDefaultText": "نمیشه صدای پیشفرض رو بازنویسی کرد", + "cantSaveAlreadyExistsText": "یک صدا با همین نام وجود داره", + "defaultGameMusicText": "<موسیقی پیش فرض بازی>", + "defaultSoundtrackNameText": "صدای پیش فرض", + "deleteConfirmText": "حذف صدا با نام:\n\n'${NAME}'?", + "deleteText": "حذف\nصدا", + "duplicateText": "ایجاد کپی\nاز صدا", + "editSoundtrackText": "ویرایشگر صدا", + "editText": "ویرایش\nصدا", + "fetchingITunesText": "گرفتن صدا از لیست پخش برنامه", + "musicVolumeZeroWarning": "هشدار:درجه ی صدای موسیقی روی صفر است", + "nameText": "نام", + "newSoundtrackNameText": "${COUNT} صدای من", + "newSoundtrackText": "صدای جدید:", + "newText": "صدای\nجدید", + "selectAPlaylistText": "انتخاب یه لیست", + "selectASourceText": "منبع موسیقی", + "testText": "آزمایشی", + "titleText": "صداهای پس زمینه", + "useDefaultGameMusicText": "موسیقی پیشفرض بازی", + "useITunesPlaylistText": "لیست موسیقی برنامه", + "useMusicFileText": "(...و mp3)فایل موسیقی", + "useMusicFolderText": "پوشه ی فایل های موسیقی" + }, + "editText": "ویرایش", + "endText": "پایان", + "enjoyText": "لذت ببرید", + "epicDescriptionFilterText": "در حماسهٔ حرکت آهسته ${DESCRIPTION}", + "epicNameFilterText": "${NAME} حماسهٔ", + "errorAccessDeniedText": "دسترسی رد شد", + "errorOutOfDiskSpaceText": "حافظه جا ندارد", + "errorText": "خطا", + "errorUnknownText": "خطای ناشناخته", + "exitGameText": "؟${APP_NAME} خروج از", + "exportSuccessText": "منتقل شد ${NAME}", + "externalStorageText": "حافظه ی خارجی", + "failText": "باختی", + "fatalErrorText": ".اوه اوه؛ فایلی خراب یا گم شده\nلطفا برنامه را از اول نصب کنید یا\n.در تماس باشید ${EMAIL} برای کمک با", + "fileSelectorWindow": { + "titleFileFolderText": "یه فایل یا پوشه را انتخاب نمایید", + "titleFileText": "یک فایل انتخاب نمایید", + "titleFolderText": "یک پوشه انتخاب نمایید", + "useThisFolderButtonText": "استفاده از این پوشه" + }, + "filterText": "فیلتر", + "finalScoreText": "امتیاز نهایی", + "finalScoresText": "امتیازهای نهایی", + "finalTimeText": "زمان نهایی", + "finishingInstallText": "...در حال اتمام نصب؛ یه لحظه", + "fireTVRemoteWarningText": "برای یک تجربه ی بهتر، از دسته ها*\n'${REMOTE_APP_NAME}' یا برنامه\nبر روی گوشی های هوشمند یا تبلت\n.خود استفاده کنید", + "firstToFinalText": "فینال ${COUNT} اولین در", + "firstToSeriesText": "مجموعه ${COUNT} اولین در", + "fiveKillText": "!!پنج نفر رو کشتی", + "flawlessWaveText": "!یک موج بدون عیب", + "fourKillText": "!!چهار نفر رو نابود کردی", + "friendScoresUnavailableText": "امتیاز دوستان در دسترس نیست", + "gameCenterText": "مرکز بازی", + "gameCircleText": "GameCircle", + "gameLeadersText": "نفر برتر بازی ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "نمیتوانید لیست بازی پیش فرض را حذف کنید", + "cantEditDefaultText": "نمیتوانید لیست بازی پیش فرض را دست کاری کنید. آن را کپی کنید یا یه لیست جدید بسازید", + "cantShareDefaultText": "شما نمیتونید لیست بازی پیش فرض رو به اشتراک بگذارید", + "deleteConfirmText": "؟ \"${LIST}\" حذف", + "deleteText": "حذف\nلیست بازی", + "duplicateText": "کپی کردن\nلیست بازی", + "editText": "ویرایش\nلیست بازی", + "newText": "لیست بازی\nجدید", + "showTutorialText": "نمایش آموزش", + "shuffleGameOrderText": "ترتیب تصادفی بازی ها", + "titleText": "${TYPE} تنظیم لیست های" + }, + "gameSettingsWindow": { + "addGameText": "افزودن بازی" + }, + "gamesToText": "${LOSECOUNT} بازی به ${WINCOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "فراموش نکنید: هردستگاه در یک گروه میتواند بیشتر\n.از یک بازیکن داشته باشد اگر به اندازه ی کافی دسته دارید", + "aboutDescriptionText": ".از این صفحات برای تشکیل یک گروه استفاده کنید\n\nگروه به شما این امکان را میدهد که بازی ها و مسابقات\n.را با دوستانتان بر روی گوشی های متفاوت بازی کنید\n\nدر گوشه ی بالای سمت راست استفاده کنید ${PARTY}از دکمه ی\n.تا با گروه چت و تعامل کنید\n(را هنگامی که در منو هستید فشار دهید${BUTTON} با دسته، دکمه ی)", + "aboutText": "درباره", + "addressFetchErrorText": "<خطا در اتصال به آدرس>", + "appInviteMessageText": "${APP_NAME}بلیط فرستاده در برنامه ی ${COUNT}برای شما ${NAME}", + "appInviteSendACodeText": "برایشان یه کد ارسال کن", + "appInviteTitleText": "${APP_NAME} دعوت به", + "bluetoothAndroidSupportText": "(کار میکنه با هر دستگاه اندرویدی که از بلوتوث پشتیبانی میکنه)", + "bluetoothDescriptionText": "میزبان/مهمان شدن یک گروه با بلوتوث :", + "bluetoothHostText": "میزبانی با بلوتوث", + "bluetoothJoinText": "ملحق شدن با بلوتوث", + "bluetoothText": "بلوتوث", + "checkingText": "...در حال چک کردن", + "copyCodeConfirmText": "کد در کلیپ بورد کپی شد.", + "copyCodeText": "کپی کردن کد", + "dedicatedServerInfoText": "برای نتیجه بهتر،یه سرور اختصاصی بزنید.به سایت زیر برین bombsquadgame.com/serverتا بفهمین چطوری", + "disconnectClientsText": "بازیکن را از ${COUNT} این کار ارتباط\nبازی قطع میکند. مطمئنید؟", + "earnTicketsForRecommendingAmountText": ".بلیط خواهند گرفت اگر این بازی را امتحان کنند ${COUNT} دوستانتان\n(بلیط به ازای هر کدامشان میگیرید${YOU_COUNT}و شما هم)", + "earnTicketsForRecommendingText": "به اشتراک گذاری بازی\nبرای بلیط های رایگان", + "emailItText": "ایمیلش کن", + "favoritesSaveText": "ذخیره به‌عنوان مورد علاقه", + "favoritesText": "مورد علاقه‌ها", + "freeCloudServerAvailableMinutesText": "سرور ابری رایگان بعدی در عرض ${MINUTES} دقیقه در دسترس است.", + "freeCloudServerAvailableNowText": "!سرور ابری رایگان در دسترس است", + "freeCloudServerNotAvailableText": ".سرورهای ابری رایگان در دسترس نیست", + "friendHasSentPromoCodeText": "${NAME}از طرف ${APP_NAME}بلیطِ بازی ${COUNT}", + "friendPromoCodeAwardText": ".بلیط خواهید گرفت هر بار که استفاده شود${COUNT}شما", + "friendPromoCodeExpireText": ".ساعت منقضی میشود و تنها بر روی بازیکنان جدید کار میکند${EXPIRE_HOURS}این کد در", + "friendPromoCodeInstructionsText": ".را باز کنید و به قسمت تنظیمات>پیشرفته>وارد کردن کد بروید${APP_NAME}برای استفاده از کد، برنامه\n.سر بزنید تا لینک دانلود بازی برای سیستم عامل های مختلف بازی را بگیرید BombSquadgame.com به سایت", + "friendPromoCodeRedeemLongText": ".بلیط رایگان به دست آورند${COUNT}نفر میتوانند از این کد استفاده کنند تا${MAX_USES}", + "friendPromoCodeRedeemShortText": ".بلیط در بازی بگیرید${COUNT}با این کد میتوانید", + "friendPromoCodeWhereToEnterText": "(در بخش تنظیمات>پیشرفته>وارد کردن کد)", + "getFriendInviteCodeText": "گرفتن کد برای دعوت دوستان", + "googlePlayDescriptionText": ":دعوت از بازیکنان گوگل پلی برای ملحق شدن به گروه شما", + "googlePlayInviteText": "دعوت", + "googlePlayReInviteText": "بازیکن از گوگل پلی در گروه شما هستند${COUNT}\n.که ارتباط‌شان قطع می‌شود اگر یک دعوت جدید را شروع کنید\n.آن ها را هم در دعوت جدید، ضمیمه کنید", + "googlePlaySeeInvitesText": "دیدن دعوت ها", + "googlePlayText": "گوگل پلی", + "googlePlayVersionOnlyText": "(Android/Google Play ورژن)", + "hostPublicPartyDescriptionText": "میزبانی یک سرور عمومی", + "hostingUnavailableText": "میزبانی در دسترس نیست", + "inDevelopmentWarningText": ":تذکر\n\n.بازی شبکه‌ای یک ویژگی جدید و درحال گسترشه\nاکنون شدیداً توصیه می‌شود همه بازیکنان\n.در یک شبکه وای‌فای مشترک باشند", + "internetText": "اینترنت", + "inviteAFriendText": "رفیقات این بازی رو ندارند؟ دعوتشون کن\n.بلیط رایگان بگیرند${COUNT}بیان بازی کننده و", + "inviteFriendsText": "دعوت دوستان", + "joinPublicPartyDescriptionText": "پیوستن به سرور های عمومی", + "localNetworkDescriptionText": "به یک سرور دیگر از طریق LAN , Bluetooth , etc بپیوندید", + "localNetworkText": "شبکه محلی", + "makePartyPrivateText": "گروه من رو از عمومی خارج کن", + "makePartyPublicText": "گروه بازی من رو عمومی کن", + "manualAddressText": "آدرس", + "manualConnectText": "وصل شدن", + "manualDescriptionText": ":ملحق شدن به یک گروه با آدرس", + "manualJoinSectionText": "پیوستن با نشانی", + "manualJoinableFromInternetText": ":کسی میتوانداز طریق اینترنت به شما ملحق شود؟", + "manualJoinableNoWithAsteriskText": "خیر*", + "manualJoinableYesText": "بلی", + "manualRouterForwardingText": "را به آدرس محلی بفرستد${PORT} برای حل این مشکل، روتر خود را تنظیم کنید تا یو دی پی پورت", + "manualText": "بطور دستی", + "manualYourAddressFromInternetText": ":آدرس شما در اینترنت", + "manualYourLocalAddressText": ":آدرس محلی شما", + "nearbyText": "افراد نزدیک", + "noConnectionText": "<اتصال برقرار نیست>", + "otherVersionsText": "(نسخه های دیگر)", + "partyCodeText": "کد گروه", + "partyInviteAcceptText": "پذیرفتن", + "partyInviteDeclineText": "نپذیرفتن", + "partyInviteGooglePlayExtraText": "(صفحه گوگل‌پلی را در صفحه \"شبکه با دوستان\" ببینید)", + "partyInviteIgnoreText": "نادیده گرفتن", + "partyInviteText": "شما را دعوت کرده${NAME} \nتا به گروهشان ملحق شوید", + "partyNameText": "نام گروه", + "partyServerRunningText": ".سرور گروه شما در حال اجراست", + "partySizeText": "اندازه دسته", + "partyStatusCheckingText": "در حال چک کردن وضعیت...", + "partyStatusJoinableText": "گروه شما حالا دیگه از طریق اینترنت قابل اتصال برای بقیه است.", + "partyStatusNoConnectionText": "عدم توانایی برقراری ارتباط با سرور", + "partyStatusNotJoinableText": "گروه شما قابل اتصال از طریق اینترنت نیست", + "partyStatusNotPublicText": "گروه بازی شما در اینترنت عمومی نیست", + "pingText": "پینگ", + "portText": "درگاه", + "privatePartyCloudDescriptionText": ".گروه‌های خصوصی بر روی سرورهای ابری اختصاصی اجرا می شوند; و نیازی به پیکربندی روتر/مودم نیست", + "privatePartyHostText": "میزبانی گروه خصوصی", + "privatePartyJoinText": "ملحق شدن به سرور خصوصی", + "privateText": "خصوصی", + "publicHostRouterConfigText": "این ممکن است نیاز به پیکربندی انتقال پورت در روتر شما داشته باشد. برای یک گزینه آسانتر ، یک سرور خصوصی برگزار کنید.", + "publicText": "عمومی", + "requestingAPromoCodeText": "...درخواست یک کد", + "sendDirectInvitesText": "ارسال دعوت مستقیم", + "shareThisCodeWithFriendsText": ":اشتراک این کد با دوستان", + "showMyAddressText": "آدرس من رو نشون بده", + "startHostingPaidText": "${COST} میزبانی اکنون برای", + "startHostingText": "میزبان", + "startStopHostingMinutesText": "شما می توانید میزبانی رایگان را برای ${MINUTES}  دقیقه دیگر شروع کرده و متوقف کنید.", + "stopHostingText": "میزبانی را متوقف کنید", + "titleText": "شبکه با دوستان", + "wifiDirectDescriptionBottomText": "داشته باشند حتماً میتونید ازش استفاده کنندWi-Fi Direct اگر دستگاه هاتون قابلیت\nو به یکدیگر وصل شوند.وقتی که دستگاه ها بهم وصل شدند میتوانید اینجا در صفحه ی\nشبکه محلی گروه تشکیل دهید.دقیقاً مثل وصل شدن از طریق وای فای معمولی\n\n.هم میزبان شود${APP_NAME}میزبان میشود درWi-Fi Directبرای گرفتن بهترین نتیجه،کسی که در", + "wifiDirectDescriptionTopText": "استفاده کرد تا دستگاه های اندرویدی را مستقیماً بهم وصل کرد بدونWi-Fi Directمیشود از\n.نیاز به شبکه ی وای فای.این روش برای اندروید۴.۲ و بالاتر عالی است\n\nبگردیدWi-Fi Directبرای استفاده از این روش، تنظیمات وای فای را باز کنید و دنبال", + "wifiDirectOpenWiFiSettingsText": "بازکردن تنظیمات وای فای", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(سیستم عامل های مختلف میتوانند به یکدیگر متصل شوند)", + "worksWithGooglePlayDevicesText": "(سازگار با تمام دستگاه های دارای ورژن گوگل پلی ایِ این بازی)", + "youHaveBeenSentAPromoCodeText": ":برایتان ارسال شده است${APP_NAME}یک کد تبلیغی از بازی" + }, + "getTicketsWindow": { + "freeText": "!مجانی", + "freeTicketsText": "بلیط های رایگان", + "inProgressText": ".یک تراکنش در حال جریان است؛ لطفاً چند لحظه بعد تلاش کنید", + "purchasesRestoredText": ".خریدهای شما بازگردانی شدند", + "receivedTicketsText": "!بلیط گرفتی${COUNT}", + "restorePurchasesText": "بازگردانی خریدهایتان", + "ticketPack1Text": "پک کوچک بلیط", + "ticketPack2Text": "پک متوسط بلیط", + "ticketPack3Text": "پک بزرگ بلیط", + "ticketPack4Text": "پک خیلی بزرگ بلیط", + "ticketPack5Text": "پک ماموتی بلیط", + "ticketPack6Text": "پک نهایی بلیط", + "ticketsFromASponsorText": "بلیط از${COUNT}\nیه اسپانسر بگیرید", + "ticketsText": "بلیط${COUNT}", + "titleText": "بلیط بگیرید", + "unavailableLinkAccountText": ".ببخشید،خرید به وسیله ی این دستگاه در دسترس نمیباشد\nمیتوانید حسابتان بر روی این دستگاه را به حسابی در دستگاهی\n.دیگر متصل کنید و آنجا خرید خود را انجام دهید", + "unavailableTemporarilyText": "در حال حاضر در دسترس نمیباشد. لطفا بعدا دوباره امتحان کنید", + "unavailableText": "متاسفانه , دردسترس نیست", + "versionTooOldText": "متاسفم،این ورژن بازی خیلی قدیمی است. لطفا بازی را آپدیت کنید", + "youHaveShortText": "دارید ${COUNT} شما", + "youHaveText": ".بلیط دارید ${COUNT} شما" + }, + "googleMultiplayerDiscontinuedText": "متأسفیم ، سرویس چند نفره Google دیگر در دسترس نیست.\nمن در اسرع وقت در حال جایگزینی هستم.\nتا آن زمان ، لطفاً روش اتصال دیگری را امتحان کنید.", + "googlePlayText": "گوگل پلی", + "graphicsSettingsWindow": { + "alwaysText": "همیشه", + "fullScreenCmdText": "تمام صفحه (Cmd-F)", + "fullScreenCtrlText": "تمام صفحه (Ctrl-F)", + "gammaText": "گاما", + "highText": "زیاد", + "higherText": "بالاتر", + "lowText": "کم", + "mediumText": "معمولی", + "neverText": "هرگز", + "resolutionText": "رزولوشن", + "showFPSText": "نمایش عدد فریم بر ثانیه", + "texturesText": "بافت", + "titleText": "گرافیک", + "tvBorderText": "مرزبندی تلویزیونی", + "verticalSyncText": "انطباق عمودی", + "visualsText": "کیفیت تصویر" + }, + "helpWindow": { + "bombInfoText": "- بمب -\nقوی تر از مشته امامیتونه\n.برای خودتون هم خطرناک باشه\nدر بهترین زمان ممکن قبل از اینکه\n.در دست خودتون بترکه، پرتش بدید", + "canHelpText": "میتونه به شما کمک کنه${APP_NAME}", + "controllersInfoText": "بازی کنید و یا${APP_NAME}میتوانید توسط شبکه با دوستانتان\n.روی یک دستگاه بازی کنید اگر به اندازه ی کافی دسته دارید\nاز این تنوع پشتیبانی میکند؛ شما حتی میتوانید${APP_NAME}\nاز گوشی های هوشمند به عنوان دسته استفاده کنید از طریق برنامه\n.برای اطلاعات بیشتر به تنظیمات>کنترلرها بروید.${REMOTE_APP_NAME}", + "controllersText": "کنترلرها", + "controlsSubtitleText": ":شما چندتا حرکت اساسی داره ${APP_NAME} بازیکن", + "controlsText": "کنترل‌ها", + "devicesInfoText": "نسخه وی آر ${APP_NAME} میتونه با شبکه با\n نسخه معمولی بازی کنه، پس تلفن یا تبلت \nیا کامپیوتر اضافیتونو آماده کنید و بازیو شروع کنید.\n حتی وقتی بخواین فقط کاری کنین که چن نفر از \nبیرون بتونن بازیو تماشا کنن هم بکار میاد.", + "devicesText": "دستگاه‌ها", + "friendsGoodText": "داشتن چنتا ازینا خیلی خوبه. ${APP_NAME} حالش بیشتره وقتی با چن نفر\nبازی بشه و میتونه همزمان تا 8 نفر ساپورت کنه که این قضیه ما رو میبره سمت:", + "friendsText": "دوستان", + "jumpInfoText": "پرش\nبرای پریدن ازاین دکمه کمک بگیرید\n همچنین پریدن قبل از پرتاب بمب و\nهمزمان بامشت بسیار در بازی بکارمیره", + "orPunchingSomethingText": "یا بهش مشت بزنی یا از یه بلندی پرتش کنی و توی راه که داره پرت میشه با یه بمب چسبونکی بترکونیش.", + "pickUpInfoText": "بلند کردن\nمیتونید هر فرد یا بمب یا هر چیزی رو\nاز زمین بلند و پرتاب کنید,حتی \nمیتونید بازیکنان رو از زمین بیرون بندازین", + "powerupBombDescriptionText": "به شما اجازه پرتاب هر بار سه بمب\nرو پشت سر هم همزمان میده", + "powerupBombNameText": "سه بمب", + "powerupCurseDescriptionText": "بهتره اینو نگیرین چون طی چند ثانیه اگه\nبه جعبه درمان نرسین منفجر میشین", + "powerupCurseNameText": "نابودگر", + "powerupHealthDescriptionText": "این جعبه درمان و خون های از دست \nرفته رو بر میگردونه و پر میکنه", + "powerupHealthNameText": "جعبه درمان", + "powerupIceBombsDescriptionText": "با این بمب های یخی میتونید حریف ها\nرو منجمد و آسیب پذیر کنید ولی بهتر خودتون\nتوی نزدیکی انفجار این بمب ها قرار نگیرین", + "powerupIceBombsNameText": "بمب یخی", + "powerupImpactBombsDescriptionText": "بهش میگن بمب ببر تا به چیزی برخورد\nنکنن منفجر نمیشن", + "powerupImpactBombsNameText": "بمب ببر", + "powerupLandMinesDescriptionText": "با این جعبه به شما سه تا مین\nداده میشه که تا وقتی پرتاب بشن\nتا چیزی روشون میخوره منفجر میشن", + "powerupLandMinesNameText": "مین زمینی", + "powerupPunchDescriptionText": "بهتون دستکش بکس میده و باعث\nمیشه ضربه مشت های قویتری داشته باشید", + "powerupPunchNameText": "دستکش بکس", + "powerupShieldDescriptionText": "سپر محافظ دورتون قرار میگیره\nو ضربه وانفجار ها روتون کم اثر میشه", + "powerupShieldNameText": "سپر محافظ", + "powerupStickyBombsDescriptionText": "این بمب ها رو به سمت هر چی پرتاب \nکنی میچسبن به اون تا لحظه ی انفجار", + "powerupStickyBombsNameText": "بمب های چسبنده", + "powerupsSubtitleText": "البته هیچ بازی کامل نیست بدون استفاده از بسته های ویژه", + "powerupsText": "بسته های ویژه", + "punchInfoText": "مشت\nبرا ضربه به حریف از این کلید \nاستفاده کنید ضربه مشت های قوی تر\nهمزمان با پرش و سرعت و چرخش هستن", + "runInfoText": "حرکت\nبرا حرکت از جهت های جوی استیک استفاده کنید و برای حرکت سریع تر میتونید جهت ها رو همزمان با یک کلید دیگه بکار ببرید\nبرای پرتاب بمب ها به مسافت بیشتر همزمان با حرکت قابل انجامه ضربه مشت ها همزمان با حرکت هم موثرتر هستن", + "someDaysText": "بعضی روزا دوس داری مشت بزنی یه جایی. یا یه چیزیو بزنی بترکونی.", + "titleText": "${APP_NAME} راهنمایی", + "toGetTheMostText": "To get the most out of this game, you'll need:", + "welcomeText": "${APP_NAME}!خوش آمدید به" + }, + "holdAnyButtonText": "<نگه داشتن هر دکمه>", + "holdAnyKeyText": "<نگه داشتن هر کلید>", + "hostIsNavigatingMenusText": "- ${HOST} مدیر تغییر منو ها در بازیست -", + "importPlaylistCodeInstructionsText": "از کدی که در ادامه است برای وارد کردن این لیست بازی در جای دیگر استفاده کنید:", + "importPlaylistSuccessText": "لیست بازی شد ${TYPE} وارد '${NAME}'", + "importText": "وارد کردن", + "importingText": "...وارد کردن", + "inGameClippedNameText": "نام درون بازیتان خواهد بود\n\"${NAME}\"", + "installDiskSpaceErrorText": "خطا: قادر به تکمیل نصب نیست\nممکنه مربوط به فضای ذخیره دستگاه شما باشه\nچک کنید و مجددا امتحان کنید", + "internal": { + "arrowsToExitListText": "برای خروج از لیست ${LEFT} یا ${RIGHT} فشار دهید", + "buttonText": "دکمه", + "cantKickHostError": ".شما نمیتوانید میزبان را بیرون کنید", + "chatBlockedText": ".ثانیه مسدود شد ${TIME} برای ${NAME} چت", + "connectedToGameText": "متصل شد '${NAME}'", + "connectedToPartyText": "${NAME} پیوستن به", + "connectingToPartyText": "در حال اتصال", + "connectionFailedHostAlreadyInPartyText": "ارتباط ناموفق: میزبان در بخش دیگریست", + "connectionFailedPartyFullText": ".اتصال ناموفق پارتی پر شده است", + "connectionFailedText": "اتصال ناموفق بود", + "connectionFailedVersionMismatchText": "ارتباط ناموفق بود؛ میزبان در حال اجرا یک نسخه متفاوت از بازی است.\nمطمئن شوید که شما هر دوتا بروز هستید و دوباره امتحان کنید.", + "connectionRejectedText": "اتصال رد شد", + "controllerConnectedText": "متصل شد ${CONTROLLER}", + "controllerDetectedText": "یک کنترلر شناسایی شد", + "controllerDisconnectedText": "اتصالش قطع شد ${CONTROLLER}", + "controllerDisconnectedTryAgainText": "قطع اتصال شد. لطفا تلاش کنید برای اتصال ازنو ${CONTROLLER}", + "controllerForMenusOnlyText": ".این کنترلر نمی‌تواند در بازی مورد استفاده قرار بگیرد. استفاده فقط برای پیمایش در منوها", + "controllerReconnectedText": "دوباره متصل شد ${CONTROLLER}", + "controllersConnectedText": "کنترولر متصل شد ${COUNT}", + "controllersDetectedText": "کنترولر شناسایی شد ${COUNT}", + "controllersDisconnectedText": "قطع اتصال شد ${COUNT}", + "corruptFileText": "Corrupt file(s) detected. Please try re-installing, or email ${EMAIL}", + "errorPlayingMusicText": "خطای موزیک بازی:${MUSIC}", + "errorResettingAchievementsText": "قادر نیست برای تنظیم مجدد دستاوردهای آنلاین. لطفا بعدا دوباره امتحان کنید.", + "hasMenuControlText": "دسترسی به منو دارد ${NAME}", + "incompatibleNewerVersionHostText": "میزبان در حال استفاده از نسخه جدید تری از بازی هست نسخه بازیتون رو به اخرین نسخه ارتقا بدین\nدوباره امتحان کنید", + "incompatibleVersionHostText": "میزبان در حال اجرا از نسخه دیگری از این بازی است.\nمطمئن شوید که شما هر دو تا بروز هستید و دوباره امتحان کنید.", + "incompatibleVersionPlayerText": "${NAME} در حال اجرا از نسخه دیگری از این بازی است.\nمطمئن شوید که شما هر دو تا بروز هستید و دوباره امتحان کنید.", + "invalidAddressErrorText": "خطا: آدرس در دسترس نیست", + "invalidNameErrorText": "نام نا معتبر", + "invalidPortErrorText": "مشکل:درگاه بی اعتبار", + "invitationSentText": "دعوت نامه ارسال شد.", + "invitationsSentText": "دعوت نامه ارسال شد ${COUNT}", + "joinedPartyInstructionsText": "فردی به گروه شما پیوسته\nبرید به بازی و بازی را شروع کنید", + "keyboardText": "صفحه‌کلید", + "kickIdlePlayersKickedText": "بعلت غیرفعال بودن ${NAME} خروج", + "kickIdlePlayersWarning1Text": "${NAME} will be kicked in ${COUNT} seconds if still idle.", + "kickIdlePlayersWarning2Text": "(شما می توانید اینو خاموش کنید در تنظیمات -> پیشرفته)", + "leftGameText": "خارج شد '${NAME}'", + "leftPartyText": "پارتی رو ترک کرد ${NAME}", + "noMusicFilesInFolderText": "پوشه حاوی هیچ فایل موسیقی نیست.", + "playerJoinedPartyText": "به گروه بازی پیوست ${NAME}", + "playerLeftPartyText": "${NAME} گروه رو ترک کرد", + "rejectingInviteAlreadyInPartyText": "رد کردن دعوت (already in a party)", + "serverRestartingText": "سرور در حال شروع مجدد است. لطفا چند لحظه دیگر مجددا متصل شوید", + "serverShuttingDownText": ".سرور درحال بسته شدن است", + "signInErrorText": "خطای ورود به سیستم", + "signInNoConnectionText": "ورود ناموفق بود اتصال به اینترنت در دسترسه؟", + "telnetAccessDeniedText": "خطا: کاربر از دسترسی به شبکه خارج شد", + "timeOutText": "(times out in ${TIME} seconds)", + "touchScreenJoinWarningText": "You have joined with the touchscreen.\nIf this was a mistake, tap 'Menu->Leave Game' with it.", + "touchScreenText": "صفحه لمسی", + "unableToResolveHostText": "مشکل:توانایی حل مشکل میزبان وجود ندارد", + "unavailableNoConnectionText": "در دسترس نیست. اتصال به اینترنت برقرار است آیا؟", + "vrOrientationResetCardboardText": "برای تنظیم مجدد جهت گیری VR از این استفاده کنید.\nبرای بازی کردن به یک کنترلر خارجی نیاز دارید.", + "vrOrientationResetText": "تنظیم مجدد VR.", + "willTimeOutText": "(زمان تموم میشه اگه بیکار بود)" + }, + "jumpBoldText": "پرش", + "jumpText": "پرش", + "keepText": "نگه داشتن", + "keepTheseSettingsText": "حفظ کردن این تنظیمات؟", + "keyboardChangeInstructionsText": ".برای تغییر صفحه‌کلید، کلید فاصله را دو بار فشار دهید", + "keyboardNoOthersAvailableText": ".صفحه‌کلید دیگری موجود نیست", + "keyboardSwitchText": ".«${NAME}» تغییر صفحه‌کلید به", + "kickOccurredText": ".بیرون انداخته شد ${NAME}", + "kickQuestionText": "بیرون انداخته شود؟ ${NAME}", + "kickText": "بیرون انداختن", + "kickVoteCantKickAdminsText": "نمیشود ادمین ها رو اخراج کنید", + "kickVoteCantKickSelfText": "نمیشه خودت رو اخراج کنی اسکل", + "kickVoteFailedNotEnoughVotersText": ".بازیکنان برای رای گیری کافی نیستند", + "kickVoteFailedText": ".رای گیری برای بیرون انداختن ناموفق", + "kickVoteStartedText": ".شروع شد ${NAME} رای گیری برای بیرون انداختن", + "kickVoteText": "رای برای بیرون انداختن", + "kickVotingDisabledText": ".رأی گیری غیرفعاله", + "kickWithChatText": "را برای نه تایپ کن ${NO} را برای بله و ${YES} در چت", + "killsTallyText": "${COUNT} کشته ها", + "killsText": "کشته ها", + "kioskWindow": { + "easyText": "آسان", + "epicModeText": "حرکت آهسته", + "fullMenuText": "فهرست کامل", + "hardText": "سخت", + "mediumText": "متوسط", + "singlePlayerExamplesText": "Single Player / Co-op Examples", + "versusExamplesText": "Versus Examples" + }, + "languageSetText": ".است «${LANGUAGE}» زبان هم‌اکنون", + "lapNumberText": "${TOTAL}/${CURRENT} دور", + "lastGamesText": "(بازی آخر ${COUNT})", + "leaderboardsText": "مدیران", + "league": { + "allTimeText": "همیشه", + "currentSeasonText": "(${NUMBER}) فصل جاری", + "leagueFullText": "${NAME} لیگ", + "leagueRankText": "رتبه لیگ", + "leagueText": "لیگ", + "rankInLeagueText": "#${RANK}, ${NAME} League${SUFFIX}", + "seasonEndedDaysAgoText": ".روز پیش پایان یافت ${NUMBER} فصل", + "seasonEndsDaysText": ".روز دیگر پایان می‌یابد ${NUMBER} فصل", + "seasonEndsHoursText": ".ساعت دیگر پایان می‌یابد ${NUMBER} فصل", + "seasonEndsMinutesText": ".دقیقهٔ دیگر پایان می‌یابد ${NUMBER} فصل", + "seasonText": "${NUMBER} فصل", + "tournamentLeagueText": ".برسید ${NAME} برای ورود به این مسابقه، باید به لیگ", + "trophyCountsResetText": ".جوایز در فصل بعد بازنشانی می‌شوند" + }, + "levelBestScoresText": "${LEVEL} بهترین امتیاز در", + "levelBestTimesText": "${LEVEL} بهترین زمان در", + "levelIsLockedText": ".قفل است ${LEVEL}", + "levelMustBeCompletedFirstText": ".باید تکمیل شود ${LEVEL} ابتدا", + "levelText": "${NUMBER} مرحله", + "levelUnlockedText": "!قفل مرحله باز شد", + "livesBonusText": "پاداش تعداد جان ها", + "loadingText": "در حال بارگزاری", + "loadingTryAgainText": "…در حال بارگذاری؛ چند لحظهٔ دیگر دوباره امتحان کنید", + "macControllerSubsystemBothText": "(هر دو (توصیه نمی‌شود", + "macControllerSubsystemClassicText": "معمولی", + "macControllerSubsystemDescriptionText": "(موقعی سعی کنید این را عوض کنید که کنترلر شما درست کار نمیکند.)", + "macControllerSubsystemMFiNoteText": ".ساخته شده برای کنترلر آی‌اواِس/مک تشخیص داده شد\n شما شاید بخواهید این‌ها رو در تنظیمات فعال کنید", + "macControllerSubsystemMFiText": "ساخته شده برای ای او اس و مک", + "macControllerSubsystemTitleText": "پشتیبانی کنترل کننده", + "mainMenu": { + "creditsText": "درباره", + "demoMenuText": "منو نسخه ی نمایشی", + "endGameText": "پایان بازی", + "exitGameText": "خروج از بازی", + "exitToMenuText": "خروج به منو؟", + "howToPlayText": "روش بازی", + "justPlayerText": "(فقط ${NAME})", + "leaveGameText": "ترک کردن بازی", + "leavePartyConfirmText": "واقعا میخوای دسته رو ترک کنی؟", + "leavePartyText": "ترک دسته", + "quitText": "خروج", + "resumeText": "ادامه", + "settingsText": "تنظیمات" + }, + "makeItSoText": "درستش کن", + "mapSelectGetMoreMapsText": "رفتن به زمین‌های بازیِ بیشتر...", + "mapSelectText": "انتخاب . . .", + "mapSelectTitleText": "${GAME} نقشه", + "mapText": "نقشه", + "maxConnectionsText": "حداکثر اتصالات", + "maxPartySizeText": "حداکثر فضای پارتی", + "maxPlayersText": "حداکثر بازیکنان", + "modeArcadeText": "حالت بازی", + "modeClassicText": "حالت کلاسیک", + "modeDemoText": "حالت نمایشی", + "mostValuablePlayerText": "ارزشمندترین بازیکن", + "mostViolatedPlayerText": "پُراشتباه‌ترین بازیکن", + "mostViolentPlayerText": "خشن‌ترین بازیکن", + "moveText": "انتقال", + "multiKillText": "! !${COUNT} ازبین بردن.", + "multiPlayerCountText": "بازیکن ${COUNT}", + "mustInviteFriendsText": "توجه:\nشما باید دعوت کنید از دوستان درصفحه اصلی از قسمت\n\"${GATHER}\"", + "nameBetrayedText": ".خیانت کرد ${VICTIM} به ${NAME}", + "nameDiedText": ".از بین رفت ${NAME}", + "nameKilledText": ".را از بین برد ${VICTIM} ${NAME}", + "nameNotEmptyText": "!نام نمی‌تواند خالی باشد", + "nameScoresText": "!موفق شد ${NAME}", + "nameSuicideKidFriendlyText": ".بیرون افتاد ${NAME}", + "nameSuicideText": ".خودکشی کرد ${NAME}", + "nameText": "نام", + "nativeText": "بومی", + "newPersonalBestText": "!رکورد قبلیتو شکستی", + "newTestBuildAvailableText": "(${BUILD} نسخهٔ ${VERSION}) !نسخهٔ آزمایشی جدیدتری دردسترس است\n${ADDRESS} :از این نشانی دریافت کنید", + "newText": "جدید", + "newVersionAvailableText": "(${VERSION}) !در دسترس است ${APP_NAME} نسخهٔ جدیدتری از", + "nextAchievementsText": ":دستاوردهای بعدی", + "nextLevelText": "مرحله بعد", + "noAchievementsRemainingText": "- هیچی", + "noContinuesText": "(ادامه ندارد)", + "noExternalStorageErrorText": "محل ذخیره سازی در این دستگاه یافت نشد", + "noGameCircleText": "GameCircleخطا: وارد نشدید به", + "noScoresYetText": "هیچ امتیازی نیست", + "noThanksText": "نه مرسی", + "noTournamentsInTestBuildText": ".هشدار: امتیازات مسابقه از این نسخهٔ آزمایشی نادیده گرفته می‌شوند", + "noValidMapsErrorText": "هیچ نقشه معتبری برای این نوع بازی یافت نشد.", + "notEnoughPlayersRemainingText": "بازیکنان باقیمانده کافی نیستند خارج بشید و دوباره یه بازی جدید رو شروع کنید", + "notEnoughPlayersText": "!بازیکن نیاز دارید ${COUNT} برای شروع بازی حداقل به", + "notNowText": "حالا نه", + "notSignedInErrorText": ".برای انجام این کار باید وارد شوید", + "notSignedInGooglePlayErrorText": "برا استفاده از این مورد شما باید به گوگل‌پلی وارد شوید", + "notSignedInText": "وارد نشده‌ای", + "nothingIsSelectedErrorText": "چیزی انتخاب نشده", + "numberText": "#${NUMBER}", + "offText": "خاموش", + "okText": "تایید", + "onText": "روشن", + "oneMomentText": "یک لحظه...", + "onslaughtRespawnText": "${PLAYER} will respawn in wave ${WAVE}", + "orText": "${A} یا ${B}", + "otherText": "دیگر...", + "outOfText": "(#${RANK} از ${ALL})", + "ownFlagAtYourBaseWarning": "پرچم شما باید در پایگاه خودتان باشد تا امتیاز کسب کنید.", + "packageModsEnabledErrorText": "شبکه‌بازی دردسترس نیست. بروبه تظیمات》 تنظیمات پیشرفته و تیک پکیج محلی رو وردار", + "partyWindow": { + "chatMessageText": "پیام\"چت\"", + "emptyText": "هیچکس نیست", + "hostText": "(میزبان)", + "sendText": "ارسال", + "titleText": "گروه شما" + }, + "pausedByHostText": "(مکث شده توسط میزبان)", + "perfectWaveText": "! عالی کار کردی", + "pickUpText": "بلند کردن", + "playModes": { + "coopText": "چند نفره", + "freeForAllText": "تک به تک", + "multiTeamText": "چند تیم", + "singlePlayerCoopText": "بازی با کامپیوتر", + "teamsText": "بازی تیمی" + }, + "playText": "شروع بازی", + "playWindow": { + "oneToFourPlayersText": "حداکثر ۴ بازیکن", + "titleText": "شروع بازی", + "twoToEightPlayersText": "حداکثر ۸ بازیکن" + }, + "playerCountAbbreviatedText": "بازیکن ${COUNT}", + "playerDelayedJoinText": ".در دور بعد وارد می‌شود ${PLAYER}", + "playerInfoText": "اطلاعات بازیکن", + "playerLeftText": ".بازی را ترک کرد ${PLAYER}", + "playerLimitReachedText": "محدودیت بازکن به ${COUNT} رسیده; عضو جدید مجاز نیست.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "شما نمی‌توانید نمایهٔ مربوط به حساب‌کاربری را حذف کنید.", + "deleteButtonText": "حذف\nنمایه", + "deleteConfirmText": "؟'${PROFILE}' حذف", + "editButtonText": "ویرایش\nنمایه", + "explanationText": "نام سفارشی بازیکن حاضر در این حساب کاربری", + "newButtonText": "نمایهٔ\nجدید", + "titleText": "نمایه‌های بازیکن" + }, + "playerText": "بازیکن", + "playlistNoValidGamesErrorText": "این لیست حاوی هیچ بازی معتبری نیست", + "playlistNotFoundText": "لیست بازی یافت نشد", + "playlistText": "لیست پخش", + "playlistsText": "لیست بازی ها", + "pleaseRateText": "خوشتان آمده، لطفاً چند لحظه‌ای وقت بگذارید و ${APP_NAME} اگر از\nآن را رتبه‌بندی کنید یا مروری بر آن بنویسید. این کار بازخوردهای مفیدی\n.را به همراه دارد و به پشتیبانی از توسعه‌ها در آینده کمک خواهد کرد\n\n!با تشکر\nاریک—", + "pleaseWaitText": "…لطفاً صبر کنید", + "pluginsDetectedText": ".افزونه(ها)ی جدید شناسایی شد. آن‌ها را در تنظیمات فعال/پیکربندی کنید", + "pluginsText": "افزونه‌ها", + "practiceText": "تمرین", + "pressAnyButtonPlayAgainText": "…فشردن هر دکمه‌ای برای بازی دوباره", + "pressAnyButtonText": "…فشردن هر دکمه‌ای برای ادامه", + "pressAnyButtonToJoinText": "…پیوستن با فشردن هر دکمه‌ای", + "pressAnyKeyButtonPlayAgainText": "…فشردن هر کلیدی/دکمه‌ای برای بازی دوباره", + "pressAnyKeyButtonText": "…فشردن هر کلیدی/دکمه‌ای برای ادامه", + "pressAnyKeyText": "…کلیدی را فشار دهید", + "pressJumpToFlyText": "** روی پرش مکرراً ضربه بزنید تا پرواز کنید **", + "pressPunchToJoinText": "…برای پیوستن دکمهٔ مشت را فشار دهید", + "pressToOverrideCharacterText": "را فشار دهید ${BUTTONS} برای انتخاب کاراکتر", + "pressToSelectProfileText": "را فشار دهید ${BUTTONS} برای انتخاب بازیکن", + "pressToSelectTeamText": "را فشار دهید ${BUTTONS} برای انتخاب تیم", + "promoCodeWindow": { + "codeText": "کد", + "enterText": "ورود" + }, + "promoSubmitErrorText": "خطا در ارسال کد؛ لطفاً اتصال اینترنت را بررسی کنید", + "ps3ControllersWindow": { + "macInstructionsText": "Switch off the power on the back of your PS3, make sure\nBluetooth is enabled on your Mac, then connect your controller\nto your Mac via a USB cable to pair the two. From then on, you\ncan use the controller's home button to connect it to your Mac\nin either wired (USB) or wireless (Bluetooth) mode.\n\nOn some Macs you may be prompted for a passcode when pairing.\nIf this happens, see the following tutorial or google for help.\n\n\n\n\nPS3 controllers connected wirelessly should show up in the device\nlist in System Preferences->Bluetooth. You may need to remove them\nfrom that list when you want to use them with your PS3 again.\n\nAlso make sure to disconnect them from Bluetooth when not in\nuse or their batteries will continue to drain.\n\nBluetooth should handle up to 7 connected devices,\nthough your mileage may vary.", + "ouyaInstructionsText": "To use a PS3 controller with your OUYA, simply connect it with a USB cable\nonce to pair it. Doing this may disconnect your other controllers, so\nyou should then restart your OUYA and unplug the USB cable.\n\nFrom then on you should be able to use the controller's HOME button to\nconnect it wirelessly. When you are done playing, hold the HOME button\nfor 10 seconds to turn the controller off; otherwise it may remain on\nand waste batteries.", + "pairingTutorialText": "آموزش تصویری جفت‌شدن", + "titleText": ":${APP_NAME} در PS3 استفاده از کنترلر" + }, + "punchBoldText": "مشت", + "punchText": "مشت", + "purchaseForText": "خرید برای ${PRICE}", + "purchaseGameText": "خرید بازی", + "purchasingText": "…در حال خرید", + "quitGameText": "؟${APP_NAME} خروج از", + "quittingIn5SecondsText": "…ترک در ۵ ثانیه", + "randomPlayerNamesText": "نام پیش‌فرض", + "randomText": "تصادفی", + "rankText": "رتبه", + "ratingText": "امتیاز", + "reachWave2Text": "برای ثبت امتیاز به دور دوم برسید", + "readyText": "آماده", + "recentText": "اخیر", + "remoteAppInfoShortText": ".خیلی جذاب تره وقتی با دوستانتون بازی می کنید ${APP_NAME} بازی\nکافیه چند تا دسته ی بازی به دستگاهتون وصل کنید یا\nرا روی گوشی و تبلت های دیگر نصب کنید تا ${REMOTE_APP_NAME} برنامه ی\n.به عنوان دسته برای بازی استفاده شوند", + "remote_app": { + "app_name": "BombSquad کنترولر مخصوص", + "app_name_short": "BSRemote", + "button_position": "مکان دکمه", + "button_size": "اندازه دکمه", + "cant_resolve_host": "میزبان را نمی‌توان یافت.", + "capturing": "گرفتن…", + "connected": ".متصل شد", + "description": "استفاده کنید از تبلت یا گوشی مبایل برای کنترول کردن بازی بمب اسکواد\nتا هشت نفر میتونن متصل بشن و بصورت چند نفر به بازی بپردازند وکنترول کنند بازی رو در یک تبلت یا تلویزیون", + "disconnected": "قطع اتصال از سرور", + "dpad_fixed": "ثابت", + "dpad_floating": "شناور", + "dpad_position": "D-Pad موقعیت", + "dpad_size": "D-Pad اندازه", + "dpad_type": "D-Pad Type", + "enter_an_address": "وارد کردن یه آدرس", + "game_full": "بازی تکمیل است یا اتصالات پذیرفته نشدند", + "game_shut_down": "بازی تعطیل شد", + "hardware_buttons": "دکمه سخت افزاری", + "join_by_address": "پیوستن با آدرس", + "lag": "ثانیه ${SECONDS} : تاخیر", + "reset": "برگرداند به پیشفرض", + "run1": "Run 1", + "run2": "Run 2", + "searching": "جستجو برای بازی‌های فعال", + "searching_caption": "بر روی نام یک بازی برای پیوستن به آن ضربه بزنید.\nاطمینان حاصل کنید که شما در شبکه وای فای همان بازی هستید.", + "start": "شروع", + "version_mismatch": "عدم تطابق نسخه‌ها\nاطمینان حاصل کنید که بمب‌اسکوار و کنترولر\nآخرین ورژن باشن و دوباره امتحان کنید" + }, + "removeInGameAdsText": "بازی را در فروشگاه بخرید تا تبلیغات حذف شوند «${PRO}» نسخهٔ", + "renameText": "تغییر نام", + "replayEndText": "پایان بازپخش", + "replayNameDefaultText": "بازپخش بازی اخیر", + "replayReadErrorText": "خطا در خواندن فایل بازپخش", + "replayRenameWarningText": "پس از بازی اگر شما خواستید نگه داشته شود,درغیر اینصورت بازنویسی خواهد شد\"${REPLAY}\"تغییر نام", + "replayVersionErrorText": "با عرض پوزش این بازبخش در ورژن مختلفیاز بازی ایجاد شده\n است و نمیتوان مورد استفاده قرار گیرد", + "replayWatchText": "دیدن بازبخش بازی", + "replayWriteErrorText": "خطا در نوشتن فایل بازپخش.", + "replaysText": "بازپخش ها", + "reportPlayerExplanationText": "از این ایمیل برای گزارش تقلب، زبان نامناسب یا موارد نامناسب دیگر استفاده کنید.\nلطفا این پایین توضیح دهید:", + "reportThisPlayerCheatingText": "تقلب کردن", + "reportThisPlayerLanguageText": "زبان نامناسب", + "reportThisPlayerReasonText": "میخواهید چه چیزی رو گزارش بدید؟", + "reportThisPlayerText": "گزارش کردن این بازیکن", + "requestingText": "درخواست", + "restartText": "شروع دوباره", + "retryText": "مجدد", + "revertText": "بازگشت", + "runText": "دویدن", + "saveText": "ذخیره", + "scanScriptsErrorText": "اسکریپت ها در حال بررسی خطا ها است؛ برای جزئیات لوگ را ببینید", + "scoreChallengesText": "امتیاز چالش", + "scoreListUnavailableText": ".لیست امتیازات در دسترس نیست", + "scoreText": "امتیاز", + "scoreUnits": { + "millisecondsText": "میلی ثانیه", + "pointsText": "امتیاز", + "secondsText": "ثانیه" + }, + "scoreWasText": "(بود ${COUNT})", + "selectText": "انتخاب", + "seriesWinLine1PlayerText": "برنده", + "seriesWinLine1TeamText": "برنده", + "seriesWinLine1Text": "برنده", + "seriesWinLine2Text": "مجموعه بازی", + "settingsWindow": { + "accountText": "حساب", + "advancedText": "پیشرفته", + "audioText": "صدا", + "controllersText": "کنترلرها", + "graphicsText": "تصویر", + "playerProfilesMovedText": "توجه: مشخصات بازیکن به پنجره حساب در منوی اصلی نقل مکان کرد.", + "titleText": "تنظیمات" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(یه صفحه‌کلید ساده و خوش‌دست بر روی صفحهٔ نمایش برای ویرایش متنی)", + "alwaysUseInternalKeyboardText": "همیشه از صفحه‌کلید داخلی استفاده شود", + "benchmarksText": "معیار و تست استرس", + "disableCameraGyroscopeMotionText": "غیرفعال کردن حرکت ژیروسکوپ دوربین", + "disableCameraShakeText": "غیرفعال کردن لرزش دوربین", + "disableThisNotice": "(شما میتوانید این اخطار را در تنظیمات یشرفته خاموش کنید)", + "enablePackageModsDescriptionText": "(فعالسازی قابلیت مد اکسترا اما شبکه بازی غیر فعال میشود)", + "enablePackageModsText": "فعالسازی پکیج مد محلی", + "enterPromoCodeText": "وارد کردن کد", + "forTestingText": "توجه: این تغیرات برای آزمایش کردن هستند و هنگام خروج به حال اول باز میگردند", + "helpTranslateText": "${APP_NAME}'s non-English translations are a community\nsupported effort. If you'd like to contribute or correct\na translation, follow the link below. Thanks in advance!", + "kickIdlePlayersText": "بیرون انداختن بازیکنان غیرفعال", + "kidFriendlyModeText": "حالت دوستانه برای کودکان. خشونت کم", + "languageText": "زبان", + "moddingGuideText": "راهنمای برنامه‌نویسان", + "mustRestartText": "برای اعمال تغییرات باید بازی را دوباره راه اندازی کنید", + "netTestingText": "تست شبکه", + "resetText": "باز گرداندن", + "showBombTrajectoriesText": "نمایش خط سیر بمب", + "showPlayerNamesText": "نمایش نام بازیکنان", + "showUserModsText": "نمایش پوشهٔ سبک بازی‌ها", + "titleText": "پیشرفته", + "translationEditorButtonText": "${APP_NAME} ویرایشگر زبان", + "translationFetchErrorText": "وضعیت ترجمه در دسترس نیست", + "translationFetchingStatusText": "چک کردن وضعیت ترجمه ...", + "translationInformMe": "!وقتی زبان من به‌روزرسانی نیاز داشت، خبرم کن", + "translationNoUpdateNeededText": "!زبان کنونی به‌روز است؛ ووهو", + "translationUpdateNeededText": "! زبان فعلی نیاز به به روز رسانی دارد", + "vrTestingText": "VR تست" + }, + "shareText": "اشتراک‌گذاری", + "sharingText": "...در حال اشتراک‌گذاری", + "showText": "نمایش", + "signInForPromoCodeText": "برای اثر گذاری کد ها باید با یک حساب کاربری وارد شوید", + "signInWithGameCenterText": "برای استفاده از حسابی که در یک گیم سنتر دارید,\nبا برنامه گیم سنتر خود وارد شوید.", + "singleGamePlaylistNameText": "${GAME} فقط", + "singlePlayerCountText": "1 بازیکن", + "soloNameFilterText": "رو در رو ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "انتخاب کارکتر", + "Chosen One": "انتخاب شده", + "Epic": "بازی قدرت", + "Epic Race": "مسابقه حرکت آهسته", + "FlagCatcher": "گرفتن پرچم", + "Flying": "رویا", + "Football": "فوتبال", + "ForwardMarch": "مبارزه", + "GrandRomp": "پیروز شدن", + "Hockey": "هاکی", + "Keep Away": "دور نگه داشتن", + "Marching": "گریز", + "Menu": "منوی اصلی", + "Onslaught": "حمله", + "Race": "مسابقه", + "Scary": "پادشاه تپه", + "Scores": "صفحه امتیاز", + "Survival": "از بين بردن", + "ToTheDeath": "نبرد مرگبار", + "Victory": "صفحه نمایش نتایج نهایی" + }, + "spaceKeyText": "فاصله", + "statsText": "آمار", + "storagePermissionAccessText": "به اجازه شما برای دسترسی به حافظه نیاز دارد", + "store": { + "alreadyOwnText": "!را در اختیار دارید ${NAME} شما", + "bombSquadProDescriptionText": "• Doubles achievement ticket rewards\n• Removes in-game ads\n• Includes ${COUNT} bonus tickets\n• +${PERCENT}% league score bonus\n• Unlocks '${INF_ONSLAUGHT}' and\n '${INF_RUNAROUND}' co-op levels", + "bombSquadProNameText": "حرفه ای ${APP_NAME}", + "bombSquadProNewDescriptionText": ".تبلیغات داخل بازی حذف میشود\n.بیشتر تنظیمات بازی باز میشوند\n:همچنین شامل", + "buyText": "خرید", + "charactersText": "شخصیت ها", + "comingSoonText": "...به زودی", + "extrasText": "دیگر", + "freeBombSquadProText": "جوخه بمب حالا مجانیه، ولی چون شما زمانی خریدینش که پولی بود؛ ما داریم\nشمارو به جوخه بمب پرو ارتقا میدیم و ${COUNT} بلیط بعنوان تشکر برای شما.\nاز امکانات جدید لذت ببرید. از شما بابت پشتیبانی و دلگرمی تان ممنونیم!\n-اریک", + "holidaySpecialText": "ویژه‌ی تعطیلات", + "howToSwitchCharactersText": "(برو به \"${SETTINGS} -> ${PLAYER_PROFILES}\" برای ایجاد و شخصی‌سازی شخصیت‌ها)", + "howToUseIconsText": "(ایجاد نمایه‌های جهانی از قسمت پنجرهٔ حساب برای استفاده در این مورد)", + "howToUseMapsText": "(از این نقشه‌ها میتونید تو بازی‌های تیمی و تک‌به‌تکی خودتون استفاده کنید)", + "iconsText": "نشانه‌ها", + "loadErrorText": "لود شدن صفحه ناموفق بود\nاتصال اینترنت رو چک کنید", + "loadingText": "در حال بارگزاری", + "mapsText": "نقشه ها", + "miniGamesText": "تک بازی‌های کوچک", + "oneTimeOnlyText": "(فقط یک بار)", + "purchaseAlreadyInProgressText": "این مورد قبلا سفارش داده شده", + "purchaseConfirmText": "را بخرید ؟ ${ITEM} واقعا می خواهید", + "purchaseNotValidError": ". سفارش معتبر نیست\n. ایمیل بزنید ${EMAIL} اگر مشکلی دارید به", + "purchaseText": "خرید", + "saleBundleText": "فروش بسته نرم‌افزاری!", + "saleExclaimText": "فروش", + "salePercentText": "(${PERCENT}% تخفیف)", + "saleText": "فروش ویژه", + "searchText": "جستجو", + "teamsFreeForAllGamesText": "بازی‌های تیمی / تک به تک", + "totalWorthText": "*** ${TOTAL_WORTH} قیمت! ***", + "upgradeQuestionText": "ارتقا؟", + "winterSpecialText": "ویژه ی زمستان", + "youOwnThisText": "شما این را در اختیار دارید" + }, + "storeDescriptionText": "8 بازیکن حزب جنون بازی!\n\nدوستان خود (یا رایانه) را در مسابقات مینی بازی‌های انفجاری مانند: پرچم، هاکی و حرکت آهسته\n\nکنترل ساده و پشتیبانی گسترده می‌توان تا حداکثر 8 نفر برای ورود به بازی اقدام کند؛ شما حتی می‌توانید دستگاه‌های تلفن‌همراه خود را به عنوان کنترل از طریق برنامه رایگان BombSquadremote استفاده کنید!\n\nبمب‌اندازی از راه دور!\n\nwww.froemling.net/bombsquad را برای اطلاعات بیشتر چک کنید.", + "storeDescriptions": { + "blowUpYourFriendsText": "بازیکن دوستات رو بترکون 😁", + "competeInMiniGamesText": "رقابت در بازی‌های کوچک بویژه مسابقه پرواز.", + "customize2Text": "سفارشی کردن شخصیت‌ها، بازی‌های کوچک، و حتی موسیقی‌های متن .", + "customizeText": "شما می تونید شخصیت‌ها رو شخصی‌سازی کنید و به سلیقه خودتون لیست‌بازی درست کنید", + "sportsMoreFunText": "ورزش ها سرگرم کننده تر میشن با انفجار", + "teamUpAgainstComputerText": "تیم در مقابل کامپیوتر ." + }, + "storeText": "فروشگاه", + "submitText": "ثبت", + "submittingPromoCodeText": "...ثبت کردن کد", + "teamNamesColorText": "نام/رنگ تیم...", + "telnetAccessGrantedText": "شبکه ی در دسترس فعال", + "telnetAccessText": "شبکه راه دور دردسترس است اجازه میدید؟", + "testBuildErrorText": "این نسخه دیگر فعال نیست، لطفا نسخه جدید را بررسی کنید", + "testBuildText": "آزمایش ساخت", + "testBuildValidateErrorText": "قادر به تأیید نسخه نیست (اتصال برقره نشد؟)", + "testBuildValidatedText": "نسخه معتبر است؛ لذت ببرید.!", + "thankYouText": "تشکر بخاطر حمایت از ما ! از بازی لذت ببرید", + "threeKillText": "نابود کردن همزمان سه نفر", + "timeBonusText": "پاداش سرعت عمل", + "timeElapsedText": "زمان سپری شده", + "timeExpiredText": "زمان تمام شده", + "timeSuffixDaysText": "روز ${COUNT}", + "timeSuffixHoursText": "ساعت ${COUNT}", + "timeSuffixMinutesText": "دقیقه ${COUNT}", + "timeSuffixSecondsText": "ثانیه ${COUNT}", + "tipText": "نکته", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "بالاترین امتیاز دوستان", + "tournamentCheckingStateText": "چک کردن وضعیت مسابقات؛ لطفا صبر کنید", + "tournamentEndedText": "این دوره از مسابقات به پایان رسیده است دوره جدیدی بزودی آغاز خواهد شد", + "tournamentEntryText": "ورودیِ مسابقات", + "tournamentResultsRecentText": "آخرین نتایج مسابقات", + "tournamentStandingsText": "جدول رده بندی مسابقات", + "tournamentText": "جام حذفی", + "tournamentTimeExpiredText": "زمان مسابقات پایان یافت", + "tournamentsText": "مسابقات", + "translations": { + "characterNames": { + "Agent Johnson": "مامور جانسون", + "B-9000": "B-9000", + "Bernard": "برنارد", + "Bones": "اسکلت", + "Butch": "بوچ", + "Easter Bunny": "Easter خرگوش جشن", + "Flopsy": "فلاپسی", + "Frosty": "آدم‌برفی", + "Gretel": "گرتل", + "Grumbledorf": "تردست", + "Jack Morgan": "جک مورگان", + "Kronk": "کرانک", + "Lee": "لی", + "Lucky": "خوش شانس", + "Mel": "مل", + "Middle-Man": "کودن", + "Minimus": "مینیموس", + "Pascal": "پنگوئن", + "Pixel": "فرشته", + "Sammy Slam": "سامی کشتی‌گیر", + "Santa Claus": "بابا نوئل", + "Snake Shadow": "سایه ی مار", + "Spaz": "فلج", + "Taobao Mascot": "تائوبائو", + "Todd": "تاد", + "Todd McBurton": "تاد", + "Xara": "ژارا", + "Zoe": "زو", + "Zola": "زولا" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} محل تمرین", + "Infinite ${GAME}": "بی پایان ${GAME}", + "Infinite Onslaught": "نبرد بی‌پایان", + "Infinite Runaround": "دوره بی نهایت", + "Onslaught Training": "نبرد مبتدی", + "Pro ${GAME}": "حرفه ای ${GAME}", + "Pro Football": "فوتبال حرفه‌ای", + "Pro Onslaught": "حمله ی سخت", + "Pro Runaround": "نگهبان خروج", + "Rookie ${GAME}": "${GAME} قدرت", + "Rookie Football": "مبارز فوتبال", + "Rookie Onslaught": "میدان مبارزه", + "The Last Stand": "آخرین مقاومت", + "Uber ${GAME}": "${GAME} بازی", + "Uber Football": "فوتبال حرفه‌ای", + "Uber Onslaught": "هجوم", + "Uber Runaround": "ایست بازرسی" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "یکی از بهترین ها برای پیروزی در زمان بازی است.\nیکی را انتخاب کنید و از بین ببرید تا به آن تبدیل شوید.", + "Bomb as many targets as you can.": "هدف ها را با بمب بزن", + "Carry the flag for ${ARG1} seconds.": "ثانیه${ARG1}حمل و نگهداری پرچم برای", + "Carry the flag for a set length of time.": "حمل و حفاظت از پرچم برای برای یک زمان معین", + "Crush ${ARG1} of your enemies.": "بار از حریف ${ARG1} پیروز شدن", + "Defeat all enemies.": "همه ی حریف ها را شکست بده", + "Dodge the falling bombs.": "از بمب ها جاخالی بده", + "Final glorious epic slow motion battle to the death.": "نبرد در حرکت آهسته", + "Gather eggs!": "تخم مرغ ها رو جمع کن", + "Get the flag to the enemy end zone.": "بردن پرچم به منطقه پایانی حریف", + "How fast can you defeat the ninjas?": "با چه سرعتی میتونید نینجاها رو شکست بدید؟", + "Kill a set number of enemies to win.": "ازبین بردن تعدادی ازحریف ها برای بردن", + "Last one standing wins.": "آخرین کسی که موند برنده س", + "Last remaining alive wins.": "تاریخ و زمان آخرین برنده زنده باقی مانده", + "Last team standing wins.": "آخرین تیمی که می‌مونه تو بازی برنده‌س", + "Prevent enemies from reaching the exit.": "جلوگیری از خروج حریف", + "Reach the enemy flag to score.": "رسیدن به پرچم دشمن", + "Return the enemy flag to score.": "برداشتن پرچم دشمن برای امتیاز گرفتن", + "Run ${ARG1} laps.": "دور ${ARG1} حرکت", + "Run ${ARG1} laps. Your entire team has to finish.": "دور ,کل تیم ${ARG1} حرکت", + "Run 1 lap.": "یک دور", + "Run 1 lap. Your entire team has to finish.": "یک دور کل تیم به خط پایان برسند", + "Run real fast!": "!حرکت واقعا سریع", + "Score ${ARG1} goals.": "گل ${ARG1}زدن", + "Score ${ARG1} touchdowns.": "بار ${ARG1}عبور پرچم ازخط", + "Score a goal.": "گل بزنید", + "Score a touchdown.": "امتیاز پرش", + "Score some goals.": "امتیاز برخی گل ها", + "Secure all ${ARG1} flags.": "پرچم ${ARG1}مال خودکردن همه", + "Secure all flags on the map to win.": "حفاظت از تمام پرچم ها. در نقشه", + "Secure the flag for ${ARG1} seconds.": "ثانیه ${ARG1} حفاظت از پرچم برای", + "Secure the flag for a set length of time.": "حفاظت از پرچم در مدت زمان معین", + "Steal the enemy flag ${ARG1} times.": "بار ${ARG1} ربودن پرچم حریف", + "Steal the enemy flag.": "ربودن پرچم حریف", + "There can be only one.": "صاحب پرچم فقط میتونه یه نفر باشه", + "Touch the enemy flag ${ARG1} times.": "بار ${ARG1}لمس پرچم دشمن", + "Touch the enemy flag.": "لمس پرچم دشمن", + "carry the flag for ${ARG1} seconds": "ثانیه${ARG1}حمل و نگهداری پرچم برای", + "kill ${ARG1} enemies": "حریف ${ARG1} ازبین بردن", + "last one standing wins": "آخرین کسی که موند برنده س", + "last team standing wins": "آخرین تیمی که می‌مونه روصحنه برنده‌س", + "return ${ARG1} flags": "پرچم ${ARG1} بدست آوردن", + "return 1 flag": "بدست آوردن 1 پرچم", + "run ${ARG1} laps": "دور ${ARG1} حرکت", + "run 1 lap": "یک دور", + "score ${ARG1} goals": "گل ${ARG1}زدن", + "score ${ARG1} touchdowns": "بار ${ARG1}عبور پرچم ازخط", + "score a goal": "گل بزنید", + "score a touchdown": "امتیاز یک پرش", + "secure all ${ARG1} flags": "پرچم ${ARG1}مال خودکردن همه", + "secure the flag for ${ARG1} seconds": "ثانیه ${ARG1} حفاظت از پرچم برای", + "touch ${ARG1} flags": "پرچم ${ARG1}لمس", + "touch 1 flag": "لمس 1 پرچم" + }, + "gameNames": { + "Assault": "لمس پرچم", + "Capture the Flag": "تسخیر پرچم", + "Chosen One": "فرد منتخب", + "Conquest": "قدرت نمایی", + "Death Match": "نبرد مرگبار", + "Easter Egg Hunt": "Easterگرفتن تخم مرغ های جشن", + "Elimination": "استقامت", + "Football": "فوتبال آمریکایی", + "Hockey": "هاکی", + "Keep Away": "دور نگه داشتن", + "King of the Hill": "پادشاه تپه", + "Meteor Shower": "بمباران", + "Ninja Fight": "نبرد با نینجاها", + "Onslaught": "مبارزه", + "Race": "مسابقه ی دو", + "Runaround": "مانع", + "Target Practice": "تمرین بمب اندازی", + "The Last Stand": "دفاع آخر" + }, + "inputDeviceNames": { + "Keyboard": "صفحه‌کلید", + "Keyboard P2": "p2 صفحه‌کلید" + }, + "languages": { + "Arabic": "عربی", + "Belarussian": "بلاروس", + "Chinese": "چینی ساده شده", + "ChineseTraditional": "چینی سنتی", + "Croatian": "کرواتی", + "Czech": "چک", + "Danish": "دانمارکی", + "Dutch": "هلندی", + "English": "انگلیسی", + "Esperanto": "اسپرانتو", + "Finnish": "فنلاندی", + "French": "فرانسوی", + "German": "آلمانی", + "Gibberish": "قبريش", + "Greek": "یونانی", + "Hindi": "هندی", + "Hungarian": "مجارستانی", + "Indonesian": "اندونزی", + "Italian": "ایتالیایی", + "Japanese": "ژاپنی", + "Korean": "کره‌ای", + "Persian": "فارسی", + "Polish": "لهستانی", + "Portuguese": "پرتغالی", + "Romanian": "رومانیایی", + "Russian": "روسی", + "Serbian": "صربستانی", + "Slovak": "اسلوواکی", + "Spanish": "اسپانیایی", + "Swedish": "سوئدی", + "Turkish": "ترکی", + "Ukrainian": "اوکراینی", + "Venetian": "ونیزی", + "Vietnamese": "ویتنامی" + }, + "leagueNames": { + "Bronze": "برنز", + "Diamond": "الماس", + "Gold": "طلا", + "Silver": "نقره" + }, + "mapsNames": { + "Big G": "بزرگ G", + "Bridgit": "بریدگیت", + "Courtyard": "حیاط", + "Crag Castle": "قلعه پرتگاه", + "Doom Shroom": "رستاخیز", + "Football Stadium": "استادیوم فوتبال", + "Happy Thoughts": "پرواز", + "Hockey Stadium": "استادیوم هاکی", + "Lake Frigid": "یخبندان", + "Monkey Face": "صورت میمون", + "Rampage": "خشم", + "Roundabout": "میدان", + "Step Right Up": "پایدار", + "The Pad": "رینگ", + "Tip Top": "تیپ تاپ", + "Tower D": "برج دی", + "Zigzag": "زیگ زاگ" + }, + "playlistNames": { + "Just Epic": "فقط حرکت آهسته", + "Just Sports": "فقط ورزشی" + }, + "scoreNames": { + "Flags": "پرچم‌ها", + "Goals": "اهداف", + "Score": "امتیاز", + "Survived": "زنده موندی", + "Time": "زمان", + "Time Held": "زمان برگزاری" + }, + "serverResponses": { + "A code has already been used on this account.": "کد قبلا در این حساب استفاده شده است.", + "A reward has already been given for that address.": "پاداش برای آن آدرس داده شده است.", + "Account linking successful!": "ارتباط موفق با حساب کاربری", + "Account unlinking successful!": "قطع شدن حساب با موفقیت انجام شد", + "Accounts are already linked.": "حساب‌ها قبلا مرتبط شده‌اند", + "An error has occurred; (${ERROR})": "متاسفانه یک مشکل رخ داده ؛ (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "یک مشکل رخ داده! لطفا با پشتیبانی تماس بگیرید؛ (${ERROR})", + "An error has occurred; please contact support@froemling.net.": ".تماس بگیرید support@froemling.net خطایی رخ داده است. لطفاً با", + "An error has occurred; please try again later.": ".خطایی رخ داده است. لطفاً بعداً تلاش کنید", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "آیا مطمئن هستید که می‌خواهید این حساب‌ها را به هم متصل کنید؟\n\n${ACCOUNT1}\n${ACCOUNT2}\n\n!این کار برگشت‌پذیر نیست", + "BombSquad Pro unlocked!": "!نسخهٔ حرفه‌ای باز شد", + "Can't link 2 accounts of this type.": ".نمی‌توان دو حساب از این نوع را پیوند داد", + "Can't link 2 diamond league accounts.": "نمی‌توان حساب دو لیگ الماس را پیوند داد.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "پیوند نمی‌شود; حداکثر از ${COUNT} پیوند پشتیبانی می‌شود.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "تقلب تشخیص داده شد; نمرات و جوایز برای ${COUNT} روز تعلیق شد.", + "Could not establish a secure connection.": "نمیتوان یک اتصال امن ایجاد کرد", + "Daily maximum reached.": "به حداکثر روزانه رسیده است", + "Entering tournament...": "ورود به مسابقات ...", + "Invalid code.": "کد نامعتبر", + "Invalid payment; purchase canceled.": "پرداخت با خطا مواجه شد؛ خرید لغو شد", + "Invalid promo code.": "کد نامعتبر است.", + "Invalid purchase.": "خرید نا معتبر", + "Invalid tournament entry; score will be ignored.": "ورود مسابقات نامعتبر است؛ نمره نادیده گرفته می شود.", + "Item unlocked!": "!مورد باز شد", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "پیوند کردن ممنوع شد ${ACCOUNT} شامل\nداده های قابل توجهی که می توانند از دست بدهند.\nشما می توانید در صورت مخالفت پیوست کنید اگر دوست دارید\n(و در عوض داده های این حساب را از دست می دهید)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "را به این حساب پیوند دهید؟ ${ACCOUNT} آیا مایلید که حساب کاربری \n.از بین خواهد رفت ${ACCOUNT} تمام اطلاعات روی حساب\nاین کار برگشت‌پذیر نیست. آیا مطمئنید؟", + "Max number of playlists reached.": "تعداد بازی ها به حداکثر رسیده است", + "Max number of profiles reached.": ".تعداد نمایه‌ها به حداکثر رسیده است", + "Maximum friend code rewards reached.": ".حداکثر جایزه کد ارسالی برای دوستان دریافت شد", + "Message is too long.": "پیام خیلی طولانی است", + "Profile \"${NAME}\" upgraded successfully.": ".با موفقیت ارتقا یافت «${NAME}» نمایهٔ", + "Profile could not be upgraded.": ".نمایه نمی‌تواند ارتقا یابد", + "Purchase successful!": "خرید با موفقیت انجام شد", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "بدست آمده ${COUNT} بلیت برای ورود به سیستم.\nفردا برگرد برای دریافت ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "این نسخه از بازی دیگر توسط سرور پشتیبانی نمی شود.\nلطفا از جدید ترین نسخه بازی استفاده کنید.", + "Sorry, there are no uses remaining on this code.": "با عرض پوزش, این کد دیگر کاربرد ندارد", + "Sorry, this code has already been used.": "با عرض پوزش، این کد قبلا استفاده شده است.", + "Sorry, this code has expired.": "متاسفانه این کد منقضی شده", + "Sorry, this code only works for new accounts.": "با عرض وزش پوزش, این کد فقط برا حساب کاربری جدید کاربرد داره", + "Temporarily unavailable; please try again later.": "در حال حاضر این گذینه موجود نمی باشد؛لطفا بعدا امتحان کنید", + "The tournament ended before you finished.": "مسابقات به پایان رسید قبل از اینکه شما به پایان برسید.", + "This account cannot be unlinked for ${NUM} days.": "این حساب برای مدت ${NUM} روز قابل جداسازی نیست!", + "This code cannot be used on the account that created it.": "کد در حسابی که با آن ساخته شده قابل استفاده نیست", + "This is currently unavailable; please try again later.": ".این گزینه در حال حاضر در دسترس نیست; لطفا بعدا دوباره تلاش کنید", + "This requires version ${VERSION} or newer.": "یا جدیدتر باشد ${VERSION} برای این کار نسخه ی بازی باید", + "Tournaments disabled due to rooted device.": "مسابقات به دلیل روت بودن دستگاه غیر فعال شده است", + "Tournaments require ${VERSION} or newer": "تورنومنت ها به${VERSION}جدید تر یا حرفه ای نیاز دارند", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "آیا مایلبد حساب ${ACCOUNT} را از حساب خودتان جداسازی کنید؟\nتمام اطلاعات حساب ${ACCOUNT} بازنویسی خواهد شد.\n(به جز بعضی از افتخارات کسب شده)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "اخطار: شکایت هایی مبنی بر استفاده شما از ابزار های تقلب به دست ما رسیده!\nدر صورت تکرار حساب کاربری شما مسدود خواهد شد! لطفا جوانمردانه بازی کنید.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "می‌خواهید دستگاه‌تون رو به این دستگاه متصل کنید؟\n\nحساب دستگاهی شما ${ACCOUNT1}\nاین حساب ${ACCOUNT2}\n\nاین مشکلی توی سیو و پیشرفت شما ایجاد نمیکنه.\nاخطار: بعدا نمی‌تونید پشیمون بشید!", + "You already own this!": "قبلا اینو گرفتی", + "You can join in ${COUNT} seconds.": ".ثانیه ${COUNT} شما میتوانید متصل شوید در", + "You don't have enough tickets for this!": "شما بلیط کافی برای این ندارید", + "You don't own that.": ".اون مال شما نیست", + "You got ${COUNT} tickets!": "! تا بلیط گرفتی ${COUNT}", + "You got a ${ITEM}!": "! گرفتی ${ITEM} یه دونه", + "You have been promoted to a new league; congratulations!": "شما به یک لیگ جدید ارتقا داده‌اید؛ تبریک می‌گوییم!", + "You must update to a newer version of the app to do this.": "برای انجام این کار شما باید بازی را به روز رسانی کنید", + "You must update to the newest version of the game to do this.": "برای انجام این کار باید از آخرین نسخه بازی استفاده کنید.", + "You must wait a few seconds before entering a new code.": "شما باید چند ثانیه قبل از وارد کردن یک کد جدید صبر کنید.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "! شدید . ممنون که شرکت کردید ${RANK} شما در آخرین مسابقه رتبه ی", + "Your account was rejected. Are you signed in?": "حساب شما رد شده است. آیا وارد حساب خود شده اید؟", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "نسخه بازی شما دستکاری شده است.\nلطفا تغییرات رو به حالت اول برگردونید و دوباره امتحان کنید.", + "Your friend code was used by ${ACCOUNT}": "کد دوست شما استفاده شده توسط ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "یک دقیقه", + "1 Second": "یک ثانیه", + "10 Minutes": "ده دقیقه", + "2 Minutes": "دو دقیقه", + "2 Seconds": "دو ثانیه", + "20 Minutes": "۲۰ دقیقه", + "4 Seconds": "چهار ثانیه", + "5 Minutes": "پنج دقیقه", + "8 Seconds": "هشت ثانیه", + "Allow Negative Scores": "نمرات منفی", + "Balance Total Lives": "مجموعِ جانِ باقيمانده", + "Bomb Spawning": "بازسازی بمب", + "Chosen One Gets Gloves": "دستکش بکس برای مالک پرچم", + "Chosen One Gets Shield": "قرار دادن سپر برای مالک پرچم", + "Chosen One Time": "انتخاب زمان", + "Enable Impact Bombs": "فعالسازی تاثیر بمب ها", + "Enable Triple Bombs": "فعال کردن بمب سه گانه", + "Entire Team Must Finish": "کل تیم باید نابود بشن", + "Epic Mode": "حرکت آهسته", + "Flag Idle Return Time": "پرچم زمان بازگشت بیدرنگ", + "Flag Touch Return Time": "زمان بازگشت لمس پرچم", + "Hold Time": "زمان نگه داشتن", + "Kills to Win Per Player": "هرکی بیشتر نابود کنه", + "Laps": "دور ها", + "Lives Per Player": "تعداد جان برای بازیکن", + "Long": "طولانی", + "Longer": "خیلی طولانی", + "Mine Spawning": "مین گذاری", + "No Mines": "بدون مین", + "None": "هيچ", + "Normal": "معمولی", + "Pro Mode": "حالت حرفه ای", + "Respawn Times": "زمان برگشت بازیکن مرده", + "Score to Win": "امتیاز بگیر تا برنده شی", + "Short": "کوتاه", + "Shorter": "کوتاه تر", + "Solo Mode": "حالت انفرادی", + "Target Count": "تعداد هدف", + "Time Limit": "محدودیت زمانی" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "بازی‌رو ترک کرد ${PLAYER} ردصلاحیت شد چون ${TEAM}", + "Killing ${NAME} for skipping part of the track!": "! از بین رفت چون جِرزنی کرد ${NAME}", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "هشدار به ${NAME}: توربو/اسپم دادن شما را مسدود میکند" + }, + "teamNames": { + "Bad Guys": "تیم حریف", + "Blue": "آبی", + "Good Guys": "تیم خودی", + "Red": "قرمز" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "یه مشت جهشی پرشی چرخشی با زمان بندی درست ، با یه ضربه حریف رو نابود میکنه\n! از این حرکتا بزن بعدش برا دوستات تعریف کن", + "Always remember to floss.": "حرفه‌ای باش، بَرنده باش..", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "برای خودت و دوستات نمایه بساز تا مجبور نشی\n.از «بازیکنان تصادفی» خودِ بازی استفاده کنی", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "جعبه ی مرگ تو رو تبدیل به یه بمب ساعتی می کنه\nاگه می خوای زنده بمونی باید سریع یه جعبه درمان بگیری", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "تمام کارکتر های بازی دارای توانایی های یکسانی هستند بنابراین شما فقط کافیه\nنزدیکترین کارکتر به شخصیت خودتون رو انتخاب و پیکربندی کنید", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "فکر میکنی با گرفتن ضد ضربه شکست ناپذیر میشی ؟ اگه از زمین پرت بشی پایین چی ؟", + "Don't run all the time. Really. You will fall off cliffs.": "تمام وقت درطول بازی با سرعت حرکت نکنید ممکنه از صخره پرت بشید", + "Don't spin for too long; you'll become dizzy and fall.": "زیاد به دور خودت نچرخ؛ سرگیجه می‌گیری و می‌افتی.", + "Hold any button to run. (Trigger buttons work well if you have them)": "هر دکمه رو با زدن روش کار میکنه همه دکمه ها بدرستی کار میکنه اگه صحیح زده بشن", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "موقع حرکت یه دکمه رو همزمان نگهدار مثلا مشت رو تا سرعت حرکت بیشتر شه\nاما توی سرعت زیاد مراقب پایین افتادن از صخره باش", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "بمب های یخی برا نابودی خیلی قدرتمند نیستن ولی نزدیک هر کی منفجر بشه منجمد میشه \n اگه نمیخواید منجمد و آسیب پذیر بشوید نزدیک این بمب ها نمونید", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "اگه کسی شما رو از زمین بلند کرد با مشت و پرش و کشیدن اجازه \nحرکت بهش ندید تا رها بشید", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "اگر روی کنترلرها کوتاه هستید، برنامه '${REMOTE_APP_NAME}' را نصب کنید\nبر روی دستگاه های تلفن همراه خود را میتوان به عنوان کنترلر استفاده کنید.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "اگر بمب چسبنده ای به شما چسبید به اطراف بپرید و بچرخید\n😅کار دیگری از شما بر نمی‌آید", + "If you kill an enemy in one hit you get double points for it.": "اگه حریف رو تنها با یک ضربه نابود کنی امتیاز دو برابر بهت تعلق میگیره برا این حرکت", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "اگه یه جعبه نابودگر رو به اشتباه گرفتین کارتون تمومه مگه اینکه ظرف چند\nثانیه یه جعبه ی درمان گیر بیارید", + "If you stay in one place, you're toast. Run and dodge to survive..": "اگه فقط توی یه مکان ثابت بمونید کباب میشید پس بهتره مرتب توی زمین تحرک داشته باشی", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "اگر تعداد زیادی از بازیکنان در حال رفت و آمد هستند، بازیکنان \"بیروت انداختن بازیکنان غیرفعال\" را روشن کنید\nتحت تنظیمات در صورتی که کسی فراموش می‌کندپیش از خروج از بازی ترک بازی را بزند.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "اگر دستگاه شما بیش از حد گرم می شود یا شما می خواهید برای حفظ قدرت باتری،\nکم کنید \"کیفیت\" یا \"وضوح تصویر\" رو در تنظیمات> گرافیک", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "اگر دیدید که نرخ فریم تصویر کمی متلاطم است کاهش بدید رزولوشن \nیا کیفیت تصویر رو در تنظیمات گرافیک بازی", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "توی بازی رساندن پرچم ، باید پرچم تیم شما همون جا باقی بمونه\nاگه تیم حریف پرچم تیم شما رو برداشت باید از دستش کش بری", + "In hockey, you'll maintain more speed if you turn gradually.": "روی زمین های یخی ، یه دفعه چرخیدن خیلی از سرعت بازیکن کم میکنه", + "It's easier to win with a friend or two helping.": "بازی خیلی راحت تر میشه اگه از یکی دوتا از دوستاتون کمک بگیرید", + "Jump just as you're throwing to get bombs up to the highest levels.": "پرش فقط به درد این می خوره که بمب ها رو بالاتر پرتاب کنی", + "Land-mines are a good way to stop speedy enemies.": "بهترین روش نابود کردن دشمن های سریع ، استفاده از مین زمینی است", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "خیلی چیزها می تونن از زمین برداشته بشن ... حتی بازیکنهای دیگه\nیه راه خوب برای از بین بردن دشمنها اینه که بلندشون کنی و پرتشون کنی توی دره ... که خیلی هم حال میده", + "No, you can't get up on the ledge. You have to throw bombs.": "! فکر کردی خونه ی خاله س ؟ باید بزنی لهشون کنی", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "بازیکن ها میتونن هر زمان وسط بازی وارد بازی بشن\nاگه کسی برای ورود مجدد مشکلی داشت ترک کنه دسته رو ومجدد وارد بشه", + "Practice using your momentum to throw bombs more accurately.": "تمرین کنید با استفاده از شتاب بیشتر دقیق تر پرتاب کنید بمب ها رو", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "برای قوی تر شدن مشت ها باید\nمثل دیوونه ها بدوی و بپری و بچرخی", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "حرکت شلاقی به عقب و جلو برای پرتاب بمب\nبه مسافت بیشتر", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "برای از بین بردن یه گله دشمن\nکافیه نزدیکشون یه جعبه تی.ان.تی بترکونی", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "سر آسیب پذیر ترین قسمت بدن یه بازیکنه بطوریکه \nاگه یه بمب چسبنده به سرش چسبید دیگه کارش تمومه", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "این مرحله هیچ وقت تمام نمی شود\nولی امتیازات بالای آن در سطح جهانی ثبت خواهد شد", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "هر چیزی دستت باشه به اون سمتی پرتاب میشه که داری حرکت میکنی\nاگه میخوای بندازیش جلوی پای خودت اصلا حرکت نکن", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "! از موزیک بازی خوشت نمیاد ؟ خب عوضش کن\nبرو به تنظیمات ، صدا ، موسیقی بازی", + "Try 'Cooking off' bombs for a second or two before throwing them.": "قبل از پرتاب بمب چند لحظه نگهشون دارید بعد پرتاب کنید تا هنگام رسیدن به حریف سریع منفجر شوند", + "Try tricking enemies into killing eachother or running off cliffs.": "سعی کنید جاهایی باشید که دشمنان اشتباهی همدیگر را بزنند یا به پایین پرتاب شوند", + "Use the pick-up button to grab the flag < ${PICKUP} >": "با این دکمه می توانید پرچم را از زمین یا از دست حریف بگیرید< ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "حرکت شلاقی سریع به چپ یا راشت برای پرتاب بمب به مسافت بیشتر", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "شما می تونید مشت های خودتون رو به جهت مشخصی وارد کنید با چرخیدن به چپ و راست\nاین کار برای پرت کردن دشمنان از لبه ی زمین به پایین یا امتیاز گرفت در هاکی موثره", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "با دیدن رنگ جرقه ی بمب میشه فهمید چه موقع منفجر میشه\n!!!! زرد ... نارنجی ... قرمز ... بوم", + "You can throw bombs higher if you jump just before throwing.": "شما می‌تونید بمب‌هاتون رو دورتر پرتاب کنید اگه همزمان با یه پرش به موقع باشه", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "شما آسیب می بینید زمانی که سرتون به چیزی برخورد میکنه پس بیشر احتیاط کنید\nزمانی هم که چیزی به سمتتون پرتاب میشه", + "Your punches do much more damage if you are running or spinning.": "مشت هاتون موثر تر خواهند بود اگر هنگام حرکت سریع و یا چرخش وارد بشن" + } + }, + "trophiesRequiredText": "جام نیاز دارد ${NUMBER} این حداقل به", + "trophiesText": "تندیس و مدال", + "trophiesThisSeasonText": "تندیس ها و مدال های این فصل", + "tutorial": { + "cpuBenchmarkText": "آموزش اجرای بهتر بازی در بدترین وضعیت سرعت بازی در درجه اول تست سرعت پردازنده", + "phrase01Text": "!سلام جیگر", + "phrase02Text": "! خوش آمدی ${APP_NAME} به", + "phrase03Text": ". قبل از شروع ، یک سری آموزش ها هست که باید ببینی", + "phrase04Text": "متکی به اعمال فیزیکی اند ${APP_NAME} چیزای زیادی در", + "phrase05Text": "... مثلا وقتی با مشت میفتی به جون یه نفر", + "phrase06Text": "! این جوری حتی خون از دماغش نمیاد", + "phrase07Text": "... چجوری برات دلقک بازی در میاره ${NAME} نگاه کن", + "phrase08Text": ".باید حالشو بگیری ! پرش و چرخش قدرت مشت رو بیشتر می کنه", + "phrase09Text": "! آهان ! حالا شد", + "phrase10Text": ".دویدن هم موثره", + "phrase11Text": "برای دویدن باید یه دکمه ی دلخواه رو فشار بدی و نگه داری", + "phrase12Text": "! دویدن و چرخیدن باعث میشه که فکش بیاد پایین", + "phrase13Text": "اومد پایین ؟ ${NAME} دیدی چطوری فک", + "phrase14Text": "${NAME} خیلی چیزها رو میشه بلند کرد و پرت کرد . یعنی پرچم یا بازیکنهای دیگه مثل", + "phrase15Text": "! حالا وقت بمبارون کردنه", + "phrase16Text": "بمب انداختن یه کم تمرین لازم داره", + "phrase17Text": "این چه طرز بمب انداختنه ؟", + "phrase18Text": "اگه حرکت کنی بمب دورتر پرت میشه", + "phrase19Text": "اگه بپری بمب بیشتر پرت میشه", + "phrase20Text": "! اگه به صورت شلاقی بمب پرت کنی که دیگه هیچی ... نمی دونی تا کجا میره", + "phrase21Text": "این که چه موقع پرتش کنی خیلی مهمه", + "phrase22Text": "!!! بوم", + "phrase23Text": "قبل از انداختن بمب یه کم نگهش دار ... تا به محض این که به هدف رسید بترکه", + "phrase24Text": "باریکلا ! به این میگن انفجار", + "phrase25Text": "دیدی اصلا سخت نبود ؟", + "phrase26Text": "حالا دیگه مثل یه ببر قوی شدی", + "phrase27Text": "دشمنا منتظرت هستن ... پس این آموزش ها رو به خاطر بسپار", + "phrase28Text": "! ببینم چند مرده حلاجی پهلوون", + "phrase29Text": "! خدا قوت", + "randomName1Text": "شایان", + "randomName2Text": "بهنام", + "randomName3Text": "کاوه", + "randomName4Text": "مهدی", + "randomName5Text": "بهرام", + "skipConfirmText": ".واقعا از آموزش رد می‌شی ؟ هر کلیدی رو بزن تا رد بشیم", + "skipVoteCountText": "نفر خواستار رد شدن از آموزش هستند ${TOTAL} نفر از ${COUNT}", + "skippingText": "از آموزش می گذریم", + "toSkipPressAnythingText": "هر کلیدی را بزنید تا از آموزش خارج شوید" + }, + "twoKillText": "نابودی دونفر همزمان", + "unavailableText": "چیزی در دسترس نیست", + "unconfiguredControllerDetectedText": ":کنترول پیکربندی نشده شناسایی شد", + "unlockThisInTheStoreText": ". این مورد باید در فروشگاه باز شود", + "unlockThisProfilesText": "برای ایجاد بیش از ${NUM} پروفال٫ احتیاج به این موارد دارید:", + "unlockThisText": ": برا باز کردن قفل این شما نیاز دارید که", + "unsupportedHardwareText": "با عرض پوزش، این سخت افزار توسط این ساخت بازی پشتیبانی نمی شود.", + "upFirstText": "برای بار اول :", + "upNextText": "${COUNT} بعدی در بازی", + "updatingAccountText": "... در حال به‌روزرسانی حساب", + "upgradeText": "ارتقا", + "upgradeToPlayText": "بازی را خریداری کنید تا این گزینه فعال شود ${PRO} نسخه ی", + "useDefaultText": "استفاده از پیش فرض", + "usesExternalControllerText": "این بازی از یک کنترلر خارجی برای ورودی استفاده می کند.", + "usingItunesText": "استفاده از برنامه ی موسیقی برای موسیقی متن", + "usingItunesTurnRepeatAndShuffleOnText": "مطمین شید که شافل روشن است و تکرار کنید همه رو در آیتونز", + "validatingTestBuildText": "... در حال بررسی حالت آزمایشی", + "victoryText": "! برنده شدی", + "voteDelayText": ".ثانیه رای گیری کنید ${NUMBER} شما نمیتوانید به مدت", + "voteInProgressText": ".یک رای گیری در حال انجام است", + "votedAlreadyText": ".شما رای داده اید", + "votesNeededText": ".رای نیاز است ${NUMBER}", + "vsText": "علیه", + "waitingForHostText": "ادامه بدهد ${HOST} صبر می کنیم تا", + "waitingForPlayersText": "...انتظار برای پیوستن بازیکنان", + "waitingInLineText": "در صف انتظار(پارتی تکمیل است) ...", + "watchAVideoText": "یک ویدئو ببینید", + "watchAnAdText": "تبلیغ ببین", + "watchWindow": { + "deleteConfirmText": "حذف شود؟\"${REPLAY}\"", + "deleteReplayButtonText": "حذف\nبازبخش", + "myReplaysText": "بازیهای ضبط شده‌ی من", + "noReplaySelectedErrorText": "بازپخشی انتخاب نشده", + "playbackSpeedText": "سرعت باز پخش:${SPEED}", + "renameReplayButtonText": "تغییرنام\nبازبخش", + "renameReplayText": ":به \"${REPLAY}\"تغییر نام", + "renameText": "تغییر نام", + "replayDeleteErrorText": "خطا در حذف بازبخش", + "replayNameText": "نام بازبخش", + "replayRenameErrorAlreadyExistsText": "بازبخش با این نام از قبل وجود دارد", + "replayRenameErrorInvalidName": "نمی‌توان نام بازبخش را تغیر داد: نام نامعتبر است", + "replayRenameErrorText": "خطا در تغییر نام بازپخش.", + "sharedReplaysText": "اشتراک گزاری بازی ها", + "titleText": "بازی های ضبط شده", + "watchReplayButtonText": "تماشای بازی\nضبط شده" + }, + "waveText": "دست", + "wellSureText": "! حتما", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "گوش دادن به Wiimotes ...", + "pressText": "دکمه های Wiimote 1 و 2 را به طور همزمان فشار دهید.", + "pressText2": "در Wiimotes جدیدتر با حرکت به علاوه در ساخته شده است، به جای فشار قرمز \"همگام\" را فشار دهید در پشت." + }, + "wiimoteSetupWindow": { + "copyrightText": "ناظر بر کپی رایت", + "listenText": "گوش بده", + "macInstructionsText": "اطمینان حاصل کنید که رشته خود خاموش است و بلوتوث را فعال کنید\nدر مک خود را، و سپس دکمه \"گوش دهید\". پشتیبانی Wiimote می توانید\nیک کمی پوسته پوسته، بنابراین شما ممکن است باید سعی کنید چند بار\nقبل از شما یک اتصال.\nبلوتوث باید به 7 دستگاه های متصل رسیدگی کردن،\nهر چند مسافت پیموده شده شما ممکن است متفاوت باشد.\n\nBombSquad پشتیبانی از Wiimotes اصلی، Nunchuks،\nو کنترل کلاسیک.\nجدیدتر رشته از راه دور علاوه در حال حاضر بیش از حد کار\nاما با فایل پیوست است.", + "thanksText": "تشکر از تیم ناظر\nبرای ایجاد این امکان", + "titleText": "Wiimote Setup" + }, + "winsPlayerText": "${NAME} برنده شد", + "winsTeamText": "${NAME} برنده شد", + "winsText": "${NAME} برنده شد", + "worldScoresUnavailableText": "امتیاز های جهانی قابل دسترس نیستند.", + "worldsBestScoresText": "بهترین امتیازهای جهانی", + "worldsBestTimesText": "بهترین زمان های جهانی", + "xbox360ControllersWindow": { + "getDriverText": "درایور", + "macInstructions2Text": "برای استفاده از کنترلرها به صورت بی سیم، شما همچنین باید گیرنده را دریافت کنید\nXbox 360 Wireless Controller for Windows می آید.\nیک گیرنده به شما اجازه می دهد تا تا 4 کنترل کننده را وصل کنید.\n\nمهم: گیرنده های شخص ثالث با این راننده کار نخواهند کرد؛\nاطمینان حاصل کنید که گیرنده شما \"مایکروسافت\" را در آن می گوید، نه \"XBOX 360\".\nمایکروسافت این را به طور جداگانه به فروش نمی رساند، بنابراین شما باید آن را دریافت کنید\nیک همراه با کنترلر و یا دیگری جستجو بی.\n\nاگر این مفید را پیدا کنید، لطفا کمک مالی به آن بدهید\nتوسعه دهنده راننده در سایت خود.", + "macInstructionsText": "برای استفاده از کنترلر Xbox 360، باید نصب کنید\nدرایور Mac موجود در لینک زیر است.\nبا کنترلر های سیمی و بی سیم کار می کند.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "برای استفاده از کنترلر Xbox 360 با BombSquad، به سادگی\nآنها را به پورت USB دستگاهتان وصل کنید. شما می توانید یک هاب USB استفاده کنید\nبرای اتصال چندین کنترل کننده.\n\nبرای استفاده از کنترلرهای بی سیم، به یک گیرنده بی سیم نیاز دارید\nبه عنوان بخشی از \"کنسول بی سیم Xbox 360 برای ویندوز\"\nبسته بندی یا فروش جداگانه. هر گیرنده به یک پورت USB وصل می شود\nاجازه می دهد تا به 4 کنترل کننده بی سیم وصل شوید.", + "titleText": "${APP_NAME}:استفاده از کنترولر های ایکس باکس با" + }, + "yesAllowText": "!بله, اجازه داده میشود", + "yourBestScoresText": "بهترین امتیاز شما", + "yourBestTimesText": "بهترین زمان شما" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/polish.json b/dist/ba_data/data/languages/polish.json new file mode 100644 index 0000000..89f505c --- /dev/null +++ b/dist/ba_data/data/languages/polish.json @@ -0,0 +1,1942 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Nazwy kont nie mogą zawierać emotikonków ani innych znaków specjalnych", + "accountProfileText": "(profil konta)", + "accountsText": "Konta", + "achievementProgressText": "Osiągnięcia: ${COUNT} z ${TOTAL}", + "campaignProgressText": "Postęp Kampanii [Trudny]: ${PROGRESS}", + "changeOncePerSeason": "Możesz to zmienić tylko raz na sezon.", + "changeOncePerSeasonError": "Musisz poczekać do następnego sezonu by znowu to zmienić (${NUM} dni)", + "customName": "Losowa Nazwa", + "linkAccountsEnterCodeText": "Wpisz Kod", + "linkAccountsGenerateCodeText": "Wygeneruj Kod", + "linkAccountsInfoText": "(przenoś postęp między różnymi platformami)", + "linkAccountsInstructionsNewText": "Aby połączyć dwa konta, wygeneruj kod na pierwszym,\ni wpisz ten kod na drugim. Postęp z drugiego\nbędzie podzielony między oba konta.\n(Postęp z pierwszego zostanie utracony)\n\nMożesz połączyć do ${COUNT} kont.\n\nWAŻNE: łącz tylko swoje własne konta;\nJeśli łączysz konto ze znajomym, nie będziecie\nmogli grać przez internet w tym samym czasie.", + "linkAccountsInstructionsText": "By połączyć dwa konta, wygeneruj kod\nna jednym z nich i wpisz na drugim.\nPostęp i ekwipunek zostaną połączone.\nMożesz połączyć do ${COUNT} kont.\n\nUWAGA: Łącz tylko konta, które należą do Ciebie!\nJeśli połączysz konto z przyjacielem,\nnie będziecie mogli grać w tym samym czasie!\n\nAktualnie nie można tego cofnąć, więc uważaj!", + "linkAccountsText": "Połącz Konta", + "linkedAccountsText": "Połączone Konta:", + "nameChangeConfirm": "Zmienić twoją nazwę konta na ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Spowoduje to wyczyszczenie postępu i lokalnych\nrekordów w trybie Kooperacji (kupony pozostaną).\nOperacja nieodwracalna. Jesteś pewny?", + "resetProgressConfirmText": "Spowoduje wyczyszczenie postępu,\nosiągnięć i lokalnych rekordów w\ntrybie Kooperacji (kupony pozostaną).\nOperacja nieodwracalna. Jesteś pewny?", + "resetProgressText": "Wyczyść postęp", + "setAccountName": "Wybierz nazwę konta", + "setAccountNameDesc": "Wybierz nazwę do wyświetlenia dla swojego konta.\nMożesz użyć nazwy z jednego z połączonych kont\n lub utworzyć unikalną niestandardową nazwę.", + "signInInfoText": "Zapisz się, by zbierać kupony, rywalizować online\ni przenosić postęp gry między urządzeniami", + "signInText": "Zapisz się", + "signInWithDeviceInfoText": "(automatyczne konto dostępne tylko z tego urządzenia)", + "signInWithDeviceText": "Zapisz się kontem z urządzenia.", + "signInWithGameCircleText": "Zapisz się z Game Circle", + "signInWithGooglePlayText": "Zapisz się kontem Google Play", + "signInWithTestAccountInfoText": "Konto.", + "signInWithTestAccountText": "Zapisz się testowym kontem.", + "signOutText": "Wypisz się", + "signingInText": "Zapisywanie się...", + "signingOutText": "Wypisywanie...", + "testAccountWarningCardboardText": "Ostrzeżenie: Zapisujesz się przy użyciu konta \"test\".\nZostanie ono zastąpione kontem Google w momencie\nwspierania gry przez Google Cardboard.\n\nOd tego momentu musisz zdobywać wszystkie kupony w grze.\n(zaktualizuj grę do wersji BombSquad Pro za darmo)", + "testAccountWarningOculusText": "Ostrzeżenie: zapisujesz się przy użyciu konta \"test\".\nZostanie ono zastąpione kontem oculusowym jeszcze w tym roku,\nktóre będzie oferowało zakup kuponów i inne funkcje.\n\nTeraz musisz zarobić wszystkie kupony grając.\n(jednakże możesz uzyskać aktualizację do wersji Pro za darmo)", + "testAccountWarningText": "Ostrzeżenie: możesz się zapisać używając konta \"test\".\nTo konto jest powiązane z konkretnym urządzeniem i \nmoże okresowo się zresetować. (wobec tego proszę nie\nzbierać/odblokowywać rzeczy lub osiągnięć dla tego konta)", + "ticketsText": "Kuponów: ${COUNT}", + "titleText": "Konto", + "unlinkAccountsInstructionsText": "Wybierz konto do rozłączenia", + "unlinkAccountsText": "Rozłącz konta", + "viaAccount": "(przez konto ${NAME})", + "youAreLoggedInAsText": "Jesteś zalogowany jako:", + "youAreSignedInAsText": "Jesteś zapisany jako:" + }, + "achievementChallengesText": "Lista Osiągnięć i Wyzwań", + "achievementText": "Osiągnięcia", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Zabij 3 złych gości używając TNT", + "descriptionComplete": "Zabiłeś 3 złych gości używając TNT", + "descriptionFull": "Zabij 3 złych gości za pomocą TNT w trybie ${LEVEL}", + "descriptionFullComplete": "Zabiłeś 3 złych gości za pomocą TNT w trybie ${LEVEL}", + "name": "Uwaga Leci Dynamit" + }, + "Boxer": { + "description": "Wygraj bez używania bomb", + "descriptionComplete": "Wygrałeś bez używania bomb", + "descriptionFull": "Ukończ ${LEVEL} bez używania żadnych bomb", + "descriptionFullComplete": "Ukończyłeś ${LEVEL} bez używania żadnych bomb", + "name": "Bokser" + }, + "Dual Wielding": { + "descriptionFull": "Podłącz dwa kontrolery (fizyczne lub BSRemote)", + "descriptionFullComplete": "Podłączone dwa kontrolery (fizyczne lub BSRemote)", + "name": "Podwójne dzierżenie" + }, + "Flawless Victory": { + "description": "Wygraj nie dając się uderzyć", + "descriptionComplete": "Wygrałeś nie dając się uderzyć", + "descriptionFull": "Wygraj w trybie ${LEVEL} nie dając się uderzyć", + "descriptionFullComplete": "Wygrałeś w trybie ${LEVEL} nie dając się uderzyć", + "name": "Zwycięstwo bez skazy" + }, + "Free Loader": { + "descriptionFull": "Zacznij grę Free-For-All z dwoma graczami lub więcej", + "descriptionFullComplete": "Zaczęto grę Free-For-All z dwoma, lub większą ilością graczy", + "name": "Łącznik graczy" + }, + "Gold Miner": { + "description": "Zabij 6 złych gości z użyciem min lądowych", + "descriptionComplete": "Zabiłeś 6 złych gości z użyciem min lądowych", + "descriptionFull": "Zabij 6 złych gości za pomocą min lądowych w trybie ${LEVEL}", + "descriptionFullComplete": "Zabiłeś 6 złych gości za pomocą min lądowych w trybie ${LEVEL}", + "name": "Złoty Saper" + }, + "Got the Moves": { + "description": "Wygraj bez użycia pięści i bomb", + "descriptionComplete": "Wygrałeś bez użycia pięści i bomb", + "descriptionFull": "Wygraj w trybie ${LEVEL} bez użycia pięści i bomb", + "descriptionFullComplete": "Wygrałeś w trybie ${LEVEL} bez użycia pięści i bomb", + "name": "Masz Ruchy" + }, + "In Control": { + "descriptionFull": "Podłącz kontroler (fizyczny lub BSRemote)", + "descriptionFullComplete": "Podłączono kontroler (fizyczny lub BSRemote)", + "name": "Pod kontrolą" + }, + "Last Stand God": { + "description": "Zdobądź 1000 punktów", + "descriptionComplete": "Zdobyłeś 1000 punktów", + "descriptionFull": "Zdobądź 1000 punktów w trybie ${LEVEL}", + "descriptionFullComplete": "Zdobyłeś 1000 punktów w trybie ${LEVEL}", + "name": "Bóg trybu ${LEVEL}" + }, + "Last Stand Master": { + "description": "Zdobądź 250 punktów", + "descriptionComplete": "Zdobyłeś 250 punktów", + "descriptionFull": "Zdobądź 250 punktów w trybie ${LEVEL}", + "descriptionFullComplete": "Zdobyłeś 250 punktów w trybie ${LEVEL}", + "name": "Mistrz trybu ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Zdobądź 500 punktów", + "descriptionComplete": "Zdobyłeś 500 punktów", + "descriptionFull": "Zdobądź 500 punktów w trybie ${LEVEL}", + "descriptionFullComplete": "Zdobyłeś 500 punktów w trybie ${LEVEL}", + "name": "Czarodziej trybu ${LEVEL}" + }, + "Mine Games": { + "description": "Zabij 3 złych gości minami lądowymi", + "descriptionComplete": "Zabiłeś 3 złych gości minami lądowymi", + "descriptionFull": "Zabij 3 złych gości za pomocą min lądowych w trybie ${LEVEL}", + "descriptionFullComplete": "Zabiłeś 3 złych gości za pomocą min lądowych w trybie ${LEVEL}", + "name": "Saperskie Gierki" + }, + "Off You Go Then": { + "description": "Wyrzuć 3 złych gości poza mapę", + "descriptionComplete": "Wyrzuciłeś 3 złych gości z mapy", + "descriptionFull": "Zrzuć 3 złych gości z mapy w trybie ${LEVEL}", + "descriptionFullComplete": "Zrzuciłeś 3 złych gości z mapy w trybie ${LEVEL}", + "name": "Spadaj!" + }, + "Onslaught God": { + "description": "Zdobądź 5000 punktów", + "descriptionComplete": "Zdobyłeś 5000 punktów", + "descriptionFull": "Zdobądź 5000 punktów w trybie ${LEVEL}", + "descriptionFullComplete": "Zdobyłeś 5000 punktów w trybie ${LEVEL}", + "name": "Bóg trybu ${LEVEL}" + }, + "Onslaught Master": { + "description": "Zdobądź 500 punktów", + "descriptionComplete": "Zdobyłeś 500 punktów", + "descriptionFull": "Zdobądź 500 punktów w trybie ${LEVEL}", + "descriptionFullComplete": "Zdobyłeś 500 punktów w trybie ${LEVEL}", + "name": "Mistrz trybu ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Pokonaj wszystkie fale", + "descriptionComplete": "Pokonałeś wszystkie fale", + "descriptionFull": "Pokonaj wszystkie fale w trybie ${LEVEL}", + "descriptionFullComplete": "Pokonałeś wszystkie fale w trybie ${LEVEL}", + "name": "Zwycięstwo w trybie ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Zdobądź 1000 punktów", + "descriptionComplete": "Zdobyłeś 1000 punktów", + "descriptionFull": "Zdobądź 1000 punktów w trybie ${LEVEL}", + "descriptionFullComplete": "Zdobyłeś 1000 punktów w trybie ${LEVEL}", + "name": "Czarodziej trybu ${LEVEL}" + }, + "Precision Bombing": { + "description": "Wygraj bez używania bonusów", + "descriptionComplete": "Wygrałeś bez używania bonusów", + "descriptionFull": "Wygraj w trybie ${LEVEL} bez pomocy bonusów", + "descriptionFullComplete": "Wygrałeś w trybie ${LEVEL} bez pomocy bomb", + "name": "Precyzyjne Bombardowanie" + }, + "Pro Boxer": { + "description": "Wygraj bez używania bomb", + "descriptionComplete": "Wygrałeś bez używania bomb", + "descriptionFull": "Ukończ tryb ${LEVEL} bez używania bomb", + "descriptionFullComplete": "Ukończyłeś tryb ${LEVEL} bez używania bomb", + "name": "Zawodowy Bokser" + }, + "Pro Football Shutout": { + "description": "Wygraj nie pozwalając zapunktować złym gościom", + "descriptionComplete": "Wygrałeś nie pozwalając zapunktować złym gościom", + "descriptionFull": "Wygraj w trybie ${LEVEL} nie pozwalając zapunktować złym gościom", + "descriptionFullComplete": "Wygrałeś w trybie ${LEVEL} nie pozwalając zapunktować złym gościom", + "name": "Zamurowanie bramki w trybie ${LEVEL}" + }, + "Pro Football Victory": { + "description": "Wygraj mecz", + "descriptionComplete": "Wygrałeś mecz", + "descriptionFull": "Wygraj mecz w trybie ${LEVEL}", + "descriptionFullComplete": "Wygrałeś mecz w trybie ${LEVEL}", + "name": "Zwycięstwo w trybie ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Pokonaj wszystkie fale", + "descriptionComplete": "Pokonałeś wszystkie fale", + "descriptionFull": "Pokonaj wszystkie fale w trybie ${LEVEL}", + "descriptionFullComplete": "Pokonałeś wszystkie fale w trybie ${LEVEL}", + "name": "Zwycięstwo w trybie ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Przetrwaj wszystkie fale", + "descriptionComplete": "Ukończyłeś wszystkie fale", + "descriptionFull": "Przetrwaj wszystkie fale w trybie ${LEVEL}", + "descriptionFullComplete": "Ukończyłeś wszystkie fale w trybie ${LEVEL}", + "name": "Zwycięstwo w trybie ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Wygraj nie pozwalając zapunktować wrogom", + "descriptionComplete": "Wygrałeś nie pozwalając zapunktować wrogom", + "descriptionFull": "Wygraj w trybie ${LEVEL} nie pozwalając zapunktować złym gościom", + "descriptionFullComplete": "Wygrałeś w trybie ${LEVEL} nie pozwalając zapunktować wrogom", + "name": "Zamurowanie bramki w trybie ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Wygraj mecz", + "descriptionComplete": "Wygrałeś mecz", + "descriptionFull": "Wygraj mecz w trybie ${LEVEL}", + "descriptionFullComplete": "Wygrałeś mecz w trybie ${LEVEL}", + "name": "Zwycięstwo w trybie ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Pokonaj wszystkie fale", + "descriptionComplete": "Pokonałeś wszystkie fale", + "descriptionFull": "Pokonaj wszystkie fale w trybie ${LEVEL}", + "descriptionFullComplete": "Pokonałeś wszystkie fale w trybie ${LEVEL}", + "name": "Zwycięstwo w trybie ${LEVEL}" + }, + "Runaround God": { + "description": "Zdobądź 2000 punktów", + "descriptionComplete": "Zdobyłeś 2000 punktów", + "descriptionFull": "Zdobądź 2000 punktów w trybie ${LEVEL}", + "descriptionFullComplete": "Zdobyłeś 2000 punktów w trybie ${LEVEL}", + "name": "Bóg trybu ${LEVEL}" + }, + "Runaround Master": { + "description": "Zdobądź 500 punktów", + "descriptionComplete": "Zdobyłeś 500 punktów", + "descriptionFull": "Zdobądź 500 punktów w trybie ${LEVEL}", + "descriptionFullComplete": "Zdobyłeś 500 punktów w trybie ${LEVEL}", + "name": "Mistrz trybu ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Zdobądź 1000 punktów", + "descriptionComplete": "Zdobyłeś 1000 punktów", + "descriptionFull": "Zdobądź 1000 punktów w trybie ${LEVEL}", + "descriptionFullComplete": "Zdobyłeś 1000 punktów w trybie ${LEVEL}", + "name": "Czarodziej trybu ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Poleć grę kumplowi", + "descriptionFullComplete": "Polecono grę kumplowi", + "name": "Troska o dzielenie" + }, + "Stayin' Alive": { + "description": "Wygraj bez umierania", + "descriptionComplete": "Wygrałeś bez umierania", + "descriptionFull": "Wygraj w trybie ${LEVEL} bez umierania", + "descriptionFullComplete": "Wygrałeś w trybie ${LEVEL} bez umierania", + "name": "Pozostań przy życiu" + }, + "Super Mega Punch": { + "description": "Zadaj jednym ciosem 100% obrażeń", + "descriptionComplete": "Zadałeś jednym ciosem 100% obrażeń", + "descriptionFull": "Zadaj jednym ciosem 100% obrażeń w trybie ${LEVEL}", + "descriptionFullComplete": "Zadałeś jednym ciosem 100% obrażeń w trybie ${LEVEL}", + "name": "Super Mega Cios" + }, + "Super Punch": { + "description": "Zadaj jednym ciosem 50% obrażeń", + "descriptionComplete": "Zadałeś jednym ciosem 50% obrażeń", + "descriptionFull": "Zadaj jednym ciosem 50% obrażeń w trybie ${LEVEL}", + "descriptionFullComplete": "Zadałeś jednym ciosem 50% obrażeń w trybie ${LEVEL}", + "name": "Super Cios" + }, + "TNT Terror": { + "description": "Zabij 6 złych gości używając TNT", + "descriptionComplete": "Zabiłeś 6 złych gości używając TNT", + "descriptionFull": "Zabij 6 złych gości używając TNT w trybie ${LEVEL}", + "descriptionFullComplete": "Zabiłeś 6 złych gości używając TNT w trybie ${LEVEL}", + "name": "Terror TNT" + }, + "Team Player": { + "descriptionFull": "Rozpocznij grę drużynową z czwórką lub większą ilością graczy", + "descriptionFullComplete": "Rozpoczęto grę drużynową z czwórką lub większą ilością graczy", + "name": "Gracz drużynowy" + }, + "The Great Wall": { + "description": "Zatrzymaj każdego złego gościa", + "descriptionComplete": "Zatrzymałeś każdego złego gościa", + "descriptionFull": "Zatrzymaj każdego złego gościa w trybie ${LEVEL}", + "descriptionFullComplete": "Zatrzymałeś każdego złego gościa w trybie ${LEVEL}", + "name": "Wielki Mur" + }, + "The Wall": { + "description": "Zatrzymaj każdego złego gościa", + "descriptionComplete": "Zatrzymałeś każdego złego gościa", + "descriptionFull": "Zatrzymaj każdego złego gościa w trybie ${LEVEL}", + "descriptionFullComplete": "Zatrzymałeś każdego złego gościa w trybie ${LEVEL}", + "name": "Ściana" + }, + "Uber Football Shutout": { + "description": "Wygraj nie pozwalając zapunktować wrogom", + "descriptionComplete": "Wygrałeś nie pozwalając zapunktować wrogom", + "descriptionFull": "Wygraj w trybie ${LEVEL} nie pozwalając zapunktować wrogom", + "descriptionFullComplete": "Wygrałeś w trybie ${LEVEL} nie pozwalając zapunktować wrogom", + "name": "Zamurowanie bramki w trybie ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Wygraj mecz", + "descriptionComplete": "Wygrałeś mecz", + "descriptionFull": "Wygraj mecz w trybie ${LEVEL}", + "descriptionFullComplete": "Wygrałeś mecz w trybie ${LEVEL}", + "name": "Zwycięstwo w trybie ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Pokonaj wszystkie fale", + "descriptionComplete": "Pokonałeś wszystkie fale", + "descriptionFull": "Pokonaj wszystkie fale w trybie ${LEVEL}", + "descriptionFullComplete": "Pokonałeś wszystkie fale w trybie ${LEVEL}", + "name": "Zwycięstwo w trybie ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Przetrwaj wszystkie fale", + "descriptionComplete": "Przetrwałeś wszystkie fale", + "descriptionFull": "Przetrwaj wszystkie fale w trybie ${LEVEL}", + "descriptionFullComplete": "Przetrwałeś wszystkie fale w trybie ${LEVEL}", + "name": "Zwycięstwo w ${LEVEL}" + } + }, + "achievementsRemainingText": "Pozostałe Osiągnięcia:", + "achievementsText": "Osiągnięcia", + "achievementsUnavailableForOldSeasonsText": "Wybacz, lecz szczegóły osiągnięć nie są dostępne dla starych sezonów.", + "addGameWindow": { + "getMoreGamesText": "Więcej rozgrywek...", + "titleText": "Dodaj grę" + }, + "allowText": "Zezwól", + "alreadySignedInText": "Twoje konto jest zalogowane z innego urządzenia;\nproszę zmienić konta lub zamknąć grę na innych\nurządzeniach i spróbować ponownie.", + "apiVersionErrorText": "Nie mogę załadować modułu ${NAME}; wersja używana - ${VERSION_USED}; wymagana - ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" aktywuj tylko wtedy, gdy są podłączone słuchawki)", + "headRelativeVRAudioText": "Head-Relative VR Audio", + "musicVolumeText": "Głośność muzyki", + "soundVolumeText": "Głośność dźwięku", + "soundtrackButtonText": "Ścieżki dźwiękowe", + "soundtrackDescriptionText": "(przypisz własne utwory aby je odtwarzać podczas rozgrywki)", + "titleText": "Audio" + }, + "autoText": "Auto", + "backText": "Wróć", + "banThisPlayerText": "Zbanuj tego gracza", + "bestOfFinalText": "Wyniki Najlepszych-z-${COUNT} Finału", + "bestOfSeriesText": "Wyniki Najlepszych z ${COUNT} serii:", + "bestOfUseFirstToInstead": 0, + "bestRankText": "Najlepsza pozycja w rankingu: ${RANK}", + "bestRatingText": "Twoja najlepsza pozycja w generalnej klasyfikacji: ${RATING}", + "betaErrorText": "Ta wersja beta jest nieaktywna; sprawdź czy istnieje nowa wersja.", + "betaValidateErrorText": "Nie można zweryfikować wersji beta. (brak połączenia z internetem?)", + "betaValidatedText": "Wersja Beta zatwierdzona; Dobrej zabawy!", + "bombBoldText": "BOMBA", + "bombText": "Bomba", + "boostText": "Dopalacz", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} jest konfigurowany w samej aplikacji.", + "buttonText": "Przycisk", + "canWeDebugText": "Chcesz aby BombSquad automatycznie raportował błędy,\nawarie i podstawowe informacje o użytkowaniu deweloperowi?\n\nPrzesyłane dane nie będą zawierać Twoich osobistych danych,\na pomogą jedynie poprawić działanie gry i usunąć jej błędy.", + "cancelText": "Anuluj", + "cantConfigureDeviceText": "Wybacz ale ${DEVICE} nie jest konfigurowalne.", + "challengeEndedText": "To wyzwanie zostało zakończone.", + "chatMuteText": "Wycisz Czat", + "chatMutedText": "Czat Wyciszony", + "chatUnMuteText": "Podgłośnij Czat", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Musisz ukończyć ten\netap aby kontynuować!", + "completionBonusText": "Bonusowe zakończenie", + "configControllersWindow": { + "configureControllersText": "Konfiguracja Kontrolerów", + "configureGamepadsText": "Skonfiguruj gamepady", + "configureKeyboard2Text": "Skonfiguruj klawiaturę P2", + "configureKeyboardText": "Skonfiguruj klawiaturę P1", + "configureMobileText": "Urządzenia mobilne jako Kontrolery", + "configureTouchText": "Skonfiguruj ekran dotykowy", + "ps3Text": "Kontrolery PS3", + "titleText": "Kontrolery", + "wiimotesText": "Kontrolery Wiimote", + "xbox360Text": "Kontrolery Xbox360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Uwaga: wsparcie kontrolera uzależnione jest od urządzenia i wersji Androida.", + "pressAnyButtonText": "Naciśnij dowolny przycisk kontrolera\njeśli chcesz go skonfigurować...", + "titleText": "Skonfiguruj Kontrolery" + }, + "configGamepadWindow": { + "advancedText": "Zaawansowane", + "advancedTitleText": "Zaawansowane ustawienia Kontrolera", + "analogStickDeadZoneDescriptionText": "(włącz jeśli Twoja postać dryfuje po zwolnieniu drążka)", + "analogStickDeadZoneText": "Martwa strefa analogowego drążka", + "appliesToAllText": "(zastosuj dla wszystkich kontrolerów tego typu)", + "autoRecalibrateDescriptionText": "(aktywuj jeśli Twoja postać nie porusza się z pełną szybkością)", + "autoRecalibrateText": "Auto kalibracja drążka analogowego", + "axisText": "oś", + "clearText": "wyczyść", + "dpadText": "dpad", + "extraStartButtonText": "Dodatkowy przycisk Start", + "ifNothingHappensTryAnalogText": "Jeśli nic się nie dzieje, spróbuj przypisać zamiast drążka analogowego.", + "ifNothingHappensTryDpadText": "Jeśli nic się nie dzieje, spróbuj przypisać zamiast d-pada.", + "ignoreCompletelyDescriptionText": ".", + "ignoreCompletelyText": "Ignoruj całkowicie", + "ignoredButton1Text": "Pomijany przycisk 1", + "ignoredButton2Text": "Pomijany przycisk 2", + "ignoredButton3Text": "Pomijany przycisk 3", + "ignoredButton4Text": "Pomijany Przycisk 4", + "ignoredButtonDescriptionText": "(użyj tego aby zapobiec wpływaniu przycisków 'home' lub 'sync' na UI)", + "ignoredButtonText": "Przycisk ignorowania", + "pressAnyAnalogTriggerText": "Naciśnij dowolny analogowy spust...", + "pressAnyButtonOrDpadText": "Naciśnij dowolny przycisk lub dpad...", + "pressAnyButtonText": "Naciśnij dowolny przycisk...", + "pressLeftRightText": "Naciśnij lewo lub prawo...", + "pressUpDownText": "Naciśnij w górę lub w dół...", + "runButton1Text": "Uruchom przycisk 1", + "runButton2Text": "Uruchom przycisk 2", + "runTrigger1Text": "Uruchom spust 1", + "runTrigger2Text": "Uruchom spust 2", + "runTriggerDescriptionText": "(analogowe triggery pozwalają na uruchomione przy różnych prędkościach)", + "secondHalfText": "Użyj aby skonfigurować drugiego kontrolera,\nktóry widoczny jest jako pierwszy będąc\npodłączonym do tego samego urządzenia.", + "secondaryEnableText": "Aktywuj", + "secondaryText": "Drugi Kontroler", + "startButtonActivatesDefaultDescriptionText": "(wyłącz jeśli przycisk 'start' używany jest jako przycisk 'menu')", + "startButtonActivatesDefaultText": "Przycisk Start aktywuje domyślny widget", + "titleText": "Ustawienia Kontrolera", + "twoInOneSetupText": "Ustawienia kontrolerów 2-w-1", + "uiOnlyDescriptionText": "(zapobiegnij temu kontrolerowi dołączenia do gry)", + "uiOnlyText": "Limit używania Menu", + "unassignedButtonsRunText": "Uruchamianie wszystkich nieprzypisanych przycisków", + "unsetText": "", + "vrReorientButtonText": "Przycisk resetu orientacji VR" + }, + "configKeyboardWindow": { + "configuringText": "Konfiguracja: ${DEVICE}", + "keyboard2NoteText": "Uwaga: większość klawiatur pozwala na jednoczesne naciśnięcie\ntylko kilku klawiszy. Lepszym rozwiązaniem będzie podłączenie\ndodatkowej klawiatury. Pamietać należy o tym, że w obydwu\nprzypadkach trzeba przypisać klawisze dla obydwu graczy." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Skala przycisków akcji", + "actionsText": "Akcje", + "buttonsText": "przyciski", + "dragControlsText": "< przeciągnij kontrolki aby zmienić ich położenie >", + "joystickText": "joystick", + "movementControlScaleText": "Skala przycisków poruszania się", + "movementText": "Przesunięcie", + "resetText": "Resetuj", + "swipeControlsHiddenText": "Ukryj ikony przycisków", + "swipeInfoText": "Kontrolowanie poprzez styl 'Swipe' wymaga przyzwyczajenia\nlecz łatwiej się wówczas gra nie zwracając uwagi na kontrolki.", + "swipeText": "swipe", + "titleText": "Skonfiguruj ekran dotykowy", + "touchControlsScaleText": "Skala przycisków dotykowych" + }, + "configureItNowText": "Skonfigurować teraz?", + "configureText": "Skonfiguruj", + "connectMobileDevicesWindow": { + "amazonText": "Sklep Amazon", + "appStoreText": "Sklep z aplikacjami", + "bestResultsText": "Dla lepszych efektów stwórz szybką sieć bezprzewodową.\nMożesz zredukować opóźnienia w grze poprzez: wyłączenie innych\nurządzeń korzystających w czasie gry z sieci wifi, będąc\nodpowiednio blisko routera wifi lub podpięcie się do hosta\nbezpośrednio przewodem sieciowym.", + "explanationText": "Aby użyć smartfona lub tableta jako kontrolera w grze, zainstaluj w nich \naplikację ${REMOTE_APP_NAME}. Do gry ${APP_NAME} można przyłączyć\n dowolną ilość urządzeń poprzez sieć WiFi i to całkowicie za darmo!", + "forAndroidText": "dla Androida:", + "forIOSText": "dla iOS:", + "getItForText": "Pobierz ${REMOTE_APP_NAME} dla systemu iOS ze sklepu Apple, a \ndla systemu Android ze sklepu Google Play lub Amazon Appstore.", + "googlePlayText": "Google Play", + "titleText": "Używanie urządzeń mobilnych jako kontrolerów:" + }, + "continuePurchaseText": "Kontynuować za ${PRICE}?", + "continueText": "Kontynuuj", + "controlsText": "Przyciski", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Powyższe nie ma zastosowania do rankingu wszechczasów.", + "activenessInfoText": "Ten mnożnik wzrasta w dniach, kiedy grasz\ni spada w dni, kiedy nie grasz.", + "activityText": "Aktywność", + "campaignText": "Kampania", + "challengesInfoText": "Zdobywaj nagrody za wykonywanie mini-gier.\n\nNagrody i poziomy trudności wzrastają za każdym razem kiedy wyzwanie jest\nukończone i \nzmniejszają kiedy wygasa bądź jest umorzone", + "challengesText": "Wyzwania", + "currentBestText": "Obecnie Najlepszy", + "customText": "Własne", + "entryFeeText": "Wpis", + "forfeitConfirmText": "Umorzyć to wyzwanie?", + "forfeitNotAllowedYetText": "To wyzwanie nie może być jeszcze umorzone.", + "forfeitText": "Umorzyć", + "multipliersText": "Mnożniki", + "nextChallengeText": "Następne wyzwanie", + "nextPlayText": "Następna gra", + "ofTotalTimeText": "z ${TOTAL}", + "playNowText": "Zagraj teraz", + "pointsText": "Punkty", + "powerRankingFinishedSeasonUnrankedText": "(zakończony sezon,poza rankingiem)", + "powerRankingNotInTopText": "(nie jesteś na liście top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} pkt", + "powerRankingPointsMultText": "(x ${NUMBER} pkt)", + "powerRankingPointsText": "${NUMBER} pkt", + "powerRankingPointsToRankedText": "(${CURRENT} z ${REMAINING} pkt)", + "powerRankingText": "Osiągnięcia", + "prizesText": "Nagrody", + "proMultInfoText": "Gracze z aktualizacją ${PRO} otrzymują\n${PERCENT}% punktów więcej.", + "seeMoreText": "Więcej...", + "skipWaitText": "Pomiń oczekiwanie", + "timeRemainingText": "Pozostały czas", + "titleText": "Kooperacja", + "toRankedText": "Do awansu", + "totalText": "Suma", + "tournamentInfoText": "Graj o wysokie wyniki z innymi graczami z twojej ligi.\n\nNagrody dostają gracze\nz najlepszymi wynikami\nkiedy zawody się kończą.", + "welcome1Text": "Witaj w ${LEAGUE}. Możesz podnieść swój ligowy\nranking zdobywając gwiazdki, kompletując osiągnięcia\ni wygrywając trofea w turniejach.", + "welcome2Text": "Możesz również zdobywać kupony z wielu tych samych działań.\nKupony mogą zostać użyte do: odblokowywania nowych postaci,\nmap, mini-gierek, uczestniczenia w turniejach i innych.", + "yourPowerRankingText": "Twoje miejsce:" + }, + "copyOfText": "${NAME} - kopia", + "createAPlayerProfileText": "Utworzyć profil gracza?", + "createEditPlayerText": "", + "createText": "Utwórz", + "creditsWindow": { + "additionalAudioArtIdeasText": "Dodatkowe udźwiękowienie, wczesna szata graficzna i pomysł - ${NAME}", + "additionalMusicFromText": "Dodatkowa muzyka - ${NAME}", + "allMyFamilyText": "Całej mojej rodzinie i wszystkim znajomym, którzy graniem pomogli w testach", + "codingGraphicsAudioText": "Kodowanie, grafika i udźwiękowienie - ${NAME}", + "languageTranslationsText": "Tłumaczenia na inne języki:", + "legalText": "Prawa autorskie:", + "publicDomainMusicViaText": "Podkład muzyczny - ${NAME}", + "softwareBasedOnText": "To oprogramowanie jest częściowo oparte na pracy ${NAME}", + "songCreditText": "${TITLE} wykonywana przez ${PERFORMER}.\nSkomponowana przez ${COMPOSER}. Zorganizowana przez ${ARRANGER}.\nOpublikowana przez ${PUBLISHER}.\nDzięki uprzejmości ${SOURCE}", + "soundAndMusicText": "Dźwięk i muzyka:", + "soundsText": "Dźwięki (${SOURCE}):", + "specialThanksText": "Specjalne podziękowania dla:", + "thanksEspeciallyToText": "Szczególne podziękowania dla ${NAME}", + "titleText": "Informacje o ${APP_NAME}", + "whoeverInventedCoffeeText": "Temu kto wymyślił kawę ;)" + }, + "currentStandingText": "Twoja obecna pozycja w rankingu: #${RANK}", + "customizeText": "Własne...", + "deathsTallyText": "${COUNT} zgonów", + "deathsText": "Zgonów", + "debugText": "debuguj", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Uwaga: zalecane jest ustawienie 'Ustawienia->Grafika->Tekstury' na 'Wysokie' aby wykonać test.", + "runCPUBenchmarkText": "Benchmark Procesora", + "runGPUBenchmarkText": "Benchmark Grafiki", + "runMediaReloadBenchmarkText": "Wykonaj Benchmark Media-Reload", + "runStressTestText": "Przeprowadź test wydajności", + "stressTestPlayerCountText": "Liczba graczy", + "stressTestPlaylistDescriptionText": "Lista testów maks. wydajności", + "stressTestPlaylistNameText": "Nazwa listy", + "stressTestPlaylistTypeText": "Typ listy", + "stressTestRoundDurationText": "Czas trwania rundy", + "stressTestTitleText": "Test wysokiej wydajności", + "titleText": "Benchmarki & testy wydajności", + "totalReloadTimeText": "Ogólny czas przeładowania: ${TIME} (szczegóły w pliku log)", + "unlockCoopText": "Odblokuj poziomy trybu Kooperacji" + }, + "defaultFreeForAllGameListNameText": "Domyślna rozgrywka Free-for-All", + "defaultGameListNameText": "Domyślna lista rozgrywek trybu ${PLAYMODE}", + "defaultNewFreeForAllGameListNameText": "Moja nowa rozgrywka Free-for-All", + "defaultNewGameListNameText": "Moja lista rozgrywek trybu ${PLAYMODE}", + "defaultNewTeamGameListNameText": "Moja nowa rozgrywka zespołowa", + "defaultTeamGameListNameText": "Domyślna rozgrywka zespołowa", + "deleteText": "Usuń", + "demoText": "Demo", + "denyText": "Odmów", + "desktopResText": "Rozdzielczość ekranu", + "difficultyEasyText": "Łatwy", + "difficultyHardOnlyText": "Tylko w trudnym trybie", + "difficultyHardText": "Trudny", + "difficultyHardUnlockOnlyText": "Ten poziom może zostać odblokowany tylko w trudnym trybie.\nCzy uważasz, że posiadasz to czego wymaga?!", + "directBrowserToURLText": "Proszę, otwórz przeglądarkę na podanym adresie:", + "disableRemoteAppConnectionsText": "Wyłącz łączenia aplikacji BS-Remote", + "disableXInputDescriptionText": "Pozwala na podłączenie 4 kontrolerów, ale może nie działać.", + "disableXInputText": "Wyłącz XInput", + "doneText": "Gotowe", + "drawText": "Remis", + "duplicateText": "Duplikuj", + "editGameListWindow": { + "addGameText": "Dodaj\ngrę", + "cantOverwriteDefaultText": "Nie można nadpisać domyślnej listy rozgrywek!", + "cantSaveAlreadyExistsText": "Lista rozgrywek z taką nazwą już istnieje!", + "cantSaveEmptyListText": "Nie można zapisać pustej listy rozgrywek!", + "editGameText": "Edytuj\ngrę", + "gameListText": "Lista rozgrywek", + "listNameText": "Nazwa listy rozgrywek", + "nameText": "Nazwa", + "removeGameText": "Usuń\ngrę", + "saveText": "Zapisz listę", + "titleText": "Edytor list rozgrywek" + }, + "editProfileWindow": { + "accountProfileInfoText": "To jest specjalny profil z nazwą i ikonką\nz konta, na które jesteś zalogowany.\n\n${ICONS}\n\nStwórz swój profil, jeśli chcesz użyć innych nazw i ikonek.\n:)", + "accountProfileText": "(nazwa profilu)", + "availableText": "Imię \"${NAME}\" jest dostępne!", + "changesNotAffectText": "Uwaga: zmiany nie będą miały wpływu na postacie będące w grze", + "characterText": "postać", + "checkingAvailabilityText": "Sprawdzanie dostępności imienia \"${NAME}\"...", + "colorText": "kolor 1", + "getMoreCharactersText": "Zdobądź więcej postaci...", + "getMoreIconsText": "Zdobądź więcej ikonek...", + "globalProfileInfoText": "Profilowi globalnemu możesz wybrać unikalną nazwę.\nMogą także mieć ikonkę.", + "globalProfileText": "(Profil globalny)", + "highlightText": "kolor 2", + "iconText": "Ikonka", + "localProfileInfoText": "Lokalny profil gracza nie możemieć ikonki i nie ma\n gwarancji, że takiej nazwy jeszcze nie ma.\nUlepsz do profilu globalnego aby stworzyć unikalną nazwę gracza i dodać ikonkę.", + "localProfileText": "(lokalny profil)", + "nameDescriptionText": "Nazwa gracza", + "nameText": "Nazwa", + "randomText": "losuj", + "titleEditText": "Edytuj profil", + "titleNewText": "Nowy profil", + "unavailableText": "\"${NAME}\" jest zajęta, spróbuj innej.", + "upgradeProfileInfoText": "To zarezerwuje nazwę gracza tylko dla\nciebie i pozwoli ci dodać do niego ikonkę.", + "upgradeToGlobalProfileText": "Ulepsz do Profilu Globalnego" + }, + "editProfilesAnyTimeText": "(możesz edytować profile w każdej chwili przechodząc do 'ustawień')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Nie możesz usunąć domyślnej ścieżki dźwiękowej.", + "cantEditDefaultText": "Nie można edytować domyślnej ścieżki dźwiękowej. Powiel ją lub utwórz nową.", + "cantEditWhileConnectedOrInReplayText": "Nie można edytować ścieżek dźwiękowych podczas imprezy lub oglądania powtórki.", + "cantOverwriteDefaultText": "Nie można nadpisać domyślnej ścieżki dźwiękowej", + "cantSaveAlreadyExistsText": "Ścieżka dźwiękowa o takiej nazwie już istnieje!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Domyślna ścieżka dźwiękowa", + "deleteConfirmText": "Usunąć ścieżkę dźwiękową:\n\n'${NAME}'?", + "deleteText": "Usuń\nścieżkę", + "duplicateText": "Powiel\nścieżkę", + "editSoundtrackText": "Edytor ścieżek dźwiękowych", + "editText": "Edytuj\nścieżkę dźwiękową", + "fetchingITunesText": "pobieranie listy z aplikacji muzycznej...", + "musicVolumeZeroWarning": "Ostrzeżenie: głośność muzyki ustawiona na 0", + "nameText": "Nazwa", + "newSoundtrackNameText": "Moja ${COUNT} ścieżka dźwiękowa", + "newSoundtrackText": "Nowa ścieżka dźwiękowa:", + "newText": "Nowa\nścieżka", + "selectAPlaylistText": "Wybierz listę", + "selectASourceText": "Wybierz źródło muzyki", + "soundtrackText": "Ścieżka dźwiękowa", + "testText": "test", + "titleText": "Ścieżki dźwiękowe", + "useDefaultGameMusicText": "Domyślna muzyka w grze", + "useITunesPlaylistText": "Lista aplikacji muzycznej", + "useMusicFileText": "Plik muzyczny (mp3, itp.)", + "useMusicFolderText": "Katalog plików muzycznych" + }, + "editText": "Edytuj", + "endText": "Koniec", + "enjoyText": "Miłej zabawy!", + "epicDescriptionFilterText": "${DESCRIPTION} Epickie zwolnione tempo.", + "epicNameFilterText": "Epicki tryb - ${NAME}", + "errorAccessDeniedText": "dostęp zabroniony", + "errorOutOfDiskSpaceText": "brak miejsca na dysku", + "errorText": "Błąd", + "errorUnknownText": "nieznany błąd", + "exitGameText": "Wyjść z ${APP_NAME}?", + "exportSuccessText": "'${NAME}' eksportowane.", + "externalStorageText": "Pamięć zewnętrzna", + "failText": "Niepowodzenie", + "fatalErrorText": "O nie, czegoś brakuje lub jest uszkodzone.\nSpróbuj przeinstalować grę lub skontaktuj się \npoprzez ${EMAIL} dla uzyskania pomocy.", + "fileSelectorWindow": { + "titleFileFolderText": "Wybierz plik lub katalog", + "titleFileText": "Wybierz plik", + "titleFolderText": "Wybierz katalog", + "useThisFolderButtonText": "Użyj tego katalogu" + }, + "filterText": "Filtr", + "finalScoreText": "Wynik końcowy", + "finalScoresText": "Wyniki końcowe", + "finalTimeText": "Ostateczny czas", + "finishingInstallText": "Kończenie instalacji; chwilka...", + "fireTVRemoteWarningText": "* Dla lepszych wrażeń, użyj\nkontrolerów do gier lub zainstaluj\naplikację ${REMOTE_APP_NAME} na\nTwoich telefonach lub tabletach.", + "firstToFinalText": "Wyniki Pierwsze-z-${COUNT}", + "firstToSeriesText": "Seria Pierwsza-z-${COUNT}", + "fiveKillText": "PIĘCIU ZABITYCH!!!", + "flawlessWaveText": "Fala Bez Skazy!", + "fourKillText": "CZWORO ZABITYCH!!!", + "freeForAllText": "Free-for-All", + "friendScoresUnavailableText": "Wyniki znajomego niedostępne.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Liderzy ${COUNT} rozgrywki", + "gameListWindow": { + "cantDeleteDefaultText": "Nie możesz usunąć domyślnej listy rozgrywek!", + "cantEditDefaultText": "Nie można edytować domyślnej listy rozgrywek! Powiel ją lub utwórz nową.", + "cantShareDefaultText": "Nie możesz udostępnić domyślnej listy rozgrywek.", + "deleteConfirmText": "Usunąć \"${LIST}\"?", + "deleteText": "Usuń\nlistę", + "duplicateText": "Powiel\nlistę", + "editText": "Edytuj\nlistę", + "gameListText": "Lista rozgrywek", + "newText": "Nowa\nlista", + "showTutorialText": "Pokaż samouczek po grze", + "shuffleGameOrderText": "Losowa kolejność rozgrywek", + "titleText": "Własne listy rozgrywek trybu ${TYPE}" + }, + "gameSettingsWindow": { + "addGameText": "Dodaj grę" + }, + "gamepadDetectedText": "Wykryto 1 gamepad.", + "gamepadsDetectedText": "Wykryto ${COUNT} gamepadów.", + "gamesToText": "${WINCOUNT} do ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Pamiętaj: każde urządzenie podczas imprezy może posiadać\nwięcej niż jednego gracza jeśli masz więcej kontrolerów.", + "aboutDescriptionText": "Używaj tych zakładek aby zorganizować imprezę.\n\nImprezy pozwalają na organizowanie rozgrywek i\nturniejów ze znajomymi wykorzystując różne urządzenia.\n\nUżyj przycisku ${PARTY} w prawym górnym rogu aby\nrozmawiać i współdziałać podczas imprezy.\n(na kontrolerze wciśnij ${BUTTON} gdy jesteś w menu)", + "aboutText": "Info", + "addressFetchErrorText": "", + "appInviteInfoText": "Zaproś przyjaciół by wypróbowali BombSquada, a dostaną \n${COUNT} darmowych kuponów. Dostaniesz ${YOU_COUNT}\nza każdego ktory wypróbuje.", + "appInviteMessageText": "${NAME} wysłał ci ${COUNT} kuponów w ${APP_NAME}", + "appInviteSendACodeText": "Wyślij im kod", + "appInviteTitleText": "Zaproszenie do gry ${APP_NAME}", + "bluetoothAndroidSupportText": "(działa z każdym urządzeniem na Androidzie wyposażonym w Bluetooth'a)", + "bluetoothDescriptionText": "Utwórz/dołącz do imprezy wykorzystując Bluetooth'a:", + "bluetoothHostText": "Utwórz poprzez BT", + "bluetoothJoinText": "Dołącz poprzez BT", + "bluetoothText": "Bluetooth", + "checkingText": "sprawdzam...", + "copyCodeConfirmText": "Kod skopiowany do schowka.", + "copyCodeText": "Skopiuj kod", + "dedicatedServerInfoText": "Dla najlepszych wyników ustaw serwer dedykowany. Zobacz jak na bombsquadgame.com/server.", + "disconnectClientsText": "Spowoduje to rozłączenie ${COUNT} graczy\nbędących na imprezie. Jesteś pewny?", + "earnTicketsForRecommendingAmountText": "Znajomi dostaną ${COUNT} kuponów jeżeli wypróbują grę\n(a Ty dostaniesz ${YOU_COUNT} za każdego kto zagra.)", + "earnTicketsForRecommendingText": "Poleć grę dla darmowych\n kuponów...", + "emailItText": "Prześlij to", + "favoritesSaveText": "Zapisz jako ulubione", + "favoritesText": "Ulubione", + "freeCloudServerAvailableMinutesText": "Następny darmowy serwer w chmurze będzie dostępny za ${MINUTES} minut.", + "freeCloudServerAvailableNowText": "Darmowy serwer w chmurze jest dostępny!", + "freeCloudServerNotAvailableText": "Aktualnie nie ma dostępnego żadnego darmowego serwera w chmurze.", + "friendHasSentPromoCodeText": "${COUNT} kuponów ${APP_NAME} od ${NAME}", + "friendPromoCodeAwardText": "Dostaniesz ${COUNT} kuponów zawsze gdy tego użyjesz.", + "friendPromoCodeExpireText": "Ten kod wygaśnie po ${EXPIRE_HOURS} godzinach i działa tylko dla nowych graczy.", + "friendPromoCodeInfoText": "Może zostać wykupione do ${COUNT} kuponów.\n\nIdź do \"Ustawienia->Zaawansowane->Wpisz Kod Promocyjny\" w grze aby go użyć. Idź do bombsquadgame.com by pobrać\nlinki do wszystkich wspieranych platform. Ten kod\nwygaśnie w ${EXPIRE_HOURS} godzin i jest prawidłowy tylko dla nowych graczy.", + "friendPromoCodeInstructionsText": "Aby użyć, otwórz ${APP_NAME} i idź do \"Ustawienia->Zaawansowane-> Wpisz kod\".\nWejdź na bombsquadgame.com by zobaczyć linki dla wszystkich dostępnych platform (Android itp.)", + "friendPromoCodeRedeemLongText": "Może być żądane do ${COUNT} darmowych kuponów dla najwięcej ${MAX_USES} ludzi.", + "friendPromoCodeRedeemShortText": "Może być żądane do ${COUNT} kuponów w grze.", + "friendPromoCodeWhereToEnterText": "(W \"Ustawienia->Zaawansowane->Wpisz kod\")", + "getFriendInviteCodeText": "Zdobądź kod promocyjny kumpla", + "googlePlayDescriptionText": "Zaproś użytkowników Google Play na imprezę:", + "googlePlayInviteText": "Zaproś", + "googlePlayReInviteText": "Obecnie jest ${COUNT} graczy Google Play'a na imprezie,\nktórzy zostaną rozłączeni jeśli uruchomisz nowe zaproszenie.\nUwzględnij ich w nowym zaproszeniu, aby mogli powrócić.", + "googlePlaySeeInvitesText": "Zobacz zaproszenia", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Tylko Android / Google Play)", + "hostPublicPartyDescriptionText": "Hostuj imprezę publiczną", + "hostingUnavailableText": "Hostowanie niedostępne", + "inDevelopmentWarningText": "Uwaga:\n\nOpcja gry sieciowej jest nowa i będąca w fazie\nrozwojowej. Od teraz mocno zalecane jest aby\nwszyscy gracze byli w tej samej sieci (Wi-Fi lub LAN).", + "internetText": "Internet", + "inviteAFriendText": "Znajomi nie mają gry?\nZaproś ich do sprawdzenia a oni otrzymają ${COUNT} darmowych kuponów.", + "inviteFriendsText": "Zaproś przyjaciół", + "joinPublicPartyDescriptionText": "Dołącz do Publicznej Imprezy", + "localNetworkDescriptionText": "Dołącz do najbliższej imprezy (LAN, Bluetooth itp.)", + "localNetworkText": "Lokalna Sieć", + "makePartyPrivateText": "Stwórz Prywatną Imprezę", + "makePartyPublicText": "Stwórz Publiczną Imprezę", + "manualAddressText": "Adres", + "manualConnectText": "Połącz", + "manualDescriptionText": "Wbijaj na imprezę przez adres IP:", + "manualJoinSectionText": "Dołącz poprzez adres", + "manualJoinableFromInternetText": "Czy jesteś dostępny z internetu?:", + "manualJoinableNoWithAsteriskText": "NIE*", + "manualJoinableYesText": "TAK", + "manualRouterForwardingText": "*aby to naprawić, spróbuj skonfigurować router aby przepuszczał UDP port ${PORT} na Twój lokalny adres", + "manualText": "Ręczne", + "manualYourAddressFromInternetText": "Twój adres dostępny z internetu:", + "manualYourLocalAddressText": "Twój adres lokalny:", + "nearbyText": "W pobliżu", + "noConnectionText": "", + "otherVersionsText": "(Inne wersje)", + "partyCodeText": "Kod imprezy", + "partyInviteAcceptText": "Akceptuj", + "partyInviteDeclineText": "Ignoruj", + "partyInviteGooglePlayExtraText": "(zobacz zakładkę 'Google Play' w oknie 'Punkt Zbiorny')", + "partyInviteIgnoreText": "Ignoruj", + "partyInviteText": "${NAME} zaprosił Cię abyś\ndołączył do ich imprezy.", + "partyNameText": "Nazwa Imprezy", + "partyServerRunningText": "Twój prywatny serwer jest uruchomiony.", + "partySizeText": "ilość graczy", + "partyStatusCheckingText": "sprawdzanie statusu...", + "partyStatusJoinableText": "twoja impreza jest teraz widoczna przez internet", + "partyStatusNoConnectionText": "nie można się podłączyć", + "partyStatusNotJoinableText": "twoja impreza jest niedostępna przez internet", + "partyStatusNotPublicText": "twoja impreza nie jest publiczna", + "pingText": "ping", + "portText": "Port", + "privatePartyCloudDescriptionText": "Prywatne imprezy działają na dedykowanych serwerach w chmurze; konfiguracja rutera nie jest wymagana.", + "privatePartyHostText": "Hostuj prywatną imprezę", + "privatePartyJoinText": "Dołącz do prywatnej imprezy", + "privateText": "Prywatne", + "publicHostRouterConfigText": "To może wymagać konfiguracji przekierowywania portów twojego rutera. Prostszym rozwiązaniem będzie zahostowanie prywatnej imprezy.", + "publicText": "Publiczne", + "requestingAPromoCodeText": "Żądanie kodu...", + "sendDirectInvitesText": "Ślij bezpośrednie zaproszenia", + "sendThisToAFriendText": "Wyslij ten kod do znajomego:", + "shareThisCodeWithFriendsText": "Podziel się tym kodem z kumplami:", + "showMyAddressText": "Pokaż mój adres", + "startHostingPaidText": "Hostuj teraz za ${COST}", + "startHostingText": "Hostuj", + "startStopHostingMinutesText": "Możesz rozpocząć i zakończyć hostowanie za darmo przez następne ${MINUTES} minut.", + "stopHostingText": "Zakończ hostowanie", + "titleText": "Punkt Zbiorny", + "wifiDirectDescriptionBottomText": "Jeśli wszystkie urządzenia posiadają panel 'Wi-Fi Direct', to powinny użyć go aby\nodnaleźć i połączyć się między sobą. Kiedy wszystkie są już połączone, możesz utworzyć\nimprezę używając zakładki 'Lokalna sieć', tak samo jak w standardowej sieci Wi-Fi.\n\nDla optymalnego działania, host Wi-Fi Direct powinien być hostem zabawy w ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct może być używany do bezpośredniego łączenia urządzeń na\nAndroidzie bez konieczności stosowania sieci Wi-Fi. Najlepiej działa na\nurządzeniach z systemem Android 4.2 lub nowszym.\nAby go użyć, otwórz ustawienia Wi-Fi urządzenia i odszukaj 'Wi-Fi Direct'.", + "wifiDirectOpenWiFiSettingsText": "Otwórz ustawienia Wi-Fi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(działa między wszystkimi platformami)", + "worksWithGooglePlayDevicesText": "(współpracuje z urządzeniami systemu Google Play (Android) tej wersji gry)", + "youHaveBeenSentAPromoCodeText": "Wysłałeś kod promocyjny ${APP_NAME}." + }, + "getCoinsWindow": { + "coinDoublerText": "Podwajacz monety", + "coinsText": "${COUNT} monet", + "freeCoinsText": "Darmowe monety", + "restorePurchasesText": "Odzyskaj zakupy", + "titleText": "Zdobądź monety" + }, + "getTicketsWindow": { + "freeText": "DARMO!", + "freeTicketsText": "Darmowe kupony", + "inProgressText": "Transakcja w toku; proszę spróbować za chwilkę.", + "purchasesRestoredText": "Zakupy przywrócone.", + "receivedTicketsText": "Otrzymano ${COUNT} kuponów!", + "restorePurchasesText": "Przywróć zakupy", + "ticketDoublerText": "Podwójne kupony", + "ticketPack1Text": "Mała paczka kuponów", + "ticketPack2Text": "Średnia paczka kuponów", + "ticketPack3Text": "Duża paczka kuponów", + "ticketPack4Text": "Paczka Kolos kuponów", + "ticketPack5Text": "Mamucia paczka kuponów", + "ticketPack6Text": "Paczka Ultimate kuponów", + "ticketsFromASponsorText": "Zdobądź ${COUNT} kuponów\nod sponsora", + "ticketsText": "${COUNT} kuponów", + "titleText": "Zdobądź kupony", + "unavailableLinkAccountText": "Niestety, zakupy nie są możliwe na tej platformie. \nJeśli chcesz, możesz połączyć to konto z kontem na innej platformie\ni dokonać zakupu tam.", + "unavailableTemporarilyText": "Chwilowo niedostępne; spróbuj później.", + "unavailableText": "Niestety to jest niedostępne.", + "versionTooOldText": "Ta wersja gry jest nieaktualna; spróbuj zaktualizować do nowszej wersji.", + "youHaveShortText": "masz ${COUNT} kuponów", + "youHaveText": "masz ${COUNT} kuponów" + }, + "googleMultiplayerDiscontinuedText": "Przepraszam, usługa gry wieloosobowej Google nie jest już dostępna.\nPracuję nad zamiennikiem tak szybko jak potrafię.\nTymczasem proszę o wypróbowanie innej metody połączenia.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Zawsze", + "fullScreenCmdText": "Pełny ekran (Cmd-F)", + "fullScreenCtrlText": "Pełny ekran (Ctrl+F)", + "gammaText": "Gamma", + "highText": "Wysokie", + "higherText": "Max", + "lowText": "Niskie", + "mediumText": "Średnie", + "neverText": "Nigdy", + "resolutionText": "Rozdzielczość", + "showFPSText": "Pokazuj FPS", + "texturesText": "Tekstury", + "titleText": "Grafika", + "tvBorderText": "Ramka TV", + "verticalSyncText": "Synchronizacja pionowa", + "visualsText": "Wizualizacje" + }, + "helpWindow": { + "bombInfoText": "- Bomba -\nSilniejsza niż ciosy, lecz\nz powodu obrażeń może Cię\nwpędzić do grobu. Dla\nlepszego efektu wyrzuć ją przed\nwypaleniem się lontu.", + "canHelpText": "${APP_NAME} może Ci w tym pomóc", + "controllersInfoText": "Możesz zagrać w ${APP_NAME} ze znajomymi poprzez sieć lub na tym\nsamym urządzeniu jeśli masz wystarczająco dużo kontrolerów.\n${APP_NAME} obsługuje wiele z nich; możesz nawet użyć smartfonów\njako kontrolery wykorzystując aplikację '${REMOTE_APP_NAME}'.\nZobacz Ustawienia->Kontrolery, aby uzyskać szczegółowe informacje", + "controllersInfoTextFantasia": "Gracz może używać zdalnego kontrolera, jednak zalecane są\ngamepady. Możesz także użyć urządzeń mobilnych jako kontrolerów\ngry za pomocą darmowej aplikacji 'BombSquad Remote'.\nSprawdź informacje dostępne w ustawieniach kontrolerów.", + "controllersInfoTextMac": "Jeden lub dwóch graczy może używać klawiatury, jednak najlepiej korzystać z\ngamepadów. Gra obsługuje pady USB, kontrolery PS3, Xbox360, Wiimote i urządzenia\nz systemem iOS/Android. Na pewno coś z tego posiadasz aby sterować postaciami?\nWięcej informacji dostępnych jest w ustawieniach kontrolerów.", + "controllersInfoTextOuya": "Do gry w BombSquad możesz wykorzystać kontrolery OUYA, PS3, Xbox360\ni wiele innych gamepadów podłączanych za pomocą USB lub Bluetootha.\nMożesz również używać jako kontrolery urządzenia z systemami iOS/Android\nz pomocą darmowej aplikacji 'BombSquad Remote'. Więcej informacji w\nustawieniach kontrolerów.", + "controllersText": "Kontrolery", + "controlsSubtitleText": "Twoja postać w ${APP_NAME} posiada kilka podstawowych umiejętności:", + "controlsText": "Przyciski", + "devicesInfoText": "Wersja VR ${APP_NAME} może być używana w rozgrywce sieciowej wraz z\nwersją regularną, więc wbijaj do gry ze swoimi telefonami, tabletami\ni komputerami do rozgrywki. Wersja regularna gry może być również\nwykorzystana do przyłączenia się zainteresowanych do wersji VR aby\npokazać jak wygląda rozgrywka.", + "devicesText": "Urządzenia", + "friendsGoodText": "Dobrze ich mieć. ${APP_NAME} sprawia największą frajdę\nz kilkoma graczami. Gra może obsłużyć do 8 graczy jednocześnie.", + "friendsText": "Znajomych", + "jumpInfoText": "- Skok -\nPodskocz aby pokonać małe luki,\nrzucać wyżej i dalej oraz wyrażać\nswoją radość.", + "orPunchingSomethingText": "chcesz walnąć w coś lub rzucić się z klifu albo przykleić komuś bombkę.", + "pickUpInfoText": "- Podnoszenie -\nPodnieś flagi, wrogów lub cokolwiek\ninnego nie przytwierdzonego do\nziemi. Naciśnij ponownie by rzucić.", + "powerupBombDescriptionText": "Pozwala wyrzucić trzy bomby\njednocześnie zamiast jednej.", + "powerupBombNameText": "Potrójne Bomby", + "powerupCurseDescriptionText": "Lepiej jej unikaj.\nChyba, że chcesz spróbować?", + "powerupCurseNameText": "Klątwa", + "powerupHealthDescriptionText": "Przywraca pełne zdrowie.\nWoah, nie zgadłbyś...", + "powerupHealthNameText": "Apteczka", + "powerupIceBombsDescriptionText": "Słabsze od normalnych bomb lecz\nzamrażają wrogów, którzy stają\nsię wówczas podatni na kruszenie.", + "powerupIceBombsNameText": "Lodowe Bomby", + "powerupImpactBombsDescriptionText": "Nieco słabsze od normalnych bomb,\nale eksplodują zaraz po upadku.", + "powerupImpactBombsNameText": "Bomby Dotykowe", + "powerupLandMinesDescriptionText": "Dostępne w paczkach po 3.\nPrzydatne do obrony bazy lub \nzatrzymania szybkich wrogów.", + "powerupLandMinesNameText": "Miny Lądowe", + "powerupPunchDescriptionText": "Sprawiają, że ciosy są silniejsze,\nszybsze i ogólnie mocniejsze.", + "powerupPunchNameText": "Rękawice Bokserskie", + "powerupShieldDescriptionText": "Pochłania groźne obrażenia,\nwięc jest niezastąpiona.", + "powerupShieldNameText": "Tarcza Energetyczna", + "powerupStickyBombsDescriptionText": "Przyklejają się do wszystkiego\nco dotkną. Niezły ubaw.", + "powerupStickyBombsNameText": "Bomby Przylepne", + "powerupsSubtitleText": "Oczywiście, gra bez bonusów byłaby niekompletna:", + "powerupsText": "Bonusy", + "punchInfoText": "- Cios -\nCiosy robią większe obrażenia\ngdy szybciej boksujesz, więc\nbiegaj i skakaj jak szaleniec!", + "runInfoText": "- Bieganie -\nPrzytrzymaj dowolny przycisk aby biec. Przyciski boczne lub triggery (o ile je masz) działają\nlepiej. Bieganie pozwala na szybsze przemieszczanie lecz trudniej kierować, więc uwaga na klify.", + "someDaysText": "Czasami masz chęć przyłożenia komuś, wysadzenia kogoś w powietrze,", + "titleText": "Pomoc ${APP_NAME}", + "toGetTheMostText": "Aby w pełni korzystać z tej gry, musisz mieć:", + "welcomeText": "Witaj w ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} nawiguje w menu jako szef -", + "importPlaylistCodeInstructionsText": "Użyj tego kodu, by zimportować tą listę w innym miejscu:", + "importPlaylistSuccessText": "Zimportowano playlistę '${NAME}' rozgrywek ${TYPE}", + "importText": "Importuj", + "importingText": "Importowanie...", + "inGameClippedNameText": "w grze widoczne jako\n\"${NAME}\"", + "installDiskSpaceErrorText": "BŁĄD: Niemożliwe dokończenie instalacji.\nByć może mało miejsca w pamięci urządzenia.\nZrób więcej miejsca i spróbuj jeszcze raz.", + "internal": { + "arrowsToExitListText": "wciśnij ${LEFT} lub ${RIGHT} aby opuścić listę", + "buttonText": "przycisk", + "cantKickHostError": "Nie możesz wyrzucić hosta.", + "chatBlockedText": "${NAME} jest zablokowany na czacie na ${TIME} sekund.", + "connectedToGameText": "Dołączono do \"${NAME}\"", + "connectedToPartyText": "${NAME}'s wbił się na imprezę!", + "connectingToPartyText": "Łączenie...", + "connectionFailedHostAlreadyInPartyText": "Połączenie nieudane; host jest na innej imprezie.", + "connectionFailedPartyFullText": "Połączenie nieudane; impreza jest pełna.", + "connectionFailedText": "Połączenie nieudane.", + "connectionFailedVersionMismatchText": "Połączenie nieudane; host pracuje na innej wersji gry.\nUpewnij się, że masz aktualne wersje i spróbuj ponownie.", + "connectionRejectedText": "Połączenie odrzucone.", + "controllerConnectedText": "${CONTROLLER} połączony.", + "controllerDetectedText": "Wykryto 1 kontroler.", + "controllerDisconnectedText": "${CONTROLLER} rozłączony.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} rozłączony. Proszę spróbuj połączyć ponownie.", + "controllerForMenusOnlyText": "Ten kontroler nie może być używany do gry; można nim sterować tylko w menu.", + "controllerReconnectedText": "${CONTROLLER} połączony ponownie.", + "controllersConnectedText": "Podłączono ${COUNT} kontrolerów.", + "controllersDetectedText": "Wykryto ${COUNT} kontrolery/ów.", + "controllersDisconnectedText": "Rozłączono ${COUNT} kontrolerów.", + "corruptFileText": "Wykryto uszkodzone(y) plik(i). Proszę przeinstalować grę lub zgłosić problem na ${EMAIL}", + "errorPlayingMusicText": "Błąd odtwarzania muzyki: ${MUSIC}", + "errorResettingAchievementsText": "Niemożliwe wyczyszczenie osiągnięć online; spróbuj ponownie później.", + "hasMenuControlText": "${NAME} ma kontrolę nad menu", + "incompatibleNewerVersionHostText": "Serwer posiada nowszą wersję gry.\nZaaktualizuj do najnowszej wersji i spróbuj ponownie.", + "incompatibleVersionHostText": "Host pracuje na innej wersji gry.\nUpewnij się, że masz aktualne wersje gry i spróbuj ponownie.", + "incompatibleVersionPlayerText": "${NAME} pracuje na innej wersji gry.\nUpewnij się, że masz aktualne wersje gry i spróbuj ponownie.", + "invalidAddressErrorText": "Błąd: nieprawidłowy adres.", + "invalidNameErrorText": "Błąd: nieprawidłowa nazwa.", + "invalidPortErrorText": "Błąd: nieprawidłowy port.", + "invitationSentText": "Zaproszenie wysłane.", + "invitationsSentText": "${COUNT} wysłanych zaproszeń.", + "joinedPartyInstructionsText": "Znajomy dołączył na imprezę.\nRozpocznij grę w menu 'Graj'.", + "keyboardText": "Klawiatura", + "kickIdlePlayersKickedText": "${NAME} wyrzucony za bezczynność.", + "kickIdlePlayersWarning1Text": "${NAME} zostanie wyrzucony za ${COUNT} sekund jeśli dalej\nbędzie bezczynnie patrzył.", + "kickIdlePlayersWarning2Text": "(możesz wyłączyć opcję w Ustawieniach -> Zaawansowane)", + "leftGameText": "Opuściłeś '${NAME}'.", + "leftPartyText": "${NAME}'s opuścił imprezę.", + "noMusicFilesInFolderText": "Katalog nie zawiera plików muzycznych.", + "playerJoinedPartyText": "${NAME} wbił się na imprezę!", + "playerLeftPartyText": "${NAME} opuścił imprezę.", + "rejectingInviteAlreadyInPartyText": "Odrzucono zaproszenie (już na imprezie).", + "serverRestartingText": "Serwer się restartuje. Dołącz za chwilę...", + "serverShuttingDownText": "Serwer się wyłącza...", + "signInErrorText": "Błąd zapisania się.", + "signInNoConnectionText": "Zapisanie się niemożliwe. (brak połączenia z internetem?)", + "teamNameText": "Zespół ${NAME}", + "telnetAccessDeniedText": "BŁĄD: użytkownikowi nie przyznano dostępu do telnetu.", + "timeOutText": "(czas upłynie za ${TIME} sekund)", + "touchScreenJoinWarningText": "Dołączyłeś się wykorzystując ekran dotykowy.\nJeśli to pomyłka, stuknij 'Menu->Opuść grę'.", + "touchScreenText": "Ekran dotykowy", + "trialText": "trial", + "unableToResolveHostText": "Błąd: nie można odnaleźć hosta.", + "unavailableNoConnectionText": "Obecnie niedostępne (sprawdź połączenie internetowe).", + "vrOrientationResetCardboardText": "Użyj tego aby zresetować orientację VR.\nAby zagrać w grę będziesz potrzebować zewnętrznego kontrolera.", + "vrOrientationResetText": "Reset orientacji VR.", + "willTimeOutText": "(czas upłynie przy bezczynności)" + }, + "jumpBoldText": "SKOK", + "jumpText": "Skok", + "keepText": "Zachowaj", + "keepTheseSettingsText": "Zachować te ustawienia?", + "keyboardChangeInstructionsText": "Wciśnij dwa razy spację, aby zmienić klawiatury.", + "keyboardNoOthersAvailableText": "Brak dostępnych innych klawiatur.", + "keyboardSwitchText": "Zmiana klawiatury na \"${NAME}\".", + "kickOccurredText": "${NAME} został wyrzucony.", + "kickQuestionText": "Wyrzucić ${NAME}?", + "kickText": "Wyrzuć", + "kickVoteCantKickAdminsText": "Nie można wyrzucić adminów.", + "kickVoteCantKickSelfText": "Nie możesz wyrzucić siebie.", + "kickVoteFailedNotEnoughVotersText": "Za mało graczy, by rozpocząć głosowanie.", + "kickVoteFailedText": "Głosowanie nie powiodło się.", + "kickVoteStartedText": "Głosowanie za wyrzuceniem gracza ${NAME}.", + "kickVoteText": "Głosuj za wyrzuceniem", + "kickVotingDisabledText": "Głosowania są wyłączone.", + "kickWithChatText": "Wpisz ${YES} na czacie żeby wyrzucić i ${NO} żeby nie wyrzucać.", + "killsTallyText": "${COUNT} zabitych", + "killsText": "Zabitych", + "kioskWindow": { + "easyText": "Łatwy", + "epicModeText": "Tryb Epicki", + "fullMenuText": "Pełne Menu", + "hardText": "Trudny", + "mediumText": "Średni", + "singlePlayerExamplesText": "Przykłady trybu Pojedyńczego Gracza / Kooperacji", + "versusExamplesText": "Przykłady trybu Versus" + }, + "languageSetText": "Obecny język gry to \"${LANGUAGE}\".", + "lapNumberText": "Okrążenie ${CURRENT}/${TOTAL}", + "lastGamesText": "(ostatnich ${COUNT} rozgrywek)", + "leaderboardsText": "Rankingi", + "league": { + "allTimeText": "Całość", + "currentSeasonText": "Obecny Sezon (${NUMBER})", + "leagueFullText": "${NAME} Liga", + "leagueRankText": "Pozycja w Lidze", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga ${SUFFIX}", + "seasonEndedDaysAgoText": "Zezon zakończył się ${NUMBER} dni temu.", + "seasonEndsDaysText": "Ilość dni do zakończenia sezonu: ${NUMBER}.", + "seasonEndsHoursText": "Do zakończenia sezonu pozostało ${NUMBER} godzin.", + "seasonEndsMinutesText": "Sezon zakończy się za ${NUMBER} minut.", + "seasonText": "Sezon ${NUMBER}", + "tournamentLeagueText": "Musisz uzyskać ligę ${NAME} aby wejść do tego turnieju.", + "trophyCountsResetText": "Liczba Zdobyczy zresetuje się w przyszłym sezonie." + }, + "levelBestScoresText": "Najlepsze wyniki w ${LEVEL}", + "levelBestTimesText": "Najlepsze czasy w ${LEVEL}", + "levelFastestTimesText": "Najszybsze czasy w ${LEVEL}", + "levelHighestScoresText": "Najwyższe wyniki w ${LEVEL}", + "levelIsLockedText": "${LEVEL} jest zablokowany.", + "levelMustBeCompletedFirstText": "${LEVEL} musi zostać ukończony jako pierwszy.", + "levelText": "Poziom ${NUMBER}", + "levelUnlockedText": "Poziom Odblokowany!", + "livesBonusText": "Bonusowe Życie", + "loadingText": "ładowanie", + "loadingTryAgainText": "Wczytywanie; spróbuj ponownie za chwilę...", + "macControllerSubsystemBothText": "Oba naraz (nie zalecane)", + "macControllerSubsystemClassicText": "Klasyczne", + "macControllerSubsystemDescriptionText": "(zmień to, jeśli kontrolery nie działają)", + "macControllerSubsystemMFiNoteText": "Kontroler Dla-iOS/Mac wykryty;\nZechcesz pewnie aktywować je w Ustawienia -> Kontrolery", + "macControllerSubsystemMFiText": "Zrobione-dla-iOS/Mac", + "macControllerSubsystemTitleText": "Wsparcie Kontrolerów", + "mainMenu": { + "creditsText": "Info", + "demoMenuText": "Menu Demo", + "endGameText": "Koniec Gry", + "exitGameText": "Wyjście z Gry", + "exitToMenuText": "Wyjść do menu?", + "howToPlayText": "Jak grać", + "justPlayerText": "(Tylko ${NAME})", + "leaveGameText": "Opuść Grę", + "leavePartyConfirmText": "Naprawdę opuszczasz imprezę?", + "leavePartyText": "Opuść Imprezę", + "leaveText": "Opuść grę", + "quitText": "Zakończ", + "resumeText": "Kontynuuj", + "settingsText": "Ustawienia" + }, + "makeItSoText": "Zastosuj", + "mapSelectGetMoreMapsText": "Zdobądź więcej map...", + "mapSelectText": "Wybierz...", + "mapSelectTitleText": "Mapy ${GAME}", + "mapText": "Mapa", + "maxConnectionsText": "Maksymalne Połączenia", + "maxPartySizeText": "Maksymalna ilość graczy", + "maxPlayersText": "Maksymalna ilość graczy", + "modeArcadeText": "Tryb Salonu Gier", + "modeClassicText": "Tryb Klasyczny", + "modeDemoText": "Tryb Demo", + "mostValuablePlayerText": "Najwartościowszy gracz", + "mostViolatedPlayerText": "Gracz najbardziej sprofanowany", + "mostViolentPlayerText": "Gracz najbardziej brutalny", + "moveText": "Przenieś", + "multiKillText": "${COUNT}-ZABITYCH!!!", + "multiPlayerCountText": "${COUNT} graczy", + "mustInviteFriendsText": "Uwaga: aby zagrać sieciowo musisz\nzaprosić znajomych w panelu\n\"${GATHER}\" lub dołączyć kontrolery.", + "nameBetrayedText": "${NAME} zdradził ${VICTIM}.", + "nameDiedText": "${NAME} zginął.", + "nameKilledText": "${NAME} zabił ${VICTIM}.", + "nameNotEmptyText": "Nazwa nie może być pusta!", + "nameScoresText": "${NAME} zdobył punkty!", + "nameSuicideKidFriendlyText": "${NAME} przypadkowo zginął.", + "nameSuicideText": "${NAME} popełnił samobójstwo.", + "nameText": "Nazwa", + "nativeText": "Natywna", + "newPersonalBestText": "Nowy rekord życiowy!", + "newTestBuildAvailableText": "Dostępna jest nowa wersja! (${VERSION} build ${BUILD}).\nPobierz ją z ${ADDRESS}", + "newText": "Nowy", + "newVersionAvailableText": "Dostępna jest nowa wersja ${APP_NAME}! (${VERSION})", + "nextAchievementsText": "Następne osiągnięcia:", + "nextLevelText": "Następny poziom", + "noAchievementsRemainingText": "- brak", + "noContinuesText": "(bez kontynuacji)", + "noExternalStorageErrorText": "Brak pamięci zewnętrznej w tym urządzeniu", + "noGameCircleText": "Błąd: niezalogowany w GameCircle", + "noJoinCoopMidwayText": "Rozgrywki trybu Kooperacji nie mogą być łączone w czasie ich trwania.", + "noProfilesErrorText": "Nie masz własnego profilu gracza, dlatego nazwano Cię: '${NAME}'.\nPrzejdź do Ustawień->Profile Gracza aby stworzyć własny.", + "noScoresYetText": "Brak wyników do tej pory.", + "noThanksText": "Nie, dziękuję", + "noTournamentsInTestBuildText": "OSTRZEŻENIE: Wyniki turniejów z tej wersji testowej będą ignorowane.", + "noValidMapsErrorText": "Nie znaleziono żadnych map dla tego typu rozgrywki.", + "notEnoughPlayersRemainingText": "Niewystarczająca ilość graczy. Spróbuj zacząć nową grę.", + "notEnoughPlayersText": "Aby rozpocząć grę potrzeba ${COUNT} graczy!", + "notNowText": "Nie teraz", + "notSignedInErrorText": "Musisz zalogować się, aby to zrobić", + "notSignedInGooglePlayErrorText": "Zaloguj się z Google Play, by to zrobić.", + "notSignedInText": "Nie zapisany", + "nothingIsSelectedErrorText": "Nic nie zaznaczyłeś!", + "numberText": "#${NUMBER}", + "offText": "Off", + "okText": "Ok", + "onText": "On", + "oneMomentText": "Chwileczkę...", + "onslaughtRespawnText": "${PLAYER} odrodzi się w fali ${WAVE}", + "orText": "${A} lub ${B}", + "otherText": "Inny...", + "outOfText": "(#${RANK} na ${ALL})", + "ownFlagAtYourBaseWarning": "Twoja flaga musi być w\nTwojej bazie aby zapunktować!", + "packageModsEnabledErrorText": "Gra sieciowa nie jest dozwolona kiedy są włączone lokalne pakiety modów (zobacz Ustawienia->Zaawansowane)", + "partyWindow": { + "chatMessageText": "Wiadomość", + "emptyText": "Twoja impreza nie ma gości", + "hostText": "(host)", + "sendText": "Wyślij", + "titleText": "Twoja Impreza" + }, + "pausedByHostText": "(rozgrywka zatrzymana przez host)", + "perfectWaveText": "Perfekcyjna Fala!", + "pickUpBoldText": "PODNIEŚ", + "pickUpText": "Podnieś", + "playModes": { + "coopText": "Kooperacja", + "freeForAllText": "Free-for-All", + "multiTeamText": "Multi-Drużyny", + "singlePlayerCoopText": "Pojedynczy gracz / Kooperacja", + "teamsText": "Drużyny" + }, + "playText": "Graj", + "playWindow": { + "coopText": "Kooperacja", + "freeForAllText": "Free-for-All", + "oneToFourPlayersText": "1-4 graczy", + "teamsText": "Gra Zespołowa", + "titleText": "Graj", + "twoToEightPlayersText": "2-8 graczy" + }, + "playerCountAbbreviatedText": "${COUNT} graczy", + "playerDelayedJoinText": "${PLAYER} wejdzie do gry przy następnej rundzie.", + "playerInfoText": "Info o graczu", + "playerLeftText": "${PLAYER} opuścił grę.", + "playerLimitReachedText": "Limit ${COUNT} graczy osiągnięty, nie można się przyłączyć.", + "playerLimitReachedUnlockProText": "Zaktualizuj do wersji \"${PRO}\" aby zagrać z więcej niż ${COUNT} graczami.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Nie możesz usunąć swojego profilu.", + "deleteButtonText": "Usuń\nProfil", + "deleteConfirmText": "Usunąć '${PROFILE}'?", + "editButtonText": "Edytuj\nProfil", + "explanationText": "(utwórz własne nazwy i postacie graczy na tym profilu)", + "newButtonText": "Nowy\nProfil", + "titleText": "Profile Graczy" + }, + "playerText": "Gracz", + "playlistNoValidGamesErrorText": "Ta lista nie zawiera żadnych odblokowanych rozgrywek.", + "playlistNotFoundText": "nie znaleziono listy", + "playlistText": "Lista", + "playlistsText": "Listy gier", + "pleaseRateText": "Jeśli polubiłeś ${APP_NAME}, proszę o poddanie go ocenie\nlub napisanie krótkiej recenzji. Pozwoli to zebrać przydatne\ninformacje, które pomogą wesprzeć rozwój gry w przyszłości.\n\nDziękuję!\n-Eric", + "pleaseWaitText": "Czekaj chwilkę...", + "pluginsDetectedText": "Nowe pluginy wykryte. Włącz/skonfiguruj je w ustawieniach.", + "pluginsText": "Pluginy", + "practiceText": "Praktyka", + "pressAnyButtonPlayAgainText": "Naciśnij dowolny przycisk aby zagrać ponownie...", + "pressAnyButtonText": "Naciśnij dowolny przycisk aby kontynuować...", + "pressAnyButtonToJoinText": "naciśnij dowolny przycisk aby dołączyć...", + "pressAnyKeyButtonPlayAgainText": "Naciśnij dowolny klawisz/przycisk aby zagrać ponownie...", + "pressAnyKeyButtonText": "Naciśnij dowolny klawisz/przycisk aby kontynuować...", + "pressAnyKeyText": "Naciśnij dowolny klawisz...", + "pressJumpToFlyText": "** Naciśnij wielokrotnie skok aby polecieć **", + "pressPunchToJoinText": "naciśnij CIOS aby dołączyć...", + "pressToOverrideCharacterText": "naciśnij ${BUTTONS} aby zastąpić swoją postać", + "pressToSelectProfileText": "naciśnij ${BUTTONS} aby wybrać gracza.", + "pressToSelectTeamText": "naciśnij ${BUTTONS} aby wybrać drużynę.", + "profileInfoText": "Stwórz profile dla siebie i swoich znajomych\ndostosowując ich nazwy i kolory postaci.", + "promoCodeWindow": { + "codeText": "Kod", + "codeTextDescription": "Kod Promocyjny", + "enterText": "Wprowadź" + }, + "promoSubmitErrorText": "Błąd wprowadzonego kodu; sprawdź połączenie internetowe", + "ps3ControllersWindow": { + "macInstructionsText": "Wyłącz zasilanie konsoli PS3, upewnij się, że masz uruchomiony\nBluetooth w swoim Mac'u, następnie podłącz kontroler do portu\nUSB Mac'a aby sparować urządzenia. Od teraz można używać na\nkontrolerze przycisku 'home' aby podłączyć go do Mac'a w obu\ntrybach - przewodowym (USB) i bezprzewodowym (Bluetooth).\n\nNa niektórych Mac'ach trzeba podać kod aby urządzenia sparować.\nJeśli się tak stanie poszukaj poradnika na google.\n\n\n\n\nKontrolery PS3 podłączone bezprzewodowo powinny być widoczne na\nliście urządzeń w Preferencjach Systemu->Bluetooth. Być może\ntrzeba będzie je usunąć z tej listy, kiedy zechcesz korzystać\nz nich ponownie w PS3.\n\nPamiętaj aby rozłączyć je z Bluetootha kiedy nie będą używane\nponieważ wyładują się ich baterie.\n\nBluetooth powinien obsłużyć do 7 podłączonych urządzeń, chociaż\nliczba ta może się wahać.", + "macInstructionsTextScale": 0.74, + "ouyaInstructionsText": "Aby używać kontrolerów PS3 z konsolą OUYA, po prostu podłącz je kablem USB\naby sparować urządzenia. Może to spowodować rozłączenie innych kontrolerów,\nwięc powinieneś później uruchomić ponownie konsolę OUYA i odłączyć kabel USB.\n\nOd teraz powinien być aktywny przycisk HOME aby połączyć się bezprzewodowo. Kiedy zakończysz grę, przytrzymaj HOME przez 10 sekund aby wyłączyć kontroler;\nw przeciwnym razie może on wyczerpać jego baterie.", + "ouyaInstructionsTextScale": 0.74, + "pairingTutorialText": "poradnik video parowania kontrolerów", + "titleText": "Korzystanie z kontrolerów PS3 w ${APP_NAME}:" + }, + "publicBetaText": "PUBLICZNA BETA", + "punchBoldText": "CIOS", + "punchText": "Cios", + "purchaseForText": "Kup za ${PRICE}", + "purchaseGameText": "Zakup Grę", + "purchasingText": "Kupowanie...", + "quitGameText": "Wyjść z ${APP_NAME}?", + "quittingIn5SecondsText": "Wyjście za 5 sekund...", + "randomPlayerNamesText": "DOMYŚLNE_NAZWY", + "randomText": "Losowo", + "rankText": "Ranking", + "ratingText": "Ocena", + "reachWave2Text": "Aby zdobyć punkty dotrwaj do 2 fali.", + "readyText": "gotowy", + "recentText": "Ostatnie", + "remainingInTrialText": "pozostań w wersji trial", + "remoteAppInfoShortText": "${APP_NAME} jest najfajniejszy gdy grasz z rodziną i przyjaciółmi. \nPodłącz jeszcze jeden lub więcej kontrolerów lub zainstaluj \n${REMOTE_APP_NAME} na telefony lub tablety i używaj ich\n jako kontrolerów.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Pozycja Przycisku", + "button_size": "Wielkość Przycisku", + "cant_resolve_host": "Nie można zanalizować hosta.", + "capturing": "Przechwytywanie...", + "connected": "Połączony.", + "description": "Użyj telefonu lub tabletu jako kontrolera w BombSquad.\nDo 8 urządzeń może być połączone naraz w epickiej wieloosobowej grze na jednym telewizorze lub tablecie.", + "disconnected": "Rozłączono przez serwer.", + "dpad_fixed": "nieruchomy", + "dpad_floating": "ruchomy", + "dpad_position": "Pozycja Joysticka", + "dpad_size": "Wielkość Joysticka", + "dpad_type": "Rodzaj Joysticka", + "enter_an_address": "Wpisz adres", + "game_full": "Gra jest pełna lub nie akceptuje połączeń.", + "game_shut_down": "Gra została zamknięta.", + "hardware_buttons": "Przyciski Sprzętu Komputerowego", + "join_by_address": "Wejdź przez adres...", + "lag": "Lag (opóźnienie): ${SECONDS} sekund", + "reset": "Resetuj do domyślnych", + "run1": "Run 1", + "run2": "Run 2", + "searching": "Szukanie gier BombSquad...", + "searching_caption": "Kliknij na nazwę gry by do niej dołączyć.\nUpewnij się, że jesteś w tej samej sieci Wi-Fi, co gra.", + "start": "Start", + "version_mismatch": "Wersje nie pasują.\nSprawdź czy BombSquad i BombSquad Remote\nmają najnowszą wersję i spróbuj ponownie." + }, + "removeInGameAdsText": "Odblokowywując wersję \"${PRO}\" pozbędziesz się reklam w grze.", + "renameText": "Zmień nazwę", + "replayEndText": "Zakończ powtórkę", + "replayNameDefaultText": "Ostatnia powtórka", + "replayReadErrorText": "Błąd odczytu pliku powtórki.", + "replayRenameWarningText": "Zmień nazwę \"${REPLAY}\" po zakończeniu gry jeśli chcesz zachować powtórkę, w przeciwnym wypadku zostanie ona nadpisana.", + "replayVersionErrorText": "Przepraszam, ale ta powtórka została utworzona\nw innej wersji gry i nie może być użyta.", + "replayWatchText": "Obejrzyj powtórkę", + "replayWriteErrorText": "Błąd zapisania pliku powtórki.", + "replaysText": "Powtórki", + "reportPlayerExplanationText": "Użyj tego emaila by zgłosić oszukiwanie, nieprawidłowy język lub inne złe zachowanie.\nProszę opisz je poniżej:", + "reportThisPlayerCheatingText": "Oszukiwanie", + "reportThisPlayerLanguageText": "Nieprawidłowy Język", + "reportThisPlayerReasonText": "Co chcesz zgłosić?", + "reportThisPlayerText": "Zgłoś tego gracza", + "requestingText": "Żądanie...", + "restartText": "Restart", + "retryText": "Ponów", + "revertText": "Przywróć", + "runBoldText": "URUCHOM", + "runText": "Uruchom", + "saveText": "Zapisz", + "scanScriptsErrorText": "Błąd w skanowaniu skryptów; sprawdź konsolę dla szczegółów", + "scoreChallengesText": "Wyzwania Punktowe", + "scoreListUnavailableText": "Lista wyników niedostępna.", + "scoreText": "Wynik", + "scoreUnits": { + "millisecondsText": "Milisekund", + "pointsText": "Punktów", + "secondsText": "Sekund" + }, + "scoreWasText": "(było ${COUNT})", + "selectText": "Wybierz", + "seriesWinLine1PlayerText": "WYGRAŁ", + "seriesWinLine1TeamText": "WYGRALI", + "seriesWinLine1Text": "WYGRAŁ", + "seriesWinLine2Text": "SERIĘ!", + "settingsWindow": { + "accountText": "Konto", + "advancedText": "Zaawansowane", + "audioText": "Audio", + "controllersText": "Kontrolery", + "graphicsText": "Grafika", + "playerProfilesMovedText": "Notka: Profile graczy zostały przeniesione do menu profili w menu głównym.", + "playerProfilesText": "Profile Gracza", + "titleText": "Ustawienia" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(prosta klawiatura na ekranie do edycji tekstu - przyjazna kontrolerom)", + "alwaysUseInternalKeyboardText": "Zawsze używaj wewn. klawiatury", + "benchmarksText": "Benchmarki & Testy Wydajności", + "disableCameraGyroscopeMotionText": "Wyłącz Kontrolę Kamery Żyroskopem", + "disableCameraShakeText": "Wyłącz Trzęsącą Kamerę", + "disableThisNotice": "(możesz wyłączyć to powiadomienie w ustawieniach zaawansowanych)", + "enablePackageModsDescriptionText": "(aktywuje dodatkowe możliwości modowania ale wyłącza grę sieciową)", + "enablePackageModsText": "Włącz lokalne pakiety modów", + "enterPromoCodeText": "Wpisz kod", + "forTestingText": "Uwaga: wartości stosowane do testów będą utracone po wyjściu z gry.", + "helpTranslateText": "Tłumaczenia ${APP_NAME} na inne języki są wysiłkiem społeczności\nfanów tej gry. Jeśli chcesz przyczynić się lub poprawić istniejące\nbłędy w tłumaczeniu, kliknij w poniższy link. Z góry dziękuję!", + "kickIdlePlayersText": "Wyrzuć nieaktywnych graczy", + "kidFriendlyModeText": "Tryb dla dzieciaków (zredukowana przemoc itd.)", + "languageText": "Język", + "moddingGuideText": "Przewodnik modowania gry", + "mustRestartText": "Musisz uruchomić ponownie grę aby zastosować zmiany.", + "netTestingText": "Testowanie sieci", + "resetText": "Reset", + "showBombTrajectoriesText": "Pokaż trajektorię bomb", + "showPlayerNamesText": "Pokazuj nazwy graczy", + "showUserModsText": "Pokaż katalog modów", + "titleText": "Zaawansowane", + "translationEditorButtonText": "Edytor tłumaczący ${APP_NAME}", + "translationFetchErrorText": "status tłumaczenia niedostępny", + "translationFetchingStatusText": "sprawdzanie statusu tłumaczenia...", + "translationInformMe": "Powiadom mnie gdy mój język będzie potrzebował uaktualnienia", + "translationNoUpdateNeededText": "Obecnie używany język jest aktualny; ekstra!", + "translationUpdateNeededText": "** obecnie używany język wymaga jego zaktualizowania! **", + "vrTestingText": "Testowanie VR" + }, + "shareText": "Udostępnij", + "sharingText": "Udostępnianie...", + "showText": "Wyświetl", + "signInForPromoCodeText": "Musisz się zalogować do konta aby kody zadziałały.", + "signInWithGameCenterText": "By użyć konta Game Center\nzapisz się aplikacją Game Center.", + "singleGamePlaylistNameText": "Tylko ${GAME}", + "singlePlayerCountText": "1 gracz", + "soloNameFilterText": "Solówka ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Wybór postaci", + "Chosen One": "Wybraniec", + "Epic": "Tryb gier epickich", + "Epic Race": "Epicki Wyścig", + "FlagCatcher": "Przechwyć Flagę", + "Flying": "Skrzydła Fantazji", + "Football": "Futbol", + "ForwardMarch": "Szturm", + "GrandRomp": "Podbój", + "Hockey": "Hokej", + "Keep Away": "Trzymaj się z dala", + "Marching": "Otaczanie", + "Menu": "Menu główne", + "Onslaught": "Atak", + "Race": "Wyścig", + "Scary": "Król Wzgórza", + "Scores": "Ekran Wyników", + "Survival": "Eliminacja", + "ToTheDeath": "Mecz Śmierci", + "Victory": "Ekran Wyników Końcowych" + }, + "spaceKeyText": "spacja", + "statsText": "Statystyki", + "storagePermissionAccessText": "To wymaga dostępu do pamięci masowej", + "store": { + "alreadyOwnText": "Masz już ${NAME}!", + "bombSquadProDescriptionText": "• Zdobywanie podwójnych kuponów w grze\n• Usunięcie reklam z rozgrywek\n•Zawiera ${COUNT} bonusowych kuponów\n•Bonus +${PERCENT}% w wyniku ligowym\n•Odblokowanie '${INF_ONSLAUGHT}' i\n'${INF_RUNAROUND}' w trybie kooperacji", + "bombSquadProFeaturesText": "Cechy:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Usuwa reklamy i wyskakujące okienka\n• Odblokowuje również więcej opcji\n• Zawiera także:", + "buyText": "Kup", + "charactersText": "Postacie", + "comingSoonText": "Wkrótce...", + "extrasText": "Dodatki", + "freeBombSquadProText": "BombSquad jest teraz darmowy, ale kiedy go oficjalnie zakupisz wówczas\notrzymasz wersję BombSquad Pro i ${COUNT} kuponów jako wyraz wdzięczności.\nMiłego korzystania z nowych funkcji i dziękuję za wsparcie!\n-Eric", + "gameUpgradesText": "Aktualizacje Gry", + "getCoinsText": "Zdobądź monety", + "holidaySpecialText": "Świąteczne Okazje", + "howToSwitchCharactersText": "(idź do \"${SETTINGS} -> ${PLAYER_PROFILES}\" aby przypisać i dostosować postacie)", + "howToUseIconsText": "(Stwórz profil globalny (w opcjach profilowych) aby tego użyć)", + "howToUseMapsText": "(użyj tych plansz w twoich listach gier free-for-all/drużynowych)", + "iconsText": "Ikonki", + "loadErrorText": "Nie można załadować strony.\nSprawdź połączenie internetowe.", + "loadingText": "ładowanie", + "mapsText": "Mapy", + "miniGamesText": "MiniGierki", + "oneTimeOnlyText": "(tylko jeden raz)", + "purchaseAlreadyInProgressText": "Zakup tego przedmiotu jest w trakcie realizacji.", + "purchaseConfirmText": "Kupujesz ${ITEM}?", + "purchaseNotValidError": "Niewłaściwy zakup.\nJeśli jest to błąd, skontaktuj się - ${EMAIL}.", + "purchaseText": "Kupuję", + "saleBundleText": "Wyprzedaż!", + "saleExclaimText": "Sprzedaż!", + "salePercentText": "(${PERCENT}% wyprzedaż)", + "saleText": "WYPRZEDAŻ", + "searchText": "Szukaj", + "teamsFreeForAllGamesText": "Rozgrywki Zespołowe / Free-for-All", + "totalWorthText": "*** Warte ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Ulepszyć?", + "winterSpecialText": "Specjały Zimowe", + "youOwnThisText": "- zdobyłeś już to -" + }, + "storeDescriptionText": "8 Osobowe Szaleństwo!\n\nWysadź w powietrze swoich znajomych (lub komputerowych przeciwników) w turnieju z wybuchowymi mini gierkami jak np. Przechwyć Flagę, Bombowy Hokej i Epicki Mecz Śmierci w zwolnionym tempie!\n\nProste sterowanie i rozszerzone wsparcie dla kontrolerów może wprowadzić do gry aż 8 przeciwników; możesz nawet wykorzystać swoje mobilne urządzenie jako kontroler do gry dostępne jako darmowa aplikacja 'BombSquad Remote'!\n\nBomby w Górę!\n\nSprawdź na www.froemling.net/bombsquad i dowiedz się więcej.", + "storeDescriptions": { + "blowUpYourFriendsText": "Wysadź w powietrze swoich znajomych.", + "competeInMiniGamesText": "Ukończ mini gierki aby awansować z wyścigów do lotów.", + "customize2Text": "Dostosuj postacie, mini gierki, a nawet ścieżkę dźwiękową.", + "customizeText": "Dostosuj postacie i stwórz własne listy mini gierek.", + "sportsMoreFunText": "Sporty są zabawniejsze z bombami.", + "teamUpAgainstComputerText": "Współpracuj przeciwko komputerowi." + }, + "storeText": "Sklep", + "submitText": "Prześlij", + "submittingPromoCodeText": "Przesyłanie Kodu...", + "teamNamesColorText": "Nazwy Drużyn/Kolory...", + "teamsText": "Gra Zespołowa", + "telnetAccessGrantedText": "Dostęp telnet włączony.", + "telnetAccessText": "Dostęp telnet wykryty; zezwolić?", + "testBuildErrorText": "Ta wersja testowa nie jest już dłużej aktywna; sprawdź dostępność nowej wersji.", + "testBuildText": "Wersja Testowa", + "testBuildValidateErrorText": "Niemożliwe zatwierdzenie wersji testowej. (brak internetu?)", + "testBuildValidatedText": "Wersja Testowa Zatwierdzona! Miłej zabawy!", + "thankYouText": "Dziękuję za Twoje wsparcie! Miłej gry!", + "threeKillText": "POTRÓJNE ZABÓJSTWO!!", + "timeBonusText": "Bonus czasowy", + "timeElapsedText": "Czas upłynął", + "timeExpiredText": "Czas minął", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}min", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Wskazówka", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Najlepsi znajomi", + "tournamentCheckingStateText": "Sprawdzanie statusu turnieju; proszę czekać...", + "tournamentEndedText": "Ten turniej został zakończony. Nowy wkrótce się rozpocznie.", + "tournamentEntryText": "Wejście do Turnieju", + "tournamentResultsRecentText": "Najnowsze wyniki Turnieju", + "tournamentStandingsText": "Klasyfikacja Turnieju", + "tournamentText": "Turniej", + "tournamentTimeExpiredText": "Czas Turnieju wygasł", + "tournamentsText": "Turnieje", + "translations": { + "characterNames": { + "Agent Johnson": "Agent Johnson", + "B-9000": "B-9000", + "Bernard": "Bernard", + "Bones": "Kostek", + "Butch": "Butch", + "Easter Bunny": "Zajączek Wielkanocny", + "Flopsy": "Flopsy", + "Frosty": "Frosty", + "Gretel": "Małgosia", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Fuks", + "Mel": "Mel", + "Middle-Man": "Kapitan Niepełniak", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Grzmot", + "Santa Claus": "Święty Mikołaj", + "Snake Shadow": "Snake Shadow", + "Spaz": "Spaz", + "Taobao Mascot": "Maskotka Taobao", + "Todd": "Todd", + "Todd McBurton": "Todd McBurton", + "Xara": "Xara", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Nieskończony\nAtak", + "Infinite\nRunaround": "Nieskończone\nOtaczanie", + "Onslaught\nTraining": "Atakujący\nTrening", + "Pro\nFootball": "Zawodowy\nFutbol", + "Pro\nOnslaught": "Atak\nZawodowca", + "Pro\nRunaround": "Zawodowe\nOtaczanie", + "Rookie\nFootball": "Futbol\nRekruta", + "Rookie\nOnslaught": "Atak\nRekruta", + "The\nLast Stand": "Ostatni\nBastion", + "Uber\nFootball": "Super\nFutbol", + "Uber\nOnslaught": "Super\nAtak", + "Uber\nRunaround": "Super\nOtaczanie" + }, + "coopLevelNames": { + "${GAME} Training": "Trening ${GAME}", + "Infinite ${GAME}": "Nieskończony tryb ${GAME}", + "Infinite Onslaught": "Nieskończony Atak", + "Infinite Runaround": "Nieskończone Otaczanie", + "Onslaught": "Nieskończony Atak", + "Onslaught Training": "Atakujący Trening", + "Pro ${GAME}": "Zawodowy tryb ${GAME}", + "Pro Football": "Zawodowy Futbol", + "Pro Onslaught": "Atak Zawodowca", + "Pro Runaround": "Zawodowe Otaczanie", + "Rookie ${GAME}": "Rekrucki tryb ${GAME}", + "Rookie Football": "Futbol Rekruta", + "Rookie Onslaught": "Atak Rekruta", + "Runaround": "Nieskończone Otaczanie", + "The Last Stand": "Ostatni Bastion", + "Uber ${GAME}": "Superowy tryb ${GAME}", + "Uber Football": "Super Futbol", + "Uber Onslaught": "Super Atak", + "Uber Runaround": "Super Otaczanie" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Bądź wybrańcem przez określony czas aby wygrać.\nZabij wybrańca aby się nim stać.", + "Bomb as many targets as you can.": "Zbombarduj tyle celów, ile tylko możesz.", + "Carry the flag for ${ARG1} seconds.": "Utrzymaj flagę przez ${ARG1} sekund.", + "Carry the flag for a set length of time.": "Utrzymaj flagę przez określony czas.", + "Crush ${ARG1} of your enemies.": "Rozbij ${ARG1} swoich wrogów.", + "Defeat all enemies.": "Pokonaj wszystkich wrogów.", + "Dodge the falling bombs.": "Unikaj spadających bomb.", + "Final glorious epic slow motion battle to the death.": "Ostateczna i chwalebna epicka bitwa w zwolnionym tempie do śmierci.", + "Gather eggs!": "Zbieraj jaja!", + "Get the flag to the enemy end zone.": "Przenieś flagę do końcowej strefy wroga.", + "How fast can you defeat the ninjas?": "Jak szybko jesteś w stanie pokonać ninja?", + "Kill a set number of enemies to win.": "Zabij ustaloną ilość wrogów aby wygrać.", + "Last one standing wins.": "Ostatni stojący wygrywa.", + "Last remaining alive wins.": "Ostatni pozostały przy życiu wygrywa.", + "Last team standing wins.": "Ostatni stojący zespół wygrywa.", + "Prevent enemies from reaching the exit.": "Zapobiegnij przed dotarciem wrogów do wyjścia.", + "Reach the enemy flag to score.": "Dotrzyj do wrogiej flagi aby zapunktować.", + "Return the enemy flag to score.": "Odzyskaj flagę wroga aby zapunktować.", + "Run ${ARG1} laps.": "Pokonaj ${ARG1} okrążeń.", + "Run ${ARG1} laps. Your entire team has to finish.": "Pokonaj ${ARG1} okrążeń. Cały Twoj zespół musi ukończyć.", + "Run 1 lap.": "Pokonaj 1 okrążenie.", + "Run 1 lap. Your entire team has to finish.": "Pokonaj 1 okrążenie. Cały Twój zespół musi ukończyć.", + "Run real fast!": "Biegnij naprawdę szybko!", + "Score ${ARG1} goals.": "Zdobądź ${ARG1} bramek.", + "Score ${ARG1} touchdowns.": "Zdobądź ${ARG1} przyłożeń.", + "Score a goal.": "Zdobądź bramkę.", + "Score a touchdown.": "Zdobądź przyłożenie.", + "Score some goals.": "Zdobądź kilka bramek.", + "Secure all ${ARG1} flags.": "Zabezpiecz wszystkie ${ARG1} flagi.", + "Secure all flags on the map to win.": "Zabezpiecz wszystkie flagi na mapie aby wygrać.", + "Secure the flag for ${ARG1} seconds.": "Zabezpiecz flagę przez ${ARG1} sekund.", + "Secure the flag for a set length of time.": "Zabezpiecz flagę przez ustalony okres czasu.", + "Steal the enemy flag ${ARG1} times.": "Ukradnij wrogą flagę ${ARG1} razy.", + "Steal the enemy flag.": "Ukradnij wrogą flagę.", + "There can be only one.": "Może być tylko jeden.", + "Touch the enemy flag ${ARG1} times.": "Dotknij wrogą flagę ${ARG1} razy.", + "Touch the enemy flag.": "Dotknij wrogą flagę.", + "carry the flag for ${ARG1} seconds": "Utrzymaj flagę przez ${ARG1} sekund", + "kill ${ARG1} enemies": "zabij ${ARG1} wrogów", + "last one standing wins": "ostatni stojący wygrywa", + "last team standing wins": "ostatni stojący zespół wygrywa", + "return ${ARG1} flags": "odzyskaj ${ARG1} flagi", + "return 1 flag": "odzyskaj 1 flagę", + "run ${ARG1} laps": "pokonaj ${ARG1} okrążenia", + "run 1 lap": "pokonaj 1 okrążenie", + "score ${ARG1} goals": "zdobądź ${ARG1} bramek", + "score ${ARG1} touchdowns": "zdobądź ${ARG1} przyłożeń", + "score a goal": "zdobądź bramkę", + "score a touchdown": "zdobądź przyłożenie", + "secure all ${ARG1} flags": "zabezpiecz wszystkie ${ARG1} flagi", + "secure the flag for ${ARG1} seconds": "zabezpiecz flagę przez ${ARG1} sekund", + "touch ${ARG1} flags": "dotknij ${ARG1} flag", + "touch 1 flag": "dotknij 1 flagę" + }, + "gameNames": { + "Assault": "Szturm", + "Capture the Flag": "Przechwyć Flagę", + "Chosen One": "Wybraniec", + "Conquest": "Podbój", + "Death Match": "Mecz śmierci", + "Easter Egg Hunt": "Polowanie na Jaja", + "Elimination": "Eliminacja", + "Football": "Futbol", + "Hockey": "Hokej", + "Keep Away": "Nie Podchodź", + "King of the Hill": "Król Wzgórza", + "Meteor Shower": "Deszcz Meteorytów", + "Ninja Fight": "Walka Ninja", + "Onslaught": "Atak", + "Race": "Wyścig", + "Runaround": "Otaczanie", + "Target Practice": "Ćwiczenie Celności", + "The Last Stand": "Ostatni Bastion" + }, + "inputDeviceNames": { + "Keyboard": "Klawiatura P1", + "Keyboard P2": "Klawiatura P2" + }, + "languages": { + "Arabic": "Arabski", + "Belarussian": "Białoruski", + "Chinese": "Chiński Uproszczony", + "ChineseTraditional": "Chiński Tradycyjny", + "Croatian": "Chorwacki", + "Czech": "Czeski", + "Danish": "Duński", + "Dutch": "Holenderski", + "English": "Angielski", + "Esperanto": "Esperanto", + "Finnish": "Fiński", + "French": "Francuski", + "German": "Niemiecki", + "Gibberish": "Szwargot", + "Greek": "Grecki", + "Hindi": "Hinduski", + "Hungarian": "Węgierski", + "Indonesian": "Indonezyjski", + "Italian": "Włoski", + "Japanese": "Japoński", + "Korean": "Koreański", + "Persian": "Perski", + "Polish": "Polski", + "Portuguese": "Portugalski", + "Romanian": "Rumuński", + "Russian": "Rosyjski", + "Serbian": "Serbski", + "Slovak": "słowacki", + "Spanish": "Hiszpański", + "Swedish": "Szwedzki", + "Turkish": "Turecki", + "Ukrainian": "Ukraiński", + "Venetian": "Wenecki", + "Vietnamese": "Wietnamski" + }, + "leagueNames": { + "Bronze": "Brązowa", + "Diamond": "Diamentowa", + "Gold": "Złota", + "Silver": "Srebrna" + }, + "mapsNames": { + "Big G": "Wielkie G", + "Bridgit": "Mostowo", + "Courtyard": "Dziedziniec", + "Crag Castle": "Urwany Zamek", + "Doom Shroom": "Fatalny Grzyb", + "Football Stadium": "Stadion Futbolowy", + "Happy Thoughts": "Skrzydła Fantazji", + "Hockey Stadium": "Hala Hokejowa", + "Lake Frigid": "Lodowate Jeziorko", + "Monkey Face": "Małpiatka", + "Rampage": "Nerwówa", + "Roundabout": "Wokoło", + "Step Right Up": "Krzątanina", + "The Pad": "Płyta", + "Tip Top": "Tip Top", + "Tower D": "Wieża D", + "Zigzag": "Zygzag" + }, + "playlistNames": { + "Just Epic": "Tylko rozgrywki Epickie", + "Just Sports": "Tylko rozgrywki Sportowe" + }, + "promoCodeResponses": { + "invalid promo code": "nieprawidłowy kod promocyjny" + }, + "scoreNames": { + "Flags": "Flagi", + "Goals": "Bramki", + "Score": "Wynik", + "Survived": "Przeżył", + "Time": "Czas", + "Time Held": "Czas trzymania" + }, + "serverResponses": { + "A code has already been used on this account.": "Kod został już użyty na tym koncie.", + "A reward has already been given for that address.": "Nagroda została już wręczona na tym adresie.", + "Account linking successful!": "Łączenie kont zakończone sukcesem!", + "Account unlinking successful!": "Pomyślnie rozłączono konta!", + "Accounts are already linked.": "Konta są już połączone.", + "An error has occurred; (${ERROR})": "Wystąpił błąd; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Wystąpił błąd; skontaktuj się z pomocą techniczną. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Wystąpił błąd; skontaktuj się z support@froemling.net.", + "An error has occurred; please try again later.": "Wystąpił błąd; spróbuj ponownie później.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Na pewno chcesz połączyć te konta?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nUwaga: To nie może być cofnięte!", + "BombSquad Pro unlocked!": "BombSquad Pro odblokowane!", + "Can't link 2 accounts of this type.": "Nie można połączyć dwóch kont tego typu.", + "Can't link 2 diamond league accounts.": "Nie można połączyć dwóch kont w diamentowej lidze.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Nie można połączyć; przekroczono limit ${COUNT} połączonych kont.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Oszukiwanie wykryte; punkty i nagrody zawieszone na ${COUNT} dni.", + "Could not establish a secure connection.": "Nie udało się nawiązać bezpiecznego połączenia.", + "Daily maximum reached.": "Dzienne maksimum wykorzystane.", + "Entering tournament...": "Wchodzenie do turnieju...", + "Invalid code.": "Nieprawidłowy kod.", + "Invalid payment; purchase canceled.": "Nieprawidłowa zapłata; zamówienie anulowane.", + "Invalid promo code.": "Nieprawidłowy kod promocyjny.", + "Invalid purchase.": "Nieprawidłowy zakup.", + "Invalid tournament entry; score will be ignored.": "Nieprawidłowe wejście do turnieju; wynik będzie zignorowany.", + "Item unlocked!": "Przedmiot odblokowany!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "ŁĄCZENIE ODRZUCONE. ${ACCOUNT} zawiera\nznaczący postęp który zostanie USUNIĘTY.\nMożecie połączyć konta na odwrót jeśli chcecie\n(i usunąć postęp tego DRUGIEGO konta).", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Połączyć konto ${ACCOUNT} z tym kontem?\nCały postęp z konta ${ACCOUNT} będzie stracony.\nNie można tego odwrócić. Pewna decyzja?", + "Max number of playlists reached.": "Osiągnięto maksymalną ilość playlist.", + "Max number of profiles reached.": "Osiągnięto maksymalną liczbę profili.", + "Maximum friend code rewards reached.": "Osiągnięto limit kodów promocyjnych.", + "Message is too long.": "Wiadomość jest za długa.", + "Profile \"${NAME}\" upgraded successfully.": "Nazwa \"${NAME}\" ulepszona pomyślnie.", + "Profile could not be upgraded.": "Profil nie może być zmieniony.", + "Purchase successful!": "Udany zakup!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Otrzymano ${COUNT} kuponów za zapisanie się. Wróć jutro aby otrzymać\n${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Funkcje serwerowe nie są dalej wspierane na tej wersji gry;\nZaktualizuj grę i spróbuj ponownie.", + "Sorry, there are no uses remaining on this code.": "Sorka, ten kod został już raz użyty.", + "Sorry, this code has already been used.": "Przepraszamy, ten kod został już użyty.", + "Sorry, this code has expired.": "Przepraszamy, ten kod wygasł.", + "Sorry, this code only works for new accounts.": "Przepraszamy, ten kod działa tylko na nowych kontach.", + "Temporarily unavailable; please try again later.": "Tymczasowo niedostępne; spróbuj ponownie później.", + "The tournament ended before you finished.": "Wyniki po zakończonym turnieju.", + "This account cannot be unlinked for ${NUM} days.": "To konto nie może zostać rozłączone przez ${NUM} dni.", + "This code cannot be used on the account that created it.": "Kod nie może zostać użyty na koncie, które go stworzyło.", + "This is currently unavailable; please try again later.": "To jest obecnie niedostępne; proszę spróbować ponownie później.", + "This requires version ${VERSION} or newer.": "Wymagana wersja ${VERSION} gry lub nowsza.", + "Tournaments disabled due to rooted device.": "Turnieje wyłączone z powodu zrootowanego urządzenia.", + "Tournaments require ${VERSION} or newer": "Turnieje potrzebują ${VERSION} albo nowszej", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Rozłączyć konto ${ACCOUNT} z tego konta?\nCały postęp z konta ${ACCOUNT} zostanie zresetowany.\n(oprócz osiągnięć w niektórych przypadkach)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "OSTRZEŻENIE: przeciwko Tobie zostały złożone skargi o oszukiwanie.\nKonta ludzi oszukujących zostaną zablokowane. Proszę grać uczciwie.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Chcesz połączyć swoje konto z urządzenia z tym?\n\nTwoje konto urządzenia to ${ACCOUNT1}\nTo konto to ${ACCOUNT2}\n\nTo pozwoli Ci zapisać istniejący postęp.\nUwaga: To nie może zostać cofnięte!!!", + "You already own this!": "Już to posiadasz!", + "You can join in ${COUNT} seconds.": "Możesz dołączyć w ciągu ${COUNT} sekund.", + "You don't have enough tickets for this!": "Nie masz wystarczająco dużo kuponów!", + "You don't own that.": "Nie posiadasz tego.", + "You got ${COUNT} tickets!": "Masz ${COUNT} kuponów!", + "You got a ${ITEM}!": "Otrzymałeś ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Zostałeś awansowany do nowej ligi; gratulacje!", + "You must update to a newer version of the app to do this.": "Musisz zaktualizować do nowszej wersji gry aby to zrobić.", + "You must update to the newest version of the game to do this.": "Musisz zaktualizować grę do nowej wersji aby to zrobić.", + "You must wait a few seconds before entering a new code.": "Musisz odczekać kilka sekund zanim wpiszesz nowy kod.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Zostałeś sklasyfikowany na ${RANK} pozycji w ostatnim turnieju. Dzięki za udział!", + "Your account was rejected. Are you signed in?": "Twoje Konto Zostało Odrzucone. Jesteś Zalogowany?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Twoja kopia gry została zmodyfikowana.\nCofnij zmiany i spróbuj ponownie.", + "Your friend code was used by ${ACCOUNT}": "Kod kumpla został użyty przez ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Minuta", + "1 Second": "1 Sekunda", + "10 Minutes": "10 Minut", + "2 Minutes": "2 Minuty", + "2 Seconds": "2 Sekundy", + "20 Minutes": "20 Minut", + "4 Seconds": "4 Sekundy", + "5 Minutes": "5 Minut", + "8 Seconds": "8 Sekund", + "Allow Negative Scores": "Pozostaw Wyniki Negatywne", + "Balance Total Lives": "Zbalansuj ogólną ilość żyć", + "Bomb Spawning": "Pojawianie się Bomb", + "Chosen One Gets Gloves": "Rękawice dla Wybrańca", + "Chosen One Gets Shield": "Tarcza dla Wybrańca", + "Chosen One Time": "Czas bycia Wybrańcem", + "Enable Impact Bombs": "Uaktywnij Bomby Dotykowe", + "Enable Triple Bombs": "Uaktywnij Potrójne Bomby", + "Entire Team Must Finish": "Cała drużyna musi ukończyć", + "Epic Mode": "Tryb Epicki", + "Flag Idle Return Time": "Powrót flagi po jej upuszczeniu", + "Flag Touch Return Time": "Powrót flagi po jej dotknięciu", + "Hold Time": "Czas wstrzymania", + "Kills to Win Per Player": "Zabójstw do zwycięstwa", + "Laps": "Okrążenia", + "Lives Per Player": "Życia na gracza", + "Long": "Długi", + "Longer": "Dłuższy", + "Mine Spawning": "Czas założenia miny", + "No Mines": "Bez Min", + "None": "Żaden", + "Normal": "Normalny", + "Pro Mode": "Tryb Zaawansowany", + "Respawn Times": "Czas odrodzenia", + "Score to Win": "Punktów do zwycięstwa", + "Short": "Krótki", + "Shorter": "Krótszy", + "Solo Mode": "Tryb pojedyńczego gracza", + "Target Count": "Liczba docelowa", + "Time Limit": "Limit czasowy" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} zostali zdyskwalifikowani, gdyż ${PLAYER} wyszedł", + "Killing ${NAME} for skipping part of the track!": "Zabito ${NAME} za pominięcie części toru!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Ostrzeżenie dla ${NAME}: turbo/spam kontrolkami nokautuje Cię." + }, + "teamNames": { + "Bad Guys": "Źli goście", + "Blue": "Niebiescy", + "Good Guys": "Dobrzy Goście", + "Red": "Czerwoni" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Perfekcyjne w czasie wykonanie biegu, skoku, obrotu i uderzenia\nmoże zabić jednym ruchem wzbudzając respekt wśród znajomych.", + "Always remember to floss.": "Pamiętaj żeby nitkować zęby.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Stwórz takie profile gracza (nazwa i wygląd) dla siebie i znajomych, które\nbędą Wam najbardziej odpowiadać zamiast tych losowo dobieranych.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Pudła z Klątwą zamienią Cię w tykającą bombę.\nJedynym ratunkiem jest wtedy szybkie zdobycie apteczki.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Pomimo różnego wyglądu, postacie posiadają te same umiejętności, więc ma\non tylko znaczenie wizualne. Wobec tego dobierz taki, który Ci odpowiada.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Nie bądź zbyt pewny siebie mając tarczę energetyczną; wciąż ktoś może Cię zrzucić z klifu.", + "Don't run all the time. Really. You will fall off cliffs.": "Nie biegaj cały czas, bo spadniesz z klifu.", + "Don't spin for too long; you'll become dizzy and fall.": "Nie obracaj się za długo; zakręci ci się w głowie i się wywrócisz.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Przytrzymaj dowolny przycisk aby biec. (Triggery działają równie\ndobrze (jeśli je masz)).", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Przytrzymaj dowolny przycisk aby biec. Wówczas osiągniesz szybciej\nswój cel lecz biegnąc ciężko się skręca, więc uważaj aby nie spaść.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Lodowe bomby nie są potężne, ale potrafią zamrozić każdego kto\nbędzie w ich polu rażenia, wówczas będzie on podatny na skruszenie.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Jeśli ktoś Cię podniesie, uderz go wówczas Cię puści.\nSkutkuje to również w prawdziwym życiu ;)", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Jeśli brakuje Ci kontrolerów, zainstaluj aplikację '${REMOTE_APP_NAME}'\nna swoich urządzeniach, aby użyć ich jako kontrolerów.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Jeśli brakuje Ci kontrolerów do gry, zainstaluj 'BombSquad Remote'\ndostępny w odpowiednich sklepach dla urządzeń iOS/Android.\nUżyj telefonów i tabletów jako kontrolerów do gry.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Jeśli przyklei się do Ciebie bomba przylepna, skacz i kręć się szalenie, to może ją\nzrzucisz. Jeżeli Ci się nie uda, wówczas twoje ostatnie chwile będą przezabawne ;).", + "If you kill an enemy in one hit you get double points for it.": "Jeśli zabijesz wroga jednym uderzeniem, otrzymasz podwójne punkty.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Jeśli zbierzesz 'Klątwę', to jedyną nadzieją aby\nprzetrwać jest szybkie zebranie apteczki.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Jeśli będziesz się czaił w jednym miejscu to jesteś usmażony.\nBiegaj i unikaj ataków aby przetrwać.", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Jeśli doświadczasz dużej rotacji wśród graczy, najlepiej włącz 'auto wyrzucanie\nbezczynnych graczy' w ustawieniach. Wyrzuci to tych, którzy nie grają a jedynie\nwiszą w grze blokując nowych chcących zagrać.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Jeśli Twoje urządzenie mocno się przegrzewa powinieneś oszczędzić baterię\nwyłączając 'Wizualizacje' lub zmniejszyć 'Rozdzielczość' w Ustawienia->Grafika", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Jeśli ilość klatek na sekundę jest zbyt niska, spróbuj\nzmniejszyć rozdzielczość lub jakość ustawień graficznych.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "W trybie 'Przechwycenia Flagi', Twoja flaga musi znajdować się w bazie aby zapunktować.\nJeśli drugi zespół zamierza zdobyć punkt, przechwycenie ich flagi będzie dobrym\nrozwiązaniem aby ich powstrzymać.", + "In hockey, you'll maintain more speed if you turn gradually.": "Grając w hokeja, większą prędkość utrzymywać będziesz przy\nstopniowym i delikatnym skręcaniu postacią.", + "It's easier to win with a friend or two helping.": "Łatwiej zwyciężyć grając w zespole z jednym lub kilkoma\ntowarzyszami broni.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Dzięki podskokowi przy wyrzucaniu bomby można je wrzucić\nna wyższe, ciężko dostępne poziomy mapy.", + "Land-mines are a good way to stop speedy enemies.": "Miny lądowe są dobrym rozwiązaniem aby powstrzymywać\nszybkich i wściekłych wrogów.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Można podnosić i rzucać wieloma rzeczami, włącznie z graczami. Zrzucanie\nwrogów z klifów może być skuteczne, a taka strategia satysfakcjonująca.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nie, nie dostaniesz się na wyższe półki. Może rzuć na nie bomby? :)", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Gracze mogą dołączyć/opuścić grę w trakcie jej trwania,\na Ty możesz dołączać/odłączać kontrolery w locie.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Gracze mogą się dołączać/opuszczać grę w trakcie większości\nrozgrywek, tak samo można w locie podłączać/rozłączać gamepady.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Bonusy mają czasowe limity jedynie w rozgrywkach trybu Kooperacji.\nW rozgrywkach drużynowych i Free-for-All są Twoje aż do śmierci.", + "Practice using your momentum to throw bombs more accurately.": "Poćwicz wyczucie tempa aby rzucać bombami dokładniej.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Ciosy powodują większe obrażenia jeśli się energicznie porusza\npięściami, więc staraj się biegać, skakać i kręcić jak szalony.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Podbiegnij do tyłu i przed rzuceniem bomby znów\ndo przodu. Pozwoli to na jej dalsze wyrzucenie.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Pozbądź się grupy wrogów poprzez\nrzucenie bomby blisko skrzyni TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Głowa jest najbardziej wrażliwym obszarem ciała, więc przyklejona\ndo niej przylepna bomba zazwyczaj oznacza koniec dla ofiary.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Ten poziom nigdy się nie kończy, ale wysoki wynik \nda Ci wieczny szacunek na całym świecie.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Siła rzutu zależy od kierunku, który trzymasz. Aby delikatnie tylko\ncoś podrzucić przed sobą, nie trzymaj żadnego klawisza kierunku.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Zmęczony domyślną ścieżką dźwiękową? Zamień ją na swoją!\nZobacz Ustawienia->Audio->Ścieżka dźwiękowa", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Przytrzymaj 'odpaloną bombę' przez 1 lub 2 sekundy zanim ją rzucisz.", + "Try tricking enemies into killing eachother or running off cliffs.": "Spróbuj oszukać wrogów aby się wzajemnie pozabijali lub zrzucili z klifów.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Użyj przycisku 'Podnieś' aby chwycić flagę < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Cofnij się... i do przodu, aby uzyskać dalsze rzuty...", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Możesz wycelować swoje uderzenia obracając się w lewo lub prawo.\nJest to przydatne do spychania wrogów poza krawędzie lub w hokeju.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Możesz sam kontrolować i określić czas kiedy bomba eksploduje\npo kolorze iskier na jej loncie: żółty.. pomarańczowy.. czerwony.. BUM!", + "You can throw bombs higher if you jump just before throwing.": "Możesz rzucać bombami wyżej jeśli podskoczysz przed wyrzuceniem.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Nie musisz edytować swojego profilu aby zmienić postacie. Naciśnij górny\nprzycisk (podnoszenie) kiedy dołączasz do gry aby zamienić domyślną postać.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Przyjmujesz obrażenia jeśli uderzasz głową w różne\nrzeczy, więc staraj się tego nie robić.", + "Your punches do much more damage if you are running or spinning.": "Twoje ciosy zadadzą więcej obrażeń, jeśli będziesz biegać lub się kręcić." + } + }, + "trialPeriodEndedText": "Wersja trial wygasła. Czy chcesz zakupić\nBombSquad i kontynuować grę?", + "trophiesRequiredText": "Potrzebujesz przynajmniej ${NUMBER} zdobyczy.", + "trophiesText": "Zdobycze", + "trophiesThisSeasonText": "Zdobycze w tym Sezonie", + "tutorial": { + "cpuBenchmarkText": "Wykonywanie testu szybkości (głównie testuje szybkość procesora)", + "phrase01Text": "Hejka!", + "phrase02Text": "Witaj w ${APP_NAME}!", + "phrase03Text": "Oto kilka porad jak kontrolować swoją postać:", + "phrase04Text": "Wiele zachowań w ${APP_NAME} to czysta FIZYKA.", + "phrase05Text": "Przykładowo, jeśli uderzasz...", + "phrase06Text": "...skala obrażeń uzależniona jest od szybkości pięści.", + "phrase07Text": "Widzisz? Nie poruszaliśmy się, więc ${NAME} prawie nie ucierpiał.", + "phrase08Text": "Teraz skoczymy i wykonamy obrót aby zwiększyć szybkość.", + "phrase09Text": "Ooo, tak lepiej.", + "phrase10Text": "Bieganie również pomaga.", + "phrase11Text": "Przytrzymaj dowolny przycisk aby biec.", + "phrase12Text": "Dla uzyskania mocnych uderzeń, staraj się biegać i obracać.", + "phrase13Text": "Ojjj... sorki ${NAME}.", + "phrase14Text": "Możesz podnosić i rzucać np. flagami, bombami, a nawet przeciwnikiem - ${NAME}.", + "phrase15Text": "Ale BombSquad to głównie BOMBY.", + "phrase16Text": "Skuteczne rzucanie bombami wymaga odrobinę praktyki.", + "phrase17Text": "Jałć! Niezbyt dobry rzut.", + "phrase18Text": "Poruszanie się przy rzucie pozwala rzucać dalej.", + "phrase19Text": "Skakanie pozwala rzucać wyżej.", + "phrase20Text": "Kręć bombą, aby cisnąć nią dalej.", + "phrase21Text": "Wyczucie czasu przy rzucaniu bombami może być trudne.", + "phrase22Text": "Pudło.", + "phrase23Text": "Spróbuj \"przysmażyć\" lont przez sekundę lub dwie.", + "phrase24Text": "I proszę! Nieźle upieczony.", + "phrase25Text": "Cóż, to by było na tyle.", + "phrase26Text": "A teraz bierz ich tygrysie!", + "phrase27Text": "Zapamiętaj to szkolenie rekrucie, a RACZEJ wrócisz żywy!", + "phrase28Text": "...może...", + "phrase29Text": "Powodzenia!", + "randomName1Text": "Fred", + "randomName2Text": "Adaś", + "randomName3Text": "Benio", + "randomName4Text": "Czesio", + "randomName5Text": "Ignaś", + "skipConfirmText": "Jeśli chcesz pominąć samouczek to stuknij lub naciśnij aby zatwierdzić.", + "skipVoteCountText": "${COUNT}/${TOTAL} pominiętych głosów", + "skippingText": "pomijam samouczek...", + "toSkipPressAnythingText": "(stuknij lub naciśnij cokolwiek aby pominąć samouczek)" + }, + "twoKillText": "PODWÓJNE ZABÓJSTWO!", + "unavailableText": "niedostępne", + "unconfiguredControllerDetectedText": "Wykryto nieskonfigurowany kontroler:", + "unlockThisInTheStoreText": "To musi zostać odblokowane w sklepie.", + "unlockThisProfilesText": "By stworzyć więcej niż ${NUM} kont, potrzebujesz:", + "unlockThisText": "Żeby to odblokować, potrzebujesz:", + "unsupportedHardwareText": "Przepraszam ale ten sprzęt nie jest obsługiwany przez tą wersję gry.", + "upFirstText": "Pierwsza gra w rozgrywce:", + "upNextText": "Kolejna, ${COUNT} gra w rozgrywce:", + "updatingAccountText": "Aktualizowanie twojego konta...", + "upgradeText": "Ulepsz", + "upgradeToPlayText": "Aby zagrać odblokuj grę w wersji \"${PRO}\".", + "useDefaultText": "Użyj domyślnych", + "usesExternalControllerText": "Ta gra wykorzystuje zewnętrzny kontroler jako wejście.", + "usingItunesText": "Korzystanie z aplikacji muzycznej jako ścieżki dźwiękowej...", + "usingItunesTurnRepeatAndShuffleOnText": "Upewnij się, że w ustawieniach iTunes tasowanie utworów i powtarzanie całości jest włączone.", + "validatingBetaText": "Legalizowanie wersji Beta...", + "validatingTestBuildText": "Sprawdzanie wersji testowej...", + "victoryText": "Zwycięstwo!", + "voteDelayText": "Nie możesz zagłosować przez następne ${NUMBER} sekund", + "voteInProgressText": "Głosowanie jest już w toku.", + "votedAlreadyText": "Już zagłosowałeś", + "votesNeededText": "Potrzeba ${NUMBER} głosów", + "vsText": "kontra", + "waitingForHostText": "(oczekiwanie na ${HOST} aby kontynuować)", + "waitingForLocalPlayersText": "Oczekiwanie na lokalnych graczy...", + "waitingForPlayersText": "oczekiwanie na dołączenie graczy...", + "waitingInLineText": "Czekanie w kolejce (impreza pełna)...", + "watchAVideoText": "Zobacz Filmik", + "watchAnAdText": "Obejrzyj reklamę", + "watchWindow": { + "deleteConfirmText": "Usunąć \"${REPLAY}\"?", + "deleteReplayButtonText": "Usuń\nPowtórkę", + "myReplaysText": "Moje Powtórki", + "noReplaySelectedErrorText": "Nie wybrano powtórki", + "playbackSpeedText": "Szybkość Powtórki: ${SPEED}", + "renameReplayButtonText": "Zmień nazwę\nPowtórki", + "renameReplayText": "Zmień nazwę \"${REPLAY}\" na:", + "renameText": "Zmień nazwę", + "replayDeleteErrorText": "Błąd usuwania powtórki.", + "replayNameText": "Nazwa Powtórki", + "replayRenameErrorAlreadyExistsText": "Powtórka z taką nazwą już istnieje.", + "replayRenameErrorInvalidName": "Nie można zmienić nazwy powtórki; nieprawidłowa nazwa.", + "replayRenameErrorText": "Błąd zmiany nazwy powtórki.", + "sharedReplaysText": "Udostępnione Powtórki", + "titleText": "Oglądaj", + "watchReplayButtonText": "Zobacz\nPowtórkę" + }, + "waveText": "Fala", + "wellSureText": "No pewnie!", + "wiimoteLicenseWindow": { + "titleText": "Prawa autorskie DarwiinRemote" + }, + "wiimoteListenWindow": { + "listeningText": "Nasłuchiwanie za Wiimotami...", + "pressText": "Naciśnij jednocześnie przyciski 1 i 2 na kontrolerze Wiimote.", + "pressText2": "Na nowszych kontrolerach Wiimote z wbudowanym czujnikiem Motion Plus, naciśnij zamiennie czerwony przycisk synchronizacji." + }, + "wiimoteSetupWindow": { + "copyrightText": "Prawa autorskie DarwiinRemote", + "listenText": "Nasłuchiwanie", + "macInstructionsText": "Upewnij się, że twój kontroler Wii jest wyłączony a\nBluetooth uruchomiony na twoim Mac'u, następnie\nnaciśnij 'Słuchaj'. Wsparcie dla Wiimote może być\ntroszkę ciężko kojarzone, więc musisz spróbować\nkilkakrotnie aby uzyskać połączenie.\n\nBluetooth poradzi sobie z połączeniem do 7 urządzeń,\nchociaż ta liczba może się wahać.\n\nBombSquad wspiera oryginalne Wiimotes, Nunchuks,\ni klasyczne kontrolery. Nowy Wii Remote Plus również\npowinien współpracować lecz bez dodatków.", + "thanksText": "Podziękowania dla zespołu DarwiinRemote\nza wykonanie tej możliwości.", + "titleText": "Ustawienia Wiimote" + }, + "winsPlayerText": "${NAME} Wygrywa!", + "winsTeamText": "${NAME} Wygrywają!", + "winsText": "${NAME} Wygrywa!", + "worldScoresUnavailableText": "Ogólnoświatowe wyniki niedostępne.", + "worldsBestScoresText": "Najlepsze ogólnoświatowe wyniki", + "worldsBestTimesText": "Najlepsze ogólnoświatowe czasy", + "xbox360ControllersWindow": { + "getDriverText": "Pobierz sterownik", + "macInstructions2Text": "Aby używać kontrolery bezprzewodowo, musisz posiadać odbiornik\ndostępny w zestawie 'Bezprzewodowy kontroler Xbox360 dla Windows'.\nJeden odbiornik pozwala na podłączenie do 4 kontrolerów.\n\nWażne: inne odbiorniki mogą nie współpracować ze sterownikiem;\nupewnij się, czy odbiornik posiada napis 'Microsoft', nie 'XBOX360'.\nMicrosoft nie sprzedaje odbiorników osobno, więc musisz nabyć go\nw zestawie z kontrolerem lub zakupić na aukcjach.\n\nJeśli powyższe informacje okazały się przydatne, rozważ przekazanie\ndotacji dla producenta sterownika na jego stronie.", + "macInstructionsText": "Aby użyć kontrolerów z konsoli Xbox360, musisz\nzainstalować sterownik Mac'a dostępny w poniższym linku.\nSterownik współpracuje zarówno z przewodowymi jak i\nbezprzewodowymi kontrolerami.", + "ouyaInstructionsText": "Aby skorzystać z bezprzewodowych kontrolerów konsoli Xbox360,\npodłącz je do portów USB urządzeń. Możesz skorzystać z hubu USB\naby podłączyć klika kontrolerów.\n\nAby użyć bezprzewodowe kontrolery musisz posiadać bezprzewodowy\nodbiornik, dostępny w zestawie \"Bezprzewodowy kontroler Xbox360\ndla Windows\" lub sprzedawany oddzielnie. Każdy odbiornik podłączony\ndo portu USB urządzenia pozwoli na podłączenie do 4 bezprzewodowych\nkontrolerów.", + "titleText": "Wykorzystywanie kontrolerów Xbox360 w ${APP_NAME}" + }, + "yesAllowText": "Dajesz!", + "yourBestScoresText": "Twoje najlepsze wyniki", + "yourBestTimesText": "Twoje najlepsze czasy" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/portuguese.json b/dist/ba_data/data/languages/portuguese.json new file mode 100644 index 0000000..c522d1a --- /dev/null +++ b/dist/ba_data/data/languages/portuguese.json @@ -0,0 +1,1978 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "O seu nome não pode conter emojis ou outros caracteres especiais", + "accountProfileText": "Perfil da Conta", + "accountsText": "Contas", + "achievementProgressText": "Conquistas: ${COUNT} de ${TOTAL}", + "campaignProgressText": "Progresso da campanha [Difícil]: ${PROGRESS}", + "changeOncePerSeason": "Você só pode mudar isso uma vez por temporada.", + "changeOncePerSeasonError": "Você deve esperar até a próxima temporada para mudar isso novamente (${NUM} dias)", + "customName": "Nome personalizado", + "linkAccountsEnterCodeText": "Inserir código", + "linkAccountsGenerateCodeText": "Gerar código", + "linkAccountsInfoText": "(compartilhar progresso entre várias plataformas)", + "linkAccountsInstructionsNewText": "Para vincular duas contas, gere um código na primeira\ne insira o código na segunda. O progresso da\nsegunda conta será sincronizado com ambas.\n(Progresso da primeira conta será perdido)\n\nVocê pode vincular ${COUNT} contas.\n\nIMPORTANTE: apenas vincule contas que você tem acesso; \nSe você vincular a conta de um amigo vocês não\nserão capazes de jogar online ao mesmo tempo.", + "linkAccountsInstructionsText": "Para vincular duas contas, gere um código\nem uma delas e o insira na outra.\nProgresso e inventário serão combinados.\nVocê pode vincular até ${COUNT} contas.\n\nIMPORTANTE: Vincule apenas as contas que você possui!\nSe você vincular contas a seus amigos, você\nnão será capaz de jogar ao mesmo tempo!\n\nAlém disso: isso não pode ser desfeito atualmente, então tenha cuidado!", + "linkAccountsText": "Vincular contas", + "linkedAccountsText": "Contas vinculadas:", + "nameChangeConfirm": "Mudar o nome da sua conta para ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Isso reiniciará o seu progresso no cooperativo e\nsuas pontuações (mas não os seus bilhetes).\nIsso não pode ser desfeito. Tem certeza?", + "resetProgressConfirmText": "Isso reiniciará o seu progresso no cooperativo,\nsuas conquistas e suas pontuações\n(mas não os seus bilhetes). Isso não pode\nser desfeito. Tem certeza?", + "resetProgressText": "Reiniciar progresso", + "setAccountName": "Escolha o nome da conta", + "setAccountNameDesc": "Escolha o nome que será exibido na sua conta.\nVocê pode usar o nome de uma de suas contas\nou criar um nome personalizado exclusivo.", + "signInInfoText": "Inicie a sessão para ganhar bilhetes, compita online e compartilhe\nseu progresso entre vários dispositivos.", + "signInText": "Iniciar sessão", + "signInWithDeviceInfoText": "(uma conta automática disponível apenas neste aparelho)", + "signInWithDeviceText": "Iniciar sessão com conta do dispositivo", + "signInWithGameCircleText": "Iniciar sessão com Game Circle", + "signInWithGooglePlayText": "Iniciar sessão com Google Play", + "signInWithTestAccountInfoText": "(tipo de conta legado; use as contas do dispositivo daqui em diante)", + "signInWithTestAccountText": "Iniciar sessão com conta teste", + "signOutText": "Finalizar sessão", + "signingInText": "Iniciando sessão...", + "signingOutText": "Finalizando sessão...", + "testAccountWarningCardboardText": "Atenção: você está entrando com uma conta \"teste\".\nUma hora elas serão trocadas por contas Google\nassim que elas forem suportadas nos apps do cardboard.\n\nPor agora, você terá que ganhar todos os tickets dentro do jogo.\n(mesmo assim você ganha o upgrade BombSquad Pro gratuitamente)", + "testAccountWarningOculusText": "Aviso: você está conectando com uma \"conta de teste\".\nEles serão substituídos por contas Oculus mais tarde que oferecerá compras de tickets\ne muito mais. \n\nPor enquanto você terá que ganhar todos os tickets no jogo.\n(No entanto, você obterá a atualização BombSquad Pro de graça)", + "testAccountWarningText": "Aviso: você está conectando com uma \"conta teste\".\nEsta conta está vinculada somente à este aparelho e \npoderá ser restaurada periodicamente. (Então por favor\nnão gaste tempo coletando/desbloqueando itens nela)\n\nExecute a versão completa do jogo para usar uma\n\"conta real\" (Game-Center, Google Plus, etc.)\nIsto permite o armazenamento do seu progresso\nna nuvem e compartilha-lo com outros aparelhos. \n", + "ticketsText": "Bilhetes: ${COUNT}", + "titleText": "Conta", + "unlinkAccountsInstructionsText": "Selecione uma conta para desvincular", + "unlinkAccountsText": "Desvincular contas", + "viaAccount": "(via ${NAME})", + "youAreLoggedInAsText": "Você está logado como:", + "youAreSignedInAsText": "Iniciou sessão como:" + }, + "achievementChallengesText": "Desafios conquistados", + "achievementText": "Conquistas", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Mate 3 inimigos com TNT", + "descriptionComplete": "Matou 3 inimigos com TNT", + "descriptionFull": "Mate 3 inimigos com TNT no ${LEVEL}", + "descriptionFullComplete": "Matou 3 inimigos com TNT no ${LEVEL}", + "name": "Quem Brinca com Fogo Sai Queimado" + }, + "Boxer": { + "description": "Ganhe sem usar bombas", + "descriptionComplete": "Ganhou sem usar bombas", + "descriptionFull": "Complete o ${LEVEL} sem usar bombas", + "descriptionFullComplete": "Completou o ${LEVEL} sem usar bombas", + "name": "Boxeador" + }, + "Dual Wielding": { + "descriptionFull": "Conecte 2 controles (hardware ou aplicativo)", + "descriptionFullComplete": "Conectou 2 controles (hardware ou aplicativo)", + "name": "Dominação dupla" + }, + "Flawless Victory": { + "description": "Ganhe sem ser atingido", + "descriptionComplete": "Ganhou sem ser atingido", + "descriptionFull": "Ganhe o ${LEVEL} sem ser atingido", + "descriptionFullComplete": "Ganhou o ${LEVEL} sem ser atingido", + "name": "Vitória perfeita" + }, + "Free Loader": { + "descriptionFull": "Comece um Todos contra todos com mais de 2 jogadores", + "descriptionFullComplete": "Começou um Todos contra todos com mais de 2 jogadores", + "name": "Carregador livre" + }, + "Gold Miner": { + "description": "Mate 6 inimigos com minas", + "descriptionComplete": "Matou 6 inimigos com minas", + "descriptionFull": "Mate 6 inimigos com minas no ${LEVEL}", + "descriptionFullComplete": "Matou 6 inimigos com minas no ${LEVEL}", + "name": "Garimpeiro" + }, + "Got the Moves": { + "description": "Ganhe sem usar socos ou bombas", + "descriptionComplete": "Ganhou sem usar socos ou bombas", + "descriptionFull": "Ganhe o ${LEVEL} sem socos ou bombas", + "descriptionFullComplete": "Ganhou o ${LEVEL} sem socos ou bombas", + "name": "Esse Tem Gingado" + }, + "In Control": { + "descriptionFull": "Conecte um controle (hardware ou aplicativo)", + "descriptionFullComplete": "Conectou um controle. (hardware ou aplicativo)", + "name": "No controle" + }, + "Last Stand God": { + "description": "Marque 1000 pontos", + "descriptionComplete": "Marcou 1000 pontos", + "descriptionFull": "Marque 1000 pontos no ${LEVEL}", + "descriptionFullComplete": "Marcou 1000 pontos no ${LEVEL}", + "name": "${LEVEL} Deus" + }, + "Last Stand Master": { + "description": "Marque 250 pontos", + "descriptionComplete": "Marcou 250 pontos", + "descriptionFull": "Marque 250 pontos no ${LEVEL}", + "descriptionFullComplete": "Marcou 250 pontos no ${LEVEL}", + "name": "${LEVEL} Mestre" + }, + "Last Stand Wizard": { + "description": "Marque 500 pontos", + "descriptionComplete": "Marcou 500 pontos", + "descriptionFull": "Marque 500 pontos no ${LEVEL}", + "descriptionFullComplete": "Marcou 500 pontos no ${LEVEL}", + "name": "${LEVEL} Mago" + }, + "Mine Games": { + "description": "Mate 3 inimigos com minas", + "descriptionComplete": "Matou 3 inimigos com minas", + "descriptionFull": "Mate 3 inimigos com minas no ${LEVEL}", + "descriptionFullComplete": "Matou 3 inimigos com minas no ${LEVEL}", + "name": "Brincando com Minas" + }, + "Off You Go Then": { + "description": "Mande 3 inimigos para fora do mapa", + "descriptionComplete": "Mandou 3 inimigos para fora do mapa", + "descriptionFull": "Mande 3 inimigos para fora do mapa no ${LEVEL}", + "descriptionFullComplete": "Mandou 3 inimigos para fora do mapa no ${LEVEL}", + "name": "Pode Ir Agora" + }, + "Onslaught God": { + "description": "Marque 5000 pontos", + "descriptionComplete": "Marcou 5000 pontos", + "descriptionFull": "Marque 5000 pontos no ${LEVEL}", + "descriptionFullComplete": "Marcou 5000 pontos no ${LEVEL}", + "name": "${LEVEL} Deus" + }, + "Onslaught Master": { + "description": "Marque 500 pontos", + "descriptionComplete": "Marcou 500 pontos", + "descriptionFull": "Marque 500 pontos no ${LEVEL}", + "descriptionFullComplete": "Marcou 500 pontos no ${LEVEL}", + "name": "${LEVEL} Mestre" + }, + "Onslaught Training Victory": { + "description": "Derrote todas as ondas", + "descriptionComplete": "Derrotou todas as ondas", + "descriptionFull": "Derrote todas as ondas no ${LEVEL}", + "descriptionFullComplete": "Derrotou todas as ondas no ${LEVEL}", + "name": "Vitória no ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Marque 1000 pontos", + "descriptionComplete": "Marcou 1000 pontos", + "descriptionFull": "Marque 1000 pontos no ${LEVEL}", + "descriptionFullComplete": "Marcou 1000 pontos no ${LEVEL}", + "name": "Mago do ${LEVEL}" + }, + "Precision Bombing": { + "description": "Ganhe sem poderes", + "descriptionComplete": "Ganhou sem poderes", + "descriptionFull": "Ganhe ${LEVEL} sem poderes", + "descriptionFullComplete": "Ganhou ${LEVEL} sem poderes", + "name": "Chuva de Bomba" + }, + "Pro Boxer": { + "description": "Ganhe sem usar bombas", + "descriptionComplete": "Ganhou sem usar bombas", + "descriptionFull": "Complete o ${LEVEL} sem usar bombas", + "descriptionFullComplete": "Completou o ${LEVEL} sem usar bombas", + "name": "Pugilista" + }, + "Pro Football Shutout": { + "description": "Ganhe sem deixar os inimigos marcarem", + "descriptionComplete": "Ganhou sem deixar os inimigos marcarem", + "descriptionFull": "Ganhe o ${LEVEL} sem deixar os inimigos marcarem", + "descriptionFullComplete": "Ganhou o ${LEVEL} sem deixar os inimigos marcarem", + "name": "${LEVEL} De Lavada" + }, + "Pro Football Victory": { + "description": "Ganhe a partida", + "descriptionComplete": "Ganhou a partida", + "descriptionFull": "Ganhe a partida no ${LEVEL}", + "descriptionFullComplete": "Ganhou a partida no ${LEVEL}", + "name": "Vitória no ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Derrote todas as ondas", + "descriptionComplete": "Todas as ondas derrotadas", + "descriptionFull": "Derrote todas as ondas no ${LEVEL}", + "descriptionFullComplete": "Todas as ondas do ${LEVEL} derrotadas", + "name": "Vitória no ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Complete todas as ondas", + "descriptionComplete": "Todas as ondas completadas", + "descriptionFull": "Complete todas as ondas no ${LEVEL}", + "descriptionFullComplete": "Todas as ondas completadas no ${LEVEL}", + "name": "Vitória no ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Ganhe sem deixar os inimigos marcarem", + "descriptionComplete": "Ganhou sem deixar os inimigos marcarem", + "descriptionFull": "Ganhe no ${LEVEL} sem deixar os inimigos marcarem", + "descriptionFullComplete": "Ganhou no ${LEVEL} sem deixar os inimigos marcarem", + "name": "${LEVEL} De Levada" + }, + "Rookie Football Victory": { + "description": "Ganhe a partida", + "descriptionComplete": "Ganhou a partida", + "descriptionFull": "Ganhe a partida no ${LEVEL}", + "descriptionFullComplete": "Ganhou a partida no ${LEVEL}", + "name": "Vitória no ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Derrote todas as ondas", + "descriptionComplete": "Todas as ondas derrotadas", + "descriptionFull": "Derrote todas as ondas no ${LEVEL}", + "descriptionFullComplete": "Todas as ondas no ${LEVEL} derrotadas", + "name": "Vitória no ${LEVEL}" + }, + "Runaround God": { + "description": "Marque 2000 pontos", + "descriptionComplete": "Marcou 2000 pontos", + "descriptionFull": "Marque 2000 pontos no ${LEVEL}", + "descriptionFullComplete": "Marcou 2000 pontos no ${LEVEL}", + "name": "Deus da ${LEVEL}" + }, + "Runaround Master": { + "description": "Marque 500 pontos", + "descriptionComplete": "Marcou 500 pontos", + "descriptionFull": "Marque 500 pontos no ${LEVEL}", + "descriptionFullComplete": "Marcou 500 pontos no ${LEVEL}", + "name": "Mestre do ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Marque 1000 pontos", + "descriptionComplete": "Marcou 1000 pontos", + "descriptionFull": "Marque 1000 pontos no ${LEVEL}", + "descriptionFullComplete": "Marcou 1000 pontos no ${LEVEL}", + "name": "Mago do ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Compartilhe o jogo com um amigo com êxito", + "descriptionFullComplete": "Jogo compartilhado com um amigo com êxito", + "name": "Compartilhar é amar" + }, + "Stayin' Alive": { + "description": "Ganhe sem morrer", + "descriptionComplete": "Ganhou sem morrer", + "descriptionFull": "Ganhe o ${LEVEL} sem morrer", + "descriptionFullComplete": "Ganhou o ${LEVEL} sem morrer", + "name": "Sobrevivendo" + }, + "Super Mega Punch": { + "description": "Cause 100% de dano com um soco", + "descriptionComplete": "Causou 100% de dano com um soco", + "descriptionFull": "Cause 100% de dano com um soco no ${LEVEL}", + "descriptionFullComplete": "Causou 100% de dano com um soco no ${LEVEL}", + "name": "Super Mega Soco" + }, + "Super Punch": { + "description": "Cause 50% de dano com um soco", + "descriptionComplete": "Causou 50% de dano com um soco", + "descriptionFull": "Cause 50% de dano com um soco no ${LEVEL}", + "descriptionFullComplete": "Causou 50% de dano com um soco no ${LEVEL}", + "name": "Super Soco" + }, + "TNT Terror": { + "description": "Mate 6 inimigos com TNT", + "descriptionComplete": "Matou 6 inimigos com TNT", + "descriptionFull": "Mate 6 inimigos com TNT no ${LEVEL}", + "descriptionFullComplete": "Matou 6 inimigos com TNT no ${LEVEL}", + "name": "Terror do TNT" + }, + "Team Player": { + "descriptionFull": "Comece um jogo de equipes com mais de 4 jogadores", + "descriptionFullComplete": "Começou um jogo de equipes com mais de 4 jogadores", + "name": "Jogador de equipe" + }, + "The Great Wall": { + "description": "Pare todos os inimigos", + "descriptionComplete": "Parou todos os inimigos", + "descriptionFull": "Pare todos os inimigos no ${LEVEL}", + "descriptionFullComplete": "Parou todos os inimigos no ${LEVEL}", + "name": "A Grande Muralha" + }, + "The Wall": { + "description": "Pare todos os inimigos", + "descriptionComplete": "Parou todos os inimigos", + "descriptionFull": "Pare todos os inimigos no ${LEVEL}", + "descriptionFullComplete": "Parou todos os inimigos no ${LEVEL}", + "name": "A Muralha" + }, + "Uber Football Shutout": { + "description": "Ganhe sem deixar os inimigos marcarem", + "descriptionComplete": "Ganhou sem deixar os inimigos marcarem", + "descriptionFull": "Ganhe o ${LEVEL} sem deixar os inimigos marcarem", + "descriptionFullComplete": "Ganhou o ${LEVEL} sem deixar os inimigos marcarem", + "name": "${LEVEL} De Lavada" + }, + "Uber Football Victory": { + "description": "Ganhe a partida", + "descriptionComplete": "Ganhou a partida", + "descriptionFull": "Ganhe a partida no ${LEVEL}", + "descriptionFullComplete": "Ganhou a partida no ${LEVEL}", + "name": "Vitória no ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Derrote todas as ondas", + "descriptionComplete": "Derrotou todas as ondas", + "descriptionFull": "Derrote todas as ondas no ${LEVEL}", + "descriptionFullComplete": "Derrotou todas as ondas no ${LEVEL}", + "name": "Vitória no ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Complete todas as ondas", + "descriptionComplete": "Completou todas as ondas", + "descriptionFull": "Complete todas as ondas no ${LEVEL}", + "descriptionFullComplete": "Completou todas as ondas no ${LEVEL}", + "name": "Vitória no ${LEVEL}" + } + }, + "achievementsRemainingText": "Conquistas restantes:", + "achievementsText": "Conquistas", + "achievementsUnavailableForOldSeasonsText": "Desculpe, algumas conquistas não estão disponíveis em temporadas antigas.", + "addGameWindow": { + "getMoreGamesText": "Mais jogos...", + "titleText": "Adicionar jogo" + }, + "allowText": "Permitir", + "alreadySignedInText": "Sua conta está logada em outro dispositivo;\nMude de conta ou feche o jogo no seu\noutro dispositivo e tente novamente.", + "apiVersionErrorText": "Não pôde carregar o módulo ${NAME}; ele é destinado à versão ${VERSION_USED}; exigimos a ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" ativado apenas quando os fones estão conectados)", + "headRelativeVRAudioText": "Áudio para fones de RV", + "musicVolumeText": "Volume da música", + "soundVolumeText": "Volume do som", + "soundtrackButtonText": "Trilha Sonora", + "soundtrackDescriptionText": "(tocar sua própria música durante o jogo)", + "titleText": "Áudio" + }, + "autoText": "Auto", + "backText": "Voltar", + "banThisPlayerText": "Banir este jogador", + "bestOfFinalText": "Final de Melhor-de-${COUNT}", + "bestOfSeriesText": "Melhor série de ${COUNT}:", + "bestRankText": "Sua melhor pontuação é #${RANK}", + "bestRatingText": "Sua melhor classificação é ${RATING}", + "betaErrorText": "Essa versão beta não está mais ativa; por favor verifique a nova versão.", + "betaValidateErrorText": "Incapaz de validar versão beta. (sem conexão de internet?) ", + "betaValidatedText": "Versão beta Validada; Aproveite!", + "bombBoldText": "BOMBA", + "bombText": "Bomba", + "boostText": "Impulso", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} é configurado no próprio aplicativo.", + "buttonText": "botão", + "canWeDebugText": "Gostaria que BombSquad reportasse erros, falhas e \ninformações básicas de uso para o desenvolvedor?\n\nEstes dados não contêm informações pessoais e ajudam\na manter o jogo funcionando sem erros e livre de problemas.", + "cancelText": "Cancelar", + "cantConfigureDeviceText": "Desculpe, ${DEVICE} não é configurável.", + "challengeEndedText": "Este desafio acabou.", + "chatMuteText": "Silenciar bate-papo", + "chatMutedText": "Bate-papo silenciado", + "chatUnMuteText": "Reativar som do bate-papo", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Você deve completar \neste nível para continuar!", + "completionBonusText": "Bônus de conclusão", + "configControllersWindow": { + "configureControllersText": "Configurar controles", + "configureGamepadsText": "Configurar Controles", + "configureKeyboard2Text": "Configurar teclado P2", + "configureKeyboardText": "Configurar teclado", + "configureMobileText": "Usar dispositivos como controles", + "configureTouchText": "Configurar touchscreen", + "ps3Text": "Controles PS3", + "titleText": "Controles", + "wiimotesText": "Wiimotes", + "xbox360Text": "Controles Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Nota: o suporte ao controle varia conforme o dispositivo e a versão do Android.", + "pressAnyButtonText": "Aperte qualquer botão no controle\nque você deseja configurar...", + "titleText": "Configurar controles" + }, + "configGamepadWindow": { + "advancedText": "Avançado", + "advancedTitleText": "Configuração avançada dos controles", + "analogStickDeadZoneDescriptionText": "(ative isto se seu personagem 'escorrega' quando você solta o direcional)", + "analogStickDeadZoneText": "Área morta do analógico", + "appliesToAllText": "(serve para todos os controles deste tipo)", + "autoRecalibrateDescriptionText": "(ative isto se o seu personagem não se move na velocidade máxima)", + "autoRecalibrateDescriptionTextScale": 0.4, + "autoRecalibrateText": "Calibrar automaticamente o analógico", + "autoRecalibrateTextScale": 0.65, + "axisText": "eixo", + "clearText": "limpar", + "dpadText": "direcional", + "extraStartButtonText": "Botão Start Extra", + "ifNothingHappensTryAnalogText": "Se nada acontecer, tente mudar para o analógico.", + "ifNothingHappensTryDpadText": "Se nada acontecer, tente mudar para o botão direcional.", + "ignoreCompletelyDescriptionText": "(impedir este controle de afetar tanto o jogo quanto o menu)", + "ignoreCompletelyText": "Ignorar totalmente", + "ignoredButton1Text": "Botão 1 ignorado", + "ignoredButton2Text": "Botão 2 ignorado", + "ignoredButton3Text": "Botão 3 ignorado", + "ignoredButton4Text": "Botão 4 ignorado", + "ignoredButtonDescriptionText": "(use isto para evitar que os botões 'home' ou 'sync' afetem a UI)", + "ignoredButtonDescriptionTextScale": 0.4, + "ignoredButtonText": "Botão Ignorado", + "pressAnyAnalogTriggerText": "Aperte qualquer gatilho...", + "pressAnyButtonOrDpadText": "Aperte qualquer botão ou direcional...", + "pressAnyButtonText": "Aperte qualquer botão...", + "pressLeftRightText": "Aperte esquerda ou direita...", + "pressUpDownText": "Aperte cima ou baixo...", + "runButton1Text": "Executar botão 1", + "runButton2Text": "Executar botão 2", + "runTrigger1Text": "Executar gatilho 1", + "runTrigger2Text": "Executar gatilho 2", + "runTriggerDescriptionText": "(os gatilhos o permitem correr em diferentes velocidades)", + "secondHalfText": "Use isto para configurar a segunda metade\nde um controle que funciona\ncomo 2-em-1.", + "secondaryEnableText": "Ativar", + "secondaryText": "Controle secundário", + "startButtonActivatesDefaultDescriptionText": "(desative se o seu botão start está mais para um botão de menu)", + "startButtonActivatesDefaultText": "O botão Start ativa o widget padrão", + "startButtonActivatesDefaultTextScale": 0.65, + "titleText": "Configuração do controle", + "twoInOneSetupText": "Configuração do Controle 2-em-1", + "uiOnlyDescriptionText": "(impede que este controle entre em um jogo)", + "uiOnlyText": "Limitar o Uso ao Menu", + "unassignedButtonsRunText": "Todo Botão Não Definido Executa", + "unassignedButtonsRunTextScale": 0.8, + "unsetText": "", + "vrReorientButtonText": "Botão Reorientar VR" + }, + "configKeyboardWindow": { + "configuringText": "Configurando ${DEVICE}", + "keyboard2NoteScale": 0.7, + "keyboard2NoteText": "Nota: a maioria dos teclados só podem registrar\nalgumas teclas pressionadas de uma só vez, portanto\npode funcionar melhor se houver um segundo teclado\nconectado. Perceba que, mesmo nesse caso, você ainda\nprecisará definir teclas exclusivas para os dois jogadores." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Escala de ação do controle", + "actionsText": "Ações", + "buttonsText": "botões", + "dragControlsText": "< arraste controles para reposicioná-los >", + "joystickText": "joystick", + "movementControlScaleText": "Escala de movimento do controle", + "movementText": "Movimento", + "resetText": "Reiniciar", + "swipeControlsHiddenText": "Ocultar ícones de deslize", + "swipeInfoText": "O estilo 'Deslize' do controle leva um tempo para se acostumar, mas\nfaz com que seja mais fácil de jogar sem olhar para os controles.", + "swipeText": "deslize", + "titleText": "Configurar touchscreen", + "touchControlsScaleText": "Escala de Toque do Controle" + }, + "configureItNowText": "Configurar agora?", + "configureText": "Configurar", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsScale": 0.65, + "bestResultsText": "Para melhores resultados, é necessário uma rede Wi-Fi\nsem lag. Você pode melhorar o desempenho desligando outros\ndispositivos, jogando perto do seu roteador e conectando\no anfitrião do jogo à rede através do Ethernet.", + "explanationScale": 0.8, + "explanationText": "Para usar um telefone ou tablet como controle,\nbaixe gratuitamente o aplicativo \"${REMOTE_APP_NAME}\".\nDá para conectar quantos dispositivos quiser a ${APP_NAME} pelo Wi-Fi!", + "forAndroidText": "para Android:", + "forIOSText": "para iOS:", + "getItForText": "Baixe ${REMOTE_APP_NAME} para iOS na Apple App Store\nou para Android na Google Play Store ou na Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Usando dispositivos como controles:" + }, + "continuePurchaseText": "Continuar por ${PRICE}?", + "continueText": "Continuar", + "controlsText": "Controles", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Isso não se aplica às classificações de todos os tempos.", + "activenessInfoText": "Este multiplicador aumenta nos dias que você joga\ne diminui nos dias que você não joga.", + "activityText": "Atividade", + "campaignText": "Campanha", + "challengesInfoText": "Ganhe prêmios por completar minijogos.\n\nPrêmios e dificuldade aumentam toda\nvez que um desafio é concluído e\ndiminui quando expira ou é abandonado.", + "challengesText": "Desafios", + "currentBestText": "O Melhor do Momento", + "customText": "Personalizado", + "entryFeeText": "Entrada", + "forfeitConfirmText": "Abandonar este desafio?", + "forfeitNotAllowedYetText": "Este desafio ainda não pode ser abandonado.", + "forfeitText": "Abandonar", + "multipliersText": "Multiplicadores", + "nextChallengeText": "Próximo desafio", + "nextPlayText": "Próximo jogo", + "ofTotalTimeText": "de ${TOTAL}", + "playNowText": "Jogar agora", + "pointsText": "Pontos", + "powerRankingFinishedSeasonUnrankedText": "(acabou a temporada casual)", + "powerRankingNotInTopText": "(não está no top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} pts", + "powerRankingPointsMultText": "(x ${NUMBER} pts)", + "powerRankingPointsText": "${NUMBER} pts", + "powerRankingPointsToRankedText": "(${CURRENT} de ${REMAINING} pts)", + "powerRankingText": "Classificação Geral", + "prizesText": "Prêmios", + "proMultInfoText": "Jogadores com a versão ${PRO}\nrecebem um aumento de ${PERCENT}% nos pontos.", + "seeMoreText": "Mais...", + "skipWaitText": "Pular espera", + "timeRemainingText": "Tempo restante", + "titleText": "Cooperativo", + "toRankedText": "Para classificar", + "totalText": "total", + "tournamentInfoText": "Jogue para ser o melhor contra\noutros jogadores na sua liga.\n\nOs prêmios são dados aos melhores\njogadores quando o torneio acaba.", + "welcome1Text": "Bem-vindo à ${LEAGUE}. Você pode melhorar a sua\nclassificação de liga ao receber estrelas, ao obter\nconquistas e ao ganhar troféus em torneios.", + "welcome2Text": "Você também pode ganhar bilhetes ao fazer várias dessas atividades.\nOs bilhetes podem ser usados para desbloquear novos personagens,\nmapas e minijogos, entrar em torneios e muito mais.", + "yourPowerRankingText": "Sua classificação geral:" + }, + "copyOfText": "Cópia de ${NAME}", + "copyText": "Copiar", + "copyrightText": "© 2013 Eric Froemling", + "createAPlayerProfileText": "Criar um perfil de jogador?", + "createEditPlayerText": "", + "createText": "Criar", + "creditsWindow": { + "additionalAudioArtIdeasText": "Áudio adicional, Arte inicial e Ideias por ${NAME}", + "additionalMusicFromText": "Música adicional de ${NAME}", + "allMyFamilyText": "Toda a família e amigos que ajudaram nos testes", + "codingGraphicsAudioText": "Programação, Gráficos e Áudio por ${NAME}", + "creditsText": " Codificação, Gráficos, e Áudio por Eric Froemling\n \n Áudio Adicional, Trabalho de Arte Inicial, e Ideias por Raphael Suter\n \n Som & Música:\n\n${SOUND_AND_MUSIC}\n\n Música de Domínio Público via Musopen.com (um ótimo site para música clássica)\n Agradecimentos especialmente para o Exército dos EUA, Marinha e Bandas da Marinha.\n\n Um grande obrigado aos seguintes colaboradores do freesound.org para o uso de seus sons:\n${FREESOUND_NAMES}\n\n Agradecimentos Especiais:\n\n Todd, Laura, e Robert Froemling\n Todos os meus amigos e familiares que ajudaram a testar o jogo\n Quem quer que inventou o café\n\n Legal:\n\n${LEGAL_STUFF}\n\n www.froemling.net\n", + "languageTranslationsText": "Traduções:", + "legalText": "Legal:", + "publicDomainMusicViaText": "Musica de domínio público via ${NAME}", + "softwareBasedOnText": "Este software é baseado em parte do trabalho de ${NAME}", + "songCreditText": "${TITLE} Executada por ${PERFORMER}\nComposta por ${COMPOSER}, Arranjo por ${ARRANGER}, Publicada por ${PUBLISHER},\nCortesia de ${SOURCE}", + "soundAndMusicText": "Som e música:", + "soundsText": "Sons (${SOURCE}):", + "specialThanksText": "Agradecimentos especiais:", + "thanksEspeciallyToText": "Obrigado especialmente a ${NAME}", + "titleText": "Créditos do ${APP_NAME}", + "whoeverInventedCoffeeText": "Seja lá quem for o inventor do café" + }, + "currentStandingText": "Sua posição atual é #${RANK}", + "customizeText": "Personalizar...", + "deathsTallyText": "${COUNT} mortes", + "deathsText": "Mortes", + "debugText": "depurar", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Nota: recomenda-se que defina Configurações->Gráficos->Texturas para \"Alto\" ao testar isto.", + "runCPUBenchmarkText": "Testar desempenho da CPU", + "runGPUBenchmarkText": "Testar desempenho da GPU", + "runMediaReloadBenchmarkText": "Testar carregamento de mídia", + "runStressTestText": "Testar estabilidade", + "stressTestPlayerCountText": "Contador de jogadores", + "stressTestPlaylistDescriptionText": "Playlist teste de estabilidade", + "stressTestPlaylistNameText": "Nome da playlist", + "stressTestPlaylistTypeText": "Tipo de playlist", + "stressTestRoundDurationText": "Tempo da rodada", + "stressTestTitleText": "Teste de estabilidade", + "titleText": "Testes de performance e estabilidade", + "totalReloadTimeText": "Tempo total de carregamento: ${TIME} (veja relatório para detalhes)", + "unlockCoopText": "Desbloquear níveis cooperativos" + }, + "defaultFreeForAllGameListNameText": "Jogos Livre-para-Todos Padrão", + "defaultGameListNameText": "Playlist ${PLAYMODE} Padrão", + "defaultNewFreeForAllGameListNameText": "Meus Jogos Livre-para-Todos", + "defaultNewGameListNameText": "Minha playlist ${PLAYMODE}", + "defaultNewTeamGameListNameText": "Meus Jogos em Equipe", + "defaultTeamGameListNameText": "Jogos em Equipe Padrão", + "deleteText": "Excluir", + "demoText": "Teste", + "denyText": "Recusar", + "desktopResText": "Resolução da área de trabalho", + "difficultyEasyText": "Fácil", + "difficultyHardOnlyText": "Modo difícil apenas", + "difficultyHardText": "Difícil", + "difficultyHardUnlockOnlyText": "Esta fase só pode ser desbloqueada no modo difícil.\nVocê acha que aguenta o desafio!?!?!", + "directBrowserToURLText": "Por favor, direcione a seguinte URL para um navegador:", + "disableRemoteAppConnectionsText": "Desativar conexões do aplicativo BombSquad Remote", + "disableXInputDescriptionText": "Permite mais de 4 controles mas pode não funcionar bem.", + "disableXInputText": "Desativar XInput", + "doneText": "Concluído", + "drawText": "Empate", + "duplicateText": "Duplicar", + "editGameListWindow": { + "addGameText": "Adicionar\nJogo", + "cantOverwriteDefaultText": "Não é possível sobrescrever a playlist padrão!", + "cantSaveAlreadyExistsText": "Já existe uma playlist com este nome!", + "cantSaveEmptyListText": "Não é possível salvar uma playlist vazia!", + "editGameText": "Editar\nJogo", + "gameListText": "Lista de Jogos", + "listNameText": "Nome da playlist", + "nameText": "Nome", + "removeGameText": "Remover\nJogo", + "saveText": "Salvar lista", + "titleText": "Editor de playlist" + }, + "editProfileWindow": { + "accountProfileInfoText": "Este perfil especial tem nome e\nícone baseado na sua conta.\n\n${ICONS}\n\nCrie perfis personalizados para usar\nnomes diferentes ou ícones personalizados.", + "accountProfileText": "(perfil da conta)", + "availableText": "O nome \"${NAME}\" está disponível.", + "changesNotAffectText": "Nota: as alterações não afetarão personagens já contidos no jogo", + "characterText": "personagem", + "checkingAvailabilityText": "Verificando disponibilidade para \"${NAME}\"...", + "colorText": "cor", + "getMoreCharactersText": "Obter mais personagens...", + "getMoreIconsText": "Obter mais ícones...", + "globalProfileInfoText": "Garante-se que perfis globais tenham nomes\núnicos. Possuem também ícones personalizados.", + "globalProfileText": "(perfil global)", + "highlightText": "detalhe", + "iconText": "ícone", + "localProfileInfoText": "Os perfis locais não possuem ícones e não se garante que seus\nnomes sejam únicos. Atualize para um perfil global para\nreservar um nome único e adicionar um ícone personalizado.", + "localProfileText": "(perfil local)", + "nameDescriptionText": "Nome do jogador", + "nameText": "Nome", + "randomText": "aleatório", + "titleEditText": "Editar perfil", + "titleNewText": "Novo perfil", + "unavailableText": "\"${NAME}\" está indisponível; tente outro nome.", + "upgradeProfileInfoText": "Isso irá reservar o nome do seu jogador mundialmente\ne permitirá definir um ícone personalizado a ele.", + "upgradeToGlobalProfileText": "Atualizar para perfil global" + }, + "editProfilesAnyTimeText": "(você pode editar os perfis a qualquer momento em \"Configurações\")", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Você não pode excluir a trilha sonora padrão.", + "cantEditDefaultText": "Não é possível editar a trilha sonora padrão. Duplique ou crie uma nova.", + "cantEditWhileConnectedOrInReplayText": "Você não pode editar trilhas sonoras enquanto estiver conectado a um grupo ou em um replay.", + "cantOverwriteDefaultText": "Não é possível sobrescrever a trilha sonora padrão", + "cantSaveAlreadyExistsText": "Já existe uma trilha sonora com esse nome!", + "copyText": "Copiar", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Trilha sonora padrão", + "deleteConfirmText": "Excluir trilha sonora:\n\n'${NAME}'?", + "deleteText": "Excluir\nTrilha sonora", + "duplicateText": "Duplicar\nTrilha sonora", + "editSoundtrackText": "Editor de trilha sonora", + "editText": "Editar\nTrilha sonora", + "fetchingITunesText": "Buscando playlists do app de música", + "musicVolumeZeroWarning": "Aviso: o volume da música está zerado", + "nameText": "Nome", + "newSoundtrackNameText": "Minha trilha sonora ${COUNT}", + "newSoundtrackText": "Nova trilha sonora:", + "newText": "Nova\nTrilha sonora", + "selectAPlaylistText": "Selecione uma playlist", + "selectASourceText": "Fonte de música", + "soundtrackText": "Trilha Sonora", + "testText": "teste", + "titleText": "Trilhas sonoras", + "useDefaultGameMusicText": "Música padrão", + "useITunesPlaylistText": "Playlist do app de música", + "useMusicFileText": "Arquivo de música (mp3, etc)", + "useMusicFolderText": "Pasta de arquivos de música" + }, + "editText": "Editar", + "endText": "Fim", + "enjoyText": "Aproveite!", + "epicDescriptionFilterText": "${DESCRIPTION} em câmera lenta épica.", + "epicNameFilterText": "${NAME} épico(a)", + "errorAccessDeniedText": "acesso negado", + "errorOutOfDiskSpaceText": "pouco espaço em disco", + "errorText": "Erro", + "errorUnknownText": "erro desconhecido", + "exitGameText": "Sair do ${APP_NAME}?", + "exportSuccessText": "'${NAME}' foi exportado.", + "externalStorageText": "Armazenamento externo", + "failText": "Falhou", + "fatalErrorText": "Ops; algo está faltando ou está corrompido.\nPor favor, tente reinstalar BombSquad ou\nentre em contato ${EMAIL} para ajuda.", + "fileSelectorWindow": { + "titleFileFolderText": "Selecione um arquivo ou pasta", + "titleFileText": "Selecione um arquivo", + "titleFolderText": "Selecione uma pasta", + "useThisFolderButtonText": "Use esta pasta" + }, + "filterText": "Filtro", + "finalScoreText": "Pontuação final", + "finalScoresText": "Pontuações finais", + "finalTimeText": "Tempo final", + "finishingInstallText": "Terminando de instalar; aguarde...", + "fireTVRemoteWarningText": "* Para uma melhor experiência, use\njoysticks ou baixe o aplicativo\n'${REMOTE_APP_NAME}' no seu\ntelefone ou tablet.", + "firstToFinalText": "Primeiro-a-${COUNT} Final", + "firstToSeriesText": "Primeiro-a-${COUNT} Séries", + "fiveKillText": "MATOU CINCO!!!", + "flawlessWaveText": "Onda Perfeita!", + "fourKillText": "MORTE QUÁDRUPLA!!!", + "freeForAllText": "Livre-para-Todos", + "friendScoresUnavailableText": "Pontuação dos amigos indisponível.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Game ${COUNT} Líderes", + "gameListWindow": { + "cantDeleteDefaultText": "Você não pode excluir a playlist padrão!", + "cantEditDefaultText": "Você não pode editar a playlist padrão! Duplique ou crie uma nova.", + "cantShareDefaultText": "Você não pode compartilhar a playlist padrão.", + "deleteConfirmText": "Excluir ${LIST}?", + "deleteText": "Excluir\nPlaylist", + "duplicateText": "Duplicar\nPlaylist", + "editText": "Editar\nPlaylist", + "gameListText": "Lista de Games", + "newText": "Nova\nPlaylist", + "showTutorialText": "Mostrar Tutorial", + "shuffleGameOrderText": "Ordem de partida aleatória", + "titleText": "Personalizar playlists de ${TYPE}" + }, + "gameSettingsWindow": { + "addGameText": "Adicionar jogo" + }, + "gamepadDetectedText": "1 controle detectado.", + "gamepadsDetectedText": "${COUNT}controles detectados.", + "gamesToText": "${WINCOUNT} jogos para ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Lembre-se: qualquer dispositivo em uma sala pode ter\nmais de um jogador se você tiver outros controles.", + "aboutDescriptionText": "Use estas guias para montar uma sala.\n\nAs salas o permitem jogar com seus amigos\natravés de dispositivos diferentes.\n\nUse o botão ${PARTY} no canto superior direito\npara conversar e interagir com a sua sala.\n(em um controle, aperte ${BUTTON} quando estiver em um menu)", + "aboutText": "Sobre", + "addressFetchErrorText": "", + "appInviteMessageText": "${NAME} mandou ${COUNT} cupons ${APP_NAME}", + "appInviteSendACodeText": "Envie um código", + "appInviteTitleText": "Convite para testar ${APP_NAME}", + "bluetoothAndroidSupportText": "(funciona com qualquer dispositivo Android com Bluetooth)", + "bluetoothDescriptionText": "Criar/entrar em uma sala pelo Bluetooth:", + "bluetoothHostText": "Criar pelo Bluetooth", + "bluetoothJoinText": "Entrar pelo Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "verificando...", + "copyCodeConfirmText": "Código copiado para área de transferência.", + "copyCodeText": "Copiar código", + "dedicatedServerInfoText": "Para melhores resultados, inicie um servidor dedicado. Visite bombsquadgame.com/server para saber como.", + "disconnectClientsText": "Você está prestes a desconectar o(s) ${COUNT}\njogador(es) da sua sala. Tem certeza?", + "earnTicketsForRecommendingAmountText": "Os amigos ganharão ${COUNT} bilhetes ao experimentar o jogo\n(e você ganhará ${YOU_COUNT} por cada um que o fizer)", + "earnTicketsForRecommendingText": "Compartilhe o jogo\npara bilhetes grátis...", + "emailItText": "Enviar e-mail", + "favoritesSaveText": "Salvar como favorito", + "favoritesText": "Favoritos", + "freeCloudServerAvailableMinutesText": "Próximo servidor na nuvem grátis disponível em ${MINUTES} minutos.", + "freeCloudServerAvailableNowText": "Servidor na nuvem grátis disponível!", + "freeCloudServerNotAvailableText": "Nenhum servidor na nuvem grátis disponível.", + "friendHasSentPromoCodeText": "${COUNT} bilhetes do ${APP_NAME} mandados por ${NAME}", + "friendPromoCodeAwardText": "Você ganhará ${COUNT} bilhetes toda vez que for usado.", + "friendPromoCodeExpireText": "O código expira em ${EXPIRE_HOURS} horas e só funcionará para novos jogadores.", + "friendPromoCodeInstructionsText": "Para usar, abra ${APP_NAME} e vá até \"Configurações-> Avançado-> Inserir código\".\nVeja bombsquadgame.com para os links de download para todas as plataformas disponíveis.", + "friendPromoCodeRedeemLongText": "Pode ser resgatado por ${COUNT} bilhetes gratuitos por até ${MAX_USES} pessoas.", + "friendPromoCodeRedeemShortText": "Pode ser resgatado por ${COUNT} bilhetes no jogo.", + "friendPromoCodeWhereToEnterText": "(em \"Configurações-> Avançado-> Inserir código\")", + "getFriendInviteCodeText": "Obter código de convite", + "googlePlayDescriptionText": "Convidar jogadores do Google Play para a sua sala:", + "googlePlayInviteText": "Convidar", + "googlePlayReInviteText": "Há ${COUNT} jogador(es) do Google Play na sua sala\nque serão desconectados se você iniciar um novo convite.\nInclua-os neste novo convite para adicioná-los de volta.", + "googlePlaySeeInvitesText": "Ver convites", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android/versão Google Play)", + "hostPublicPartyDescriptionText": "Criar uma sala pública", + "hostingUnavailableText": "Host indisponível", + "inDevelopmentWarningText": "Nota:\n\nJogo em rede ainda é um novo recurso em desenvolvimento.\nPor enquanto, é altamente recomendável que todos\nos jogadores estejam na mesma rede Wi-Fi.", + "internetText": "Internet", + "inviteAFriendText": "Seus amigos ainda não têm o jogo? Convide para\nexperimentar e eles ganharão ${COUNT} bilhetes grátis.", + "inviteFriendsText": "Convidar amigos", + "joinPublicPartyDescriptionText": "Entrar em uma sala pública", + "localNetworkDescriptionText": "Entrar em uma Sala Próxima (LAN, Bluetooth, etc.)", + "localNetworkText": "Rede local", + "makePartyPrivateText": "Tornar minha sala privada", + "makePartyPublicText": "Tornar minha sala pública", + "manualAddressText": "Endereço", + "manualConnectText": "Conectar", + "manualDescriptionText": "Entrar em uma sala pelo endereço:", + "manualJoinSectionText": "Entrar pelo endereço", + "manualJoinableFromInternetText": "Consegue entrar pela internet?:", + "manualJoinableNoWithAsteriskText": "NÃO*", + "manualJoinableYesText": "SIM", + "manualRouterForwardingText": "*para resolver, tente configurar seu roteador para encaminhar a porta UDP ${PORT} para o seu endereço local", + "manualText": "Manual", + "manualYourAddressFromInternetText": "Seu endereço na internet:", + "manualYourLocalAddressText": "Seu endereço local:", + "nearbyText": "Próximo", + "noConnectionText": "", + "otherVersionsText": "(outras versões)", + "partyCodeText": "Código da Sala", + "partyInviteAcceptText": "Aceitar", + "partyInviteDeclineText": "Negar", + "partyInviteGooglePlayExtraText": "(veja a guia 'Google Play' na janela 'Montar Sala')", + "partyInviteIgnoreText": "Ignorar", + "partyInviteText": "${NAME} chamou você para\nentrar na sala dele(a)!", + "partyNameText": "Nome da sala", + "partyServerRunningText": "Sua Sala pública está funcionando.", + "partySizeText": "tamanho da sala", + "partyStatusCheckingText": "verificando estado...", + "partyStatusJoinableText": "agora podem entrar na sua sala pela internet", + "partyStatusNoConnectionText": "não foi possível conectar ao servidor", + "partyStatusNotJoinableText": "não podem entrar na sua sala pela internet", + "partyStatusNotPublicText": "sua sala não é pública", + "pingText": "latência", + "portText": "Porta", + "privatePartyCloudDescriptionText": "Salas privadas funcionam em servidores da nuvem dedicados; nenhuma configuração no roteador é requirida.", + "privatePartyHostText": "Crie uma Sala Privada", + "privatePartyJoinText": "Entre em uma Sala Pública", + "privateText": "Privado", + "publicHostRouterConfigText": "Isso pode requerir uma configuração de porta no seu roteador. Para uma opção mais simples, crie uma sala privada.", + "publicText": "Público", + "requestingAPromoCodeText": "Solicitando um código...", + "sendDirectInvitesText": "Enviar convites diretos", + "shareThisCodeWithFriendsText": "Compartilhe esse código com seus amigos:", + "showMyAddressText": "Mostrar meu endereço", + "startHostingPaidText": "Crie Agora por ${COST}", + "startHostingText": "Criar", + "startStopHostingMinutesText": "Você pode iniciar ou interromper a hospedagem de graça nos próximos ${MINUTES} minutos.", + "stopHostingText": "Interromper hospedagem", + "titleText": "Montar sala", + "wifiDirectDescriptionBottomText": "Se todos os dispositivos tiverem 'Wi-Fi Direct', poderão usar para encontrar\ne conectar um com o outro. Conectados todos os dispositivos, você pode montar salas\naqui usando a guia 'Rede Local', como em uma rede Wi-Fi comum.\n\nPara melhores resultados, o anfitrião do Wi-Fi Direct deverá ser também o anfitrião do grupo do ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct pode conectar dispositivos Android sem a necessidade\nde uma rede Wi-Fi. Funciona melhor a partir da versão Android 4.2.\n\nPara usar, abra as configurações de Wi-Fi e procure por 'Wi-Fi Direct'.", + "wifiDirectOpenWiFiSettingsText": "Abrir as configurações de Wi-Fi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(funciona entre todas as plataformas)", + "worksWithGooglePlayDevicesText": "(funciona com dispositivos rodando o jogo na versão Google Play)", + "youHaveBeenSentAPromoCodeText": "Mandaram um código promocional do ${APP_NAME} para você:" + }, + "getCoinsWindow": { + "coinDoublerText": "Duplicador de moedas", + "coinsText": "${COUNT}Moedas", + "freeCoinsText": "Moedas grátis", + "restorePurchasesText": "Restaurar Compras", + "titleText": "Pegue moedas" + }, + "getTicketsWindow": { + "freeText": "GRÁTIS!", + "freeTicketsText": "Cupons Grátis", + "inProgressText": "Uma transação está em andamento; tente de novo em um momento.", + "purchasesRestoredText": "Compras restauradas.", + "receivedTicketsText": "Você recebeu ${COUNT} cupons!", + "restorePurchasesText": "Restaurar Compras", + "ticketDoublerText": "Duplicador de Cupons", + "ticketPack1Text": "Pacote pequeno de bilhetes", + "ticketPack2Text": "Pacote médio de bilhetes", + "ticketPack3Text": "Pacote grande de bilhetes", + "ticketPack4Text": "Pacote jumbo de bilhetes", + "ticketPack5Text": "Pacote mamute de bilhete", + "ticketPack6Text": "Pacote ultimate de bilhetes", + "ticketsFromASponsorText": "Ganhe ${COUNT} bilhetes\nde um patrocinador", + "ticketsText": "${COUNT} bilhetes", + "titleText": "Obter bilhetes", + "unavailableLinkAccountText": "Desculpe, as compras não estão disponíveis nesta plataforma.\nComo solução alternativa, você pode vincular esta conta para\noutra conta de outra plataforma e fazer compras lá.", + "unavailableTemporarilyText": "Indisponível no momento; tente novamente mais tarde.", + "unavailableText": "Desculpe, não está disponível.", + "versionTooOldText": "Desculpe, esta versão do jogo é muito antiga; por favor, atualize.", + "youHaveShortText": "você tem ${COUNT}", + "youHaveText": "você possui ${COUNT} bilhetes" + }, + "googleMultiplayerDiscontinuedText": "Desculpe, o serviço multijogador do Google não está mais disponível.\nEstou trabalhando em uma substituição o mais rápido possível.\nAté lá, tente outro método de conexão.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Sempre", + "autoText": "Auto", + "fullScreenCmdText": "Tela cheia (Cmd-F)", + "fullScreenCtrlText": "Tela cheia (Ctrl-F)", + "gammaText": "Gama", + "highText": "Alto", + "higherText": "Mais alto", + "lowText": "Baixo", + "mediumText": "Médio", + "neverText": "Nunca", + "resolutionText": "Resolução", + "showFPSText": "Mostrar FPS", + "texturesText": "Texturas", + "titleText": "Gráficos", + "tvBorderText": "Borda da TV", + "verticalSyncText": "Sincronização vertical", + "visualsText": "Visuais" + }, + "helpWindow": { + "bombInfoText": "- Bomba - \nMais forte que socos, mas pode \ncausar graves ferimentos em você mesmo.\nPara melhores resultados, atire no\ninimigo antes do pavio acabar.", + "bombInfoTextScale": 0.6, + "canHelpText": "${APP_NAME} pode ajudar.", + "controllersInfoText": "Você pode jogar ${APP_NAME} com amigos em uma rede, ou todos \npodem jogar no mesmo dispositivo se tiverem controles suficientes.\n${APP_NAME} suporta muitos deles; você pode até mesmo usar\ncelulares como controle com o aplicativo '${REMOTE_APP_NAME}'.\nVeja as Configurações->Controles para mais informações.", + "controllersInfoTextFantasia": "Um jogador pode usar o controle remoto como controle, mas controles para games\nsão altamente recomendados. Você também pode usar dispositivos móveis\ncomo controles usando o app gratuito 'BombSquad Remote'.\nVeja 'Controles' em 'Configurações' para mais informações.", + "controllersInfoTextMac": "Um ou dois jogadores podem usar o teclado, mas BombSquad é melhor com gamepads.\nBombSquad pode usar gamepads USB, controles de PS3, controles de Xbox 360,\nWiimotes e dispositivos iOS/Android para controlar personagens. Esperamos que você tenha\nalguns desses acessíveis. Consulte 'Controles', em 'Configurações' para mais informações.", + "controllersInfoTextOuya": "Você pode usar controles OUYA, controles PS3, controles Xbox 360,\ne muitos outros controles USB e Bluetooth com BombSquad.\nVocê também pode usar dispositivos iOS e Android como controles via app\ngratuito 'BombSquad Remote'. Veja 'Controles' em 'Configurações' para detalhes.", + "controllersInfoTextScaleFantasia": 0.51, + "controllersInfoTextScaleMac": 0.58, + "controllersInfoTextScaleOuya": 0.63, + "controllersText": "Controles", + "controllersTextScale": 0.67, + "controlsSubtitleText": "O seu personagem bonitinho do ${APP_NAME} tem algumas ações básicas:", + "controlsSubtitleTextScale": 0.7, + "controlsText": "Controles", + "controlsTextScale": 1.4, + "devicesInfoText": "A versão VR de ${APP_NAME} pode ser jogada em rede com\na versão comum, portanto saquem seus celulares extras, tablets,\ne computadores e mandem ver. Pode ser útil conectar uma\nversão comum do jogo à versão VR para permitir que\npessoas de fora assistam a ação.", + "devicesText": "Dispositivos", + "friendsGoodText": "São sempre boas companhias. ${APP_NAME} é mais divertido com muitos\njogadores e pode suportar até 8 ao mesmo tempo, o que nos leva a:", + "friendsGoodTextScale": 0.62, + "friendsText": "Amigos", + "friendsTextScale": 0.67, + "jumpInfoText": "- Saltar -\nSalte por cima de buracos,\npara jogar coisas mais alto e\npara expressar sua alegria.", + "jumpInfoTextScale": 0.6, + "orPunchingSomethingExtraSpace": 0, + "orPunchingSomethingText": "Ou dar um soco em algo, jogar de um penhasco e explodir em pedacinhos com uma bomba grudenta.", + "pickUpInfoText": "- Pegar -\nAgarre bandeiras, inimigos ou\nqualquer coisa não aparafusada no\nchão. Aperte novamente para jogar.", + "pickUpInfoTextScale": 0.6, + "powerupBombDescriptionText": "Permite você lançar três bombas\nseguidas ao invés de uma só.", + "powerupBombNameText": "Tribombas", + "powerupCurseDescriptionText": "É melhor você ficar longe de um desses.\n...ou será que não?", + "powerupCurseNameText": "Maldição", + "powerupHealthDescriptionText": "Restaura toda a sua energia.\nVocê jamais teria adivinhado.", + "powerupHealthNameText": "Kit médico", + "powerupIceBombsDescriptionText": "Mais fraca que a normal, mas\ndeixa seus inimigos congelados\ne bem fragilizados.", + "powerupIceBombsNameText": "Criobombas", + "powerupImpactBombsDescriptionText": "Um pouco mais fraca que a normal\nregular, mas explode ao impacto.", + "powerupImpactBombsNameText": "Impactobombas", + "powerupLandMinesDescriptionText": "Estes vêm em pacotes de 3;\nÉ útil para proteger a base ou\ndeter inimigos correndo em sua direção.", + "powerupLandMinesNameText": "Minas", + "powerupPunchDescriptionText": "Deixa seus socos poderosos, \nrápidos, melhores, fortes.", + "powerupPunchNameText": "Luvas de Boxe", + "powerupShieldDescriptionText": "Absorve um pouco de dano para\nvocê não ter passar por isso.", + "powerupShieldNameText": "Escudo de Energia", + "powerupStickyBombsDescriptionText": "Gruda em tudo que toca.\nHilaridade segue.", + "powerupStickyBombsNameText": "Bombas Grudentas", + "powerupsSubtitleText": "É claro, nenhum jogo está completo sem poderes:", + "powerupsSubtitleTextScale": 0.8, + "powerupsText": "Poderes", + "punchInfoText": "- Soco -\nQuanto mais rápidos seus punhos,\nmais danos seus socos dão, então\nsaia correndo feito um louco.", + "punchInfoTextScale": 0.6, + "runInfoText": "- Correr -\nSegure QUALQUER botão para correr. Gatilhos ou botões de ombro funcionam bem se você tiver. \nCorrer o leva a lugares mais rápido, mas dificulta na hora de virar, então fique de olho nos penhascos.", + "someDaysText": "Tem dias que você só quer bater em algo. Ou então explodir coisas.", + "titleText": "Ajuda do ${APP_NAME}", + "toGetTheMostText": "Para aproveitar ao máximo este jogo, você precisará de:", + "welcomeText": "Bem-vindo ao ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} está navegando pelos menus como se não houvesse amanhã -", + "importPlaylistCodeInstructionsText": "Use o seguinte código para importar essa playlist:", + "importPlaylistSuccessText": "Playlist '${NAME}' importada ${TYPE}", + "importText": "Importar", + "importingText": "Importando...", + "inGameClippedNameText": "No jogo será\n\"${NAME}\"", + "installDiskSpaceErrorText": "ERRO: Não pôde concluir a instalação.\nVocê deve estar com pouco espaço no seu dispositivo.\nApague algumas coisas e tente novamente.", + "internal": { + "arrowsToExitListText": "aperte ${LEFT} ou ${RIGHT} para sair da lista", + "buttonText": "botão", + "cantKickHostError": "Você não pode expulsar o anfitrião.", + "chatBlockedText": "O bate-papo de ${NAME} está bloqueado por ${TIME} segundo(s).", + "connectedToGameText": "Conectou-se a '${NAME}'", + "connectedToPartyText": "Entrou na sala de ${NAME}!", + "connectingToPartyText": "Conectando...", + "connectionFailedHostAlreadyInPartyText": "A conexão falhou; o anfitrião está em outra sala.", + "connectionFailedPartyFullText": "Conexão falhou; a sala está cheia.", + "connectionFailedText": "A conexão falhou.", + "connectionFailedVersionMismatchText": "A conexão falhou; o anfitrião está rodando uma versão diferente do jogo.\nTenha certeza de que ambos estejam atualizados e tente novamente.", + "connectionRejectedText": "Conexão negada.", + "controllerConnectedText": "${CONTROLLER} conectado.", + "controllerDetectedText": "1 controle detectado.", + "controllerDisconnectedText": "${CONTROLLER} desconectado.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} desconectado. Por favor, tente novamente.", + "controllerForMenusOnlyText": "Este controle não pode ser usado para jogar; apenas para navegar pelo menu.", + "controllerReconnectedText": "${CONTROLLER} reconectado.", + "controllersConnectedText": "${COUNT} controles conectados.", + "controllersDetectedText": "${COUNT} controles detectados.", + "controllersDisconnectedText": "${COUNT} controles desconectados.", + "corruptFileText": "Arquivos corrompidos detectados. Tente reinstalar o jogo ou mande um e-mail para ${EMAIL}", + "errorPlayingMusicText": "Erro ao tocar música: ${MUSIC}", + "errorResettingAchievementsText": "Não foi possível reiniciar as conquistas online; por favor, tente novamente mais tarde.", + "hasMenuControlText": "${NAME} possui o controle do menu.", + "incompatibleNewerVersionHostText": "O anfitrião da sala usa uma versão diferente da sua.\nAtualize seu jogo e tente novamente.", + "incompatibleVersionHostText": "O anfitrião está rodando uma versão diferente do jogo.\nTenha certeza de que ambos estejam atualizados e tente de novo.", + "incompatibleVersionPlayerText": "${NAME} está rodando uma versão diferente do jogo.\nTenha certeza de que ambos estejam atualizados e tente novamente.", + "invalidAddressErrorText": "Erro: endereço invalido.", + "invalidNameErrorText": "Erro: nome inválido.", + "invalidPortErrorText": "Erro: porta inválida.", + "invitationSentText": "Convite enviado.", + "invitationsSentText": "${COUNT} convites enviados.", + "joinedPartyInstructionsText": "Alguém entrou na sua sala.\nAperte 'Jogar' para começar.", + "keyboardText": "Teclado", + "kickIdlePlayersKickedText": "Expulsando ${NAME} por não mostrar movimento.", + "kickIdlePlayersWarning1Text": "${NAME} será expulso em ${COUNT} segundos se continuar imóvel.", + "kickIdlePlayersWarning2Text": "(você pode desativar isto em Configurações-> Avançado)", + "leftGameText": "Saiu de '${NAME}'", + "leftPartyText": "Você saiu da sala de ${NAME}.", + "noMusicFilesInFolderText": "A pasta não contém arquivos de música.", + "playerJoinedPartyText": "${NAME} entrou na sala!", + "playerLeftPartyText": "${NAME} saiu da sala.", + "rejectingInviteAlreadyInPartyText": "Negando convite (já está em uma sala).", + "serverRestartingText": "Servidor reiniciando. Por favor, reconecte-se em um momento...", + "serverShuttingDownText": "Servidor está desligando...", + "signInErrorText": "Erro ao entrar.", + "signInNoConnectionText": "Não pôde entrar. (sem conexão à internet?)", + "teamNameText": "Time ${NAME}", + "telnetAccessDeniedText": "ERRO: o usuário não liberou acesso telnet.", + "timeOutText": "(tempo acaba em ${TIME} segundos)", + "touchScreenJoinWarningText": "Você entrou com o modo touchscreen.\nSe isso foi um erro, toque em 'Menu->Sair do jogo'.", + "touchScreenText": "Touchscreen", + "trialText": "teste", + "unableToResolveHostText": "Erro: não é possível resolver a fonte do anfitrião", + "unavailableNoConnectionText": "Isso não está disponível agora (sem conexão à internet?)", + "vrOrientationResetCardboardText": "Use para reiniciar a orientação do VR.\nPara jogar, você precisa de um controle externo.", + "vrOrientationResetText": "Orientação do VR reiniciada.", + "willTimeOutText": "(o tempo acabará se ficar imóvel)" + }, + "jumpBoldText": "SALTAR", + "jumpText": "Saltar", + "keepText": "Manter", + "keepTheseSettingsText": "Manter essas configurações?", + "keyboardChangeInstructionsText": "Pressione duas vezes o espaço para alterar os teclados.", + "keyboardNoOthersAvailableText": "Nenhum outro teclado disponível.", + "keyboardSwitchText": "Alterando teclado para \"${NAME}\".", + "kickOccurredText": "${NAME} foi expulso.", + "kickQuestionText": "Expulsar ${NAME}?", + "kickText": "Expulsar", + "kickVoteCantKickAdminsText": "O Administrador não pode ser expulso.", + "kickVoteCantKickSelfText": "Você não pode se expulsar.", + "kickVoteFailedNotEnoughVotersText": "Não há jogadores suficientes para uma votação.", + "kickVoteFailedText": "A votação para expulsão falhou.", + "kickVoteStartedText": "Uma votação para expulsar ${NAME} foi iniciada.", + "kickVoteText": "Votação para expulsar", + "kickVotingDisabledText": "A votação para expulsar está desativado.", + "kickWithChatText": "Digite ${YES} no bate-papo para sim e ${NO} para não.", + "killsTallyText": "${COUNT} abates", + "killsText": "Abates", + "kioskWindow": { + "easyText": "Fácil", + "epicModeText": "Modo épico", + "fullMenuText": "Menu Completo", + "hardText": "Difícil", + "mediumText": "Médio", + "singlePlayerExamplesText": "Exemplos de Um jogador / Cooperativo", + "versusExamplesText": "Exemplos de Versus" + }, + "languageSetText": "O idioma agora é \"${LANGUAGE}\".", + "lapNumberText": "Volta ${CURRENT}/${TOTAL}", + "lastGamesText": "(últimas ${COUNT} partidas)", + "leaderboardsText": "Classificação", + "league": { + "allTimeText": "Todos os Tempos", + "currentSeasonText": "Temporada atual (${NUMBER})", + "leagueFullText": "Liga ${NAME}", + "leagueRankText": "Classificação da liga", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga${SUFFIX}", + "seasonEndedDaysAgoText": "A temporada acabou ${NUMBER} dia(s) atrás.", + "seasonEndsDaysText": "A temporada acaba em ${NUMBER} dia(s).", + "seasonEndsHoursText": "A temporada acaba em ${NUMBER} hora(s).", + "seasonEndsMinutesText": "Temporada acaba em ${NUMBER} minuto(s).", + "seasonText": "Temporada ${NUMBER}", + "tournamentLeagueText": "Você deve alcançar a liga ${NAME} para entrar neste torneio.", + "trophyCountsResetText": "A contagem de troféus reiniciará na próxima temporada." + }, + "levelBestScoresText": "Melhores pontuações no ${LEVEL}", + "levelBestTimesText": "Melhores tempos no ${LEVEL}", + "levelFastestTimesText": "Melhores tempos em ${LEVEL}", + "levelHighestScoresText": "Melhores pontuações em ${LEVEL}", + "levelIsLockedText": "${LEVEL} está bloqueado.", + "levelMustBeCompletedFirstText": "${LEVEL} deve ser concluído primeiro.", + "levelText": "Nível ${NUMBER}", + "levelUnlockedText": "Nível desbloqueado!", + "livesBonusText": "Bônus de Vida", + "loadingText": "carregando", + "loadingTryAgainText": "Carregando; tente novamente daqui a pouco...", + "macControllerSubsystemBothText": "Ambos (não recomendado)", + "macControllerSubsystemClassicText": "Clássico", + "macControllerSubsystemDescriptionText": "Tente ativar isso se os controles não estiverem funcionando", + "macControllerSubsystemMFiNoteText": "Feito para iOS/Mac controle detectado ;\nvocê pode querer ativar isso em Configurações -> Controles", + "macControllerSubsystemMFiText": "Feito para iOS/Mac", + "macControllerSubsystemTitleText": "Suporte para controle", + "mainMenu": { + "creditsText": "Créditos", + "demoMenuText": "Menu de demonstração", + "endGameText": "Finalizar jogo", + "exitGameText": "Sair do jogo", + "exitToMenuText": "Sair para o menu?", + "howToPlayText": "Como jogar", + "justPlayerText": "(Somente ${NAME})", + "leaveGameText": "Sair do jogo", + "leavePartyConfirmText": "Deseja realmente sair da sala?", + "leavePartyText": "Sair da sala", + "leaveText": "Deixar", + "quitText": "Sair", + "resumeText": "Retomar", + "settingsText": "Configurações" + }, + "makeItSoText": "Aplicar", + "mapSelectGetMoreMapsText": "Obter mais mapas...", + "mapSelectText": "Selecionar...", + "mapSelectTitleText": "${GAME} mapas", + "mapText": "Mapa", + "maxConnectionsText": "Limite de Conexões", + "maxPartySizeText": "Tamanho Máximo da Sala", + "maxPlayersText": "Limite de jogadores", + "modeArcadeText": "Modo Arcade", + "modeClassicText": "Modo Clássico", + "modeDemoText": "Modo Demo", + "mostValuablePlayerText": "Jogador mais valioso", + "mostViolatedPlayerText": "Jogador Mais Violado", + "mostViolentPlayerText": "Jogador Mais Violento", + "moveText": "Mover", + "multiKillText": "MATOU ${COUNT}!!!", + "multiPlayerCountText": "${COUNT} jogadores", + "mustInviteFriendsText": "Nota: você deve convidar amigos no\npainel \"${GATHER}\" ou adicionar\ncontroles para jogar no multijogador.", + "nameBetrayedText": "${NAME} traiu ${VICTIM}.", + "nameDiedText": "${NAME} morreu.", + "nameKilledText": "${NAME} matou ${VICTIM}.", + "nameNotEmptyText": "Nome não pode estar vazio!", + "nameScoresText": "${NAME} fez um ponto!", + "nameSuicideKidFriendlyText": "${NAME} morreu acidentalmente.", + "nameSuicideText": "${NAME} cometeu suicídio.", + "nameText": "Nome", + "nativeText": "Nativo", + "newPersonalBestText": "Novo recorde pessoal!", + "newTestBuildAvailableText": "Uma nova versão de teste está disponível! (${VERSION} teste ${BUILD}).\nAdquira em ${ADDRESS}", + "newText": "Novo", + "newVersionAvailableText": "Uma nova versão de ${APP_NAME} está disponível! (${VERSION})", + "nextAchievementsText": "Próximas conquistas:", + "nextLevelText": "Próximo nível", + "noAchievementsRemainingText": "- nenhum", + "noContinuesText": "(sem continuar)", + "noExternalStorageErrorText": "Armazenamento externo não encontrado", + "noGameCircleText": "Erro: não conectado no GameCircle", + "noJoinCoopMidwayText": "Jogos cooperativos não podem ser afiliados no meio do caminho.", + "noProfilesErrorText": "Você não tem perfis de jogadores, então você está preso com '${NAME}'.\nVá para Configurações->Perfis de Jogador para criar um perfil.", + "noScoresYetText": "Ainda sem pontuação.", + "noThanksText": "Não, obrigado", + "noTournamentsInTestBuildText": "Atenção: As pontuações dos torneios desta compilação de teste serão ignoradas.", + "noValidMapsErrorText": "Nenhum mapa válido encontrado para este tipo de jogo.", + "notEnoughPlayersRemainingText": "Não há jogadores suficientes sobrando; saia e comece um novo jogo.", + "notEnoughPlayersText": "Você precisa de pelo menos ${COUNT} jogadores para começar o jogo!", + "notNowText": "Agora não", + "notSignedInErrorText": "Você deve iniciar sessão primeiro.", + "notSignedInGooglePlayErrorText": "Você deve iniciar sessão no Google Play primeiro.", + "notSignedInText": "sem sessão iniciada", + "nothingIsSelectedErrorText": "Nada foi selecionado!", + "numberText": "#${NUMBER}", + "offText": "Desligar", + "okText": "OK", + "onText": "Ligar", + "oneMomentText": "Um Momento...", + "onslaughtRespawnText": "${PLAYER} vai renascer na onda ${WAVE}", + "orText": "${A} ou ${B}", + "otherText": "Other...", + "outOfText": "(#${RANK} de ${ALL})", + "ownFlagAtYourBaseWarning": "Sua própria bandeira deve estar\nem sua base para fazer um ponto!", + "packageModsEnabledErrorText": "Não pode jogar em rede enquanto local-package-mods estiverem ativados (veja Configurações > Avançado)", + "partyWindow": { + "chatMessageText": "Mensagem do bate-papo", + "emptyText": "Sua sala está vazia", + "hostText": "(anfitrião)", + "sendText": "Enviar", + "titleText": "Sua sala" + }, + "pausedByHostText": "(pausado pelo anfitrião)", + "perfectWaveText": "Onda Perfeita!", + "pickUpBoldText": "PEGAR", + "pickUpText": "Pegar", + "playModes": { + "coopText": "Cooperativo", + "freeForAllText": "Todos contra todos", + "multiTeamText": "Equipes múltiplas", + "singlePlayerCoopText": "Um jogador / Cooperativo", + "teamsText": "Equipes" + }, + "playText": "Jogar", + "playWindow": { + "coopText": "Cooperativo", + "freeForAllText": "Livre-para-Todos", + "oneToFourPlayersText": "1-4 jogadores", + "teamsText": "Times", + "titleText": "Jogar", + "twoToEightPlayersText": "2-8 jogadores" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerDelayedJoinText": "${PLAYER} entrará no começo da próxima rodada.", + "playerInfoText": "Info. do jogador", + "playerLeftText": "${PLAYER} saiu da partida.", + "playerLimitReachedText": "Limite de ${COUNT} jogadores atingido; entrada não permitida.", + "playerLimitReachedUnlockProText": "Atualize para \"${PRO}\" na loja para jogar com mais de ${COUNT} jogadores.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Você não pode excluir o seu perfil.", + "deleteButtonText": "Excluir\nPerfil", + "deleteConfirmText": "Excluir '${PROFILE}'?", + "editButtonText": "Editar\nPerfil", + "explanationText": "(personalize nomes e aparências do jogador para esta conta)", + "newButtonText": "Novo\nPerfil", + "titleText": "Perfis de jogadores" + }, + "playerText": "Jogador", + "playlistNoValidGamesErrorText": "Esta playlist não contém nenhum jogo desbloqueado válido.", + "playlistNotFoundText": "playlist não encontrada", + "playlistText": "Playlist", + "playlistsText": "Playlists", + "pleaseRateText": "Se você está curtindo ${APP_NAME}, por favor, dê um tempinho\npara avaliar e comentar. Isso nos dá uma opinião útil\ne ajuda no desenvolvimento do jogo.\n\nobrigado!\n-eric", + "pleaseWaitText": "Por favor, aguarde...", + "pluginsDetectedText": "Novo(s) plugin(s) detected. Ative-os/configure-os em configurações.", + "pluginsText": "Plugins", + "practiceText": "Praticar", + "pressAnyButtonPlayAgainText": "Aperte qualquer botão para jogar novamente...", + "pressAnyButtonText": "Aperte qualquer botão para continuar...", + "pressAnyButtonToJoinText": "aperte qualquer botão para entrar...", + "pressAnyKeyButtonPlayAgainText": "Aperte qualquer tecla/botão para jogar novamente...", + "pressAnyKeyButtonText": "Aperte qualquer tecla/botão para continuar...", + "pressAnyKeyText": "Aperte qualquer tecla...", + "pressJumpToFlyText": "** Aperte saltar repetidamente para voar **", + "pressPunchToJoinText": "aperte SOCO para entrar...", + "pressToOverrideCharacterText": "aperte ${BUTTONS} para substituir o seu personagem", + "pressToSelectProfileText": "aperte ${BUTTONS} para selecionar um jogador", + "pressToSelectTeamText": "aperte ${BUTTONS} para selecionar uma equipe", + "profileInfoText": "Crie perfis para você e seus amigos\npara personalizar seus nomes, personagens e cores.", + "promoCodeWindow": { + "codeText": "Código", + "codeTextDescription": "Código Promocional", + "enterText": "Entrar" + }, + "promoSubmitErrorText": "Erro verifique sua internet", + "ps3ControllersWindow": { + "macInstructionsText": "Desligue a energia na parte traseira do seu PS3, verifique se\no Bluetooth do seu Mac está ativado, em seguida conecte o seu controle\nno seu Mac através de um cabo USB para emparelhar os dois. A partir daí, você\npode usar o botão home do controle para conectá-lo ao seu Mac\nseja por fio (USB) ou sem fio (Bluetooth).\n\nEm alguns Macs, uma senha pode ser solicitada ao emparelhar.\nSe isso acontecer, consulte o seguinte tutorial ou o Google para obter ajuda.\n\n\n\n\nOs controles de PS3 conectados sem fio devem aparecer na lista de\ndispositivos em Preferências do Sistema > Bluetooth. Você pode precisar remover\nda lista quando você quiser usar com o seu PS3 novamente.\n\nTambém certifique-se de desconectá-los do Bluetooth quando não estiver\nusando ou a bateria ficará acabando.\n\nBluetooth deve suportar até sete dispositivos conectados,\nembora a sua capacidade possa variar.", + "macInstructionsTextScale": 0.74, + "ouyaInstructionsText": "Para usar um controle de PS3 com seu OUYA, basta conectá-lo com um cabo USB\numa vez para emparelhá-lo. Fazer isso pode desconectar seus outros controles, por\nisso você deve, em seguida, reiniciar seu OUYA e desconectar o cabo USB.\n\nA partir de então você deve ser capaz de usar o botão HOME do controle para\nconectá-lo sem fio. Quando você terminar de jogar, pressione o botão HOME\npor 10 segundos para desligar o controle; caso contrário, pode permanecer ligado\ne desperdiçar bateria.", + "ouyaInstructionsTextScale": 0.74, + "pairingTutorialText": "vídeo tutorial do emparelhamento", + "titleText": "Usando Controles de PS3 com ${APP_NAME}:" + }, + "publicBetaText": "BETA PÚBLICO", + "punchBoldText": "SOCAR", + "punchText": "Socar", + "purchaseForText": "Compre por ${PRICE}", + "purchaseGameText": "Comprar jogo", + "purchasingText": "Comprando...", + "quitGameText": "Sair do ${APP_NAME}?", + "quittingIn5SecondsText": "Saindo em 5 segundos...", + "randomPlayerNamesText": "João,Maria,Anderson,Lucas,Roberto,César,Felipe,Pedro,Zézinho,Jaílson,Hélvio,Plínio,Clara,Lorena,Beatriz,Wandernilson,Marcos,Michele,Taís,Florentina,Tadeu,Teodoro,Gabriel,Joelma,Chimbinha,Lula,Dilma,Leonardo,Irene,Samanta,Gioconda,Guilhermina,Guilherme,Frederico,Bartolomeu,Dionísio,Diógenes,Haroldo,Ronaldinho,Ricardo,Selma,Bruna,Vanderlei,Danilo,Celso,Vitória,Denise,Samuel,Daniel,Gigi,Manuel,Wiz,Gretchen,Creusa,Chico,Leôncio,Leônidas,Washington,Cleusa,José,Joane,Severino,Casé,Carlos,Davi,Bianca,Clautila,Dafne,Jorge,Sandra,Armando,Basílio,Rochele,Camila,Débora,Rafael,Jonatan,Clodomiro,Clodovil,Vera,Simão,Raíssa,Toni,Tânia,Regina,Bela,Max,Maximiliano,Claudinei,Cláudio,Luciana,Anália,Aparecida,Marcelo,Flávio,Emílio,Tiago,Hebe,Ana,Beth,Gugu,Vítor,Nílton,Maurício,Marciano,Belquior,Clemente,Rosa,Rose,Rosemar,Gabriela,Sérgio,Antônio,Ben,Ivan,jamim,Abreu,Luís,Elton,Fabiana,Waldir,Wilson,Tainá,Tainara,Xuxa,Sacha,Teotônio,Téo,Valdirene,Laurindo,Priscila,Joaquim,Estevão,Gilmar,Erick,Gilson,Romário,Dunga,Ludmila,Luciano,Gilvan,Tamara,Carla,Zezé,Fernando,Fernanda,Adegesto,Acheropita,Anatalino,Lino,Araci,Marluci,Eusébio,Darcília,Dignatario,Ernesto,Cássio,Conrado,Fábio,Heitor,Ivan,Murilo,Andressa,Mateus,Otávio,Helena,atuamãe,Laís,Lavínia,Leila,Letícia,Nair,Henrique,Lara,Diogo,Diego,Geniclécio,Serafim,Lisa,Inri,Eusébio,Gerônimo,Bernardo,Bernadete,Henriete,Eliete,Fudêncio,Peruíbe,Tomás,Tomashedisso,Giovana,Prieto,Gabriely,Suélen,Jamily,Jamil,Geraldo,Nazareth,Forníco,Ícaro,Breno,Bruno,Cilmara,Nilza,Caio,Borges,Cleimara,Janeclécio,Iram,Tico,Teco,Genilson,Marlos,William,Nando,Nanda,Isabel,Jamal,Elias,Félix,Caroline,Carolina,Vilma,Rafaely,Tonho,Túnica,Miguel,Cona,Jones,Juan,Anastácio", + "randomText": "Aleatório", + "rankText": "Classificação", + "ratingText": "Avaliação", + "reachWave2Text": "Chegue a onda 2 para pontuar.", + "readyText": "pronto", + "recentText": "Recente", + "remainingInTrialText": "permanecendo em teste", + "remoteAppInfoShortText": "${APP_NAME} é mais divertido quando é jogado com família e amigos.\nConecte um ou mais controles de hardware ou instale o aplicativo \n${REMOTE_APP_NAME} em telefones ou tablets para usá-los \ncomo controles.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Posição do botão", + "button_size": "Tamanho do botão", + "cant_resolve_host": "Não foi possível localizar o anfitrião.", + "capturing": "Aguardando...", + "connected": "Conectado.", + "description": "Use seu telefone ou tablet como controle com BombSquad.\nAté 8 dispositivos podem se conectar de uma vez para uma loucura épica de multijogador local em uma TV ou tablet.", + "disconnected": "Desconectado pelo servidor.", + "dpad_fixed": "fixo", + "dpad_floating": "Móvel", + "dpad_position": "Posição do direcional", + "dpad_size": "Tamanho do direcional", + "dpad_type": "Tipo de direcional", + "enter_an_address": "Insira um endereço", + "game_full": "A partida está cheia ou não aceita conexões.", + "game_shut_down": "A partida foi fechada.", + "hardware_buttons": "Botões físicos", + "join_by_address": "Entrar por endereço...", + "lag": "Lag: ${SECONDS} segundos", + "reset": "Restaurar padrão", + "run1": "Correr 1", + "run2": "Correr 2", + "searching": "Procurando partidas...", + "searching_caption": "Clique em uma partida para entrar.\nVeja se está na mesma rede Wi-Fi do jogo.", + "start": "Iniciar", + "version_mismatch": "Versão incompatível.\nCertifique-se que o BombSquad e o BombSquad Remote\nestão atualizados e tente novamente." + }, + "removeInGameAdsText": "Desbloqueie \"${PRO}\" na loja para remover anúncios dentro do jogo.", + "renameText": "Renomear", + "replayEndText": "Terminar replay", + "replayNameDefaultText": "Replay do último jogo", + "replayReadErrorText": "Erro ao ler arquivo de replay.", + "replayRenameWarningText": "Renomeie \"${REPLAY}\" após uma partida caso queira salvá-lo; caso contrário será sobrescrito.", + "replayVersionErrorText": "Desculpe, este replay foi feito em uma versão\ndiferente do jogo e não pode ser usado.", + "replayWatchText": "Ver replay", + "replayWriteErrorText": "Erro ao gravar arquivo de replay.", + "replaysText": "Replays", + "reportPlayerExplanationText": "Use este e-mail para denunciar trapaças, linguagem inapropriada, ou outro comportamento ruim.\nPor favor, descreva abaixo:", + "reportThisPlayerCheatingText": "Trapaça", + "reportThisPlayerLanguageText": "Linguagem inapropriada", + "reportThisPlayerReasonText": "O que gostaria de denunciar?", + "reportThisPlayerText": "Denunciar este jogador", + "requestingText": "Solicitando...", + "restartText": "Reiniciar", + "retryText": "Tentar novamente", + "revertText": "Reverter", + "runText": "Correr", + "saveText": "Salvar", + "scanScriptsErrorText": "Erro ao ler scripts; verifica o Log para mais informações", + "scoreChallengesText": "Desafios de Pontuação", + "scoreListUnavailableText": "Lista de pontuação indisponível.", + "scoreText": "Pontuação", + "scoreUnits": { + "millisecondsText": "Milisegundos", + "pointsText": "Pontos", + "secondsText": "Segundos" + }, + "scoreWasText": "(foi ${COUNT})", + "selectText": "Selecionar", + "seriesWinLine1PlayerText": "VENCEU A", + "seriesWinLine1Scale": 0.65, + "seriesWinLine1TeamText": "VENCEU A", + "seriesWinLine1Text": "VENCEU A", + "seriesWinLine2Scale": 1.0, + "seriesWinLine2Text": "SÉRIE!", + "settingsWindow": { + "accountText": "Conta", + "advancedText": "Avançado", + "audioText": "Áudio", + "controllersText": "Controles", + "enterPromoCodeText": "Digite o Código Promocional", + "graphicsText": "Gráficos", + "kickIdlePlayersText": "Chutar Jogadores Ociosos", + "playerProfilesMovedText": "Nota: os perfis de jogador foram movidos à janela de Conta no menu principal.", + "playerProfilesText": "Perfis de Jogador", + "showPlayerNamesText": "Mostrar Nomes dos Jogadores", + "titleText": "Configurações" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(um teclado simples para edição de texto)", + "alwaysUseInternalKeyboardText": "Sempre usar o teclado interno", + "benchmarksText": "Testes de desempenho", + "disableCameraGyroscopeMotionText": "Desativar o movimento do giroscópio da câmera", + "disableCameraShakeText": "Desabilitar o shake da câmera", + "disableThisNotice": "(você pode desativar este aviso em configurações avançadas)", + "enablePackageModsDescriptionText": "(ativa habilidades de modificação do jogo mas desabilita jogos em LAN)", + "enablePackageModsText": "Ativar modificações locais do jogo", + "enterPromoCodeText": "Inserir código", + "forTestingText": "Nota: esses valores são somente para teste e serão perdidos assim que o aplicativo for fechado.", + "helpTranslateText": "As traduções do ${APP_NAME} são sustentadas pelo\nesforço público. Se você gostaria de ajudar ou corrigir\numa tradução, siga o link a seguir. Agradeço desde já!", + "kickIdlePlayersText": "Expulsar jogadores ausentes", + "kidFriendlyModeText": "Modo para crianças (violência reduzida, etc)", + "languageText": "Idioma", + "moddingGuideText": "Guia de modificação", + "mustRestartText": "Você deve reiniciar o jogo para a modificação ter efeito.", + "netTestingText": "Teste de conexão", + "resetText": "Redefinir", + "showBombTrajectoriesText": "Mostrar trajetórias da bomba", + "showPlayerNamesText": "Mostrar nomes dos jogadores", + "showUserModsText": "Mostrar Pasta de Modificações", + "titleText": "Avançado", + "translationEditorButtonText": "Editor de tradução do ${APP_NAME}", + "translationFetchErrorText": "estado da tradução indisponível", + "translationFetchingStatusText": "verificando estado da tradução...", + "translationInformMe": "Informe quando meu idioma precisar de atualizações", + "translationNoUpdateNeededText": "o idioma atual está atualizado; woohoo!", + "translationUpdateNeededText": "** o idioma atual precisa de atualizações!! **", + "vrTestingText": "Teste de RV" + }, + "shareText": "Compartilhar", + "sharingText": "Compartilhando...", + "showText": "Mostrar", + "signInForPromoCodeText": "Você deve iniciar uma sessão em uma conta para que os códigos promocionais se efetuem.", + "signInWithGameCenterText": "Para usar uma conta Game Center,\ninicie a sessão com o aplicativo Game Center.", + "singleGamePlaylistNameText": "Somente ${GAME}", + "singlePlayerCountText": "1 jogador", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Seleção do personagem", + "Chosen One": "O Escolhido", + "Epic": "Partidas no modo épico", + "Epic Race": "Corrida épica", + "FlagCatcher": "Capture a bandeira", + "Flying": "Pensamentos felizes", + "Football": "Futebol americano", + "ForwardMarch": "Assalto", + "GrandRomp": "Conquista", + "Hockey": "Hóquei", + "Keep Away": "Fique longe", + "Marching": "Runaround", + "Menu": "Menu principal", + "Onslaught": "Embate", + "Race": "Corrida", + "Scary": "Rei da colina", + "Scores": "Tela de pontuação", + "Survival": "Eliminatória", + "ToTheDeath": "Mata-mata", + "Victory": "Tela de pontuação final" + }, + "spaceKeyText": "espaço", + "statsText": "Estatísticas", + "storagePermissionAccessText": "É necessário acesso ao armazenamento", + "store": { + "alreadyOwnText": "Você já possui ${NAME}!", + "bombSquadProDescriptionText": "• Duplica os tickets ganhos em desafios\n• Remove anúncios no jogo\n• Adiciona ${COUNT} tickets bônus\n• +${PERCENT}% na pontuação da liga de bônus\n• Desbloqueia níveis coop '${INF_ONSLAUGHT}'\n e '${INF_RUNAROUND}'", + "bombSquadProFeaturesText": "Recursos:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Remove anúncios no jogo e nas telas\n• Desbloqueia mais configurações do jogo\n• Também inclui:", + "buyText": "Comprar", + "charactersText": "Personagens", + "comingSoonText": "Em breve...", + "extrasText": "Extras", + "freeBombSquadProText": "BombSquad agora é gratuito, mas já que você já tinha comprado ele\nvocê está recebendo BombSquad Pro e ${COUNT} tickets\ncomo um obrigado.\nDivirta-se com os novos recursos, e obrigado pelo suporte.\n-Eric", + "gameUpgradesText": "Upgrades de Jogo", + "getCoinsText": "Pegue moedas", + "holidaySpecialText": "Especial de feriado", + "howToSwitchCharactersText": "(vá para \"${SETTINGS} -> ${PLAYER_PROFILES}\" para atribuir e personalizar personagens)", + "howToUseIconsText": "(crie perfis globais - na janela de conta - para usá-los)", + "howToUseMapsText": "(use estes mapas em suas próprias playlist de equipes/todos contra todos)", + "iconsText": "Ícones", + "loadErrorText": "Não foi possível carregar a página.\nVerifique a sua conexão com a internet.", + "loadingText": "carregando", + "mapsText": "Mapas", + "miniGamesText": "Minijogos", + "oneTimeOnlyText": "(somente uma vez)", + "purchaseAlreadyInProgressText": "Uma compra deste item já está em progresso.", + "purchaseConfirmText": "Comprar ${ITEM}?", + "purchaseNotValidError": "A compra não é valida.\nEntre em contato com ${EMAIL} se isso foi um erro.", + "purchaseText": "Comprar", + "saleBundleText": "Venda de pacotes!", + "saleExclaimText": "Promoção!", + "salePercentText": "(${PERCENT}% de desconto)", + "saleText": "PROMOÇÃO", + "searchText": "Buscar", + "teamsFreeForAllGamesText": "Jogos em equipes / Todos contra todos", + "totalWorthText": "*** Apenas ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Atualizar?", + "winterSpecialText": "Especial de Inverno", + "youOwnThisText": "- você tem isso -" + }, + "storeDescriptionText": "Loucura total com até 8 jogadores!\n\nExploda seus amigos (ou o computador) em um torneio de minijogos desafiadores como Capture a bandeira, Hóquei bombástico e Mata-mata em câmera lenta épica!\n\nControles normais e controles externos possibilitam jogar com até 8 pessoas no mesmo aparelho; você também pode usar outros aparelhos como controles externos usando o aplicativo grátis ‘BombSquad Remote’!\n\nCuidado com as bombas!\n\nDê uma olhada em www.froemling.net/bombsquad para ficar ligado nas novidades.", + "storeDescriptions": { + "blowUpYourFriendsText": "Exploda seus amigos.", + "competeInMiniGamesText": "Compita em minijogos que vão de corridas a voos.", + "customize2Text": "Personalize personagens, minijogos e até mesmo a trilha sonora.", + "customizeText": "Personalize personagens e crie sua própria playlist de minijogos.", + "sportsMoreFunText": "Esportes são mais divertidos com explosivos.", + "teamUpAgainstComputerText": "Una-se a outros contra o computador." + }, + "storeText": "Loja", + "submitText": "Valor", + "submittingPromoCodeText": "Enviando código promocional...", + "teamNamesColorText": "Nome/cores das equipes...", + "teamsText": "Times", + "telnetAccessGrantedText": "Acesso ao Telnet ativado.", + "telnetAccessText": "Acesso ao Telnet detectado; permitir?", + "testBuildErrorText": "Esta versão de teste não é mais compatível; por favor, confira uma nova versão.", + "testBuildText": "Versão de teste", + "testBuildValidateErrorText": "Não foi possível validar esta versão. (sem conexão com a internet?)", + "testBuildValidatedText": "Versão de teste validada; divirta-se!", + "thankYouText": "Obrigado pelo seu apoio! Aproveite o jogo!!", + "threeKillText": "MATOU TRÊS!!", + "timeBonusText": "Bônus de tempo", + "timeElapsedText": "Tempo Decorrido", + "timeExpiredText": "Tempo Expirado", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Dica", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Melhores amigos", + "tournamentCheckingStateText": "Verificando o estado do torneio; espere um momento...", + "tournamentEndedText": "Este torneio foi finalizado. Um novo começará em breve.", + "tournamentEntryText": "Entrada para o torneio", + "tournamentResultsRecentText": "Resultados recentes do torneio.", + "tournamentStandingsText": "Classificação do torneio", + "tournamentText": "Torneio", + "tournamentTimeExpiredText": "O tempo do torneio expirou.", + "tournamentsText": "Torneios", + "translations": { + "characterNames": { + "Agent Johnson": "Agente Johnson", + "B-9000": "B-9000", + "Bernard": "Bernardo", + "Bones": "Bones", + "Butch": "Butch", + "Easter Bunny": "Coelho de Páscoa", + "Flopsy": "Flopsy", + "Frosty": "Frosty", + "Gretel": "Gretel", + "Grumbledorf": "Grumblodor", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Sotavento", + "Lucky": "Lucky", + "Mel": "Mel", + "Middle-Man": "Homenzinho", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Papai Noel", + "Snake Shadow": "Serpente sombria", + "Spaz": "Spaz", + "Taobao Mascot": "Mascote da Taobao", + "Todd": "Teddy", + "Todd McBurton": "Todd McBurton", + "Xara": "Zara", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Infinito\nAtaque Violento", + "Infinite\nRunaround": "Evasiva\nInfinita", + "Onslaught\nTraining": "Embate\nTreinamento", + "Pro\nFootball": "Futebol\nPro", + "Pro\nOnslaught": "Embate\nPro", + "Pro\nRunaround": "Evasiva\nPro", + "Rookie\nFootball": "Futebol\nAmador", + "Rookie\nOnslaught": "Embate\nAmador", + "The\nLast Stand": "O último\na ficar em pé", + "Uber\nFootball": "Futebol\nElite", + "Uber\nOnslaught": "Embate\nElite", + "Uber\nRunaround": "Evasiva\nElite" + }, + "coopLevelNames": { + "${GAME} Training": "Treinamento de ${GAME}", + "Infinite ${GAME}": "${GAME} Infinito", + "Infinite Onslaught": "Embate Infinito", + "Infinite Runaround": "Evasiva Infinita", + "Onslaught": "Embate Infinito", + "Onslaught Training": "Embate Treinamento", + "Pro ${GAME}": "${GAME} Pro", + "Pro Football": "Futebol americano Pro", + "Pro Onslaught": "Embate Pro", + "Pro Runaround": "Evasiva Pro", + "Rookie ${GAME}": "${GAME} Amador", + "Rookie Football": "Futebol Americano Amador", + "Rookie Onslaught": "Embate Amador", + "Runaround": "Evasiva Infinita", + "The Last Stand": "O Sobrevivente", + "Uber ${GAME}": "${GAME} Elite", + "Uber Football": "Futebol Americano Elite", + "Uber Onslaught": "Embate Elite", + "Uber Runaround": "Evasiva Elite" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Seja o escolhido por um período de tempo para vencer.\nElimine o escolhido para se tornar ele.", + "Bomb as many targets as you can.": "Bombardeie o máximo de alvos que você puder.", + "Carry the flag for ${ARG1} seconds.": "Carregue a bandeira por ${ARG1} segundos.", + "Carry the flag for a set length of time.": "Carregue a bandeira por um tempo determinado.", + "Crush ${ARG1} of your enemies.": "Esmague ${ARG1} de seus inimigos.", + "Defeat all enemies.": "Derrote todos inimigos.", + "Dodge the falling bombs.": "Esquive das bombas caindo.", + "Final glorious epic slow motion battle to the death.": "Épica gloriosa batalha final até a morte em câmera lenta.", + "Gather eggs!": "Recolha os ovos!", + "Get the flag to the enemy end zone.": "Pegue a bandeira no final da zona inimiga.", + "How fast can you defeat the ninjas?": "Quão rápido você pode derrotar os ninjas?", + "Kill a set number of enemies to win.": "Mate um determinado número de inimigos para vencer.", + "Last one standing wins.": "Último em pé vence.", + "Last remaining alive wins.": "Último sobrevivente vence.", + "Last team standing wins.": "Última equipe de pé vence.", + "Prevent enemies from reaching the exit.": "Impeça que os inimigos alcancem a saída.", + "Reach the enemy flag to score.": "Alcance a bandeira inimiga para marcar.", + "Return the enemy flag to score.": "Retorne a bandeira inimiga para marcar.", + "Run ${ARG1} laps.": "Corra ${ARG1} voltas.", + "Run ${ARG1} laps. Your entire team has to finish.": "Corra ${ARG1} voltas. Toda a sua equipe precisa terminar.", + "Run 1 lap.": "Corra 1 volta.", + "Run 1 lap. Your entire team has to finish.": "Corra 1 volta. Toda a sua equipe precisa terminar.", + "Run real fast!": "Corra muito rápido!", + "Score ${ARG1} goals.": "Marque ${ARG1} gols.", + "Score ${ARG1} touchdowns.": "Marque ${ARG1} touchdowns.", + "Score a goal": "Marque um gol", + "Score a goal.": "Marque um gol.", + "Score a touchdown.": "Marque um touchdown.", + "Score some goals.": "Marque alguns gols.", + "Secure all ${ARG1} flags.": "Proteja todas as ${ARG1} bandeiras.", + "Secure all flags on the map to win.": "Proteja todas as bandeiras no mapa para vencer.", + "Secure the flag for ${ARG1} seconds.": "Proteja a bandeira por ${ARG1} segundos.", + "Secure the flag for a set length of time.": "Proteja a bandeira por um determinado período de tempo.", + "Steal the enemy flag ${ARG1} times.": "Roube a bandeira do inimigo ${ARG1} vezes.", + "Steal the enemy flag.": "Roube a bandeira do inimigo.", + "There can be only one.": "Só pode existir um.", + "Touch the enemy flag ${ARG1} times.": "Toque a bandeira inimiga ${ARG1} vezes.", + "Touch the enemy flag.": "Toque a bandeira inimiga.", + "carry the flag for ${ARG1} seconds": "carregue a bandeira por ${ARG1} segundos", + "kill ${ARG1} enemies": "mate ${ARG1} inimigos", + "last one standing wins": "último em pé vence", + "last team standing wins": "última equipe de pé vence", + "return ${ARG1} flags": "retorne ${ARG1} bandeiras.", + "return 1 flag": "retorne 1 bandeira", + "run ${ARG1} laps": "corra ${ARG1} voltas", + "run 1 lap": "corra 1 volta", + "score ${ARG1} goals": "marque ${ARG1} gols", + "score ${ARG1} touchdowns": "marque ${ARG1} touchdowns", + "score a goal": "marque um gol", + "score a touchdown": "marque um touchdown", + "secure all ${ARG1} flags": "Proteja todas ${ARG1} bandeiras", + "secure the flag for ${ARG1} seconds": "Proteja a bandeira por ${ARG1} segundos", + "touch ${ARG1} flags": "toque ${ARG1} bandeiras", + "touch 1 flag": "toque uma bandeira" + }, + "gameNames": { + "Assault": "Assalto", + "Capture the Flag": "Capture a Bandeira", + "Chosen One": "O Escolhido", + "Conquest": "Conquista", + "Death Match": "Mata-mata", + "Easter Egg Hunt": "Caça aos ovos de Páscoa", + "Elimination": "Eliminatória", + "Football": "Futebol americano", + "Hockey": "Hóquei", + "Keep Away": "Fique longe", + "King of the Hill": "Rei da colina", + "Meteor Shower": "Chuva de meteoros", + "Ninja Fight": "Luta ninja", + "Onslaught": "Embate", + "Race": "Corrida", + "Runaround": "Evasiva", + "Target Practice": "Treino de Alvo", + "The Last Stand": "O Sobrevivente" + }, + "inputDeviceNames": { + "Keyboard": "Teclado", + "Keyboard P2": "Teclado P2" + }, + "languages": { + "Arabic": "Árabe", + "Belarussian": "Bielorrusso", + "Chinese": "Chinês Simplificado", + "ChineseTraditional": "Chinês Tradicional", + "Croatian": "Croata", + "Czech": "Tcheco", + "Danish": "Dinamarquês", + "Dutch": "Holandês", + "English": "Inglês", + "Esperanto": "Esperanto", + "Finnish": "Finlandês", + "French": "Francês", + "German": "Alemão", + "Gibberish": "Embromation", + "Greek": "Grego", + "Hindi": "Hindu", + "Hungarian": "Húngaro", + "Indonesian": "Indonésio", + "Italian": "Italiano", + "Japanese": "Japonês", + "Korean": "Coreano", + "Persian": "Persa", + "Polish": "Polonês", + "Portuguese": "Português", + "Romanian": "Romeno", + "Russian": "Russo", + "Serbian": "Sérvio", + "Slovak": "Eslovaco", + "Spanish": "Espanhol", + "Swedish": "Sueco", + "Turkish": "Turco", + "Ukrainian": "Ucraniano", + "Venetian": "Veneziano", + "Vietnamese": "Vietnamita" + }, + "leagueNames": { + "Bronze": "Bronze", + "Diamond": "Diamante", + "Gold": "Ouro", + "Silver": "Prata" + }, + "mapsNames": { + "Big G": "Grande G", + "Bridgit": "Bridgit", + "Courtyard": "Pátio", + "Crag Castle": "Castelo Crag", + "Doom Shroom": "Cogumelo da Morte", + "Football Stadium": "Estádio de Futebol", + "Happy Thoughts": "Pensamentos felizes", + "Hockey Stadium": "Estádio de hóquei", + "Lake Frigid": "Lago Frígido", + "Monkey Face": "Cara de macaco", + "Rampage": "Tumulto", + "Roundabout": "Evasiva", + "Step Right Up": "Degrau acima", + "The Pad": "The Pad", + "Tip Top": "Tip Top", + "Tower D": "Torre D", + "Zigzag": "Ziguezague" + }, + "playlistNames": { + "Just Epic": "Somente épico", + "Just Sports": "Somente esportes" + }, + "promoCodeResponses": { + "invalid promo code": "código promocional inválido" + }, + "scoreNames": { + "Flags": "Bandeiras", + "Goals": "Gols", + "Score": "Placar", + "Survived": "Sobreviveu", + "Time": "Tempo", + "Time Held": "Tempo realizado" + }, + "serverResponses": { + "A code has already been used on this account.": "Um código já está sendo usado nesta conta.", + "A reward has already been given for that address.": "Uma recompensa já foi dada para este endereço.", + "Account linking successful!": "A conta foi vinculada com êxito!", + "Account unlinking successful!": "Conta desvinculada com êxito!", + "Accounts are already linked.": "As contas já estão vinculadas.", + "An error has occurred; (${ERROR})": "Ocorreu um erro; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Ocorreu um erro; entre em contato com a assistência. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Ocorreu um erro; por favor, entre em contato com support@froemling.net.", + "An error has occurred; please try again later.": "Aconteceu um erro; tente novamente mais tarde.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Tem certeza de que deseja vincular estas contas?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nIsso não poderá ser desfeito!", + "BombSquad Pro unlocked!": "BombSquado Pro desbloqueado!", + "Can't link 2 accounts of this type.": "Não é possível vincular duas contas deste tipo.", + "Can't link 2 diamond league accounts.": "Não é possível vincular duas contas de liga diamante.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Impossível vincular; ultrapassaria o máximo de ${COUNT} contas vinculadas.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Trapaça detectada; pontuação e prêmios suspensos por ${COUNT} dias.", + "Could not establish a secure connection.": "Não foi possível estabelecer uma conexão segura.", + "Daily maximum reached.": "Máximo diário atingido.", + "Entering tournament...": "Entrando no torneio...", + "Invalid code.": "Código invalido.", + "Invalid payment; purchase canceled.": "Pagamento inválido; compra cancelada.", + "Invalid promo code.": "Código promocional invalido.", + "Invalid purchase.": "Compra inválida.", + "Invalid tournament entry; score will be ignored.": "Entrada errada de treinamento; pontuação será ignorada.", + "Item unlocked!": "Item unlocked", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "VINCULAÇÃO NEGADA. ${ACCOUNT} contém\ndados significativos que TODOS SERÃO PERDIDOS.\nVocê pode vincular na ordem oposta se desejar\n(e em vez disso perca os dados desta conta)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Vincular conta ${ACCOUNT} com essa conta?\nTodo o progresso em ${ACCOUNT} será perdido.\nIsso não pode ser desfeito. Tem certeza?", + "Max number of playlists reached.": "Número máximo de playlists alcançado.", + "Max number of profiles reached.": "Número máximo de perfis alcançado.", + "Maximum friend code rewards reached.": "Máximo de recompensas de códigos de amigos atingido.", + "Message is too long.": "A mensagem é muito longa.", + "Profile \"${NAME}\" upgraded successfully.": "Perfil \"${NAME}\" atualizado com sucesso.", + "Profile could not be upgraded.": "Perfil não pôde ser criado.", + "Purchase successful!": "Compra feita com êxito!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Recebeu ${COUNT} tickets por entrar.\nVolte amanhã para receber ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "A funcionalidade do servidor não é mais compatível nesta versão do jogo;\nPor favor, atualize-o para uma versão mais recente.", + "Sorry, there are no uses remaining on this code.": "Desculpe, não há mais usos remanescentes neste código.", + "Sorry, this code has already been used.": "Desculpe, este código já foi usado.", + "Sorry, this code has expired.": "Desculpe, este código já expirou.", + "Sorry, this code only works for new accounts.": "Desculpe, este código só funciona para novas contas.", + "Temporarily unavailable; please try again later.": "Não disponível; por favor, tente novamente mais tarde.", + "The tournament ended before you finished.": "O torneio acabou antes de você finalizar.", + "This account cannot be unlinked for ${NUM} days.": "Esta conta não pode ser desvinculada por ${NUM} dias.", + "This code cannot be used on the account that created it.": "Este código não pode ser usado pela conta que o criou.", + "This is currently unavailable; please try again later.": "Isso está atualmente indisponível; por favor tente mais tarde.", + "This requires version ${VERSION} or newer.": "Isso requer a versão ${VERSION} ou novo.", + "Tournaments disabled due to rooted device.": "Torneios desativados devido ao dispositivo estar rooteado.", + "Tournaments require ${VERSION} or newer": "Torneios requerem ${VERSION} ou mais recente", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Desvincular ${ACCOUNT} dessa conta?\nTodo o progresso em ${ACCOUNT} será reiniciado.\n(exceto por conquistas em alguns casos)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ATENÇÃO: denúncias sobre você estar usando hack foram feitas.\nContas que usam hack serão banidas. Por favor, jogue limpo.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Gostaria de vincular a sua conta de dispositivo com esta?\n\nA sua conta de dispositivo é ${ACCOUNT1}\nEsta conta é ${ACCOUNT2}\n\nIsso permitirá que você mantenha seu progresso atual.\nAviso: isso não pode ser desfeito!", + "You already own this!": "Você já possui isso.", + "You can join in ${COUNT} seconds.": "Você poderá entrar em ${COUNT} segundos", + "You don't have enough tickets for this!": "Você não tem bilhetes suficientes para isso!", + "You don't own that.": "Você não comprou isso.", + "You got ${COUNT} tickets!": "Você obteve ${COUNT} tickets!", + "You got a ${ITEM}!": "Você ganhou ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Você foi promovido a uma nova liga; parabéns!", + "You must update to a newer version of the app to do this.": "Você deve atualizar para uma nova versão do aplicativo para fazer isso.", + "You must update to the newest version of the game to do this.": "Você deve atualizar para a nova versão do jogo para fazer isso.", + "You must wait a few seconds before entering a new code.": "Você deve esperar alguns segundos antes de inserir um novo código.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Você teve a classificação #${RANK} no último torneio. Obrigado por jogar!", + "Your account was rejected. Are you signed in?": "Sua conta foi rejeitada. Você iniciou a sessão?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Sua copia do jogo foi modificada.\nReverta as modificações e tente novamente.", + "Your friend code was used by ${ACCOUNT}": "Seu código de amigo foi usado por ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 minuto", + "1 Second": "1 segundo", + "10 Minutes": "10 minutos", + "2 Minutes": "2 minutos", + "2 Seconds": "2 segundos", + "20 Minutes": "20 minutos", + "4 Seconds": "4 segundos", + "5 Minutes": "5 minutos", + "8 Seconds": "8 segundos", + "Allow Negative Scores": "Permite Pontuação Negativa", + "Balance Total Lives": "Saldo Total de Vidas", + "Bomb Spawning": "Bombas Surgindo", + "Chosen One Gets Gloves": "O escolhido obtém luvas", + "Chosen One Gets Shield": "O escolhido obtém escudo", + "Chosen One Time": "Hora do Escolhido", + "Enable Impact Bombs": "Ativar bombas de impacto", + "Enable Triple Bombs": "Ativar tribombas", + "Entire Team Must Finish": "A equipe inteira precisa terminar", + "Epic Mode": "Modo épico", + "Flag Idle Return Time": "Tempo de retorno da bandeira inativa", + "Flag Touch Return Time": "Tempo de retorno da bandeira", + "Hold Time": "Tempo de retenção", + "Kills to Win Per Player": "Mortes para ganhar por jogador", + "Laps": "Voltas", + "Lives Per Player": "Vidas por jogador", + "Long": "Longo", + "Longer": "Mais longo", + "Mine Spawning": "Minas surgindo", + "No Mines": "Sem minas", + "None": "Nenhum", + "Normal": "Normal", + "Pro Mode": "Modo Pro", + "Respawn Times": "Número de renascimentos", + "Score to Win": "Marque para Ganhar", + "Short": "Curto", + "Shorter": "Mais curto", + "Solo Mode": "Modo Solo", + "Target Count": "Número de Alvos", + "Time Limit": "Limite de Tempo" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} está em desvantagem porque ${PLAYER} saiu", + "Killing ${NAME} for skipping part of the track!": "Matando ${NAME} por cortar o caminho!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Aviso para ${NAME}: o turbo / spam de botão faz você sair." + }, + "teamNames": { + "Bad Guys": "Inimigos", + "Blue": "Azul", + "Good Guys": "Aliados", + "Red": "Vermelho" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Se você bater, correr, pular e girar em perfeita sincronia poderá matar\nem um único golpe e garantir o respeito eterno de seus amigos.", + "Always remember to floss.": "Lembre-se de sempre usar fio dental.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Crie perfis de jogadores para você e seus amigos com\nseus nomes preferidos e aparências ao invés de usar os aleatórios.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Caixas amaldiçoadas o transformam em uma bomba-relógio.\nA única cura é agarrar rapidamente um pacote de saúde.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Apesar de suas aparências, as habilidades de todos os personagens são idênticas,\nentão basta escolher qualquer um que você mais se assemelha.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Não fique muito convencido com o escudo de energia; você ainda pode ser arremessado de um penhasco.", + "Don't run all the time. Really. You will fall off cliffs.": "Não corra o tempo todo. Sério. Você vai cair de penhascos.", + "Don't spin for too long; you'll become dizzy and fall.": "Não gire por muito tempo; você vai ficar tonto e cair!", + "Hold any button to run. (Trigger buttons work well if you have them)": "Pressione qualquer botão para correr. (Botões de gatilho funcionam bem, se os tiver)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Mantenha pressionado qualquer botão para correr. Você vai alcançar lugares mais\nrapidamente, mas não vai fazer curvas muito bem, por isso esteja atento para penhascos.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "As bombas de gelo não são muito poderosas, mas elas congelam\nquem for atingido, deixando-os vulneráveis ​​a estilhaçamentos.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Se alguém te levantar, soque-o e ele irá te largar.\nIsso também funciona na vida real.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Se você está sem controles, instale o aplicativo '${REMOTE_APP_NAME}'\nem seus dispositivos móveis para usá-los como controles.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Se você estiver com controles insuficientes, instale o aplicativo 'BombSquad Remote'\nno seu dispositivo iOS ou Android para usá-los como controles.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Se você tiver uma bomba grudenta presa em você, salte e gire em círculos. Você pode\nsacudir a bomba para fora ou, pelo menos, seus últimos momentos serão divertidos.", + "If you kill an enemy in one hit you get double points for it.": "Se você matar um inimigo com um golpe, você obtêm o dobro de pontos por isso.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Se você pegar uma maldição, sua única esperança de sobrevivência é\nencontrar um poder de saúde nos próximos segundos.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Se você ficar em um lugar, você está frito. Corra e se esquive para sobreviver.", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Se você tem muitos jogadores entrando e saindo, ligue 'Expulsar Jogadores Ociosos Automaticamente'\nnas configurações no caso de alguém esquecer de deixar o jogo.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Se seu dispositivo ficar muito quente ou você quiser conservar bateria,\nabaixe os \"Visuais\" ou \"Resolução\" nas Configurações-> Graficos", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Se sua taxa de quadros estiver baixa, tente diminuir a resolução\nou visuais nas configurações gráficas do jogo.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "Em Capture a bandeira, a sua própria bandeira deve estar em sua base para marcar. Se a outra\nequipe está prestes a marcar, roubar a sua bandeira pode ser uma boa maneira de detê-los.", + "In hockey, you'll maintain more speed if you turn gradually.": "No hóquei, você manterá mais velocidade se girar gradualmente.", + "It's easier to win with a friend or two helping.": "É mais fácil ganhar com um ou dois amigos ajudando.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Apenas salte enquanto você está arremessando para conseguir bombas até os níveis mais altos.", + "Land-mines are a good way to stop speedy enemies.": "Minas terrestres são uma boa maneira de parar inimigos rápidos.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Muitas coisas podem ser apanhadas e lançadas, incluindo outros jogadores. Jogar\nos seus inimigos de penhascos pode ser uma estratégia eficaz e emocionalmente gratificante.", + "No, you can't get up on the ledge. You have to throw bombs.": "Não, você não pode levantar-se na borda. Você tem que jogar bombas.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Jogadores podem entrar e sair no meio da maioria dos jogos,\ne você também pode ligar ou desligar controles quando quiser.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Os jogadores podem entrar e sair no meio da maioria dos jogos,\ne você também pode ligar e desligar gamepads em qualquer momento.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Poderes só tem limite de tempo no modo cooperativo.\nEm times e cada-um-por-si eles são seus até você morrer.", + "Practice using your momentum to throw bombs more accurately.": "Pratique usando a inércia para arremessar bombas com maior precisão.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Socos fazem mais danos quanto mais rápido os punhos estão se movendo,\nentão tente correr, pular e girar como um louco.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Corra para frente e para trás antes de arremessar uma bomba\npara 'chicoteá-la' e jogá-la longe.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Elimine um grupo de inimigos ao\ndesencadear uma bomba perto de uma caixa de TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "A cabeça é a área mais vulnerável, portanto uma bomba grudenta\nna cuca geralmente significa fim de jogo.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Este nível nunca termina, mas uma alta pontuação aqui\nfaz você ganhar respeito eterno por todo o mundo.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Força de arremesso baseia-se na direção em que você está pressionando.\nPara arremessar algo suavemente na sua frente, não pressione qualquer direção.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Cansado das músicas? Troque-as pelas suas próprias!\nVeja em Configurações-> Áudio-> Músicas", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Experimente 'cozinhar' bombas por um segundo ou dois antes de jogá-las.", + "Try tricking enemies into killing eachother or running off cliffs.": "Tente enganar inimigos para se matarem ou se jogarem do precipício.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Use o botão pegar para pegar a bandeira < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Balance para trás e para frente para fazer arremessos distantes..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Você pode 'mirar' seus socos girando para esquerda ou direita.\nIsso é útil para derrubar inimigos das beiradas ou marcar no hóquei.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Você pode avaliar quando uma bomba vai explodir baseado na\ncor das faíscas do pavio: amarelo..laranja..vermelho..BOOM.", + "You can throw bombs higher if you jump just before throwing.": "Você pode jogar as bombas mais alto ao saltar logo antes de arremessar.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Você não precisa editar seu perfil para mudar de personagem; É só pressionar\no botão de cima (pegar) quando estiver entrando numa partida.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Você se fere quando você bate sua cabeça em coisas,\nassim tente não bater sua cabeça em coisas.", + "Your punches do much more damage if you are running or spinning.": "Seus socos causam muito mais dano se você estiver correndo ou girando." + } + }, + "trialPeriodEndedText": "Seu período experimental terminou. Gostaria\nde comprar BombSquad e continuar jogando?", + "trophiesRequiredText": "Isso necessita de pelo menos ${NUMBER} troféus.", + "trophiesText": "Troféus", + "trophiesThisSeasonText": "Troféus nesta temporada", + "tutorial": { + "cpuBenchmarkText": "Rodando o tutorial numa velocidade MUITO baixa (para testar o processador)", + "phrase01Text": "Olá!", + "phrase02Text": "Bem-vindo ao ${APP_NAME}!", + "phrase03Text": "Aqui estão algumas dicas para controlar seu personagem:", + "phrase04Text": "Muitas coisas no ${APP_NAME} são baseadas na física.", + "phrase05Text": "Por exemplo, quando você soca,..", + "phrase06Text": "..o dano é baseado na velocidade de seus punhos.", + "phrase07Text": "Viu? Nós não estávamos nos movendo então mal fez cócegas no ${NAME}.", + "phrase08Text": "Agora vamos pular e girar para ganhar mais velocidade.", + "phrase09Text": "Ah, assim é melhor.", + "phrase10Text": "Correr ajuda também.", + "phrase11Text": "Mantenha QUALQUER botão pressionado para correr.", + "phrase12Text": "Para socos adicionais incríveis, tente correr e girar.", + "phrase13Text": "Ops! foi mal aí, ${NAME}.", + "phrase14Text": "Você pode pegar e jogar coisas como bandeiras.. ou ${NAME}.", + "phrase15Text": "Por último, há bombas.", + "phrase16Text": "Arremessar bombas requer prática.", + "phrase17Text": "Ai! Não foi um arremesso muito bom.", + "phrase18Text": "Movimentar-se te ajuda a arremessar mais longe.", + "phrase19Text": "Saltar ajuda você a arremessar mais alto.", + "phrase20Text": "Gire e corra e suas bombas irão ainda mais longe.", + "phrase21Text": "Calcular o tempo da explosão pode ser complicado.", + "phrase22Text": "Droga!", + "phrase23Text": "Tente deixar o pavio queimar por um ou dois segundos.", + "phrase24Text": "Eba! No tempo ideal.", + "phrase25Text": "Bem, acho que é só isso.", + "phrase26Text": "Agora vai lá e arrebenta!", + "phrase27Text": "Lembre-se do seu treinamento e você voltará vivo!", + "phrase28Text": "...bem, talvez...", + "phrase29Text": "Boa sorte!", + "randomName1Text": "Fernando", + "randomName2Text": "Henrique", + "randomName3Text": "Guilherme", + "randomName4Text": "Carlos", + "randomName5Text": "Felipe", + "skipConfirmText": "Você deseja realmente pular o tutorial? Toque ou aperte confirmar.", + "skipVoteCountText": "${COUNT}/${TOTAL} votos para pular", + "skippingText": "pulando o tutorial...", + "toSkipPressAnythingText": "(pressione qualquer coisa para pular o tutorial)" + }, + "twoKillText": "MATOU DOIS!", + "unavailableText": "indisponível", + "unconfiguredControllerDetectedText": "Controle não configurado detectado:", + "unlockThisInTheStoreText": "Isto deve ser desbloqueado na loja.", + "unlockThisProfilesText": "Para criar mais que ${NUM} perfis, você precisa:", + "unlockThisText": "Para desbloquear isso:", + "unsupportedHardwareText": "Desculpe, este hardware não é suportado por esta versão do jogo.", + "upFirstText": "Em primeiro lugar:", + "upNextText": "O próximo jogo em ${COUNT}:", + "updatingAccountText": "Atualizando sua conta...", + "upgradeText": "Melhorar", + "upgradeToPlayText": "Atualize para \"${PRO}\" na loja para jogar.", + "useDefaultText": "Usar padrão", + "usesExternalControllerText": "Este jogo usa um controle externo para entrada.", + "usingItunesText": "Usando o app de música para a trilha sonora", + "usingItunesTurnRepeatAndShuffleOnText": "Por favor, certifique-se de que o aleatório esteja ligado e que a repetição seja TODAS AS MÚSICAS no iTunes.", + "validatingBetaText": "Validando Beta...", + "validatingTestBuildText": "Validando versão de teste...", + "victoryText": "Vitória!", + "voteDelayText": "Você não pode começar outra votação por ${NUMBER} segundo(s)", + "voteInProgressText": "Uma votação já está em progresso.", + "votedAlreadyText": "Você já votou", + "votesNeededText": "${NUMBER} votos necessários", + "vsText": "vs.", + "waitingForHostText": "(esperando ${HOST} continuar)", + "waitingForLocalPlayersText": "esperando por jogadores locais...", + "waitingForPlayersText": "esperando os jogadores entrarem...", + "waitingInLineText": "Esperando na fila (o grupo está cheio)...", + "watchAVideoText": "Ver um vídeo", + "watchAnAdText": "Assistir uma propaganda.", + "watchWindow": { + "deleteConfirmText": "Excluir \"${REPLAY}\"?", + "deleteReplayButtonText": "Excluir\nReplay", + "myReplaysText": "Meus replays", + "noReplaySelectedErrorText": "Nenhum replay selecionado.", + "playbackSpeedText": "Velocidade de reprodução: ${SPEED}", + "renameReplayButtonText": "Renomear\nReplay", + "renameReplayText": "Renomear \"${REPLAY}\" para:", + "renameText": "Renomear", + "replayDeleteErrorText": "Erro ao excluir o replay.", + "replayNameText": "Nome do replay", + "replayRenameErrorAlreadyExistsText": "Um replay com este nome já existe.", + "replayRenameErrorInvalidName": "Não foi possível renomear; nome invalido.", + "replayRenameErrorText": "Erro ao renomear replay.", + "sharedReplaysText": "Replays compartilhados", + "titleText": "Assistir", + "watchReplayButtonText": "Assistir\nReplay" + }, + "waveText": "Onda", + "wellSureText": "Claro!", + "wiimoteLicenseWindow": { + "licenseTextScale": 0.62, + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Procurando por Wiimotes...", + "pressText": "Aperte os botões 1 e 2 no Wiimote simultaneamente.", + "pressText2": "Em Wiimotes mais recentes com Motion Plus embutido, aperte o botão vermelho 'sync' na parte de trás em seu lugar.", + "pressText2Scale": 0.55, + "pressTextScale": 1.0 + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "copyrightTextScale": 0.6, + "listenText": "Procurar", + "macInstructionsText": "Certifique-se de que o seu Wii está desligado e o Bluetooth\nativado no Mac, então pressione 'Listen'. O Wiimote pode\nser um pouco trabalhoso, você pode ter que tentar algumas\nvezes antes de conseguir uma conexão.\n\nO Bluetooth deve gerenciar até 7 dispositivos conectados,\nembora a distância pode variar.\n\nO BombSquad suporta Wiimotes, Nunchucks\ne o Controle clássico.\nO novo Wii Remote Plus agora também funciona mas não \ncom acessórios.", + "macInstructionsTextScale": 0.7, + "thanksText": "Obrigado à equipe DarwiinRemote\nPor tornar isto possível.", + "thanksTextScale": 0.8, + "titleText": "Configuração do Wiimote" + }, + "winsPlayerText": "${NAME} venceu!", + "winsTeamText": "${NAME} venceu!", + "winsText": "${NAME} ganhou!", + "worldScoresUnavailableText": "Pontuações mundiais indisponíveis.", + "worldsBestScoresText": "Melhores pontuações do mundo", + "worldsBestTimesText": "Melhores tempos do mundo", + "xbox360ControllersWindow": { + "getDriverText": "Obter Driver", + "macInstructions2Text": "Para usar os controles sem fio, você também precisará de um receptor\nque acompanha o 'Xbox 360 Wireless Controller for Windows'.\nUm receptor permite conectar até quatro controles.\n\nImportante: receptores de terceiros não irão funcionar com esse driver;\nCertifique-se de que seu receptor exiba 'Microsoft' nele, não 'Xbox 360'.\nMicrosoft não os vende mais separadamente, portanto será preciso comprar\num kit com um controle, ou algo assim, procure no ebay.\n\nSe você achou isso útil, por favor, considere fazer uma doação no site \ndo desenvolvedor do driver.", + "macInstructionsText": "Para usar os controles do Xbox 360, você precisará instalar\no driver Mac disponível no link abaixo.\nIsso funciona com ambos os controles com e sem fios.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "Para usar os controles com fio do Xbox 360 com BombSquad, basta\nligá-los na porta USB do seu dispositivo. Você pode usar um hub USB\npara conectar vários controles.\n\nPara usar os controles sem fio, você precisará de um receptor sem fio,\ndisponível como parte do pacote \"Controles sem fios do Xbox 360 para Windows\"\nou vendido separadamente. Cada receptor se conecta a uma porta USB e\npermite conectar até 4 controles sem fio.", + "ouyaInstructionsTextScale": 0.8, + "titleText": "Usando Controles Xbox 360 com o ${APP_NAME}:" + }, + "yesAllowText": "Sim, permitir!", + "yourBestScoresText": "Suas melhores pontuações", + "yourBestTimesText": "Seus melhores tempos" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/romanian.json b/dist/ba_data/data/languages/romanian.json new file mode 100644 index 0000000..a726289 --- /dev/null +++ b/dist/ba_data/data/languages/romanian.json @@ -0,0 +1,1683 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Numele Contului nu poate conține emoji sau alte semne speciale", + "accountProfileText": "(profil de cont)", + "accountsText": "Conturi", + "achievementProgressText": "Realizări: ${COUNT} din ${TOTAL}", + "campaignProgressText": "Progres campanie [Greu]: ${PROGRESS}", + "changeOncePerSeason": "Acesta poate fi schimbat o singura data pe sezon", + "changeOncePerSeasonError": "Trebuie sa astepti pana urmatorul sezon ca sa schimbi asta din nou (${NUM} zile)", + "customName": "Nume personalizat", + "deviceSpecificAccountText": "Foloseşti un cont specific dispozitivului: ${NAME}", + "linkAccountsEnterCodeText": "Introdu Codul", + "linkAccountsGenerateCodeText": "Generează Codul", + "linkAccountsInfoText": "(împărtăşeşte progresul între diferite platforme)", + "linkAccountsInstructionsText": "Pentru a conecta 2 conturi, generează un cod pe\nunul din ele şi introdu acelmcod pe celălalt.\nProgresul şi inventarul tău vor fi combinate.\nPoți conecta până la ${COUNT} conturi.\n\nAi grijă; acest lucru nu poate fi şters!", + "linkAccountsText": "Conectează Conturi", + "linkedAccountsText": "Conturi conectate:", + "nameChangeConfirm": "Schimbăti numele contului in ${NAME}?", + "resetProgressConfirmNoAchievementsText": "Această acțiune va reseta progresul co-op\nși high-score-urile (nu și biletele) și nu\npoate fi anulată. Ești sigur(ă)?", + "resetProgressConfirmText": "Această acțiune va reseta progresul\nco-op, realizările și high-scoreurile\n(nu și biletele) și nu poate fi\nanulată. Ești sigur?", + "resetProgressText": "Resetează progres", + "setAccountName": "Seteazaă numele contului", + "setAccountNameDesc": "Selectati numele pentru a afisa pentru contul tau.Tu poti folosi numele de la unul dintre conturile tale legate sau sa creezi un nume unic si personalizat", + "signInInfoText": "Conectează-te pentru a colecta bilete, a juca online,\nşi a juca cu acelaşi cont pe dispozitive diferite.", + "signInText": "Conectează-te", + "signInWithDeviceInfoText": "(un cont automat care este disponibil doar pe acest dispozitiv)", + "signInWithDeviceText": "Conectează-te cu un cont de dispozitiv", + "signInWithGameCircleText": "Conectrază-te cu Game Circle", + "signInWithGooglePlayText": "Conectează-te cu Google Play", + "signInWithTestAccountInfoText": "(tip de cont normal; foloseşte conturi de dispozitiv şi cele noi)", + "signInWithTestAccountText": "Conectează-te cu un cont de test.", + "signOutText": "Decoectează-te", + "signingInText": "Se conecteză...", + "signingOutText": "Se deconectează...", + "testAccountWarningCardboardText": "Atenție: Te conectezi cu un cont \"de test\". Aceste\nconturi vor fi înlocuite cu conturi Google atunci\ncând vor fi suportate în aplicații cardboard.\n\nDeocamdată vei avea să obții toate biletele în joc.\n(Însă vei primi upgrade-ul BombSquad Pro gratis)", + "testAccountWarningOculusText": "Atenție: Te conectezi cu un cont \"de test\". Aceste\nconturi vor fi înlocuite cu conturi Oculus mai încolo anul\nacesta ce vor oferi cumpărarea de bilete și alte funcționalități.\n\nDeocamdată vei avea să obții toate biletele în joc.\n(Însă vei primi upgrade-ul BombSquad Pro gratis)", + "testAccountWarningText": "Atenție: te conectezi cu un cont \"de test\". Acest cont\nva fi legat numai de acest dispozitiv și va fi resetat\nperiodic. (deci nu pierde prea mult timp colectând/\ndeblocând lucruri pe el)\n\nFolosește o versiune retail a jocului pentru a folosi\ncontul \"real\" (Game-Center, Google+ etc.). Aceasta te\nlasă să iți și salvezi progresul pe cloud și să îl\nîmparți pe mai multe dispozitive.", + "ticketsText": "Bilete: ${COUNT}", + "titleText": "Cont", + "unlinkAccountsInstructionsText": "Selecteaza un cont pentru a te deconecta", + "unlinkAccountsText": "Conturi deconectate", + "youAreSignedInAsText": "Ești conectat ca și:" + }, + "achievementChallengesText": "Provocări pentru Realizări", + "achievementText": "Realizare", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Omoară 3 inamici folosind TNT", + "descriptionComplete": "Ai omorât 3 inamici folosind TNT", + "descriptionFull": "Omoară 3 inamici folosind TNT la ${LEVEL}", + "descriptionFullComplete": "Ai omorât 3 inamici folosind TNT la ${LEVEL}", + "name": "Bum Face Dinamita" + }, + "Boxer": { + "description": "Câștigă fără a folosi bombe", + "descriptionComplete": "Ai câștigat fără sa folosești bombe", + "descriptionFull": "Completează ${LEVEL} fără sa folosești bombe", + "descriptionFullComplete": "Ai completat ${LEVEL} fără sa folosești bombe", + "name": "Boxer" + }, + "Dual Wielding": { + "descriptionFull": "Conectează 2 controllere (fizice sau prin aplicație)", + "descriptionFullComplete": "Ai conectat 2 controllere (fizice sau prin aplicație)", + "name": "Înarmat dublu" + }, + "Flawless Victory": { + "description": "Câștigă fără să fii lovit", + "descriptionComplete": "Ai câștigat fără să fii lovit", + "descriptionFull": "Câștigă ${LEVEL} fără să fii lovit", + "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să fii lovit", + "name": "Victorie perfectă" + }, + "Free Loader": { + "descriptionFull": "Porneşte un joc Fiecare-pentru-el cu 2+ jucători", + "descriptionFullComplete": "Ai pornit un joc Fiecare-pentru-el cu 2+ jucători", + "name": "Încărcător Liber" + }, + "Gold Miner": { + "description": "Omoară 6 inamici folosind mine", + "descriptionComplete": "Ai omorât 6 inamici folosind mine", + "descriptionFull": "Omoară 6 inamici folosind mine la ${LEVEL}", + "descriptionFullComplete": "Ai omorât 6 inamici folosind mine la ${LEVEL}", + "name": "Miner de aur" + }, + "Got the Moves": { + "description": "Câștigă fără a folosi pumnii sau bombe", + "descriptionComplete": "Ai câștigat fără să folosești pumnii sau bombele", + "descriptionFull": "Câștigă ${LEVEL} fără să folosești pumni sau bombe", + "descriptionFullComplete": "Ai câștigat ${LEVEL} fără sa folosești pumni sau bombe", + "name": "Știi mișcările" + }, + "In Control": { + "descriptionFull": "Conectează un controller (fizic sau prin aplicație)", + "descriptionFullComplete": "Ai conectat un controller. (fizic sau prin aplicație)", + "name": "În control" + }, + "Last Stand God": { + "description": "Marchează 1000 puncte", + "descriptionComplete": "A marcat 1000 de puncte", + "descriptionFull": "Înscrie 1000 de punctele la ${LEVEL}", + "descriptionFullComplete": "Ai înscris 1000 puncte la ${LEVEL}", + "name": "Zeu ${LEVEL}" + }, + "Last Stand Master": { + "description": "Înscrie 250 puncte", + "descriptionComplete": "Ai înscris 250 puncte", + "descriptionFull": "Înscrie 250 puncte la ${LEVEL}", + "descriptionFullComplete": "Ai înscris 250 puncte la ${LEVEL}", + "name": "Maestru ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Înscrie 500 de puncte", + "descriptionComplete": "Ai înscris 500 puncte", + "descriptionFull": "Înscrie 500 puncte la ${LEVEL}", + "descriptionFullComplete": "Ai înscris 500 punce la ${LEVEL}", + "name": "Vrăjitor ${LEVEL}" + }, + "Mine Games": { + "description": "Omoară 3 inamici cu mine", + "descriptionComplete": "Omoară 3 inamici folosind mine", + "descriptionFull": "Omoară 3 inamici folosind mine la ${LEVEL}", + "descriptionFullComplete": "Ai omorât 3 inamici folosind mine la ${LEVEL}", + "name": "Jocurile minelor" + }, + "Off You Go Then": { + "description": "Aruncă 3 inamici peste hartă", + "descriptionComplete": "Ai aruncat 3 inamici peste hartă", + "descriptionFull": "Aruncă 3 inamici peste hartă la ${LEVEL}", + "descriptionFullComplete": "Ai aruncat 3 inamici peste hartă la ${LEVEL}", + "name": "Jos cu tine" + }, + "Onslaught God": { + "description": "Înscrie 5000 puncte", + "descriptionComplete": "Ai înscris 5000 puncte", + "descriptionFull": "Înscrie 5000 puncte la ${LEVEL}", + "descriptionFullComplete": "Ai înscris 5000 puncte la ${LEVEL}", + "name": "Zeu ${LEVEL}" + }, + "Onslaught Master": { + "description": "Înscrie 500 puncte", + "descriptionComplete": "Ai înscris 500 puncte", + "descriptionFull": "Înscrie 500 puncte la ${LEVEL}", + "descriptionFullComplete": "Ai înscris 500 puncte ${LEVEL}", + "name": "Maestru ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Înfrânge toate valurile", + "descriptionComplete": "Ai înfrânt toate valurile", + "descriptionFull": "Înfrânge toate valurile la ${LEVEL}", + "descriptionFullComplete": "Ai înfrânt toate valurile la ${LEVEL}", + "name": "Victorie ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Inscrie 1000 de puncte", + "descriptionComplete": "Ai insris 100 de puncte", + "descriptionFull": "Inscrie 1000 de puncte in ${LEVEL}", + "descriptionFullComplete": "Ai insris 1000 de puncte in ${LEVEL}", + "name": "Vrajitor ${LEVEL}" + }, + "Precision Bombing": { + "description": "Câștigă fără să folosești powerup-uri", + "descriptionComplete": "Ai câștigat fără să folosești powerup-uri", + "descriptionFull": "Câștigă ${LEVEL} fără să folosești powerup-uri", + "descriptionFullComplete": "Ai câștigat ${LEVEL}. fără să folosești powerup-uri", + "name": "Bombardament de precizie" + }, + "Pro Boxer": { + "description": "Castiga fara sa folosesti vreo bomba", + "descriptionComplete": "Ai castigat fara sa folosesti vreo bomba", + "descriptionFull": "Completeaza ${LEVEL} fara sa folosesti vreo bomba", + "descriptionFullComplete": "Ai completat ${LEVEL} fara sa folosesti vreo bomba", + "name": "Boxeor profesionist" + }, + "Pro Football Shutout": { + "description": "Castiga fara sa ii lasi pe inamici sa inscrie", + "descriptionComplete": "Ai castigat fară sa îi laşi le inamici sa inscrie", + "descriptionFull": "Castiga ${LEVEL} fara sa ii lasi pe inamici sa inscrie", + "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să lași pe inamici să înscrie", + "name": "Shutout ${LEVEL}" + }, + "Pro Football Victory": { + "description": "Câștigă jocul", + "descriptionComplete": "Ai câștigat jocul", + "descriptionFull": "Câștigă jocul din ${LEVEL}", + "descriptionFullComplete": "Ai câștigat jocul din ${LEVEL}", + "name": "Victorie ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Înfrânge toate valurile", + "descriptionComplete": "Ai înfrânt toate valurile", + "descriptionFull": "Înfrânge toate valurile din ${LEVEL}", + "descriptionFullComplete": "Ai înfrânt toate valurile din ${LEVEL}", + "name": "Victorie ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Completează toate valurile", + "descriptionComplete": "Ai completat toate valurile", + "descriptionFull": "Completează toate valurile la ${LEVEL}", + "descriptionFullComplete": "Ai completat toate valurile la ${LEVEL}", + "name": "Victorie ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Câștigă fără să înscrie tipii răi", + "descriptionComplete": "Ai câștigat fără să înscrie tipii răi", + "descriptionFull": "Câștigă ${LEVEL} fără să înscrie tipii răi", + "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să înscrie tipii răi", + "name": "Shutout ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Câștigă jocul", + "descriptionComplete": "Ai câștigat jocul", + "descriptionFull": "Câștigă jocul la ${LEVEL}", + "descriptionFullComplete": "Ai câștigat jocul la ${LEVEL}", + "name": "Victorie ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Învinge toate trupele", + "descriptionComplete": "Învinge toate trupele", + "descriptionFull": "Învinge toate trupele în ${LEVEL}", + "descriptionFullComplete": "Învinge toate trupele în ${LEVEL}", + "name": "${LEVEL} Victorie" + }, + "Runaround God": { + "description": "Strânge 2000 de puncte", + "descriptionComplete": "Strânge 2000 de puncte", + "descriptionFull": "Strânge 2000 de puncte în ${LEVEL}", + "descriptionFullComplete": "Strânge 2000 de puncte în ${LEVEL}", + "name": "${LEVEL} Zeu" + }, + "Runaround Master": { + "description": "Strânge 500 de puncte", + "descriptionComplete": "Ai strâns 500 de puncte", + "descriptionFull": "Strânge 500 de puncte în ${LEVEL}", + "descriptionFullComplete": "Ai strâns 500 de puncte în ${LEVEL}", + "name": "Maestru la ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Strânge 1000 de puncte", + "descriptionComplete": "Ai strâns 1000 de puncte", + "descriptionFull": "Strânge 1000 de puncte pe ${LEVEL}", + "descriptionFullComplete": "Ai strâns 1000 de puncte pe ${LEVEL}", + "name": "Vrăjitor la ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Împărtăşeşte jocul cu un prieten", + "descriptionFullComplete": "Ai împărtăşit jocul cu un prieten", + "name": "E bine să împarți" + }, + "Stayin' Alive": { + "description": "Câştigă fără a muri", + "descriptionComplete": "Ai câştigat fără a muri", + "descriptionFull": "Câştigă pe ${LEVEL} fără a muri", + "descriptionFullComplete": "Ai câştigat pe ${LEVEL} fără a muri", + "name": "Rămănând in viață" + }, + "Super Mega Punch": { + "description": "Cauzează un damage de 100% cu o lovitură", + "descriptionComplete": "Ai cauzat un damage de 100% cu o lovitură", + "descriptionFull": "Cauzează un damage de 100% cu o lovitură în ${LEVEL}", + "descriptionFullComplete": "Ai cauzat un damage de 100% cu o lovitură în ${LEVEL}", + "name": "Lovitură super mega" + }, + "Super Punch": { + "description": "Cauzează un damage de 50% cu o lovitură", + "descriptionComplete": "Ai cauzat un damage de 50% cu o lovitură", + "descriptionFull": "Cauzează un damage de 50% cu o lovitură în ${LEVEL}", + "descriptionFullComplete": "Ai cauzat un damage de 50% cu o lovitură în ${LEVEL}", + "name": "Super Pumn" + }, + "TNT Terror": { + "description": "Omoară 6 tipi răi cu TNT", + "descriptionComplete": "Ai omorât 6 tipi răi cu TNT", + "descriptionFull": "Omoară 6 tipi răi cu TNT la ${LEVEL}", + "descriptionFullComplete": "Ai omorât 6 tipi răi cu TNT la ${LEVEL}", + "name": "Teroarea TNT" + }, + "Team Player": { + "descriptionFull": "Porneşte un joc pe Echipe cu 4+ jucători", + "descriptionFullComplete": "Ai pornit un joc pe Echipe cu 4+ jucători", + "name": "Jucător in Echipă" + }, + "The Great Wall": { + "description": "Opreşte fiecare tip rău", + "descriptionComplete": "Ai oprit fiecare tip rău", + "descriptionFull": "Opreşte fiecare tip rău în ${LEVEL}", + "descriptionFullComplete": "Ai oprit fiecare tip rău în ${LEVEL}", + "name": "Marele zid" + }, + "The Wall": { + "description": "Oprește fiecare tip rău", + "descriptionComplete": "Ai oprit fiecare tip rău", + "descriptionFull": "Oprește fiecare tip rău la ${LEVEL}", + "descriptionFullComplete": "Ai oprit fiecare tip rău la ${LEVEL}", + "name": "Zidul" + }, + "Uber Football Shutout": { + "description": "Câștigă fără să lași pe inamici să înscrie", + "descriptionComplete": "Ai câștigat fără să lași pe inamici să înscrie", + "descriptionFull": "Câștigă ${LEVEL} fără să lași pe inamici să înscrie", + "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să lași pe inamici să înscrie", + "name": "Shutout ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Câștigă jocul", + "descriptionComplete": "Ai câștigat jocul", + "descriptionFull": "Câștigă jocul la ${LEVEL}", + "descriptionFullComplete": "Ai câștigat jocul la ${LEVEL}", + "name": "Victorie ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Înfrânge toate valurile", + "descriptionComplete": "Ai înfrânt toate valurile", + "descriptionFull": "Înfrânge toate valurile la ${LEVEL}", + "descriptionFullComplete": "Ai înfrânt toate valurile la ${LEVEL}", + "name": "Victorie ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Completează toate valurile", + "descriptionComplete": "Ai completat toate valurile", + "descriptionFull": "Completează toate valurile la ${LEVEL}", + "descriptionFullComplete": "Ai terminat toate valurile de la ${LEVEL}", + "name": "Victorie ${LEVEL}" + } + }, + "achievementsRemainingText": "Realizări rămase:", + "achievementsText": "Realizări", + "achievementsUnavailableForOldSeasonsText": "Scuze, dar detaliile realizărilor din sezoanele trecute sunt indisponibile.", + "addGameWindow": { + "getMoreGamesText": "Ia mai multe jocuri...", + "titleText": "Adaugă joc" + }, + "allowText": "Permite", + "apiVersionErrorText": "Nu se poate deschide moduluL ${NAME}; acela accesează versiunea api ${VERSION_USED}, pe când e nevoie de ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" activează asta doar când căștile sunt conectate)", + "headRelativeVRAudioText": "Audio VR relativ capului", + "musicVolumeText": "Volum muzică", + "soundVolumeText": "Volum sunete", + "soundtrackButtonText": "Coloană sonoră", + "soundtrackDescriptionText": "(setează ce muzică sa cânte în timpul jocului)", + "titleText": "Audio" + }, + "autoText": "Automat", + "backText": "Înapoi", + "banThisPlayerText": "Interzice acest jucator", + "bestOfFinalText": "Finală cel-mai-bun-din-${COUNT}", + "bestOfSeriesText": "Serii cel mai bun din ${COUNT}:", + "bestRankText": "Cea mai bună poziție a ta: #${RANK}", + "bestRatingText": "Cel mai bun rating al tău e ${RATING}", + "bombBoldText": "BOMBĂ", + "bombText": "Bombă", + "boostText": "Viteza", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} se configureaza în însăși aplicatia", + "buttonText": "buton", + "canWeDebugText": "Ai dori ca BombSquad să trimită automat bug-uri,\ncrash-uri și informații de bază programatorului?\n\nAceste informații nu conțin date personale și doar\najută la îmbunătățirea jocului.", + "cancelText": "Anulează", + "cantConfigureDeviceText": "Scuze, dar ${DEVICE} nu e configurabil.", + "challengeEndedText": "Acest concurs s-a terminat.", + "chatMuteText": "Dezactiveaza chat-ul", + "chatMutedText": "Chat dezactivat", + "chatUnMuteText": "Activeaza chat-ul", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Trebuie să completezi\nacest nivel pentru a continua!", + "completionBonusText": "Bonus de completare", + "configControllersWindow": { + "configureControllersText": "Configurează controllere", + "configureKeyboard2Text": "Configurează tastatură P2", + "configureKeyboardText": "Configurează tastatură", + "configureMobileText": "Dispozitive mobile ca și controllere", + "configureTouchText": "Configurează touchscreen", + "ps3Text": "Controllere PS3", + "titleText": "Controllere", + "wiimotesText": "Wiimotes", + "xbox360Text": "Controllere X-Box 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Notă: suportul controllerului poate varia de la versiunea android și de la dispozitiv.", + "pressAnyButtonText": "Apasă ce buton de pe controller\nVrei să configurezi...", + "titleText": "Configurează controllere" + }, + "configGamepadWindow": { + "advancedText": "Avansat", + "advancedTitleText": "Setări controller avansate", + "analogStickDeadZoneDescriptionText": "(Ridică valoarea acestuia dacă iți \"alunecă\" omulețul când dai drumul la 'stick)", + "analogStickDeadZoneText": "Dead-zone 'stick analog", + "appliesToAllText": "(se aplică la toate controllerele de acest tip)", + "autoRecalibrateDescriptionText": "(activează asta dacă omulețul nu se mișcă la viteza maximă)", + "autoRecalibrateText": "Recalibrare automată 'stick analog", + "axisText": "axă", + "clearText": "șterge", + "dpadText": "D-pad", + "extraStartButtonText": "Buton start secundar", + "ifNothingHappensTryAnalogText": "Dacă nu se întâmplă nimic, încearcă să folosești 'stick-ul analog.", + "ifNothingHappensTryDpadText": "Dacă nimic nu se întâmplă, încearcă d-pad-ul.", + "ignoreCompletelyDescriptionText": "(fă ca acest controller sa nu afecteze jocul sau meniul)", + "ignoreCompletelyText": "Ignorare completa", + "ignoredButton1Text": "Buton ignorat 1", + "ignoredButton2Text": "Buton ignorat 2", + "ignoredButton3Text": "Buton ignorat 3", + "ignoredButton4Text": "Butonul 4 Ignorat", + "ignoredButtonDescriptionText": "(folosește asta pentru butoane ca 'home' sau 'sync' pentru a fi ignorate de joc)", + "pressAnyAnalogTriggerText": "Apasă orice trigger analog...", + "pressAnyButtonOrDpadText": "Apasă orice buton sau dpad-ul...", + "pressAnyButtonText": "Apasă orice buton...", + "pressLeftRightText": "Apasă stânga/dreapta...", + "pressUpDownText": "Apasă sus/jos...", + "runButton1Text": "Buton fugă 1", + "runButton2Text": "Buton fugă 2", + "runTrigger1Text": "Trigger fugă 1", + "runTrigger2Text": "Trigger fugă 2", + "runTriggerDescriptionText": "(trigger-urile analogice te lasă să alergi la viteze diferite)", + "secondHalfText": "Foloseşte această opțiune pentru a configura\na doua parte dintr-un 2-controllere-în-1 controller\ncare se arată ca unul singur.", + "secondaryEnableText": "Permite", + "secondaryText": "Controller secundar", + "startButtonActivatesDefaultDescriptionText": "(opreşte această funcție dacă butonul tău de start este un buton de 'meniu')", + "startButtonActivatesDefaultText": "Butonul Start activează Widget-ul Implicit", + "titleText": "Setup pentru controller", + "twoInOneSetupText": "Setup pentru un controller 2-în-1", + "uiOnlyDescriptionText": "(blochează acest controller pentru a nu putea intra in joc)", + "uiOnlyText": "Limiteaza la controluri", + "unassignedButtonsRunText": "Toate butoanele neatrebuite rulează", + "unsetText": "<șterge>" + }, + "configKeyboardWindow": { + "configuringText": "Se configurează ${DEVICE}", + "keyboard2NoteText": "Notă: multe tastaturi pot inregistra doar câteva apăsări\nodată, deci avănd o a doua tastatură ar merge mai bine\ndacă este un al doilea jucător.\nNotează că va trebui să editezi controlurile la amândoi\njucători, chiar şi în cazul de mai sus." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Dimensiune controluri acțiuni", + "actionsText": "Acțiuni", + "buttonsText": "butoane", + "dragControlsText": "< trage de controluri pentru a le poziționa >", + "joystickText": "joystick", + "movementControlScaleText": "Dimensiune control mișcare", + "movementText": "Mișcare", + "resetText": "Resetare", + "swipeControlsHiddenText": "Ascunde iconițele swipe", + "swipeInfoText": "Modul \"swipe\" are nevoie de puțin antrenament, dar e mai\nușor să joci fără să te uiți la controluri.", + "swipeText": "glisare", + "titleText": "Configurează touchscreen" + }, + "configureItNowText": "Configurează acum?", + "configureText": "Configurează", + "connectMobileDevicesWindow": { + "amazonText": "Amazon appstore", + "appStoreText": "App store", + "bestResultsText": "Pentru cele mai bune rezultate vei avea nevoie de o rețea Wi-Fi \nfără lag. Poți să reduci lag-ul prin oprirea Wi-Fi-ului altor telefoane,\nprin jucarea lângă router, şi prin conectarea \"jocului-mamă\" direct la rețea\nprin Ethernet.", + "explanationText": "Pentru a folosi un telefon inteligent sau o tabletă drept controller, instalează\naplicația \"${REMOTE_APP_NAME}\" pe el. Orice număr de dispozitive se pot conecta la\nun joc ${APP_NAME} peste Wi-Fi, şi este gratis!", + "forAndroidText": "pentru Android:", + "forIOSText": "Pentru iOS:", + "getItForText": "Ia-ți ${REMOTE_APP_NAME} pentru iOS din Apple App Store\nsau pentru Android din Google Play sau Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Pentru a folosi dispozitive mobile ca și controllere:" + }, + "continuePurchaseText": "Continuă pentru ${PRICE}?", + "continueText": "Continuă", + "controlsText": "Controluri", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Acesta nu se aplică clasamentelor din totdeauna", + "activenessInfoText": "Acest multiplicator crește în zilele în\ncare joci și scade în cele in care nu", + "activityText": "Activitate", + "campaignText": "Campanie", + "challengesInfoText": "Primeşte premii pentru completarea minijocurilor.\n\nPremiile şi dificultatea nivelelor cresc\nde fiecare dată când un Challenge este făcut\nşi scade când unul expiră sau este abandonat.", + "challengesText": "Provocări", + "currentBestText": "Best-ul curent", + "customText": "Particularizat", + "entryFeeText": "Intrare", + "forfeitConfirmText": "Abandonezi acest challenge?", + "forfeitNotAllowedYetText": "Acest concurs nu poate fi abandonat acum.", + "forfeitText": "Abandonare", + "multipliersText": "Amplificatori", + "nextChallengeText": "Următorul Challenge", + "nextPlayText": "Următorul joc", + "ofTotalTimeText": "din ${TOTAL}", + "playNowText": "Joacă acum", + "pointsText": "Puncte", + "powerRankingFinishedSeasonUnrankedText": "(ai terminat sezonul fără rank)", + "powerRankingNotInTopText": "(nu eşti în top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} puncte", + "powerRankingPointsMultText": "(x ${NUMBER} puncte)", + "powerRankingPointsText": "${NUMBER} puncte", + "powerRankingPointsToRankedText": "(${CURRENT} din ${REMAINING} puncte)", + "powerRankingText": "Rank de putere", + "prizesText": "Premii", + "proMultInfoText": "Jucătorii cu upgradeul ${PRO} \nprimesc un bonus de ${PERCENT}% la puncte aici.", + "seeMoreText": "Mai mult...", + "skipWaitText": "Treci peste aşteptare", + "timeRemainingText": "Timp Rămas", + "toRankedText": "Către rank", + "totalText": "total", + "tournamentInfoText": "Întrecete pentru scoruri mari\ncu alți jucători din liga ta.\n\nPremiile sunt dăruite jucătorilor cu\nscoruri in top când expiră concursul.", + "welcome1Text": "Bine ai venit la ${LEAGUE}. Poți să-ți măreşti\nrankul prin completarea medaliilor, câştigând trofee\nin concursuri şi prin rankul de 3 stele la jocuri.", + "welcome2Text": "Mai poți primii bilete şi prin alte activități de acelaşi fel.\nBiletele se pot folosi pentru a debloca charactere, hărți, şi\nmini-jocuri, intra in concursurii, şi altele.", + "yourPowerRankingText": "Rankul tău de putere:" + }, + "copyOfText": "Copie de ${NAME}", + "createEditPlayerText": "", + "createText": "Crează", + "creditsWindow": { + "additionalAudioArtIdeasText": "Sunet adițional, Artă, și Idei de ${NAME}", + "additionalMusicFromText": "Muzică adițională de ${NAME}", + "allMyFamilyText": "Toți prietenii și familia care m-au ajutat să testez", + "codingGraphicsAudioText": "Cod, Grafice, şi Audio de ${NAME}", + "languageTranslationsText": "Traduceri:", + "legalText": "Legal:", + "publicDomainMusicViaText": "Public-Muzica din domeniulc ${NAME}", + "softwareBasedOnText": "Acest software este bazat in partea de lucru al lui ${NAME}", + "songCreditText": "${TITLE} Performat de ${PERFORMER}\nCompus de ${COMPOSER}, Aranjat de ${ARRANGER}, Publicat de ${PUBLISHER},\ncurtoazie din ${SOURCE}", + "soundAndMusicText": "Sunete & Muzică:", + "soundsText": "Sunete (${SOURCE}):", + "specialThanksText": "Mulțumiri speciale:", + "thanksEspeciallyToText": "Mulțumiri deosebite: ${NAME}", + "titleText": "Credite ${APP_NAME}", + "whoeverInventedCoffeeText": "Cine a inventat cafeaua" + }, + "currentStandingText": "Rangul tău este #${RANK}", + "customizeText": "Particularizează...", + "deathsTallyText": "${COUNT} vieți pierdute", + "deathsText": "Vieți pierdute", + "debugText": "Eliminare bugguri", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Notă: Este recomandat ca tu să setezi Setări->Grafici->Texturi la 'Mare' când testezi.", + "runCPUBenchmarkText": "Rulează test CPU", + "runGPUBenchmarkText": "Rulează test GPU", + "runMediaReloadBenchmarkText": "Pornește Media-Reîncarcă Contualizator", + "runStressTestText": "Pornește Test-Stres", + "stressTestPlayerCountText": "Numărătoare Playeri", + "stressTestPlaylistDescriptionText": "Lista de Redări pentru Testele de Stres", + "stressTestPlaylistNameText": "Numele listei de redare", + "stressTestPlaylistTypeText": "Tipul Listei de Redare", + "stressTestRoundDurationText": "Durata rundei", + "stressTestTitleText": "Test de Stres", + "titleText": "Teste Stres şi repere", + "totalReloadTimeText": "Timp total de reîncărcare: ${TIME} (vezi logul pentru detalii)" + }, + "defaultGameListNameText": "Lista de redare ${PLAYMODE} normală", + "defaultNewGameListNameText": "${PLAYMODE} Lista mea de redare.", + "deleteText": "Șterge", + "demoText": "Demo", + "denyText": "Refuză", + "desktopResText": "Rezoluție desktop", + "difficultyEasyText": "Ușor", + "difficultyHardOnlyText": "Numai pe \"Greu\"", + "difficultyHardText": "Greu", + "difficultyHardUnlockOnlyText": "Acest nivel poate fi deblocat doar pe \"Greu\".\nCrezi că poți s-o faci?!?!", + "directBrowserToURLText": "Direcționează un browser web la următorul URL:", + "disableXInputDescriptionText": "Permite mai mult de 4 controlere dar nu va merge asa de bine", + "doneText": "Gata", + "drawText": "Egalitate", + "duplicateText": "Multiplica", + "editGameListWindow": { + "addGameText": "Adaugă\nun joc", + "cantOverwriteDefaultText": "Nu poți î locuii lista de redare normală!", + "cantSaveAlreadyExistsText": "Un playlist cu acelaşi nume deja există!", + "cantSaveEmptyListText": "Nu se poate salva un playlist gol!", + "editGameText": "Editează\nUn joc", + "listNameText": "Numele playlistului", + "nameText": "Nume", + "removeGameText": "Şterge\nun joc", + "saveText": "Salvează Lista", + "titleText": "Editor Lista de redare." + }, + "editProfileWindow": { + "accountProfileInfoText": "Acest profil special are nume\nşi imagice bazată pe contul tău.\n\n${ICONS}\n\nCreează profiluri personalizate\npentru a folosi nume sau diferite imagini.", + "accountProfileText": "(profil cont)", + "availableText": "Numele \"${NAME}\" este disponibil.", + "changesNotAffectText": "Notă: schimbările nu vor afecta caracterele care sunt deja în joc", + "characterText": "caracter", + "checkingAvailabilityText": "Se verifică disponibilitatea pentru \"${NAME}\". . .", + "colorText": "culoare", + "getMoreCharactersText": "Ia mai multe...", + "getMoreIconsText": "Adună mai multe imagini...", + "globalProfileInfoText": "Profilurile globale de playeri sunt garantate \nsa aibă nume si imagini unice.", + "globalProfileText": "(profil global)", + "highlightText": "accente", + "iconText": "imagine", + "localProfileInfoText": "Profilurile locale nu au imagini şi numele lor nu sunt \ngarantate să fie speciale. Upgradează-ți profilul\npentru a rezerva un nume special şi să adaugi o imagine.", + "localProfileText": "(profil local)", + "nameDescriptionText": "Nume jucător", + "nameText": "Nume", + "randomText": "aleator", + "titleEditText": "Editează profil", + "titleNewText": "Profil nou", + "unavailableText": "\"${NAME}\" nu este disponibil; încearcă alt nume.", + "upgradeProfileInfoText": "Acest lucru vă va rezerva numele jucătorului\nşi vă va lăsa sa introduceți o imagine pentru el.", + "upgradeToGlobalProfileText": "Upgradează la Profil Global" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Nu poți şterge soundtrackul normal.", + "cantEditDefaultText": "Nu poți edita soundtrackul normal. Duplică-l sau creează unul nou.", + "cantOverwriteDefaultText": "Nu poți înlocui soundtrackul normal", + "cantSaveAlreadyExistsText": "Un soundtrack cu acelaşi nume deja există!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Soundtrack Normal", + "deleteConfirmText": "Ştergi Soundtrackul:\n\n'${NAME}'?", + "deleteText": "Şterge\nSoundtrack", + "duplicateText": "Duplică\nSoundtrack", + "editSoundtrackText": "Editor de Soundtrack", + "editText": "Editează\nSoundtrack", + "fetchingITunesText": "Se încarcă listele de redare de pe iTunes...", + "musicVolumeZeroWarning": "Atenție: volumul muzicii este setat pe 0", + "nameText": "Nume", + "newSoundtrackNameText": "Soundtrackul Meu ${COUNT}", + "newSoundtrackText": "Soundtrack Nou:", + "newText": "Soundtrack\nNou", + "selectAPlaylistText": "Selectează o Listă de Redare", + "selectASourceText": "Sursa Muzicii", + "testText": "test", + "titleText": "Soundtrack-uri", + "useDefaultGameMusicText": "Muzică Normală de Joc", + "useITunesPlaylistText": "Listă de redare iTunes", + "useMusicFileText": "Fişier de Muzica (mp3, etc)", + "useMusicFolderText": "Folder pentru Fişierele de Muzică" + }, + "editText": "Editeaza", + "endText": "Sfârșit", + "enjoyText": "Bucurați-vă!", + "epicDescriptionFilterText": "${DESCRIPTION} În slow motion epic.", + "epicNameFilterText": "${NAME} Epic", + "errorAccessDeniedText": "acces respins", + "errorOutOfDiskSpaceText": "rămas fără memorie", + "errorText": "Eroare", + "errorUnknownText": "eroare necunoscută", + "exitGameText": "Închizi ${APP_NAME}?", + "exportSuccessText": "'${NAME}' exportat", + "externalStorageText": "Depozit External", + "failText": "Eșuare", + "fatalErrorText": "Oh nu; ceva lipseşte sau este stricat.\nTe rog încearcă să reinstalezi aplicația sau\ncontactează ${EMAIL} pentru ajutor.", + "fileSelectorWindow": { + "titleFileFolderText": "Selectează un fișier sau un folder", + "titleFileText": "Selectează un fișier", + "titleFolderText": "Selectează un folder", + "useThisFolderButtonText": "Folosește acest folder" + }, + "finalScoreText": "Scor final", + "finalScoresText": "Scoruri Finale", + "finalTimeText": "Timp Final", + "finishingInstallText": "Se termină de instalat; un moment...", + "fireTVRemoteWarningText": "* Pentru o experiență mai bună,\nfolosiți Controllere sau instalați\naplicația '${REMOTE_APP_NAME}' pe telefon\nsau tabletă.", + "firstToFinalText": "Finala Primul-la-${COUNT}", + "firstToSeriesText": "Seriile Primul-la-${COUNT}", + "fiveKillText": "Penta-omor!!!", + "flawlessWaveText": "Val perfect!", + "fourKillText": "Cvadr-omor!!!", + "friendScoresUnavailableText": "Scorurile prietenilor nu sunt disponibile.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Liderii Jocului ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "Nu poți şterge lista de redare normală.", + "cantEditDefaultText": "Nu poți edita lista de redare normală! Duplic-o sau creează una nouă.", + "cantShareDefaultText": "Nu poti trimite playlistul default", + "deleteConfirmText": "Ştergi \"${LIST}\"?", + "deleteText": "Şterge\nlistă de redare", + "duplicateText": "Duplică\nO Listă de redare", + "editText": "Editează\no listă de redare", + "newText": "Listă de redare\nNouă", + "showTutorialText": "Arată Tutorialul", + "shuffleGameOrderText": "Hărți la nimereală", + "titleText": "Particularizează Listele de Redare de ${TYPE}" + }, + "gameSettingsWindow": { + "addGameText": "Adaugă joc" + }, + "gamesToText": "${WINCOUNT} jocuri la ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Ține minte: orice dispozitiv din grup poate avea\nmai mult de un jucător dacă sunt destule controllere.", + "aboutDescriptionText": "Folosește aceste tab-uri pentru a asambla un grup.\n\nGrupurile te lasă să joci jocuri și turnee împreună\ncu prietenii tăi, folosind mai multe dispozitive.\n\nFolosește butonul ${PARTY} din dreapta-sus pentru a\nvorbi și interacționa cu grupul.\n(dacă folosești un controller, folosește ${BUTTON} cât timp ești într-un meniu)", + "aboutText": "Despre", + "addressFetchErrorText": "", + "appInviteInfoText": "Invită-ți prietenii să încerce BombSquad și\nvor primii ${COUNT} bilete gratis. Tu vei primi\n${YOU_COUNT} pentru fiecare care o face.", + "appInviteMessageText": "${NAME} ți-a trimis ${COUNT} de bilete în ${APP_NAME}", + "appInviteSendACodeText": "Trimite-i un cod", + "appInviteTitleText": "Invitație pentru ${APP_NAME}", + "bluetoothAndroidSupportText": "(funcționează cu orice dispozitiv android care suportă bluetooth)", + "bluetoothDescriptionText": "Ține/intră într-un joc folosind bluetooth:", + "bluetoothHostText": "Ține un joc bluetooth", + "bluetoothJoinText": "Intră într-un joc bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "se verifică...", + "disconnectClientsText": "Aceasta va deconecta ${COUNT} jucător(i) din\ngrupul tău curent. Ești sigur?", + "earnTicketsForRecommendingAmountText": "Prietenii vor primii ${COUNT} tichete daca vor incerca jocul \n(iar tu vei primii ${YOU_COUNT} pentru fiecare care incearca)", + "earnTicketsForRecommendingText": "Împărtăşeşte jocul\npentru bilete gratuite...", + "emailItText": "Dă-l prin Email", + "friendHasSentPromoCodeText": "${COUNT} Bilete ${APP_NAME} de la ${NAME}", + "friendPromoCodeAwardText": "Vei primi ${COUNT} de bilete de fiecare dată când este folosit.", + "friendPromoCodeExpireText": "Codul v-a expira în ${EXPIRE_HOURS} ore şi merge doar pentru jucătorii noi.", + "friendPromoCodeInstructionsText": "Pentru al folosi, deschide BombSquad şi mergi la \"Setări->Avansat->Introdu cod Promo\".\nVezi bombsquadgame.com pentru linkuri de download pentru toate platformele suportate.", + "friendPromoCodeRedeemLongText": "Poate fi înscris pentru ${COUNT} de bilete gratuite de către un maxim de ${MAX_USES} de persoane.", + "friendPromoCodeRedeemShortText": "Poate fi înscris pentru ${COUNT} de bilete în joc.", + "friendPromoCodeWhereToEnterText": "(în \"Setări->Avansat->Introdu cod Promo\")", + "getFriendInviteCodeText": "Cere un cod de invitare pentru prieteni", + "googlePlayDescriptionText": "Invită jucători Google Play în grupul tău:", + "googlePlayInviteText": "Invită", + "googlePlayReInviteText": "Sunt ${COUNT} jucători Google Play în grupul tău care vor\nfi deconectați dacă faci o altă invitație. Include-i și\npe ei în noua invitație pentru a-i avea înapoi în grup.", + "googlePlaySeeInvitesText": "Vezi invitații", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Numai pentru versiunea Android/Google Play)", + "inDevelopmentWarningText": "Notă:\n\nJocul peste rețea este încă o opțiune în dezvoltare.\nDeocamdată se recomandă ca toți jucătorii să fie pe\naceiași rețea Wi-Fi.", + "internetText": "Internet", + "inviteAFriendText": "Prietenii nu au jocul? Invită-i să-l\nîncerce şi vor primii ${COUNT} de bilete gratis.", + "inviteFriendsText": "Invită Prieteni", + "localNetworkDescriptionText": "Intră într-un grup de pe rețeaua ta:", + "localNetworkText": "Rețea locală", + "manualAddressText": "Adresă", + "manualConnectText": "Conectare", + "manualDescriptionText": "Intră într-un grup la adresa:", + "manualJoinableFromInternetText": "Se poate conecta la tine de pe internet?:", + "manualJoinableNoWithAsteriskText": "NU*", + "manualJoinableYesText": "DA", + "manualRouterForwardingText": "*pentru a repara asta, configurează router-ul tău să dea forward la portul UDP ${PORT} către adresa ta locală", + "manualText": "Manual", + "manualYourAddressFromInternetText": "Adresa ta de pe internet:", + "manualYourLocalAddressText": "Adresa ta locală:", + "noConnectionText": "", + "otherVersionsText": "(alte versiuni)", + "partyInviteAcceptText": "Acceptă", + "partyInviteDeclineText": "Refuză", + "partyInviteGooglePlayExtraText": "(vezi tab-ul 'Google Play' din fereastra 'Adunare')", + "partyInviteIgnoreText": "Ignoră", + "partyInviteText": "${NAME} te-a invitat\nsă intri în grupul lui/ei!", + "partyNameText": "Numele grupului", + "requestingAPromoCodeText": "Se cere codul...", + "sendDirectInvitesText": "Trimite Invitați Direct", + "shareThisCodeWithFriendsText": "Împărtăşeşte codul ăsta cu prietenii:", + "titleText": "Adunare", + "wifiDirectDescriptionBottomText": "Dacă toate dispozitivele au un panou 'Wi-Fi Direct', ar trebui să poată să îl folosească să\nse găsească și să se conecteze unii la alții. Când toate dispozitivele sunt conectate se pot\nforma grupuri, aici, folosind tab-ul 'Rețea locală', ca și când ați fi pe aceiași rețea Wi-Fi.\n\nPentru cele mai bune rezultate, host-ul Wi-Fi Direct ar trebui să fie și host-ul grupului ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi direct se poate folosi pentru conectarea dispozitivelor Android fără\na folosi o rețea Wi-Fi. Aceasta funcționează (bine) pe Android 4.2 sau mai nou.\n\nPentru a folosi Wi-Fi direct, deschide setările Wi-Fi și caută 'Wi-Fi Direct' în meniu.", + "wifiDirectOpenWiFiSettingsText": "Deschide setări Wi-Fi", + "wifiDirectText": "Wi-fi direct", + "worksBetweenAllPlatformsText": "(funcționează între toate platformele)", + "worksWithGooglePlayDevicesText": "(funcționează pe orice dispozitiv care are versiunea Google Play(android) a jocului)", + "youHaveBeenSentAPromoCodeText": "Ai primit un cod Promo ${APP_NAME}:" + }, + "getTicketsWindow": { + "freeText": "GRATIS!", + "freeTicketsText": "Bilete gratis", + "inProgressText": "O tranzacție e în progres; reîncearcă în câteva secunde.", + "purchasesRestoredText": "Cumpărări resetate.", + "receivedTicketsText": "Ai primit ${COUNT} bilete!", + "restorePurchasesText": "Resetează Cumpărări", + "ticketDoublerText": "Dublator Bilete", + "ticketPack1Text": "Pachet bilete mic", + "ticketPack2Text": "Pachet bilete mediu", + "ticketPack3Text": "Pachet bilete mare", + "ticketPack4Text": "Super-pachet bilete", + "ticketPack5Text": "Pachet bilete MAMUT", + "ticketPack6Text": "Pachet bilete extrem", + "ticketsFromASponsorText": "Ia ${COUNT} bilete de\nla un sponsor", + "ticketsText": "${COUNT} Bilete", + "titleText": "Ia bilete", + "unavailableLinkAccountText": "Scuze, dar cumpăratul nu funcționează pe această platformă.\nDacă dorești, poți conecta acest cont cu unul de\npe o altă platformă și să faci cumpărăturile acolo.", + "unavailableTemporarilyText": "Acest serviciu e indisponibil deocamdată; încearcă mai târziu.", + "unavailableText": "Scuze, aceasta e indisponibilă.", + "versionTooOldText": "Scuze, dar versiunea jocului e prea veche; dă update pentru a lua una mai nouă.", + "youHaveShortText": "tu ai ${COUNT}", + "youHaveText": "ai ${COUNT} bilete" + }, + "googlePlayText": "Magazin Play", + "graphicsSettingsWindow": { + "alwaysText": "Întotdeauna", + "fullScreenCmdText": "Fullscreen (Cmd+F)", + "fullScreenCtrlText": "Fullscreen (Ctrl+F)", + "gammaText": "Luminozitate", + "highText": "Înalt", + "higherText": "Mai înalt", + "lowText": "Slab", + "mediumText": "Mediu", + "neverText": "Niciodată", + "resolutionText": "Rezoluție", + "showFPSText": "Arată FPS", + "texturesText": "Texturi", + "titleText": "Grafică", + "tvBorderText": "Margine TV", + "verticalSyncText": "V-sync", + "visualsText": "Vizual" + }, + "helpWindow": { + "bombInfoText": "- Bomba -\nMai puternică decât pumnii, dar poate\nrezulta în a te lovi pe tine însuți.\nPentru rezultate pozitive, aruncă\nînspre inamici înainte să se termine fitilul.", + "canHelpText": "${APP_NAME} poate ajuta.", + "controllersInfoText": "Poți juca BombSquad cu prietenii peste o rețea, sau dacă aveți\ndestule controllere, pe același dispozitiv. BombSquad suportă o\nmare varietate de controllere; Poți folosi până și telefoane\nca unul descărcând aplicația \"BombSquad remote\".\nVezi Setări->Controllere pentru mai multe informații.", + "controllersText": "Controllere", + "controlsSubtitleText": "Caracterul tău BombSquad poate face următoarele acțiuni:", + "controlsText": "Controluri", + "devicesInfoText": "Versiunea VR a jocului poate fi jucată peste rețea cu versiunea\nnormală, deci scoate telefoanele, tabletele și calculatoarele\nși să înceapă jocul! Poți conecta versiunea VR la cea normală\npentru a putea lăsa alte persoane să ia parte la acțiune ca\nși spectatori.", + "devicesText": "Dispozitive", + "friendsGoodText": "E bine să ai și de-aceștia. ${APP_NAME} e și mai amuzant cu mai\nmulți jucători și suportă până la 8 deodată, ce ne duce la:", + "friendsText": "Prieteni", + "jumpInfoText": "- Sari -\nSari peste gropi mici,\naruncă lucruri mai înalt\nși exprimă-ți fericirea.", + "orPunchingSomethingText": "Sau să dai cu pumnii în ceva, să arunci acel cava într-o prăpastie și să explodeze până jos de la o bombă lipicioasă.", + "pickUpInfoText": "- Ridică -\nIa steaguri, inamici, sau orice\ncare nu e fixat de pământ.\nApasă încă odată pentru a arunca.", + "powerupBombDescriptionText": "Te lasă să poți arunca 3 bombe\ndeodată în loc de numai una.", + "powerupBombNameText": "Bombe triple", + "powerupCurseDescriptionText": "Ar trebui să eviți astea.\n ...sau oare?", + "powerupCurseNameText": "Blestem", + "powerupHealthDescriptionText": "Îți regenerează viața la maxim.\nNici nu ai fi ghicit.", + "powerupHealthNameText": "Trusă de prim-ajutor", + "powerupIceBombsDescriptionText": "Mai slabe decât bombele\nnormale, dar îți lasă inamicii\nînghețați și aparent fragili.", + "powerupIceBombsNameText": "Bombe de gheață", + "powerupImpactBombsDescriptionText": "Mai slabe decât bombele normale,\ndar explodează la impact.", + "powerupImpactBombsNameText": "Bombe de impact", + "powerupLandMinesDescriptionText": "Acestea vin câte 3; Folositoare\npentru apărarea bazelor sau\npentru a opri inamici vitezomani.", + "powerupLandMinesNameText": "Mine", + "powerupPunchDescriptionText": "Dai cu pumnii mai repede,\nmai bine și mai puternic.", + "powerupPunchNameText": "Mănuși de box", + "powerupShieldDescriptionText": "Absoarbe daunele ca să\nnu o faci tu.", + "powerupShieldNameText": "Scut-Energic", + "powerupStickyBombsDescriptionText": "Se lipesc de orice ating.\nHilaritate garantată.", + "powerupStickyBombsNameText": "Bombe Lipicioase", + "powerupsSubtitleText": "Desigur, nici un joc e joc fără powerup-uri:", + "powerupsText": "Powerup-uri", + "punchInfoText": "- Pumnii -\nPumnii dăunează mai mult cu cât\nse mișcă mai repede, deci aleargă\nși rotește-te ca un dement!", + "runInfoText": "- Fugi -\nȚine apăsat ORICE buton pentru a fugi. Triggerele sau butoanele de umăr funcționează dacă le ai.\nFugitul te ajută să ajungi repede în locuri, deși virezi greu, deci ai grijă la prăpastii.", + "someDaysText": "În unele zile pur și simplu vrei să lovești ceva. Sau să explodezi altceva.", + "titleText": "Ajutor BombSquad", + "toGetTheMostText": "Pentru a vedea tot ce are jocul acesta, vei avea nevoie de:", + "welcomeText": "Bine ai venit la BombSquad!" + }, + "holdAnyButtonText": "<ține apăsat orice buton>", + "holdAnyKeyText": "<ține apăsată orice tastă>", + "hostIsNavigatingMenusText": "- ${HOST} navighează meniurile boss de boss -", + "installDiskSpaceErrorText": "EROARE: Nu s-a putut completa instalarea.\nSe poate să fi rămas fără spațiu pe dispozitiv.\nEliberează niște spațiu și reîncearcă.", + "internal": { + "arrowsToExitListText": "Apasă ${LEFT} sau ${RIGHT} pentru a ieși din listă", + "buttonText": "buton", + "connectedToPartyText": "Ai intrat în grupul lui ${NAME}!", + "connectingToPartyText": "Se conectează...", + "connectionFailedHostAlreadyInPartyText": "Conexiunea a eşuat; hostul este în altă petrecere", + "connectionFailedText": "Conexiunea a eşuat.", + "connectionFailedVersionMismatchText": "Conexiunea a eşuat; hostul rulează o versiune diferită a jocului.\nAsigurați-vă ca aveți amândoi cea mai nouă versiune a jocului şi incercați din nou.", + "connectionRejectedText": "Conexiune Respinsă.", + "controllerConnectedText": "${CONTROLLER} conectat.", + "controllerDetectedText": "1 controller detectat.", + "controllerDisconnectedText": "${CONTROLLER} a ieşit.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} a ieşit. Încearcă să-l conectezi din nou.", + "controllerReconnectedText": "${CONTROLLER} reconectat.", + "controllersConnectedText": "${COUNT} controllere conectate.", + "controllersDetectedText": "${COUNT} controllere detectate.", + "controllersDisconnectedText": "${COUNT} controllere au ieşit.", + "corruptFileText": "Fişiere Corupte detectate. Reinstalează jocul, sau dă email la ${EMAIL}", + "errorPlayingMusicText": "Eroare la începerea muzicii 0: ${MUSIC}", + "errorResettingAchievementsText": "Nu se pot reseta medaliile; încearcă din nou mai tărziu.", + "hasMenuControlText": "${NAME} are controlul meniului.", + "incompatibleVersionHostText": "Hostul rulează o versiune diferită a jocului.\nAsigurați-vă că aveți cea mai nouă versiune şi încercați din nou.", + "incompatibleVersionPlayerText": "${NAME} rulează o versiune diferită a jocului.\nAsigurați-vă că aveți cea mai nouă versiune si reîncercați.", + "invalidAddressErrorText": "Eroare: adresă invalidă.", + "invitationSentText": "Invitație Trimisă.", + "invitationsSentText": "${COUNT} (de) invitații trimise.", + "joinedPartyInstructionsText": "Un prieten a venit la petrecerea ta.\nDu-te la 'Joacă' pentru a începe un joc.", + "keyboardText": "Tastatură", + "kickIdlePlayersKickedText": "Îl dăm afară pe ${NAME} pentru că nu face nimic.", + "kickIdlePlayersWarning1Text": "${NAME} va fi dat afară în ${COUNT} (de) secunde dacă tot nu face nimic.", + "kickIdlePlayersWarning2Text": "(poți opri aceasta în Setări->Avansat)", + "leftPartyText": "Ai ieşit din petrecerea lui ${NAME}.", + "noMusicFilesInFolderText": "Folderul nu conține niciun fişier de muzică.", + "playerJoinedPartyText": "${NAME} a venit la petrecere!", + "playerLeftPartyText": "${NAME} a plecat de la petrecere.", + "rejectingInviteAlreadyInPartyText": "Se declină invitația (eşti deja într-o petrecere).", + "signInErrorText": "Eroare la Conectare.", + "signInNoConnectionText": "Nu se poate conecta. (fără conexiune la internet?)", + "telnetAccessDeniedText": "Eroare: utilizatorul nu are acces la telnet.", + "timeOutText": "(rămâne fără timp în ${TIME} (de) secunde)", + "touchScreenJoinWarningText": "Ai intrat cu touchscreen-ul.\nDacă a fost o greşeală, apasă 'Meniu-Sfârşeşte Joc'.", + "touchScreenText": "TouchScreen", + "unavailableNoConnectionText": "Aceasta nu este disponibilă acum (fără conexiune la internet?)", + "vrOrientationResetCardboardText": "Foloseşte această opțiune pentru a reseta orientatea RV.\nCa să joci jocul ai nevoie de un controller external.", + "vrOrientationResetText": "Orientare RV resetată.", + "willTimeOutText": "(se va reseta dacă nu face ceva)" + }, + "jumpBoldText": "SARI", + "jumpText": "Sari", + "keepText": "Păstrează", + "keepTheseSettingsText": "Păstrează aceste setări?", + "killsTallyText": "${COUNT} (de) ucideri", + "killsText": "Omoruri", + "kioskWindow": { + "easyText": "Uşor", + "epicModeText": "Modul Epic", + "fullMenuText": "Meniu Full", + "hardText": "Greu", + "mediumText": "Mediu", + "singlePlayerExamplesText": "Singur / Mostre Co-Op", + "versusExamplesText": "Mostre Versus" + }, + "languageSetText": "Limba este acum \"${LANGUAGE}\"", + "lapNumberText": "Tura ${CURRENT}/${TOTAL}", + "lastGamesText": "(ultimele ${COUNT} (de) jocuri)", + "leaderboardsText": "Clasamente", + "league": { + "allTimeText": "Din totdeauna", + "currentSeasonText": "Sezon curent (${NUMBER})", + "leagueFullText": "Liga de ${NAME}", + "leagueRankText": "Rang în ligă", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga${SUFFIX}", + "seasonEndedDaysAgoText": "Sezonul s-a sfârșit acum ${NUMBER} zile.", + "seasonEndsDaysText": "Sezonul se sfârșește în ${NUMBER} zile.", + "seasonEndsHoursText": "Sezonul se sfârșește în ${NUMBER} ore.", + "seasonEndsMinutesText": "Sezonul se sfârșește în ${NUMBER} minute.", + "seasonText": "Sezonul ${NUMBER}", + "tournamentLeagueText": "Trebuie să ajungi în liga ${NAME} pentru a intra în acest turneu.", + "trophyCountsResetText": "Numărul de trofee se va reseta la sfârșitul sezonului." + }, + "levelBestScoresText": "Cele mai bune scoruri pe ${LEVEL}", + "levelBestTimesText": "Cele mai bune timpuri pe ${LEVEL}", + "levelFastestTimesText": "Cel mai rapid timp pe ${LEVEL}", + "levelHighestScoresText": "Cel mai mare scor pe ${LEVEL}", + "levelIsLockedText": "${LEVEL} este blocat.", + "levelMustBeCompletedFirstText": "${LEVEL} trebuie terminat întâi.", + "levelText": "Nivelul ${NUMBER}", + "levelUnlockedText": "Nivel Deblocat!", + "livesBonusText": "Vieți Bonus", + "loadingText": "se încarcă", + "mainMenu": { + "creditsText": "Credite", + "demoMenuText": "Meniu Demo", + "endGameText": "Sfârșește joc", + "exitGameText": "Ieși din joc", + "exitToMenuText": "Te întorci la meniu?", + "howToPlayText": "Cum joc?", + "justPlayerText": "(Doar ${NAME})", + "leaveGameText": "Părăsește joc", + "leavePartyConfirmText": "Sigur părăsești grupul?", + "leavePartyText": "Părăsește grup", + "quitText": "Ieși", + "resumeText": "Continuă", + "settingsText": "Setări" + }, + "makeItSoText": "Așa să fie!", + "mapSelectGetMoreMapsText": "Vezi mai multe...", + "mapSelectText": "Selectează", + "mapSelectTitleText": "Hărți ${GAME}", + "mapText": "Hartă", + "mostValuablePlayerText": "Cel mai valoros jucător", + "mostViolatedPlayerText": "Cel mai violat jucător", + "mostViolentPlayerText": "Cel mai violent jucător", + "moveText": "Mișcă-te", + "multiKillText": "${COUNT} ucideri!!!", + "multiPlayerCountText": "${COUNT} (de) jucători", + "mustInviteFriendsText": "Notă: trebuie să inviți prieteni pe \npanelul \"${GATHER}\" sau ataşează c\ncontroalăre pentru a juca multiplayer.", + "nameBetrayedText": "${NAME} a trădat pe ${VICTIM}", + "nameDiedText": "${NAME} a murit.", + "nameKilledText": "${NAME} a omorât pe ${VICTIM}", + "nameNotEmptyText": "Numele nu poate fi gol!", + "nameScoresText": "${NAME} înscrie!", + "nameSuicideKidFriendlyText": "${NAME} a murit din greșeală.", + "nameSuicideText": "${NAME} s-a sinucis.", + "nativeText": "nativ", + "newPersonalBestText": "Best Personal Nou!", + "newTestBuildAvailableText": "Un Test Build nou este lansat! (${VERSION} build ${BUILD}).\nIa-o pe ${ADDRESS}", + "newText": "Nou", + "newVersionAvailableText": "O versiune nouă pentru BombSquad este disponibila! (${VERSION})", + "nextAchievementsText": "Realizările următoare:", + "nextLevelText": "Nivelul următor", + "noAchievementsRemainingText": "- niciunul", + "noContinuesText": "(fără continuări)", + "noExternalStorageErrorText": "Nu a fost găsit niciun depozit External pe acest dispozitiv.", + "noGameCircleText": "Eroare: nu eşti logat în GameCircle", + "noProfilesErrorText": "Nu ai niciun Profil de Jucător, deci eşti blocat cu '${NAME}'.\nDute la Setări->Profile de Jucător pentru a-ți face un profil.", + "noScoresYetText": "Niciun scor deocamdată.", + "noThanksText": "Nu mulțumesc", + "noValidMapsErrorText": "Nu s-a găsit nici-o hartă validă pentru acest mod de joc.", + "notEnoughPlayersRemainingText": "Nu au rămas destui jucători; ieşi şi începe un joc nou.", + "notEnoughPlayersText": "Ai nevoie de cel puțin ${COUNT} jucători pentru a începe acest joc!", + "notNowText": "Nu Acum", + "notSignedInErrorText": "Trebuie să fi conectat pentru a face asta.", + "notSignedInGooglePlayErrorText": "Trebuie să fi conectat prin Google Play pentru a face asta.", + "notSignedInText": "nu este conectat", + "nothingIsSelectedErrorText": "Nimic nu este selectat!", + "numberText": "#${NUMBER}", + "offText": "Oprit", + "okText": "Ok", + "onText": "Pornit", + "onslaughtRespawnText": "${PLAYER} va reveni în valul ${WAVE}", + "orText": "${A} sau ${B}", + "outOfText": "(#${RANK} din ${ALL})", + "ownFlagAtYourBaseWarning": "Steagul tău trebuie să fie\nla bază pentru a înscrie!", + "packageModsEnabledErrorText": "Network-play nu este activat cănd modurile-local-package sunt active (vezi Setări->Avansat)", + "partyWindow": { + "chatMessageText": "Mesaje", + "emptyText": "Petrecerea ta este goală", + "hostText": "(host)", + "sendText": "Trimite", + "titleText": "Petrecerea Ta" + }, + "pausedByHostText": "(pus pe pauză)", + "perfectWaveText": "Val Perfect!", + "pickUpText": "Ridică", + "playModes": { + "coopText": "Co-operare", + "freeForAllText": "Fiecare pentru el", + "multiTeamText": "Mai multe echipe", + "singlePlayerCoopText": "Un singur jucător / Cooperare", + "teamsText": "Echipe" + }, + "playText": "Joacă", + "playWindow": { + "oneToFourPlayersText": "1-4 jucători", + "titleText": "Joacă", + "twoToEightPlayersText": "2-8 jucători" + }, + "playerCountAbbreviatedText": "${COUNT} jucători", + "playerDelayedJoinText": "${PLAYER} va intra la începutul jocului următor.", + "playerInfoText": "Info Jucător", + "playerLeftText": "${PLAYER} a ieşit din joc.", + "playerLimitReachedText": "Limita de ${COUNT} jucători atinsă; nimeni nu mai are voie să intre.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Nu poți să-ți ștergi profilul de cont.", + "deleteButtonText": "Șterge\nprofil", + "deleteConfirmText": "Șterge '${PROFILE}'?", + "editButtonText": "Editează\nProfil", + "explanationText": "(crează nume & aparențe personalizate pentru jucătorii se pe acest dispozitiv)", + "newButtonText": "Profil\nNou", + "titleText": "Profile jucător" + }, + "playerText": "Jucător", + "playlistNoValidGamesErrorText": "Această listă de redare nu conține jocuri deblocate valide.", + "playlistNotFoundText": "lista de redare nu a fost găsită", + "playlistsText": "Liste de redare", + "pleaseRateText": "Dacă îți place BombSquad, consideră să-ți iei un moment să\nevaluezi jocul sau să scrii o revizuire. Aceasta furnizează\nfeedback folositor și ajută să suporte dezvoltarea jocului în viitor.\n\nmulțumesc!\n-eric", + "practiceText": "Antrenament", + "pressAnyButtonPlayAgainText": "Apasă orice buton pentru a juca din nou...", + "pressAnyButtonText": "Apasă orice buton pentru a continua...", + "pressAnyButtonToJoinText": "apasă orice buton pentru a veni...", + "pressAnyKeyButtonPlayAgainText": "Apasă orice buton/tastă pentru a juca din nou...", + "pressAnyKeyButtonText": "Apasă orice buton/tastă pentru a continua...", + "pressAnyKeyText": "Apasă orice tastă...", + "pressJumpToFlyText": "** Apasă Săritura de multe ori pentru a zbura **", + "pressPunchToJoinText": "apasă PUMN pentru a veni...", + "pressToOverrideCharacterText": "apasă ${BUTTONS} pentru ați înlocuii caracterul", + "pressToSelectProfileText": "apasă ${BUTTONS} pentru ați selecta caracterul", + "pressToSelectTeamText": "apasă ${BUTTONS} pentru ați selecta echipa", + "promoCodeWindow": { + "codeText": "Cod", + "codeTextDescription": "Cod Promoțional", + "enterText": "Enter" + }, + "promoSubmitErrorText": "Eroare la introducerea codului; verifică conexiunea la internet", + "ps3ControllersWindow": { + "macInstructionsText": "Închide sursa de putere din spatele PS3-ului, asigură-te că \nBluetooth-ul este pornit în Mac-ul tău, apoi conectează controllerul\nprintr-un cablu USB pentru a le conecta. De acum înainte vei putea\nfolosi butonul de home de pe controller pentru al conecta în Mac-ul tău\nprin USB sau prin Bluetooth (apăsând pe butonul HOME).\n\nÎn unele Mac-uri s-ar putea să apară o fereastră care va cere un cod când conectați controllerul.\nDacă se întâmplă acest lucru, vezi tutorialul de mai jos sau caută pe google.\n\n\n\n\nControllerele PS3 conectate fără fir as trebui să apară în\nDevice List în System Preferences->Bluetooth. Ar trebuii să le ştergi de\npe acea listă dacă vrei să le foloseşti cu PS3-ul tău din nou.\n\nDesigur asigură-te ca le desconectezi din Bluetooth când nu le foloseşti \nsau bateria lor va continua să scadă.\n\nBluetooth-ul ar trebuii să țină până la 7 dispozitive conectate,\ndar acest lucru s-ar putea să varieze.", + "ouyaInstructionsText": "Pentru a folosi un controller de PS3 cu OUYA-ul tău, conectează-l simplu printr-un cablu USB\no dată pentru al conecta. Acest lucru s-ar putea să-ți scoată celelalte controllere, deci \natunci ar trebuii să-ți restartezi OUYA-ul şi să scoți cablul USB.\n\nDupă vei putea conecta controllerul simplu prin apăsarea butonului HOME.\nCând ai terminat de jucat, ține apăsat pe butonul de HOME pentru\n10 secunde pentru a închide controllerul; sau va rămâne conectat\nşi va irosi baterii.", + "pairingTutorialText": "video tutorial pentru pairing", + "titleText": "Folosind controllere PS3 cu BombSquad:" + }, + "punchBoldText": "PUMN", + "punchText": "Pumn", + "purchaseForText": "Cumpără pentru ${PRICE}", + "purchaseGameText": "Cumpără Joc", + "purchasingText": "Se cumpără...", + "quitGameText": "Părăsești ${APP_NAME}?", + "quittingIn5SecondsText": "Se închide în 5 secunde...", + "randomPlayerNamesText": "Andrei, Marius, Marian, Cosmin, Cristi, Ioana, Gabi, Călin, Teodor, Mihai, Alex, Alexandra, Bogdan, Cătălin, Ştefan, George.", + "randomText": "La Nimereală", + "rankText": "Rank", + "ratingText": "Evaluare", + "reachWave2Text": "Ajunge la valul 2 pentru ca scorul tău să fie rankat", + "readyText": "pregătit", + "recentText": "Recent", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Poziție butoane", + "button_size": "Dimensiune butoane", + "cant_resolve_host": "Nu se poate găsi gazda.", + "capturing": "Se capturează...", + "connected": "Conectat.", + "description": "Folosește-ți telefonul sau tableta ca un controller pt. BombSquad.\nPoți conecta până la 8 dispozitive simultan pentru o nebunie multiplayer pe un singur TV sau o tabletă.", + "disconnected": "Deconectat de către server.", + "dpad_fixed": "fixat", + "dpad_floating": "liber", + "dpad_position": "Poziție D-Pad", + "dpad_size": "Dimensiune D-Pad", + "dpad_type": "Tip D-Pad", + "enter_an_address": "Introdu o adresă", + "game_full": "Jocul e plin sau nu acceptă conexiuni noi.", + "game_shut_down": "Jocul s-a oprit.", + "hardware_buttons": "Butoane Hardware", + "join_by_address": "Conectează-te după adresă...", + "lag": "Lag: ${SECONDS} secunde", + "reset": "Resetează la valorile implicite", + "run1": "Alergat 1", + "run2": "Alergat 2", + "searching": "Se caută jocuri BombSquad...", + "searching_caption": "Apasă pe numele unui joc pentru a intra în el.\nFii sigur că ești pe aceiași rețea WiFi ca și jocul.", + "start": "Start", + "version_mismatch": "Nepotrivire versiune.\nFii sigur că BombSquad și BombSquad Remote\nau ambele ultima versiune și reîncearcă." + }, + "removeInGameAdsText": "Deblochează \"${PRO}\" în magazin pentru a şterge ad-urile din joc.", + "renameText": "Redenumeşte", + "replayEndText": "Sfărşeşte Reluarea", + "replayNameDefaultText": "Reluarea din ultimul joc", + "replayReadErrorText": "Eroare la citirea fişierului de reluare.", + "replayRenameWarningText": "Redenumeşte \"${REPLAY}\" după un joc dacă vrei s-o păstrezi; altfel se va înlocui.", + "replayVersionErrorText": "Scuze, această reluare a fost făcută în altă versiune\nde joc şi nu poate fi folosită.", + "replayWatchText": "Vezi Reluarea", + "replayWriteErrorText": "Eroare la scrierea fişierului de reluare.", + "replaysText": "Reluări", + "reportPlayerExplanationText": "Foloseşte acest email pentru a raporta trişări, limbaj licențios, sau alte tipuri de comportament rău.\nTe rog descrie mai jos:", + "reportThisPlayerCheatingText": "Trișează", + "reportThisPlayerLanguageText": "Limbaj nepotrivit", + "reportThisPlayerReasonText": "Ce dorești să reclami?", + "reportThisPlayerText": "Reportează acest Jucător", + "requestingText": "Se încarcă...", + "restartText": "Restart", + "retryText": "Reîncearcă", + "revertText": "Revin-o la normal", + "runText": "Fugi", + "saveText": "Salvează", + "scoreChallengesText": "Provocări de Scor", + "scoreListUnavailableText": "Listă de Scoruri nedisponibilă.", + "scoreText": "Scor", + "scoreUnits": { + "millisecondsText": "Milisecunde", + "pointsText": "Puncte", + "secondsText": "Secunde" + }, + "scoreWasText": "(a fost ${COUNT})", + "selectText": "Selectează", + "seriesWinLine1PlayerText": "CÂŞTIGĂ", + "seriesWinLine1TeamText": "CÂŞTIGĂ", + "seriesWinLine1Text": "CÂŞTIGĂ", + "seriesWinLine2Text": "SERIILE", + "settingsWindow": { + "accountText": "Cont", + "advancedText": "Avansat", + "audioText": "Audio", + "controllersText": "Controllere", + "graphicsText": "Grafici", + "playerProfilesText": "Profile de Jucător", + "titleText": "Setări" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(o simplă, pentru controller tastatură pe-ecran pentru editarea textului)", + "alwaysUseInternalKeyboardText": "Foloseşte mereu tastatura Internală", + "benchmarksText": "Teste-Stres şi referințe", + "enablePackageModsDescriptionText": "Activează mai multe capabilități dar dezactivează net-play)", + "enablePackageModsText": "Activează moduri cu pachete locale", + "enterPromoCodeText": "Introdu un cod Promo", + "forTestingText": "Notă: aceste valori sunt doar pentru teste şi for fi pierdute când aplicația se închide.", + "helpTranslateText": "Translatările BombSquad sunt un efort depus de comunitate.\nDacă ai vrea să te implici la translatarea unei limbi,\nurmează linkul de mai jos. Mulțumiri în plus!", + "kickIdlePlayersText": "Dă afară jucătorii care nu fac nimic", + "kidFriendlyModeText": "Modul Pentru Copii (mai puțina violența, etc)", + "languageText": "Limbă", + "moddingGuideText": "Ajutor pentru Modare", + "mustRestartText": "Va trebuii să restartezi jocul pentru ca acestea să aibă efect.", + "netTestingText": "Teste pentru Internet", + "resetText": "Reset", + "showPlayerNamesText": "Arată numele Jucătorilor", + "showUserModsText": "Arată folderul pentru moduri", + "titleText": "Avansat", + "translationEditorButtonText": "Editor de Translatări ${APP_NAME}", + "translationFetchErrorText": "status pentru translatare nedisponibil", + "translationFetchingStatusText": "Se verifică statusul...", + "translationNoUpdateNeededText": "Limba curentă are cea mai nouă versiune; uraaaaa!", + "translationUpdateNeededText": "** limba curentă are nevoie de update-uri!! **", + "vrTestingText": "Teste RV" + }, + "showText": "Arată", + "signInForPromoCodeText": "Va trebuii să te conectezi la un cont pentru ca codurile-promo să aibă efect.", + "signInWithGameCenterText": "Pentru a folosi un cont Game Center\nconectează-te folosind aplicația Game Center.", + "singleGamePlaylistNameText": "Doar ${GAME}", + "singlePlayerCountText": "1 jucător", + "soloNameFilterText": "${NAME} Solo", + "soundtrackTypeNames": { + "CharSelect": "Selecție de Caracter", + "Chosen One": "Alesul", + "Epic": "Jocurile în mod Epic", + "Epic Race": "Cursă Epică", + "FlagCatcher": "Capturează Steagul", + "Flying": "Memorii Fericite", + "Football": "Fotbal", + "ForwardMarch": "Asalt", + "GrandRomp": "Cucerire", + "Hockey": "Hockey", + "Keep Away": "Stai Departe", + "Marching": "Runaround", + "Menu": "Meniul principal", + "Onslaught": "Măcel", + "Race": "Cursă", + "Scary": "Regele Dealului", + "Scores": "Meniul pentru Scor", + "Survival": "Eliminare", + "ToTheDeath": "Meciul Morții", + "Victory": "Meniul pentru Scorul Final" + }, + "spaceKeyText": "space", + "store": { + "alreadyOwnText": "Deja ai ${NAME}!", + "bombSquadProDescriptionText": "•Dublează biletele primite\n•Şterge advertismentele din joc\n•Include ${COUNT} de bilete bonus\n•+${PERCENT}% la scorul de la ligi\n•Deblochează '${INF_ONSLAUGHT}' şi \n '${INF_RUNAROUND}' ca nivele co-op", + "bombSquadProFeaturesText": "Caracteristici:", + "bombSquadProNameText": "${APP_NAME} Pro", + "buyText": "Cumpără", + "charactersText": "Caractere", + "comingSoonText": "Va veni...", + "extrasText": "Extra", + "freeBombSquadProText": "BombSquad este acum gratis, dar fiindcă tu l-ai cumpărat primeşti upgrade-ul \nla BombSquad Pro şi ${COUNT} de bilete ca un mulțumesc.\nMulțumesc pentru suportul tău!\n-Eric", + "gameUpgradesText": "Upgrade-uri pentru joc", + "holidaySpecialText": "Speciale pentru Sărbători", + "howToSwitchCharactersText": "(dute la \"${SETTINGS} -> ${PLAYER_PROFILES}\" pentru ați seta şi particulariza caractere)", + "howToUseIconsText": "(creează profile de jucător globale în \"${SETTINGS} -> ${PLAYER_PROFILES}\" pentru a folosi acestea)", + "howToUseMapsText": "(foloseşte aceste hărți în listele de redare Echipe/Fiecare Pentru El ale tale)", + "iconsText": "Imagini", + "loadErrorText": "Imposibil de încărcat pagina.\nVerifică-ți conexiunea la internet.", + "loadingText": "încărcare", + "mapsText": "Harti", + "miniGamesText": "MiniJocuri", + "oneTimeOnlyText": "(doar o dată)", + "purchaseAlreadyInProgressText": "Deja se cumpără acest obiect.", + "purchaseConfirmText": "Cumperi ${ITEM}?", + "purchaseNotValidError": "Achiziționarea nu este validă.\nContactează ${EMAIL} dacă aceasta este o eroare.", + "purchaseText": "Cumpără", + "saleBundleText": "Pachet de Vânzare!", + "saleExclaimText": "Vânzare!", + "salePercentText": "(${PERCENT}% mai ieftin)", + "saleText": "VÂNZARE", + "searchText": "Caută", + "teamsFreeForAllGamesText": "Joc în echipe / fiecare pentru el", + "winterSpecialText": "Speciale pentru iarnă", + "youOwnThisText": "-deja ai asta-" + }, + "storeDescriptionText": "Nebunie pentru 8 Jucători!\n\nExplodează-ți prietenii (sau computerul) într-un concurs de mini-jocuri explozive ca Capturează Steagul, Hockey-Bombist, şi Meci-de-Moarte-În-Slow-Motion-Epic!\n\nControllurile simple şi suportul pentru diferite tipuri de controller fac posibilă jucarea cu un maxim de 8 jucători; poți chiar folosi şi telefonul ca controller prin aplicația 'BombSquad Remote'!\n\nBombele Afară!\n\nVezi www.froemling.net/bombsquad pentru mai multe informații.", + "storeDescriptions": { + "blowUpYourFriendsText": "Explodează-ți prietenii.", + "competeInMiniGamesText": "Întrecete în mini-jocuri de la curse până la zbor.", + "customize2Text": "Particularizează caractere, mini-jocuri şi chiar soundtrack-ul.", + "customizeText": "Particularizează caractere şi creează-ți listele de redare cu mini-jocuri cum vrei tu.", + "sportsMoreFunText": "Sporturile sunt mai distractive cu explozibil.", + "teamUpAgainstComputerText": "Fă echipă contra computerului." + }, + "storeText": "Magazin", + "submittingPromoCodeText": "Se introduce Codul Promo...", + "telnetAccessGrantedText": "Acces la Telnet activat.", + "telnetAccessText": "Acces Telnet detectat; permite?", + "testBuildErrorText": "Acest Test Build nu mai este activ; te rog verifică pentru o versiune nouă.", + "testBuildText": "Test Build", + "testBuildValidateErrorText": "Nu se poate valida Test Build-ul. (fără conexiune la internet?)", + "testBuildValidatedText": "Test Build validat; bucură-te de el!", + "thankYouText": "Mulțumesc pentru suport! Bucură-te de joc!!", + "threeKillText": "TRIPLU-OMOR!!", + "timeBonusText": "Timp Bonus", + "timeElapsedText": "Timp Scurs", + "timeExpiredText": "Timp Expirat", + "timeSuffixDaysText": "${COUNT}z", + "timeSuffixHoursText": "${COUNT}o", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Sfat", + "titleText": "BombSquad", + "titleVRText": "BombSquad RV", + "topFriendsText": "Prieteni Top", + "tournamentCheckingStateText": "Se verifică starea campionatului; aşteaptă...", + "tournamentEndedText": "Acest campionat s-a terminat. Va începe un altul curând.", + "tournamentEntryText": "Preț pentru intrare", + "tournamentResultsRecentText": "Rezultatele Recente ale Campionatului", + "tournamentStandingsText": "Clasament pentru Campionat", + "tournamentText": "Campionat", + "tournamentTimeExpiredText": "Timpul Concursului a Expirat", + "tournamentsText": "Campionate", + "translations": { + "characterNames": { + "Bernard": "Bernard", + "Bones": "Oase", + "Frosty": "Frosty", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Mel": "Mel", + "Santa Claus": "Moş Crăciun", + "Snake Shadow": "Umbra Şarpelui", + "Spaz": "Spaz", + "Zoe": "Zoe" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Antrenament", + "Infinite ${GAME}": "${GAME} Infinit", + "Infinite Onslaught": "Măcel Infinit", + "Infinite Runaround": "Runaround Infinit", + "Onslaught Training": "Antrenament Măcel", + "Pro ${GAME}": "${GAME} Pro", + "Pro Football": "Fotbal Pro", + "Pro Onslaught": "Măcel Pro", + "Pro Runaround": "Runaround Pro", + "Rookie ${GAME}": "${GAME} Începător", + "Rookie Football": "Fotbal Începător", + "Rookie Onslaught": "Măcel Începător", + "The Last Stand": "Ultimul Rămas", + "Uber ${GAME}": "MEGA ${GAME}", + "Uber Football": "MEGA Fotbal", + "Uber Onslaught": "MEGA Măcel", + "Uber Runaround": "MEGA Runaround" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Fi ales pentru o perioadă de timp pentru a câştiga.\nOmoară-l pe ales pentru a deveni tu.", + "Bomb as many targets as you can.": "Bombează cât mai multe ținte.", + "Carry the flag for ${ARG1} seconds.": "Ține steagul pentru ${ARG1} (de) secunde.", + "Carry the flag for a set length of time.": "Ține steagul pentru o perioadă de timp.", + "Crush ${ARG1} of your enemies.": "Ucide ${ARG1} (de) inamici.", + "Defeat all enemies.": "Înfrânge toți inamicii.", + "Dodge the falling bombs.": "Ai grijă la bombe.", + "Final glorious epic slow motion battle to the death.": "Bătaie finală glorioasă în slow motion epic până la moarte.", + "Get the flag to the enemy end zone.": "Cară steagul către partea inamică.", + "How fast can you defeat the ninjas?": "Cât de rapid îi poți doborî pe ninja?", + "Kill a set number of enemies to win.": "Ucide un număr de inamici pentru a câştiga.", + "Last one standing wins.": "Ultimul rămas în picioare câştigă.", + "Last remaining alive wins.": "Ultimul rămas în viață câştigă.", + "Last team standing wins.": "Ultima echipă rămasă în picioare câştigă.", + "Prevent enemies from reaching the exit.": "Nu lăsa inamicii să ajungă la sfârşit.", + "Reach the enemy flag to score.": "Ajunge la steagul inamic pentru a puncta.", + "Return the enemy flag to score.": "Returnează steagul inamic pentru a puncta.", + "Run ${ARG1} laps.": "Fugi ${ARG1} (de) ture.", + "Run ${ARG1} laps. Your entire team has to finish.": "Fugi ${ARG1} (de) ture. Toată echipa trebuie să termine.", + "Run 1 lap.": "Fugi o tură.", + "Run 1 lap. Your entire team has to finish.": "Fugi o tură. Toată echipa trebuie să termine.", + "Run real fast!": "Fugi extrem de repede!", + "Score ${ARG1} goals.": "Înscrie ${ARG1} (de) goluri.", + "Score ${ARG1} touchdowns.": "Înscrie ${ARG1} (de) Touchdown-uri.", + "Score a goal.": "Înscrie un gol.", + "Score a touchdown.": "Înscrie un Touchdown.", + "Score some goals.": "Înscrie nişte goluri.", + "Secure all ${ARG1} flags.": "Cucereşte toate cele ${ARG1} (de) steaguri.", + "Secure all flags on the map to win.": "Toate steagurile trebuie să fie ale tale pentru a câştiga.", + "Secure the flag for ${ARG1} seconds.": "Ține steagul pentru ${ARG1} (de) secunde.", + "Secure the flag for a set length of time.": "Ține steagul pentru o perioadă de timp.", + "Steal the enemy flag ${ARG1} times.": "Fură steagul inamic de ${ARG1} (de) ori.", + "Steal the enemy flag.": "Fură steagul inamicilor", + "There can be only one.": "Aici poate fi doar unul.", + "Touch the enemy flag ${ARG1} times.": "Atinge steagul inamicului de ${ARG1} (de) ori", + "Touch the enemy flag.": "Atinge steagul inamicului", + "carry the flag for ${ARG1} seconds": "ține steagul pentru ${ARG1} (de) secunde", + "kill ${ARG1} enemies": "ucide ${ARG1} (de) inamici", + "last one standing wins": "ultimul rămas în picioare câştigă", + "last team standing wins": "ultima echipă rămasă în picioare câştigă", + "return ${ARG1} flags": "returnează ${ARG1} steaguri", + "return 1 flag": "returnează un steag", + "run ${ARG1} laps": "fugi ${ARG1} (de) ture", + "run 1 lap": "fugi o tură", + "score ${ARG1} goals": "înscrie ${ARG1} (de) goluri", + "score ${ARG1} touchdowns": "înscrie ${ARG1} (de) touchdown-uri", + "score a goal": "înscrie un gol", + "score a touchdown": "înscrie un Touchdown", + "secure all ${ARG1} flags": "cucereşte toate cele ${ARG1} (de) steaguri.", + "secure the flag for ${ARG1} seconds": "ține steagul pentru ${ARG1} (de) secunde", + "touch ${ARG1} flags": "atinge ${ARG1} (de) steaguri", + "touch 1 flag": "atinge un steag" + }, + "gameNames": { + "Assault": "Asalt", + "Capture the Flag": "Capturează steagul", + "Chosen One": "Alesul", + "Conquest": "Cucerire", + "Death Match": "Meciul Morții", + "Elimination": "Eliminare", + "Football": "Fotbal", + "Hockey": "Hockey", + "Keep Away": "Stai Departe", + "King of the Hill": "Regele Dealului", + "Meteor Shower": "Ploaie de Meteoriți", + "Ninja Fight": "Luptă cu Ninja", + "Onslaught": "Măcel", + "Race": "Cursă", + "Runaround": "Runaround", + "Target Practice": "Antrenament pe Ținte", + "The Last Stand": "Ultimul Rămas" + }, + "inputDeviceNames": { + "Keyboard": "Tastatură", + "Keyboard P2": "Tastatură 2" + }, + "languages": { + "Belarussian": "Belarusă", + "Chinese": "Chineză", + "Croatian": "Croată", + "Czech": "Cehă", + "Danish": "Daneză", + "Dutch": "Olandeză", + "English": "Engleză", + "Esperanto": "Esperanto", + "Finnish": "Finlandeză", + "French": "Franceză", + "German": "Germană", + "Gibberish": "Bolboroseală", + "Hindi": "Hindi", + "Hungarian": "Ungară", + "Italian": "Italiană", + "Japanese": "Japoneză", + "Korean": "Coreană", + "Persian": "Persian", + "Polish": "Poloneză", + "Portuguese": "Portugheză", + "Romanian": "Română", + "Russian": "Rusă", + "Spanish": "Spaniolă", + "Swedish": "Suedeză" + }, + "leagueNames": { + "Bronze": "Bronz", + "Diamond": "Diamant", + "Gold": "Aur", + "Silver": "Argint" + }, + "mapsNames": { + "Big G": "G mare", + "Bridgit": "Bridgit", + "Courtyard": "Curte", + "Crag Castle": "Castelul Crag", + "Doom Shroom": "Shroom-ul Morții", + "Football Stadium": "Stadion de fotbal", + "Happy Thoughts": "Amintiri Fericite", + "Hockey Stadium": "Stadion de Hockey", + "Lake Frigid": "Lacul Înghețat", + "Monkey Face": "Față de Maimuță", + "Rampage": "Nebunie", + "Roundabout": "Roundabout", + "Step Right Up": "Pășește-n sus", + "The Pad": "Placa", + "Tip Top": "Tip top", + "Tower D": "Turnul D", + "Zigzag": "Zig zag" + }, + "playlistNames": { + "Just Epic": "Doar Epic", + "Just Sports": "Doar Sporturi" + }, + "scoreNames": { + "Flags": "Steaguri", + "Goals": "Goluri", + "Score": "Scor", + "Survived": "Supraviețuit", + "Time": "Timp", + "Time Held": "Timp ținut" + }, + "serverResponses": { + "A code has already been used on this account.": "Un cod s-a folosit deja pe acest cont.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Eşti sigur că vrei să conectezi aceste 2 conturi?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nAcest lucru nu poate fi şters!", + "BombSquad Pro unlocked!": "Versiunea BombSquad Pro deblocată!", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Trişare detectată; scoruri şi premii suspendate pentru ${COUNT} (de) zile.", + "Daily maximum reached.": "Maxim pe zi ajuns.", + "Entering tournament...": "Se intră în campionat...", + "Invalid code.": "Cod greșit.", + "Invalid promo code.": "Cod Promo invalid", + "Invalid purchase.": "Achiziționare Invalidă.", + "Max number of playlists reached.": "Număr maxim de liste de redare ajuns.", + "Max number of profiles reached.": "Număr maxim de profile ajuns.", + "Message is too long.": "Mesajul este prea lung.", + "Profile \"${NAME}\" upgraded successfully.": "Profilul \"${NAME}\" a fost îmbunătățit cu succes.", + "Profile could not be upgraded.": "Profilul nu a putut fi îmbunătățit.", + "Purchase successful!": "Achiziționare reuşită!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Ai primit ${COUNT} de bilete pentru conectare.\nRevin-o mâine pentru a primi ${TOMORROW_COUNT}.", + "Sorry, there are no uses remaining on this code.": "Scuze, codul nu mai poate fi folosit.", + "Sorry, this code has already been used.": "Scuze, codul acesta s-a folosit deja.", + "Sorry, this code has expired.": "Scuze, acest cod a expirat.", + "Sorry, this code only works for new accounts.": "Scuze, acest cod merge doar pentru conturile noi.", + "The tournament ended before you finished.": "Campionatul s-a sfârşit înainte să termini.", + "This code cannot be used on the account that created it.": "Acest cod nu poate fi folosit de contul care l-a creat.", + "This requires version ${VERSION} or newer.": "Acest lucru are nevoie de versiunea ${VERSION} sau mai nouă.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Ai dori să iți conectezi contul tău de dispozitiv cu acesta?\n\nContul tău de dispozitiv este ${ACCOUNT1}\nAcest cont este ${ACCOUNT2}\n\nAcest lucru iți va salva progresul existent.\nAtenție: acest lucru nu poate fi şters!", + "You already own this!": "Deja ai acest lucru!", + "You don't have enough tickets for this!": "Nu ai destule bilete pentru acest lucru!", + "You got ${COUNT} tickets!": "Ai primit ${COUNT} de bilete!", + "You got a ${ITEM}!": "Ai primit un/o ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Ai fost promovat într-o ligă noua; felicitări!", + "You must wait a few seconds before entering a new code.": "Va trebui să aştepți câteva secunde înainte să introduci un cod nou.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Ai avut rankul #${RANK} în ultimul campionat. Mulțumesc pentru participare!", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Copia ta de joc a fost modificată.\nTe rog să treci la normal orice schimbâri şi încearcă din nou.", + "Your friend code was used by ${ACCOUNT}": "Codul tau de prieten a fost folosit de ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 minut", + "1 Second": "o secundă", + "10 Minutes": "10 minute", + "2 Minutes": "2 minute", + "2 Seconds": "2 secunde", + "20 Minutes": "20 minute", + "4 Seconds": "4 secunde", + "5 Minutes": "5 minute", + "8 Seconds": "8 secunde", + "Balance Total Lives": "Balansează nr. de vieți", + "Chosen One Gets Gloves": "Alesul primește mănuși", + "Chosen One Gets Shield": "Alesul primește un scut", + "Chosen One Time": "Timpul Alesului", + "Enable Impact Bombs": "Permite Bombe de Impact", + "Enable Triple Bombs": "Permite Bombe Triple", + "Epic Mode": "Modul Epic", + "Flag Idle Return Time": "", + "Flag Touch Return Time": "Timpul de returnare a steagului atins", + "Hold Time": "Timp de Ținut", + "Kills to Win Per Player": "Omoruri pentru a câştiga pe jucător", + "Laps": "Ture", + "Lives Per Player": "Vieți pe jucător", + "Long": "Lung", + "Longer": "Foarte Lung", + "Mine Spawning": "Apar Mine", + "No Mines": "Fără Mine", + "None": "Deloc", + "Normal": "Normal", + "Respawn Times": "Timp de revenire", + "Score to Win": "Înscrie pentru a câştiga", + "Short": "Scurt", + "Shorter": "Foarte Scurt", + "Solo Mode": "Mod Solo", + "Target Count": "Număr de Ținte", + "Time Limit": "Limită de timp" + }, + "statements": { + "Killing ${NAME} for skipping part of the track!": "${NAME} e mort deoarece a luat o scurtătură!" + }, + "teamNames": { + "Bad Guys": "Tipii răi", + "Blue": "Albastru", + "Good Guys": "Tipii buni", + "Red": "Roșu" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "O lovitură alergat-sărit-rotit-pumn perfect sincronizată poate omorî\ndintr-o singură lovitură și-ți poate primi respectul prietenilor.", + "Always remember to floss.": "Nu uita să te speli pe dinți.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Creează-ți profile de jucător pentru tine şi prietenii tăi cu\nnumele şi caracterele voastre preferate în loc să le folosiți pe cele la nimereală.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Blestemele te transformă înntr-o bombă cu timp.\nSingurul remediu este de a prinde rapid o trusă de prim-ajutor.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Înafară de look-ul lor, toate caracterele au abilități identice,\ndeci alegeți unul cu care te asemeni.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Nu te împăuna cu scutul ală de energie; tot poți fi aruncat de pe un deal.", + "Don't run all the time. Really. You will fall off cliffs.": "Nu fugi tot timpul. Pe bune. Vei cădea de pe dealuri.", + "Don't spin for too long; you'll become dizzy and fall.": "Nu te învârti prea mult: o să amețești și vei cădea.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Ține apăsat orice buton pentru a fugi. (Butoanele Trigger merg bine şi ele dacă le ai)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Ține apăsat orice buton pentru a fugi. Vei ajunge mai repede unde vrei,\ndar nu vei face curba prea bine, deci ai grijă la dealuri.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Bombele de gheață nu sunt foarte puternice, dar îngheață\norice ating, lăsându-le vulnerabile la spargere.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Dacă cineva te ia pe sus, dă-i cu pumnul şi te va lăsa.\nMerge şi în viața reală apropo.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Dacă nu ai destule controllere, instalează 'BombSquad Remote' pe\nun dispozitiv Android sau iOS pentru a-l folosi ca pe un controller.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Dacă ți s-a lipit o bombă lipicoasă de tine, sari și învârte-te în cercuri. S-ar\nputea să scapi... și dacă nu, vei face un spectacol din ultimele tale momente.", + "If you kill an enemy in one hit you get double points for it.": "Dacă omori un inamic dintr-o singură lovitură primești dublu puncte.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Dacă atingi un Blestem, singura ta şansă de viața este să\nprinzi o trusă de prim-ajutor în următoarele câteva secunde.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Dacă stai într-un singur loc, vei fi pâine prăjită. Fugi şi fereşte pentru a supraviețuii..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Dacă ai foarte mulți jucători care vin continuu, porneşte 'Dă afară jucătorii care nu fac nimic'\nîn Setări în caz ca cineva uită să iasă din joc.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Dacă dispozitivul tău se încăljeşte prea tare sau ai vrea să conservezi baterie,\ndă mai jos opțiunile de \"Vizual\" sau \"Rezoluție\" în Setări->Grafici.", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Dacă jocul rulează mai încet, încearcă să dai mai jos rezoluția \nsau vizualele în setările grafice din joc.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "În Capturează-Steagul, steagul tău trebuie să fie la baza ta pentru a înscrie. Dacă\nechipa opusă e pe cale să înscrie, o opțiune bună pentru ai opri este să le furi steagul.", + "In hockey, you'll maintain more speed if you turn gradually.": "În Hockey, vei menține mai multă viteză dacă te roteşti când trebuie.", + "It's easier to win with a friend or two helping.": "E mai ușor să câștigi cu un prieten sau 2 care te ajută.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Sari fix înainte să arunci pentru a trimite bombele către cele mai înalte niveluri.", + "Land-mines are a good way to stop speedy enemies.": "Minele sunt folositoare pentru a opri inamicii rapizi.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Multe lucruri pot fi ridicate şi aruncate, inclusiv alți jucători. Aruncarea\ninamicilor de pe dealuri poate fi o strategie efectivă şi plină de emoții.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nu, nu te poți urca pe platformă. Trebuie să arunci bombe.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Jucătorii pot intra şi ieşii în mijlocul jocului,\nşi poți şi să conectezi şi deconectezi controllere când vrei.", + "Practice using your momentum to throw bombs more accurately.": "Antrenează-te folosindu-ți momentum-ul pentru a arunca bombe mai corect.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Pumnii cauzează mai multe daune când te mişti,\ndeci încearcă să fugi, să sari, şi să te învârți ca un nebun.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Fugi în față şi în spate înainte să arunci o bombă\npentru a o învârti şi arunca mai departe.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Omoară un grupmde inamici printr-o\nbombă lângă o cutie de TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Capul este cea mai vulnerabilă parte, deci o Bombă Lipicioasă\nîn ceafă înseamnă game-over.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Acest nivel nu se termină niciodată, dar un scor mare\niți va aduce respect eternal prin lume.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Puterea aruncării este bazată pe direcția care o ții.\nPentru a arunca ceva în fața ta frumuşel, nu ține nici-o direcție.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Eşti săturat de coloana sonoră? Schimbăl cu al tău!\nVezi Setări->Audio->Coloană Sonoră", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Încearcă să 'Prăjeşti' bombele pentru o secundă sau două înainte să le arunci.", + "Try tricking enemies into killing eachother or running off cliffs.": "Încearcă să păcăleşti inamicii să se omoare pe ei înşişi sau sa cadă de pe dealuri.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Foloseşte butonul Ridică pentru a ridica steagul < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Învârte-te în față şi în spate pentru a avea distanța mai mare la aruncări..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Poți să dai cu pumnii în stânga sau dreapta dacă te învârți.\nAcest lucru este folositor pentru a jos de pe dealuri tipi răi sau a înscrie în hockey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Poți să îti dai seama când o bombă va exploda dupa culoarea\nscânteilor de pe fitil: galben..portocaliu..roşu..BOOM.", + "You can throw bombs higher if you jump just before throwing.": "Poți arunca bombe mai sus dacă sari fix înainte să arunci.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Iei daune când iți trânteşti capul de lucruri,\ndeci nu-ți trânti capul de lucruri.", + "Your punches do much more damage if you are running or spinning.": "Pumnii tăi dau mai multe daune dacă fugi sau te învârți." + } + }, + "trophiesRequiredText": "Asta necesită măcar ${NUMBER} trofee.", + "trophiesText": "Trofee", + "trophiesThisSeasonText": "Trofeele acestui Sezon", + "tutorial": { + "cpuBenchmarkText": "Rulând tutorial la viteze ridiculoase (în principal testează viteza CPU)", + "phrase01Text": "Salut!", + "phrase02Text": "Bun Venit în ${APP_NAME}!", + "phrase03Text": "Uite niște sfaturi pentru a-ți controla caracterul:", + "phrase04Text": "Multe lucruri din ${APP_NAME} sunt bazate pe FIZICĂ.", + "phrase05Text": "De exemplu, când dai cu pumnul...", + "phrase06Text": "...daunele sunt bazate pe viteza pumnilor tăi.", + "phrase07Text": "Vezi? Nu ne mișcăm, deci ${NAME} de abia dacă a simțit ceva.", + "phrase08Text": "Acum hai să sărim și sa ne rotim pentru a avea viteză mai mare.", + "phrase09Text": "Aha, e mai bine.", + "phrase10Text": "De asemenea, și alergatul ajută.", + "phrase11Text": "Ține apăsat orice buton pentru a alerga.", + "phrase12Text": "Pentru pumni super tari, încearcă sa te rotești și să fugi.", + "phrase13Text": "Ups; scuze pentru aia, ${NAME}.", + "phrase14Text": "Poți ridica obiecte ca și steaguri... sau ${NAME}", + "phrase15Text": "În final, sunt bombele.", + "phrase16Text": "Aruncatul lor are nevoie de antrenament.", + "phrase17Text": "Au! Nu a fost chiar o aruncare bună.", + "phrase18Text": "Mișcatul te ajută să arunci mai departe.", + "phrase19Text": "Săritul te ajută să arunci mai înalt.", + "phrase20Text": "Învârte bombele pentru a arunca și mai departe.", + "phrase21Text": "Sincronizarea bombelor poate fi complicată.", + "phrase22Text": "Fir-ar.", + "phrase23Text": "Încearcă sa-ți arzi fitilul pentru un moment.", + "phrase24Text": "Ura! Bine ars.", + "phrase25Text": "Păi, cam asta-i tot.", + "phrase26Text": "Acu ia-i, tigrule!", + "phrase27Text": "Ține-ți minte antrenamentul și vei supraviețui!", + "phrase28Text": "...poate...", + "phrase29Text": "Noroc!", + "randomName1Text": "Vasile", + "randomName2Text": "Virgilian", + "randomName3Text": "Florin", + "randomName4Text": "Mircea", + "randomName5Text": "Filipin", + "skipConfirmText": "Sigur treci peste tutorial? Apasă pentru a confirma.", + "skipVoteCountText": "${COUNT}/${TOTAL} voturi pentru a trece", + "skippingText": "Se trece peste tutorial...", + "toSkipPressAnythingText": "(apasă orice pentru a trece peste tutorial)" + }, + "twoKillText": "DUBLU OMOR!", + "unavailableText": "indisponibil", + "unconfiguredControllerDetectedText": "Controller neconfigurat detectat:", + "unlockThisInTheStoreText": "Aceasta trebuie deblocată în magazin.", + "unlockThisProfilesText": "Că să creezi mai mult de ${NUM} profile, îți trebuie:", + "unlockThisText": "Pentru a debloca acest lucru, ai nevoie de:", + "unsupportedHardwareText": "Scuze, acest hardware nu este suportat de acest build al jocului.", + "upFirstText": "Primul joc:", + "upNextText": "În următorul joc cu nr ${COUNT}:", + "updatingAccountText": "Se îmbunătățeşte contul tău...", + "upgradeText": "Îmbunătățeşte", + "upgradeToPlayText": "Deblochează \"${PRO}\" în magazinul din joc pentru a juca această hartă.", + "useDefaultText": "Folosește setările prestabilite", + "usesExternalControllerText": "Acest joc folosește un controller extern ca dispozitiv de intrare.", + "usingItunesText": "Se folosește iTunes pentru coloana sonoră...", + "usingItunesTurnRepeatAndShuffleOnText": "Fii sigur(ă) că shuffle e activat și repeat e pus pe ALL în iTunes.", + "validatingTestBuildText": "Se validează versiune test...", + "victoryText": "Victorie!", + "voteDelayText": "Poți începe alt vot peste ${NUMBER} secunde", + "votedAlreadyText": "Deja ai votat", + "votesNeededText": "${NUMBER} voturi necesare", + "vsText": "vs.", + "waitingForHostText": "(se așteaptă pentru ca ${HOST} să continue)", + "waitingForPlayersText": "se așteaptă să intre jucători...", + "waitingInLineText": "Așteaptă la coadă (grupul este plin)...", + "watchAVideoText": "Urmărește un Video", + "watchAnAdText": "Vezi o reclamă", + "watchWindow": { + "deleteConfirmText": "Sigur ștergi \"${REPLAY}\"?", + "deleteReplayButtonText": "Șterge\nReluare", + "myReplaysText": "Reluările mele", + "noReplaySelectedErrorText": "Nicio reluare selectată", + "playbackSpeedText": "Viteza de redare: ${SPEED}", + "renameReplayButtonText": "Redenumește\nReluare", + "renameReplayText": "Redenumește \"${REPLAY}\" în:", + "renameText": "Redenumește", + "replayDeleteErrorText": "Eroare la ștergerea reluării.", + "replayNameText": "Nume reluare", + "replayRenameErrorAlreadyExistsText": "Nume reluare deja existent.", + "replayRenameErrorInvalidName": "Nu se poate redenumi; nume invalid.", + "replayRenameErrorText": "Eroare la redenumirea reluării.", + "sharedReplaysText": "Reluări împărțite", + "titleText": "Vizionează", + "watchReplayButtonText": "Vezi\nReluare" + }, + "waveText": "Val", + "wellSureText": "Sigur!", + "wiimoteLicenseWindow": { + "titleText": "Copyright Darwiinmote" + }, + "wiimoteListenWindow": { + "listeningText": "Se ascultă după Wiimote-uri...", + "pressText": "Apasă butoanele 1 și 2 de pe wiimote simultan.", + "pressText2": "Pe Wiimote-urile noi cu Motion Plus, apasă butonul roșu 'sync' de pe spate.", + "pressTextScale": 1.0 + }, + "wiimoteSetupWindow": { + "copyrightText": "Copyright DarwiinRemote", + "listenText": "Ascultă", + "macInstructionsText": "Asigură-te că Wii-ul e închis și Bluetooth e activat\npe Mac, apoi apasă 'Listen'. Suportul wiimote e cam\nslab, deci s-ar putea să trebuiască să încerci de câteva\nori până să meargă.\n\nBluetooth ar trebui să permită până la 7 dispozitive conectate,\nînsă acest număr poate varia.\n\nBombSquad suportă Wiimote-urile și Nunchuck-urile originale\nși controllerul clasic.\nNoul Wii Remote Plus funcționează și el,\nînsă fără atașamente.", + "thanksText": "Mulțumim echipei Darwiinmote pentru\na face asta posibil.", + "titleText": "Setări Wiimote" + }, + "winsPlayerText": "${NAME} Câștigă!", + "winsTeamText": "${NAME} Câștigă!", + "winsText": "${NAME} câștigă!", + "worldScoresUnavailableText": "Scor global indisponibil.", + "worldsBestScoresText": "Top scoruri mondial", + "worldsBestTimesText": "Top timpi mondial", + "xbox360ControllersWindow": { + "getDriverText": "Instaleaza driver-ul.", + "macInstructions2Text": "Pentru a folosi controllere fără fir, vei avea nevoie de un receptor\ncare vine cu pachetul 'Xbox 360 Wireless Controller for Windows'.\nFiecare receptor te lasă să conectezi până la 4 controllere.\n\nImportant: Receptoare third-party nu vor funcționa cu acest driver;\nAsigură-te că pe receptor scrie 'Microsoft', nu 'XBOX 360'.\nMicrosoft nu mai vinde acestea separat, deci va trebui să iei unul\nîmpreună cu controllerul sau de pe un site ca ebay.\n\nDacă ai găsit acest mesaj folositor, consideră să donezi programatorului\ndriverului pe site-ul lui.", + "macInstructionsText": "Pentru a folosi controalele Xbox 360, trebuie sa instalezi \ndriver-ul de Mac disponibil in link-ul de mai jos. \nFunctioneaza pentru controalele cu si fara fir.", + "ouyaInstructionsText": "Pentru a folosi controllere X-Box 360 cu fir, pur și simplu\nconectează-le la un port USB. Poți folosi un USB-hub pentru a\nconecta mai multe controllere.\n\nPentru a folosi controllere fără fir vei avea nevoie de un receptor\nwireless, valabil ca parte a pachetului \"Xbox 360 wireless controller\nfor Windows\" sau vândut separat. Fiecare receptor intră într-un port\nUSB și te lasă să conectezi până la 4 controllere.", + "titleText": "Folosind controllere XBox 360 cu ${APP_NAME}:" + }, + "yesAllowText": "Da, Permite!", + "yourBestScoresText": "Top Scor", + "yourBestTimesText": "Top Timpi" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/russian.json b/dist/ba_data/data/languages/russian.json new file mode 100644 index 0000000..c43cc2d --- /dev/null +++ b/dist/ba_data/data/languages/russian.json @@ -0,0 +1,1946 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Имена учетных записей не могут содержать эмодзи или другие специальные символы", + "accountProfileText": "(профиль)", + "accountsText": "Аккаунты", + "achievementProgressText": "Достижения: ${COUNT} из ${TOTAL}", + "campaignProgressText": "Прогресс кампании [Сложный режим]: ${PROGRESS}", + "changeOncePerSeason": "Вы можете изменить это только раз в сезон.", + "changeOncePerSeasonError": "Вы должны подождать до следующего сезона, чтобы изменить это снова (${NUM} дней)", + "customName": "Имя учётной записи", + "deviceSpecificAccountText": "Сейчас используется аккаунт имениустройства: ${NAME}", + "linkAccountsEnterCodeText": "Введите код", + "linkAccountsGenerateCodeText": "Сгенерировать код", + "linkAccountsInfoText": "(делиться достижениями с другими платформами)", + "linkAccountsInstructionsNewText": "Чтобы связать две учетные записи, сгенерируйте код на первом\nи введите этот код на втором. Данные из\nвторой учетной записи будут распределены между ними.\n(Данные с первой учетной записи будут потеряны)\n\nВы можете связать ${COUNT} аккаунтов.\n\nВАЖНО: связывайте только собственные учетные записи;\nЕсли вы свяжетесь с аккаунтами друзей, вы не сможете\nодновременно играть онлайн.", + "linkAccountsInstructionsText": "Для связки двух аккаунтов, создайте код на одном\nиз них и введите код на другом.\nПрогресс и инвентарь будут объединены.\nВы можете связать до ${COUNT} аккаунтов.", + "linkAccountsText": "Связать акаунты", + "linkedAccountsText": "Привязанные аккаунты:", + "nameChangeConfirm": "Вы уверены, что хотите сменить имя аккаунта на ${NAME}?", + "notLoggedInText": "<не авторизован>", + "resetProgressConfirmNoAchievementsText": "Это сбросит весь ваш кооперативный прогресс\nи локальные лучшие результаты (кроме билетов).\nЭтот процесс необратим. Вы уверены?", + "resetProgressConfirmText": "Это сбросит весь ваш кооперативный\nпрогресс, достижения и локальные результаты\n(кроме билетов). Этот процесс необратим.\nВы уверены?", + "resetProgressText": "Сбросить прогресс", + "setAccountName": "Задать имя аккаунта", + "setAccountNameDesc": "Выберите имя для отображения своей учетной записи.\nВы можете использовать имя одной из ваших связанных\nучетных записей или создать уникальное имя учётной записи.", + "signInInfoText": "Войдите в аккаунт, чтобы собирать билеты, \nсоревноваться онлайн и делиться успехами.", + "signInText": "Войти", + "signInWithDeviceInfoText": "(стандартный аккаунт только для этого устройства)", + "signInWithDeviceText": "Войти с аккаунта устройства", + "signInWithGameCircleText": "Войти через Game Circle", + "signInWithGooglePlayText": "Войти через Google Play", + "signInWithTestAccountInfoText": "(устаревший тип аккаунта; в дальнейшем используйте аккаунт устройства)", + "signInWithTestAccountText": "Войти через тестовый аккаунт", + "signOutText": "Выйти", + "signingInText": "Вход...", + "signingOutText": "Выход...", + "testAccountWarningCardboardText": "Внимание: вы подписываете в с \"тест\" счета.\nЭти данные будут заменены со счетами Google сразу\nони становятся поддерживается в картонные приложений.\n\nСейчас вы будете зарабатывать все билеты в игре.\n(вы , тем не менее , получить Pro обновление BombSquad бесплатно )", + "testAccountWarningOculusText": "Внимание: вы входите под акканутом \"test\".\nОни будут заменены аккаунтами Oculus позже в этом\nгоду, что позволит совершать покупки билетов и многое другое.\n\nНа данный момент вам придется зарабатывать билеты в игре.\n(однако, вы получаете обновление BombSquad Pro бесплатно)", + "testAccountWarningText": "Внимание: вы заходите под аккаунтом \"test\".\nЭтот аккаунт привязан к конкретному устройству и может\nпериодически сбрасываться. (так что лучше особо\nне тратить время на сбор/разблокировку добра в нем)\n\nЧтобы использовать настоящий аккаунт (Game-Center,\nGoogle Plus и т.д.), запустите платную версию. Это также\nпозволит сохранять свой прогресс в облаке и делать его\nдоступным для разных устройств.", + "ticketsText": "Билетов: ${COUNT}", + "titleText": "Аккаунт", + "unlinkAccountsInstructionsText": "Выберите аккаунт, который хотите отвязать", + "unlinkAccountsText": "Отвязать аккаунты", + "viaAccount": "(через аккаунт ${NAME})", + "youAreLoggedInAsText": "Вы зашли как:", + "youAreSignedInAsText": "Вы вошли как:" + }, + "achievementChallengesText": "Достижения", + "achievementText": "Медаль", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Убейте 3 плохих парней с помощью TNT", + "descriptionComplete": "С помощью TNT убито 3 плохих парней", + "descriptionFull": "Убейте 3 плохих парней с помощью TNT на уровне ${LEVEL}", + "descriptionFullComplete": "3 плохих парней убито с помощью TNT на уровне ${LEVEL}", + "name": "Сейчас бабахнет" + }, + "Boxer": { + "description": "Победите без использования бомб", + "descriptionComplete": "Победа без использования бомб", + "descriptionFull": "Завершите уровень ${LEVEL} без использования бомб", + "descriptionFullComplete": "Уровень ${LEVEL} завершен без использвания бомб", + "name": "Боксёр" + }, + "Dual Wielding": { + "descriptionFull": "Соединить 2 контроллера (аппарат или приложение)", + "descriptionFullComplete": "Соединено 2 контроллера (аппарат или приложение)", + "name": "Двойное оружие" + }, + "Flawless Victory": { + "description": "Победите не получив повреждений", + "descriptionComplete": "Победа без повреждений", + "descriptionFull": "Пройдите уровень ${LEVEL} не получив повреждений", + "descriptionFullComplete": "Уровень ${LEVEL} пройден без повреждений", + "name": "Безупречная победа" + }, + "Free Loader": { + "descriptionFull": "Начать игру каждый сам за себя с 2 и более игроками", + "descriptionFullComplete": "Начата игра каждый сам за себя с 2 и более игроками", + "name": "Один в поле воин" + }, + "Gold Miner": { + "description": "Убейте 6 плохих парней с помощью мин", + "descriptionComplete": "С помощью мин убито 6 плохих парней", + "descriptionFull": "Убейте 6 плохих парней с помощью мин на уровне ${LEVEL}", + "descriptionFullComplete": "6 плохих парней убито с помощью мин на уровне ${LEVEL}", + "name": "Золотой минёр" + }, + "Got the Moves": { + "description": "Победите без ударов и бомб", + "descriptionComplete": "Победа без ударов и бомб", + "descriptionFull": "Победите уровень ${LEVEL} без ударов и бомб", + "descriptionFullComplete": "Победа на уровне ${LEVEL} без ударов и бомб", + "name": "Точные движения" + }, + "In Control": { + "descriptionFull": "Присоедините контроллер (аппарат или приложение)", + "descriptionFullComplete": "Подсоединен контроллер. (аппарат или приложение)", + "name": "Все под контролем" + }, + "Last Stand God": { + "description": "Наберите 1000 очков", + "descriptionComplete": "Набрано 1000 очков", + "descriptionFull": "Наберите 1000 очков на уровне ${LEVEL}", + "descriptionFullComplete": "Набрано 1000 очков на уровне ${LEVEL}", + "name": "Бог уровня ${LEVEL}" + }, + "Last Stand Master": { + "description": "Наберите 250 очков", + "descriptionComplete": "Набрано 250 очков", + "descriptionFull": "Наберите 250 очков на уровне ${LEVEL}", + "descriptionFullComplete": "250 очков набрано на уровне ${LEVEL}", + "name": "Мастер уровня ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Наберите 500 очков", + "descriptionComplete": "Набрано 500 очков", + "descriptionFull": "Наберите 500 очков на уровне ${LEVEL}", + "descriptionFullComplete": "500 очков набрано на уровне ${LEVEL}", + "name": "Волшебник уровня ${LEVEL}" + }, + "Mine Games": { + "description": "Убейте 3 плохих парней с помощью мин", + "descriptionComplete": "С помощью мин убито 3 злодея", + "descriptionFull": "Убейте 3 плохих парней с помощью мин на уровне ${LEVEL}", + "descriptionFullComplete": "С помощью мин убито 3 негодяя на уровне ${LEVEL}", + "name": "Игры с минами" + }, + "Off You Go Then": { + "description": "Сбросьте 3 негодяев с карты", + "descriptionComplete": "С карты сброшено 3 негодяя", + "descriptionFull": "Сбросьте 3 негодяев с карты на уровне ${LEVEL}", + "descriptionFullComplete": "3 негодяя сброшено с карты на уровне ${LEVEL}", + "name": "Давай отсюда" + }, + "Onslaught God": { + "description": "Наберите 5000 очков", + "descriptionComplete": "Набрано 5000 очков", + "descriptionFull": "Наберите 5000 очков на уровне ${LEVEL}", + "descriptionFullComplete": "5000 очков набрано на уровне ${LEVEL}", + "name": "Бог уровня ${LEVEL}" + }, + "Onslaught Master": { + "description": "Наберите 500 очков", + "descriptionComplete": "Набрано 500 очков", + "descriptionFull": "Наберите 500 очков на уровне ${LEVEL}", + "descriptionFullComplete": "500 очков набрано на уровне ${LEVEL}", + "name": "Мастер уровня ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Победите все волны", + "descriptionComplete": "Побеждены все волны", + "descriptionFull": "Победите все волны на уровне ${LEVEL}", + "descriptionFullComplete": "Все волны побеждены на уровне ${LEVEL}", + "name": "Победа на уровне ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Наберите 1000 очков", + "descriptionComplete": "Набрано 1000 очков", + "descriptionFull": "Наберите 1000 очков на уровне ${LEVEL}", + "descriptionFullComplete": "1000 очков набрано на уровне ${LEVEL}", + "name": "Волшебник уровня ${LEVEL}" + }, + "Precision Bombing": { + "description": "Победите без усилителей", + "descriptionComplete": "Победа без усилителей", + "descriptionFull": "Победите уровень ${LEVEL} без усилителей", + "descriptionFullComplete": "Победа на уровне ${LEVEL} без усилителей", + "name": "Прицельное бомбометание" + }, + "Pro Boxer": { + "description": "Победите без использования бомб", + "descriptionComplete": "Победа без использования бомб", + "descriptionFull": "Пройдите уровень ${LEVEL} без использования бомб", + "descriptionFullComplete": "Уровень ${LEVEL} пройден без использования бомб", + "name": "Боксёр профи" + }, + "Pro Football Shutout": { + "description": "Победите в сухую", + "descriptionComplete": "Победил в сухую", + "descriptionFull": "Выиграйте матч ${LEVEL} в сухую", + "descriptionFullComplete": "Победа в матче ${LEVEL} в сухую", + "name": "${LEVEL} в сухую" + }, + "Pro Football Victory": { + "description": "Выиграйте матч", + "descriptionComplete": "Матч выигран", + "descriptionFull": "Выиграйте матч ${LEVEL}", + "descriptionFullComplete": "Выигран матч ${LEVEL}", + "name": "Победа на уровне ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Победите все волны", + "descriptionComplete": "Побеждены все волны", + "descriptionFull": "Победите все волны на уровне ${LEVEL}", + "descriptionFullComplete": "Побеждены все волны на уровне ${LEVEL}", + "name": "Победа на уровне ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Пройдите все волны", + "descriptionComplete": "Пройдены все волны", + "descriptionFull": "Пройдите все волны на уровне ${LEVEL}", + "descriptionFullComplete": "Пройдены все волны на уровне ${LEVEL}", + "name": "Победа на уровне ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Выиграйте, не дав злодеям забить", + "descriptionComplete": "Победа в сухую", + "descriptionFull": "Выиграйте матч ${LEVEL}, не дав злодеям забить", + "descriptionFullComplete": "Победа в матче ${LEVEL} в сухую", + "name": "${LEVEL} в сухую" + }, + "Rookie Football Victory": { + "description": "Выиграйте матч", + "descriptionComplete": "Матч выигран", + "descriptionFull": "Выиграйте матч ${LEVEL}", + "descriptionFullComplete": "Выигран матч ${LEVEL}", + "name": "Победа в матче ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Победите все волны", + "descriptionComplete": "Побеждены все волны", + "descriptionFull": "Победите все волны уровня ${LEVEL}", + "descriptionFullComplete": "Побеждены все волны уровня ${LEVEL}", + "name": "Победа на уровне ${LEVEL}" + }, + "Runaround God": { + "description": "Заработайте 2000 очков", + "descriptionComplete": "Заработано 2000 очков", + "descriptionFull": "Наберите 2000 очков на уровне ${LEVEL}", + "descriptionFullComplete": "2000 очков набрано на уровне ${LEVEL}", + "name": "Бог уровня ${LEVEL}" + }, + "Runaround Master": { + "description": "Заработайте 500 очков", + "descriptionComplete": "Заработано 500 очков", + "descriptionFull": "Наберите 500 очков на уровне ${LEVEL}", + "descriptionFullComplete": "500 очков набрано на уровне ${LEVEL}", + "name": "Мастер уровня ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Заработайте 1000 очков", + "descriptionComplete": "Заработано 1000 очков", + "descriptionFull": "Наберите 1000 очков на уровне ${LEVEL}", + "descriptionFullComplete": "1000 очков набрано на уровне ${LEVEL}", + "name": "Волшебник уровня ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Успешно поделиться игрой с другом", + "descriptionFullComplete": "Игра успешно поделена с другом", + "name": "Делиться - значит заботиться" + }, + "Stayin' Alive": { + "description": "Выиграйте без смертей", + "descriptionComplete": "Победа без смертей", + "descriptionFull": "Выиграйте уровень ${LEVEL} не умирая", + "descriptionFullComplete": "Победа на уровне ${LEVEL} без смертей", + "name": "Остаться в живых" + }, + "Super Mega Punch": { + "description": "Нанесите 100% урона одним ударом", + "descriptionComplete": "Нанесено 100% урона одним ударом", + "descriptionFull": "Нанесите 100% урона одним ударом на уровне ${LEVEL}", + "descriptionFullComplete": "100% урона нанесено одним ударом на уровне ${LEVEL}", + "name": "Супер-мега-удар" + }, + "Super Punch": { + "description": "Нанесите 50% урона одним ударом", + "descriptionComplete": "Нанесено 50% урона одним ударом", + "descriptionFull": "Нанесите 50% урона одним ударом на уровне ${LEVEL}", + "descriptionFullComplete": "Нанесено 50% урона одним ударом на уровне ${LEVEL}", + "name": "Супер-удар" + }, + "TNT Terror": { + "description": "Убейте 6 негодяев с помощью TNT", + "descriptionComplete": "С помощью TNT убито 6 негодяев", + "descriptionFull": "Убейте 6 злодеев с помощью TNT на уровне ${LEVEL}", + "descriptionFullComplete": "С помощью TNT убито 6 злодеев на уровне ${LEVEL}", + "name": "Тротиловый ужас" + }, + "Team Player": { + "descriptionFull": "Начать игру в командах с 4 и более игроками", + "descriptionFullComplete": "Начал игру в командах с 4 и более игроками", + "name": "Командный игрок" + }, + "The Great Wall": { + "description": "Остановите всех злодеев", + "descriptionComplete": "Остановлены все злодеи", + "descriptionFull": "Остановите всех злодеев до одного на уровне ${LEVEL}", + "descriptionFullComplete": "Остановлены все злодеи на уровне ${LEVEL}", + "name": "Великая стена" + }, + "The Wall": { + "description": "Остановите всех злодеев", + "descriptionComplete": "Остановлены все злодеи", + "descriptionFull": "Остановите всех злодеев до одного на уровне ${LEVEL}", + "descriptionFullComplete": "Остановлены все злодеи на уровне ${LEVEL}", + "name": "Стена" + }, + "Uber Football Shutout": { + "description": "Выиграйте в сухую", + "descriptionComplete": "Победа в сухую", + "descriptionFull": "Выиграйте матч ${LEVEL} в сухую", + "descriptionFullComplete": "Победа в матче ${LEVEL} в сухую", + "name": "${LEVEL} в сухую" + }, + "Uber Football Victory": { + "description": "Выиграйте матч", + "descriptionComplete": "Матч выигран", + "descriptionFull": "Выиграйте матч ${LEVEL}", + "descriptionFullComplete": "Выигран матч ${LEVEL}", + "name": "Победа в матче ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Победите все волны", + "descriptionComplete": "Побеждены все волны", + "descriptionFull": "Победите все волны на уровне ${LEVEL}", + "descriptionFullComplete": "Побеждены все волны на уровне ${LEVEL}", + "name": "Победа на уровне ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Пройдите все волны", + "descriptionComplete": "Все волны пройдены", + "descriptionFull": "Пройдите все волны на уровне ${LEVEL}", + "descriptionFullComplete": "Пройдены все волны на уровне ${LEVEL}", + "name": "Победа на уровне ${LEVEL}" + } + }, + "achievementsRemainingText": "Оставшиеся медали:", + "achievementsText": "Медали", + "achievementsUnavailableForOldSeasonsText": "К сожалению, подробности достижений не доступны для старых сезонов.", + "addGameWindow": { + "getMoreGamesText": "Еще игр...", + "titleText": "Добавить игру" + }, + "allowText": "Разрешить", + "alreadySignedInText": "На вашем аккаунте играют на другом устройстве;\nпожалуйста зайдите с другого аккаунта или закройте\nигру на другом устройстве и попытайтесь снова.", + "apiVersionErrorText": "Невозможно загрузить модуль ${NAME}; он предназначен для API версии ${VERSION_USED}; здесь требуется версия ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(Режим \"Авто\" включает его только когда подключены наушники)", + "headRelativeVRAudioText": "Позиционно-зависимое ВР-аудио", + "musicVolumeText": "Громкость музыки", + "soundVolumeText": "Громкость звука", + "soundtrackButtonText": "Саундтреки", + "soundtrackDescriptionText": "(выберите свою собственную музыку, которая будет звучать во время игры)", + "titleText": "Аудио" + }, + "autoText": "Авто", + "backText": "Назад", + "banThisPlayerText": "Забанить игрока", + "bestOfFinalText": "Финал: лучший из ${COUNT}", + "bestOfSeriesText": "Лучший в ${COUNT} партиях:", + "bestRankText": "Ваш лучший ранг: ${RANK}", + "bestRatingText": "Ваш лучший рейтинг: ${RATING}", + "betaErrorText": "Эта бета-версия больше не активна, проверьте наличие новой версии. ", + "betaValidateErrorText": "Невожно проверить бета-версию. (нет сетевого соединения?)", + "betaValidatedText": "Бета-версия проверена. Наслаждайтесь!", + "bombBoldText": "БОМБА", + "bombText": "Бомба", + "boostText": "Увеличение", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} настраивается в самом приложении.", + "buttonText": "кнопка", + "canWeDebugText": "Хотите, чтобы BombSquad автоматически сообщал разработчику\nоб ошибках, сбоях и основную информацию об использовании?\n\nЭти данные не содержат никакой личной информации и помогают\nподдерживать игру в рабочем состоянии без сбоев и ошибок.", + "cancelText": "Отмена", + "cantConfigureDeviceText": "Извините, ${DEVICE} невозможно настроить.", + "challengeEndedText": "Это состязание завершено.", + "chatMuteText": "Заглушить чат", + "chatMutedText": "Чат отключен", + "chatUnMuteText": "Включить чат", + "choosingPlayerText": "<выбор игрока>", + "completeThisLevelToProceedText": "Чтобы продолжить, нужно\nпройти этот уровень!", + "completionBonusText": "Бонус за прохождение", + "configControllersWindow": { + "configureControllersText": "Настройка контроллеров", + "configureGamepadsText": "Настройка контроллеров", + "configureKeyboard2Text": "Настройка клавиатуры P2", + "configureKeyboardText": "Настройка клавиатуры", + "configureMobileText": "Мобильные устройства как контроллеры", + "configureTouchText": "Настройка сенсорного экрана", + "ps3Text": "Контроллеры PS3", + "titleText": "Контроллеры", + "wiimotesText": "Контроллеры Wii", + "xbox360Text": "Контроллеры Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Внимание: поддержка контроллеров различается в зависимости от устройства и версии Android.", + "pressAnyButtonText": "Нажмите любую кнопку на контроллере,\n который хотите настроить...", + "titleText": "Настроить контроллер" + }, + "configGamepadWindow": { + "advancedText": "Дополнительно", + "advancedTitleText": "Дополнительные настройки контроллера", + "analogStickDeadZoneDescriptionText": "(Включите, если персонаж продолжает двигаться после того, как стик отпущен)", + "analogStickDeadZoneText": "Мертвая зона аналогового стика", + "appliesToAllText": "(относится ко всем контроллерам данного типа)", + "autoRecalibrateDescriptionText": "(включить, если персонаж не двигается на полной скорости)", + "autoRecalibrateText": "Авто-перекалибровка аналоговых стиков", + "axisText": "ось", + "clearText": "очистить", + "dpadText": "D-Pad", + "extraStartButtonText": "Дополнительная кнопка \"Старт", + "ifNothingHappensTryAnalogText": "Если ничего не происходит, попробуйте вместо этого присвоить аналоговому стику.", + "ifNothingHappensTryDpadText": "Если ничего не происходит, попробуйте вместо этого присвоить D-Pad.", + "ignoreCompletelyDescriptionText": "(не дайте этому контроллеру воздействовать на игру, или меню)", + "ignoreCompletelyText": "Игнорировать полностью", + "ignoredButton1Text": "Игнорируемая кнопка 1", + "ignoredButton2Text": "Игнорируемая кнопка 2", + "ignoredButton3Text": "Игнорируемая кнопка 3", + "ignoredButton4Text": "Игнорируемая Кнопка 4", + "ignoredButtonDescriptionText": "(используйте это, чтобы кнопки 'home' или 'sync' не влияли на пользовательский интерфейс)", + "ignoredButtonText": "Проигнорированная кнопка", + "pressAnyAnalogTriggerText": "Нажмите любой аналоговый триггер...", + "pressAnyButtonOrDpadText": "Нажмите любую кнопку или D-Pad...", + "pressAnyButtonText": "Нажмите любую кнопку", + "pressLeftRightText": "Нажмите вправо или влево...", + "pressUpDownText": "Нажмите вверх или вниз...", + "runButton1Text": "Кнопка для бега 1", + "runButton2Text": "Кнопка для бега 2", + "runTrigger1Text": "Триггер для бега 1", + "runTrigger2Text": "Триггер для бега 2", + "runTriggerDescriptionText": "(аналоговые триггеры позволяют бегать с разной скоростью)", + "secondHalfText": "Используйте это для настройки второй половины\nустройства 'два контроллера в одном',\nкоторое показывается как один контроллер.", + "secondaryEnableText": "Включить", + "secondaryText": "Вторичный контроллер", + "startButtonActivatesDefaultDescriptionText": "(выключить, если ваша кнопка \"старт\" работает больше в качестве кнопки \"меню\")", + "startButtonActivatesDefaultText": "Кнопка Старт активирует стандартный виджет", + "titleText": "Настройка контроллера", + "twoInOneSetupText": "Настройка контроллера 2-в-1", + "uiOnlyDescriptionText": "(Запретить этому контроллеру присоединяться к игре)", + "uiOnlyText": "Ограничить только для меню", + "unassignedButtonsRunText": "Все неприсвоенные кнопки для бега", + "unsetText": "<не установлено>", + "vrReorientButtonText": "Кнопка переориентации VR" + }, + "configKeyboardWindow": { + "configuringText": "Настройка ${DEVICE}", + "keyboard2NoteScale": 0.7, + "keyboard2NoteText": "Примечание: большинство клавиатур могут зарегистрировать только\nнесколько нажатий клавиш за раз, так что второму игроку с клавиатуры\nлучше играть с отдельной подключенной для них клавиатуры.\nЗаметьте, что даже в этом случае все равно нужно будет присвоить\nуникальные клавиши для двух игроков." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Размеры кнопок действия", + "actionsText": "Действия", + "buttonsText": "кнопки", + "dragControlsText": "< чтобы передвинуть элементы управления, перетащите их >", + "joystickText": "джойстик", + "movementControlScaleText": "Размеры кнопок движения", + "movementText": "Движение", + "resetText": "Сброс", + "swipeControlsHiddenText": "Спрятать иконки смахивания", + "swipeInfoText": "К контроллерам, работающим со смахиванием, нужно привыкнуть,\nзато с ними можно играть, не глядя на контроллер.", + "swipeText": "смахнуть", + "titleText": "Настройка сенсорного экрана", + "touchControlsScaleText": "Шкала сенсоров" + }, + "configureItNowText": "Настроить сейчас?", + "configureText": "Настроить", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "Для наилучшего результата вам понадобится сеть Wi-Fi без задержек.\nВы можете уменьшить задержки Wi-Fi, выключив другие беспроводные\nустройства, играя близко к маршрутизатору Wi-Fi, и подключив\nхост игры непосредственно к сети через Ethernet.", + "explanationScale": 0.8, + "explanationText": "Для настройки смартфона или планшета в качестве контроллера\nустановите на нем приложение \"${REMOTE_APP_NAME}\". К игре ${APP_NAME}\nможно подключить любое количество устройств через Wi-Fi, бесплатно!", + "forAndroidText": "для Android:", + "forIOSText": "для iOS:", + "getItForText": "Скачайте приложение ${REMOTE_APP_NAME} для iOS в Apple App Store\nили для Android в Google Play Store или Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Использование мобильных устройств в качестве контроллеров:" + }, + "continuePurchaseText": "Продолжить за ${PRICE}?", + "continueText": "Продолжить", + "controlsText": "Управление", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Это не относится к абсолютным рекордам.", + "activenessInfoText": "Этот множитель повышается когда вы играете,\nи понижается когда вы не играете.", + "activityText": "Активность", + "campaignText": "Кампания", + "challengesInfoText": "Выигрывай призы за мини-игры.\n\nПризы и сложность увеличиваются\nкаждый раз после победы и\nуменьшаются в случае провала.", + "challengesText": "Испытания", + "currentBestText": "Последний рекорд", + "customText": "Другое", + "entryFeeText": "Участие", + "forfeitConfirmText": "Так просто сдаться?", + "forfeitNotAllowedYetText": "Вы не можете покинуть это состязание.", + "forfeitText": "Сдаться", + "multipliersText": "Множители", + "nextChallengeText": "Следующее испытание", + "nextPlayText": "Следующая игра", + "ofTotalTimeText": "из ${TOTAL}", + "playNowText": "Играть", + "pointsText": "Очки", + "powerRankingFinishedSeasonUnrankedText": "(сезон завершен без ранга)", + "powerRankingNotInTopText": "(не в ${NUMBER} лучших)", + "powerRankingPointsEqualsText": "= ${NUMBER} очков", + "powerRankingPointsMultText": "(x ${NUMBER} очков)", + "powerRankingPointsText": "${NUMBER} очков", + "powerRankingPointsToRankedText": "(${CURRENT} из ${REMAINING} очков)", + "powerRankingText": "Ранг игрока", + "prizesText": "Призы", + "proMultInfoText": "Здесь игроки с обновлением ${PRO}\nполучают ${PERCENT}% умножение очков.", + "seeMoreText": "Подробнее...", + "skipWaitText": "Пропустить ожидание", + "timeRemainingText": "Оставшееся время", + "titleText": "Кооператив", + "toRankedText": "Получено", + "totalText": "всего", + "tournamentInfoText": "Добейся высокого результата с\nдругими игроками твоей лиги.\n\nНаграды вручаются самым крутым\nпо окончании турнира.", + "welcome1Text": "Добро пожаловать в ${LEAGUE}. Вы можете\nповысить свою лигу получая звёзды, получая \nдостижения и выигрывая трофеи в турнирах.", + "welcome2Text": "Вы также можете заработать билеты от многих из тех же видов деятельности.\nБилеты могут быть использованы , чтобы разблокировать новые персонажи , карты и\nмини -игры, чтобы войти турниры, и многое другое.", + "yourPowerRankingText": "Ваш ранг:" + }, + "copyOfText": "Копия ${NAME}", + "copyText": "Копия", + "copyrightText": "© 2013 Eric Froemling", + "createAPlayerProfileText": "Создать профиль игрока?", + "createEditPlayerText": "<Создание / редактирование игрока>", + "createText": "Создать", + "creditsWindow": { + "additionalAudioArtIdeasText": "Дополнительное аудио, предварительные иллюстрации и идеи: ${NAME}", + "additionalMusicFromText": "Дополнительная музыка: ${NAME}", + "allMyFamilyText": "Всем моим друзьям и семье, которые помогли играть тестовую версию", + "codingGraphicsAudioText": "Программирование, графика и аудио: ${NAME}", + "languageTranslationsText": "Языковые переводы:", + "legalText": "Юридическая информация:", + "publicDomainMusicViaText": "Общедоступная музыка через ${NAME}", + "softwareBasedOnText": "Это программное обеспечение частично основано на работе ${NAME}", + "songCreditText": "${TITLE} исполняет ${PERFORMER}\nКомпозитор ${COMPOSER}, Аранжировка ${ARRANGER}, Издано ${PUBLISHER},\nПредоставлено ${SOURCE}", + "soundAndMusicText": "Звук и музыка:", + "soundsText": "Звуки (${SOURCE}):", + "specialThanksText": "Особая благодарность:", + "thanksEspeciallyToText": "Отдельное спасибо ${NAME}", + "titleText": "Участвовали в создании ${APP_NAME}", + "whoeverInventedCoffeeText": "Тому, кто изобрел кофе" + }, + "currentStandingText": "Ваша текущая позиция: ${RANK}", + "customizeText": "Изменить...", + "deathsTallyText": "${COUNT} смертей", + "deathsText": "Смерти", + "debugText": "отладка", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Внимание: для этого теста рекомендуется установить Настройки->Графика->Текстуры на 'Высок.'", + "runCPUBenchmarkText": "Запустить тест производительности CPU", + "runGPUBenchmarkText": "Запустить тест производительности GPU", + "runMediaReloadBenchmarkText": "Запустить тест производительности загрузки медиа", + "runStressTestText": "Выполнить тест-нагрузку", + "stressTestPlayerCountText": "Количество игроков", + "stressTestPlaylistDescriptionText": "плей-лист нагрузочного испытания", + "stressTestPlaylistNameText": "Название плей-листа", + "stressTestPlaylistTypeText": "Тип плей-листа", + "stressTestRoundDurationText": "Продолжительность раунда", + "stressTestTitleText": "Тест-нагрузка", + "titleText": "Тесты производительности и тесты-нагрузки", + "totalReloadTimeText": "Общее время перезагрузки: ${TIME} (подробности см. в логе)", + "unlockCoopText": "Разблокировать кооперативные уровни" + }, + "defaultFreeForAllGameListNameText": "Стандартные игры 'Каждый за себя'", + "defaultGameListNameText": "Стандартный плей-лист режима ${PLAYMODE}", + "defaultNewFreeForAllGameListNameText": "Мои игры 'Каждый за себя'", + "defaultNewGameListNameText": "Мой плей-лист ${PLAYMODE}", + "defaultNewTeamGameListNameText": "Мои командные игры", + "defaultTeamGameListNameText": "Стандартные командные игры", + "deleteText": "Удалить", + "demoText": "демонстрация", + "denyText": "Отклонить", + "desktopResText": "Разреш. экрана", + "difficultyEasyText": "Легкий", + "difficultyHardOnlyText": "Только в трудном режиме", + "difficultyHardText": "Трудный", + "difficultyHardUnlockOnlyText": "Этот уровень может быть открыт только в сложном режиме.\nДумаете, сможете!?!?!", + "directBrowserToURLText": "Пожалуйста, направьте веб-браузер по следующему адресу:", + "disableRemoteAppConnectionsText": "Отключить соединения RemoteApp", + "disableXInputDescriptionText": "Позволяет подключение более 4 контроллеров, но может не очень хорошо работать.", + "disableXInputText": "Отключить XInput", + "doneText": "Готово", + "drawText": "Ничья", + "duplicateText": "Дублировать", + "editGameListWindow": { + "addGameText": "Добавить\nигру", + "cantOverwriteDefaultText": "Невозможно перезаписать стандартный плей-лист!", + "cantSaveAlreadyExistsText": "Плей-лист с таким именем уже существует!", + "cantSaveEmptyListText": "Невозможно сохранить пустой плей-лист!", + "editGameText": "Изменить\nигру", + "gameListText": "Список игр", + "listNameText": "Название плей-листа", + "nameText": "Название", + "removeGameText": "Удалить\nигру", + "saveText": "Сохранить список", + "titleText": "Редактор плей-листа" + }, + "editProfileWindow": { + "accountProfileInfoText": "У этого особенного профиля есть имя\nи иконка, основанные на вашем аккаунте.\n\n${ICONS}\n\nСоздайте дополнительные профили, чтобы \nиспользовать разные имена и иконки.", + "accountProfileText": "(профиль аккаунта)", + "availableText": "Имя \"${NAME}\" доступно.", + "changesNotAffectText": "Внимание: изменения не повлияют на профили, уже находящиеся в игре", + "characterText": "персонаж", + "checkingAvailabilityText": "Проверка доступности для \"${NAME}\"...", + "colorText": "цвет", + "getMoreCharactersText": "Еще персонажей...", + "getMoreIconsText": "Еще иконок...", + "globalProfileInfoText": "Глобальным профилям игроков гарантированы уникальные\nимена. У них также есть дополнительные иконки.", + "globalProfileText": "(глобальный профиль)", + "highlightText": "акцент", + "iconText": "Иконка", + "localProfileInfoText": "У локальных игровых профилей нет иконок и им не гарантированы\nуникальные имена. Обновите до глобального профиля, чтобы\nзарезервировать уникальное имя и добавить иконку.", + "localProfileText": "(локальный профиль)", + "nameDescriptionText": "Имя игрока", + "nameText": "Имя", + "randomText": "случайное", + "titleEditText": "Изменение профиля", + "titleNewText": "Новый профиль", + "unavailableText": "\"${NAME}\" недоступно; попробуйте другое имя.", + "upgradeProfileInfoText": "Это зарезервирует ваше имя игрока\nи позволит вам добавить иконку.", + "upgradeToGlobalProfileText": "Обновить до глобального профиля" + }, + "editProfilesAnyTimeText": "(профиль можно изменить в любое время в 'настройках')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Невожможно удалить стандартный саундтрек.", + "cantEditDefaultText": "Невозможно изменить стандартный саундтрек. Создайте копию или новый саундтрек.", + "cantEditWhileConnectedOrInReplayText": "Нельзя редактировать саундтреки находясь в лобби или в записи.", + "cantOverwriteDefaultText": "Невозможно перезаписать стандартный саундтрек", + "cantSaveAlreadyExistsText": "Саундтрек с таким именем уже существует!", + "copyText": "Копировать", + "defaultGameMusicText": "<стандартная музыка игры>", + "defaultSoundtrackNameText": "Стандартный саундтрек", + "deleteConfirmText": "Удалить саундтрек:\n\n${NAME}'?", + "deleteText": "Удалить\nсаундтрек", + "duplicateText": "Копировать\nсаундтрек", + "editSoundtrackText": "Редактор саундтрека", + "editText": "Изменить\nсаундтрек", + "fetchingITunesText": "загрузка плей-листов Music App…", + "musicVolumeZeroWarning": "Внимание: громкость музыки установлена на 0", + "nameText": "Название", + "newSoundtrackNameText": "Мой саундтрек ${COUNT}", + "newSoundtrackText": "Новый саундтрек:", + "newText": "Новый\nсаундтрек", + "selectAPlaylistText": "Выберите плей-лист", + "selectASourceText": "Источник музыки", + "soundtrackText": "Саундтрек", + "testText": "тест", + "titleText": "Саундтреки", + "useDefaultGameMusicText": "Стандартная музыка", + "useITunesPlaylistText": "Плейлист музыкального приложения", + "useMusicFileText": "Музыкальный файл (mp3 и т. д.)", + "useMusicFolderText": "Папка с музыкой" + }, + "editText": "Редактировать", + "endText": "Конец", + "enjoyText": "Удачи!", + "epicDescriptionFilterText": "${DESCRIPTION} в эпическом замедленном действии.", + "epicNameFilterText": "${NAME} в эпическом режиме", + "errorAccessDeniedText": "доступ запрещен", + "errorOutOfDiskSpaceText": "нет места на диске", + "errorText": "Ошибка", + "errorUnknownText": "неизвестная ошибка", + "exitGameText": "Выйти из ${APP_NAME}?", + "exportSuccessText": "'${NAME}' экспортирован.", + "externalStorageText": "Внешняя память", + "failText": "Провал", + "fatalErrorText": "Ой, что-то потерялось или сломалось.\nПопытайтесь переустановить приложение или\nобратитесь к ${EMAIL} за помощью.", + "fileSelectorWindow": { + "titleFileFolderText": "Выберите файл или папку", + "titleFileText": "Выберите файл", + "titleFolderText": "Выберите папку", + "useThisFolderButtonText": "Использовать эту папку" + }, + "filterText": "Поиск", + "finalScoreText": "Финальные очки", + "finalScoresText": "Финальные очки", + "finalTimeText": "Финальное время", + "finishingInstallText": "Завершается установка, минутку...", + "fireTVRemoteWarningText": "* Для лучшего результата используйте\nигровые контроллеры или установите\nприложение '${REMOTE_APP_NAME}'\nна ваших телефонах и планшетах.", + "firstToFinalText": "Финал до ${COUNT} очков", + "firstToSeriesText": "Серия до ${COUNT} очков", + "fiveKillText": "ПЯТЕРЫХ ЗА РАЗ!!!", + "flawlessWaveText": "Безупречная волна!", + "fourKillText": "ЧЕТВЕРЫХ ЗА РАЗ!!!", + "freeForAllText": "Каждый за себя", + "friendScoresUnavailableText": "Очки друзей недоступны.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Лидеры игры ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "Невозможно удалить стандартный плей-лист!", + "cantEditDefaultText": "Невозможно изменить стандартный плей-лист! Создайте копию или новый список.", + "cantShareDefaultText": "Вы не можете делиться стандартным плейлистом.", + "deleteConfirmText": "Удалить \"${LIST}\"?", + "deleteText": "Удалить\nплей-лист", + "duplicateText": "Дублировать\nплей-лист", + "editText": "Изменить\nплей-лист", + "gameListText": "Список игр", + "newText": "Новый\nплей-лист", + "showTutorialText": "Показать обучение", + "shuffleGameOrderText": "Смешать порядок игр", + "titleText": "Настроить плей-листы '${TYPE}'" + }, + "gameSettingsWindow": { + "addGameText": "Добавить игру" + }, + "gamepadDetectedText": "обнаружен 1 контроллер", + "gamepadsDetectedText": "Обнаружены ${COUNT} контроллера.", + "gamesToText": "${WINCOUNT}:${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Помните: любое устройство в совместной игре поддерживает\nнесколько игроков, если у вас хватает контроллеров.", + "aboutDescriptionText": "Используйте эти закладки, чтобы создать лобби.\n\nЛобби позволяет играть в игры и турниры\nс друзьями на разных устройствах.\n\nНажмите кнопку ${PARTY} в правом верхнем углу,\nчтобы общаться с друзьями в вашем лобби.\n(на контроллере, нажмите ${BUTTON} находясь в меню)", + "aboutText": "Инфо", + "addressFetchErrorText": "<ошибка запроса адресов>", + "appInviteInfoText": "Пригласите друзей поиграть в BombSquad и они\nполучат ${COUNT} бесплатных билетов. Ты получишь\n${YOU_COUNT} за каждого друга.", + "appInviteMessageText": "${NAME} отправил вам ${COUNT} билетов в ${APP_NAME}", + "appInviteSendACodeText": "Отправьте им код", + "appInviteTitleText": "Приглашение в ${APP_NAME}", + "bluetoothAndroidSupportText": "(работает с любым устройством, поддерживающим Bluetooth)", + "bluetoothDescriptionText": "Создать/войти в лобби через Bluetooth:", + "bluetoothHostText": "Создать лобби через Bluetooth", + "bluetoothJoinText": "Присоединиться через Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "проверка...", + "copyCodeConfirmText": "Код скопирован в буфер обмена.", + "copyCodeText": "Копировать код", + "dedicatedServerInfoText": "Для лучшего результата создайте отдельный сервер. Смотри bombsquadgame.com/server чтобы узнать как.", + "disconnectClientsText": "Это отключит ${COUNT} игроков\nв вашем лобби. Вы уверены?", + "earnTicketsForRecommendingAmountText": "Друзья получат ${COUNT} билетов, если они активируют ваш код\n(а вы будете получать ${YOU_COUNT} билетов за каждую активацию)", + "earnTicketsForRecommendingText": "Поделись игрой\nПолучи билеты...", + "emailItText": "Послать Email", + "favoritesSaveText": "Сохранить как избранное", + "favoritesText": "Избранное", + "freeCloudServerAvailableMinutesText": "Следующий бесплатный облачный сервер будет доступен через ${MINUTES} минут(у).", + "freeCloudServerAvailableNowText": "Облачный сервер доступен!", + "freeCloudServerNotAvailableText": "Нет бесплатных облачных серверов.", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} билетов от ${NAME}", + "friendPromoCodeAwardText": "Вы получите ${COUNT} билетов за каждую активацию.", + "friendPromoCodeExpireText": "Код действителен в течении ${EXPIRE_HOURS} часов и только для новых игроков.", + "friendPromoCodeInstructionsText": "Для активации в ${APP_NAME} зайдите в\n\"Настройки->Дополнительно->Ввести промо-код\". Перейди на сайт bombsquadgame.com, чтобы скачать версию игры для своей платформы.", + "friendPromoCodeRedeemLongText": "Каждая активация принесет ${COUNT} билетов до ${MAX_USES} игрокам.", + "friendPromoCodeRedeemShortText": "Он принесет ${COUNT} билетов в игре.", + "friendPromoCodeWhereToEnterText": "(в \"Настройках->Дополнительно->Ввести код\")", + "getFriendInviteCodeText": "Получить промо-код", + "googlePlayDescriptionText": "Пригласить игроков Google Play в ваше лобби:", + "googlePlayInviteText": "Пригласить", + "googlePlayReInviteText": "В вашем лобби ${COUNT} игроков Google Play.\nЕсли вы начнете новое приглашение, они будут отключены.\nДобавьте их в новое приглашение, чтобы их вернуть.", + "googlePlaySeeInvitesText": "Смотреть приглашения", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Только для Android / Google Play)", + "hostPublicPartyDescriptionText": "Создать публичное лобби", + "hostingUnavailableText": "Хостинг недоступен", + "inDevelopmentWarningText": "Примечание:\n\nСетевая игра является новой и еще развивающейся функцией.\nВ настоящее время, настоятельно рекомендуется, чтобы все\nигроки были в той же сети Wi-Fi.", + "internetText": "Интернет", + "inviteAFriendText": "Друзья еще не играют? Пригласи их\nпопробовать и они получат ${COUNT} билетов.", + "inviteFriendsText": "Пригласить друзей", + "joinPublicPartyDescriptionText": "Присоединитесь к публичной вечеринке", + "localNetworkDescriptionText": "Присоединяйтесь к ближайшему лобби (локальная сеть, Bluetooth, и т.д.)", + "localNetworkText": "Локальная сеть", + "makePartyPrivateText": "Сделать мое лобби приватным", + "makePartyPublicText": "Сделать мое лобби публичным", + "manualAddressText": "Адрес", + "manualConnectText": "Подключиться", + "manualDescriptionText": "Войти в лобби по адресу:", + "manualJoinSectionText": "Присоединиться по адресу", + "manualJoinableFromInternetText": "К вам можно подключаться из Интернета?:", + "manualJoinableNoWithAsteriskText": "НЕТ*", + "manualJoinableYesText": "ДА", + "manualRouterForwardingText": "*чтобы исправить, попробуйте настроить маршрутизатор для перенаправления UDP порта ${PORT} на ваш локальный адрес", + "manualText": "Вручную", + "manualYourAddressFromInternetText": "Ваш адрес из Интернета:", + "manualYourLocalAddressText": "Ваш локальный адрес:", + "nearbyText": "Поблизости", + "noConnectionText": "<нет соединения>", + "otherVersionsText": "(другие версии)", + "partyCodeText": "Код лобби", + "partyInviteAcceptText": "Принять", + "partyInviteDeclineText": "Отклонить", + "partyInviteGooglePlayExtraText": "(смотрите вкладку 'Google Play' в разделе 'Собрать')", + "partyInviteIgnoreText": "Игнорировать", + "partyInviteText": "${NAME} пригласил\nвас в его лобби!", + "partyNameText": "Имя команды", + "partyServerRunningText": "Твой сервер для лобби работает.", + "partySizeText": "Размер группы", + "partyStatusCheckingText": "Проверка...", + "partyStatusJoinableText": "Ваша команда доступна через интернет", + "partyStatusNoConnectionText": "Невозможно подключиться к серверу", + "partyStatusNotJoinableText": "Ваше лобби недоступно через интернет", + "partyStatusNotPublicText": "Ваше лобби не для всех", + "pingText": "пинг", + "portText": "Порт", + "privatePartyCloudDescriptionText": "Частные лобби работают на выделенных облачных серверах; настройка маршрутизатора не требуется.", + "privatePartyHostText": "Создать частное лобби", + "privatePartyJoinText": "Присоединиться к частному лобби", + "privateText": "Частный", + "publicHostRouterConfigText": "Это может потребовать настройки переадресации портов на вашем маршрутизаторе. Для более легкого варианта организуйте частное лобби.", + "publicText": "Открытый", + "requestingAPromoCodeText": "Запрашиваем код...", + "sendDirectInvitesText": "Послать приглашение", + "shareThisCodeWithFriendsText": "Поделись кодом с друзьями:", + "showMyAddressText": "Показать мой адрес", + "startAdvertisingText": "Включить доступ", + "startHostingPaidText": "Создать сейчас за ${COST}", + "startHostingText": "Создать", + "startStopHostingMinutesText": "Вы можете начать и остановить хостинг бесплатно в течение следующих ${MINUTES} минут.", + "stopAdvertisingText": "Отключить доступ", + "stopHostingText": "Остановить", + "titleText": "Мультиплеер", + "wifiDirectDescriptionBottomText": "Если у всех устройств есть панель 'Wi-Fi Direct', они могут использовать ее, чтобы найти\nи соединиться друг с другом. Когда все устройства подключены, вы можете создать здесь лобби,\nиспользуя вкладку 'Локальная сеть', также, как и с обычной Wi-Fi сетью.\n\nДля лучших результатов, хост Wi-Fi Direct также должен быть хостом группы ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct может быть использован для соединения устройств Android напрямую без\nWi-Fi сети. Это лучше всего работает на Android 4.2 или новее.\n\nЧтобы использовать, откройте настройки Wi-Fi и поищите Wi-Fi Direct в меню.", + "wifiDirectOpenWiFiSettingsText": "Открыть настройки Wi-Fi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(работает между всеми платформами)", + "worksWithGooglePlayDevicesText": "(работает с устройствами, использующими версию игры Google Play (Android)", + "youHaveBeenSentAPromoCodeText": "Вам был выслан ${APP_NAME} промо-код:" + }, + "getCoinsWindow": { + "coinDoublerText": "Удвоение монет", + "coinsText": "Монеты: ${COUNT}", + "freeCoinsText": "Бесплатные монеты", + "restorePurchasesText": "Восстановить покупки", + "titleText": "Получить монеты" + }, + "getTicketsWindow": { + "freeText": "БЕСПЛАТНО!", + "freeTicketsText": "Бесплатные билеты", + "inProgressText": "Выполняется транзакция; повторите попытку через несколько секунд.", + "purchasesRestoredText": "Покупки восстановлены.", + "receivedTicketsText": "Получено ${COUNT} билетов!", + "restorePurchasesText": "Восстановить покупки", + "ticketDoublerText": "Удвоитель билетов", + "ticketPack1Text": "Маленькая пачка билетов", + "ticketPack2Text": "Средняя пачка билетов", + "ticketPack3Text": "Большая пачка билетов", + "ticketPack4Text": "Огромная пачка билетов", + "ticketPack5Text": "Слоновая пачка билетов", + "ticketPack6Text": "Максимальная пачка билетов", + "ticketsFromASponsorText": "Получить ${COUNT} билетов\nот спонсора", + "ticketsText": "Билетов: ${COUNT}", + "titleText": "Получить билеты", + "unavailableLinkAccountText": "Извините, но на этой платформе покупки недоступны.\nВ качестве решения, вы можете привязать этот аккаунт \nк аккаунту на другой платформе, и совершать покупки там.", + "unavailableTemporarilyText": "Сейчас недоступно; повторите попытку позже.", + "unavailableText": "К сожалению это не доступно.", + "versionTooOldText": "Извините, ваша версия игры устарела; пожалуйста, обновите на новую.", + "youHaveShortText": "у вас ${COUNT}", + "youHaveText": "У вас ${COUNT} билетов" + }, + "googleMultiplayerDiscontinuedText": "Простите, сервис многопользовательской игры Google больше не поддерживается.\nЯ работаю над заменой так быстро, насколько это возможно.\nДо тех пор, пожалуйста выберете другой способ подключения.\n-Эрик", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Всегда", + "fullScreenCmdText": "Полноэкранный (Cmd-F)", + "fullScreenCtrlText": "Полноэкранный (Ctrl-F)", + "gammaText": "Гамма", + "highText": "Высокий", + "higherText": "Ультра", + "lowText": "Низкий", + "mediumText": "Средний", + "neverText": "Никогда", + "resolutionText": "Разрешение", + "showFPSText": "Показывать FPS", + "texturesText": "Текстуры", + "titleText": "Графика", + "tvBorderText": "Граница телевизора", + "verticalSyncText": "Вертикальная синхронизация (V-Sync)", + "visualsText": "Визуальные эффекты" + }, + "helpWindow": { + "bombInfoText": "- Бомба -\nСильнее ударов, но может привести\nк смертельным повреждениям. Для\nнаилучших результатов бросать в\nпротивника пока не догорел фитиль.", + "bombInfoTextScale": 0.6, + "canHelpText": "${APP_NAME} может помочь.", + "controllersInfoText": "Вы можете играть в ${APP_NAME} с друзьями по сети, или вы все можете\nиграть на одном устройстве, если у вас достаточно контроллеров.\n${APP_NAME} поддерживает любые контроллеры; можно даже использовать телефоны \nв качестве контроллеров через бесплатное приложение '${REMOTE_APP_NAME}.\nСмотрите Настройки→Контроллеры для получения дополнительной информации.", + "controllersInfoTextFantasia": "Один игрок может использовать пульт дистанционного управления,\nно рекомендуется использовать геймпад. Также вместо контроллеров\nможно использовать мобильные устройства с помощью\nбесплатного приложения 'BombSquad Remote'.\nДополнительную информацию см. в 'Настройки'>'Контроллеры'.", + "controllersInfoTextMac": "Один-два игрока могут играть с клавиатуры, но BombSquad лучше всего работает\nс геймпадами. Управлять персонажами в BombSquad можно с помощью USB геймпадов,\nконтроллеров PS3, Xbox 360, Wii и устройств iOS/Android. Надеюсь, у вас есть такие\nпод рукой. Дополнительную информацию см. в разделе 'Настройки' > 'Контроллеры'.", + "controllersInfoTextOuya": "С BombSquad можно использовать контроллеры OUYA, PS3, Xbox 360, а также\nмножество других USB и Bluetooth геймпадов. Также можно использовать\nустройства iOS и Android в качестве контроллеров через бесплатное приложение\n'BombSquad Remote'. Дополнительную информацию см. в 'Настройки' > 'Контроллеры'.", + "controllersText": "Контроллеры", + "controlsSubtitleText": "У вашего дружелюбного персонажа из ${APP_NAME} есть несколько простых действий:", + "controlsText": "Управление", + "devicesInfoText": "В ВР-версию ${APP_NAME} можно играть по сети с обычной версией,\nтак что вытаскивайте свои дополнительные телефоны, планшеты\nи компьютеры, и играйте на них. Можно даже подключить\nобычную версию игры к ВР-версии, чтобы позволить\nостальным наблюдать за действием.", + "devicesText": "Устройства", + "friendsGoodText": "Бывают полезны. В ${APP_NAME} веселее играть с несколькими игроками;\nподдерживается до 8 игроков одновременно, что приводит нас к:", + "friendsText": "Друзья", + "jumpInfoText": "- Прыжок -\nПрыгайте для перескакивания,\nшвыряния предметов подальше\nили для выражения радости.", + "orPunchingSomethingText": "Или ударить, сбросить с обрыва и взорвать бомбой-липучкой по дороге вниз.", + "pickUpInfoText": "- Захват -\nХватайте флаги, врагов\nи все, что не прикручено к полу.\nЧтобы бросить, нажмите еще раз.", + "powerupBombDescriptionText": "Позволяет швырнуть три бомбы\nподряд, вместо одной.", + "powerupBombNameText": "Тройные бомбы", + "powerupCurseDescriptionText": "Этого, наверное, лучше избегать.\n ...или нет?...", + "powerupCurseNameText": "Проклятие", + "powerupHealthDescriptionText": "Ни за что не догадаетесь.\nВозвращает полное здоровье.", + "powerupHealthNameText": "Аптечка", + "powerupIceBombsDescriptionText": "Слабее, чем обычные бомбы\nно делает врагов заморожеными\nи особенно хрупкими.", + "powerupIceBombsNameText": "Ледяные бомбы", + "powerupImpactBombsDescriptionText": "Чуть слабее обычных бомб,\nно взрываются при ударе.", + "powerupImpactBombsNameText": "Ударные бомбы", + "powerupLandMinesDescriptionText": "Выдаются по 3 штуки.\nПолезны для защиты базы или\nусмирения быстроногих врагов.", + "powerupLandMinesNameText": "Мины", + "powerupPunchDescriptionText": "Делают ваши удары быстрее,\nлучше, сильнее.", + "powerupPunchNameText": "Боксерские перчатки", + "powerupShieldDescriptionText": "Немного поглощает урон,\nчтобы вам не навредили.", + "powerupShieldNameText": "Энергетический щит", + "powerupStickyBombsDescriptionText": "Липнут ко всему, чего касаются.\nИ начинается веселье.", + "powerupStickyBombsNameText": "Бомбы-липучки", + "powerupsSubtitleText": "Конечно, ни одна игра не обходится без усилителей:", + "powerupsText": "Усилители", + "punchInfoText": "- Удар -\nЧем быстрее двигаются кулаки -\nтем cильнее удар. Так что бегайте\nи крутитесь как ненормальные.", + "runInfoText": "- Бег -\nДля бега удерживайте нажатой ЛЮБУЮ кнопку. Для этого хороши верхние триггеры\nили плечевые кнопки, если они у вас есть. Бегом передвигаться быстрее,\nно труднее поворачивать, так что осторожно с обрывами.", + "someDaysText": "Иногда просто хочется что-нибудь ударить. Или взорвать.", + "titleText": "Справка ${APP_NAME}", + "toGetTheMostText": "Чтобы выжать максимум из этой игры, вам необходимо:", + "welcomeText": "Добро пожаловать в ${APP_NAME}!" + }, + "holdAnyButtonText": "<держать любую кнопку>", + "holdAnyKeyText": "<держать любую клавишу>", + "hostIsNavigatingMenusText": "- ${HOST} в меню навигации как босс -", + "importPlaylistCodeInstructionsText": "Используйте показанный код, чтобы импортировать этот плейлист где-то ещё:", + "importPlaylistSuccessText": "Импортирован плейлист ${TYPE} '${NAME}'", + "importText": "Импорт", + "importingText": "Импортирую...", + "inGameClippedNameText": "В игре будет\n\"${NAME}\"", + "installDiskSpaceErrorText": "ОШИБКА: не удалось завершить установку. Может быть,\nне хватает свободного места на вашем устройстве.\nОсвободите место и попробуйте еще раз.", + "internal": { + "arrowsToExitListText": "чтобы выйти из списка нажмите ${LEFT} или ${RIGHT}", + "buttonText": "кнопка", + "cantKickHostError": "Невозможно выгнать создателя.", + "chatBlockedText": "${NAME} заблокирован на ${TIME} секунд.", + "connectedToGameText": "Вошел в игру '${NAME}'", + "connectedToPartyText": "Вошел в лобби ${NAME}!", + "connectingToPartyText": "Идет соединение...", + "connectionFailedHostAlreadyInPartyText": "Соединение не удалось; хост находится в другом лобби.", + "connectionFailedPartyFullText": "Соединение не удалось; группа полная", + "connectionFailedText": "Соединение не удалось.", + "connectionFailedVersionMismatchText": "Соединение не удалось; хост использует другую версию игры.\nУбедитесь, что версии обеих сторон обновлены, и попытайтесь снова.", + "connectionRejectedText": "Соединение отклонено.", + "controllerConnectedText": "${CONTROLLER} подключен.", + "controllerDetectedText": "Обнаружен 1 контроллер.", + "controllerDisconnectedText": "${CONTROLLER} отсоединен.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} отсоединен. Попробуйте подключиться еще раз.", + "controllerForMenusOnlyText": "Этот контороллер не используется в игре; только в навигационном меню", + "controllerReconnectedText": "${CONTROLLER} снова подключен.", + "controllersConnectedText": "Подключено ${COUNT} контроллеров.", + "controllersDetectedText": "Обнаружено ${COUNT} контроллеров.", + "controllersDisconnectedText": "Отсоединено ${COUNT} контроллеров.", + "corruptFileText": "Обнаружены поврежденные файлы. Попытайтесь переустановить или обратитесь к ${EMAIL}", + "errorPlayingMusicText": "Ошибка воспроизведения музыки: ${MUSIC}", + "errorResettingAchievementsText": "Не удается сбросить онлайн-медали, пожалуйста, попробуйте позже.", + "hasMenuControlText": "${NAME} контролирует меню", + "incompatibleNewerVersionHostText": "Хост использует более новую версию игры.\nОбновитесь до последней версии и попробуйте снова.", + "incompatibleVersionHostText": "Хост использует другую версию игры.\nУбедитесь, что обе ваши версии обновлены, и попытайтесь снова.", + "incompatibleVersionPlayerText": "${NAME} использует другую версию игры и не может соединится.\nУбедитесь, что обе ваши версии обновлены, и попытайтесь снова.", + "invalidAddressErrorText": "Ошибка: неправильный адрес.", + "invalidNameErrorText": "Ошибка: некорректное имя.", + "invalidPortErrorText": "Ошибка: неверный порт.", + "invitationSentText": "Приглашение отправлено.", + "invitationsSentText": "Отправлено ${COUNT} приглашений.", + "joinedPartyInstructionsText": "Кто-то вошел в ваше лобби.\nНажмите 'Играть' чтобы начать игру.", + "keyboardText": "Клавиатура", + "kickIdlePlayersKickedText": "${NAME} выкинут за бездействие.", + "kickIdlePlayersWarning1Text": "${NAME} будет выкинут через ${COUNT} секунд при бездействии.", + "kickIdlePlayersWarning2Text": "(это можно выключить в Настройки -> Дополнительно)", + "leftGameText": "Покинул '${NAME}'.", + "leftPartyText": "Вышел из лобби ${NAME}.", + "noMusicFilesInFolderText": "В папке нет музыкальных файлов.", + "playerJoinedPartyText": "${NAME} вошел в лобби!", + "playerLeftPartyText": "${NAME} покинул лобби.", + "rejectingInviteAlreadyInPartyText": "Приглашение отклонено (уже в лобби).", + "serverRestartingText": "Сервер перезагружается. Попробуйте позже...", + "serverShuttingDownText": "Сервер выключается...", + "signInErrorText": "Ошибка входа.", + "signInNoConnectionText": "Невозможно войти. (нет интернет соединения?)", + "teamNameText": "Команда ${NAME}", + "telnetAccessDeniedText": "ОШИБКА: пользователь не предоставил доступ Telnet.", + "timeOutText": "(осталось ${TIME} секунд)", + "touchScreenJoinWarningText": "Вы присоединились с сенсорным экраном.\nЕсли это была ошибка, нажмите 'Меню->Покинуть игру'.", + "touchScreenText": "Сенсорный экран", + "trialText": "проба", + "unableToResolveHostText": "Ошибка: невозможно достичь хоста.", + "unavailableNoConnectionText": "Сейчас это недоступно (нет интернет соединения?)", + "vrOrientationResetCardboardText": "Используйте это, чтобы сбросить ориентации VR.\nЧтобы играть в игру, вам понадобится внешний контроллер.", + "vrOrientationResetText": "Сброс ориентации ВР.", + "willTimeOutText": "(время выйдет при бездействии)" + }, + "jumpBoldText": "ПРЫЖОК", + "jumpText": "Прыгнуть", + "keepText": "Оставить", + "keepTheseSettingsText": "Оставить эти настройки?", + "keyboardChangeInstructionsText": "Нажмите на пробел два раза, чтобы сменить раскладку.", + "keyboardNoOthersAvailableText": "Нету других раскладок.", + "keyboardSwitchText": "Раскладка изменена на \"${NAME}\".", + "kickOccurredText": "${NAME} изгнали.", + "kickQuestionText": "Изгнать ${NAME}?", + "kickText": "Изгнать", + "kickVoteCantKickAdminsText": "Администраторов нельзя выгнать.", + "kickVoteCantKickSelfText": "Вы не можете выгонять самого себя.", + "kickVoteFailedNotEnoughVotersText": "Недостаточно игроков для голосования.", + "kickVoteFailedText": "Голосование на вылет не удалось.", + "kickVoteStartedText": "Начато голосование за вылет ${NAME}.", + "kickVoteText": "Голосовать за вылет", + "kickVotingDisabledText": "Голосование за вылет отключено.", + "kickWithChatText": "Наберите ${YES} для согласия или ${NO} для отказа.", + "killsTallyText": "Убито ${COUNT}", + "killsText": "Убито", + "kioskWindow": { + "easyText": "Легкий", + "epicModeText": "Эпический режим", + "fullMenuText": "Полное меню", + "hardText": "Трудный", + "mediumText": "Средний", + "singlePlayerExamplesText": "Примеры одиночной игры / кооператива", + "versusExamplesText": "Примеры игр друг против друга" + }, + "languageSetText": "Язык установлен на \"${LANGUAGE}\".", + "lapNumberText": "Круг ${CURRENT}/${TOTAL}", + "lastGamesText": "(последние ${COUNT} игр)", + "leaderboardsText": "Таблицы лидеров", + "league": { + "allTimeText": "Абсолютные", + "currentSeasonText": "Текущий сезон (${NUMBER})", + "leagueFullText": "Лига ${NAME}", + "leagueRankText": "Ранг лиги", + "leagueText": "Лига", + "rankInLeagueText": "#${RANK}, ${NAME} League${SUFFIX}", + "seasonEndedDaysAgoText": "Сезон завершился ${NUMBER} дней назад.", + "seasonEndsDaysText": "Сезон завершится через ${NUMBER} дней.", + "seasonEndsHoursText": "Сезон завершится через ${NUMBER} часов.", + "seasonEndsMinutesText": "Сезон завершится через ${NUMBER} минут.", + "seasonText": "Сезон ${NUMBER}", + "tournamentLeagueText": "Чтобы участвовать в этом турнире, вы должны достичь лиги ${NAME}.", + "trophyCountsResetText": "Трофеи будут сброшены в следующем сезоне." + }, + "levelBestScoresText": "Лучший рекорд на ${LEVEL}", + "levelBestTimesText": "Лучшее время на ${LEVEL}", + "levelFastestTimesText": "Лучшее время уровня ${LEVEL}", + "levelHighestScoresText": "Лучшие очки уровня ${LEVEL}", + "levelIsLockedText": "${LEVEL} заблокирован.", + "levelMustBeCompletedFirstText": "Сначала должен быть пройден ${LEVEL}.", + "levelText": "Уровень ${NUMBER}", + "levelUnlockedText": "Уровень разблокирован!", + "livesBonusText": "Бонус жизней", + "loadingText": "Загрузка", + "loadingTryAgainText": "Загрузка; попробуй снова через несколько секунд...", + "macControllerSubsystemBothText": "Оба (не рекомендуется)", + "macControllerSubsystemClassicText": "Классический", + "macControllerSubsystemDescriptionText": "(попробуйте изменить это, если ваши контроллеры не работают)", + "macControllerSubsystemMFiNoteText": "Обнаружен Сделанный-для-iOS/Mac контроллер;\nВозможно, вы захотите включить его в Настройки → Контроллеры", + "macControllerSubsystemMFiText": "Сделано для iOS/Mac", + "macControllerSubsystemTitleText": "Поддержка контроллера", + "mainMenu": { + "creditsText": "Благодарности", + "demoMenuText": "Меню примеров", + "endGameText": "Закончить игру", + "exitGameText": "Выйти из игры", + "exitToMenuText": "Выйти в меню?", + "howToPlayText": "Как играть", + "justPlayerText": "(Только ${NAME})", + "leaveGameText": "Покинуть игру", + "leavePartyConfirmText": "Действительно покинуть лобби?", + "leavePartyText": "Покинуть лобби", + "leaveText": "Уйти", + "quitText": "Выйти", + "resumeText": "Продолжить", + "settingsText": "Настройки" + }, + "makeItSoText": "Поехали!", + "mapSelectGetMoreMapsText": "Ещё карт...", + "mapSelectText": "Выбрать...", + "mapSelectTitleText": "Карты игры ${GAME}", + "mapText": "Карта", + "maxConnectionsText": "Максимум соединений", + "maxPartySizeText": "Размер группы", + "maxPlayersText": "Максимум игроков", + "modeArcadeText": "Аркадный режим", + "modeClassicText": "Обычный режим", + "modeDemoText": "Демонстрационный режим", + "mostValuablePlayerText": "Самый ценный игрок", + "mostViolatedPlayerText": "Самый побитый игрок", + "mostViolentPlayerText": "Самый буйный игрок", + "moveText": "Движение", + "multiKillText": "${COUNT} ЗА РАЗ!!!", + "multiPlayerCountText": "${COUNT} игроков", + "mustInviteFriendsText": "Примечание: вы должны пригласить друзей\nна панели \"${GATHER}\" или присоединить\nконтроллеры для совместной игры.", + "nameBetrayedText": "${NAME} предал ${VICTIM}.", + "nameDiedText": "${NAME} умер.", + "nameKilledText": "${NAME} убил ${VICTIM}.", + "nameNotEmptyText": "Имя не может быть пустым!", + "nameScoresText": "${NAME} ведет!", + "nameSuicideKidFriendlyText": "${NAME} убился.", + "nameSuicideText": "${NAME} совершил суицид.", + "nameText": "Имя", + "nativeText": "Разрешение устройства", + "newPersonalBestText": "Новый личный рекорд!", + "newTestBuildAvailableText": "Доступна новая тестовая версия! (${VERSION} сборка ${BUILD}).\nОбновить: ${ADDRESS}", + "newText": "Новый", + "newVersionAvailableText": "Доступна новая версия ${APP_NAME}! (${VERSION})", + "nextAchievementsText": "Следующие достижения:", + "nextLevelText": "Следующий уровень", + "noAchievementsRemainingText": "- нет", + "noContinuesText": "(без продолжений)", + "noExternalStorageErrorText": "На данном устройстве не найдено внешней памяти", + "noGameCircleText": "Ошибка: не вошли в GameCircle", + "noJoinCoopMidwayText": "К кооперативным играм нельзя присоединиться посреди игры.", + "noProfilesErrorText": "У вас нет профиля игрока, так что вас будут звать '${NAME}'.\nСоздать профиль можно перейдя в 'Настройки' > 'Профили игроков'.", + "noScoresYetText": "Счета пока нет.", + "noThanksText": "Нет, спасибо", + "noTournamentsInTestBuildText": "ВНИМАНИЕ: Турнирные очки из этой тестовой сборки будут не засчитаны.", + "noValidMapsErrorText": "Для данного типа игры не найдено корректных карт.", + "notEnoughPlayersRemainingText": "Не осталось достаточно игроков; выйдите и начните новую игру.", + "notEnoughPlayersText": "Для начала этой игры нужно как минимум ${COUNT} игрока!", + "notNowText": "Не сейчас", + "notSignedInErrorText": "Войдите в аккаунт для начала.", + "notSignedInGooglePlayErrorText": "Войдите сначала в Google Play, а там посмотрим.", + "notSignedInText": "(вы не вошли)", + "nothingIsSelectedErrorText": "Ничего не выбрано!", + "numberText": "${NUMBER}", + "offText": "Выкл", + "okText": "Oк", + "onText": "Вкл", + "oneMomentText": "Один момент…", + "onslaughtRespawnText": "${PLAYER} возродится в ${WAVE} волне", + "orText": "${A} или ${B}", + "otherText": "Другие...", + "outOfText": "(${RANK} из ${ALL})", + "ownFlagAtYourBaseWarning": "Чтобы набрать очки, ваш собственный\nфлаг должен быть на вашей базе!", + "packageModsEnabledErrorText": "Сетевая игра запрещена, когда включены моды локального пакета (см. Настройки->Дополнительно)", + "partyWindow": { + "chatMessageText": "Сообщение чата", + "emptyText": "Ваше лобби пустое", + "hostText": "(хост)", + "sendText": "Отправить", + "titleText": "Ваше лобби" + }, + "pausedByHostText": "(остановлено хостом)", + "perfectWaveText": "Идеальная волна!", + "pickUpBoldText": "ПОДНЯТЬ", + "pickUpText": "Поднять", + "playModes": { + "coopText": "Кооператив", + "freeForAllText": "Каждый сам за себя", + "multiTeamText": "Мультикомандный", + "singlePlayerCoopText": "Одиночная игра / Кооператив", + "teamsText": "Команды" + }, + "playText": "Играть", + "playWindow": { + "coopText": "Кооператив", + "freeForAllText": "Каждый за себя", + "oneToFourPlayersText": "1-4 игрока", + "teamsText": "Команды", + "titleText": "Играть", + "twoToEightPlayersText": "2-8 игроков" + }, + "playerCountAbbreviatedText": "${COUNT}и", + "playerDelayedJoinText": "${PLAYER} присоединится в следующем раунде.", + "playerInfoText": "Об игроке", + "playerLeftText": "${PLAYER} покинул игру.", + "playerLimitReachedText": "Достигнут лимит в ${COUNT} игроков. Больше добавить нельзя.", + "playerLimitReachedUnlockProText": "Обновитесь до \"${PRO}\" в магазине чтобы играть с более чем ${COUNT} игроками.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Вы не можете удалить профиль аккаунта.", + "deleteButtonText": "Удалить\nпрофиль", + "deleteConfirmText": "Удалить '${PROFILE}'?", + "editButtonText": "Изменить\nпрофиль", + "explanationText": "(настраивайте имена и внешний вид игрока)", + "newButtonText": "Новый\nпрофиль", + "titleText": "Профили игроков" + }, + "playerText": "Игрок", + "playlistNoValidGamesErrorText": "Этот плейлист содержит неоткрытые игры.", + "playlistNotFoundText": "плей-лист не найден", + "playlistText": "Плей-лист", + "playlistsText": "Плей-листы", + "pleaseRateText": "Если вам нравится игра ${APP_NAME}, пожалуйста, подумайте о том,\nчтобы оценить ее или написать рецензию. Это обеспечивает полезную\nобратную связь и помогает поддержать дальнейшую разработку.\n\nСпасибо!\n- Эрик", + "pleaseWaitText": "Пожалуйста, подождите...", + "pluginsDetectedText": "Обнаружены новые плагины. Включите/настройте их в настройках.", + "pluginsText": "Плагины", + "practiceText": "Тренировка", + "pressAnyButtonPlayAgainText": "Нажмите любую кнопку чтобы играть снова...", + "pressAnyButtonText": "Нажмите любую кнопку чтобы продолжить...", + "pressAnyButtonToJoinText": "нажмите любую кнопку чтобы присоединиться...", + "pressAnyKeyButtonPlayAgainText": "Нажмите любую клавишу/кнопку чтобы играть снова...", + "pressAnyKeyButtonText": "Нажмите любую клавишу/кнопку чтобы продолжить...", + "pressAnyKeyText": "Нажмите любую клавишу...", + "pressJumpToFlyText": "** Чтобы лететь, продолжайте нажимать прыжок **", + "pressPunchToJoinText": "нажмите УДАР чтобы присоединиться...", + "pressToOverrideCharacterText": "нажмите ${BUTTONS} чтобы переопределить своего персонажа", + "pressToSelectProfileText": "Нажмите ${BUTTONS} чтобы выбрать игрока", + "pressToSelectTeamText": "нажмите ${BUTTONS} чтобы выбрать команду", + "profileInfoText": "Создайте профили для себя и своих друзей, чтобы\nнастроить ваши имена, цвета и персонажей.", + "promoCodeWindow": { + "codeText": "Код", + "codeTextDescription": "Промо-код", + "enterText": "Отправить" + }, + "promoSubmitErrorText": "Ошибка отправки кода, проверьте своё интернете соединение", + "ps3ControllersWindow": { + "macInstructionsText": "Выключите питание на задней панели PS3, убедитесь, что Bluetooth\nвключен на вашем компьютере, а затем подключите контроллер к Маку\nс помощью кабеля USB для синхронизации. Теперь можно использовать\nкнопку контроллера 'PS' чтобы подключить его к Маку\nв проводном (USB) или беспроводном (Bluetooth) режиме.\n\nНа некоторых Маках при синхронизации может потребоваться код доступа.\nВ этом случае обратитесь к следующей инструкции или к гуглу.\n\n\n\n\nКонтроллеры PS3, связанные по беспроводной сети, должны появиться\nв списке устройств в Настройках системы -> Bluetooth. Возможно, вам придется\nудалить их из этого списка, если вы хотите снова использовать их с PS3.\n\nТакже всегда отключайте их от Bluetooth, когда он не используется,\nиначе будут садиться батарейки.\n\nBluetooth должен обрабатывать до 7 подключенных устройств,\nхотя у вас может получиться по-другому.", + "ouyaInstructionsText": "Чтобы использовать контроллер PS3 с OUYA, просто подключите его один раз\nс помощью кабеля USB для синхронизации. Это может отключить другие\nконтроллеры, тогда нужно перезагрузить OUYA и отсоединить кабель USB.\n\nПосле этого можно использовать кнопку 'PS' контроллера для беспроводного\nподключения. После игры нажмите и удерживайте кнопку 'PS' в течение\n10 секунд чтобы выключить контроллер, в противном случае он может\nостаться включенным и разрядит батарейки.", + "pairingTutorialText": "видео-тьюториал по синхронизации", + "titleText": "Использование контроллеров PS3 с ${APP_NAME}:" + }, + "publicBetaText": "ОТКРЫТАЯ БЕТА-ВЕРСИЯ", + "punchBoldText": "УДАР", + "punchText": "Ударить", + "purchaseForText": "Купить за ${PRICE}", + "purchaseGameText": "Купить игру", + "purchasingText": "Покупка...", + "quitGameText": "Выйти из ${APP_NAME}?", + "quittingIn5SecondsText": "Выход через 5 секунд...", + "randomPlayerNamesText": "Дима, Кузя, Вован, Маха, Русский, Какуля, Бибер, Борька, Няшка, Толян", + "randomText": "Случайный", + "rankText": "Ранг", + "ratingText": "Рейтинг", + "reachWave2Text": "Достигните второй волны, чтобы получить ранг.", + "readyText": "готов", + "recentText": "Последний", + "remainingInTrialText": "осталось в пробной версии", + "remoteAppInfoShortText": "Играть в ${APP_NAME} с семьей или друзьями гораздо веселее.\nПодключите один или несколько джойстиков или установите\n${REMOTE_APP_NAME} на свои устройства, чтобы использовать\nих в качестве джойстиков.", + "remote_app": { + "app_name": "ДУ BombSquad", + "app_name_short": "ДУBS", + "button_position": "Положение кнопки", + "button_size": "Размер кнопки", + "cant_resolve_host": "Сервер не найден.", + "capturing": "Слушаю...", + "connected": "Соединено.", + "description": "Используйте Ваш телефон или планшет как контроллер BombSquad.\nДо 8 устройств могут быть одновременно подключены для эпических битв в мультиплеере на одном ТВ или планшете.", + "disconnected": "Выброшен сервером.", + "dpad_fixed": "неподвижный", + "dpad_floating": "плавающий", + "dpad_position": "Расположение D-Pad", + "dpad_size": "Размер D-Pad", + "dpad_type": "Тип D-Pad", + "enter_an_address": "Введите адрес", + "game_full": "Комната заполнена или соединения не принимаются.", + "game_shut_down": "Сервер отключился.", + "hardware_buttons": "Аппаратные кнопки", + "join_by_address": "Присоединиться по адресу...", + "lag": "Лаг: ${SECONDS} секунд", + "reset": "Сброс", + "run1": "Бег 1", + "run2": "Бег 2", + "searching": "Ищем игры BombSquad...", + "searching_caption": "Нажмите на название игры для входа.\nУбедитесь, что вы находитесь в той же сети, что и игра.", + "start": "Старт", + "version_mismatch": "Несовпадение версий.\nУбедитесь, что BombSquad и контроллер BombSquad\nобновлены до последней версии и повторите попытку." + }, + "removeInGameAdsText": "Разблокируйте \"${PRO}\" в магазине, чтобы убрать рекламу в игре.", + "renameText": "Переименовать", + "replayEndText": "Завершить просмотр записи", + "replayNameDefaultText": "Запись последней игры", + "replayReadErrorText": "Ошибка чтения файла записи.", + "replayRenameWarningText": "Переименуйте запись \"${REPLAY}\" чтобы ее сохранить; иначе она будет перезаписана.", + "replayVersionErrorText": "К сожалению, эта запись была сделана в другой\nверсии игры и не может быть использована.", + "replayWatchText": "Смотреть запись", + "replayWriteErrorText": "Ошибка записи файла записи.", + "replaysText": "Записи", + "reportPlayerExplanationText": "Пожалуйтесь по этому адресу на читеров, грубиянов или кто плохо себя ведет.\nОпишите чем недовольны:", + "reportThisPlayerCheatingText": "Нечестный игрок", + "reportThisPlayerLanguageText": "Грубиян", + "reportThisPlayerReasonText": "На что хотите пожаловаться?", + "reportThisPlayerText": "Жалоба на Игрока", + "requestingText": "Запрос данных...", + "restartText": "Начать заново", + "retryText": "Повтор", + "revertText": "Восстановить", + "runText": "Бежать", + "saveText": "Сохранить", + "scanScriptsErrorText": "Ошибка(и) сканирования скриптов; посмотри лог для подробностей.", + "scoreChallengesText": "Медали за очки", + "scoreListUnavailableText": "Список очков недоступен.", + "scoreText": "Очки", + "scoreUnits": { + "millisecondsText": "Миллисекунды", + "pointsText": "Очки", + "secondsText": "Секунды" + }, + "scoreWasText": "(было ${COUNT})", + "selectText": "Выбрать", + "seriesWinLine1PlayerText": "ПОБЕДИЛ В", + "seriesWinLine1TeamText": "ПОБЕДИЛИ В", + "seriesWinLine1Text": "ПОБЕДИЛ В", + "seriesWinLine2Text": "СЕРИИ!", + "settingsWindow": { + "accountText": "Аккаунт", + "advancedText": "Дополнительно", + "audioText": "Аудио", + "controllersText": "Контроллеры", + "graphicsText": "Графика", + "playerProfilesMovedText": "Примечание: Профили игроков были перемещены в окно Аккаунты в главном меню.", + "playerProfilesText": "Профили игроков", + "titleText": "Настройки" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(простая, удобная для контроллера виртуальная клавиатура для ввода текста)", + "alwaysUseInternalKeyboardText": "Всегда использовать встроенную клавиатуру", + "benchmarksText": "Тест производительности и тест-нагрузка", + "disableCameraGyroscopeMotionText": "Отключить движение камеры с помощью гироскопа", + "disableCameraShakeText": "Отключить тряску камеры", + "disableThisNotice": "(вы можете отключить это уведомление в настройках)", + "enablePackageModsDescriptionText": "(позволяет дополнительные возможности для моддинга, но отключает сетевую игру)", + "enablePackageModsText": "Включить моды локального пакета", + "enterPromoCodeText": "Введите промо-код", + "forTestingText": "Примечание: эти значения используются только для тестирования и будут потеряны, когда приложение завершит работу.", + "helpTranslateText": "Переводы игры ${APP_NAME} с английского совершён общественными\nусилиями. Если вы хотите предложить или исправить\nперевод, следуйте по ссылке ниже. Заранее спасибо!", + "kickIdlePlayersText": "Выкидывать бездействующих игроков", + "kidFriendlyModeText": "Семейный режим (меньше насилия, и т.д.)", + "languageText": "Язык", + "moddingGuideText": "Руководство по моддингу", + "mustRestartText": "Необходимо перезапустить игру, чтобы изменения вступили в силу.", + "netTestingText": "Тестирование сети", + "resetText": "Сбросить", + "showBombTrajectoriesText": "Показывать траекторию бомбы", + "showPlayerNamesText": "Показывать имена игроков", + "showUserModsText": "Показать папку модов", + "titleText": "Дополнительно", + "translationEditorButtonText": "Редактор перевода ${APP_NAME}", + "translationFetchErrorText": "статус перевода недоступен", + "translationFetchingStatusText": "проверка статуса перевода...", + "translationInformMe": "Сообщите мне, если мой язык нуждается в обновлениях", + "translationNoUpdateNeededText": "данный язык полностью обновлен, ура!", + "translationUpdateNeededText": "** данный язык нуждается в обновлениях!! **", + "vrTestingText": "Тестирование VR" + }, + "shareText": "Поделиться", + "sharingText": "Делимся...", + "showText": "Показать", + "signInForPromoCodeText": "Вы должны войти в аккаунт для активации кода.", + "signInWithGameCenterText": "Чтобы использовать аккаунт GameCenter,\nвойдите через GameCenter.", + "singleGamePlaylistNameText": "Просто ${GAME}", + "singlePlayerCountText": "1 игрок", + "soloNameFilterText": "${NAME} соло", + "soundtrackTypeNames": { + "CharSelect": "Выбор персонажа", + "Chosen One": "Избранный", + "Epic": "Игры в эпическом режиме", + "Epic Race": "Эпическая гонка", + "FlagCatcher": "Захват флага", + "Flying": "Счастливые мысли", + "Football": "Регби", + "ForwardMarch": "Нападение", + "GrandRomp": "Завоевание", + "Hockey": "Хоккей", + "Keep Away": "Не подходить!", + "Marching": "Манёвр", + "Menu": "Главное меню", + "Onslaught": "Атака", + "Race": "Гонка", + "Scary": "Царь горы", + "Scores": "Счетное табло", + "Survival": "Ликвидация", + "ToTheDeath": "Смертельный бой", + "Victory": "Табло финального счета" + }, + "spaceKeyText": "пробел", + "statsText": "Статистика", + "storagePermissionAccessText": "Это требует доступа к хранилищу", + "store": { + "alreadyOwnText": "У вас уже есть ${NAME}!", + "bombSquadProDescriptionText": "• Удваивает билеты заработанные за достижения\n• Убирает внутриигровую рекламу\n• Включает ${COUNT} бонусных билетов\n• +${PERCENT}% дополнительных очков лиги\n• Разблокировывает '${INF_ONSLAUGHT}' и\n '${INF_RUNAROUND}' кооперативные уровни", + "bombSquadProFeaturesText": "Особенности:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "- Убирает рекламу\n- Откроет мини-игры\n- Также включает:", + "buyText": "Купить", + "charactersText": "Персонажи", + "comingSoonText": "Скоро…", + "extrasText": "Дополнительно", + "freeBombSquadProText": "BombSquad теперь бесплатная игра, но если вы приобрели ее ранее, то вы\nполучаете обновление BombSquad Pro и ${COUNT} билетов в качестве благодарности.\nНаслаждайтесь новыми возможностями, и спасибо за вашу поддержку! \n-Эрик", + "gameUpgradesText": "Обновления игры", + "getCoinsText": "Купить монеты", + "holidaySpecialText": "Праздничная акция", + "howToSwitchCharactersText": "(Зайдите в \"${SETTINGS} -> ${PLAYER_PROFILES}\", чтобы выбрать и изменить персонажей)", + "howToUseIconsText": "(создайте глобальные профили игроков (в окне аккаунт) чтобы использовать это)", + "howToUseMapsText": "(используйте эти карты в ваших собственных командных/все-против-всех плейлистах)", + "iconsText": "Иконки", + "loadErrorText": "Не удается загрузить страницу.\nПроверьте подключение к Интернету.", + "loadingText": "загрузка", + "mapsText": "Карты", + "miniGamesText": "Мини-игры", + "oneTimeOnlyText": "(только один раз)", + "purchaseAlreadyInProgressText": "Эта покупка уже выполняется.", + "purchaseConfirmText": "Купить ${ITEM}?", + "purchaseNotValidError": "Покупка недействительна.\nЕсли это ошибка, обратитесь к ${EMAIL}.", + "purchaseText": "Купить", + "saleBundleText": "Распродажа в комплекте!", + "saleExclaimText": "Распродажа!", + "salePercentText": "(Скидка ${PERCENT}%)", + "saleText": "СКИДКА", + "searchText": "Поиск", + "teamsFreeForAllGamesText": "Командные игры / Каждый сам за себя", + "totalWorthText": "*** ${TOTAL_WORTH} значение! ***", + "upgradeQuestionText": "Обновить?", + "winterSpecialText": "Зимняя акция", + "youOwnThisText": "- у вас это уже есть -" + }, + "storeDescriptionText": "Игровое безумие с 8 игроками!\n\nВзрывайте своих друзей (или ботов) в турнире взрывных мини-игр, таких как Захват флага и Эпический смертельный бой замедленного действия!\n\nС простым управлением и расширенной поддержкой контроллеров 8 человек могут присоединиться к действию, можно даже использовать мобильные устройства как контроллеры через бесплатное приложение 'BombSquad Remote'!\n\nВ атаку!\n\nСм. www.froemling.net/BombSquad для дополнительной информации.", + "storeDescriptions": { + "blowUpYourFriendsText": "Взорви друзей.", + "competeInMiniGamesText": "Соревнуйтесь в мини-играх от гонок до левитации.", + "customize2Text": "Настройка персонажей, мини-игр и даже саундтрека.", + "customizeText": "Настройка персонажей и создание своих собственных плей-листов мини-игр.", + "sportsMoreFunText": "Спорт веселее со взрывчаткой.", + "teamUpAgainstComputerText": "Команды против компьютера." + }, + "storeText": "Магазин", + "submitText": "Отправить", + "submittingPromoCodeText": "Активация кода....", + "teamNamesColorText": "имена/цвета команд", + "teamsText": "Команды", + "telnetAccessGrantedText": "Доступ Telnet включен.", + "telnetAccessText": "Обнаружен доступ Telnet. Разрешить?", + "testBuildErrorText": "Эта версия устарела, пожалуйста, проверьте обновления.", + "testBuildText": "Тестовая версия", + "testBuildValidateErrorText": "Не удается проверить тестовую сборку. (нет соединения с сетью?)", + "testBuildValidatedText": "Тестовая сборка проверена. Наслаждайтесь!", + "thankYouText": "Спасибо за вашу поддержку! Веселой игры!!", + "threeKillText": "ТРЕХ ЗА РАЗ!!", + "timeBonusText": "Бонус времени", + "timeElapsedText": "Прошло времени", + "timeExpiredText": "Время вышло", + "timeSuffixDaysText": "${COUNT}д", + "timeSuffixHoursText": "${COUNT}ч", + "timeSuffixMinutesText": "${COUNT}м", + "timeSuffixSecondsText": "${COUNT}с", + "tipText": "Подсказка", + "titleText": "BombSquad", + "titleVRText": "BombSquad ВР", + "topFriendsText": "Топ друзей", + "tournamentCheckingStateText": "Проверка статуса турнира, пожалуйста, подождите...", + "tournamentEndedText": "Турнир закончился. Скоро начнется новый.", + "tournamentEntryText": "Вход в турнир", + "tournamentResultsRecentText": "Последние Результаты турнира", + "tournamentStandingsText": "Позиции в турнире", + "tournamentText": "Турнир", + "tournamentTimeExpiredText": "Время турнира истекло", + "tournamentsText": "Турниры", + "translations": { + "characterNames": { + "Agent Johnson": "Агент Джонсон", + "B-9000": "B-9000", + "Bernard": "Бернард", + "Bones": "Костяшка", + "Butch": "Силач", + "Easter Bunny": "Пасхальный кролик", + "Flopsy": "Флопси", + "Frosty": "Снежный", + "Gretel": "Гретель", + "Grumbledorf": "Грамблдорф", + "Jack Morgan": "Джек Морган", + "Kronk": "Кронк", + "Lee": "Ли", + "Lucky": "Счастливчик", + "Mel": "Мэл", + "Middle-Man": "Средняк", + "Minimus": "Минимус", + "Pascal": "Паскаль", + "Pixel": "Пиксель", + "Sammy Slam": "Сэмми Слэм", + "Santa Claus": "Санта Клаус", + "Snake Shadow": "Тень Змеи", + "Spaz": "Спаз", + "Taobao Mascot": "Талисман Таобао", + "Todd": "Тодд", + "Todd McBurton": "Тодд МакБартон", + "Xara": "Ксара", + "Zoe": "Зои", + "Zola": "Зола" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Бесконечная\nатака", + "Infinite\nRunaround": "Бесконечный\nманевр", + "Onslaught\nTraining": "Атака:\nтренировка", + "Pro\nFootball": "Регби\nпрофи", + "Pro\nOnslaught": "Атака\nпрофи", + "Pro\nRunaround": "Манёвр\nпрофи", + "Rookie\nFootball": "Регби\nдля новичков", + "Rookie\nOnslaught": "Атака\nдля новичков", + "The\nLast Stand": "Последний\nрубеж", + "Uber\nFootball": "Убер\nрегби", + "Uber\nOnslaught": "Убер\nатака", + "Uber\nRunaround": "Убер\nманёвр" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME}: тренировка", + "Infinite ${GAME}": "Бесконечный уровень ${GAME}", + "Infinite Onslaught": "Бесконечная атака", + "Infinite Runaround": "Бесконечный манёвр", + "Onslaught": "Бесконечная атака", + "Onslaught Training": "Атака: тренировка", + "Pro ${GAME}": "${GAME} профи", + "Pro Football": "Регби профи", + "Pro Onslaught": "Атака профи", + "Pro Runaround": "Манёвр профи", + "Rookie ${GAME}": "${GAME} для новичков", + "Rookie Football": "Регби для новичков", + "Rookie Onslaught": "Атака для новичков", + "Runaround": "Бесконечный манёвр", + "The Last Stand": "Последний рубеж", + "Uber ${GAME}": "Убер ${GAME}", + "Uber Football": "Убер регби", + "Uber Onslaught": "Убер атака", + "Uber Runaround": "Убер манёвр" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Чтобы победить, стань избранным на некоторое время.\nЧтобы стать избранным, убей избранного.", + "Bomb as many targets as you can.": "Взорвите столько мишеней, сколько сможете.", + "Carry the flag for ${ARG1} seconds.": "Пронесите флаг в течение ${ARG1} секунд.", + "Carry the flag for a set length of time.": "Пронесите флаг в течение заданного времени.", + "Crush ${ARG1} of your enemies.": "Разбейте ${ARG1} врагов.", + "Defeat all enemies.": "Победите всех врагов.", + "Dodge the falling bombs.": "Увернитесь от падающих бомб.", + "Final glorious epic slow motion battle to the death.": "Финальная эпическая смертельная битва в замедленном действии.", + "Gather eggs!": "Соберите яйца!", + "Get the flag to the enemy end zone.": "Отнесите флаг в зону защиты противника.", + "How fast can you defeat the ninjas?": "Как быстро вы сможете победить ниндзя?", + "Kill a set number of enemies to win.": "Убейте заданное число врагов, чтобы выиграть.", + "Last one standing wins.": "Побеждает последний стоящий на ногах.", + "Last remaining alive wins.": "Побеждает последний в живых.", + "Last team standing wins.": "Побеждает последняя стоящая на ногах команда.", + "Prevent enemies from reaching the exit.": "Не дайте врагам дойти до выхода.", + "Reach the enemy flag to score.": "Доберитесь до вражеского флага чтобы набрать очки.", + "Return the enemy flag to score.": "Принесите вражеский флаг на базу, чтобы набрать очки.", + "Run ${ARG1} laps.": "Пробегите ${ARG1} кругов.", + "Run ${ARG1} laps. Your entire team has to finish.": "Пробегите ${ARG1} кругов. Финишировать должна вся команда.", + "Run 1 lap.": "Пробегите 1 круг.", + "Run 1 lap. Your entire team has to finish.": "Пробегите 1 круг. Финишировать должна вся команда.", + "Run real fast!": "Бегите очень быстро!", + "Score ${ARG1} goals.": "Забейте ${ARG1} голов.", + "Score ${ARG1} touchdowns.": "Сделайте ${ARG1} тачдауна.", + "Score a goal": "Забейте гол", + "Score a goal.": "Забейте гол.", + "Score a touchdown.": "Сделайте тачдаун.", + "Score some goals.": "Забейте несколько голов.", + "Secure all ${ARG1} flags.": "Захватите все флаги: ${ARG1}.", + "Secure all flags on the map to win.": "Захватите все флаги на карте, чтобы выиграть.", + "Secure the flag for ${ARG1} seconds.": "Захватите флаг на ${ARG1} секунд.", + "Secure the flag for a set length of time.": "Захватите флаг на определенное время.", + "Steal the enemy flag ${ARG1} times.": "Украдите вражеский флаг ${ARG1} раз.", + "Steal the enemy flag.": "Украдите вражеский флаг.", + "There can be only one.": "Может быть только один.", + "Touch the enemy flag ${ARG1} times.": "Коснитесь вражеского флага ${ARG1} раз.", + "Touch the enemy flag.": "Коснитесь вражеского флага.", + "carry the flag for ${ARG1} seconds": "пронесите флаг в течение ${ARG1} секунд", + "kill ${ARG1} enemies": "убейте ${ARG1} врагов", + "last one standing wins": "побеждает последний стоящий на ногах", + "last team standing wins": "побеждает последняя стоящая на ногах команда", + "return ${ARG1} flags": "принесите ${ARG1} флагов на базу", + "return 1 flag": "принесите 1 флаг на базу", + "run ${ARG1} laps": "пробегите ${ARG1} кругов", + "run 1 lap": "пробегите 1 круг", + "score ${ARG1} goals": "забейте ${ARG1} голов", + "score ${ARG1} touchdowns": "сделайте ${ARG1} тачдауна", + "score a goal": "забейте гол", + "score a touchdown": "сделайте тачдаун", + "secure all ${ARG1} flags": "захватите все флаги: ${ARG1}", + "secure the flag for ${ARG1} seconds": "захватите флаг на ${ARG1} секунд", + "touch ${ARG1} flags": "коснитесь ${ARG1} флагов", + "touch 1 flag": "коснитесь 1 флага" + }, + "gameNames": { + "Assault": "Нападение", + "Capture the Flag": "Захват флага", + "Chosen One": "Избранный", + "Conquest": "Завоевание", + "Death Match": "Смертельный бой", + "Easter Egg Hunt": "Охота на пасхальные яйца", + "Elimination": "Ликвидация", + "Football": "Регби", + "Hockey": "Хоккей", + "Keep Away": "Не подходить!", + "King of the Hill": "Царь горы", + "Meteor Shower": "Метеоритный дождь", + "Ninja Fight": "Бой с ниндзя", + "Onslaught": "Атака", + "Race": "Гонка", + "Runaround": "Обход", + "Target Practice": "Стрельба по мишеням", + "The Last Stand": "Последний рубеж" + }, + "inputDeviceNames": { + "Keyboard": "Клавиатура", + "Keyboard P2": "Клавиатура P2" + }, + "languages": { + "Arabic": "Арабский", + "Belarussian": "Белорусский", + "Chinese": "Китайский упрощенный", + "ChineseTraditional": "Китайский традиционный", + "Croatian": "Харватский", + "Czech": "Чешский", + "Danish": "Датский", + "Dutch": "Голландский", + "English": "Английский", + "Esperanto": "Эсперанто", + "Finnish": "Финский", + "French": "Французский", + "German": "Немецкий", + "Gibberish": "Абракадабра", + "Greek": "греческий", + "Hindi": "Хинди", + "Hungarian": "Венгерский", + "Indonesian": "Индонезийский", + "Italian": "Итальянский", + "Japanese": "Японский", + "Korean": "Корейский", + "Persian": "Персидский", + "Polish": "Польский", + "Portuguese": "Португальский", + "Romanian": "Румынский", + "Russian": "Русский", + "Serbian": "Сербский", + "Slovak": "Словацкий", + "Spanish": "Испанский", + "Swedish": "Шведский", + "Turkish": "Турецкий", + "Ukrainian": "Украинский", + "Venetian": "Венецианский", + "Vietnamese": "Вьетнамский" + }, + "leagueNames": { + "Bronze": "Бронзовая", + "Diamond": "Бриллиантовая", + "Gold": "Золотая", + "Silver": "Серебряная" + }, + "mapsNames": { + "Big G": "Большая G", + "Bridgit": "Мостики", + "Courtyard": "Внутренний двор", + "Crag Castle": "Замок на скале", + "Doom Shroom": "Роковой гриб", + "Football Stadium": "Стадион регби", + "Happy Thoughts": "Счастливые мысли", + "Hockey Stadium": "Хоккейный стадион", + "Lake Frigid": "Ледяное озеро", + "Monkey Face": "Обезьяна", + "Rampage": "Беспредел", + "Roundabout": "Кольцевая", + "Step Right Up": "Проходите", + "The Pad": "Подушка", + "Tip Top": "Тип-топ", + "Tower D": "Башня D", + "Zigzag": "Зигзаг" + }, + "playlistNames": { + "Just Epic": "Только эпический", + "Just Sports": "Только спорт" + }, + "promoCodeResponses": { + "invalid promo code": "недействительный промо-код" + }, + "scoreNames": { + "Flags": "Флаги", + "Goals": "Голы", + "Score": "Очки", + "Survived": "Выжил", + "Time": "Время", + "Time Held": "Время удержания" + }, + "serverResponses": { + "A code has already been used on this account.": "Код уже был активирован на этом аккаунте.", + "A reward has already been given for that address.": "Эта награда уже была выдана на этот ip адрес", + "Account linking successful!": "Аккаунт успешно привязан!", + "Account unlinking successful!": "Аккаунт успешно отвязан!", + "Accounts are already linked.": "Аккаунты уже привязаны.", + "An error has occurred; (${ERROR})": "Произошла ошибка; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "произошла ошибка;Пожалуйста обратитесь в службу поддержки. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Произошла ошибка; пожалуйста, свяжитесь с support@froemling.net.", + "An error has occurred; please try again later.": "Произошла ошибка, пожалуйста, повторите попытку позже.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Точно хотите связать аккаунты?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nОтменить будет нельзя!", + "BombSquad Pro unlocked!": "BombSquad Pro разблокирован!", + "Can't link 2 accounts of this type.": "Невозможно связать 2 аккаунта этого типа.", + "Can't link 2 diamond league accounts.": "Невозможно связать 2 аккаунта бриллиантовой лиги.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Невозможно связать; будет превышен максимум ${COUNT} связанных аккаунтов.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Ах ты, читер; Очки и награды заморожены на ${COUNT} дней.", + "Could not establish a secure connection.": "Не удалось установить безопасное соединение.", + "Daily maximum reached.": "Хватит на сегодня.", + "Entering tournament...": "Вход в турнир...", + "Invalid code.": "Неверный код.", + "Invalid payment; purchase canceled.": "Что-то пошло не так. Покупка отменена.", + "Invalid promo code.": "Неверный промо-код.", + "Invalid purchase.": "Ошибка транзакции.", + "Invalid tournament entry; score will be ignored.": "Неверная заявка на турнир; счет будет проигнорирован.", + "Item unlocked!": "Предмет разблокирован!!!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "СВЯЗКА ЗАПРЕЩЕНА. ${ACCOUNT} содержит \nважные данные, которые БУДУТ ПОТЕРЯНЫ.\nВы можете связать в обратном порядке\n(и потерять данные ЭТОГО аккаунта)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Связать этот ${ACCOUNT} аккаунт с этим?\nВсе существующие данные на ${ACCOUNT} будут потеряны.\nЭто действие не может быть отменено. Вы уверены?", + "Max number of playlists reached.": "Достигнуто максимальное количество плейлистов.", + "Max number of profiles reached.": "Достигнуто максимальное количество профилей.", + "Maximum friend code rewards reached.": "Достигнут лимит кодов.", + "Message is too long.": "Сообщение слишком длинное.", + "Profile \"${NAME}\" upgraded successfully.": "Профиль \"${NAME}\" обновлен успешно.", + "Profile could not be upgraded.": "Профиль не может быть обновлен.", + "Purchase successful!": "Успешная транзакция!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Получено ${COUNT} билетов за вход.\nПриходите завтра, чтобы получить ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Функциональность сервера больше не поддерживается в этой версии игры;\nПожалуйста обновите игру.", + "Sorry, there are no uses remaining on this code.": "Упс, код больше не может быть активирован.", + "Sorry, this code has already been used.": "Упс, этот код уже использован.", + "Sorry, this code has expired.": "Упс, время действия кода истекло.", + "Sorry, this code only works for new accounts.": "Упс, этот код работает только для новых аккаунтов.", + "Temporarily unavailable; please try again later.": "Временно недоступно; Пожалуйста, повторите попытку позже.", + "The tournament ended before you finished.": "Турнир закончился прежде, чем вы закончили.", + "This account cannot be unlinked for ${NUM} days.": "Этот аккаунт невозможно отвязать в течение ${NUM} дней.", + "This code cannot be used on the account that created it.": "Этот код не может быть использован его создателем.", + "This is currently unavailable; please try again later.": "В настоящее время он недоступен; пожалуйста, повторите попытку позже", + "This requires version ${VERSION} or newer.": "Для этого необходима версия ${VERSION} или новее.", + "Tournaments disabled due to rooted device.": "Турниры отключены из-за рутированного устройства.", + "Tournaments require ${VERSION} or newer": "Для турниров требуется версия ${VERSION} или выше.", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Отвязать ${ACCOUNT} от этого аккаунта?\nВсе данные на ${ACCOUNT} будут сброшены.\n(за исключением достижений в некоторых случаях)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ПРЕДУПРЕЖДЕНИЕ: жалобы на хакерство были выданы на вашу учетную запись.\nУчетные записи, которые считаются взломанными, будут заблокированы. Пожалуйста, играйте честно.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Желаете связать аккаунт устройства вот c этим?\n\nАккаунт устройства ${ACCOUNT1}\nТекущий аккаунт ${ACCOUNT2}\n\nЭто позволит сохранить ваши нынешние достижения.\nВнимание: отмена невозможна!", + "You already own this!": "Вы это уже приобрели!", + "You can join in ${COUNT} seconds.": "Ты можешь войти через ${COUNT} секунд", + "You don't have enough tickets for this!": "У вас недостаточно билетов для этой покупки!", + "You don't own that.": "У вас этого нету.", + "You got ${COUNT} tickets!": "Вы получили ${COUNT} билетов!", + "You got a ${ITEM}!": "Вы получили ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Вас повысили и перевели в новую лигу; поздравляем!", + "You must update to a newer version of the app to do this.": "Чтобы это сделать, вы должны обновить приложение.", + "You must update to the newest version of the game to do this.": "Вы должны обновиться до новейшей версии игры, чтобы сделать это.", + "You must wait a few seconds before entering a new code.": "Подождите несколько секунд, прежде чем вводить новый код.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Ваш ранг в последнем турнире: ${RANK}! Спасибо за игру!", + "Your account was rejected. Are you signed in?": "Ваш аккаунт отклонён. Вы вошли в систему?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Ваша версия игры была модифицирована.\nУберите все изменения и попробуйте снова.", + "Your friend code was used by ${ACCOUNT}": "Ваш код был использован ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 минута", + "1 Second": "1 секунда", + "10 Minutes": "10 минут", + "2 Minutes": "2 минуты", + "2 Seconds": "2 секунды", + "20 Minutes": "20 минут", + "4 Seconds": "4 секунды", + "5 Minutes": "5 минут", + "8 Seconds": "8 секунд", + "Allow Negative Scores": "Разрешить отрицательный счет", + "Balance Total Lives": "Распределять оставшиеся жизни", + "Bomb Spawning": "Появление бомб", + "Chosen One Gets Gloves": "Избранный получает перчатки", + "Chosen One Gets Shield": "Избранный получает щит", + "Chosen One Time": "Время избранного", + "Enable Impact Bombs": "Включить ударные бомбы", + "Enable Triple Bombs": "Включить тройные бомбы", + "Entire Team Must Finish": "Вся команда должна финишировать", + "Epic Mode": "Эпический режим", + "Flag Idle Return Time": "Время возврата брошенного флага", + "Flag Touch Return Time": "Время захвата флага", + "Hold Time": "Время удержания", + "Kills to Win Per Player": "Убийств на игрока до победы", + "Laps": "Круги", + "Lives Per Player": "Жизней на игрока", + "Long": "Долго", + "Longer": "Дольше", + "Mine Spawning": "Минирование", + "No Mines": "Без мин", + "None": "Нет", + "Normal": "Норм.", + "Pro Mode": "Профессиональный режим", + "Respawn Times": "Время до воскрешения", + "Score to Win": "Очков для победы", + "Short": "Коротк.", + "Shorter": "Короче", + "Solo Mode": "Режим соло", + "Target Count": "Количество целей", + "Time Limit": "Лимит времени" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} дисквалифицирована потому что ${PLAYER} вышел", + "Killing ${NAME} for skipping part of the track!": "Ликвидация ${NAME} за срезание трассы!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Предупреждение для ${NAME}: за турбо / быстрое повторное нажатие кнопки можно вылететь." + }, + "teamNames": { + "Bad Guys": "Плохие парни", + "Blue": "Синие", + "Good Guys": "Хорошие парни", + "Red": "Красные" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Идеально выполненная последовательность бег-прыжок-поворот-удар может убить\nодним ударом и заработать вам пожизненное уважение друзей.", + "Always remember to floss.": "Не забывайте пользоваться зубной нитью.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Создайте профили игроков для себя и своих друзей с собственными\nименами и внешностью вместо случайных.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Ящики с проклятием превращают вас в тикающую бомбу.\nЕдинственное лекарство - быстро схватить аптечку.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Независимо от внешности, способности всех персонажей идентичны,\nтак что просто выбирайте того, на кого вы больше похожи.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Не зазнавайтесь с этим энергетическим щитом, вас все еще могут сбросить с обрыва.", + "Don't run all the time. Really. You will fall off cliffs.": "Не бегай все время. Серьезно. Свалишься с обрыва.", + "Don't spin for too long; you'll become dizzy and fall.": "Не крутись долго; у тебя закружится голова и ты упадёшь.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Для бега нажмите и держите любую кнопку. (Для этого удобны триггеры, если они есть)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Для бега удерживайте любую кнопку. Бегать, конечно, быстрее,\nзато труднее поворачивать, так что не забывайте про обрывы.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ледяные бомбы не очень мощные, но они замораживают\nвсех вокруг, делая их хрупкими и бьющимися.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Если кто-то вас схатил, бейте, и вас отпустят.\nВ реальной жизни это тоже работает.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Если вам не хватает контроллеров, установите приложение '${REMOTE_APP_NAME}' \nна ваши мобильные устройства, чтобы использовать их в качестве контроллеров.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Если не хватает контроллеров, установите приложение 'BombSquad Remote' на\nустройства iOS или Android, чтобы использовать их в качестве контроллеров.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Если к вам прилипла липкая бомба, прыгайте и крутитесь. Может повезет\nстряхнуть бомбу или, на худой конец, повеселить окружающих.", + "If you kill an enemy in one hit you get double points for it.": "Если убиваешь врага с одного удара, то получаешь двойные очки.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Если подхватили проклятие, то единственная надежда на выживание\n- это найти аптечку в ближайшие несколько секунд.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Не стой на месте - поджаришься. Беги и уворачивайся чтобы выжить..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Если у вас много игроков, которые приходят и уходят, включите \"автоматически выкидывать\nбездействующих игроков\" в настройках на случай, если кто-то забудет выйти из игры.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Если ваше устройство нагревается или вы хотите сохранить заряд батареи,\nуменьшите \"Визуальные эффекты\" или \"Разрешение\" в Настройки->Графика", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Если картинка прерывистая, попробуйте уменьшить разрешение\nили визуальные эффекты в настройках графики в игре.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "В Захвате флага, чтобы набрать очки, свой собственный флаг должен быть на базе.\nЕсли другая команда вот-вот забьет, можно украсть их флаг, чтобы их остановить.", + "In hockey, you'll maintain more speed if you turn gradually.": "В хоккее можно поддерживать более высокую скорость, если поворачивать постепенно.", + "It's easier to win with a friend or two helping.": "Выиграть легче всего с помощью друга или двух.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Прыгни прямо перед броском, чтобы забросить бомбу как можно выше.", + "Land-mines are a good way to stop speedy enemies.": "Мины - неплохой способ остановить быстроногих врагов.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Можно много чего поднять и бросить, включая других игроков. Швыряние врагов\nс обрыва бывает эффективной и бодрящей стратегией.", + "No, you can't get up on the ledge. You have to throw bombs.": "Нет, вы не сможете залезть на выступ. Бросайте бомбы.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Игроки могут присоединяться и уходить посреди игры,\nтакже можно подключать и отключать контроллеры прямо на лету.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Игроки могут присоединяться и выходить в середине большинства игр,\nтакже можно подключать и отключать геймпады на лету.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Усилители ограничены по времени только в кооперативных играх.\nВ команых играх и 'каждый за себя' они ваши пожизненно.", + "Practice using your momentum to throw bombs more accurately.": "Тренируйтесь использовать инерцию для более точных бросков.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Удары наносят тем больше ущерба, чем быстрее двигаются кулаки,\nтак что старайтесь бегать, прыгать и крутиться, как ненормальные.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Пробегитесь туда-обратно перед броском, чтобы\n'подкрутить' бомбу и бросить ее дальше.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Сметите группу врагов, взорвав\nбомбу возле коробки TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Голова - самое уязвимое место, так что липкая бомба\nпо чайнику, как правило, означает капут.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Этот уровень бесконечен, но высокие очки здесь\nзаработают вам вечное уважение во всем мире.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Сила броска зависит от направления, которое нажато. Чтобы аккуратно\nбросить что-то прямо перед собой, не нажимайте ни в каком направлении.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Надоел саундтрек? Замените его собственным!\nСмотрите Настройки->Аудио->Саундтрек", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Попробуйте 'подогреть' бомбы секунду или две, прежде чем их бросить.", + "Try tricking enemies into killing eachother or running off cliffs.": "Попробуйте обмануть врагов, чтобы они убили друг друга или прыгнули с обрыва.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Используйте кнопку выбора (треугольник), чтобы схватить флаг < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Взмахни туда-сюда, чтобы забросить подальше..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Вы можете 'нацеливать' удары, крутясь влево или вправо.\nЭто полезно для сталкивания плохих парней с края или для голов в хоккее.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Когда взорвется бомба можно судить по цвету искр фитиля:\nжелтый..оранжевый..красный..БАБАХ.", + "You can throw bombs higher if you jump just before throwing.": "Бомбу можно бросить выше, если подпрыгнуть прямо перед броском.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Чтобы поменять персонажа не обязательно редактировать ​профиль. Просто нажмите\nверхнюю кнопку (подобрать) при вступлении в игру чтобы сменить персонажа.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Вы получаете повреждения, когда ударяетесь головой,\nтак что берегите голову.", + "Your punches do much more damage if you are running or spinning.": "От ударов гораздо больше вреда, когда бежишь или крутишься." + } + }, + "trialPeriodEndedText": "Ваш пробный период закончился. Хотите приобрести\nBombSquad и продолжать играть?", + "trophiesRequiredText": "Для этого надо минимум ${NUMBER} трофеев.", + "trophiesText": "Трофеев", + "trophiesThisSeasonText": "Трофеи за этот Сезон", + "tutorial": { + "cpuBenchmarkText": "Прогон тьюториала на безумной скорости (проверяет скорость процессора)", + "phrase01Text": "Привет!", + "phrase02Text": "Добро пожаловать в ${APP_NAME}!", + "phrase03Text": "Несколько советов по управлению персонажем:", + "phrase04Text": "Многое в ${APP_NAME} основано на законах ФИЗИКИ.", + "phrase05Text": "Например, при ударе кулаком..", + "phrase06Text": "..повреждения зависят от скорости кулака.", + "phrase07Text": "Видите? Мы не двигались, поэтому ${NAME} почти в полном порядке.", + "phrase08Text": "Теперь подпрыгнем и крутанемся для скорости.", + "phrase09Text": "Ага, так-то лучше.", + "phrase10Text": "Также помогает бег.", + "phrase11Text": "Для бега удерживайте любую кнопку.", + "phrase12Text": "Для супер-крутых ударов попробуйте бежать и крутиться.", + "phrase13Text": "Упс.. Извини, ${NAME}.", + "phrase14Text": "Можно поднимать и бросать вещи, например флаги.. или ${NAME}.", + "phrase15Text": "И, наконец, бомбы.", + "phrase16Text": "Бросание бомб требует тренировки.", + "phrase17Text": "Ай! Не очень хороший бросок.", + "phrase18Text": "В движении бросок получается дальше.", + "phrase19Text": "В прыжке бросок выше.", + "phrase20Text": "\"Подкрученные\" бомбы летят еще дальше.", + "phrase21Text": "\"Выжидать\" бомбы довольно сложно.", + "phrase22Text": "Черт.", + "phrase23Text": "Попробуйте \"подогреть\" фитиль секунду или две.", + "phrase24Text": "Ура! Хорошо подогрето.", + "phrase25Text": "Ну на этом, пожалуй, всё.", + "phrase26Text": "Вперед, на мины!", + "phrase27Text": "Не забывай эти советы, и ТОЧНО вернешься живым!", + "phrase28Text": "...может быть...", + "phrase29Text": "Удачи!", + "randomName1Text": "Вася", + "randomName2Text": "Петя", + "randomName3Text": "Иннокентий", + "randomName4Text": "Шурик", + "randomName5Text": "Пушок", + "skipConfirmText": "Пропустить тьюториал? Коснитесь или нажмите кнопку для подтверждения.", + "skipVoteCountText": "${COUNT}/${TOTAL} голосов за пропуск", + "skippingText": "пропуск обучения...", + "toSkipPressAnythingText": "(коснитесь или нажмите что-нибудь чтобы пропустить тьюториал)" + }, + "twoKillText": "ДВОИХ ЗА РАЗ!", + "unavailableText": "недоступно", + "unconfiguredControllerDetectedText": "Обнаружен ненастроенный контроллер:", + "unlockThisInTheStoreText": "Это должно быть разблокировано в магазине.", + "unlockThisProfilesText": "Чтобы создать более ${NUM} профиль, Вам необходимо:", + "unlockThisText": "Чтобы разблокировать это, вам нужно:", + "unsupportedHardwareText": "К сожалению, это оборудование не поддерживается в этой сборке игры.", + "upFirstText": "Для начала:", + "upNextText": "Далее в игре ${COUNT}:", + "updatingAccountText": "Обновление вашего аккаунта", + "upgradeText": "Обновление", + "upgradeToPlayText": "Разблокируйте \"${PRO}\" в магазине что-бы играть в это.", + "useDefaultText": "Использовать стандартные", + "usesExternalControllerText": "Эта игра может использовать внешний контроллер для управления.", + "usingItunesText": "Использование музыкального приложения для саундтрека...", + "usingItunesTurnRepeatAndShuffleOnText": "Убедитесь, что в iTunes включен случайный порядок, и повтор установлен на 'все'.", + "validatingBetaText": "Валидация бета-версии...", + "validatingTestBuildText": "Проверка тестовой сборки...", + "victoryText": "Победа!", + "voteDelayText": "Невозможно начать новое голосование еще ${NUMBER} секунд", + "voteInProgressText": "Голосование уже в процессе.", + "votedAlreadyText": "Вы уже проголосовали", + "votesNeededText": "Нужно ${NUMBER} голосов", + "vsText": "против", + "waitingForHostText": "(ожидание ${HOST} чтобы продолжить)", + "waitingForLocalPlayersText": "ожидание локальных игроков...", + "waitingForPlayersText": "ожидание присоединения игроков...", + "waitingInLineText": "Подожди немного (комната заполнена)...", + "watchAVideoText": "Смотреть видео", + "watchAnAdText": "Смотреть рекламу", + "watchWindow": { + "deleteConfirmText": "Удалить \"${REPLAY}\"?", + "deleteReplayButtonText": "Удалить\nзапись", + "myReplaysText": "Мои записи", + "noReplaySelectedErrorText": "Запись не выбрана", + "playbackSpeedText": "Скорость воспроизведения: ${SPEED}", + "renameReplayButtonText": "Переименовать\nзапись", + "renameReplayText": "Переименовать \"${REPLAY}\" на:", + "renameText": "Переименовать", + "replayDeleteErrorText": "Ошибка удаления записи.", + "replayNameText": "Имя записи", + "replayRenameErrorAlreadyExistsText": "Запись с таким именем уже существует.", + "replayRenameErrorInvalidName": "Невозможно переименовать запись; неверное имя.", + "replayRenameErrorText": "Ошибка переименования записи.", + "sharedReplaysText": "Общие записи", + "titleText": "Смотреть", + "watchReplayButtonText": "Смотреть\nзапись" + }, + "waveText": "Волна", + "wellSureText": "Сойдет!", + "wiimoteLicenseWindow": { + "titleText": "Авторские права DarwiinRemote" + }, + "wiimoteListenWindow": { + "listeningText": "Ожидание контроллеров Wii...", + "pressText": "Нажмите кнопки 1 и 2 на Wiimote одновременно.", + "pressText2": "На новых контроллерах Wiimote со встроенным Motion Plus нажмите красную кнопку 'sync' на задней части." + }, + "wiimoteSetupWindow": { + "copyrightText": "Авторские права DarwiinRemote", + "listenText": "Слушать", + "macInstructionsText": "Убедитесь, что ваш Wii выключен, а на вашем Маке включен\nBluetooth, затем нажмите 'Слушать'. Поддержка контроллеров Wii\nбывает немного капризна, так что, возможно, придется\nпопробовать несколько раз, пока получится подсоединиться. \n\nBluetooth должен обрабатывать до 7 подключенных устройств,\nхотя всякое бывает.\n\nBombSquad поддерживает оригинальные контроллеры Wiimote, \nнунчаки и классические контроллеры. \nТакже теперь работает и новый Wii Remote Plus,\nправда, без аксессуаров.", + "thanksText": "Это стало возможным благодаря\nкоманде DarwiinRemote.", + "titleText": "Настройка контроллера Wii" + }, + "winsPlayerText": "Победил ${NAME}!", + "winsTeamText": "Победили ${NAME}!", + "winsText": "${NAME} выиграл!", + "worldScoresUnavailableText": "Мировые результаты недоступны.", + "worldsBestScoresText": "Лучшие в мире очки", + "worldsBestTimesText": "Лучшее в мире время", + "xbox360ControllersWindow": { + "getDriverText": "Скачать драйвер", + "macInstructions2Text": "Для использования контроллеров по беспроводной связи, вам также\nпотребуется ресивер, который поставляется с \"беспроводным контроллером\nXbox 360 для Windows\". Один ресивер позволяет подключить до 4 контроллеров.\n\nВнимание: ресиверы сторонних производителей не будут работать с этим драйвером,\nубедитесь, что на вашем ресивере написано \"Microsoft\", а не \"XBOX 360\".\nMicrosoft больше не продает их отдельно, так что вам нужно будет найти\nресивер в комплекте с контроллером, либо искать на ebay.\n\nЕсли вы считаете это полезным, можете отправить денег разработчику\nдрайвера на его сайте.", + "macInstructionsText": "Для использования контроллеров Xbox 360 необходимо\nустановить драйвер Mac, доступный по ссылке ниже.\nОн работает и с проводными и беспроводными контроллерами.", + "ouyaInstructionsText": "Для использования проводных контроллеров Xbox 360 в BombSquad,\nпросто подключите их к USB-порту вашего устройства. Для нескольких\nконтроллеров можно использовать концентратор USB.\n\nДля использования беспроводных контроллеров вам понадобится беспроводной\nресивер который поставляется в наборе \"беспроводного геймпада Xbox 360\nдля Windows\" или продается отдельно. Каждый ресивер подключается\nк порту USB и позволяет подключать до 4 беспроводных контроллеров.", + "titleText": "Использование контроллеров Xbox 360 в ${APP_NAME}:" + }, + "yesAllowText": "Да, разрешить!", + "yourBestScoresText": "Ваши лучшие очки", + "yourBestTimesText": "Ваше лучшее время" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/serbian.json b/dist/ba_data/data/languages/serbian.json new file mode 100644 index 0000000..c714bef --- /dev/null +++ b/dist/ba_data/data/languages/serbian.json @@ -0,0 +1,1861 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Име налога не сме садржати смајлије и друге специјалне симболе", + "accountProfileText": "Profilno ime", + "accountsText": "Налози", + "achievementProgressText": "Достигнућа: ${COUNT} од ${TOTAL}", + "campaignProgressText": "Напредак [Тежак]: ${PROGRESS}", + "changeOncePerSeason": "Ово можеш променити само једном током сезоне.", + "changeOncePerSeasonError": "Мораш сачекати следећу сезону да би ово опет променио (${NUM} дан/а)", + "customName": "Сопствено име", + "linkAccountsEnterCodeText": "Унеси код", + "linkAccountsGenerateCodeText": "Генериши код", + "linkAccountsInfoText": "(подели напредак на више резличитих уређаја)", + "linkAccountsInstructionsNewText": "Да би спојио два налога, генериши код на првом и\nунеси тај код на други. Подаци са другог налога ће\nсе појавити на оба налога. (Подаци са првог налога\nће бити изгубљени)\n\nМожеш да повежеш највише ${COUNT} налога.\n\nВАЖНО: спајај само оне налоге које ти поседујеш;\nАко спојиш налог са налогом од пријатеља нећете\nмоћи да играте на мрежи у исто време.", + "linkAccountsInstructionsText": "Da povežeš dva profila, generiši kod sa jednog \nod njih i unesi kod na drugi.\nNapredak i inventar će biti spojeni.\nMožeš povezati najviše ${COUNT} profila.\n\nPAŽNJA:Samo poveži naloge koje poseduješ!\nAko povežeš naloge sa prijateljima onda\nnećeš biti u mogućnosti da igraš u isto vreme!\n\nVAŽNO:Ovo se trenutno ne može poništiti, zato budi pažljiv!", + "linkAccountsText": "Повежи налоге", + "linkedAccountsText": "Повезани налози:", + "nameChangeConfirm": "Промени име налога у ${NAME}?", + "resetProgressConfirmNoAchievementsText": "Ово ће твој кооперативни напредак и локалне \nрекорде вратити на почетак (али не и тикете).\nОво се не може поништити. Да ли си сигуран?", + "resetProgressConfirmText": "Ово ће твој кооперативни напредак,\nдостигнућа и локалне рекорде (али не\nи тикете) вратити на почетак. Ово се\nне може поништити. Да ли си сигуран?", + "resetProgressText": "Ресетуј напредак", + "setAccountName": "Промени име налога", + "setAccountNameDesc": "Изабери име које ће се приказивати на твом налогу.\nМожеш користити име са једног од твоји повезаних\nналога или направити ново јединствено име.", + "signInInfoText": "Пријави се да зарађујеш тикете, такмичиш на мрежи\nи делиш напредак на више различитих уређаја.", + "signInText": "Пријави се", + "signInWithDeviceInfoText": "(аутоматски налог достпупан једино са овог уређаја)", + "signInWithDeviceText": "Пријави се налогом уређаја", + "signInWithGameCircleText": "Пријави се преко Гејм Сркла.", + "signInWithGooglePlayText": "Пријави се преко Гугл Плеја", + "signInWithTestAccountInfoText": "(налог за тестирање нових ствари које ће ускоро изаћи)", + "signInWithTestAccountText": "Пријави се са тест профилом", + "signOutText": "Одјави се", + "signingInText": "Пријављивање...", + "signingOutText": "Одјављивање...", + "testAccountWarningOculusText": "Upozorenje: ulogujes se sa \"test\" adresom.\nOvo ce se promjeniti sa \"pravom\" adresom kasnije ove\ngodine ce se ponuditi kupovanje tiketi i ostale stvari.\n\n\nOd sada ces morati osvajati tiketi u igru.", + "testAccountWarningText": "Upozorenje: ulogujes se sa \"test\" adresom.\nOva adresa je ista sa ovim uredjajom i\nmoze se resetovati povremeno. (zato molim vas ne\ntrosite vremena kupovati/otkrivati stvari)", + "ticketsText": "Тикети: ${COUNT}", + "titleText": "Налог", + "unlinkAccountsInstructionsText": "Изабери налог за раздвајање", + "unlinkAccountsText": "Раздвоји налоге", + "viaAccount": "(преко налога ${NAME})", + "youAreSignedInAsText": "Пријављен си као:" + }, + "achievementChallengesText": "Изазови", + "achievementText": "Достигнуће", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Убиј 3 непријатеља са динамитом", + "descriptionComplete": "Убио си 3 непријатеља са динамитом", + "descriptionFull": "Убиј 3 непријатеља са динамитом на мапи ${LEVEL}", + "descriptionFullComplete": "Убио си 3 непријатеља са динамитом на мапи ${LEVEL}", + "name": "Динамит је експлодирао" + }, + "Boxer": { + "description": "Победи без коришћења бомби", + "descriptionComplete": "Победио си без коришћења бомби", + "descriptionFull": "Победи на мапи ${LEVEL} без коришћења бомби", + "descriptionFullComplete": "Победио си на мапи ${LEVEL} без коришћења бомби", + "name": "Боксер" + }, + "Dual Wielding": { + "descriptionFull": "Повежи 2 контролера (уређај или апликацију)", + "descriptionFullComplete": "Повезао си 2 контролера (уређаја или апликације)", + "name": "Заједничко руковање" + }, + "Flawless Victory": { + "description": "Победи нетакнут", + "descriptionComplete": "Победио си нетакнут", + "descriptionFull": "Победи на мапи ${LEVEL} нетакнут", + "descriptionFullComplete": "Победио си на мапи ${LEVEL} нетакнут", + "name": "Савршена победа" + }, + "Free Loader": { + "descriptionFull": "Започни \"Свако за себе\" игру са 2+ играча", + "descriptionFullComplete": "Започео си \"Свако за себе\" игру са 2 или више играча", + "name": "Мали администратор" + }, + "Gold Miner": { + "description": "Убиј 6 непријатеља минама", + "descriptionComplete": "Убио си 6 непријатеља минама", + "descriptionFull": "Убиј 6 непријатеља минама на мапи ${LEVEL}", + "descriptionFullComplete": "Убио си 6 непријатеља минама на мапи ${LEVEL}", + "name": "Миноловци" + }, + "Got the Moves": { + "description": "Победи без коришћења удараца или бомби", + "descriptionComplete": "Победио си без коришћења удараца или бомби", + "descriptionFull": "Победи на мапи ${LEVEL} без коришћења удараца иии бомби", + "descriptionFullComplete": "Победио си на мапи ${LEVEL} без коришћења бомби или удараца", + "name": "Спретно кретање" + }, + "In Control": { + "descriptionFull": "Повежи контролер (уређај или апликацију)", + "descriptionFullComplete": "Повезао си контролер (уређај или апликацију)", + "name": "Контролисан" + }, + "Last Stand God": { + "description": "Освоји 1000 поена", + "descriptionComplete": "Освојио си 1000 поена", + "descriptionFull": "Освоји 1000 поена на мапи ${LEVEL}", + "descriptionFullComplete": "Освојио си 1000 поена на мапи ${LEVEL}", + "name": "${LEVEL} Бог" + }, + "Last Stand Master": { + "description": "Освоји 250 поена", + "descriptionComplete": "Освојио си 250 поена", + "descriptionFull": "Освоји 250 поена на мапи ${LEVEL}", + "descriptionFullComplete": "Освојио си 250 поена на мапи ${LEVEL}", + "name": "${LEVEL} мајстор" + }, + "Last Stand Wizard": { + "description": "Освоји 500 поена", + "descriptionComplete": "Освојио си 500 поена", + "descriptionFull": "Освоји 500 поена на мапи ${LEVEL}", + "descriptionFullComplete": "Освојио си 500 поена на мапи ${LEVEL}", + "name": "${LEVEL} научник" + }, + "Mine Games": { + "description": "Убиј 3 непријатеља минама", + "descriptionComplete": "Убио си 3 непријатеља минама", + "descriptionFull": "Убиј 3 непријатеља минама на мапи ${LEVEL}", + "descriptionFullComplete": "Убио си 3 непријатеља минама на мапи ${LEVEL}", + "name": "Игра мина" + }, + "Off You Go Then": { + "description": "Баци 3 непријатеља са мапе", + "descriptionComplete": "Бацио си 3 непријатеља са мапе", + "descriptionFull": "Баци 3 непријатеља са мапе ${LEVEL}", + "descriptionFullComplete": "Бацио си 3 непријатеља са мапе ${LEVEL}", + "name": "Оде са платформе" + }, + "Onslaught God": { + "description": "Освоји 5000 поена", + "descriptionComplete": "Освојио си 5000 поена", + "descriptionFull": "Освоји 5000 поена на мапи ${LEVEL}", + "descriptionFullComplete": "Освојио си 5000 поена на мапи ${LEVEL}", + "name": "${LEVEL} Бог" + }, + "Onslaught Master": { + "description": "Освоји 500 поена", + "descriptionComplete": "Освојио си 500 поена", + "descriptionFull": "Освоји 500 поена на мапи ${LEVEL}", + "descriptionFullComplete": "Освојио си 500 поена на мапи ${LEVEL}", + "name": "${LEVEL} мајстор" + }, + "Onslaught Training Victory": { + "description": "Победи све рунде", + "descriptionComplete": "Победио си све рунде", + "descriptionFull": "Победи све рунде на мапи ${LEVEL}", + "descriptionFullComplete": "Победио си све рунде на мапи ${LEVEL}", + "name": "${LEVEL} победник" + }, + "Onslaught Wizard": { + "description": "Освоји 1000 поена", + "descriptionComplete": "Освојио си 1000 поена", + "descriptionFull": "Освоји 1000 поена на мапи ${LEVEL}", + "descriptionFullComplete": "Освојио си 1000 поена на мапи ${LEVEL}", + "name": "${LEVEL} научник" + }, + "Precision Bombing": { + "description": "Победи без коришћења супер моћи", + "descriptionComplete": "Победио си без коришћења супер моћи", + "descriptionFull": "Победи на мапи ${LEVEL} без коришћења супер моћи", + "descriptionFullComplete": "Победио си на мапи ${LEVEL} без коришћења супер моћи", + "name": "Прецизне бомбе" + }, + "Pro Boxer": { + "description": "Победи без коришћења бомби", + "descriptionComplete": "Победио си без коришћења бомби", + "descriptionFull": "Победи на мапи ${LEVEL}без коршћења бомби", + "descriptionFullComplete": "Победио си на мапи ${LEVEL} без коришћења бомби", + "name": "Професионални боксер" + }, + "Pro Football Shutout": { + "description": "Победи без примљених голова од стране непријатеља", + "descriptionComplete": "Победио си без примљених голова од стране непријатеља", + "descriptionFull": "Победи на мапи ${LEVEL} без примљених голова од стране непријатеља", + "descriptionFullComplete": "Победио си на мапи ${LEVEL} без примљених голова од стране непријатеља", + "name": "${LEVEL} дриблер" + }, + "Pro Football Victory": { + "description": "Победи утакмицу", + "descriptionComplete": "Победио си утакмицу", + "descriptionFull": "Победи утакмицу на мапи ${LEVEL}", + "descriptionFullComplete": "Победио си утакмицу на мапи ${LEVEL}", + "name": "${LEVEL} победник" + }, + "Pro Onslaught Victory": { + "description": "Победи све рунде", + "descriptionComplete": "Победио си све рунде", + "descriptionFull": "Победи све рунде на мапи ${LEVEL}", + "descriptionFullComplete": "Победио си све рунде на мапи ${LEVEL}", + "name": "${LEVEL} победник" + }, + "Pro Runaround Victory": { + "description": "Заврши све рунде", + "descriptionComplete": "Завршио си све рунде", + "descriptionFull": "Заврши све рунде на мапи ${LEVEL}", + "descriptionFullComplete": "Завршио си све рунде на мапи ${LEVEL}", + "name": "${LEVEL} победник" + }, + "Rookie Football Shutout": { + "description": "Победи без примљеих голова од стране непријатеља", + "descriptionComplete": "Победио си без примљених голова од стране непријатеља", + "descriptionFull": "Победи на мапи ${LEVEL} без примљених голова од стране непријатеља", + "descriptionFullComplete": "Победио си на мапи ${LEVEL} без примљених голова од стране непријатеља", + "name": "${LEVEL} дриблер" + }, + "Rookie Football Victory": { + "description": "Победи утакмицу", + "descriptionComplete": "Победио си утакмицу", + "descriptionFull": "Победи утакмицу на мапи ${LEVEL}", + "descriptionFullComplete": "Победио си утакмицу на мапи ${LEVEL}", + "name": "${LEVEL} победник" + }, + "Rookie Onslaught Victory": { + "description": "Победи све рунде", + "descriptionComplete": "Победио си све рунде", + "descriptionFull": "Победи све рунде на мапи ${LEVEL}", + "descriptionFullComplete": "Победио си све рунде на мапи ${LEVEL}", + "name": "${LEVEL} победник" + }, + "Runaround God": { + "description": "Освоји 2000 поена", + "descriptionComplete": "Освојио си 2000 поена", + "descriptionFull": "Освоји 2000 поена на мапи ${LEVEL}", + "descriptionFullComplete": "Освојио си 2000 поена на мапи ${LEVEL}", + "name": "${LEVEL} Бог" + }, + "Runaround Master": { + "description": "Освоји 500 поена", + "descriptionComplete": "Освојио си 500 поена", + "descriptionFull": "Освоји 500 поена на мапи ${LEVEL}", + "descriptionFullComplete": "Освојио си 500 поена на мапи ${LEVEL}", + "name": "${LEVEL} мајстор" + }, + "Runaround Wizard": { + "description": "Освоји 1000 поена", + "descriptionComplete": "Освојио си 1000 поена", + "descriptionFull": "Освоји 1000 поена на мапи ${LEVEL}", + "descriptionFullComplete": "Освојио си 1000 поена на мапи ${LEVEL}", + "name": "${LEVEL} научник" + }, + "Sharing is Caring": { + "descriptionFull": "Успешно позови пријатеља да игра игру", + "descriptionFullComplete": "Успешно си позвао пријатеља да игра игру", + "name": "Заједно је забавније" + }, + "Stayin' Alive": { + "description": "Победи без умирања", + "descriptionComplete": "Победио си без умирања", + "descriptionFull": "Победи на мапи ${LEVEL} без умирања", + "descriptionFullComplete": "Победио си на мапи ${LEVEL} без умирања", + "name": "Преживели" + }, + "Super Mega Punch": { + "description": "Нанеси 100% штете једним ударцем", + "descriptionComplete": "Нанео си 100% штете једним ударцем", + "descriptionFull": "Нанеси 100% штете једним ударцем на мапи ${LEVEL}", + "descriptionFullComplete": "Нанео си 100% штете једним ударцем на мапи ${LEVEL}", + "name": "Мега супер ударац" + }, + "Super Punch": { + "description": "Нанеси 50% штете једним ударцем", + "descriptionComplete": "Нанео си 50% штете једним ударцем", + "descriptionFull": "Нанеси 50% штете једним ударцем на мапи ${LEVEL}", + "descriptionFullComplete": "Нанео си 50% штете једним удрацем на мапи ${LEVEL}", + "name": "Супер ударац" + }, + "TNT Terror": { + "description": "Убиј 6 непријатеља са динамитом", + "descriptionComplete": "Убио си 6 непријатеља са динамитом", + "descriptionFull": "Убиј 6 непријатеља са динамитом на мапи ${LEVEL}", + "descriptionFullComplete": "Убио си 6 непријатеља са динамитом на мапи ${LEVEL}", + "name": "Терориста" + }, + "Team Player": { + "descriptionFull": "Започни тимску игру са 4 или више играча", + "descriptionFullComplete": "Започео си тимску игру са 4 или више играча", + "name": "Тимски играч" + }, + "The Great Wall": { + "description": "Заустави све непријатеље", + "descriptionComplete": "Зауставио си све непријатеље", + "descriptionFull": "Заустави све непријатеље на мапи ${LEVEL}", + "descriptionFullComplete": "Зауставио си све непријатеље на мапи ${LEVEL}", + "name": "Велики зид" + }, + "The Wall": { + "description": "Заустави све непријатеље", + "descriptionComplete": "Зауставио си све непријатеље", + "descriptionFull": "Заустави све непријатеље на мапи ${LEVEL}", + "descriptionFullComplete": "Зауставио си све напријатеље на мапи ${LEVEL}", + "name": "Зид" + }, + "Uber Football Shutout": { + "description": "Победи без примљених голова од стране непријатеља", + "descriptionComplete": "Победио си без примљених голова од стране непријатеља", + "descriptionFull": "Победи на мапи ${LEVEL} без примљених голова од стране непријатеља", + "descriptionFullComplete": "Победио си на мапи ${LEVEL} без примљених голова од стране непријатеља", + "name": "${LEVEL} дриблер" + }, + "Uber Football Victory": { + "description": "Победи утакмицу", + "descriptionComplete": "Победио си утакмицу", + "descriptionFull": "Победи утакмицу на мапи ${LEVEL}", + "descriptionFullComplete": "Победио си утакмицу на мапи ${LEVEL}", + "name": "${LEVEL} победник" + }, + "Uber Onslaught Victory": { + "description": "Победи све рунде", + "descriptionComplete": "Победио си све рунде", + "descriptionFull": "Победи све рунде на мапи ${LEVEL}", + "descriptionFullComplete": "Победио си све рунде на мапи ${LEVEL}", + "name": "${LEVEL} победник" + }, + "Uber Runaround Victory": { + "description": "Заврши све рунде", + "descriptionComplete": "Завршио си све рунде", + "descriptionFull": "Заврши све рунде на мапи ${LEVEL}", + "descriptionFullComplete": "Завршио си све рунде на мапи ${LEVEL}", + "name": "${LEVEL} победник" + } + }, + "achievementsRemainingText": "Неизвршена достигнућа:", + "achievementsText": "Достигнућа", + "achievementsUnavailableForOldSeasonsText": "Извини, информације нису доступне за старије сезоне.", + "addGameWindow": { + "getMoreGamesText": "Додај више игри...", + "titleText": "Додај игру" + }, + "allowText": "Дозволи", + "alreadySignedInText": "Овај налог је тренутно пријављен на другом уређају;\nмолимо вас да замените налог или искључите игру на\nдругом уређају и покушате поново.", + "apiVersionErrorText": "Не можемо учитати мод ${NAME}; он тражи верзију ${VERSION_USED}; ми користимо ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Аутоматски\" активира само када су прикључене слушалице)", + "headRelativeVRAudioText": "Виртуелни аудио у простору", + "musicVolumeText": "Јачина музике", + "soundVolumeText": "Јачина звука", + "soundtrackButtonText": "Музичке траке", + "soundtrackDescriptionText": "(додај своју сопствену музику која ће се пуштати)", + "titleText": "Аудио" + }, + "autoText": "Аутоматски", + "backText": "Назад", + "banThisPlayerText": "Забрани овог играча", + "bestOfFinalText": "Први до ${COUNT} победник", + "bestOfSeriesText": "Први до ${COUNT} серије:", + "bestOfUseFirstToInstead": 1, + "bestRankText": "Твоје најбоље место је #${RANK}", + "bestRatingText": "Твоја најбоља оцена је ${RATING}", + "bombBoldText": "БОМБА", + "bombText": "Бомба", + "boostText": "Појачај", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} је подешен у апликацији.", + "buttonText": "дугме", + "canWeDebugText": "Да ли желиш да игра аутоматски шање извештај о кваровима,\nгрешкама и основним информацијама о коришћењу девелоперу?\n\nОви подаци не садрже личне податке и помоћи\nће да игра ради глатко и без кварова.", + "cancelText": "Откажи", + "cantConfigureDeviceText": "Извини, ${DEVICE} није подесив.", + "challengeEndedText": "Овај изазов је завршен.", + "chatMuteText": "Искључи ћаскање", + "chatMutedText": "Ћаскање искључено", + "chatUnMuteText": "Укључи ћаскање", + "choosingPlayerText": "<бира играча>", + "completeThisLevelToProceedText": "Мораш прво завршити\nовај ниво да наставиш!", + "completionBonusText": "Бонус за завршетак", + "configControllersWindow": { + "configureControllersText": "Подеси контролере", + "configureKeyboard2Text": "Подеси тастатуру П2", + "configureKeyboardText": "Подеси тастатуру", + "configureMobileText": "Мобилни уређаји као контролери", + "configureTouchText": "Подеси екран", + "ps3Text": "ПС3 контролери", + "titleText": "Контролери", + "wiimotesText": "Вимоутс", + "xbox360Text": "Иксбокс 360 контролери" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Обавштење: прилагођавање контролера варира у зависности од уређаја и Андроид верзије.", + "pressAnyButtonText": "Притисни било које дугме на контролеру\n које желиш да подесиш...", + "titleText": "Подеси контролере" + }, + "configGamepadWindow": { + "advancedText": "Напредно", + "advancedTitleText": "Напредно подешавање контролера", + "analogStickDeadZoneDescriptionText": "(упали ово уколико твој лик \"проклизава\" и након отпуштања дугмета)", + "analogStickDeadZoneText": "Мртва зона аналогног дугмета", + "appliesToAllText": "(примењује се на све контролере ове врсте)", + "autoRecalibrateDescriptionText": "(укључи ово ако се твој лик не креће пуном брзином)", + "autoRecalibrateText": "Аутоматски рекалибрирај аналогно дугме", + "axisText": "оса", + "clearText": "очисти", + "dpadText": "окидач", + "extraStartButtonText": "Додатно стартно дугме", + "ifNothingHappensTryAnalogText": "Ако се ништа не деси, пробај да \"упослиш\" анлогно дугме уместо тога.", + "ifNothingHappensTryDpadText": "Ако се ништа не дешава, пробај да \"упослиш\" окидач уместо тога.", + "ignoreCompletelyDescriptionText": "(спречи да овај контролер утиче на игру или мени)", + "ignoreCompletelyText": "Игнориши у потпуности", + "ignoredButton1Text": "Игнорисано дугме 1", + "ignoredButton2Text": "Игнорисано дугме 2", + "ignoredButton3Text": "Игнорисано дугме 3", + "ignoredButton4Text": "Игнорисано дугме 4", + "ignoredButtonDescriptionText": "(користи ово да спречиш дугмад \"кућа\" и \"синхронизација\" да утичу на екран)", + "pressAnyAnalogTriggerText": "Притисни неки аналогни окидач...", + "pressAnyButtonOrDpadText": "Притисни неко дугме или окидач...", + "pressAnyButtonText": "Притисни неко дугме...", + "pressLeftRightText": "Притисни лево или десно...", + "pressUpDownText": "Притисни горе или доле...", + "runButton1Text": "Дугме трчања 1", + "runButton2Text": "Дугме трчања 2", + "runTrigger1Text": "Окидач трчања 1", + "runTrigger2Text": "Окидач трчања 2", + "runTriggerDescriptionText": "(аналогни окидачи дозвољавају ти да трчиш у променљивим брзинама)", + "secondHalfText": "Користи ово да подесиш другу половину\n\"2 у 1\" контролера уређаја који се\nпоказује као посебан контролер.", + "secondaryEnableText": "Омогућено", + "secondaryText": "Секундарни контролер", + "startButtonActivatesDefaultDescriptionText": "(упали ово ако је твоје стартно дугме више од \"мени\" дугмета)", + "startButtonActivatesDefaultText": "Стартно дугме активира уобичајене пречице", + "titleText": "Подешавање контролера", + "twoInOneSetupText": "Подешавање 2 у 1 контролера", + "uiOnlyDescriptionText": "(спречи да уз помоћ овог контролера улазиш у игру)", + "uiOnlyText": "Ограничи коришћење менија", + "unassignedButtonsRunText": "Трчање свим дугмадима без додељене команде", + "unsetText": "<неподешено>", + "vrReorientButtonText": "Дугме виртуелног преусмеравања" + }, + "configKeyboardWindow": { + "configuringText": "Конфигурисање ${DEVICE}", + "keyboard2NoteText": "Обавештење: Већина тастатура може да региструје само неколико\nпритиска одједном, зато ако имаш другу тастатуру играч би можда\nбоље радио ако би посебна тастатура била укључена да би је он\nкористио. Обавештење које ти такође треба јесте да и у том\nслучају упослиш различитe тастере за два играча." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Величина контроле акције", + "actionsText": "Акција", + "buttonsText": "дугмад", + "dragControlsText": "< вуци контроле да их помераш >", + "joystickText": "џојстик", + "movementControlScaleText": "Величина контроле кретања", + "movementText": "Кретање", + "resetText": "Ресетуј", + "swipeControlsHiddenText": "Сакриј контроле на повлачење", + "swipeInfoText": "Стил контрола на повлачење захтева искуство за коришћење\nали омогућава лакшу игру без гледања у контроле.", + "swipeText": "превуци", + "titleText": "Подеси екран" + }, + "configureItNowText": "Подеси их сад?", + "configureText": "Подеси", + "connectMobileDevicesWindow": { + "amazonText": "Амазон продавница", + "appStoreText": "Епл продавница", + "bestResultsScale": 0.65, + "bestResultsText": "За најбоље резултате треба ти интернет без кочења. Можеш\nсмањити кочења тако што ћеш искључити остале бежичне уређаје,\nиграти близу твог интернет рутера или конектовати се на игру\nкоја је направљена директно са интернета који ти користиш.", + "explanationText": "Да би користио паметни телефон или таблет као бежични контролер,\nинталирај \"${REMOTE_APP_NAME}\" апликацију на њега. Било који број уређаја\nможеш повезати на \"${APP_NAME}\" игру преко интернета, и то бесплатно!", + "forAndroidText": "за Андроид:", + "forIOSText": "за Еплов ОС:", + "getItForText": "Преузми \"${REMOTE_APP_NAME}\" за Еплов ОС преко Епл\nпродавнице или за Андроид у Гугловој или Амазон продавници", + "googlePlayText": "Гугл", + "titleText": "Коришћење мобилних уређаја као контролера:" + }, + "continuePurchaseText": "Настави за ${PRICE}?", + "continueText": "Настави", + "controlsText": "Контроле", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Ово се неће применити на укупну табелу.", + "activenessInfoText": "Овај множилац расте сваког дана када ти\nиграш и опада сваког дана када не играш.", + "activityText": "Активност", + "campaignText": "Напредак", + "challengesInfoText": "Освајај награде приликом завршавања мини-игри.\n\nНаграде и тежина нивоа се повећавају\nсваки пута када је изазов завршен и\nсмањују се кад изазов истекне или одустанеш.", + "challengesText": "Изазови", + "currentBestText": "Тренутно најбољи", + "customText": "Намештен", + "entryFeeText": "Уђи", + "forfeitConfirmText": "Одустани од изазова?", + "forfeitNotAllowedYetText": "Још увек не можеш да одустанеш од овог изазова.", + "forfeitText": "Одустани", + "multipliersText": "Множилац", + "nextChallengeText": "Следећи изазов", + "nextPlayText": "Следећи улазак", + "ofTotalTimeText": "од ${TOTAL}", + "playNowText": "Играј одмах", + "pointsText": "Поени", + "powerRankingFinishedSeasonUnrankedText": "(сезона завршена без ранка)", + "powerRankingNotInTopText": "(ниси у топ ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} поен/а", + "powerRankingPointsMultText": "(x ${NUMBER} поен/а)", + "powerRankingPointsText": "${NUMBER} поен/а", + "powerRankingPointsToRankedText": "(${CURRENT} од ${REMAINING} поена)", + "powerRankingText": "Бодовање", + "prizesText": "Награде", + "proMultInfoText": "Играчи са \"${PRO}\" надоградњом\nдобијају ${PERCENT}% поена више.", + "seeMoreText": "Више...", + "skipWaitText": "Прескочи чекање", + "timeRemainingText": "Преостало време", + "toRankedText": "да добијеш ранк", + "totalText": "укупно", + "tournamentInfoText": "Такмичи се за најбољи резултат са\nосталим играчима у твојој лиги.\n\nНаграде ће бити додељене играчима са најбољим\nрезултатом када време за турнир истекне.", + "welcome1Text": "Добродошао у лигу \"${LEAGUE}\". Можеш повећати свој\nранк у лиги освајајући звездице, зевршавајући\nзадатке и освајајући трофеје на турнирима.", + "welcome2Text": "Тикете можеш освојити у много раличитих активности.\nТикете можеш да користиш да би откључао нове ликове, \nмапе и мини-игре, да уђеш на турнир и остало.", + "yourPowerRankingText": "Твој ранк:" + }, + "copyOfText": "Копирај \"${NAME}\"", + "createEditPlayerText": "<Направи/измени играча>", + "createText": "Направи", + "creditsWindow": { + "additionalAudioArtIdeasText": "Додатни аудио, илустрације и идеје су од ${NAME}", + "additionalMusicFromText": "Додатна музика је од ${NAME}", + "allMyFamilyText": "Сви моји пријатељи и фамилија који су помогли при тестирању", + "codingGraphicsAudioText": "Кодирање, графике и аудио су од ${NAME}", + "languageTranslationsText": "Преводили су: (за српски - Игор М. (Чачак))", + "legalText": "Права:", + "publicDomainMusicViaText": "Музика јавног домена од ${NAME}", + "softwareBasedOnText": "Овај софтвер је делимићно базиран на раду од ${NAME}", + "songCreditText": "${TITLE} је изведен од стране ${PERFORMER}\nСастављено од стране ${COMPOSER}, уређено од стране ${ARRANGER}\nобјављено од стране ${PUBLISHER}, учтиво од ${SOURCE}", + "soundAndMusicText": "Звук и музика:", + "soundsText": "Звукови (${SOURCE}):", + "specialThanksText": "Специјална захвалност:", + "thanksEspeciallyToText": "Поготову се захваљујемо ${NAME}", + "titleText": "\"${APP_NAME}\" заслуге", + "whoeverInventedCoffeeText": "Ко год да је измислио кафу" + }, + "currentStandingText": "Твоје тренутно место је #${RANK}", + "customizeText": "Прилагоди...", + "deathsTallyText": "${COUNT} смрт/и", + "deathsText": "Смрти", + "debugText": "уклони грешке", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Обавештење: препоручује се да наместиш Подешавања->Графике->Текстуре на \"Висока\" док тестираш ово.", + "runCPUBenchmarkText": "Уради тест процесора", + "runGPUBenchmarkText": "Уради тест оптимизације", + "runMediaReloadBenchmarkText": "Уради тест учитавања медија", + "runStressTestText": "Уради тест оптерећења", + "stressTestPlayerCountText": "Број играча", + "stressTestPlaylistDescriptionText": "Листа игара на тесту", + "stressTestPlaylistNameText": "Име листе игара", + "stressTestPlaylistTypeText": "Тип листе игара", + "stressTestRoundDurationText": "Трајање рунде", + "stressTestTitleText": "Тест оптерећења", + "titleText": "Провере и тестови телефона", + "totalReloadTimeText": "Време учитавања: ${TIME} (погледај дневник за информације)" + }, + "defaultGameListNameText": "Уобичајена \"${PLAYMODE}\" листа игара", + "defaultNewGameListNameText": "Моја \"${PLAYMODE}\" листа игара", + "deleteText": "Обриши", + "demoText": "Проба", + "denyText": "Одбиј", + "desktopResText": "Рез. екрана", + "difficultyEasyText": "Лако", + "difficultyHardOnlyText": "Само на тешком моду", + "difficultyHardText": "Тешко", + "difficultyHardUnlockOnlyText": "Овај ниво може да се откључа само на тешком моду.\nДа ли мислиш да си спреман за то!?!?!", + "directBrowserToURLText": "Молимо вас усмерите интернет претарживач на следећи URL:", + "disableRemoteAppConnectionsText": "Онемогући повезивање додатне апликације", + "disableXInputDescriptionText": "Дозвољава више од 4 контролера али су могући проблеми.", + "disableXInputText": "Онемогући Икс-контролере", + "doneText": "Заврши", + "drawText": "Нерешено", + "duplicateText": "Дуплирај", + "editGameListWindow": { + "addGameText": "Додај\nигру", + "cantOverwriteDefaultText": "Не можеш променити уобичајену листу игара!", + "cantSaveAlreadyExistsText": "Листа игара са тим именом већ постоји!", + "cantSaveEmptyListText": "Не можеш сачувати празну листу игара!", + "editGameText": "Измени\nигру", + "listNameText": "Име листе игара", + "nameText": "Име", + "removeGameText": "Обриши\nигру", + "saveText": "Сачувај листу", + "titleText": "Мењање листе игара" + }, + "editProfileWindow": { + "accountProfileInfoText": "Овај специјални профил има име и \nиконицу базирану на твом налогу.\n\n${ICONS}\n\nНаправи прилагођене профиле да их користиш\nса различитим именима или иконицама.", + "accountProfileText": "(профил налога)", + "availableText": "Име \"${NAME}\" је доступно.", + "characterText": "лик", + "checkingAvailabilityText": "Проверавање доступности \"${NAME}\"...", + "colorText": "боја", + "getMoreCharactersText": "Набави више ликова...", + "getMoreIconsText": "Набави више иконица...", + "globalProfileInfoText": "Профили глобалних играча загарантовано имају јединствена \nимена широм света. Они такође укључују иконице.", + "globalProfileText": "(глобални профил)", + "highlightText": "сјај", + "iconText": "иконица", + "localProfileInfoText": "Профили локалних играча немају иконице и није загарантовано\nда ће њихова имена бити јединствена. Угради га на глобални \nпрофил да резервишеш јединствено име и додаш иконице.", + "localProfileText": "(локални профил)", + "nameDescriptionText": "Име играча", + "nameText": "Име", + "randomText": "насумично", + "titleEditText": "Измени профил", + "titleNewText": "Нови профил", + "unavailableText": "\"${NAME}\" није доступно; пробај друго име.", + "upgradeProfileInfoText": "Ово ће резервисати твоје име широм света и \nдозволиће ти да додаш иконицу на њега.", + "upgradeToGlobalProfileText": "Угради глобални профил" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Не можеш обрисати подразумевану листу звукова.", + "cantEditDefaultText": "Не можеш измењивати подразумевану листу звукова. Удвсотручи је или направи нову.", + "cantOverwriteDefaultText": "Не можеш мењати подразумевану листу звукова", + "cantSaveAlreadyExistsText": "Листа звукова са тим именом већ постоји!", + "defaultGameMusicText": "<подразумевана музика у игри>", + "defaultSoundtrackNameText": "Подразумевана листа звукова", + "deleteConfirmText": "Обриши листу звукова?\n\n\"${NAME}\"?", + "deleteText": "Обриши\nлисту звукова", + "duplicateText": "Удвостручи\nлисту звукова", + "editSoundtrackText": "Уређивање листе звукова", + "editText": "Измени\nлисту звукова", + "fetchingITunesText": "преузимање музичке листе из апликације", + "musicVolumeZeroWarning": "Пажња: јачина музике је подешена на 0", + "nameText": "Име", + "newSoundtrackNameText": "Моја листа звукова ${COUNT}", + "newSoundtrackText": "Нова листа звукова:", + "newText": "Нова\nлиста звукова", + "selectAPlaylistText": "Изабери листу музике", + "selectASourceText": "Извор музике", + "testText": "тест", + "titleText": "Листа звукова", + "useDefaultGameMusicText": "Подразумевана музика у игри", + "useITunesPlaylistText": "Листа музике из апликације", + "useMusicFileText": "Музички фајл", + "useMusicFolderText": "Фолдер са музичким фајловима" + }, + "editText": "Измени", + "endText": "Крај", + "enjoyText": "Уживај!", + "epicDescriptionFilterText": "${DESCRIPTION} у епско успореној игри.", + "epicNameFilterText": "Епски ${NAME}", + "errorAccessDeniedText": "приступ одбијен", + "errorOutOfDiskSpaceText": "нема места на диску", + "errorText": "Грешка", + "errorUnknownText": "непозната грешка", + "exitGameText": "Изађи из \"${APP_NAME}\"?", + "exportSuccessText": "\"${NAME}\" извезено.", + "externalStorageText": "Спољна меморија", + "failText": "Неуспешно", + "fatalErrorText": "Ух ох; нешто недостаје или је покварено.\nМолимо вас пробајте да опет инсталирате апликацију\nили контактирате ${EMAIL} за помоћ.", + "fileSelectorWindow": { + "titleFileFolderText": "Изабери фајл или фолдер", + "titleFileText": "Изабери фајл", + "titleFolderText": "Изабери фолдер", + "useThisFolderButtonText": "Користи овај фолдер" + }, + "filterText": "Филтер", + "finalScoreText": "Коначни резултат", + "finalScoresText": "Коначни резултати", + "finalTimeText": "Коначно време", + "finishingInstallText": "Завршавање инсталација; сачекајте моменат...", + "fireTVRemoteWarningText": "* За боље искуство, користите\nконтролере или инсталирајте \n\"${REMOTE_APP_NAME}\" апликацију\nна своје телефоне или таблете.", + "firstToFinalText": "\"Први до ${COUNT}\" резултат", + "firstToSeriesText": "\"Први до ${COUNT}\" серије", + "fiveKillText": "ПЕТОСТРУКО УБИСТВO!!!", + "flawlessWaveText": "Беспрекорна рунда!", + "fourKillText": "ЧЕТВОРОСТРУКО УБИСТВО!!!", + "friendScoresUnavailableText": "Резултати пријатеља недоступни.", + "gameCenterText": "Гејм Сентр", + "gameCircleText": "Гејм сркл", + "gameLeadersText": "Лидери ${COUNT} игре", + "gameListWindow": { + "cantDeleteDefaultText": "Не можеш да обришеш уобичајену листу игара.", + "cantEditDefaultText": "Не можеш да измениш уобичајену листу игара! Удвостручи је или направи нову.", + "cantShareDefaultText": "Не можеш да делиш уобичајену листу игара.", + "deleteConfirmText": "Обрисати \"${LIST}\"?", + "deleteText": "Обриши\nлисту игара", + "duplicateText": "Дуплирај\nлисту игара", + "editText": "Измени\nлисту игара", + "newText": "Нова\nлиста игара", + "showTutorialText": "Прикажи туториал", + "shuffleGameOrderText": "Насумичне игре", + "titleText": "Прилагоди \"${TYPE}\" листе игара" + }, + "gameSettingsWindow": { + "addGameText": "Додај игру" + }, + "gamesToText": "${WINCOUNT} према ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Запамти: било који уређај у заједници може да има\nвише од једног играча ако има довољно контролера.", + "aboutDescriptionText": "Користи ово да организујеш заједницу.\n\nЗаједнице ти пружају могућност да играш игре и\nтурнире са твојим пријатељима преко различитих уређаја.\n\nКористи ${PARTY} дугме у горњем десном углу да\nразговараш са играчима у заједници.\n(на контролеру, притисни ${BUTTON} док си у менију)", + "aboutText": "О заједници", + "addressFetchErrorText": "<грешка при преузимању адреса>", + "appInviteMessageText": "${NAME} ти је послао ${COUNT} тикета у \"${APP_NAME}\" игри", + "appInviteSendACodeText": "Пошаљи им код", + "appInviteTitleText": "${APP_NAME} позив за апликацију", + "bluetoothAndroidSupportText": "(ради са било којим Андроид уређајом који подржава блутуф)", + "bluetoothDescriptionText": "Буди домаћин/придружи се заједници преко блутуфа:", + "bluetoothHostText": "Буди домаћин преко блутуфа", + "bluetoothJoinText": "Придружи се преко блутуфа", + "bluetoothText": "Блутуф", + "checkingText": "проверавање...", + "copyCodeConfirmText": "Код копиран у привременој меморији.", + "copyCodeText": "Копирај код", + "dedicatedServerInfoText": "За најбоље резултате постави наменски сервер. Погледај bombsquadgame.com/server да сазнаш како.", + "disconnectClientsText": "Ово ће уклонити ${COUNT} играч/а из твоје\nзаједнице. Да ли си сигуран?", + "earnTicketsForRecommendingAmountText": "Пријатељи ће добити ${COUNT} тикета ако пробају игру\n(и ти ћеш освојити ${YOU_COUNT} за сваког који проба)", + "earnTicketsForRecommendingText": "Подели игру за \nбесплатне тикете...", + "emailItText": "Пошаљи Email", + "favoritesSaveText": "Додај као омиљено", + "favoritesText": "Омиљено", + "freeCloudServerAvailableMinutesText": "Следећи бесплатан сервер ће бити доступан за ${MINUTES} минута.", + "freeCloudServerAvailableNowText": "Бесплатан севрер је спреман!", + "freeCloudServerNotAvailableText": "Нема доступних бесплатних сервера.", + "friendHasSentPromoCodeText": "Добио си ${COUNT} \"${APP_NAME}\" тикета од играча \"${NAME}\"", + "friendPromoCodeAwardText": "Добићеш ${COUNT} тикета сваки пут када буде искоришћен.", + "friendPromoCodeExpireText": "Код ће истећи за ${EXPIRE_HOURS} сати и важи само за нове играче.", + "friendPromoCodeInstructionsText": "Да би га искористио, отвори \"${APP_NAME}\" и иди у \"Подешавања->Напредно->\nУнеси код\". Погледај bombsquadgame.com да преузмеш линк за све подржане платформе.", + "friendPromoCodeRedeemLongText": "Може се искористити за ${COUNT} бесплатних тикета од стране ${MAX_USES} људи.", + "friendPromoCodeRedeemShortText": "Може се искористити за ${COUNT} тикета у игри.", + "friendPromoCodeWhereToEnterText": "(у Подешавања->Напредно->Унеси код\")", + "getFriendInviteCodeText": "Узми позивни код за пријатеља", + "googlePlayDescriptionText": "Позови Гугл Плеј играче у твоју заједницу:", + "googlePlayInviteText": "Позови", + "googlePlayReInviteText": "У твојој заједници је ${COUNT} играч/а који ће\nизгубити конекцију ако пошаљеш нови позив. Убаци\nи њих у нови позив за игру да их вратиш назад.", + "googlePlaySeeInvitesText": "Погледај позиве", + "googlePlayText": "Гугл Плеј", + "googlePlayVersionOnlyText": "(Андроид / Гугл Плеј верзија)", + "hostPublicPartyDescriptionText": "Буди домаћин заједнице", + "hostingUnavailableText": "Недоступно", + "inDevelopmentWarningText": "Обавештење:\n\nМрежна игра је нова ствар и још увек се поправља.\nЗа сада је препоручљиво да сви играчи\nбуду на истој интернет мрежи.", + "internetText": "Интернет", + "inviteAFriendText": "Твоји пријатељи још увек немају игру? Позови их\nда пробају игру и добиће ${COUNT} бесплатних тикета.", + "inviteFriendsText": "Позови пријатеље", + "joinPublicPartyDescriptionText": "Придружи се заједници", + "localNetworkDescriptionText": "Придружи се заједници у близини (ЛРМ, Блутуф, итд...)", + "localNetworkText": "Локална мрежа", + "makePartyPrivateText": "Начини моју заједницу приватном", + "makePartyPublicText": "Начини моју заједницу јавном", + "manualAddressText": "Адреса", + "manualConnectText": "Повежи се", + "manualDescriptionText": "Придружи се заједници преко адресе:", + "manualJoinSectionText": "Придружи се преко адресе", + "manualJoinableFromInternetText": "Да ли доступан преко интернета?:", + "manualJoinableNoWithAsteriskText": "НЕ*", + "manualJoinableYesText": "ДА", + "manualRouterForwardingText": "*да поправиш ово, покушај да наместиш свој рутер да проследи порт ${PORT} на твоју локалну адресу", + "manualText": "Ручно", + "manualYourAddressFromInternetText": "Твоја интернет адреса:", + "manualYourLocalAddressText": "Твоја локална адреса:", + "nearbyText": "У близини", + "noConnectionText": "<без конекције>", + "otherVersionsText": "(остале верзије)", + "partyCodeText": "Код заједнице", + "partyInviteAcceptText": "Прихвати", + "partyInviteDeclineText": "Одбиј", + "partyInviteGooglePlayExtraText": "(погледај \"Гугл Плеј\" одељак у прозору \"Окупљање\")", + "partyInviteIgnoreText": "Игнориши", + "partyInviteText": "${NAME} те је позвао да се\nпридружиш њиховој заједници!", + "partyNameText": "Име заједнице", + "partyServerRunningText": "Твој сервер тренутно ради.", + "partySizeText": "величина заједнице", + "partyStatusCheckingText": "проверавање статуса...", + "partyStatusJoinableText": "могуће је придружити се твојој заједници преко интернета", + "partyStatusNoConnectionText": "није могуће повезивање на сервер", + "partyStatusNotJoinableText": "није могуће придружити се твојој заједници преко интернета", + "partyStatusNotPublicText": "твоја зеједница није јавна", + "pingText": "пинг", + "portText": "Прикључак", + "privatePartyCloudDescriptionText": "Приватне заједнице које раде на серверима; није потребно подешавање рутера.", + "privatePartyHostText": "Направи приватну заједницу", + "privatePartyJoinText": "Придружи се заједници", + "privateText": "Приватно", + "publicHostRouterConfigText": "Ово ће можда требати додатно подешавање твог рутера. Лакша опција је да направиш приватну заједницу.", + "publicText": "Јавно", + "requestingAPromoCodeText": "Тражење кода...", + "sendDirectInvitesText": "Пошаљи директне позиве", + "shareThisCodeWithFriendsText": "Подели овај код са пријатељима:", + "showMyAddressText": "Прикажи моју адресу", + "startHostingPaidText": "Направи сада за ${COST}", + "startHostingText": "Направи", + "startStopHostingMinutesText": "Можеш бесплатно започети или зауставити сервер наредних ${MINUTES} минута.", + "stopHostingText": "Заустави сервер", + "titleText": "Окупљање", + "wifiDirectDescriptionBottomText": "Ако сви уређаји имају \"Вај-фај директ\", они би могли то икористити да нађу једни\nдруге и да се повежу. Када су сви уређаји повезани, овде можеш направити\nзаједнице користећи \"Локална мрежа\" одељак, исто као и са регуларном интернет мрежом.\n\nЗа најбоље резултате, \"Вај-фај директ\" домаћин би такође требало да буде домаћин \"${APP_NAME}\" заједнице.", + "wifiDirectDescriptionTopText": "\"Вај-фај директ\" се може користити да се директно повежу Андроид уређаји\nбез интернет мреже. Ово најбоље ради на Андроид 4.2 или новијим верзијама.\n\nДа би користио ово, отвори интернет подешавања и потражи \"Вај-фај директ\" у менију.", + "wifiDirectOpenWiFiSettingsText": "Отвори интернет подешавања", + "wifiDirectText": "Вај-фај директ", + "worksBetweenAllPlatformsText": "(ради између свих платформи)", + "worksWithGooglePlayDevicesText": "(ради са уређајима који имају Гугл Плеј (андроид) верзију игре)", + "youHaveBeenSentAPromoCodeText": "Послао си \"${APP_NAME}\" промо код:" + }, + "getTicketsWindow": { + "freeText": "БЕСПЛАТНО!", + "freeTicketsText": "Бесплатни тикети", + "inProgressText": "Трансакција је у току; молимо вас покушајте касније.", + "purchasesRestoredText": "Куповине обновљене.", + "receivedTicketsText": "Добио си ${COUNT} тикета!", + "restorePurchasesText": "Обнови куповине", + "ticketPack1Text": "Мали пакет тикета", + "ticketPack2Text": "Средњи пакет тикета", + "ticketPack3Text": "Велики пакет тикета", + "ticketPack4Text": "Огромни пакет тикета", + "ticketPack5Text": "Џиновски пакет тикета", + "ticketPack6Text": "Ултимативни пакет тикета", + "ticketsFromASponsorText": "Узми ${COUNT} тикета\nод спонзора", + "ticketsText": "${COUNT} тикета", + "titleText": "Узми тикете", + "unavailableLinkAccountText": "Извињавамо се, куповина није доступна на овој платформи.\nКао решење, можеш повезати налог на налог на другој\nплатформи и ту изведеш куповину.", + "unavailableTemporarilyText": "Ово тренутно није доступно; молимо вас покушајте касније.", + "unavailableText": "Извињавамо се, ово није доступно.", + "versionTooOldText": "Извињавамо се, ова верзија игре је превише стара; молимо вас унапредите је на новију.", + "youHaveShortText": "имаш ${COUNT}", + "youHaveText": "имаш ${COUNT} тикета" + }, + "googleMultiplayerDiscontinuedText": "Извињавам се, Гуглов сервис више није доступан.\nРадимо на замени што је пре могуће.\nДо тада, покушајте другу методу повезивања.\n-Ерик", + "googlePlayText": "Гугл Плеј", + "graphicsSettingsWindow": { + "alwaysText": "Увек", + "fullScreenCmdText": "Цео екран (Cmd-F)", + "fullScreenCtrlText": "Цео екран (Ctrl-F)", + "gammaText": "Осветљење", + "highText": "Високо", + "higherText": "Веома високо", + "lowText": "Ниско", + "mediumText": "Средње", + "neverText": "Никад", + "resolutionText": "Резолуција", + "showFPSText": "Прикажи БСС", + "texturesText": "Текстуре", + "titleText": "Графике", + "tvBorderText": "ТВ оквир", + "verticalSyncText": "Вертикална синхронизација", + "visualsText": "Детаљи" + }, + "helpWindow": { + "bombInfoText": "- Бомба -\nЈача од удараца, али може\nдовести до самоповређивања.\nЗа најбоље резултате, бацити је испред\nнепријатеља пре него што фитиљ изгори.", + "bombInfoTextScale": 0.6, + "canHelpText": "\"${APP_NAME}\" ти може помоћи.", + "controllersInfoText": "Можеш да играш \"${APP_NAME}\" са пријатељима преко мреже, или \nможете играти на истом уређају ако имате довољно контролера.\n\"${APP_NAME}\" подржава доста њих; можете чак користити и \nтелефоне као контролере уз помоћ бесплатне \"${REMOTE_APP_NAME}\"\nапликације. Погледај Подешавања->Контролери за више информација.", + "controllersText": "Контролери", + "controlsSubtitleText": "Твој пријатељ из \"${APP_NAME}\" игре има неколико основних акција:", + "controlsText": "Контроле", + "devicesInfoText": "Виртуелна верзија ${APP_NAME} може да се игра преко мреже са регуларном\nверзијом, зато извадите ваше додатне телефоне, таблете и компјутере\nи упалите вашу игру. Такође може бити корисно да повежете регуларну\nверзију игре на VR верзију само да би дозволили људима споља да\nгледају акцију.", + "devicesText": "Уређаји", + "friendsGoodText": "Ово је добро да имаш. \"${APP_NAME}\" је најзабавнији са неколико\nиграча а може да подржи и до 8 у исто време, што нас води ка:", + "friendsText": "Пријатељи", + "jumpInfoText": "- Скок -\nСкочи да пређеш мале рупе,\nда бациш ствари даље, и да\nизразиш осећања радости.", + "jumpInfoTextScale": 0.6, + "orPunchingSomethingExtraSpace": 0, + "orPunchingSomethingText": "Или да удараш некога, бациш га са литице и разносиш га са лепљивом бомбом док пада у провалију.", + "pickUpInfoText": "- Покупи -\nУзми заставе, непријатеље или било\nшта друго што није причвршћено за\nземљу. Притисни опет да бациш то.", + "pickUpInfoTextScale": 0.6, + "powerupBombDescriptionText": "Дозвољава ти да бациш три бомбе\nза редом уместо само једне.", + "powerupBombNameText": "Троструке бомбе", + "powerupCurseDescriptionText": "Вероватно желиш да избегнеш ово.\n ...или можда не?", + "powerupCurseNameText": "Клетва", + "powerupHealthDescriptionText": "Враћа цело твоје здравље.\nНикад не би погодио.", + "powerupHealthNameText": "Прва помоћ", + "powerupIceBombsDescriptionText": "Слабије него нормалне бомбе\nали оставља твоје непријатеље\nзалеђене и посебно ломљиве.", + "powerupIceBombsNameText": "Ледене бомбе", + "powerupImpactBombsDescriptionText": "Мало слабије него нормалне\nбомбе, али експлодирају на додир.", + "powerupImpactBombsNameText": "Бомбе на додир", + "powerupLandMinesDescriptionText": "Долазе у паковању од 3;\nКорисне за одбрану базе\nи заустављање брзих људи.", + "powerupLandMinesNameText": "Мине", + "powerupPunchDescriptionText": "Ојачава, убрзава и\nпобољшава твоје ударце.", + "powerupPunchNameText": "Боксерске рукавице", + "powerupShieldDescriptionText": "Прима део штете да\nти не би морао.", + "powerupShieldNameText": "Енергетски штит", + "powerupStickyBombsDescriptionText": "Залепи се за све што додирне.\nХитно реаговати!", + "powerupStickyBombsNameText": "Лепљива бомба", + "powerupsSubtitleText": "Наравно, ниједна игра није поптуна спцијалних моћи:", + "powerupsSubtitleTextScale": 0.8, + "powerupsText": "Специјалне моћи", + "punchInfoText": "- Ударац -\nУдарци праве више штете\nако се брже крећеш, тако да\nтрчи и врти се као лудак.", + "punchInfoTextScale": 0.6, + "runInfoText": "- Трчање -\nДржи БИЛО КОЈЕ дугме да трчиш. Дугмад на повлачење или права раде боље ако их имате.\nТрчање ће ти помоћи да стигнеш на неко место брже али се теже окрећеш, зато пази на ивице.", + "runInfoTextScale": 0.6, + "someDaysExtraSpace": 0, + "someDaysText": "Неким данима се осећаш као да би желео да удараш нешто. Или да разносиш нешто у ваздух.", + "titleText": "\"${APP_NAME}\" помоћ", + "toGetTheMostText": "Да би извукао највише из ове игре, требаће ти:", + "welcomeText": "Добродошао у \"${APP_NAME}\"!" + }, + "holdAnyButtonText": "<држи било које дугме>", + "holdAnyKeyText": "<држи било који тастер>", + "hostIsNavigatingMenusText": "- ${HOST} управља менијем као неки шеф -", + "importPlaylistCodeInstructionsText": "Користи следећи код да увезеш ову листу музике негде:", + "importPlaylistSuccessText": "\"${TYPE}\" листа пуштања увежена (\"${NAME})", + "importText": "Увези", + "importingText": "Увожење...", + "inGameClippedNameText": "у игри ће бити\n\"${NAME}\"", + "installDiskSpaceErrorText": "ГРЕШКА: Немогуће да се заврши инсталација.\nМожда си остао без простора на уређају.\nОслободи мало простора и покушај опет.", + "internal": { + "arrowsToExitListText": "притисни ${LEFT} или ${RIGHT} да изађеш са листе", + "buttonText": "дугме", + "cantKickHostError": "Не можеш да избациш домаћина.", + "chatBlockedText": "${NAME} не може да ћаска наредних ${TIME} секунди.", + "connectedToGameText": "${NAME} је ушао", + "connectedToPartyText": "Ушао си у ${NAME} заједницу!", + "connectingToPartyText": "Повезивање...", + "connectionFailedHostAlreadyInPartyText": "Конекција неуспела; домаћин је у другој заједници.", + "connectionFailedPartyFullText": "Конекција неуспела; заједница је пуна.", + "connectionFailedText": "Конекција неуспела.", + "connectionFailedVersionMismatchText": "Конекција неуспела; домаћин користи другу верзију игре.\nПровери да ли користиш најновију верзију и пробај опет.", + "connectionRejectedText": "Конекција одбијена.", + "controllerConnectedText": "${CONTROLLER} је повезан.", + "controllerDetectedText": "1 контролер пронађен.", + "controllerDisconnectedText": "${CONTROLLER} више није повезан.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} није повезан. Молимо вас покушајте опет.", + "controllerForMenusOnlyText": "Овај контролер не може послужити за игру, већ само за управљање менијем.", + "controllerReconnectedText": "${CONTROLLER} је поново повезан.", + "controllersConnectedText": "${COUNT} контролера је повезано.", + "controllersDetectedText": "${COUNT} контролера је повезано.", + "controllersDisconnectedText": "${COUNT} контролера више није повезано.", + "corruptFileText": "Пронађен претећи фајл/ови. Покушај да поново инсталираш игру или пошаљи поруку на ${EMAIL}", + "errorPlayingMusicText": "Грешка при пуштању музике: ${MUSIC}", + "errorResettingAchievementsText": "Немогуће рестартовање интернет достигнућа; молимо вас покушајте касније.", + "hasMenuControlText": "${NAME} има контролу над менијем.", + "incompatibleNewerVersionHostText": "Домаћин користи новију верзију игре.\nАжурирај на последњу верзију и покушај опет.", + "incompatibleVersionHostText": "Домаћин користи другу верзију игре. Провери да ли\nсте обојица усклађени са верзијама и покушај опет.", + "incompatibleVersionPlayerText": "${NAME} користи другу верзију игре. Провери да ли\nсте обојица усклађени са верзијама и покушај опет.", + "invalidAddressErrorText": "Грешка: неважећа адреса.", + "invalidNameErrorText": "Грешка: неважеће име.", + "invalidPortErrorText": "Грешка: неважећи прикључак.", + "invitationSentText": "Позивница послата.", + "invitationsSentText": "${COUNT} позивница послато.", + "joinedPartyInstructionsText": "Неко се придружио твојој заједници.\nИди на \"Играј\" да стартујеш игру.", + "keyboardText": "Тастатура", + "kickIdlePlayersKickedText": "Избачен ${NAME} због неактивности.", + "kickIdlePlayersWarning1Text": "${NAME} ће бити избачен за ${COUNT} секунди ако остане неактиван.", + "kickIdlePlayersWarning2Text": "(ово можеш да искључиш у Подешавања -> Напредно)", + "leftGameText": "${NAME} је изашао.", + "leftPartyText": "Изашао си из заједнице играча ${NAME}.", + "noMusicFilesInFolderText": "Фолдер не садржи музичке фајлове.", + "playerJoinedPartyText": "${NAME} се придружио заједници!", + "playerLeftPartyText": "${NAME} је напустио заједницу.", + "rejectingInviteAlreadyInPartyText": "Одбијање позива (већ је у заједници).", + "serverRestartingText": "Сервер се рестартује. Молимо вас уђите опет за који моменат...", + "serverShuttingDownText": "Сервер се гаси...", + "signInErrorText": "Грешка при пријављивању.", + "signInNoConnectionText": "Немогуће пријављивање. (нема интернет конекције?)", + "telnetAccessDeniedText": "ГРЕШКА: корисник нема одобрен приступ.", + "timeOutText": "(време истиче за ${TIME} секунду/и)", + "touchScreenJoinWarningText": "Придружили сте се уређајом са екраном на додир.\nАко је ово била грешка, клкни на \"Мени->Напусти игру\" са њим.", + "touchScreenText": "Екран на додир", + "unableToResolveHostText": "Грешка: немогуће препознавање домаћина.", + "unavailableNoConnectionText": "Ово тренутно није доступно (нема интернет конекције?)", + "vrOrientationResetCardboardText": "Користи ово да рестартујеш виртуелну оријентацију.\nДа би играо игру требаће ти спољни контролер.", + "vrOrientationResetText": "Виртуелна оријентација рестартована.", + "willTimeOutText": "(време ће му истећи ако је неактиван)" + }, + "jumpBoldText": "СКОК", + "jumpText": "Скок", + "keepText": "Задржи", + "keepTheseSettingsText": "Задржи ова подешавања?", + "keyboardChangeInstructionsText": "Притисни \"размак\" два пута да промениш тастатуре.", + "keyboardNoOthersAvailableText": "Нема доступних тастатура.", + "keyboardSwitchText": "Мењање тастатуре на \"${NAME}\".", + "kickOccurredText": "${NAME} је избачен.", + "kickQuestionText": "Избацити ${NAME}?", + "kickText": "Избаци", + "kickVoteCantKickAdminsText": "Админи не могу бити избачени.", + "kickVoteCantKickSelfText": "Не можеш избацити себе.", + "kickVoteFailedNotEnoughVotersText": "Нема довољно играча за гласање.", + "kickVoteFailedText": "Неуспешно гласање за ибацивање.", + "kickVoteStartedText": "Гласање за избацивање играча \"${NAME}\" је стартовано.", + "kickVoteText": "Гласај за избацивање.", + "kickVotingDisabledText": "Гласања за избацивање искључена.", + "kickWithChatText": "Напиши \"${YES}\" у ћаскању за избацивање и \"${NO}\" за против.", + "killsTallyText": "${COUNT} убиства", + "killsText": "Убиства", + "kioskWindow": { + "easyText": "Лако", + "epicModeText": "Епски мод", + "fullMenuText": "Пун мени", + "hardText": "Тешко", + "mediumText": "Средње", + "singlePlayerExamplesText": "Соло игра / кооперативни примери", + "versusExamplesText": "Примери борбе у 2 тима..." + }, + "languageSetText": "Тренутни језик је ${LANGUAGE}.", + "lapNumberText": "Круг ${CURRENT}/${TOTAL}", + "lastGamesText": "(последњих ${COUNT} игара)", + "leaderboardsText": "Табеле", + "league": { + "allTimeText": "Укупно", + "currentSeasonText": "Тренутна сезона (${NUMBER})", + "leagueFullText": "Лига \"${NAME}\"", + "leagueRankText": "Ранк у лиги", + "leagueText": "Лига", + "rankInLeagueText": "#${RANK}, Лига \"${NAME}\"${SUFFIX}", + "seasonEndedDaysAgoText": "Сезона завршена пре ${NUMBER} дана.", + "seasonEndsDaysText": "Сезона се завршава за ${NUMBER} дана.", + "seasonEndsHoursText": "Сезона се завршава за ${NUMBER} сати.", + "seasonEndsMinutesText": "Сезона се завршава за ${NUMBER} минута.", + "seasonText": "Сезона ${NUMBER}", + "tournamentLeagueText": "Мораш достићи лигу \"${NAME}\" да уђеш у овај турнир.", + "trophyCountsResetText": "Број трофеја ће се рестартовати следеће сезоне." + }, + "levelBestScoresText": "Најбољи резултати на нивоу \"${LEVEL}\"", + "levelBestTimesText": "Најбоља времена на нивоу \"${LEVEL}\"", + "levelIsLockedText": "Ниво \"${LEVEL}\" је закључан.", + "levelMustBeCompletedFirstText": "Прво мора бити завршен ниво \"${LEVEL}\".", + "levelText": "Ниво ${NUMBER}", + "levelUnlockedText": "Ниво откључан!", + "livesBonusText": "Бонус животи", + "loadingText": "учитавање", + "loadingTryAgainText": "Учитавање; покушај за који моменат...", + "macControllerSubsystemBothText": "Оба (није препоручљиво)", + "macControllerSubsystemClassicText": "Класични", + "macControllerSubsystemDescriptionText": "(покушај да промениш ово ако ти контролери не раде)", + "macControllerSubsystemMFiNoteText": "Ај-Оу-Ес (iOS)/Мек контролери су пронађени;\nМожда желиш да их омогућиш у Подешавања -> Контролери", + "macControllerSubsystemMFiText": "Ај-Оу-Ес (iOS)/Мек", + "macControllerSubsystemTitleText": "Подешавање контролера", + "mainMenu": { + "creditsText": "Заслуге", + "demoMenuText": "Демо мени", + "endGameText": "Заврши игру", + "exitGameText": "Изађи из игре", + "exitToMenuText": "Изађи на мени?", + "howToPlayText": "Како играти", + "justPlayerText": "(Само ${NAME})", + "leaveGameText": "Напусти игру", + "leavePartyConfirmText": "Стварно хоћеш да напустиш заједницу?", + "leavePartyText": "Напусти заједницу", + "quitText": "Изађи", + "resumeText": "Настави", + "settingsText": "Подешавања" + }, + "makeItSoText": "Нека буде тако", + "mapSelectGetMoreMapsText": "Набави више мапа...", + "mapSelectText": "Изабери...", + "mapSelectTitleText": "\"${GAME}\" мапе", + "mapText": "Мапа", + "maxConnectionsText": "Максимално конекција", + "maxPartySizeText": "Максимална величина заједнице", + "maxPlayersText": "Максимално играча", + "modeArcadeText": "Аркадни мод", + "modeClassicText": "Класични мод", + "modeDemoText": "Тест мод", + "mostValuablePlayerText": "Највреднији играч", + "mostViolatedPlayerText": "Најповређиванији играч", + "mostViolentPlayerText": "Најнасилнији играч", + "moveText": "Кретање", + "multiKillText": "${COUNT} УБИСТВА!!!", + "multiPlayerCountText": "${COUNT} играча", + "mustInviteFriendsText": "Обавештење: мораш да позовеш пријатеље\nу \"${GATHER}\" прозору или да прикључиш\nконтролере како би играо са више играча.", + "nameBetrayedText": "${NAME} је издао играча ${VICTIM}.", + "nameDiedText": "${NAME} је умро.", + "nameKilledText": "${NAME} је убио играча ${VICTIM}.", + "nameNotEmptyText": "Име не може бити празно!", + "nameScoresText": "${NAME} је постигао поен!", + "nameSuicideKidFriendlyText": "${NAME} је случајно умро.", + "nameSuicideText": "${NAME} је починио самоубиство.", + "nameText": "Име", + "nativeText": "Урођена", + "newPersonalBestText": "Нови лични рекорд!", + "newTestBuildAvailableText": "Новији систем за тестирање је доступан! (${VERSION} тест ${BUILD}).\nНабави је овде: ${ADDRESS}", + "newText": "Ново", + "newVersionAvailableText": "Новија верзија \"${APP_NAME}\" игре је доступна! (${VERSION})", + "nextAchievementsText": "Следећа достигнћа:", + "nextLevelText": "Следећи ниво", + "noAchievementsRemainingText": "- ниједан", + "noContinuesText": "(без наставака)", + "noExternalStorageErrorText": "Није нађена спољна меморија на овом уреају", + "noGameCircleText": "Грешка: ниси пријавњен на Гејм Сркл", + "noScoresYetText": "Још увек нема резултата.", + "noThanksText": "Не, хвала", + "noTournamentsInTestBuildText": "УПОЗОРЕЊЕ: Резултати са турнира у тест верзији игре се не рачунају.", + "noValidMapsErrorText": "Није пронађена важећа мапа за овај тип игре.", + "notEnoughPlayersRemainingText": "Није остало довољно играча; изађи и започни нову игру.", + "notEnoughPlayersText": "Требаш најмање ${COUNT} играча да започнеш игру!", + "notNowText": "Не сада", + "notSignedInErrorText": "Мораш бити пријављен да радиш ово.", + "notSignedInGooglePlayErrorText": "Мораш бити пријавњен на Гугл Плеј да радиш ово.", + "notSignedInText": "ниси пријављен", + "nothingIsSelectedErrorText": "Ништа није изабрано!", + "numberText": "#${NUMBER}", + "offText": "Искључи", + "okText": "Окej", + "onText": "Укључи", + "oneMomentText": "Један моменат...", + "onslaughtRespawnText": "${PLAYER} ће оживети у рунди ${WAVE}", + "orText": "${A} или ${B}", + "otherText": "Друго...", + "outOfText": "(#${RANK} од ${ALL})", + "ownFlagAtYourBaseWarning": "Твоја застава мора бити у\nбази да би освојио поен!", + "packageModsEnabledErrorText": "Мрежна игра није дозвољена док је локални пакет модова искључен (погледај Подешавања->Напредно)", + "partyWindow": { + "chatMessageText": "Порука за заједницу", + "emptyText": "Твоја заједница је празна", + "hostText": "(домаћин)", + "sendText": "Пошаљи", + "titleText": "Твоја заједница" + }, + "pausedByHostText": "(паузирано од стране домаћина)", + "perfectWaveText": "Перфектна рунда!", + "pickUpText": "Покупи", + "playModes": { + "coopText": "Кооперативно", + "freeForAllText": "Свако за себе", + "multiTeamText": "Мањи тимови", + "singlePlayerCoopText": "Соло игра / Кооперативно", + "teamsText": "Тимови" + }, + "playText": "Играј", + "playWindow": { + "oneToFourPlayersText": "1-4 играча", + "titleText": "Играј", + "twoToEightPlayersText": "2-8 играча" + }, + "playerCountAbbreviatedText": "${COUNT} иг.", + "playerDelayedJoinText": "${PLAYER} ће ући на старту следеће рунде.", + "playerInfoText": "Информације играча", + "playerLeftText": "${PLAYER} је напустио игру.", + "playerLimitReachedText": "Лимит од ${COUNT} играча је достигнут; забрањен улаз новим играчима.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Не можеш да обришеш профил твог налога.", + "deleteButtonText": "Обриши\nпрофил", + "deleteConfirmText": "Обрисати \"${PROFILE}\"?", + "editButtonText": "Измени\nпрофил", + "explanationText": "(сопствена имена играча и изгледи за овај налог)", + "newButtonText": "Нови\nпрофил", + "titleText": "Профили играча" + }, + "playerText": "Играч", + "playlistNoValidGamesErrorText": "Ова листа игара садржи игре које нису откључане.", + "playlistNotFoundText": "није нађена листа игара", + "playlistText": "Листа игара", + "playlistsText": "Листе игара", + "pleaseRateText": "Ако уживате у \"${APP_NAME}\" игри, молимо вас да издвојите\nмало времена да оцените апликацију или напишете коментар.\nОво нам даје корисне информације и помаже у будућем развоју.\n\nХвала!\n-Ерик", + "pleaseWaitText": "Молимо вас сачекајте...", + "pluginsDetectedText": "Нови додатак/ци пронађен/и. Укључи/подеси их у подешавањима.", + "pluginsText": "Додаци", + "practiceText": "Вежбање", + "pressAnyButtonPlayAgainText": "Притисни било које дугме да одиграш опет...", + "pressAnyButtonText": "Притисни било које дугме да наставиш...", + "pressAnyButtonToJoinText": "притисни било које дугме да се придружиш...", + "pressAnyKeyButtonPlayAgainText": "Притисни било који тастер/дугме да одиграш опет...", + "pressAnyKeyButtonText": "Притисни било који тастер/дугме да наставиш...", + "pressAnyKeyText": "Притисни било који тастер...", + "pressJumpToFlyText": "** Притискај \"Скок\" изнова и изнова да полетиш **", + "pressPunchToJoinText": "притисни \"Ударац\" да се придружиш...", + "pressToOverrideCharacterText": "притисни ${BUTTONS} да промениш свог лика", + "pressToSelectProfileText": "притисни ${BUTTONS} да изабереш играча", + "pressToSelectTeamText": "притисни ${BUTTONS} да изабереш тим", + "promoCodeWindow": { + "codeText": "Код", + "enterText": "Уђи" + }, + "promoSubmitErrorText": "Грешка при прихватању кода; провери своју интенет конекцију", + "ps3ControllersWindow": { + "macInstructionsText": "Искључи напајање са задње стране ПС3, буди сигуран да је\nблутуф омогућен на твом Мек уређају, онда повежи контролер\nна Мек преко кабла да упариш ова два. Сада можеш да\nискористиш стартно дугме контролера да га повежеш на Mac\nили преко кабла или бежично (блутуф).\n\nНа неким Мек уређајима можете бити упитани за шифру током упаривања.\nАко се ово деси погледај следећи туториал или Гугл за помоћ.\n\n\n\n\nПС3 контролери конектовани бежично би требали бити приказани на\nлисти урађаја у Поставке система->Блутуф. Можда ћеш требати да их\nуклониш са те листа када хоћеш да их поново користиш на твом PS3.\n\nТакође буди сигуран да више нису повезани на блутуф када их не\nкористиш или ће батерија наставити да се троши.\n\nБлутуф би требало да издржи 7 повезаних уређаја, мада може да\nварира.", + "macInstructionsTextScale": 0.74, + "ouyaInstructionsText": "Да користиш ПС3 контролер са твојим \"OУЈА\", једноставно их повежи са каблом\nда их упариш. Радећи ово остали контролери можда више неће бити\nконектовани, зато би требало да растартујеш \"ОУЈА\" и извадиш кабл.\n\nСада би требао да можеш да искористиш дугме \"кућа\" на контролеру дугме да\nих конектујеш бежично. Када завршиш са играњем, држи дугме \"кућа\" 10\nсекунди да искључиш контролер; иначе ако остану укључени трошиће батерију\nна уређају.", + "ouyaInstructionsTextScale": 0.74, + "pairingTutorialText": "видео-туториал за упаривање", + "titleText": "Користи ПС3 контролер са \"${APP_NAME}\" игром:" + }, + "punchBoldText": "УДАРАЦ", + "punchText": "Ударац", + "purchaseForText": "Купи за ${PRICE}", + "purchaseGameText": "Купи игру", + "purchasingText": "Куповина...", + "quitGameText": "Изађи из \"${APP_NAME}\" игре?", + "quittingIn5SecondsText": "Излажење за 5 секунди...", + "randomPlayerNamesText": "Боб, Неца, Маре, Џони, Цане, Мики, Змај, Ноле, Перо, Ацо, Каћа, Нина, Мина, Маки, Сташа, Маја, Тања, Теа, Тина, Ема, Лаки, Лара, Уна, Лена, Реља", + "randomText": "Насумично", + "rankText": "Ранк", + "ratingText": "Оцена", + "reachWave2Text": "Стигни до 2. рунде да добијеш ранк.", + "readyText": "спреман", + "recentText": "Скорије", + "remoteAppInfoShortText": "\"${APP_NAME}\" игра је најзабавнија када се игра са породицом\nили пријатељима. Повежи један или више контролера и инсталирај\n\"${REMOTE_APP_NAME}\" апликацију на телефоне или таблете да би\nих користио као контролере.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Позиција дугмади", + "button_size": "Вечичина дугмета", + "cant_resolve_host": "Не може да се пронађе домаћин.", + "capturing": "Чекање…", + "connected": "Повезано.", + "description": "Користи свој телефон или таблет као контролер за BombSquad игру.\nДо 8 уређаја може да се повеже у исто време на ТВ или таблет за праву играчку лудницу.", + "disconnected": "Ниси више повезан на сервер.", + "dpad_fixed": "фиксирано", + "dpad_floating": "покретно", + "dpad_position": "Позиција окидача", + "dpad_size": "Величина окидача", + "dpad_type": "Тип окидача", + "enter_an_address": "Унеси адресу", + "game_full": "Игра је пуна или не прихвата конекције.", + "game_shut_down": "Игра је угашена.", + "hardware_buttons": "Хардверска дугмад", + "join_by_address": "Придружи се преко адресе...", + "lag": "Кашњење: ${SECONDS} секунди", + "reset": "Врати на подразумевано", + "run1": "Трчање 1", + "run2": "Трчање 2", + "searching": "Тражење \"BombSquad\" игара...", + "searching_caption": "Притисни на име игре да се придружиш.\nБуди сигуран да си на истој Wi-Fi мрежи као и игра.", + "start": "Стартуј", + "version_mismatch": "Верзије се не подударају.\nБуди сигуран да су \"BombSquad\" игра и \"BombSquad\nRemote\" на последњој верзији и покушај опет." + }, + "removeInGameAdsText": "Откључај \"${PRO}\" у продавници да уклониш рекламе из игре.", + "renameText": "Промени име", + "replayEndText": "Прекини гледање", + "replayNameDefaultText": "Снимак последње игре", + "replayReadErrorText": "Грешка при пуштању снимка.", + "replayRenameWarningText": "Промени име \"${REPLAY}\" после игре ако хоћеш да сачуваш; у другом случају ће бити избрисан.", + "replayVersionErrorText": "Извињавамо се, овај снимак је направљен у \nдругој верзији игре и више не може да се гледа.", + "replayWatchText": "Погледај снимак", + "replayWriteErrorText": "Грешка при прављењу снимка.", + "replaysText": "Снимци игре", + "reportPlayerExplanationText": "Корсти ову пошту да пријавиш играча за варање, неприкладан говор или друго лоше\nпонашање. Молимо вас опишите испод:", + "reportThisPlayerCheatingText": "Варање", + "reportThisPlayerLanguageText": "Неприкладан говор", + "reportThisPlayerReasonText": "За шта хоћеш да га пријавиш?", + "reportThisPlayerText": "Пријави овог играча", + "requestingText": "Узимање...", + "restartText": "Рестарт", + "retryText": "Покушај опет", + "revertText": "Врати", + "runText": "Трчање", + "saveText": "Сачувај", + "scanScriptsErrorText": "Грешка/е у скенирању скрипти; видети дневник за детаље.", + "scoreChallengesText": "Резултати изазова", + "scoreListUnavailableText": "Листа резултата недоступна.", + "scoreText": "Резултат", + "scoreUnits": { + "millisecondsText": "Милисекунди", + "pointsText": "Поени", + "secondsText": "Секунди" + }, + "scoreWasText": "(био је ${COUNT})", + "selectText": "Изабери", + "seriesWinLine1PlayerText": "ЈЕ ПОБЕДИО", + "seriesWinLine1TeamText": "ЈЕ ПОБЕДИО", + "seriesWinLine1Text": "ЈЕ ПОБЕДИО", + "seriesWinLine2Text": "СЕРИЈУ ИГАРА!", + "settingsWindow": { + "accountText": "Налог", + "advancedText": "Напредно", + "audioText": "Аудио", + "controllersText": "Контролери", + "graphicsText": "Графика", + "playerProfilesMovedText": "Обавештење: Профили играча су померени у прозор \"Налог\" у главном менију.", + "titleText": "Подешавања" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(једноставна, уграђена тастатура не екрану за уређивање текста)", + "alwaysUseInternalKeyboardText": "Увек користи интерну тастатуру", + "benchmarksText": "Провере и тестови оптерећења", + "disableCameraGyroscopeMotionText": "Онемогући кретање жироскопа камере", + "disableCameraShakeText": "Исључи дрмање камере", + "disableThisNotice": "(можеш да искључиш ова обавештења у напредним подешавањима)", + "enablePackageModsDescriptionText": "(омогућива убацивања додатних модова али онемогућава игра на мрежи)", + "enablePackageModsText": "Омогући локални пакет модова", + "enterPromoCodeText": "Унеси код", + "forTestingText": "Обавештење: ове вредности су само за тестирање и биће обрисане када се изађе из апликације.", + "helpTranslateText": "\"${APP_NAME}\" језички преводи су рад играча. Ако би желео\nда учествујеш у преводу или да измениш превод прати линк\nиспод. Користите само ако сте познавалац језика. Хвала унапред!", + "kickIdlePlayersText": "Избаци неактивне играче", + "kidFriendlyModeText": "Дечији мод (умањено насиље и сл.)", + "languageText": "Језици", + "moddingGuideText": "Водич за модове", + "mustRestartText": "Мораш да рестартујеш игру да би се ово применило.", + "netTestingText": "Тестирање мреже", + "resetText": "Ресетуј", + "showBombTrajectoriesText": "Приказуј путању бомби", + "showPlayerNamesText": "Приказуј имена играча", + "showUserModsText": "Покажи фолдер са модовима", + "titleText": "Напредно", + "translationEditorButtonText": "\"${APP_NAME}\" измењивање превода", + "translationFetchErrorText": "недоступан статус превода", + "translationFetchingStatusText": "проверавање статуса превода...", + "translationInformMe": "Обавести ме када мом језику требају нове измене", + "translationNoUpdateNeededText": "тренутни језик је завршен са превођењем; јуупии!", + "translationUpdateNeededText": "** тренутни језик треба додатне измене!! **", + "vrTestingText": "Тестирање ВР" + }, + "shareText": "Подели", + "sharingText": "Дељење...", + "showText": "Прикажи", + "signInForPromoCodeText": "Мораш бити пријавњен на налог да би кодови имали ефекта.", + "signInWithGameCenterText": "Да би користио Гејм Сентр налог,\nпријави се преко Гејм Сентр апликације.", + "singleGamePlaylistNameText": "Само \"${GAME}\"", + "singlePlayerCountText": "1 играч", + "soloNameFilterText": "Соло \"${NAME}\"", + "soundtrackTypeNames": { + "CharSelect": "Бирање лика", + "Chosen One": "Одабран", + "Epic": "Епски мод игре", + "Epic Race": "Епска трка", + "FlagCatcher": "Укради заставу", + "Flying": "Лепи снови", + "Football": "Фудбал", + "ForwardMarch": "Јуриш", + "GrandRomp": "Освајање", + "Hockey": "Хокеј", + "Keep Away": "Бежање", + "Marching": "Јурцање", + "Menu": "Главни мени", + "Onslaught": "Нападање", + "Race": "Трка", + "Scary": "Краљ брда", + "Scores": "Екран са резултатима", + "Survival": "Елиминација", + "ToTheDeath": "Смртна борба", + "Victory": "Екран са финалним резултатима" + }, + "spaceKeyText": "размак", + "statsText": "Статистике", + "storagePermissionAccessText": "Ово треба дозволу за коришћење меморије.", + "store": { + "alreadyOwnText": "Већ поседујеш \"${NAME}\"!", + "bombSquadProNameText": "Професионални \"${APP_NAME}\"", + "bombSquadProNewDescriptionText": "• Уклања рекламе и поруке за куповину пуне верзије\n• Откључава више подешавања игре\n• Такође укључује:", + "buyText": "Купи", + "charactersText": "Ликови", + "comingSoonText": "Долази ускоро...", + "extrasText": "Додаци", + "freeBombSquadProText": "\"BombSquad\" игра је сада бесплатна, али пошто си је већ платио добићеш\nпрофесионалну BombSquad надоградњу и ${COUNT} тикета за захвалност.\nУживај у новим стварима и хвала ти за подршку!\n-Ерик", + "holidaySpecialText": "Празнични специјал", + "howToSwitchCharactersText": "(иди у \"${SETTINGS} -> ${PLAYER_PROFILES}\" да одобриш и прилагодиш лика)", + "howToUseIconsText": "(направи глобални профил играча (у прозору за налоге) да користиш ово)", + "howToUseMapsText": "(користи ове мапе у својим сопственим \"Свако за себе\"/тимским листама игара)", + "iconsText": "Иконице", + "loadErrorText": "Не може да се учита страница.\nПровери своју интернет конекцију.", + "loadingText": "учитавање", + "mapsText": "Мапе", + "miniGamesText": "Мини игре", + "oneTimeOnlyText": "(јединствена прилика)", + "purchaseAlreadyInProgressText": "Коповина ове ставке је већ у процесу.", + "purchaseConfirmText": "Купити \"${ITEM}\"?", + "purchaseNotValidError": "Куповина није важећа.\nКонтактирај ${EMAIL} ако је ово грешка.", + "purchaseText": "Купи", + "saleBundleText": "Попуст за комплет!", + "saleExclaimText": "Попуст!", + "salePercentText": "(${PERCENT}% снижено)", + "saleText": "ПОПУСТ", + "searchText": "Тражи", + "teamsFreeForAllGamesText": "\"Свако за себе\" / тимске игре", + "totalWorthText": "*** вредност од ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Надогради?", + "winterSpecialText": "Зимски специјал", + "youOwnThisText": "- поседујеш ово -" + }, + "storeDescriptionText": "Лудница за заједницу од 8 играча!\n\nРазнеси своје пријатеље (или компјутер) у турнирима експлозивних мини игара као на пример: \"Узми заставу\", \"Експлозивни хокеј\" и \"Епски успорена смртна борба\"!\n\nЈедноставне контроле и широк обим подршке контролера праве ову игру лаку за до 8 играча у акцији; можеш чак користити и мобилне уређаје каи контролере преко \"BombSquad Remote\" апликације!\n\nБаци бомбе!\n\nПровери www.froemling.net/bombsquad за више информација.", + "storeDescriptions": { + "blowUpYourFriendsText": "Разнеси своје пријатеље.", + "competeInMiniGamesText": "Такмичи се у мини играма које обухватају све од тркања до летења.", + "customize2Text": "Прилагоди ликове, мини игре, па чак и листу звукова.", + "customizeText": "Прилагоди ликове и направи своје сопствене листе мини игара.", + "sportsMoreFunText": "Спортови су забавнији са ексползијама.", + "teamUpAgainstComputerText": "Удружите се против компјутера." + }, + "storeText": "Продавница", + "submitText": "Унеси", + "submittingPromoCodeText": "Уношење кода...", + "teamNamesColorText": "Тимска имена/боје...", + "telnetAccessGrantedText": "Телнет приступ одбијен.", + "telnetAccessText": "Телнет приступ примећен; дозволи?", + "testBuildErrorText": "Овај систем за тестирање више није доступан; молимо вас надоградите на најновију верзију.", + "testBuildText": "Систем за тестирање", + "testBuildValidateErrorText": "Није могуће одобрити систем за тестирање. (нема интернет конекције?)", + "testBuildValidatedText": "Систем за тестирање валидан; уживај!", + "thankYouText": "Хвала на подршци! Уживај у игри!!", + "threeKillText": "ТРОСТРУКО УБИСТВО!!", + "timeBonusText": "Додатно време", + "timeElapsedText": "Истекло време", + "timeExpiredText": "Истекло време", + "timeSuffixDaysText": "${COUNT}д", + "timeSuffixHoursText": "${COUNT}с", + "timeSuffixMinutesText": "${COUNT}м", + "timeSuffixSecondsText": "${COUNT}сeк", + "tipText": "Савет", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Топ пријатељи", + "tournamentCheckingStateText": "Провера статуса турнира; молимо вас сачекајте...", + "tournamentEndedText": "Овај турнир се завршио. Нови ће почети ускоро.", + "tournamentEntryText": "Улазак на турнир", + "tournamentResultsRecentText": "Скорашњи резултати турнира", + "tournamentStandingsText": "Табела турнира", + "tournamentText": "Турнир", + "tournamentTimeExpiredText": "Време за турнир истекло", + "tournamentsText": "Турнири", + "translations": { + "characterNames": { + "Agent Johnson": "Агент Џонсон", + "B-9000": "Б-9000", + "Bernard": "Медведић", + "Bones": "Костурко", + "Butch": "Џон", + "Easter Bunny": "Ускршњи зека", + "Flopsy": "Зечић", + "Frosty": "Снешко", + "Gretel": "Гретела", + "Grumbledorf": "Гандалф", + "Jack Morgan": "Џек Морган", + "Kronk": "Кронк", + "Lee": "Леа", + "Lucky": "Срећко", + "Mel": "Кувар", + "Middle-Man": "Супермен", + "Minimus": "Минимус", + "Pascal": "Жиле", + "Pixel": "Звончица", + "Sammy Slam": "Боксер", + "Santa Claus": "Деда Мраз", + "Snake Shadow": "Хитра Сенка", + "Spaz": "Валтер", + "Taobao Mascot": "Таубау Маскота", + "Todd McBurton": "Рамбо", + "Zoe": "Зои", + "Zola": "Зола" + }, + "coopLevelNames": { + "${GAME} Training": "\"${GAME}\" тренинг", + "Infinite ${GAME}": "Бесконачна \"${GAME}\"", + "Infinite Onslaught": "Бесконачна инвазија", + "Infinite Runaround": "Бесконачнo јурцање", + "Onslaught Training": "Тренинг инвазије", + "Pro ${GAME}": "Професионална \"${GAME}\"", + "Pro Football": "Професионални фудбал", + "Pro Onslaught": "Професионална инвазија", + "Pro Runaround": "Професионално јурцање", + "Rookie ${GAME}": "Почетничка \"${GAME}\"", + "Rookie Football": "Почетнички фудбал", + "Rookie Onslaught": "Почетничка инвазија", + "The Last Stand": "Последњи преживели", + "Uber ${GAME}": "Легендарна ${GAME}", + "Uber Football": "Легендарни фудбал", + "Uber Onslaught": "Легендарна инвазија", + "Uber Runaround": "Легендарно јурцање" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Буди означен на одређено време да победиш.\nУбиј означеног да постанеш он.", + "Bomb as many targets as you can.": "Бомбардуј што више мета можеш.", + "Carry the flag for ${ARG1} seconds.": "Носи заставу ${ARG1} секунди.", + "Carry the flag for a set length of time.": "Носи заставу одређен временски период.", + "Crush ${ARG1} of your enemies.": "Разбиј ${ARG1} својих непријатеља.", + "Defeat all enemies.": "Порази све непријатеље.", + "Dodge the falling bombs.": "Избегни падајуће бомбе.", + "Final glorious epic slow motion battle to the death.": "Финална славна епско успорена битка до смрти.", + "Gather eggs!": "Сакупи јаја!", + "Get the flag to the enemy end zone.": "Однеси заставу до непријатељске завршне зоне.", + "How fast can you defeat the ninjas?": "Колико брзо можеш поразити нинџе?", + "Kill a set number of enemies to win.": "Убиј одређен број непријатеља за победу.", + "Last one standing wins.": "Последњи на ногама побеђује.", + "Last remaining alive wins.": "Последњи који остане жив побеђује.", + "Last team standing wins.": "Последњи тим на ногама побеђује.", + "Prevent enemies from reaching the exit.": "Спречи непријатеље да дођу до излаза.", + "Reach the enemy flag to score.": "Дођи до непријатељске заставе да постигнеш поен.", + "Return the enemy flag to score.": "Врати непријатељску заставу да постигнеш поен.", + "Run ${ARG1} laps.": "Истрчи ${ARG1} круга.", + "Run ${ARG1} laps. Your entire team has to finish.": "Истрчи ${ARG1} круга. Цео твој тим мора да заврши.", + "Run 1 lap.": "Истрчи 1 круг.", + "Run 1 lap. Your entire team has to finish.": "Истрчи 1 круг. Цео твој тим мора да заврши.", + "Run real fast!": "Трчи баш брзо!", + "Score ${ARG1} goals.": "Постигни ${ARG1} гола.", + "Score ${ARG1} touchdowns.": "Постигни ${ARG1} гола.", + "Score a goal.": "Постигни гол.", + "Score a touchdown.": "Постигни гол.", + "Score some goals.": "Постигни неколико голова.", + "Secure all ${ARG1} flags.": "Осигурај све ${ARG1} заставе.", + "Secure all flags on the map to win.": "Осигурај све заставе на мапи за победу.", + "Secure the flag for ${ARG1} seconds.": "Осигурај заставу на ${ARG1} секунди.", + "Secure the flag for a set length of time.": "Осигурај заставу на одређени временски период.", + "Steal the enemy flag ${ARG1} times.": "Укради непријатељску заставу ${ARG1} пута.", + "Steal the enemy flag.": "Укради непријатељску заставу.", + "There can be only one.": "Овде може бити само један.", + "Touch the enemy flag ${ARG1} times.": "Додирни непријатељску заставу ${ARG1} пута.", + "Touch the enemy flag.": "Додирни непријатељску заставу.", + "carry the flag for ${ARG1} seconds": "носи заставу ${ARG1} секунди", + "kill ${ARG1} enemies": "убиј ${ARG1} непријатеља", + "last one standing wins": "последњи на ногама побеђује", + "last team standing wins": "последњи тим на ногама побеђује", + "return ${ARG1} flags": "врати ${ARG1} заставе", + "return 1 flag": "врати 1 заставу", + "run ${ARG1} laps": "истрчи ${ARG1} круга", + "run 1 lap": "истрчи 1 круг", + "score ${ARG1} goals": "постигни ${ARG1} гола", + "score ${ARG1} touchdowns": "постигни ${ARG1} гола", + "score a goal": "постигни гол", + "score a touchdown": "постигни гол", + "secure all ${ARG1} flags": "осигурај све ${ARG1} заставе", + "secure the flag for ${ARG1} seconds": "осигурај заставу на ${ARG1} секунди", + "touch ${ARG1} flags": "додирни ${ARG1} заставе", + "touch 1 flag": "додирни 1 заставу" + }, + "gameNames": { + "Assault": "Напад", + "Capture the Flag": "Узми заставу", + "Chosen One": "Одабрани", + "Conquest": "Освајање", + "Death Match": "Битка до смрти", + "Easter Egg Hunt": "Лов на ускршња јаја", + "Elimination": "Елиминација", + "Football": "Фудбал", + "Hockey": "Хокеј", + "Keep Away": "Држи подаље", + "King of the Hill": "Краљ брда", + "Meteor Shower": "Киша метеора", + "Ninja Fight": "Битка са нинџама", + "Onslaught": "Инвазија", + "Race": "Трка", + "Runaround": "Јурцање", + "Target Practice": "Гађање мете", + "The Last Stand": "Последњи преживели" + }, + "inputDeviceNames": { + "Keyboard": "Тастатура", + "Keyboard P2": "Тастатура И2" + }, + "languages": { + "Arabic": "Арапски", + "Belarussian": "Белоруски", + "Chinese": "Кинески (поједностављен)", + "ChineseTraditional": "Кинески (традиционални)", + "Croatian": "Хрватски", + "Czech": "Чешки", + "Danish": "Дански", + "Dutch": "Холандски", + "English": "Енглески", + "Esperanto": "Есперантски", + "Finnish": "Фински", + "French": "Француски", + "German": "Немачки", + "Gibberish": "Џиберски", + "Greek": "Грчки", + "Hindi": "Хинди", + "Hungarian": "Мађарски", + "Indonesian": "Индонежански", + "Italian": "Италијански", + "Japanese": "Јапански", + "Korean": "Корејски", + "Persian": "Персијски", + "Polish": "Пољски", + "Portuguese": "Португалски", + "Romanian": "Румунски", + "Russian": "Руски", + "Serbian": "Српски", + "Slovak": "Словачки", + "Spanish": "Шпански", + "Swedish": "Шведски", + "Turkish": "Турски", + "Ukrainian": "Украјински", + "Venetian": "Венецијански", + "Vietnamese": "Вијетнамски" + }, + "leagueNames": { + "Bronze": "Бронза", + "Diamond": "Дијамант", + "Gold": "Златo", + "Silver": "Сребрo" + }, + "mapsNames": { + "Big G": "Велико С", + "Bridgit": "Мост", + "Courtyard": "Игралиште", + "Crag Castle": "Две тврђаве", + "Doom Shroom": "Зла печурка", + "Football Stadium": "Фудбалски стадион", + "Happy Thoughts": "Лепи снови", + "Hockey Stadium": "Хокејашки стадион", + "Lake Frigid": "Ледено језеро", + "Monkey Face": "Лице мајмуна", + "Rampage": "Пирамида", + "Roundabout": "Кружни ток", + "Step Right Up": "Степенице", + "The Pad": "Платформа", + "Tip Top": "Црни врх", + "Tower D": "Торањ", + "Zigzag": "Цик-цак" + }, + "playlistNames": { + "Just Epic": "Само епско", + "Just Sports": "Само спортови" + }, + "scoreNames": { + "Flags": "Заставе", + "Goals": "Голови", + "Score": "Резултат", + "Survived": "Преживео", + "Time": "Време", + "Time Held": "Времена издржано" + }, + "serverResponses": { + "A code has already been used on this account.": "Код је већ искоришћен на овом налогу.", + "A reward has already been given for that address.": "Награда је већ предата за ту адресу.", + "Account linking successful!": "Повезивање налога успешно!", + "Account unlinking successful!": "Растављање налога успешно!", + "Accounts are already linked.": "Налози су већ повезани.", + "An error has occurred; (${ERROR})": "Дошло је до грешке; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Дошло је до грешке; молимо вас обратите се подршци. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Дошло је до грешке; молимо контактирајте support@froemling.net.", + "An error has occurred; please try again later.": "Дошло је до грешке; молимо вас покушајте поново касније.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Да ли сте сигурни да желите да повежете ове налоге?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nОво се не може вратити!", + "BombSquad Pro unlocked!": "Професионални BombSquad откључан!", + "Can't link 2 accounts of this type.": "Не могу да се повежу 2 налога овог типа.", + "Can't link 2 diamond league accounts.": "Не могу бити повезана 2 налога дијамантске лиге.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Немогуће повезати; превазишао си максимум од ${COUNT} повезаних налога.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Примећено је варање; резултати и награде су суспендовани на ${COUNT} дана.", + "Could not establish a secure connection.": "Немогуће успоставити безбедну конекцију.", + "Daily maximum reached.": "Дневни максимум достигнут.", + "Entering tournament...": "Улажење на турнир...", + "Invalid code.": "Неважећи код.", + "Invalid payment; purchase canceled.": "Неважећа уплата; куповина отказана.", + "Invalid promo code.": "Неважећи промо код.", + "Invalid purchase.": "Неважећа куповина.", + "Invalid tournament entry; score will be ignored.": "Неважећи улазак на турнир; резултат ће бити игнорисан.", + "Item unlocked!": "Ствар откључана!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "ПОВЕЗИВАЊЕ ОДБИЈЕНО. ${ACCOUNT} садржи\nзначајне податке и СВИ ЋЕ БИТИ ИЗГУБЉЕНИ.\nМожеш повезати у супротном смеру ако желите\n(и изгубити податке са ОВОГ налога)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Повежи налог ${ACCOUNT} са овим налогом?\nСви постојећи подаци на ${ACCOUNT} ће бити изгубљени.\nОво се не може вратити. Да ли си сигуран?", + "Max number of playlists reached.": "Максималан број листи играња достигнут.", + "Max number of profiles reached.": "Максималан број профила достигнут.", + "Maximum friend code rewards reached.": "Максималан број награда од пријатељских кодова достигнут.", + "Message is too long.": "Порука је предугачка.", + "Profile \"${NAME}\" upgraded successfully.": "Профил \"${NAME}\" је успешно надограђен.", + "Profile could not be upgraded.": "Профил не може бити надограђен.", + "Purchase successful!": "Куповина успешна!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Примљено ${COUNT} тикета за пријављивање.\nВрати се сутра да добијеш ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Фукционалност сервера више није подржана на овај верзији игре;\nМолимо вас надоградите на новију верзију.", + "Sorry, there are no uses remaining on this code.": "Извињавамо се, овај код више не може да се користи.", + "Sorry, this code has already been used.": "Извињавамо се, овај код је већ употребљен.", + "Sorry, this code has expired.": "Извињавамо се, код је истекао.", + "Sorry, this code only works for new accounts.": "Извињавамо се, овај код ради само за нове налоге.", + "Temporarily unavailable; please try again later.": "Привремено недоступно; молимо вас покушајте поново касније.", + "The tournament ended before you finished.": "Турнир се завршио пре него што си завршио.", + "This account cannot be unlinked for ${NUM} days.": "Овај налог не може бити растављен још ${NUM} дана.", + "This code cannot be used on the account that created it.": "Овај код не може бити употребљен на налогу који га је направио.", + "This is currently unavailable; please try again later.": "Ово тренутно није доступно; молимо вас покушајте касније.", + "This requires version ${VERSION} or newer.": "Ово захтева верзију ${VERSION} или новију.", + "Tournaments disabled due to rooted device.": "Турнири су онемогућени због рутованог уређаја.", + "Tournaments require ${VERSION} or newer": "За турнир ти треба ${VERSION} или новија", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Растави ${ACCOUNT} од овог налога?\nСви подаци са ${ACCOUNT} ће бити рестартовани.\n(осим достигнућа у неким случајевима)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "УПОЗОРЕЊЕ: жалбе за варање су поднете против твог налога.\nНалозима код којих се установи да варају биће забрањен приступ. Молимо вас играјте поштено.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Да ли би желеo да повежеш налог на твом уређају са овим?\n\nНалог на твом уређају је ${ACCOUNT1}\nОвај налог је ${ACCOUNT2}\n\nОво ће ти омогућити да задржиш постојећи напредак.\nУпозорење: ово се не може вратити!", + "You already own this!": "Већ поседујеш ово!", + "You can join in ${COUNT} seconds.": "Можеш се придружити за ${COUNT} секунди.", + "You don't have enough tickets for this!": "Немаш довољно тикета за ово!", + "You don't own that.": "Не поседујеш ово.", + "You got ${COUNT} tickets!": "Добио си ${COUNT} тикета!", + "You got a ${ITEM}!": "Добио си ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Унапређен си у нову лигу; честитамо!", + "You must update to a newer version of the app to do this.": "Мораш надоградити на новију верзију апликације да урадиш ово.", + "You must update to the newest version of the game to do this.": "Мораш надоградити на најновију верзију игре да урадиш ово.", + "You must wait a few seconds before entering a new code.": "Мораш чекати неколико секунди пре него што унесеш нови код.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Рангиран си #${RANK} на последњем турниру. Хвала на игрању!", + "Your account was rejected. Are you signed in?": "Твој налог је одбијен. Да ли си пријављен?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Твоја копија игре је модификована. Молимо\nвас вратите неке промене и покушајте поново.", + "Your friend code was used by ${ACCOUNT}": "Твој код за пријатеља је искоришћен од стране ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 минут", + "1 Second": "1 секунда", + "10 Minutes": "10 минута", + "2 Minutes": "2 минута", + "2 Seconds": "2 секунде", + "20 Minutes": "20 минута", + "4 Seconds": "4 секунде", + "5 Minutes": "5 минута", + "8 Seconds": "8 секунди", + "Allow Negative Scores": "Дозволи негативне поене", + "Balance Total Lives": "Балансирај број укупних живота", + "Bomb Spawning": "Стварање бомби", + "Chosen One Gets Gloves": "Одабрани добија рукавице", + "Chosen One Gets Shield": "Одабрани добија штит", + "Chosen One Time": "Време за одабраног", + "Enable Impact Bombs": "Омогући бомбе на додир", + "Enable Triple Bombs": "Омогући троструке бомбе", + "Entire Team Must Finish": "Цео тим мора да заврши", + "Epic Mode": "Епски мод", + "Flag Idle Return Time": "Време повратка испуштене заставе", + "Flag Touch Return Time": "Време повратка заставе од додира", + "Hold Time": "Време задржавања", + "Kills to Win Per Player": "Убистава за победу по играчу", + "Laps": "Кругови", + "Lives Per Player": "Животи по играчу", + "Long": "Дуго", + "Longer": "Дуже", + "Mine Spawning": "Стварање мина", + "No Mines": "Без мина", + "None": "Ниједан", + "Normal": "Нормално", + "Pro Mode": "Професионални мод", + "Respawn Times": "Време оживљавања", + "Score to Win": "Поени за победу", + "Short": "Кратко", + "Shorter": "Краће", + "Solo Mode": "Соло мод", + "Target Count": "Број мета", + "Time Limit": "Временско ограничење" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} је дисквалификован јер је ${PLAYER} напустио игру", + "Killing ${NAME} for skipping part of the track!": "Убијање ${NAME} због прескакања дела стазе!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Упозорење за ${NAME}: турбо / непрестано притискање те избацује." + }, + "teamNames": { + "Bad Guys": "Лоши момци", + "Blue": "Плави", + "Good Guys": "Добри момци", + "Red": "Црвени" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Перфектно усклађен ударац из залета са скоком вртећи се успут може убити\nједним ударцем и зарадити вам доживотно поштовање пријатеља.", + "Always remember to floss.": "Увек се сети да опереш зубе.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Направите профиле играча за себе и своје пријатеље са\nжељеним именима и изгледом уместо да користите насумично одабране.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Уклете кутије те претварају у темпирану бомбу.\nЈедини лек је да брзо зграбиш прву помоћ.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Упркос њиховом изгледу, све способности ликова су идентичне,\nзато само изабери оног који највише личи на тебе.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Немој да се превише опустиш са штитом; и даље можеш да паднеш са мапе.", + "Don't run all the time. Really. You will fall off cliffs.": "Не трчи све време. Озбиљно. Пашћеш са мапе.", + "Don't spin for too long; you'll become dizzy and fall.": "Не врти се предуго; почеће да ти се врти у глави и пашћеш.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Држи било које дугме за трчање. (Окидачи добро раде ако их имаш)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Држи било које дугме за трчање. Стићи ћеш на места брже\nали нећеш скретати баш добро, зато се пази ивица.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ледене бомбе нису много јаке, али оне заледе\nсве које закаче, остављајући их подложне ломљењу.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Ако те неко ухвати, ударај га и испустиће те.\nОво такође ради и у стварном животу.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Ако сте танки са контролерима, преузмите \"${REMOTE_APP_NAME}\" апликацију\nна мобилне уређаје и користите их као контролере.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Ако добијеш лепљиву бомбу залепњену за тебе, скачи унаоколо и врти се у круг. Можда\nћеш отрести бомбу са себе, или ако ништа барем ће твоји последњи тренуци бити забавни.", + "If you kill an enemy in one hit you get double points for it.": "Ако убијеш непријатеља једним ударцом добијаш двоструке поене за то.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Ако покупиш клетву, твоја једина нада за преживљавање\nје да нађеш прву помоћ у следећих неколико секунди.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Ако стојиш у једном месту, ти си мртав. Трчи и избегавај да преживиш..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Ако имаш много играча који долазе и одлазе, укључи \"аутоматски избаци неактивне играче\"\nу подешавањима у случају да неко заборави да напусти игру.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Ако уређај постаје врућ или желиш да сачуваш батерију,\nсмањи детаље или резолуцију у Подешавања->Графике", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Ако ти је слика сецка, покушај да смањиш резолуцију\nили детаље у подешавањима графика у игри.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "У игри \"Узми заставу\", твоја властита застава мора бити у бази да би постигао поен. Ако је\nдруги тим близу постизања поена, крађа њихове заставе може бити добар начин да их зауставите.", + "In hockey, you'll maintain more speed if you turn gradually.": "У хокеју, одржаћеш више брзине ако скрећеш постепено.", + "It's easier to win with a friend or two helping.": "Лакше је победити са пријатељем или два у помоћи.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Скочи у тренутку бацања бомбе да би је бацио на вишу тачку.", + "Land-mines are a good way to stop speedy enemies.": "Мине су добар начин да се зауставе брзи непријатељи.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Многе ствари могу бити подигнуте и бачене, укључујући друге играче. Бацање\nнепријатеља са литице може бити ефективна али и емоционално задовољавајућа стратегија.", + "No, you can't get up on the ledge. You have to throw bombs.": "Не, не можеш само стајати на ивици. Мораш да бацаш бомбе.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Играчи се могу придружити и напустити у сред већине игара,\nтакође и контролери се могу укључити и искључити у лету.", + "Practice using your momentum to throw bombs more accurately.": "Вежбај коришћење залета да бациш бомбу прецизније.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Ударци наносе више штете ако се твоје песнице крећу,\nзато пробај трчање, скакање, и вртење као да си луд.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Трчи напред и назад пре бацања бомбе\nда је завртиш и бациш даље.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Уништи групу непријатеља активирајући\nбомбу близу кутије са динамитом.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Глава је најрањивији део, зато лепљива бомба\nна глави обично значи крај живота.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Овај ниво нема крај, али високи резултат овде\nзарадиће ти вечно поштовање широм света.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Снага бацања је базирана на страни коју држиш на контролеру.\nДа бациш нешто нежно испред себе, не држи ни једну страну.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Досадила ти је листа звукова? Замени је својом!\nВиди Подешавања->Аудио->Листа звукова", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Пробај да \"пржиш\" бомбе секунду или две пре него што их бацаш.", + "Try tricking enemies into killing eachother or running off cliffs.": "Пробај да завараш непријатеље да се међусобно убију или падну са ивице.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Користи дугме за подизање да узмеш заставу < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Окрећи се напред-назад да добијеш већу даљину када бацаш.", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Можеш да \"нациљаш\" ударце окретањем лево или десно.\nОво је корисно за бацање лоших момака са ивица или за постизање поена у хокеју.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Можеш да процениш када ће бомба да експлодира гледајући на\nбоју варница са фитиља: жуте..наранџасте..црвене..БУМ.", + "You can throw bombs higher if you jump just before throwing.": "Можеш да бациш бомбе више ако поскочиш само трен пре бацања.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Добијаш штету када удариш главом у нешто, зато\nпокушај да не удараш главом о ствари.", + "Your punches do much more damage if you are running or spinning.": "Твоји ударци наносе више штете ако трчиш или се окрећеш." + } + }, + "trophiesRequiredText": "Ово захтева барем ${NUMBER} трофеја.", + "trophiesText": "Трофеји", + "trophiesThisSeasonText": "Трофеји ове сезоне", + "tutorial": { + "cpuBenchmarkText": "Покретање туториала на смешним брзинама (најпре тестира брзину процесора)", + "phrase01Text": "Хеј ти!", + "phrase02Text": "Добродошао у \"${APP_NAME}\"!", + "phrase03Text": "Ево неколико савета како да контролишеш твог лика:", + "phrase04Text": "Много ствари у \"${APP_NAME}\" је засновано на ФИЗИЦИ.", + "phrase05Text": "На пример, када удариш,..", + "phrase06Text": "..штета је заснована на брзини твојих песница.", + "phrase07Text": "Видиш? Нисмо се кретали, ${NAME} је ово једва осетио.", + "phrase08Text": "Хајде сада да скочимо и да се завртимо за већу брзину.", + "phrase09Text": "Ах, то је боље.", + "phrase10Text": "Трчање такође помаже.", + "phrase11Text": "Држи БИЛО КОЈЕ дугме за трчање.", + "phrase12Text": "За супер-екстра ударце, пробај трчање И вртење.", + "phrase13Text": "Упс; ${NAME}, извини за ово.", + "phrase14Text": "Многе ствари можеш покупити и бацити као што су заставе.. или ${NAME}.", + "phrase15Text": "На крају, ту су бомбе.", + "phrase16Text": "Бацање бомби захтева вежбу.", + "phrase17Text": "Аућ! Не баш добро бацање.", + "phrase18Text": "Кретање ти помаже да бациш даље.", + "phrase19Text": "Скакање ти помаже да бациш више.", + "phrase20Text": "\"Заврти\" твоје бомбе за још даље бацање.", + "phrase21Text": "Темпирање бомби може бити изазовно.", + "phrase22Text": "Јој.", + "phrase23Text": "Пробај да \"пржиш\" фитиљ секунду или две.", + "phrase24Text": "Опа! Лепо испржено.", + "phrase25Text": "Паа, то је отприлике то.", + "phrase26Text": "Сада иди и направи ме поносним, лаве!", + "phrase27Text": "Запамти свој тренинг, и ВРАТИЋЕШ се жив!", + "phrase28Text": "...па, можда...", + "phrase29Text": "Срећно!", + "randomName1Text": "Чода", + "randomName2Text": "Иван", + "randomName3Text": "Мили", + "randomName4Text": "Лука", + "randomName5Text": "Фићо", + "skipConfirmText": "Озбиљно желиш да прескочиш туториал? Додирни или притисни да наставиш.", + "skipVoteCountText": "${COUNT}/${TOTAL} гласова за прескакање", + "skippingText": "прескакање туториала...", + "toSkipPressAnythingText": "(додирни или притисни било шта да прескочиш туторијал)" + }, + "twoKillText": "ДВОСТРУКО УБИСТВО!", + "unavailableText": "недоступно", + "unconfiguredControllerDetectedText": "Неподешен контролер откривен:", + "unlockThisInTheStoreText": "Ово мора бити откључано у продавници.", + "unlockThisProfilesText": "Да направиш више од ${NUM} профила, треба да:", + "unlockThisText": "Да откључаш ово, треба да:", + "unsupportedHardwareText": "Извињавамо се, овај хардвер није подржан од стране игре.", + "upFirstText": "Прво на реду:", + "upNextText": "Следећа игра на реду ${COUNT}:", + "updatingAccountText": "Ажурирање твог налога...", + "upgradeText": "Надогради", + "upgradeToPlayText": "Откључај \"${PRO}\" у продавници игре да играш ово.", + "useDefaultText": "Користи подразумевано", + "usesExternalControllerText": "Ова игра користи спољашњи контролер за унос.", + "usingItunesText": "Коришћење музичке апликације за листу звукова...", + "validatingTestBuildText": "Потврђивање система за тестирање...", + "victoryText": "Победа!", + "voteDelayText": "Не можеш почети ново гласање још ${NUMBER} секунди", + "voteInProgressText": "Гласање је већ у току.", + "votedAlreadyText": "Већ си гласао", + "votesNeededText": "${NUMBER} гласова потребно", + "vsText": "против", + "waitingForHostText": "(чекање ${HOST} да настави)", + "waitingForPlayersText": "чекање играча да се придруже...", + "waitingInLineText": "Чекање у реду (заједница је пуна)...", + "watchAVideoText": "Погледај видео", + "watchAnAdText": "Погледај рекламу", + "watchWindow": { + "deleteConfirmText": "Избриши \"${REPLAY}\"?", + "deleteReplayButtonText": "Избриши\nснимак", + "myReplaysText": "Моји снимци", + "noReplaySelectedErrorText": "Нема изабраних снимака", + "playbackSpeedText": "Брзина пуштања: ${SPEED}", + "renameReplayButtonText": "Преименуј\nснимак", + "renameReplayText": "Преименуј \"${REPLAY}\" у:", + "renameText": "Преименуј", + "replayDeleteErrorText": "Грешка при брисању снимка.", + "replayNameText": "Име снимка", + "replayRenameErrorAlreadyExistsText": "Снимак са тим именом већ постоји.", + "replayRenameErrorInvalidName": "Немогуће преименовати снимак; неважеће име.", + "replayRenameErrorText": "Грешка при преименовању снимка.", + "sharedReplaysText": "Дељени снимци", + "titleText": "Гледај", + "watchReplayButtonText": "Погледај\nснимак" + }, + "waveText": "Рунда", + "wellSureText": "Наравно!", + "wiimoteLicenseWindow": { + "titleText": "Дарвин Римоут права задржана" + }, + "wiimoteListenWindow": { + "listeningText": "Ослушкивање за Вимоутс...", + "pressText": "Притисните истовремено дугмад 1 и 2 на Вимоуту.", + "pressText2": "На новијим Вимоут са уграђеним плус покретима, позади притисни црвено дугме \"синхронизација\"." + }, + "wiimoteSetupWindow": { + "copyrightText": "Дарвин Римоут права задржана", + "listenText": "Слушај", + "macInstructionsText": "Буди сигуран да је твој Ви искључен и да је блутуф\nомогућен на твом Мек уређају, онда притисни \"Слушај\".\nВимоут подршка може бити помало непоуздана, па ћеш можда\nморати да покушаш неколико пута пре него што се повежеш.\n\nБлутуф би требао да подржи до 7 повезаних уређаја,\nмада може да варира.\n\nИгра подржава оригинални Вимоутс, Нанчакс,\nкао и класичне контролере.\nНовији 'Ви ремоут плус' сада ради такође\nали не са додацима.", + "thanksText": "Хвала Дарвин Ремоут тиму\nшто је ово омогућио.", + "titleText": "Вимоут подешавања" + }, + "winsPlayerText": "${NAME} је победио!", + "winsTeamText": "${NAME} је победио!", + "winsText": "${NAME} је победио!", + "worldScoresUnavailableText": "Светски резултати недоступни.", + "worldsBestScoresText": "Најбољи резултати на свету", + "worldsBestTimesText": "Најбоља времена на свету", + "xbox360ControllersWindow": { + "getDriverText": "Набави драјвер", + "macInstructions2Text": "Да користиш контролере бежично, треба ти ресивер који долази са\n\"Иксбокс 360 безични контролери за Виндовс\". Један ресивер\nдозвољава повезивање до 4 контролера.\n\nВажно: ресивери треће стране неће радити са овим драјвером;\nбуди сигуран да ресивер има \"Мајкрософт\" на себи, не \"Иксбокс\n360\". Мајкрософт ово више не продаје одвојено, па ћете морати\nда га набавите заједно са контролером или потражите на и-бају.\n\nАко вам ово помогне, молимо вас размототрите донирање\nдивелоперу драјвера на његовом сајту.", + "macInstructionsText": "Да користиш Иксбокс 360 контролере, биће ти потребно\nда инсталираш Мек драјвер доступан на линку испод.\nРади и са жичаним и са бежичним контролерима.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "Да користиш жичане Иксбокс 360 контролере са \"BombSquad\",\nједноставно их укључи у USB прикључак вашег уређаја. Можеш\nкористити USB адаптер да повежеш више контролера.\n\nЗа коришћење бежичних управљача биће ти потребан бежични ресивер,\nдоступан као део \"Иксбокс 360 бежичних контролера за Виндовс\"\nпакета или продат посебно. Сваки ресивер се прикључује на USB\nприкључак и омогућава повезивање до 4 бежична управљача.", + "titleText": "Коришћење Иксбокс 360 управљача са \"${APP_NAME}\":" + }, + "yesAllowText": "Да, дозволи!", + "yourBestScoresText": "Твоји најбољи резултати", + "yourBestTimesText": "Твоја најбоља времена" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/slovak.json b/dist/ba_data/data/languages/slovak.json new file mode 100644 index 0000000..a30ffc6 --- /dev/null +++ b/dist/ba_data/data/languages/slovak.json @@ -0,0 +1,1809 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Mená účtov nemôžu obsahovať emoji alebo špeciálne znaky", + "accountProfileText": "Profil konta", + "accountsText": "Účty", + "achievementProgressText": "Achievementy: ${COUNT} z ${TOTAL}", + "campaignProgressText": "Postup v Campaign [Hard] : ${PROGRESS}", + "changeOncePerSeason": "Toto môžete zmeniť len raz za sezónu.", + "changeOncePerSeasonError": "Musíte počkať do ďalšej sezóny aby ste mohli toto znova zmeniť (${NUM} days)", + "customName": "Vlastný názov", + "linkAccountsEnterCodeText": "Vložte kód", + "linkAccountsGenerateCodeText": "Vygenerovať kód", + "linkAccountsInfoText": "(zdielajte postup medzi rôznymi platformami)", + "linkAccountsInstructionsNewText": "Pre prepojenie dvoch účtov, vygenerujte kód na prvom \na vložte ten kód na druhom. Dáta z \ndruhého účtu budú zdieľané medzi nimi.\n (Dáta z prvého účtu budú stratené)\n\nMôžete prepojiť až ${COUNT} účtov.\n\nDÔLEŽITÉ: prepájajte len účty ktoré vlastníte;\nak prepojíte účty priateľov, nebudete \nschopní hrať online naraz.", + "linkAccountsInstructionsText": "Abyste mohli pripojiť dva účty, vygenerujte na prvom kód,\nktorý následne vložte na účte druhom.\nPostup a inventár sa skombinujú.\nMôžete spojiť až ${COUNT} účtov.", + "linkAccountsText": "Pripojené účty", + "linkedAccountsText": "Spojené účty:", + "nameChangeConfirm": "Zmeniť vás účet na ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Týmto zresetuješ progres v tímovej hre\na všetky dosiahnuté skóre (tikety ostanú). Nedá sa \nto vrátiť späť. Pokračovať?", + "resetProgressConfirmText": "Týmto zresetuješ progres v tímovej hre,\nvšetky úspechy a dosiahnuté skóre\n(tikety ostanú). \nNedá sa to vrátiť späť. \nPokračovať?", + "resetProgressText": "Zresetovať progres", + "setAccountName": "Nastavte meno účtu", + "setAccountNameDesc": "Vyberte meno, ktoré sa bude zobrazovať na vašom účte.\nMôžete použiť meno jedného zo svojich prepojených \núčtov alebo si vytvorte svoje vlastné meno.", + "signInInfoText": "Prihláste sa, abyste zbierali tickety, dokončite online \na zdielajte postup medzi zariadeními.", + "signInText": "Prihlasujem", + "signInWithDeviceInfoText": "(iba automatický účet je dostupný pre toto zariadenie)", + "signInWithDeviceText": "Prihláste sa s účtom na zariadení", + "signInWithGameCircleText": "Prihlásiť sa s Game Circle", + "signInWithGooglePlayText": "Príhlásit sa s Google Play", + "signInWithTestAccountInfoText": "(starý typ účtu; v budúcnosti používajte účty zariadení)", + "signInWithTestAccountText": "Prihlásit sa s testovacím účtom", + "signOutText": "Odhlasujem", + "signingInText": "Prihlasujem", + "signingOutText": "Odhlasujem", + "testAccountWarningOculusText": "Upozornenie: ak ste prihlásení s \" test \" účtom .\nBude nahradený \"skutočný \" účet v priebehu tohto\nroku , ktorý ponúkne nákup vstupeniek a ďalšie funkcie .\nTeraz budete musieť získať všetky lístky v hre .\n( Tie sa však dostanete pre BombSquad aktualizáciou zdarma )", + "testAccountWarningText": "VAROVANIE : ste prihlásení s \" test \" účtu .\nTento účet je viazaný na tejto konkrétne zariadenie a\nmôže dôjsť k vynulovanie pravidelne . ( Takže nie je dôvod míňať\nveľa času a zneškodnenia / odomknutie veci pre neho )\nPrevádzkujeme predajnej verzii hry používať \" skutočné \"\núčtu ( Game - Center , Google Plus , atď ) To tiež\numožňuje uložiť svoj ​​postup v cloude a podielu\nže medzi rôznymi zariadeniami .", + "ticketsText": "Tikety: ${COUNT}", + "titleText": "Konto", + "unlinkAccountsInstructionsText": "Vyberte účet, s ktorým chcete zrušiť prepojenie", + "unlinkAccountsText": "Zrušiť prepojenie účtov", + "viaAccount": "(cez účet ${NAME})", + "youAreLoggedInAsText": "Si prihlásený ako:", + "youAreSignedInAsText": "Si prihlásený ako:" + }, + "achievementChallengesText": "Achievementy", + "achievementText": "Achievement", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Zabi 3 nepriateľov pomocou TNT", + "descriptionComplete": "Zabití 3 nepriatelia pomocou TNT", + "descriptionFull": "Zabi 3 nepriateľov pomocou TNT v ${LEVEL}", + "descriptionFullComplete": "Zabití 3 nepriatelia pomocou TNT v ${LEVEL}", + "name": "Bombastická trojka" + }, + "Boxer": { + "description": "Vyhraj bez použitia bômb", + "descriptionComplete": "Výhra bez použitia bômb", + "descriptionFull": "Vyhraj ${LEVEL} bez použitia bômb", + "descriptionFullComplete": "Výhra v ${LEVEL} bez použitia bômb", + "name": "Boxér" + }, + "Dual Wielding": { + "descriptionFull": "Pripoj 2 ovládače (hardware alebo aplikácia)", + "descriptionFullComplete": "Pripojené 2 ovládače (hardware alebo aplikácia)", + "name": "Dvojité Ovládanie" + }, + "Flawless Victory": { + "description": "Vyhraj bez zranenia", + "descriptionComplete": "Výhra bez zranenia", + "descriptionFull": "Vyhraj ${LEVEL} bez zranenia", + "descriptionFullComplete": "Výhra v ${LEVEL} bez zranenia", + "name": "Bez Škrabanca" + }, + "Free Loader": { + "descriptionFull": "Začni hru Všetci-proti-Všetkým hru s 2+ hráčmi", + "descriptionFullComplete": "Začatá hra Všetci-proti-Všetkým s 2+ hráčmi", + "name": "Boj Začal" + }, + "Gold Miner": { + "description": "Zabi 6 nepriateľov pomocou mín", + "descriptionComplete": "Zabití 6 nepriatelia pomocou mín", + "descriptionFull": "Zabi 6 nepriateľov pomocou mín v ${LEVEL}", + "descriptionFullComplete": "Zabití 6 nepriatelia pomocou mín v ${LEVEL}", + "name": "Zlatokop" + }, + "Got the Moves": { + "description": "Vyhraj bez použitia úderov a bômb", + "descriptionComplete": "Výhra bez použitia úderov a bômb", + "descriptionFull": "Vyhraj ${LEVEL} bez použitia úderov a bômb", + "descriptionFullComplete": "Výhra v ${LEVEL} bez použitia úderov a bômb", + "name": "Tancuj, tancuj, vykrúcaj!" + }, + "In Control": { + "descriptionFull": "Pripoj ovládač (hardware alebo aplikácia)", + "descriptionFullComplete": "Pripojený ovládač (hardware alebo aplikácia)", + "name": "Všetko pod Kontrolou" + }, + "Last Stand God": { + "description": "Nahraj 1000 bodov", + "descriptionComplete": "Nahraných 1000 bodov", + "descriptionFull": "Nahraj 1000 bodov v ${LEVEL}", + "descriptionFullComplete": "Nahraných 1000 bodov v ${LEVEL}", + "name": "${LEVEL} Boh" + }, + "Last Stand Master": { + "description": "Nahraj 250 bodov", + "descriptionComplete": "Nahraných 250 bodov", + "descriptionFull": "Nahraj 250 bodov v ${LEVEL}", + "descriptionFullComplete": "Nahraných 250 bodov v ${LEVEL}", + "name": "${LEVEL} Majster" + }, + "Last Stand Wizard": { + "description": "Nahraj 500 bodov", + "descriptionComplete": "Nahraných 500 bodov", + "descriptionFull": "Nahraj 500 bodov v ${LEVEL}", + "descriptionFullComplete": "Nahraných 500 bodov v ${LEVEL}", + "name": "${LEVEL} Čarodej" + }, + "Mine Games": { + "description": "Zabi 3 nepriateľov pomocou mín", + "descriptionComplete": "Zabití 3 nepriatelia pomocou mín", + "descriptionFull": "Zabi 3 nepriateľov pomocou mín v ${LEVEL}", + "descriptionFullComplete": "Zabití 3 nepriatelia pomocou mín v ${LEVEL}", + "name": "Mínové Hry" + }, + "Off You Go Then": { + "description": "Zhoď 3 nepriateľov z mapy", + "descriptionComplete": "Zhodení 3 nepriatelia z mapy", + "descriptionFull": "Zhoď 3 nepriateľov z mapy v ${LEVEL}", + "descriptionFullComplete": "Zhodení 3 nepriatelia z mapy v ${LEVEL}", + "name": "Leť!" + }, + "Onslaught God": { + "description": "Nahraj 5000 bodov", + "descriptionComplete": "Nahraných 5000 bodov", + "descriptionFull": "Nahraj 5000 bodov v ${LEVEL}", + "descriptionFullComplete": "Nahraných 5000 bodov v ${LEVEL}", + "name": "${LEVEL} Boh" + }, + "Onslaught Master": { + "description": "Nahraj 500 bodov", + "descriptionComplete": "Nahraných 500 bodov", + "descriptionFull": "Nahraj 500 bodov v ${LEVEL}", + "descriptionFullComplete": "Nahraných 500 bodov v ${LEVEL}", + "name": "${LEVEL} Majster" + }, + "Onslaught Training Victory": { + "description": "Poraz všetky vlny", + "descriptionComplete": "Porazené všetky vlny", + "descriptionFull": "Poraz všetky vlny v ${LEVEL}", + "descriptionFullComplete": "Porazené všetky vlny v ${LEVEL}", + "name": "${LEVEL} Víťazstvo" + }, + "Onslaught Wizard": { + "description": "Nahraj 1000 bodov", + "descriptionComplete": "Nahraných 1000 bodov", + "descriptionFull": "Nahraj 1000 bodov v ${LEVEL}", + "descriptionFullComplete": "Nahraných 1000 bodov v ${LEVEL}", + "name": "${LEVEL} Čarodej" + }, + "Precision Bombing": { + "description": "Vyhraj bez schopností", + "descriptionComplete": "Výhra bez schopností", + "descriptionFull": "Vyhraj ${LEVEL} bez schopností", + "descriptionFullComplete": "Výhra v ${LEVEL} bez schopností", + "name": "Rozhodnutie Bombardovať" + }, + "Pro Boxer": { + "description": "Vyhraj bez použitia bômb", + "descriptionComplete": "Výhra bez použitia bômb", + "descriptionFull": "Vyhraj ${LEVEL} bez použitia bômb", + "descriptionFullComplete": "Výhra v ${LEVEL} bez použitia bômb", + "name": "Pro Boxér" + }, + "Pro Football Shutout": { + "description": "Vyhraj bez toho aby protivník skóroval", + "descriptionComplete": "Výhra bez toho aby protivník skóroval", + "descriptionFull": "Vyhraj bez toho aby protivník skóroval v ${LEVEL}", + "descriptionFullComplete": "Výhra bez toho aby protivník skóroval v ${LEVEL}", + "name": "${LEVEL} Vypínak" + }, + "Pro Football Victory": { + "description": "Vyhraj zápas", + "descriptionComplete": "Vyhratý zápas", + "descriptionFull": "Vyhraj zápas v ${LEVEL}", + "descriptionFullComplete": "Vyhratý zápas v ${LEVEL}", + "name": "${LEVEL} Víťazstvo" + }, + "Pro Onslaught Victory": { + "description": "Poraz všetky vlny", + "descriptionComplete": "Porazené všetky vlny", + "descriptionFull": "Poraz všetky vlny v ${LEVEL}", + "descriptionFullComplete": "Porazené všetky vlny v ${LEVEL}", + "name": "${LEVEL} Víťazstvo" + }, + "Pro Runaround Victory": { + "description": "Dokonči všetky vlny", + "descriptionComplete": "Dokončené všetky vlny", + "descriptionFull": "Dokonči všetky vlny v ${LEVEL}", + "descriptionFullComplete": "Dokončené všetky vlny v ${LEVEL}", + "name": "${LEVEL} Víťazstvo" + }, + "Rookie Football Shutout": { + "description": "Vyhraj bez toho aby protivníci skórovali", + "descriptionComplete": "Výhra bez toho aby protivníci skórovali", + "descriptionFull": "Vyhraj ${LEVEL} bez toho aby protivníci skórovali", + "descriptionFullComplete": "Výhra v ${LEVEL} bez toho aby protivníci skórovali", + "name": "${LEVEL} Vypínak" + }, + "Rookie Football Victory": { + "description": "Vyhraj hru", + "descriptionComplete": "Vyhraná hra", + "descriptionFull": "Vyhraj hru v ${LEVEL}", + "descriptionFullComplete": "Vyhraná hra v ${LEVEL}", + "name": "${LEVEL} Víťazstvo" + }, + "Rookie Onslaught Victory": { + "description": "Poraz všetky vlny", + "descriptionComplete": "Porazené všetky vlny", + "descriptionFull": "Poraz všetky vlny v ${LEVEL}", + "descriptionFullComplete": "Porazené všetky vlny v ${LEVEL}", + "name": "${LEVEL} Víťazstvo" + }, + "Runaround God": { + "description": "Nahraj 2000 bodov", + "descriptionComplete": "Nahraných 2000 bodov", + "descriptionFull": "Nahraj 2000 bodov v ${LEVEL}", + "descriptionFullComplete": "Nahraných 2000 bodov v ${LEVEL}", + "name": "${LEVEL} Boh" + }, + "Runaround Master": { + "description": "Nahraj 500 bodov", + "descriptionComplete": "Nahraných 500 bodov", + "descriptionFull": "Nahraj 500 bodov v ${LEVEL}", + "descriptionFullComplete": "Nahraných 500 bodov v ${LEVEL}", + "name": "${LEVEL} Majster" + }, + "Runaround Wizard": { + "description": "Nahraj 1000 bodov", + "descriptionComplete": "Nahraných 1000 bodov", + "descriptionFull": "Nahraj 1000 bodov na ${LEVEL}", + "descriptionFullComplete": "Nahraných 1000 bodov na ${LEVEL}", + "name": "${LEVEL} Čarodej" + }, + "Sharing is Caring": { + "descriptionFull": "Zdieľaj hru s priateľom", + "descriptionFullComplete": "Zdieľaná hra s priateľom", + "name": "Hráč Plus" + }, + "Stayin' Alive": { + "description": "Vyhraj bez úmrtia", + "descriptionComplete": "Výhra bez úmrtia", + "descriptionFull": "Vyhraj ${LEVEL} bez úmrtia", + "descriptionFullComplete": "Výhra v ${LEVEL} bez úmrtia", + "name": "Stále Nažive" + }, + "Super Mega Punch": { + "description": "Spôsob 100% damageu jedným úderom", + "descriptionComplete": "Spôsobených 100% damageu jedným úderom", + "descriptionFull": "Spôsob 100% damageu jedným úderom v ${LEVEL}", + "descriptionFullComplete": "Spôsobených 100% damageu jedným úderom v ${LEVEL}", + "name": "Super Mega Úder" + }, + "Super Punch": { + "description": "Spôsob 50% damageu jedným úderom", + "descriptionComplete": "Spôsobených 50% damageu jedným úderom", + "descriptionFull": "Spôsob 50% damageu jedným úderom v ${LEVEL}", + "descriptionFullComplete": "Spôsobených 50% damageu jedným úderom v ${LEVEL}", + "name": "Super Úder" + }, + "TNT Terror": { + "description": "Zabi 6 protivníkov s TNT", + "descriptionComplete": "Zabitie 6 protivníkov s TNT", + "descriptionFull": "Zabi 6 protivníkov s TNT v ${LEVEL}", + "descriptionFullComplete": "Zabitie 6 protivníkov s TNT v ${LEVEL}", + "name": "Terorista s TNT" + }, + "Team Player": { + "descriptionFull": "Začnite hru na tímy s 4+ hráčmi", + "descriptionFullComplete": "Začatá hra na tímy s 4+ hráčmi", + "name": "Tímový Hráč" + }, + "The Great Wall": { + "description": "Zastav všetkých protivníkov", + "descriptionComplete": "Zastavenie všetkých protivníkov", + "descriptionFull": "Zastav všetkých protivníkov v ${LEVEL}", + "descriptionFullComplete": "Zastavenie všetkých protivníkov v ${LEVEL}", + "name": "Veľká Stena" + }, + "The Wall": { + "description": "Zastav všetkých protivníkov", + "descriptionComplete": "Zastavenie všetkých protivníkov", + "descriptionFull": "Zastav všetkých protivníkov v ${LEVEL}", + "descriptionFullComplete": "Zastavenie všetkých protivníkov v ${LEVEL}", + "name": "Stena" + }, + "Uber Football Shutout": { + "description": "Vyhraj bez toho aby protivníci skórovali", + "descriptionComplete": "Výhra bez toho aby protivníci skórovali", + "descriptionFull": "Vyhraj v ${LEVEL} bez toho aby protivníci skórovali", + "descriptionFullComplete": "Výhra v ${LEVEL} bez toho aby protivníci skórovali", + "name": "${LEVEL} Vypínak" + }, + "Uber Football Victory": { + "description": "Vyhraj hru", + "descriptionComplete": "Vyhraná hra", + "descriptionFull": "Vyhraj hru v ${LEVEL}", + "descriptionFullComplete": "Vyhraný hra v ${LEVEL}", + "name": "${LEVEL} Víťazstvo" + }, + "Uber Onslaught Victory": { + "description": "Poraz všetky vlny", + "descriptionComplete": "Porazenie všetkých vĺn", + "descriptionFull": "Poraz všetky vlny v ${LEVEL}", + "descriptionFullComplete": "Porazenie všetkých vĺn v ${LEVEL}", + "name": "${LEVEL} Víťazstvo" + }, + "Uber Runaround Victory": { + "description": "Dokonči všetky vlny", + "descriptionComplete": "Dokončené všetky vlny", + "descriptionFull": "Dokonči všetky vlny v ${LEVEL}", + "descriptionFullComplete": "Dokončené všetky vlny v ${LEVEL}", + "name": "${LEVEL} Víťazstvo" + } + }, + "achievementsRemainingText": "Zostávajúce Achievementy", + "achievementsText": "Achievementy", + "achievementsUnavailableForOldSeasonsText": "Prepáč, podrobnosti achievementov nie sú dostupné pre minulé sezóny.", + "addGameWindow": { + "getMoreGamesText": "Viac Hier...", + "titleText": "Pridať Hru" + }, + "allowText": "Povol", + "alreadySignedInText": "Tvoj účet je prihlásený z iného zariadenia;\nprosím prepni si účty alebo ukonči hru na\ntvojich ostatných zariadeniach a skús to znovu.", + "apiVersionErrorText": "Nemožno načítať modul ${NAME}; používa api-verziu ${VERSION_USED}; my potrebujeme ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Automatika\" toto povolí len keď sú slúchadlá zapojené)", + "headRelativeVRAudioText": "Head-Relative VR Audio", + "musicVolumeText": "Hlasitosť Hudby", + "soundVolumeText": "Hlasitosť Zvukov", + "soundtrackButtonText": "Soundtracky", + "soundtrackDescriptionText": "(nastav si vlastnú hudbu ktorá sa bude hrať počas hry)", + "titleText": "Audio" + }, + "autoText": "Auto", + "backText": "Späť", + "banThisPlayerText": "Zakážte prehrávač", + "bestOfFinalText": "Najlepší-z-${COUNT} Finále", + "bestOfSeriesText": "Najlepší z ${COUNT} sérií:", + "bestRankText": "Tvoj rekord je #${RANK}", + "bestRatingText": "Tvoje najlepšie hodnotenie je #${RATING}", + "bombBoldText": "BOMBA", + "bombText": "Bomba", + "boostText": "Pridať", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} je nastavené v aplikácii", + "buttonText": "tlačidlo", + "canWeDebugText": "Chcel by si, aby Bombsquad automaticky nahlasovalo\nbugy, crashe, a základné informácie o použití vývojárovi?\n\nToto data neobsahuje žiadne osobné informácie a pomáha\nnechávať hru bežať hladko a bez bugov.", + "cancelText": "Zrušiť", + "cantConfigureDeviceText": "Prepáč, ${DEVICE} nie je konfigurovateľný", + "challengeEndedText": "Táto challenge skončila.", + "chatMuteText": "Zablokovať Čet", + "chatMutedText": "Čet Zablokovaný", + "chatUnMuteText": "Odblokovať Čet", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Musíš dokončiť\ntento level pre postup!", + "completionBonusText": "Bonus za Dokončenie", + "configControllersWindow": { + "configureControllersText": "Prispôsobiť Ovládače", + "configureKeyboard2Text": "Prispôsobiť Klávesnicu P2", + "configureKeyboardText": "Prispôsobiť Klávesnicu", + "configureMobileText": "Mobil ako Ovládač", + "configureTouchText": "Prispôsobiť Obrazovku", + "ps3Text": "PS3 Ovládače", + "titleText": "Ovládače", + "wiimotesText": "Wiimotes", + "xbox360Text": "Xbox 360 Ovládače" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Poznámka: podpora ovládaču sa líši v závislosti od zariadenia a verzie Androidu.", + "pressAnyButtonText": "Stlač tlačidlo na ovládači,\nktoré chceš prispôsobiť...", + "titleText": "Prispôsobiť Ovládače" + }, + "configGamepadWindow": { + "advancedText": "Prokročilé", + "advancedTitleText": "Pokročilé Nastavenie Ovládača.", + "analogStickDeadZoneDescriptionText": "(zapni, pokiaľ tvoja postava \"driftuje\" keď pustíš joystick)", + "analogStickDeadZoneText": "Joystick Dead Zóna", + "appliesToAllText": "(aplikuje sa na všetky ovládače tohoto typu)", + "autoRecalibrateDescriptionText": "(zapni pokiaľ sa tvoja postava nehýbe v plnej rýchlosti)", + "autoRecalibrateText": "Auto-Recalibrovanie Joysticku", + "axisText": "os", + "clearText": "zmaž", + "dpadText": "dpad", + "extraStartButtonText": "Extra Štartovacie Tlačidlo", + "ifNothingHappensTryAnalogText": "Ak sa nič nestane, skús prideliť k joystickom)", + "ifNothingHappensTryDpadText": "Ak sa nič nestane, skús prideľiť k d-padu)", + "ignoreCompletelyDescriptionText": "(zamedz tento ovládač od ovplyvňovania hry alebo menu)", + "ignoreCompletelyText": "Kompletne Ignoruj", + "ignoredButton1Text": "Ignorované Tlačidlo 1", + "ignoredButton2Text": "Ignorované Tlačidlo 2", + "ignoredButton3Text": "Ignorované Tlačidlo 3", + "ignoredButton4Text": "Ignorované Tlačidlo 4", + "ignoredButtonDescriptionText": "(toto použi pre zabránenie \"home\" alebo \"sync\" tlačidiel od ovplyvnenia UI)", + "pressAnyAnalogTriggerText": "Stlač joystick...", + "pressAnyButtonOrDpadText": "Stlač tlačidlo alebo dpad...", + "pressAnyButtonText": "Stlač akékolvek tlačidlo.", + "pressLeftRightText": "Stlač vpravo alebo vlavo...", + "pressUpDownText": "Stlač hore alebo dole...", + "runButton1Text": "Beh Tlačidlo 1", + "runButton2Text": "Beh Tlačidlo 2", + "runTrigger1Text": "Beh Spúšťač 1", + "runTrigger2Text": "Beh Spúšťač 2", + "runTriggerDescriptionText": "(spúšťače ti dovoľujú behať v rôznych rýchlostiach)", + "secondHalfText": "Použi pre nastavenie druhej polovice\n2-v-1 zariadenia, ktoré sa javí \nako jedno zariadenie.", + "secondaryEnableText": "Povoliť", + "secondaryText": "Druhý Ovládač", + "startButtonActivatesDefaultDescriptionText": "(toto vypni ak je tvoje štartovacie tlačidlo skôr tlačidlo menu", + "startButtonActivatesDefaultText": "Štartovacie Tlačidlo Aktivuje Predvolený Prístroj", + "titleText": "Nastavenie Ovládača", + "twoInOneSetupText": "Nastavenia Ovládača 2-v-1", + "uiOnlyDescriptionText": "(zabrániť tomuto ovládaču pripojiť sa do hry)", + "uiOnlyText": "Limitovať Použitie Menu", + "unassignedButtonsRunText": "Všetky Nenastavené Tlačidla sú Beh", + "unsetText": "", + "vrReorientButtonText": "VR Reorientovacie Tlačidlo" + }, + "configKeyboardWindow": { + "configuringText": "Konfigurujem ${DEVICE}", + "keyboard2NoteText": "Poznámka: veľa klávesníc môžu registrovať iba nekoľko \nstlačení naraz, takže mať klávesnicu pre druhého hráča\nby malo fungovať lepšie pokiaľ je osobitná klávesnica\npre nich zapojená. Pamätaj že stále budeš musieť nastaviť\nklávesy pre dvoch hráčov." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Veľkosť Akčných Tlačidiel", + "actionsText": "Akcie", + "buttonsText": "tlačidlá", + "dragControlsText": "< ťahaj ovládače pre ich rozmiestnenie >", + "joystickText": "joystick", + "movementControlScaleText": "Veľkosť Panelu pre Pohyb", + "movementText": "Pohyb", + "resetText": "Reset", + "swipeControlsHiddenText": "Skryť Ťahacie ikony", + "swipeInfoText": "Na \"posúvací\" štýl ovládania si musíte trochu zvyknúť ale\nľahšie sa s nimi hrá lebo sa na ne nemusíte pozerať.", + "swipeText": "posúvanie", + "titleText": "Konfigurovať Obrazovku" + }, + "configureItNowText": "Konfigurovať teraz?", + "configureText": "Konfigurovať", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "Pre najlepšie výsledky budeš potrebovať bez-lagovú wifi. Môžeš\nzmenšiť wifi lagovanie tým že vypneš iné bezdrôtové zariadenia,\nhraním blízko wifi routera a pripojením sa na game host rovno\nna network cez ethernet.", + "explanationText": "Ak chceš používať smart-phone alebo tablet ako bezdrôtový \novládač, nainštaluj na ňom \"${REMOTE_APP_NAME}\" aplikáciu. \nHocijaký počet sa môže pripojiť do ${APP_NAME} hry cez Wi-Fi, zadarmo!", + "forAndroidText": "pre Android:", + "forIOSText": "pre iOS:", + "getItForText": "Zožeň ${REMOTE_APP_NAME} pre iOS na Apple App Store\nalebo pre Android na Google Play Store alebo Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Používanie Mobilov ako Ovládače:" + }, + "continuePurchaseText": "Pokračovať za ${PRICE}?", + "continueText": "Pokračovať", + "controlsText": "Ovládanie", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Toto sa neaplikuje do all-time rebríčku.", + "activenessInfoText": "Tento multiplier rastie na dňoch keď\nhráš a znižuje sa keď nehráš.", + "activityText": "Aktivita", + "campaignText": "Campaign", + "challengesInfoText": "Získavaj ceny za dokončovanie minihier.\n\nCeny a levely obtiažnosti rastú vždy\nkeď je challenge dokončená a znižuje\nkeď jedna vyprší alebo prepadne.", + "challengesText": "Challenge", + "currentBestText": "Rekord", + "customText": "Vlastné", + "entryFeeText": "Vstupné", + "forfeitConfirmText": "Vzdať sa tejto challenge?", + "forfeitNotAllowedYetText": "Tu sa ešte nemôžte vzdať.", + "forfeitText": "Vzdať sa", + "multipliersText": "Multipliere", + "nextChallengeText": "Ďalšia challenge", + "nextPlayText": "Ďalšia Hra", + "ofTotalTimeText": "z ${TOTAL}", + "playNowText": "Hrať", + "pointsText": "Body", + "powerRankingFinishedSeasonUnrankedText": "(neumiestnili ste sa v tejto sezóne)", + "powerRankingNotInTopText": "(nie ste v top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} bodov", + "powerRankingPointsMultText": "(x ${NUMBER} bodov)", + "powerRankingPointsText": "${NUMBER} bodov", + "powerRankingPointsToRankedText": "(${CURRENT} z ${REMAINING} bodov)", + "powerRankingText": "Umiestnenie", + "prizesText": "Ceny", + "proMultInfoText": "Hráči s ${PRO} vylepšením\ndostávajú ${PERCENT}% bodový boost.", + "seeMoreText": "Viac...", + "skipWaitText": "Preskočiť Čakanie", + "timeRemainingText": "Zostávajúci Čas", + "toRankedText": "Do Umiestnenia", + "totalText": "spolu", + "tournamentInfoText": "Súťažte o rekordy s ostatnými\nhráčmi v tvojej lige.\n\nCeny dostanú najlepší hráči keď\nturnaj skončí.", + "welcome1Text": "Vitaj v ${LEAGUE}. Môžeš zlepšovať svoj\nrank získavaním hviezd, plnením achievementov,\na vyhrávaním trofejí v turnajoch.", + "welcome2Text": "Taktiež môžeš získavať tikety z veľa aktivít. Tikety sa \nmôžu používať na odomknutie nových charakterov, máp, \nminihier, ako vstupné do turnajov, a viac.", + "yourPowerRankingText": "Tvoje Umiestnenie:" + }, + "copyOfText": "${NAME} Kópia", + "createEditPlayerText": "", + "createText": "Vytvoriť", + "creditsWindow": { + "additionalAudioArtIdeasText": "Dodatočné Audio, Predčasné Maľby a Nápady sú od ${NAME}", + "additionalMusicFromText": "Dodatočná hudba od ${NAME}", + "allMyFamilyText": "Všetci kamaráti a rodina ktorá pomohla hrať test", + "codingGraphicsAudioText": "Kódovanie, Grafika a Audio sú od ${NAME}", + "languageTranslationsText": "Preklad:", + "legalText": "Legál:", + "publicDomainMusicViaText": "Public-domain hudba cez ${NAME}", + "softwareBasedOnText": "Tento softvér je založený z časti na práci ${NAME}", + "songCreditText": "${TITLE} Performed by ${PERFORMER}\nComposed by ${COMPOSER}, Arranged by ${ARRANGER}, Published by ${PUBLISHER},\nCourtesy of ${SOURCE}", + "soundAndMusicText": "Zvuk & Hudba:", + "soundsText": "Zvuky (${SOURCE}):", + "specialThanksText": "Špeciálna Vďaka:", + "thanksEspeciallyToText": "Vďaka hlavne ${NAME}", + "titleText": "${APP_NAME} Credits", + "whoeverInventedCoffeeText": "Kto vymyslel kávu" + }, + "currentStandingText": "Tvoje postavenie je #${RANK}", + "customizeText": "Upraviť...", + "deathsTallyText": "${COUNT} úmrtí", + "deathsText": "Úmrtia", + "debugText": "debug", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Poznámka: je odporúčané aby si nastavil Nastavenia->Grafika->Textúry na \"Vysoké\" pri tomto testovaní.", + "runCPUBenchmarkText": "Spustiť CPU Benchmark", + "runGPUBenchmarkText": "Spustiť GPU Benchmark", + "runMediaReloadBenchmarkText": "Spustiť Media-Reload Benchmark", + "runStressTestText": "Spustiť stres test", + "stressTestPlayerCountText": "Počet Hráčov", + "stressTestPlaylistDescriptionText": "Stres Test Playlist", + "stressTestPlaylistNameText": "Meno Playlistu", + "stressTestPlaylistTypeText": "Typ Playlistu", + "stressTestRoundDurationText": "Dĺžka Kola", + "stressTestTitleText": "Stres Test", + "titleText": "Benchmarky & Atres Tasty", + "totalReloadTimeText": "Reload Time Spolu: ${TIME} (pozri zápis pre detaily)" + }, + "defaultGameListNameText": "Predvolený ${PLAYMODE} Playlist", + "defaultNewGameListNameText": "Môj ${PLAYMODE} Playlist", + "deleteText": "Vymazať", + "demoText": "Demo", + "denyText": "odmietnuť", + "desktopResText": "Desktop Res", + "difficultyEasyText": "Jednoduchá", + "difficultyHardOnlyText": "Len Ťažký Mód", + "difficultyHardText": "Ťažké", + "difficultyHardUnlockOnlyText": "Tento level môže byť odomknutý len v ťažkom móde.\nMyslíš že máš čo ti na to treba!?!?!", + "directBrowserToURLText": "Prosím zadajte do prehliadača následujúcu URL:", + "disableRemoteAppConnectionsText": "Zakázať Remote-App Pripojenia", + "disableXInputDescriptionText": "Povolí viac ako 4 ovládače ale nemusí fungovať dobre.", + "disableXInputText": "Zakázať XInput", + "doneText": "Hotovo", + "drawText": "Remíza", + "duplicateText": "Duplikovať", + "editGameListWindow": { + "addGameText": "Pridať\nHru", + "cantOverwriteDefaultText": "Nemožno prepísať štandartný playlist!", + "cantSaveAlreadyExistsText": "Playlist s takým menom už existuje!", + "cantSaveEmptyListText": "Nemožno uložiť prázdny playlist!", + "editGameText": "Upraviť\nHru", + "listNameText": "Meno Playlistu", + "nameText": "Meno", + "removeGameText": "Odstrániť\nHru", + "saveText": "Uložiť List", + "titleText": "Playlist Editor" + }, + "editProfileWindow": { + "accountProfileInfoText": "Tento špeciálny profil má meno\na ikonu založenú na tvojom účte.\n\n${ICONS}\n\nVytvor si vlastný profil aby si\nmohol použiť iné mená a vlastné icony", + "accountProfileText": "(profil účtu)", + "availableText": "Meno \"${NAME}\" je dostupné.", + "characterText": "charakter", + "checkingAvailabilityText": "Kontrolujem dostupnosť pre \"${NAME}\"...", + "colorText": "farba", + "getMoreCharactersText": "Získať Viac Charakterov...", + "getMoreIconsText": "Získať Viac Ikon...", + "globalProfileInfoText": "Globálne profily majú jedinečné celosvetové názvy.\nVlastnia tiež vlastné ikony.", + "globalProfileText": "(globálny profil)", + "highlightText": "zvýraznenie", + "iconText": "ikona", + "localProfileInfoText": "Lokálne profily nemajú ikonu a ich mená nemajú garanciu\njedinečného mena. Upgradni profil na globálny aby ste\nsi vyhradili jedinečné meno a vlastnú ikonu.", + "localProfileText": "(lokálny profil)", + "nameDescriptionText": "Meno Hráča", + "nameText": "Meno", + "randomText": "náhodne", + "titleEditText": "Upraviť Profil", + "titleNewText": "Nový Profil", + "unavailableText": "\"${NAME}\" nie je dostupné; skús iné meno.", + "upgradeProfileInfoText": "Toto vyhradí tvoje meno hráča celosvetovo a \ndovolí ti priradiť k nemu vlastnú ikonu.", + "upgradeToGlobalProfileText": "Upgradenuť na Globálny Profil" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Nemôžeš vymazať štandartný soundtrack.", + "cantEditDefaultText": "Nemožno upraviť štandartný soundtrack. Duplikuj ho alebo vytvor nový.", + "cantOverwriteDefaultText": "Nemožno prepísať štandartný soundtrack.", + "cantSaveAlreadyExistsText": "Soundtrack s takým menom už existuje!", + "defaultGameMusicText": "<štandartná hudba hry>", + "defaultSoundtrackNameText": "Štandartný Soundtrack", + "deleteConfirmText": "Vymazať Soundtrack:\n\n\"${NAME}\"?", + "deleteText": "Vymazať\nSoundtrack", + "duplicateText": "Duplikovať\nSoundtrack", + "editSoundtrackText": "Soundtrack Editor", + "editText": "Upraviť\nSoundtrack", + "fetchingITunesText": "načítavam Music App playlisty...", + "musicVolumeZeroWarning": "Varovanie: hlasitosť hudby je nastavená na 0", + "nameText": "Meno", + "newSoundtrackNameText": "Môj Soundtrack ${COUNT}", + "newSoundtrackText": "Nový Soundtrack:", + "newText": "Nový\nSoundtrack", + "selectAPlaylistText": "Vybrať Playlist", + "selectASourceText": "Zdroj Hudby", + "testText": "test", + "titleText": "Soundtracky", + "useDefaultGameMusicText": "Štandartná Hudba Hry", + "useITunesPlaylistText": "Music App Playlist", + "useMusicFileText": "Súbor Hudby (mp3, atď)", + "useMusicFolderText": "Zložka Súborov Hudby" + }, + "editText": "Upraviť", + "endText": "Ukončiť", + "enjoyText": "Užite si to!", + "epicDescriptionFilterText": "${DESCRIPTION} Spomalene.", + "epicNameFilterText": "Epic ${NAME}", + "errorAccessDeniedText": "prístup odmietnutý", + "errorOutOfDiskSpaceText": "žiadne miesto na disku", + "errorText": "Error", + "errorUnknownText": "neznámy error", + "exitGameText": "Ukončiť ${APP_NAME}?", + "exportSuccessText": "\"${NAME}\" exportovaný.", + "externalStorageText": "Externé Úložisko", + "failText": "Fail", + "fatalErrorText": "Ups; niečo chýba alebo je niečo rozbité.\nProsím skús reinštalovať aplikáciu\nalebo kontaktuj ${EMAIL} pre pomoc.", + "fileSelectorWindow": { + "titleFileFolderText": "Vyber Súbor alebo Zložku", + "titleFileText": "Vybrať Súbor", + "titleFolderText": "Vybrať Zložku", + "useThisFolderButtonText": "Použiť Túto Zložku" + }, + "finalScoreText": "Finálne Skóre", + "finalScoresText": "Finálne Skóre", + "finalTimeText": "Finálny Čas", + "finishingInstallText": "Dokončujem inštaláciu; moment...", + "fireTVRemoteWarningText": "* pre lepšiu skúsenosť, použi\nHerné Ovládače alebo nainštaluj\n\"${REMOTE_APP_NAME}\" aplikáciu\nna mobily alebo tablety.", + "firstToFinalText": "Prvý-u-${COUNT} Finále", + "firstToSeriesText": "Prvý-u-${COUNT} Séria", + "fiveKillText": "PENTAKILL!!!", + "flawlessWaveText": "Bezchybná vlna!", + "fourKillText": "QUADKILL!!", + "friendScoresUnavailableText": "Skóre kamarátov nedostupné.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Leadery Hry ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "Nemôžeš vymazať štandartný playlist.", + "cantEditDefaultText": "Nemôžeš upraviť štandartný playlist! Duplikuj ho alebo si vytvor nový.", + "cantShareDefaultText": "Nemôžeš zdieľať štandartný playlist", + "deleteConfirmText": "Vymazať \"${LIST}\"?", + "deleteText": "Vymazať\nPlaylist", + "duplicateText": "Duplikovať\nPlaylist", + "editText": "Upraviť\nPlaylist", + "newText": "Nový\nPlaylist", + "showTutorialText": "Ukázať Tutoriál", + "shuffleGameOrderText": "Náhodné Poradie Hier", + "titleText": "Upraviť ${TYPE} Playlisty" + }, + "gameSettingsWindow": { + "addGameText": "Pridať Hru" + }, + "gamesToText": "${WINCOUNT} hier ku ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Pamätaj: hocijaké zariadenie v párty môže mať\nviac hráčov pokiaľ máš dosť ovládačov.", + "aboutDescriptionText": "Použi tieto tabuľky pre vytvorenie párty.\n\nPárty ti dovoľujú hrať hry a turnaje s\ntvojimi kamarátmi cez iné zariadenia.\n\nStlač ${PARTY} tlačidlo v pravo hore pre\nchatovanie a narábanie s párty.\n(na ovládači, stlač ${BUTTON} keď si v menu)", + "aboutText": "Informácie", + "addressFetchErrorText": "", + "appInviteMessageText": "${NAME} ti poslal ${COUNT} tiketov v hre ${APP_NAME}", + "appInviteSendACodeText": "Poslať Im Kód", + "appInviteTitleText": "${APP_NAME} Pozvánka", + "bluetoothAndroidSupportText": "(funguje na všetkých Android zariadeniach podporujúce Bluetooth)", + "bluetoothDescriptionText": "Hostovať/Pripojiť sa na párty cez Bluetooth", + "bluetoothHostText": "Hostovať párty cez Bluetooth", + "bluetoothJoinText": "Pripojiť sa cez Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "kontrolujem...", + "dedicatedServerInfoText": "Pre najlepšie výsledky, nastav si dedikovaný server. Pozri bombsquadgame.com/server a nauč sa ako.", + "disconnectClientsText": "Toto odpojí ${COUNT} hráča/hráčov v tvojej\npárty. Si si istý?", + "earnTicketsForRecommendingAmountText": "Kamaráti dostanú ${COUNT} tiketov a si hru vyskúšajú (a\nza každého kto tak urobí dostaneš ${YOU_COUNT} tiketov)", + "earnTicketsForRecommendingText": "Zdieľaj hru pre\ntikety zadarmo...", + "emailItText": "Poslať Emailom", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} tiketov od ${NAME}", + "friendPromoCodeAwardText": "Dostaneš ${COUNT} tiketov každý raz keď sa použie.", + "friendPromoCodeExpireText": "Kód vyprší za ${EXPIRE_HOURS} hodín a funguje len pre nové účty.", + "friendPromoCodeInstructionsText": "Ak ho chceš použiť, otvor ${APP_NAME} a choď do \"Settings->Advanced->Enter Code\".\nPozri bombsquadgame.com pre download linky pre všetky podporované platformy.", + "friendPromoCodeRedeemLongText": "Môže byť uplatnený za ${COUNT} tiketov až pre ${MAX_USES} ľudí.", + "friendPromoCodeRedeemShortText": "Môže byť uplatnený za ${COUNT} tiketov v hre.", + "friendPromoCodeWhereToEnterText": "(v \"Settings->Advanced->Enter Code\")", + "getFriendInviteCodeText": "Zohnať Pozvánku", + "googlePlayDescriptionText": "Pozvi Google Play hráčov do párty:", + "googlePlayInviteText": "Pozvať", + "googlePlayReInviteText": "Je tu ${COUNT} Google Play hráč(ov) v tvojej párty\nktorí budú odpojený ak pošleš novú pozvánku.\nPošli im novú aby si ich dostal späť.", + "googlePlaySeeInvitesText": "Pozrieť Pozvánky", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play verzia)", + "hostPublicPartyDescriptionText": "Hostovať Verejnú Párty:", + "inDevelopmentWarningText": "Poznámka:\n\nHra cez internet je nová a stále vylepšujúca sa\nvlastnosť. Zatiaľ, je vysoko odporúčané aby\nvšetci hráči boli na tej istej Wi-Fi.", + "internetText": "Internet", + "inviteAFriendText": "Priatelia nemajú hru? Pozvi ich nech si hru\nvyskúšajú a dostanú ${COUNT} tiketov.", + "inviteFriendsText": "Pozvať Kamarátov", + "joinPublicPartyDescriptionText": "Pripojiť sa na Verejnú Párty:", + "localNetworkDescriptionText": "Pripojiť sa na párty na tvojom internete:", + "localNetworkText": "Lokálny Internet", + "makePartyPrivateText": "Urobiť Moju Párty Verejnú", + "makePartyPublicText": "Urobiť Moju Párty Verejnú", + "manualAddressText": "Adresa", + "manualConnectText": "Pripojiť", + "manualDescriptionText": "Pripojiť sa na párty cez adresu:", + "manualJoinableFromInternetText": "Si pripojiteľný z internetu?:", + "manualJoinableNoWithAsteriskText": "NIE*", + "manualJoinableYesText": "ÁNO", + "manualRouterForwardingText": "*pre opravu, skús konfigurovať tvoj router na predný UDP port ${PORT} na svoju adresu", + "manualText": "Manuálne", + "manualYourAddressFromInternetText": "Tvoja adresa z internetu:", + "manualYourLocalAddressText": "Tvoja lokálna adresa:", + "noConnectionText": "<žiadne pripojenie>", + "otherVersionsText": "(ostatné verzie)", + "partyInviteAcceptText": "Potvrdiť", + "partyInviteDeclineText": "Odmietnuť", + "partyInviteGooglePlayExtraText": "(pozri \"Google Play\" tabuľku v \"Viac Hráčov\" okne)", + "partyInviteIgnoreText": "Ignorovať", + "partyInviteText": "${NAME} ťa pozval \ndo jeho/jej párty!", + "partyNameText": "Meno Párty", + "partySizeText": "veľkosť párty", + "partyStatusCheckingText": "kontrolujem status...", + "partyStatusJoinableText": "tvoja párty je teraz pripojiteľná z internetu", + "partyStatusNoConnectionText": "nemožno sa pripojiť na server", + "partyStatusNotJoinableText": "tvoja párty nie je pripojiteľná z internetu", + "partyStatusNotPublicText": "tvoja párty nie je verejná", + "pingText": "ping", + "portText": "Port", + "requestingAPromoCodeText": "Získavam kód...", + "sendDirectInvitesText": "Poslať Pozvánku", + "shareThisCodeWithFriendsText": "Zdieľaj tento kód s kamarátmi:", + "showMyAddressText": "Ukáž Moju Adresu", + "titleText": "Viac Hráčov", + "wifiDirectDescriptionBottomText": "Ak všetky zariadenia majú \"Wi-Fi Direct\" tabuľku, mali by byť schopní ho použiť\naby sa našli a pripojili medzi sebou. Keď budú všetky zariadenia pripojené,\nmôžeš formovať párty použitím tabuľky \"Lokálny Internet\" tak ako pri normálnej Wi-Fi.\n\nPre najlepšie výsledky, Wi-Fi Direct host by mal tiež byť ${APP_NAME} párty host.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct môže byť použitý na pripojenie Android zariadení bez\npoužitia Wi-Fi. Toto môže fungovať najlepšie na Androit 4.2 alebo novšom.\n\nPre jeho použitie, otvor Wi-Fi nastavenia a hľadaj \"Wi-Fi Direct\" v menu.", + "wifiDirectOpenWiFiSettingsText": "Otvoriť Wi-Fi Nastavenia", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(funguje medzi všetkými platformami)", + "worksWithGooglePlayDevicesText": "(funguje so zariadeniami, na ktorých beží Google Play (Android) verzia hry)", + "youHaveBeenSentAPromoCodeText": "Dostali ste ${APP_NAME} promo kód:" + }, + "getTicketsWindow": { + "freeText": "ZADARMO!", + "freeTicketsText": "Tikety Zadarmo", + "inProgressText": "Prebieha transakcia; prosím skús to znova neskôr", + "purchasesRestoredText": "Nákupy obnovené.", + "receivedTicketsText": "Dostal si ${COUNT} tiketov!", + "restorePurchasesText": "Obnoviť Nákupy", + "ticketPack1Text": "Malý Balíček Tiketov", + "ticketPack2Text": "Stredný Balíček Tiketov", + "ticketPack3Text": "Veľký Balíček Tiketov", + "ticketPack4Text": "Obrovský Balíček Tiketov", + "ticketPack5Text": "Gigantický Balíček Tiketov", + "ticketPack6Text": "Ultimátny Balíček Tiketov", + "ticketsFromASponsorText": "Dostaň ${COUNT} tiketov\nod sponzora", + "ticketsText": "${COUNT} Tiketov", + "titleText": "Dostať Tikety", + "unavailableLinkAccountText": "Prepáč, nákupy sú nedostupné na tejto platforme.\nAko riešenie, môžeš stále prepojiť tento účet s účtom na \ninej platforme a nakupovať.", + "unavailableTemporarilyText": "Toto aktuálne nie je dostupné; prosím skús to znova neskôr.", + "unavailableText": "Prepáč, toto nie je dostupné.", + "versionTooOldText": "Prepáč, táto verzia hry je moc stará; prosím nainštaluj novšiu verziu.", + "youHaveShortText": "máš ${COUNT}", + "youHaveText": "máš ${COUNT} tiketov" + }, + "googleMultiplayerDiscontinuedText": "Prepáč, Google multiplayer už viac nie je dostupný.\nSnažím sa to prehodiť čo najskôr. Dovtedy prosím\nskús inú metódu pripojenia.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Stále", + "fullScreenCmdText": "Celá Obrazovka (Cmd+F)", + "fullScreenCtrlText": "Celá Obrazovka (Ctrl+F)", + "gammaText": "Gamma", + "highText": "Vysoko", + "higherText": "Vyššie", + "lowText": "Nízko", + "mediumText": "Stredne", + "neverText": "Nikdy", + "resolutionText": "Rozlíšenie", + "showFPSText": "Ukazuj FPS", + "texturesText": "Textúry", + "titleText": "Grafika", + "tvBorderText": "TV Okraj", + "verticalSyncText": "Vertikálna Synchronizácia", + "visualsText": "Vizualizácia" + }, + "helpWindow": { + "bombInfoText": "- Bomba -\nSilnejšia ako údery, ale\nmôže spôsobiť vážne zranenie.\nPre najlepšie výsledky ju hoď\nna nepriateľa skôr ako vybúchne.", + "canHelpText": "${APP_NAME} môže pomôcť.", + "controllersInfoText": "Môžeš hrať ${APP_NAME} s kamarátmi cez internet, alebo môžete\nvšetci hrať na jednom zariadení pokiaľ máte dosť ovládačov.\n${APP_NAME} podporuje veľa z nich; môžeš pokojne použiť mobily\nako ovládače cez \"${REMOTE_APP_NAME}\" aplikáciu.\nPozri Nastavenia->Ovládače pre viac info.", + "controllersText": "Ovládače", + "controlsSubtitleText": "Tvoj ${APP_NAME} charakter má pár základných zručností:", + "controlsText": "Ovládanie", + "devicesInfoText": "VR verzia hry ${APP_NAME} sa môže hrať cez internet s \nnormálnou verziou, takže bež pre ďalšie mobily, tablety \na počítače a sťahuj hru. To môže byť užitočné pre pripojenie \nnormálnej verzie hry k VR verzii len pre to aby mohli ľudia \nvon sledovať akciu.", + "devicesText": "Zariadenia", + "friendsGoodText": "Tých je dobre mať. ${APP_NAME} je hlavne zábava s trocha \nhráčmi a môžeš hrať až s 8 hráčmi naraz, čo nás privádza k:", + "friendsText": "Kamaráti", + "jumpInfoText": "- Skok -\nSkáč aby si preskočil malé\nškáry alebo aby si vyhodil\nveci vyššie a zlepšil náladu.", + "orPunchingSomethingText": "Alebo mlátiť do niečoho, zhodiť dačo z útesu, a odpáliť to na ceste dole sticky bombou.", + "pickUpInfoText": "- Zobrať -\nZober vlajky, protivníkov, alebo\nhocičo čo nie je pripevnené k zemi.\nStlač znova a odhoď to.", + "powerupBombDescriptionText": "Dovolí ti vyhodiť tri bomby\nza sebou namiesto jednej.", + "powerupBombNameText": "Trojité-Bomby", + "powerupCurseDescriptionText": "Tomuto by si sa chcel radšej vyhnúť.\n... či?", + "powerupCurseNameText": "Zošalenie", + "powerupHealthDescriptionText": "Dá ti plné životy.\nTo si neočakával.", + "powerupHealthNameText": "Lekárnička", + "powerupIceBombsDescriptionText": "Slabšie ako normálne bomby\nale nechajú protivníkov zamrznutých\na krehkých.", + "powerupIceBombsNameText": "Ľadové Bomby", + "powerupImpactBombsDescriptionText": "Trocha slabšie ako normálne\nbomby, ale vybúchnu na dotyk.", + "powerupImpactBombsNameText": "Totykové Bomby", + "powerupLandMinesDescriptionText": "Tieto chodia dostaneš 3;\nUžitočné pre ochranu územia\nalebo zastavenie rýchlych protivníkov", + "powerupLandMinesNameText": "Míny", + "powerupPunchDescriptionText": "Robia tvoje údery ťažšie,\nrýchlejšie, lepšie, silnejšie.", + "powerupPunchNameText": "Boxérske Rukavice", + "powerupShieldDescriptionText": "Absorbuje trocha bolesti\ntakže ty nemusíš.", + "powerupShieldNameText": "Obrana", + "powerupStickyBombsDescriptionText": "Zalepia sa o hocičo čo trafia.\nNasleduje veselosť.", + "powerupStickyBombsNameText": "Sticky Bomby", + "powerupsSubtitleText": "Samozrejme, žiadna hra nie je hrou bez schopností:", + "powerupsText": "Schopnosti", + "punchInfoText": "- Úder -\nÚdery bolia viac keď sa tvoje\npäste hýbu, takže sa krúť a skáč\na bež ako šialenec.", + "runInfoText": "- Beh -\nAk chceš behať, podrž HOCIJAKÉ tlačidlo. Dobre fungujú aj bočné tlačidlá pokiaľ ich máš.\nBehom sa dostaneš na miesta rýchlejšie, ale budeš ťažšie zatáčať, takže bacha na útesy.", + "someDaysText": "Niektoré dni by si chcel do dačoho mlátiť. Alebo to odpáliť.", + "titleText": "${APP_NAME} Pomoc", + "toGetTheMostText": "Ak chceš z tejto hry vyšťaviť najviac, potrebuješ:", + "welcomeText": "Vitaj v hre ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} je teraz tvoj boss -", + "importPlaylistCodeInstructionsText": "Použi tento kód aby si mohol importovať tento playlist inam:", + "importPlaylistSuccessText": "Importovaný ${TYPE} playlist \"${NAME}\"", + "importText": "Importovať", + "importingText": "Importujem...", + "inGameClippedNameText": "v hre to bude\n\"${NAME}\"", + "installDiskSpaceErrorText": "ERROR: Nemožno dokončiť inštaláciu.\nAsi budeš mať málo miesta na úložisku.\nTrocha ho vyčisti a skús to znova.", + "internal": { + "arrowsToExitListText": "stlač ${LEFT} alebo ${RIGHT} pre zatvorenie listu", + "buttonText": "tlačidlo", + "cantKickHostError": "Nemôžeš odstrániť bossa.", + "chatBlockedText": "${NAME} má blokovaný chat na ${TIME} sekúnd.", + "connectedToGameText": "Pripojil si sa na \"${NAME}\"", + "connectedToPartyText": "Pripojil si sa párty hráča ${NAME}!", + "connectingToPartyText": "Pripájam sa...", + "connectionFailedHostAlreadyInPartyText": "Nemožno sa pripojiť; hráč je v inej párty.", + "connectionFailedPartyFullText": "Nemožno sa pripojiť; párty je plná.", + "connectionFailedText": "Nemožno sa pripojiť.", + "connectionFailedVersionMismatchText": "Nemožno sa pripojiť; hráč má spustenú inú verziu hry.\nUistite sa že máte obidvaja najnovšiu verziu hry a skús to znova.", + "connectionRejectedText": "Nemožno sa pripojiť.", + "controllerConnectedText": "${CONTROLLER} je pripojený.", + "controllerDetectedText": "Detekovaný 1 ovládač", + "controllerDisconnectedText": "${CONTROLLER} je odpojený.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} je odpojený. Skúste ho pripojiť znova.", + "controllerForMenusOnlyText": "Tento ovládač sa nepoužíva na hranie; len pre navigovanie menu.", + "controllerReconnectedText": "${CONTROLLER} je znova pripojený.", + "controllersConnectedText": "${COUNT} pripojených ovládačov.", + "controllersDetectedText": "${COUNT} ovládačov detekovaných.", + "controllersDisconnectedText": "${COUNT} ovládačov odpojených.", + "corruptFileText": "Rozbitý súbor detekovaný. Prosím skús reinštalovať hru alebo napíš na ${EMAIL}.", + "errorPlayingMusicText": "Error pri prehrávaní hudby: ${MUSIC}", + "errorResettingAchievementsText": "Nemožno resetovať online achievementy; prosím skús to neskôr.", + "hasMenuControlText": "${NAME} má kontrolu nad menu.", + "incompatibleNewerVersionHostText": "Hráč má novšiu verziu hry. Nainštaluj najnovšiu\nverziu hry a skús to neskôr.", + "incompatibleVersionHostText": "Hráč má inakšiu verziu hry. Uistite sa že máte\nobidvaja najnovšiu verziu hry skús to znova.", + "incompatibleVersionPlayerText": "${NAME} má inakšiu verziu hry. Uistite sa že máte\nobidvaja najnovšiu verziu hry a skús to znova.", + "invalidAddressErrorText": "Error: neplatná/zlá adresa.", + "invalidNameErrorText": "Error: neplatné meno.", + "invalidPortErrorText": "Error: neplatný port.", + "invitationSentText": "Pozvánka poslaná.", + "invitationsSentText": "${COUNT} pozvánok poslaných.", + "joinedPartyInstructionsText": "Niekto sa pripojil do tvojej párty.\nChoď do \"Hrať\" a začni hru.", + "keyboardText": "Klávesnica", + "kickIdlePlayersKickedText": "Vyhadzujem ${NAME} kvôli nečinnosti.", + "kickIdlePlayersWarning1Text": "${NAME} bude vyhodený za ${COUNT} sekúnd ak bude stále nečinný.", + "kickIdlePlayersWarning2Text": "(toto môžeš vypnúť v Nastavenia->Pokročilé", + "leftGameText": "\"${NAME}\" odišiel.", + "leftPartyText": "Odišiel si z párty hráča ${NAME}.", + "noMusicFilesInFolderText": "Zložka neobsahuje žiadne súbory.", + "playerJoinedPartyText": "${NAME} sa pripojil do párty!", + "playerLeftPartyText": "${NAME} odišiel z párty.", + "rejectingInviteAlreadyInPartyText": "Ruším pozvánku (hráč je už v párty).", + "serverRestartingText": "REŠTARUJEM. Prosím pripoj sa o chvíľu...", + "serverShuttingDownText": "Server sa vypína...", + "signInErrorText": "Error pri prihlasovaní.", + "signInNoConnectionText": "Nemožno sa prihlásiť. (žiadny internet?)", + "telnetAccessDeniedText": "ERROR: účet nemá telnet povolenie.", + "timeOutText": "(vyprší za ${TIME} sekúnd)", + "touchScreenJoinWarningText": "Pripojil si sa na obrazovke.\nAk to bolo nechtiac, stlač na obrazovke Menu->Opustiť Hru.", + "touchScreenText": "Obrazovka", + "unableToResolveHostText": "Error: žiadny internet/zlé host meno.", + "unavailableNoConnectionText": "Toto nie je aktuálne dostupné (žiadny internet?)", + "vrOrientationResetCardboardText": "Toto použi pre resetovanie orientácie VR.\nAk chceš hrať, potrebuješ ovládač.", + "vrOrientationResetText": "Orientácia VR resetovaná.", + "willTimeOutText": "(ak bude nečinný, vyprší čas)" + }, + "jumpBoldText": "SKOČIŤ", + "jumpText": "Skočiť", + "keepText": "Ponechať", + "keepTheseSettingsText": "Ponechať tieto nastavenia?", + "kickOccurredText": "${NAME} bol vyhodený.", + "kickQuestionText": "Vyhodiť ${NAME}?", + "kickText": "Vyhodiť", + "kickVoteFailedNotEnoughVotersText": "Nie je dostatok hráčov pre hlasovanie.", + "kickVoteFailedText": "Hlasovanie pre vyhodenie zlyhalo.", + "kickVoteStartedText": "Hlasovanie pre vyhodenie začalo pre ${NAME}.", + "kickVoteText": "Hlasovať pre Vyhodenie", + "kickWithChatText": "Napíš ${YES} pre áno a ${NO} pre nie do chatu.", + "killsTallyText": "${COUNT} zabití.", + "killsText": "Zabitia", + "kioskWindow": { + "easyText": "Ľahké", + "epicModeText": "Spomalene", + "fullMenuText": "Celé Menu", + "hardText": "Ťažké", + "mediumText": "Stredné", + "singlePlayerExamplesText": "Jeden Hráč / Proti Počítaču Príklady", + "versusExamplesText": "Versus Príklady" + }, + "languageSetText": "Aktuálny jazyk je \"${LANGUAGE}\".", + "lapNumberText": "Kolo ${CURRENT}/${TOTAL}", + "lastGamesText": "(posledných ${COUNT} hier)", + "leaderboardsText": "Rebríčky", + "league": { + "allTimeText": "Celý Čas", + "currentSeasonText": "Aktuálna Sezóna (${NUMBER})", + "leagueFullText": "${NAME} Liga", + "leagueRankText": "Miesto v Lige", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga${SUFFIX}", + "seasonEndedDaysAgoText": "Sezóna skončila pred ${NUMBER} dňami.", + "seasonEndsDaysText": "Sezóna skončí za ${NUMBER} dní.", + "seasonEndsHoursText": "Sezóna skončí za ${NUMBER} hodín.", + "seasonEndsMinutesText": "Sezóna skončí za ${NUMBER} minút.", + "seasonText": "Sezóna ${NUMBER}", + "tournamentLeagueText": "Musíš dosiahnuť ${NAME} ligu aby si mohol hrať tento turnaj.", + "trophyCountsResetText": "Trofeje sa resetujú ďalšiu sezónu." + }, + "levelBestScoresText": "Najlepšie skóre na ${LEVEL}", + "levelBestTimesText": "Najlepšie časy na ${LEVEL}", + "levelIsLockedText": "${LEVEL} je zamknutý.", + "levelMustBeCompletedFirstText": "Najskôr musíš dokončiť ${LEVEL}.", + "levelText": "Level ${NUMBER}", + "levelUnlockedText": "Level Odomknutý.", + "livesBonusText": "Bonus za životy.", + "loadingText": "načítavam", + "loadingTryAgainText": "Načítavam; skús to znova za chvíľu...", + "macControllerSubsystemBothText": "Obidva (neodporúčané)", + "macControllerSubsystemClassicText": "Klasické", + "macControllerSubsystemDescriptionText": "(toto skús zmeniť ak ti ovládače nefungujú)", + "macControllerSubsystemMFiNoteText": "Detekovaný ovládač pre iOS/Mac;\nAsi ich budeš chcieť povoliť v Nastavenia -> Ovládače", + "macControllerSubsystemMFiText": "Vyrobené pre iOS/Mac", + "macControllerSubsystemTitleText": "Podporovanie Ovládačov", + "mainMenu": { + "creditsText": "Credits", + "demoMenuText": "Demo Menu", + "endGameText": "Ukončiť Hru", + "exitGameText": "Uzavrieť Hru", + "exitToMenuText": "Odísť do menu?", + "howToPlayText": "Ako Hrať", + "justPlayerText": "(Len ${NAME})", + "leaveGameText": "Opustiť Hru", + "leavePartyConfirmText": "Vážne chceš opustiť hru?", + "leavePartyText": "Opustiť Párty", + "quitText": "Ukončiť", + "resumeText": "Pokračovať", + "settingsText": "Nastavenia" + }, + "makeItSoText": "Aplikuj", + "mapSelectGetMoreMapsText": "Zohnať Viac Máp...", + "mapSelectText": "Vybrať...", + "mapSelectTitleText": "${GAME} Mapy", + "mapText": "Mapa", + "maxConnectionsText": "Maximum Pripojení", + "maxPartySizeText": "Maximálna Veľkosť Párty", + "maxPlayersText": "Maximum Hráčov", + "mostValuablePlayerText": "Najcennejší Hráč", + "mostViolatedPlayerText": "Najzomierajúcejší Hráč", + "mostViolentPlayerText": "Najvražednejší Hráč", + "moveText": "Pohyb", + "multiKillText": "${COUNT}-KILL!!!", + "multiPlayerCountText": "${COUNT} hráčov", + "mustInviteFriendsText": "Poznámka: musíš pozvať kamarátov\nv \"${GATHER}\" paneli alebo zapojiť\novládače ak chceš hrať multiplayer.", + "nameBetrayedText": "${NAME} zradil ${VICTIM}.", + "nameDiedText": "${NAME} zomrel.", + "nameKilledText": "${NAME} zabil ${VICTIM}.", + "nameNotEmptyText": "Meno nemôže byť prázdne!", + "nameScoresText": "${NAME} Skóruje!", + "nameSuicideKidFriendlyText": "${NAME} nechtiac zomrel.", + "nameSuicideText": "${NAME} spáchal samovraždu.", + "nameText": "Meno", + "nativeText": "Prírodné", + "newPersonalBestText": "Nový osobný rekord!", + "newTestBuildAvailableText": "Novšia testovacia verzia je dostupná! (${VERSION} test ${BUILD}).\nZožeň ho na ${ADDRESS}", + "newText": "Nový", + "newVersionAvailableText": "Novšia verzia hry ${APP_NAME} je dostupná! (${VERSION})", + "nextAchievementsText": "Ďalšie Achievementy:", + "nextLevelText": "Ďalší Level", + "noAchievementsRemainingText": "- žiadne", + "noContinuesText": "(žiadne pokračovania)", + "noExternalStorageErrorText": "Žiadne úložisko sa v tomto zariadení nenašlo", + "noGameCircleText": "Error: nie si prihlásený do GameCircle", + "noScoresYetText": "Zatiaľ žiadne skóre.", + "noThanksText": "Nie Vďaka", + "noValidMapsErrorText": "Žiadne platné mapy sa pre tento typ hry nenašli.", + "notEnoughPlayersRemainingText": "Nedostatok hráčov; začni novú hru.", + "notEnoughPlayersText": "Potrebuješ aspoň ${COUNT} hráčov ak chceš toto hrať!", + "notNowText": "Teraz Nie", + "notSignedInErrorText": "Ak toto chceš urobiť, musíš sa prihlásiť.", + "notSignedInGooglePlayErrorText": "Ak chceš toto urobiť, musíš sa prihlásiť do Google Play.", + "notSignedInText": "nie si prihlásený", + "nothingIsSelectedErrorText": "Nič nie je vybraté!", + "numberText": "#${NUMBER}", + "offText": "Vypnúť", + "okText": "Ok", + "onText": "Zapnúť", + "onslaughtRespawnText": "${PLAYER} sa znova zjaví vo vlne ${WAVE}", + "orText": "${A} alebo ${B}", + "otherText": "Ďalšie...", + "outOfText": "(#${RANK} z ${ALL})", + "ownFlagAtYourBaseWarning": "Tvoja vlajka musí byť \nnamieste ak chceš skórovať!", + "packageModsEnabledErrorText": "Online hranie nie je povolené pokiaľ sú povolené local-package-módy (pozri Nastavenia->Pokročilé)", + "partyWindow": { + "chatMessageText": "Chat Správa", + "emptyText": "Tvoja párty je prázdna", + "hostText": "(host)", + "sendText": "Poslať", + "titleText": "Tvoja Párty" + }, + "pausedByHostText": "(pozastavené hostom)", + "perfectWaveText": "Perfektná Vlna!", + "pickUpText": "Zobrať", + "playModes": { + "coopText": "Proti Počítaču", + "freeForAllText": "Všetci-proti-Všetkým", + "multiTeamText": "Viacero Týmov", + "singlePlayerCoopText": "Jeden Hráč / Proti Počítaču", + "teamsText": "Tímy" + }, + "playText": "Hrať", + "playWindow": { + "oneToFourPlayersText": "1-4 hráčov", + "titleText": "Hrať", + "twoToEightPlayersText": "2-8 hráčov" + }, + "playerCountAbbreviatedText": "${COUNT} hráč", + "playerDelayedJoinText": "${PLAYER} bude hrať na začiatku ďalšieho kola.", + "playerInfoText": "Info Hráča", + "playerLeftText": "${PLAYER} opustil hru.", + "playerLimitReachedText": "Limit hráčov (${COUNT}) dosiahnutý; nedá sa pripojiť.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Nemôžeš odstrániť profil účtu.", + "deleteButtonText": "Odstrániť\nProfil", + "deleteConfirmText": "Odstrániť \"${PROFILE}\"?", + "editButtonText": "Upraviť\nProfil", + "explanationText": "(vlastné mená a zobrazenia hráča pre tento účet)", + "newButtonText": "Nový\nProfil", + "titleText": "Profily Hráča" + }, + "playerText": "Hráč", + "playlistNoValidGamesErrorText": "Tento playlist neobsahuje žiadne platné odomknuté hry.", + "playlistNotFoundText": "playlist sa nenašiel", + "playlistsText": "Playlisty", + "pleaseRateText": "Ak si ${APP_NAME} užívaš, prosím pouvažuj nad chvíľou\nohodnotenia a napísania recenzie. Toto poskytuje\nužitočnú spätnú väzbu a pomáha podporovať budúci rozvoj.\n\nvďaka!\n-Eric", + "pleaseWaitText": "Prosím počkaj...", + "practiceText": "Tréning", + "pressAnyButtonPlayAgainText": "Stlač hocijaké tlačidlo ak chceš hrať znova...", + "pressAnyButtonText": "Stlač hocijaké tlačidlo ak chceš pokračovať...", + "pressAnyButtonToJoinText": "stlač hocijaké tlačidlo ak sa chceš pripojiť...", + "pressAnyKeyButtonPlayAgainText": "Stlač hocijakú klávesu/tlačidlo ak chceš hrať znova...", + "pressAnyKeyButtonText": "Stlač hocijakú klávesu/tlačidlo ak chceš pokračovať...", + "pressAnyKeyText": "Stlač hocijakú klávesu...", + "pressJumpToFlyText": "** Stláčaj Skok opakovane ak chceš lietať **", + "pressPunchToJoinText": "stlač ÚDER ak sa chceš pripojiť...", + "pressToOverrideCharacterText": "stlač ${BUTTONS} ak chceš zmeniť svoj charakter", + "pressToSelectProfileText": "stlač ${BUTTONS} ak si chceš zmeniť profil", + "pressToSelectTeamText": "stlač ${BUTTONS} ak si chceš zmeniť tím", + "promoCodeWindow": { + "codeText": "Kód", + "enterText": "Vložiť" + }, + "promoSubmitErrorText": "Error pri overovaní kódu; skontroluj svoj internet", + "ps3ControllersWindow": { + "macInstructionsText": "Vypni svoje PS3 vzadu, uisti sa že je že je Bluetooth\nna tvojom Mac-u zapnutý. Potom pripoj svoj ovládač \ndo svojho Mac-u a cez USB kábel ich spárujte. Odteraz\nmôžeš použiť home-button na ovládači a pripojiť sa na\nMac aj cez Bluetooth.\n\nNa niektorých Mac-och ťa pri párovaní môžu pýtať kód.\nAk sa to stane, pozri následujúci tutoriál alebo si \nvygoogli pomoc.\n\n\n\nPS3 ovládače pripojené cez Bluetooth by mali byť zobrazené\nv liste zariadení v Systémové Preferencie->Bluetooth. Mal\nby si ho z toho listu odstrániť a chceš ovládač znova použiť.\n\nTakisto sa uisti že si ich najskôr odpojil od Bluetoothu keď\nsa nepoužívajú lebo ich batéria sa bude stále používať.\n\nBluetooth by mal udržať max. 7 pripojených zariadení,\nto ale závisí na tom aké staré je zariadenie.", + "ouyaInstructionsText": "Ak chceš používať svoj PS3 ovládač na OUYA-e, jednoducho ich spoj USB káblom\na už sú spárované. Toto by malo odpojiť všetky ostatné zapojené ovládače, \npreto by si mal reštartovať OUYA-u a odpojiť USB kábel.\n\nOdteraz by si mal byť schopný použiť home button na ovládači pre bezdrôtové \npripojenie. Keď ťa to prestane baviť, podrž na ovládači home button na\n10 sekúnd ak chceš ovládač vypnúť; inak sa ti bude na nich míňať\nbaterka.", + "pairingTutorialText": "párujem tutoriálne video", + "titleText": "Používam PS3 ovládače s aplikáciou ${APP_NAME}:" + }, + "punchBoldText": "ÚDER", + "punchText": "Úder", + "purchaseForText": "Kúpiť za ${PRICE}", + "purchaseGameText": "Kúpiť Hru", + "purchasingText": "Kupujem...", + "quitGameText": "Ukončiť ${APP_NAME}?", + "quittingIn5SecondsText": "Ukončujem za 5 sekúnd..", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Náhodne", + "rankText": "Umiestnenie", + "ratingText": "Hodnotenie", + "reachWave2Text": "Dosiahni vlnu 2 pre umiestnenie.", + "readyText": "pripravený", + "recentText": "Nedávne", + "remoteAppInfoShortText": "${APP_NAME} je väčšia zábava s rodinou a kamarátmi.\nPripoj jeden alebo viac hardware ovládačov alebo si\nnainštaluj aplikáciu ${REMOTE_APP_NAME} na mobily a \ntablety a použi ich ako ovládače.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Pozícia Tlačidiel", + "button_size": "Veľkosť Tlačidiel", + "cant_resolve_host": "Nemožno nájsť hostiteľa.", + "capturing": "Potvrdujem...", + "connected": "Pripojené.", + "description": "Použi svoj mobil alebo tablet ako ovládač pre Bombsquad.\nNaraz sa môže pripojiť max. 8 zariadení na jedinej TV alebo tablete.", + "disconnected": "Odpojené serverom.", + "dpad_fixed": "stály", + "dpad_floating": "uvoľnený", + "dpad_position": "Pozícia D-Padu", + "dpad_size": "Veľkosť D-Padu", + "dpad_type": "Typ D-Padu", + "enter_an_address": "Pripojiť na Adresu", + "game_full": "Párty je plná alebo nepodporuje pripojenia.", + "game_shut_down": "Hra sa vypla.", + "hardware_buttons": "Hardwarové Tlačidlá", + "join_by_address": "Pripojiť sa na Adresu...", + "lag": "Lag: ${SECONDS} sekúnd", + "reset": "Resetovať na štandartné", + "run1": "Beh 1", + "run2": "Beh 2", + "searching": "Hľadám Bombsquad hry...", + "searching_caption": "Stlač meno hry aby si sa na ňu pripojil.\nUisti sa že ste na tej istej Wi-Fi ako hra.", + "start": "Začať", + "version_mismatch": "Nezhoda verzií.\nUisti sa že je Bombsquad aj BSRemote\nna najnovšej verzii." + }, + "removeInGameAdsText": "Odomkni si \"${PRO}\" v obchode pre odstránenie reklám.", + "renameText": "Premenovať", + "replayEndText": "Ukončiť Replay", + "replayNameDefaultText": "Replay Poslednej Hry", + "replayReadErrorText": "Error pri načítaní replay súboru.", + "replayRenameWarningText": "Premenuj \"${REPLAY}\" po hre ak si ho chceš uložiť; inak bude nahradený.", + "replayVersionErrorText": "Prepáč, tento replay bol vyrobený v inej\nverzii hry a nemôže byť použitý.", + "replayWatchText": "Pozrieť Replay", + "replayWriteErrorText": "Error pri písaní replay súboru.", + "replaysText": "Replaye", + "reportPlayerExplanationText": "Použi tento email pre nahlásenie podvodov, nevhodný jazyk, alebo iné zlé veci.\nProsím popíš nižšie:", + "reportThisPlayerCheatingText": "Podvod", + "reportThisPlayerLanguageText": "Nevhodný Jazyk", + "reportThisPlayerReasonText": "Čo chceš nahlásiť?", + "reportThisPlayerText": "Nahlásiť Hráča", + "requestingText": "Dostávam...", + "restartText": "Reštartovať", + "retryText": "Znova", + "revertText": "Späť", + "runText": "Bežať", + "saveText": "Uložiť", + "scanScriptsErrorText": "Error pri skenovaní skriptov; pozri zápis pre detaily.", + "scoreChallengesText": "Challenge pre Skóre", + "scoreListUnavailableText": "List pre skóre je nedostupné.", + "scoreText": "Skóre", + "scoreUnits": { + "millisecondsText": "Milisekúnd", + "pointsText": "Body", + "secondsText": "Sekúnd" + }, + "scoreWasText": "(predtým ${COUNT})", + "selectText": "Vybrať", + "seriesWinLine1PlayerText": "VYHRÁVA", + "seriesWinLine1TeamText": "VYHRÁVA", + "seriesWinLine1Text": "VYHRÁVA", + "seriesWinLine2Text": "SÉRIU!", + "settingsWindow": { + "accountText": "Účet", + "advancedText": "Pokročilé", + "audioText": "Audio", + "controllersText": "Ovládače", + "graphicsText": "Grafika", + "playerProfilesMovedText": "Poznámka: Profily Hráča sa premiestili do okna Účet v hlavnom menu.", + "titleText": "Nastavenia" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(jednoduchá, podporujúca-ovládač na-obrazovke-klávesnica pre písanie textu)", + "alwaysUseInternalKeyboardText": "Stále Používať Klávesnicu v Programe", + "benchmarksText": "Benchmarky & Stres-Testy", + "disableThisNotice": "(toto upozornenie môžeš vypnúť v Pokročilých nastaveniach)", + "enablePackageModsDescriptionText": "(povoľuje extra módovanie ale ruší hry na internete)", + "enablePackageModsText": "Povoliť Lokálny Balíček Módov", + "enterPromoCodeText": "Vložiť Kód", + "forTestingText": "Poznámka: tieto čísla sú len pre testovanie a budú stratené keď sa aplikácia vypne.", + "helpTranslateText": "${APP_NAME} podporuje aj iné jazyky ako Slovenský.\nAk chceš preložiť hru do iného jazyka alebo upraviť\npreklad, klikni na tlačidlo nižšie. Vďaka za pokrok!", + "kickIdlePlayersText": "Vyhodiť Nečinných Hráčov", + "kidFriendlyModeText": "Mód pre Deti (znížené násilie, atď.)", + "languageText": "Jazyk", + "moddingGuideText": "Tutoriál pre Módovanie", + "mustRestartText": "Ak chceš toto nastavenie použiť, musíš reštartovať hru.", + "netTestingText": "Testovanie Internetu", + "resetText": "Resetovať", + "showBombTrajectoriesText": "Ukázovať Trajektóriu Bomby", + "showPlayerNamesText": "Ukazovať Mená Hráčov", + "showUserModsText": "Ukázať Zložku pre Módy", + "titleText": "Pokročilé", + "translationEditorButtonText": "${APP_NAME} Preklad", + "translationFetchErrorText": "status pre jazyk nedostupný", + "translationFetchingStatusText": "kontrolujem status jazyka...", + "translationInformMe": "Informuj ma keď môj jazyk potrebuje vylepšiť", + "translationNoUpdateNeededText": "Slovenčina je celá preložená!", + "translationUpdateNeededText": "** Slovenčina potrebuje dokončiť!! **", + "vrTestingText": "VR Testovanie" + }, + "shareText": "Zdieľať", + "sharingText": "Zdieľam...", + "showText": "Ukazujem", + "signInForPromoCodeText": "Musíš sa prihlásiť ak chceš použiť kód.", + "signInWithGameCenterText": "Ak chceš použiť GameCircle účet,\nprihlás sa s Game Center aplikáciou.", + "singleGamePlaylistNameText": "Len ${GAME}", + "singlePlayerCountText": "1 hráč", + "soloNameFilterText": "Sólo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Výber Charakteru", + "Chosen One": "Vyvolený", + "Epic": "Spomalené Hry", + "Epic Race": "Spomalené Preteky", + "FlagCatcher": "Capture the Flag", + "Flying": "Sen", + "Football": "Futbal", + "ForwardMarch": "Prepad", + "GrandRomp": "Conquest", + "Hockey": "Hokej", + "Keep Away": "Únos", + "Marching": "Obeh", + "Menu": "Hlavné Menu", + "Onslaught": "Útok", + "Race": "Preteky", + "Scary": "Kráľ Hory", + "Scores": "Obrazovka so Skóre", + "Survival": "Eliminácia", + "ToTheDeath": "Death Match", + "Victory": "Obrazovka s Finálovým Skóre" + }, + "spaceKeyText": "medzerník", + "statsText": "Štatistiky", + "storagePermissionAccessText": "Na toto potrebuješ povolenie k úložisku", + "store": { + "alreadyOwnText": "Už vlastníš ${NAME}!", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Odstráni reklamy,\n• Odomkne viac nastavení,\n• A k tomu ešte dostaneš:", + "buyText": "Kúpiť", + "charactersText": "Charaktery", + "comingSoonText": "Už čoskoro...", + "extrasText": "Extra", + "freeBombSquadProText": "Bombsquad je teraz zadarmo, ale preto že si si ho predtým kúpil dostávaš\nBombsquad Pro zadarmo a ${COUNT} tiketov ako vďaku. Uži si nové vylepšenia\na vďaka za podporu!\n-Eric", + "holidaySpecialText": "Sviatočný Špeciál", + "howToSwitchCharactersText": "(choď do \"${SETTINGS} -> ${PLAYER_PROFILES}\" a nastav si a uprav charakter)", + "howToUseIconsText": "(vytvor si globálny profil (v okne Konto) ak ich chceš použiť)", + "howToUseMapsText": "(použi tieto mapy v tvojom všetci-proti-všetkým/tímovom playliste)", + "iconsText": "Ikony", + "loadErrorText": "Nemožno načítať stránku.\nSkontroluj internet.", + "loadingText": "načítavam", + "mapsText": "Mapy", + "miniGamesText": "MiniHry", + "oneTimeOnlyText": "(len jeden krát)", + "purchaseAlreadyInProgressText": "Už kupovanie tohoto predmetu prebieha.", + "purchaseConfirmText": "Kúpiť ${ITEM}?", + "purchaseNotValidError": "Objednávka nie je platná.\nKontaktuj ${EMAIL} ak to bol error.", + "purchaseText": "Kúpiť", + "saleBundleText": "Zľava na Bundle!", + "saleExclaimText": "Zľava!", + "salePercentText": "(${PERCENT}% zľava)", + "saleText": "ZĽAVA", + "searchText": "Hľadať", + "teamsFreeForAllGamesText": "Hry na Tímy / Všetci-proti-Všetkým", + "totalWorthText": "*** Cena ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Vylepšiť?", + "winterSpecialText": "Zimný Špeciál", + "youOwnThisText": "- toto už vlastníš -" + }, + "storeDescriptionText": "Zábava pre ôsmich hráčov!\n\nOdpáľ svojich kamarátov (alebo počítač) v turnajoch v explozívnych minihrách ako napríklad Capture-the-Flag, Hokej a Spomalený-Death-Match!\n\nJednoduché ovládanie a ovládač hru robia veľmi ľahkú na ovládanie až pre 8 hráčov! Môžeš pokojne použiť aj mobilné zariadenia ako ovládače cez aplikáciu \"BombSquad Remote\" zadarmo!\n\nVidíme sa v hre!\n\nPozri www.froemling.net/bombsquad pre viac info.", + "storeDescriptions": { + "blowUpYourFriendsText": "Odpáľ kamarátov.", + "competeInMiniGamesText": "Súťažte v minihrách siahajúce od pretekov po lietanie.", + "customize2Text": "Nastav si charakter, minihry, alebo aj soundtrack.", + "customizeText": "Nastav si charakter a vytvor svoj vlastný playlist pre minihry.", + "sportsMoreFunText": "Športy sú viac zábavne s výbuchmi.", + "teamUpAgainstComputerText": "Spojte sa proti počítaču." + }, + "storeText": "Obchod", + "submitText": "Poslať", + "submittingPromoCodeText": "Overujem Kód...", + "teamNamesColorText": "Mená/Farby tímov", + "telnetAccessGrantedText": "Telnet povolené.", + "telnetAccessText": "Telnet detekovaný; povoliť?", + "testBuildErrorText": "Táto testovacia verzia už viac nie je aktívna; prosím obzri sa pre novú verziu.", + "testBuildText": "Testovacia Verzia", + "testBuildValidateErrorText": "Nemožno uplatniť testovaciu verziu. (žiadny internet?)", + "testBuildValidatedText": "Testovacia Verzia Platná; Uži si to!", + "thankYouText": "Vďaka za podporu! Uži si hru!!", + "threeKillText": "TRIPLE-KILL!!!", + "timeBonusText": "Časový Bonus", + "timeElapsedText": "Čas Uplynul", + "timeExpiredText": "Čas Uplynul", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Tip", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Top Kamaráti", + "tournamentCheckingStateText": "Kontrolujem štádium turnaju; prosím počkaj...", + "tournamentEndedText": "Tento turnaj skončil. Nový začne o chvíľu.", + "tournamentEntryText": "Vstupné", + "tournamentResultsRecentText": "Nedávne Výsledky Turnaja", + "tournamentStandingsText": "Postavenie v Turnaji", + "tournamentText": "Turnaj", + "tournamentTimeExpiredText": "Čas v Turnaji Vypršal", + "tournamentsText": "Turnaje", + "translations": { + "characterNames": { + "Agent Johnson": "Agent Johnson", + "B-9000": "B-9000", + "Bernard": "Bernard", + "Bones": "Bonesy", + "Butch": "Butch", + "Easter Bunny": "Easter Bunny", + "Flopsy": "Flopsy", + "Frosty": "Frosty", + "Gretel": "Gretel", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Lucky", + "Mel": "Mel", + "Middle-Man": "Middle-Man", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Santa Claus", + "Snake Shadow": "Snake Shadow", + "Spaz": "Spaz", + "Taobao Mascot": "Taobao Mascot", + "Todd McBurton": "Todd McBurton", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Tréning", + "Infinite ${GAME}": "Nekonečný ${GAME}", + "Infinite Onslaught": "Nekonečný Útok", + "Infinite Runaround": "Nekonečný Obeh", + "Onslaught Training": "Útok Tréning", + "Pro ${GAME}": "Pro ${GAME}", + "Pro Football": "Pro Futbal", + "Pro Onslaught": "Pro Útok", + "Pro Runaround": "Pro Obeh", + "Rookie ${GAME}": "Pokročilý ${GAME}", + "Rookie Football": "Pokročilý Futbal", + "Rookie Onslaught": "Pokročilý Útok", + "The Last Stand": "The Last Stand", + "Uber ${GAME}": "Extrémny ${GAME}", + "Uber Football": "Extrémny Futbal", + "Uber Onslaught": "Extrémny Útok", + "Uber Runaround": "Extrémny Obeh" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Buď vyvolený na určitý čas aby si vyhral.\nZabi vyvoleného sa staň sa vyvoleným.", + "Bomb as many targets as you can.": "Traf čo najviac terčov.", + "Carry the flag for ${ARG1} seconds.": "Drž vlajku na ${ARG1} sekúnd.", + "Carry the flag for a set length of time.": "Drž vlajku na určitú dobu.", + "Crush ${ARG1} of your enemies.": "Zabi ${ARG1} protivníkov.", + "Defeat all enemies.": "Zabi všetkých protivníkov.", + "Dodge the falling bombs.": "Vyhni sa padajúcim bombám.", + "Final glorious epic slow motion battle to the death.": "Finálna spomalená bitka na život a na smrť.", + "Gather eggs!": "Zbieraj vajíčka!", + "Get the flag to the enemy end zone.": "Zober vlajku do protivníkovej konečnej zóny.", + "How fast can you defeat the ninjas?": "Ako rýchlo dokážeš poraziť ninjov?", + "Kill a set number of enemies to win.": "Zabi určitý počet protivníkov.", + "Last one standing wins.": "Posledný na nohách vyhráva.", + "Last remaining alive wins.": "Posledný nažive vyhráva.", + "Last team standing wins.": "Posledný tím nažive vyhráva.", + "Prevent enemies from reaching the exit.": "Zabráň protivníkom dosiahnúť východ.", + "Reach the enemy flag to score.": "Dotkni sa protivníkovej vlajky a skóruj.", + "Return the enemy flag to score.": "Ukradni protivníkovu vlajku a skóruj.", + "Run ${ARG1} laps.": "Obehni ${ARG1} kolá.", + "Run ${ARG1} laps. Your entire team has to finish.": "Obehni ${ARG1} kolá. Tvoj celý tím musí dokončiť.", + "Run 1 lap.": "Obehni 1 kolo.", + "Run 1 lap. Your entire team has to finish.": "Obehni 1 kolo. Tvoj celý tím musí dokončiť.", + "Run real fast!": "3..2..1..BEŽ!", + "Score ${ARG1} goals.": "Skóruj ${ARG1} góly.", + "Score ${ARG1} touchdowns.": "Skóruj ${ARG1} gólov.", + "Score a goal.": "Skóruj gól.", + "Score a touchdown.": "Skóruj gól.", + "Score some goals.": "Skóruj niekoľko gólov.", + "Secure all ${ARG1} flags.": "Obráň všetky ${ARG1} vlajky.", + "Secure all flags on the map to win.": "Obráň všetky vlajky na mape a vyhraj.", + "Secure the flag for ${ARG1} seconds.": "Buď pri vlajke ${ARG1} sekúnd.", + "Secure the flag for a set length of time.": "Buď pri vlajke určitý čas.", + "Steal the enemy flag ${ARG1} times.": "Ukradni vlajku nepriateľa ${ARG1} razy.", + "Steal the enemy flag.": "Ukradni nepriateľovu vlajku.", + "There can be only one.": "Môže byť len jeden.", + "Touch the enemy flag ${ARG1} times.": "Dotkni sa nepriateľovej vlajky ${ARG1} krát.", + "Touch the enemy flag.": "Dotkni sa nepriateľovej vlajky", + "carry the flag for ${ARG1} seconds": "drž vlajku na ${ARG1} sekúnd", + "kill ${ARG1} enemies": "zabi ${ARG1} protivníkov", + "last one standing wins": "posledný na nohách vyhráva", + "last team standing wins": "posledný tím nažive vyhráva", + "return ${ARG1} flags": "ukradni ${ARG1} vlajky", + "return 1 flag": "ukradni 1 vlajku", + "run ${ARG1} laps": "obehni ${ARG1} kolá", + "run 1 lap": "obehni 1 kolo", + "score ${ARG1} goals": "skóruj ${ARG1} góly", + "score ${ARG1} touchdowns": "skóruj ${ARG1} gólov", + "score a goal": "skóruj gól", + "score a touchdown": "skóruj gól", + "secure all ${ARG1} flags": "obráň všetky ${ARG1} vlajky", + "secure the flag for ${ARG1} seconds": "buď pri vlajke ${ARG1} sekúnd", + "touch ${ARG1} flags": "dotkni sa ${ARG1} vlajok", + "touch 1 flag": "dotkni sa 1 vlajky" + }, + "gameNames": { + "Assault": "Prepad", + "Capture the Flag": "Krádež", + "Chosen One": "Vyvolený", + "Conquest": "Conquest", + "Death Match": "Death Match", + "Easter Egg Hunt": "Zber Vajíčok", + "Elimination": "Eliminácia", + "Football": "Futbal", + "Hockey": "Hokej", + "Keep Away": "Keep Away", + "King of the Hill": "Kráľ Hory", + "Meteor Shower": "Sprcha Meteorov", + "Ninja Fight": "Ninja Boj", + "Onslaught": "Útok", + "Race": "Preteky", + "Runaround": "Obeh", + "Target Practice": "Tréning Mierenia", + "The Last Stand": "Posledný na nohách" + }, + "inputDeviceNames": { + "Keyboard": "Klávesnica", + "Keyboard P2": "Klávesnica Hráč 2" + }, + "languages": { + "Arabic": "Arabčina", + "Belarussian": "Bieloruština", + "Chinese": "Zjednodušená Čínština", + "ChineseTraditional": "Tradičná Čínština", + "Croatian": "Chorváčtina", + "Czech": "Čeština", + "Danish": "Dánčina", + "Dutch": "Holandčina", + "English": "Angličtina", + "Esperanto": "Esperanto", + "Finnish": "Fínčina", + "French": "Francúžtina", + "German": "Nemčina", + "Gibberish": "Somarina", + "Greek": "Gréčtina", + "Hindi": "Hindčina", + "Hungarian": "Maďarčina", + "Indonesian": "Indonézčina", + "Italian": "Taliančina", + "Japanese": "Japončina", + "Korean": "Kórejčina", + "Persian": "Perzština", + "Polish": "Poľština", + "Portuguese": "Portugálčina", + "Romanian": "Rumunčina", + "Russian": "Ruština", + "Serbian": "Srbčina", + "Slovak": "Slovenčina", + "Spanish": "Španielčina", + "Swedish": "Švédčina", + "Turkish": "Turečtina", + "Ukrainian": "Ukrainčina", + "Vietnamese": "Vietnamčina" + }, + "leagueNames": { + "Bronze": "Bronzová", + "Diamond": "Diamantová", + "Gold": "Zlatá", + "Silver": "Strieborná" + }, + "mapsNames": { + "Big G": "Veľké G", + "Bridgit": "Most", + "Courtyard": "Courtyard", + "Crag Castle": "Hrad", + "Doom Shroom": "Hríb", + "Football Stadium": "Futbalové Ihrisko", + "Happy Thoughts": "Sen", + "Hockey Stadium": "Hokejové Ihrisko", + "Lake Frigid": "Zmrznuté Jazero", + "Monkey Face": "Tvár Opice", + "Rampage": "Rampa", + "Roundabout": "Roundabout", + "Step Right Up": "Schody", + "The Pad": "Plošina", + "Tip Top": "Hora", + "Tower D": "Tower D", + "Zigzag": "ZigZag" + }, + "playlistNames": { + "Just Epic": "Len Spomalené", + "Just Sports": "Len Športy" + }, + "scoreNames": { + "Flags": "Vlajky", + "Goals": "Góly", + "Score": "Skóre", + "Survived": "Prežil", + "Time": "Čas", + "Time Held": "Čas Držania" + }, + "serverResponses": { + "A code has already been used on this account.": "Kód už bol na tomto zariadení použitý.", + "A reward has already been given for that address.": "Za túto adresu už odmena bola daná.", + "Account linking successful!": "Prepojenie prebehlo úspešne!", + "Account unlinking successful!": "Odpojenie prebehlo úspešne!", + "Accounts are already linked.": "Účty už sú prepojené.", + "An error has occurred; (${ERROR})": "Našiel sa error; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Našiel sa error; prosím kontaktuj podporu. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Našiel sa error; prosím kontaktuj support@froemling.net.", + "An error has occurred; please try again later.": "Našiel sa error; prosím skús to znova neskôr.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Si si istý že chcete prepojiť účty?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nToto nemôže byť odvolané!", + "BombSquad Pro unlocked!": "Bombsquad Pro odomknuté!", + "Can't link 2 accounts of this type.": "Nemožno prepojiť 2 účty tohoto typu.", + "Can't link 2 diamond league accounts.": "Nemožno prepojiť 2 účty s Diamantovou ligou.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Nemožno prepojiť; prekročilo by to maximum prepojených účtov (${COUNT}).", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Zachytili sme podvod; skóre a ceny nedostupné na ${COUNT} dni.", + "Could not establish a secure connection.": "Nepodarilo sa vytvoriť zabezpečené pripojenie.", + "Daily maximum reached.": "Denný maximum dosiahnutý.", + "Entering tournament...": "Vstupujem do turnaja...", + "Invalid code.": "Nesprávny kód.", + "Invalid payment; purchase canceled.": "Nemožná platba; nákup zrušený.", + "Invalid promo code.": "Nesprávny promo kód.", + "Invalid purchase.": "Zlý nákup.", + "Invalid tournament entry; score will be ignored.": "Nemožné vstupné; skóre bude ignorované.", + "Item unlocked!": "Item odomkutý!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "PREPOJENIE RUŠENÉ. ${ACCOUNT} obsahuje\nvzácne dáta ktoré bude STRATENÉ.\nMôžeš prepojiť \"opačne\" ak chceš\n(a stratiť dáta na TOMTO účte).", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Prepojiť účet ${ACCOUNT} s týmto účtom?\nVšetky dáta na účte ${ACCOUNT} bude stratené.\nToto nemôžeš odvolať. Si si istý?", + "Max number of playlists reached.": "Maximálny počet playlistov dosiahnutý.", + "Max number of profiles reached.": "Maximálny počet profilov dosiahnutý.", + "Maximum friend code rewards reached.": "Maximálny počet odmien za nových hráčov dosiahnutý.", + "Message is too long.": "Správa je moc dlhá.", + "Profile \"${NAME}\" upgraded successfully.": "Profil \"${NAME}\" bol úspešne vylepšený.", + "Profile could not be upgraded.": "Profil nemožno vylepšiť.", + "Purchase successful!": "Nákup prebehol úspešne!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Dostal si ${COUNT} tiketov za prihlásenie.\nPríď zase zajtra a dostaň ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Funkčnosť servera už nie je podporovaná na tejto verzii hry;\nProsím nainštaluj novšiu verziu.", + "Sorry, there are no uses remaining on this code.": "Prepáč, maximálne použitia na tomto kóde dosiahnuté.", + "Sorry, this code has already been used.": "Prepáč, tento kód už bol použitý.", + "Sorry, this code has expired.": "Prepáč, platnosť kódu vypršala.", + "Sorry, this code only works for new accounts.": "Prepáč, tento kód funguje len pre nové účty.", + "Temporarily unavailable; please try again later.": "Dočasne nedostupné; prosím skús to znova neskôr.", + "The tournament ended before you finished.": "Turnaj sa skončil predtým ako si ho dokončil.", + "This account cannot be unlinked for ${NUM} days.": "Tento účet nemôže byť odpojený v podobu ${NUM} dní.", + "This code cannot be used on the account that created it.": "Tento kód nemôže byť použitý na účte ktorý ho vytvoril.", + "This requires version ${VERSION} or newer.": "Toto požaduje verziu ${VERSION} alebo novšiu.", + "Tournaments disabled due to rooted device.": "Turnaje zrušené kvôli \"root-nutemu\" zariadeniu.", + "Tournaments require ${VERSION} or newer": "Turnaje vyžadujú ${VERSION} alebo novšiu.", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Odpojiť ${ACCOUNT} z tohoto zariadenia?\nVšetky dáta na ${ACCOUNT} sa resetujú.\n(okrem achievementov pri niektorých prípadoch)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "Varovanie: našli sa náznaky podvádzania na tomto účte.\nÚčty ktoré podvádzajú budú zabanované. Prosím hraj férovo.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Chcel by si prepojiť tvoj účet zariadenia na tento účet?\n\nTvoj účet zariadenia je {ACCOUNT1}\nTento účet je ${ACCOUNT2}\n\nToto ti povolí ponechať tvoje pokroky.\nVarovanie: toto nejde vrátiť!", + "You already own this!": "Toto už vlastníš!", + "You can join in ${COUNT} seconds.": "Môžeš sa pripojiť za ${COUNT} sekúnd.", + "You don't have enough tickets for this!": "Na toto nemáš tikety!", + "You don't own that.": "Toto ešte nevlastníš.", + "You got ${COUNT} tickets!": "Máš ${COUNT} tiketov!", + "You got a ${ITEM}!": "Máš ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Bol si povýšený na vyššiu ligu; gratulujeme!", + "You must update to a newer version of the app to do this.": "Musíš nainštalovať novšiu verziu hry ak chceš toto spraviť.", + "You must update to the newest version of the game to do this.": "Musíš nainštalovať najnovšiu verziu hry ak chceš toto spraviť.", + "You must wait a few seconds before entering a new code.": "Musíš počkať pár sekúnd predtým ako vložíš nový kód.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Umiestnil si sa na #${RANK} mieste v turnaji. Vďaka za hranie!", + "Your account was rejected. Are you signed in?": "Tvoj účet nebol prijatý. Si prihlásený?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Tvoja kópia hry je modifikovaná.\nProsím vráť späť všetky zmeny a skús to znova.", + "Your friend code was used by ${ACCOUNT}": "Tvoj kód bol použitý hráčom ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Minúta", + "1 Second": "1 Sekunda", + "10 Minutes": "10 Minút", + "2 Minutes": "2 Minúty", + "2 Seconds": "2 Sekundy", + "20 Minutes": "20 Minút", + "4 Seconds": "4 Sekundy", + "5 Minutes": "5 Minút", + "8 Seconds": "8 Sekúnd", + "Allow Negative Scores": "Mínusové Skóre", + "Balance Total Lives": "Vybalancovať Životy", + "Bomb Spawning": "Zjavovanie Bômb", + "Chosen One Gets Gloves": "Vyvolený Dostane Rukavice", + "Chosen One Gets Shield": "Vyvolený Dostane Štít", + "Chosen One Time": "Čas Vyvoleného", + "Enable Impact Bombs": "Povoliť Dotykové Bomby", + "Enable Triple Bombs": "Povoliť Trojité Bomby", + "Entire Team Must Finish": "Celý Tím Musí Dokončiť", + "Epic Mode": "Spomalene", + "Flag Idle Return Time": "Čas Vrátenia \"Neaktívnej\" Vlajky", + "Flag Touch Return Time": "Čas Vrátenia Vlajky Na Dotyk", + "Hold Time": "Čas Držania", + "Kills to Win Per Player": "Zabitia pre Výhru Na Hráča", + "Laps": "Kolá", + "Lives Per Player": "Životy Na Hráča", + "Long": "Dlhý", + "Longer": "Dlhší", + "Mine Spawning": "Zjavovanie Mín", + "No Mines": "Žiadne Míny", + "None": "Žiadny", + "Normal": "Normálny", + "Pro Mode": "Pro Mód", + "Respawn Times": "Čas Respawnu", + "Score to Win": "Výherné Skóre", + "Short": "Krátky", + "Shorter": "Kratší", + "Solo Mode": "Sólo Mód", + "Target Count": "Počet Terčov", + "Time Limit": "Časový Limit" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "Tím ${TEAM} je diskvalifikovaný lebo hráč ${PLAYER} odišiel", + "Killing ${NAME} for skipping part of the track!": "Zabitý hráč ${NAME} pre preskočenie časti trate!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Varovanie pre hráča ${NAME}: turbo stláčanie tlačidiel ťa zabije." + }, + "teamNames": { + "Bad Guys": "Protivníkov Tím", + "Blue": "Modrí", + "Good Guys": "Tvoj Tím", + "Red": "Červení" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Perfektne načasovaný skákajúci-bežiaci-točiaci-úder môže hráča zabiť\nna jednu ranu a získaš od kamarátov doživotný rešpekt.", + "Always remember to floss.": "Môžeš prosím ťa konečne začať hru?", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Vytvor nové profily per seba a tvojich kamarátov s vašimi\nvlastnými menami namiesto toho aby si používal náhodné.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Zošalenie ťa premení na časovanú bombu.\nAk nechceš vybúchnuť, nájdi lekárničku.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Napriek ich vzhľadu, sú schopnosti charakterov rovnaké,\ntakže si vyber, ktorý z nich sa ti najviac páči.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Nebuď moc drsný s tým štítom; stále ťa môžu zhodiť dole z útesu.", + "Don't run all the time. Really. You will fall off cliffs.": "Nebež celý čas. Vážne. Skončíš dole v útese.", + "Don't spin for too long; you'll become dizzy and fall.": "Netoč sa moc dlho; inak odpadneš.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Drž hocijaké tlačidlo aby si behal. (Najlepšie fungujú bočné tlačidlá ak ich máš)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Drž hocijaké tlačidle aby si behal. Dostaneš sa na miesta \nrýchlejšie ale budeš zle zatáčať, takže bacha na útesy.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ľadové bomby nie sú moc silné, ale zamrznú každého\nkoho trafia a nechajú ich ležať krehkých na zemi.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Ak ťa dakto zobere, udri ho ak on ťa nechá.\nToto funguje aj v reálnom živote.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Ak máš málo ovládačov, nainštaluj si aplikáciu \"${REMOTE_APP_NAME}\"\nna mobiloch a použi ich ako ovládače.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Ak sa na teba zalepí sticky-bomba, skáč a toč sa v kruhoch. Mala by sa od teba\nodlepiť a ak nie, uži si posledné sekundy života.", + "If you kill an enemy in one hit you get double points for it.": "Ak zabiješ protivníka na jednu ranu dostaneš zaň dvojité body.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Ak zoberieš zošalenie, tvoja jediná nádej je nájsť \nlekárničku počas ďalších pár sekúnd.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Ak budeš stáť na jednom mieste, budeš ako toast; čierny a asi aj mŕtvy.", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Ak máš dosť veľa prichádzajúcich hráčov, zapni \"vyhadzovanie neaktívnych hráčov\"\nv nastaveniach v prípade že niekto zabudne opustiť hru.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Ak bude tvoje zariadenie horúce alebo si chceš šetriť baterku,\nzníž \"Vizualizáciu\" alebo \"Rozlíšenie\" v Nastavenia->Grafika.", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Ak sú tvoje FPS-ka biedne, skús znížiť \"Vizualizáciu\"\nalebo \"Rozlíšenie\" v Nastavenia->Grafika.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "V Capture-the-Flag, tvoja vlajka musí byť namieste ak chceš skórovať, ak druhý \ntím ide skórovať, ukradnutie ich vlajky môže byť dobrá cesta ako ich zastaviť.", + "In hockey, you'll maintain more speed if you turn gradually.": "V hokeji naberieš väčšiu rýchlosť ak sa budeš točiť v oblúkoch.", + "It's easier to win with a friend or two helping.": "Je ľahšie vyhrať keď ti pomáha kamarát alebo aj dvaja.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Ak skočíš hneď pred vyhodením bomby, hodíš ju vyššie.", + "Land-mines are a good way to stop speedy enemies.": "Míny sú dobrá cesta ako zastaviť rýchlych protivníkov.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Veľa vecí môžeš zobrať a zahodiť, takisto aj iných hráčov. Zhodením protivníka \nz útesu môže byť dobrá a zároveň emocionálna stratégia.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nie, nedočiahneš tam hore. Použi bombu.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Hráči sa môžu pripájať a odpájať aj v strede hry. Takisto\nmôžeš zapájať a odpájať ovládače počas hry.", + "Practice using your momentum to throw bombs more accurately.": "Trénuj mierenie a čas vyhodenia s bombami.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Údery bolia viac keď sa päste viac hýbu. Preto sa hýb,\ntoč, skáč ako šialenec.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Bež dozadu a dopredu pred odhodením bomby \naby si ju zahodil ďalej.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Odpáľ skupinu protivníkov tak\nže položíš bombu blízko TNT-čka.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Hlava je najbolestivejšie miesto, takže sticky-bomba \ndo hlavy často znamená okamžitá smrť.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Tento level nikdy nekončí, ale pokiaľ tu nahráš\nveľké skóre, dostaneš rešpekt od mnoho ľudí.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Sila zahodenia je založená na smere akým sa krútiš.\nAk chceš dačo hodiť pred seba, netoč sa nijako.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Unavený zo soundtracku? Nahraď ho vlastným!\nPozri Nastavenia->Audio->Soundtrack.", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Skús počkať sekundu alebo dve predtým ako bombu zahodíš.", + "Try tricking enemies into killing eachother or running off cliffs.": "Skús prekabátiť protivníkov aby sa zabili navzájom alebo spadli z útesu.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Použi tlačidlo pre zobratie aby si vlajku zobral < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Choď dozadu a naspäť aby si niečo zahodil ďalej.", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Môžeš \"namieriť\" svoje údery tak že sa budeš točiť doľava\nalebo doprava. Toto môže byť užitočné pri hokeji.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Môžeš predpokladať kedy bomba vybúchne na farbách\niskier: žltá..oranžová..červená..BOOM.", + "You can throw bombs higher if you jump just before throwing.": "Môžeš hodiť bomby vyššie tak že pred ich vyhodením skočíš.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Môžeš sa zraniť ak sa budeš búchať o hlavu;\ntak sa skús nebúchať o hlavu.", + "Your punches do much more damage if you are running or spinning.": "Tvoje údery bolia viacej ak bežíš alebo sa točíš." + } + }, + "trophiesRequiredText": "Na toto potrebuješ aspoň ${NUMBER} trofejí.", + "trophiesText": "Trofeje", + "trophiesThisSeasonText": "Trofeje Tejto Sezóny", + "tutorial": { + "cpuBenchmarkText": "Púšťam tutoriál na neuveriteľnej rýchlosti (patrí k testu CPU rýchlosti)", + "phrase01Text": "Čau!", + "phrase02Text": "Vitaj v hre ${APP_NAME}!", + "phrase03Text": "Tu sú nejaké tipy ako ovládať svoj charakter:", + "phrase04Text": "Veľa vecí v hre ${APP_NAME} sú založené na FYZIKE.", + "phrase05Text": "Napríklad, keď dakoho udrieš,...", + "phrase06Text": "Bolesť je založená na rýchlosti tvojich pästí.", + "phrase07Text": "Vidíš? Nepohybujem sa, takže to ${NAME}a vôbec nebolí.", + "phrase08Text": "Teraz poďme skákať a točiť sa aby sme nabrali rýchlosť.", + "phrase09Text": "No, to je lepšie.", + "phrase10Text": "Beh pomáha tiež.", + "phrase11Text": "Podrž HOCIJAKÉ tlačidlo ak chceš bežať.", + "phrase12Text": "Pre úžasné údery, skús behať A točiť sa.", + "phrase13Text": "Ups; prepáč ${NAME}.", + "phrase14Text": "Môžeš brať a zahadzovať veci, vlajky, bomby... alebo ${NAME}a.", + "phrase15Text": "Nakoniec tu sú bomby.", + "phrase16Text": "Hádzanie bômb si vyžaduje tréning.", + "phrase17Text": "Jau! Nie moc dobrý hod.", + "phrase18Text": "Pohyb ti ju pomáha hodiť ďalej.", + "phrase19Text": "Skok ti ju pomáha hodiť vyššie.", + "phrase20Text": "\"Roztoč\" svoju bombu pre ešte dlhšie hody.", + "phrase21Text": "Načasovanie môže byť zložité.", + "phrase22Text": "Kurňa.", + "phrase23Text": "Skús ju \"upiecť\" sekundu alebo dve pred vyhodením.", + "phrase24Text": "Hurá! Pekne upečená.", + "phrase25Text": "No, to je asi všetko.", + "phrase26Text": "Teraz choď na nich!", + "phrase27Text": "A nezabudni piecť!", + "phrase28Text": "...ale nie moc dlho...", + "phrase29Text": "Veľa Šťastia!", + "randomName1Text": "Maroš", + "randomName2Text": "Erik", + "randomName3Text": "Baltazár", + "randomName4Text": "Denis", + "randomName5Text": "Laura", + "skipConfirmText": "Vážne chceš preskočiť tutoriál? Stlač znova pre potvrdenie.", + "skipVoteCountText": "${COUNT} z ${TOTAL} hráčov chcú tutoriál preskočiť", + "skippingText": "preskakujem tutoriál...", + "toSkipPressAnythingText": "(stlač dačo pre preskočenie tutoriálu)" + }, + "twoKillText": "DOUBLE-KILL!", + "unavailableText": "nedostupné", + "unconfiguredControllerDetectedText": "Nenastavený ovládač detekovaný:", + "unlockThisInTheStoreText": "Toto musí byť odomknuté v obchode.", + "unlockThisProfilesText": "Ak chceš vytvoriť viac ako ${NUM} profilov, potrebuješ:", + "unlockThisText": "Ak chceš toto odomknúť, potrebuješ:", + "unsupportedHardwareText": "Prepáč, tento hardware nie je podporovaný pre túto verziu hry.", + "upFirstText": "Ako prvé ide:", + "upNextText": "Ďalej v hre ${COUNT}:", + "updatingAccountText": "Vylepšujem účet...", + "upgradeText": "Vylepšiť", + "upgradeToPlayText": "Odomkni \"${PRO}\" v obchode (v hre) ak toto chceš hrať.", + "useDefaultText": "Použiť Štandartné", + "usesExternalControllerText": "Táto hra používa ako vstup externý ovládač.", + "usingItunesText": "Používam Music App ako soundtrack...", + "validatingTestBuildText": "Overujem Testovaciu Verziu...", + "victoryText": "Výhra!", + "voteDelayText": "Nemôžeš začať ďalšie hlasovanie v podobe ${NUMBER} sekúnd", + "voteInProgressText": "Hlasovanie už prebieha.", + "votedAlreadyText": "Už si hlasoval", + "votesNeededText": "Je treba ${NUMBER} hlasov", + "vsText": "vs", + "waitingForHostText": "(čakám na príkazy hráča ${HOST})", + "waitingForPlayersText": "čakám na hráčov aby sa pripojili...", + "waitingInLineText": "Čakám v rade (párty je plná)...", + "watchAVideoText": "Pozrieť Video", + "watchAnAdText": "Pozrieť Reklamu", + "watchWindow": { + "deleteConfirmText": "Vymazať \"${REPLAY}\"?", + "deleteReplayButtonText": "Vymazať\nReplay", + "myReplaysText": "Moje Replaye", + "noReplaySelectedErrorText": "Nie Je Vybratý Žiadny Replay", + "playbackSpeedText": "Rýchlosť Prehrávania: ${SPEED}", + "renameReplayButtonText": "Premenovať\nReplay", + "renameReplayText": "Premenovať \"${REPLAY}\" na:", + "renameText": "Premenovať", + "replayDeleteErrorText": "Error pri vymazávaní replayu.", + "replayNameText": "Meno Replayu", + "replayRenameErrorAlreadyExistsText": "Replay s takým menom už existuje.", + "replayRenameErrorInvalidName": "Nemožno premenovať replay; neplatné meno.", + "replayRenameErrorText": "Error pri premenovaní replayu.", + "sharedReplaysText": "Zdieľané Replaye", + "titleText": "Pozerať", + "watchReplayButtonText": "Pozrieť\nReplay" + }, + "waveText": "Vlna", + "wellSureText": "Jasné!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Počúvam Wiimotes...", + "pressText": "Stlač Wiimote tlačidlá 1 a 2 naraz.", + "pressText2": "Na novších Wiimotes so zabudovaným Motion Plus, stlač červené \"sync\" tlačidlo vzadu." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Copyright", + "listenText": "Počúvať", + "macInstructionsText": "Uisti sa že tvoj Wii je vypnutý a bluetooth je zapnutý\nna tvojom Mac-u, potom stlač \"Počúvať\". Wiimote podpora \nmôže byť trocha zbláznená, preto by si to mal skúsiť\nviac krát pred tým ako sa to pripojí.\n\nBluetooth by mal uniesť až 7 pripojených zariadení,\nto záleží na tom, aké staré je zariadenie.\n\nBombsquad podporuje originálne Wiimotes, Nunchucky,\na klasický ovládač.\nNovšie Wii Remote Plus už funguje tiež ale nie\ns prídavnými zariadeniami.", + "thanksText": "Vďaka DarwiinRemote tímu\nza toto uskutočnenie.", + "titleText": "Wiimote Setup" + }, + "winsPlayerText": "${NAME} Vyhráva!", + "winsTeamText": "${NAME} Vyhráva!", + "winsText": "${NAME} Vyhráva!", + "worldScoresUnavailableText": "Svetové skóre nedostupné.", + "worldsBestScoresText": "Svetovo Najlepšie Skóre", + "worldsBestTimesText": "Svetovo Najlepšie Časy", + "xbox360ControllersWindow": { + "getDriverText": "Zohnať Driver", + "macInstructions2Text": "Ak chceš použiť ovládače bezdrôtovo, budeš potrebovať receiver\nktorý by mal prísť s \"Xbox 360 Wireless Controller for Windows\".\nJeden \"receiver\" ti povoľuje pripojiť až 4 vládače.\n\nDôležité: \"3rd-party receiver\" nebude fungovať s týmto driverom;\nuisti sa že tvoj receiver má na sebe \"Microsoft\", nie \"XBOX 360\".\nMicrosoft ich už Nepredáva osobitne, takže budeš musieť zohnať\njeden pribalený s ovládačom alebo ho vyhľadať na ebay.\n\nAk si myslíš že to je užitočné, skús porozmýšľať nad donácii \ndriver vývojárovi na jeho stránke.", + "macInstructionsText": "Ak chceš použiť Xbox 360 ovládače, budeš musieť \nnainštalovať Mac driver dostupný v linku nižšie.\nToto platí pre káblové aj bezdrôtové ovládače.", + "ouyaInstructionsText": "Ak chceš použiť káblový Xbox ovládač na Bombsquade, jednoducho\nich zapoj do do USB portu. Môžeš použiť USB hub pre pripojenie\nviacerých ovládačov.\n\nAk chceš použiť bezdrôtový ovládač, potrebuješ receiver,\ndostupný ako časť \"Xbox 360 wireless Controller for Windows\"\nbalíku alebo kúpený osobitne. Každý receiver sa zapája do USB\nportu a povoľuje ti zapojiť až 4 ovládače.", + "titleText": "Používam Xbox 360 Ovládače s hrou ${APP_NAME}:" + }, + "yesAllowText": "Áno, Povoliť!", + "yourBestScoresText": "Tvoje Najlepšie Skóre", + "yourBestTimesText": "Tvoje Najlepšie Časy" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/spanish.json b/dist/ba_data/data/languages/spanish.json new file mode 100644 index 0000000..65735a2 --- /dev/null +++ b/dist/ba_data/data/languages/spanish.json @@ -0,0 +1,1969 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Los nombres no deben contener emoticonos o caracteres especiales", + "accountProfileText": "(Perfil de la cuenta)", + "accountsText": "Cuentas", + "achievementProgressText": "Logros: ${COUNT} de ${TOTAL}", + "campaignProgressText": "Progreso de campaña [Dificil]: ${PROGRESS}", + "changeOncePerSeason": "Solamente puedes cambiarlo una vez por temporada.", + "changeOncePerSeasonError": "Debes esperar hasta la siguiente temporada para cambiarlo de nuevo (${NUM} días)", + "customName": "Nombre personalizado", + "deviceSpecificAccountText": "Actualmente usando una cuenta específica de dispositivo: ${NAME}", + "linkAccountsEnterCodeText": "Registrar Código", + "linkAccountsGenerateCodeText": "Generar Código", + "linkAccountsInfoText": "(compartir progreso a través de diferentes plataformas)", + "linkAccountsInstructionsNewText": "Para enlazar dos cuentas, genera un código en la primera cuenta\ne introduce el código en la segunda. \nLos datos de la segunda cuenta serán compartidos con la primera.\n\n(Los datos de la primera cuenta se perderán).\n\nPuedes vincular hasta ${COUNT} cuentas.\n\nIMPORTANTE: enlaza cuentas tuyas;\nSi enlazas cuentas de tus amigos, no podrán jugar online al mismo tiempo.", + "linkAccountsInstructionsText": "Para enlazar dos cuentas, genera un código en una\n de ellas y escribe el código en la otra.\nEl progreso e accesorios se combinarán.\nPuedes enlazar hasta ${COUNT} cuentas. \n\nIMPORTANTE: Solo enlaza cuentas tuyas!\n\nSi enlazas cuentas con tus amigos no podrán jugar al mismo tiempo!\n\nTambién: esto no se puede deshacer actualmente, así que se cuidadoso!", + "linkAccountsText": "Enlazar Cuentas", + "linkedAccountsText": "Cuentas enlazadas:", + "nameChangeConfirm": "¿Cambiar tu nombre a ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Esto reiniciará tu progreso en el modo\ncooperativo y tus puntajes (A excepción de tus tickets).\nNo podrás recuperar los cambios. ¿Estás seguro?", + "resetProgressConfirmText": "Esto reiniciará tus logros, récords\ny progreso en el modo cooperativo.\nLos cambios no se pueden deshacer.\n¿Estás seguro?", + "resetProgressText": "Reiniciar progreso", + "setAccountName": "Establecer nombre de cuenta", + "setAccountNameDesc": "Selecciona el nombre a mostrar para tu cuenta. \nPuedes usar el nombre de tu cuenta asociada\no crear un nombre personalizado.", + "signInInfoText": "Inicia sesión para obtener tickets, competir en línea\ny compartir tu progreso en otras plataformas.", + "signInText": "Iniciar sesión", + "signInWithDeviceInfoText": "(una cuenta automática disponible únicamente en este dispositivo)", + "signInWithDeviceText": "Iniciar sesión con cuenta del dispositivo", + "signInWithGameCircleText": "Iniciar sesión con Game Circle", + "signInWithGooglePlayText": "Iniciar sesión con Google Play", + "signInWithTestAccountInfoText": "(Cuenta de herencia, usa la cuenta del dispositivo para avanzar)", + "signInWithTestAccountText": "Registrarse con una Cuenta de Prueba", + "signOutText": "Cerrar Sesión", + "signingInText": "Iniciando sesión...", + "signingOutText": "Cerrando sesión...", + "testAccountWarningCardboardText": "Advertencia: inicias sesión con una cuenta \"de prueba\".\nSerá reemplazada por una cuenta Google una \nvez sea soportada en cardboard apps.\n\nPor ahora tendrás que ganar todos los tickets en el juego.\n(hazlo ,sin embargo, puedes obtener BombSquad Pro upgrade gratis)", + "testAccountWarningOculusText": "Cuidado: Está ingresando con una cuenta \"De prueba\".\nEsta será reemplazada con su cuenta Oculus más adelante\nen este año, lo cual ofrecerá compra de tiquetes y otras opciones.\n\nPor ahora tendrá que ganarse todos los tiquetes en-juego.\n(Sin embargo, puedes obtener BombSquad Pro gratis)", + "testAccountWarningText": "Advertencia: te estás registrando con una cuenta de\nprueba. Esta cuenta está vinculada a este dispositivo y\npuede que se reinicie periódicamente. (Así que no\ngastes mucho tiempo coleccionando/desbloqueando objetos)\n\nJuega una versión pública del juego y usa una \"cuenta real\"\n(Game-Center, Google Plus, etc.) Esto también te dejará\nguardar tu progreso en la nube y poder compartirlo con\ndiferentes dispositivos.", + "ticketsText": "Boletos: ${COUNT}", + "titleText": "Cuenta", + "unlinkAccountsInstructionsText": "Selecciona una cuenta para dejar de enlazar con ella", + "unlinkAccountsText": "Desenlazar cuentas.", + "viaAccount": "(por cuenta ${NAME})", + "youAreLoggedInAsText": "Estás conectado como:", + "youAreSignedInAsText": "Has iniciado sesión como:" + }, + "achievementChallengesText": "Logros de desafíos", + "achievementText": "Logro", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Elimina 3 enemigos con TNT", + "descriptionComplete": "Eliminaste a 3 enemigos con TNT", + "descriptionFull": "Elimina 3 enemigos con TNT en ${LEVEL}", + "descriptionFullComplete": "Eliminaste 3 enemigos con TNT en ${LEVEL}", + "name": "La dinamita hace Boom!" + }, + "Boxer": { + "description": "Gana sin usar bombas", + "descriptionComplete": "Ganaste sin usar bombas", + "descriptionFull": "Completa ${LEVEL} sin usar bombas", + "descriptionFullComplete": "Completaste ${LEVEL} sin usar bombas", + "name": "Boxeador" + }, + "Dual Wielding": { + "descriptionFull": "Conecta 2 controles (de hardware o de aplicación)", + "descriptionFullComplete": "2 controles conectados (de hardware o de aplicación)", + "name": "Doble empuñadura" + }, + "Flawless Victory": { + "description": "Gana sin ser golpeado", + "descriptionComplete": "Ganaste sin ser golpeado", + "descriptionFull": "Gana ${LEVEL} sin ser golpeado", + "descriptionFullComplete": "Ganaste ${LEVEL} sin ser golpeado", + "name": "Victoria Perfecta" + }, + "Free Loader": { + "descriptionFull": "Empieza un juego de Todos-Contra-Todos con 2 o más jugadores", + "descriptionFullComplete": "Has empezado un juego de Todos-Contra-Todos con 2 o más jugadores", + "name": "Cargador Libre" + }, + "Gold Miner": { + "description": "Elimina 6 enemigos con minas", + "descriptionComplete": "Eliminaste 6 enemigos con minas", + "descriptionFull": "Elimina 6 enemigos con minas en ${LEVEL}", + "descriptionFullComplete": "Eliminaste 6 enemigos con minas en ${LEVEL}", + "name": "Minero de Oro" + }, + "Got the Moves": { + "description": "Gana sin usar golpes o bombas", + "descriptionComplete": "Ganaste sin usar golpes o bombas", + "descriptionFull": "Gana ${LEVEL} sin usar golpes o bombas", + "descriptionFullComplete": "Ganaste ${LEVEL} sin usar golpes o bombas", + "name": "Tú si sabes como moverte!" + }, + "In Control": { + "descriptionFull": "Conecta un control (de hardware o de aplicación)", + "descriptionFullComplete": "Conectado un control (de hardware o de aplicación)", + "name": "Bajo Control" + }, + "Last Stand God": { + "description": "Anota 1000 puntos", + "descriptionComplete": "Anotaste 1000 puntos", + "descriptionFull": "Anota 1000 puntos en ${LEVEL}", + "descriptionFullComplete": "Anotaste 1000 puntos en ${LEVEL}", + "name": "Dios del ${LEVEL}" + }, + "Last Stand Master": { + "description": "Anota 250 puntos", + "descriptionComplete": "Anotaste 250 puntos", + "descriptionFull": "Anota 250 puntos en ${LEVEL}", + "descriptionFullComplete": "Anotaste 250 puntos en ${LEVEL}", + "name": "Maestro de ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Anota 500 puntos", + "descriptionComplete": "Anotaste 500 puntos", + "descriptionFull": "Anota 500 puntos en ${LEVEL}", + "descriptionFullComplete": "Anotaste 500 puntos en ${LEVEL}", + "name": "Mago de ${LEVEL}" + }, + "Mine Games": { + "description": "Elimina 3 enemigos con minas", + "descriptionComplete": "Eliminaste 3 enemigos con minas", + "descriptionFull": "Elimina 3 enemigos con minas en ${LEVEL}", + "descriptionFullComplete": "Eliminaste 3 enemigos con minas en ${LEVEL}", + "name": "Juegos de minas" + }, + "Off You Go Then": { + "description": "Arroja 3 enemigos al precipicio", + "descriptionComplete": "Arrojaste 3 enemigos al precipicio", + "descriptionFull": "Arroja 3 enemigos al precipicio en ${LEVEL}", + "descriptionFullComplete": "Arrojaste 3 enemigos al precipicio en ${LEVEL}", + "name": "Te vas abajo entonces" + }, + "Onslaught God": { + "description": "Anota 5000 puntos", + "descriptionComplete": "Anotaste 5000 puntos", + "descriptionFull": "Anota 5000 puntos en ${LEVEL}", + "descriptionFullComplete": "Anotaste 5000 puntos en ${LEVEL}", + "name": "Dios de ${LEVEL}" + }, + "Onslaught Master": { + "description": "Anota 500 puntos", + "descriptionComplete": "Anotaste 500 puntos", + "descriptionFull": "Anota 500 puntos en ${LEVEL}", + "descriptionFullComplete": "Anotaste 500 puntos en ${LEVEL}", + "name": "Maestro de ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Vence todas las hordas", + "descriptionComplete": "Venciste todas las hordas", + "descriptionFull": "Vence todas las hordas en ${LEVEL}", + "descriptionFullComplete": "Venciste todas las hordas en ${LEVEL}", + "name": "Victoria en ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Anota 1000 puntos", + "descriptionComplete": "Anotaste 1000 puntos", + "descriptionFull": "Anota 1000 puntos en ${LEVEL}", + "descriptionFullComplete": "Anotaste 1000 puntos en ${LEVEL}", + "name": "Mago de ${LEVEL}" + }, + "Precision Bombing": { + "description": "Gana sin usar poderes", + "descriptionComplete": "Ganaste sin usar poderes", + "descriptionFull": "Gana ${LEVEL} sin usar poderes", + "descriptionFullComplete": "Ganaste ${LEVEL} sin usar poderes", + "name": "Bombardeo preciso" + }, + "Pro Boxer": { + "description": "Gana sin usar bombas", + "descriptionComplete": "Ganaste sin usar bombas", + "descriptionFull": "Completa ${LEVEL} sin usar bombas", + "descriptionFullComplete": "Completaste ${LEVEL} sin usar bombas", + "name": "Boxeador Pro" + }, + "Pro Football Shutout": { + "description": "Gana sin que los enemigos anoten", + "descriptionComplete": "Ganaste sin que los enemigos anotaran", + "descriptionFull": "Gana ${LEVEL} sin que los enemigos anoten", + "descriptionFullComplete": "Ganaste ${LEVEL} sin que los enemigos anotaran", + "name": "Balón de oro en ${LEVEL}" + }, + "Pro Football Victory": { + "description": "Gana el partido", + "descriptionComplete": "Ganaste el partido", + "descriptionFull": "Gana el partido en ${LEVEL}", + "descriptionFullComplete": "Ganaste el partido en ${LEVEL}", + "name": "Victoria en ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Vence todas las hordas", + "descriptionComplete": "Venciste todas las hordas", + "descriptionFull": "Vence todas las hordas de ${LEVEL}", + "descriptionFullComplete": "Venciste todas las hordas de ${LEVEL}", + "name": "Victoria en ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Supera todas las hordas", + "descriptionComplete": "Superaste a todas las hordas", + "descriptionFull": "Supera todas las hordas en ${LEVEL}", + "descriptionFullComplete": "Supera todas las hordas en ${LEVEL}", + "name": "Victoria en ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Gana sin que los enemigos anoten", + "descriptionComplete": "Ganaste sin que los enemigos anotaran", + "descriptionFull": "Gana ${LEVEL} sin que los enemigos anoten", + "descriptionFullComplete": "Ganaste ${LEVEL} sin que los enemigos anotaran", + "name": "Balón de oro en ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Gana el partido", + "descriptionComplete": "Ganaste el partido", + "descriptionFull": "Gana el partido en ${LEVEL}", + "descriptionFullComplete": "Ganaste el partido en ${LEVEL}", + "name": "Victoria en ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Vence todas las hordas", + "descriptionComplete": "Venciste todas las hordas", + "descriptionFull": "Vence todas las hordas en ${LEVEL}", + "descriptionFullComplete": "Venciste todas las hordas en ${LEVEL}", + "name": "Victoria en ${LEVEL}" + }, + "Runaround God": { + "description": "Anota 2000 puntos", + "descriptionComplete": "Anotaste 2000 puntos", + "descriptionFull": "Anota 2000 puntos en ${LEVEL}", + "descriptionFullComplete": "Anotaste 2000 puntos en ${LEVEL}", + "name": "Dios de ${LEVEL}" + }, + "Runaround Master": { + "description": "Anota 500 puntos", + "descriptionComplete": "Anotaste 500 puntos", + "descriptionFull": "Anota 500 puntos en ${LEVEL}", + "descriptionFullComplete": "Anotaste 500 puntos en ${LEVEL}", + "name": "Maestro de ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Anota 1000 puntos", + "descriptionComplete": "Anotaste 1000 puntos", + "descriptionFull": "Anota 1000 puntos en ${LEVEL}", + "descriptionFullComplete": "Anotaste 1000 puntos en ${LEVEL}", + "name": "Gran Sabio de ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Comparte exitosamente el juego con un amigo", + "descriptionFullComplete": "Compartido exitosamente el juego con un amigo", + "name": "Compartir es Amar" + }, + "Stayin' Alive": { + "description": "Gana sin ser eliminado", + "descriptionComplete": "Ganaste sin ser eliminado", + "descriptionFull": "Gana ${LEVEL} sin ser eliminado", + "descriptionFullComplete": "Ganaste ${LEVEL} sin ser eliminado", + "name": "¡Sobreviviendo!" + }, + "Super Mega Punch": { + "description": "Inflige 100% de daño con un solo golpe", + "descriptionComplete": "Infligiste 100% de daño con un solo golpe", + "descriptionFull": "Inflige 100% de daño con un solo golpe en ${LEVEL}", + "descriptionFullComplete": "Infligiste 100% de daño con un solo golpe en ${LEVEL}", + "name": "¡Súper mega golpe!" + }, + "Super Punch": { + "description": "Inflige 50% de daño con un solo golpe", + "descriptionComplete": "Infligiste 50% de daño con un solo golpe", + "descriptionFull": "Inflige 50% de daño con un solo golpe en ${LEVEL}", + "descriptionFullComplete": "Infligiste 50% de daño con un solo golpe en ${LEVEL}", + "name": "¡Super golpe!" + }, + "TNT Terror": { + "description": "Elimina 6 enemigos con TNT", + "descriptionComplete": "Eliminaste 6 enemigos con TNT", + "descriptionFull": "Elimina 6 enemigos con TNT en ${LEVEL}", + "descriptionFullComplete": "Eliminaste 6 enemigos con TNT en ${LEVEL}", + "name": "¡Pirómano!" + }, + "Team Player": { + "descriptionFull": "Empieza un juego de Equipos con 4 o más jugadores", + "descriptionFullComplete": "Has empezado un juego de Equipos con 4 o más jugadores", + "name": "Jugador de Equipo" + }, + "The Great Wall": { + "description": "Detén todos los enemigos", + "descriptionComplete": "Detuviste todos los enemigos", + "descriptionFull": "Detén todos los enemigos en ${LEVEL}", + "descriptionFullComplete": "Detuviste todos los enemigos en ${LEVEL}", + "name": "La Gran Muralla" + }, + "The Wall": { + "description": "Detén a todos los enemigos", + "descriptionComplete": "Detuviste a todos los enemigos", + "descriptionFull": "Detén a todos los enemigos en ${LEVEL}", + "descriptionFullComplete": "Detuviste a todos los enemigos en ${LEVEL}", + "name": "La Muralla" + }, + "Uber Football Shutout": { + "description": "Gana sin que los enemigos anoten", + "descriptionComplete": "Ganaste sin que los enemigos anotaran", + "descriptionFull": "Gana ${LEVEL} sin que los enemigos anoten", + "descriptionFullComplete": "Ganaste ${LEVEL} sin que los enemigos anotaran", + "name": "Balón de oro ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Gana el partido", + "descriptionComplete": "Ganaste el partido", + "descriptionFull": "Gana el partido en ${LEVEL}", + "descriptionFullComplete": "Ganaste el partido en ${LEVEL}", + "name": "Victoria en ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Vence todas las hordas", + "descriptionComplete": "Venciste todas las hordas", + "descriptionFull": "Vence todas las hordas en ${LEVEL}", + "descriptionFullComplete": "Venciste todas las hordas en ${LEVEL}", + "name": "Victoria en ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Vence a todas las hordas", + "descriptionComplete": "Terminaste con todas las hordas", + "descriptionFull": "Vence a todas las hordas en ${LEVEL}", + "descriptionFullComplete": "Terminaste con todas las hordas en ${LEVEL}", + "name": "Victoria en ${LEVEL}" + } + }, + "achievementsRemainingText": "Logros pendientes:", + "achievementsText": "Logros", + "achievementsUnavailableForOldSeasonsText": "Lo sentimos, los logros específicos no están disponibles para temporadas anteriores.", + "addGameWindow": { + "getMoreGamesText": "Más Juegos...", + "titleText": "Agregar Juego" + }, + "allowText": "Permitir", + "alreadySignedInText": "Tu cuenta está registrada en otro dispositivo;\npor favor cambia de cuentas o cierra el juego en tu \notro dispositivo e inténtalo de nuevo.", + "apiVersionErrorText": "No se puede cargar el módulo ${NAME}; se dirige a la versión-api ${VERSION_USED}; necesitamos la ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" se activa solo si tiene audífonos conectados)", + "headRelativeVRAudioText": "Audio VR de Cabeza", + "musicVolumeText": "Volumen de la Música", + "soundVolumeText": "Volumen del Sonido", + "soundtrackButtonText": "Canciones de fondo/Bandas Sonoras", + "soundtrackDescriptionText": "(usa tu propia música para reproducir durante los juegos)", + "titleText": "Sonido" + }, + "autoText": "Auto", + "backText": "Atrás", + "banThisPlayerText": "Expulsar a Este Jugador", + "bestOfFinalText": "El mejor de ${COUNT}", + "bestOfSeriesText": "Mejor serie de ${COUNT}:", + "bestOfUseFirstToInstead": 0, + "bestRankText": "Tu mejor clasificación es #${RANK}", + "bestRatingText": "Tu mejor clasificación es ${RATING}", + "betaErrorText": "Esta versión beta está caducada, por favor actualizala.", + "betaValidateErrorText": "Imposible validar beta. (¿Tienes conexión a internet?)", + "betaValidatedText": "Beta Validada; ¡disfruta!", + "bombBoldText": "BOMBA", + "bombText": "Bomba", + "boostText": "Potenciar", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} se configura en la propia aplicación.", + "buttonText": "botón", + "canWeDebugText": "¿Te gustaría que BombSquad enviara errores, accidentes, e\ninformación de uso básico al desarrollador de forma automática?\n\nLa información no contendrá datos personales y ayudará a\nmantener el juego funcionando sin errores y libre de problemas.", + "cancelText": "Cancelar", + "cantConfigureDeviceText": "Perdón, pero ${DEVICE} no es configurable.", + "challengeEndedText": "Este desafío ha terminado.", + "chatMuteText": "Silenciar Chat", + "chatMutedText": "Chat Silenciado", + "chatUnMuteText": "Activar Chat", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "¡Debes completar\neste nivel para avanzar!", + "completionBonusText": "Bono extra por Acabar", + "configControllersWindow": { + "configureControllersText": "Configuración de Controles", + "configureGamepadsText": "Configurar controles", + "configureKeyboard2Text": "Configurar Teclado P2", + "configureKeyboardText": "Configurar Teclado", + "configureMobileText": "Dispositivos Móviles como Controles", + "configureTouchText": "Configurar Pantalla Táctil", + "ps3Text": "Controles de PS3", + "titleText": "Controles", + "wiimotesText": "Controles de Wii", + "xbox360Text": "Controles de Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Nota: el soporte de controles varía según el dispositivo y la versión de Android.", + "pressAnyButtonText": "Pulsa cualquier botón del control\n que desees configurar...", + "titleText": "Configurar tu control" + }, + "configGamepadWindow": { + "advancedText": "Avanzado", + "advancedTitleText": "Configuración Avanzada del Control", + "analogStickDeadZoneDescriptionText": "(súbele si tu personaje 'derrapa' cuando sueltas la palanca)", + "analogStickDeadZoneText": "Zona Muerta del Stick Analógico", + "appliesToAllText": "(aplica a todos los controles de este tipo)", + "autoRecalibrateDescriptionText": "(habilita esta opción si tu personaje no se mueve a toda velocidad)", + "autoRecalibrateText": "Auto-Calibrar Stick Analógico", + "axisText": "eje", + "clearText": "vaciar", + "dpadText": "DPad", + "extraStartButtonText": "Botón Extra de Inicio", + "ifNothingHappensTryAnalogText": "Si no ocurre nada, intenta asignar al stick analógico.", + "ifNothingHappensTryDpadText": "Si no ocurre nada, intenta asignar al DPad.", + "ignoreCompletelyDescriptionText": "(evita que este controlador impacte el juego y/o menus)", + "ignoreCompletelyText": "Ignorar Completamente", + "ignoredButton1Text": "Botón 1 Ignorado", + "ignoredButton2Text": "Botón 2 ignorado", + "ignoredButton3Text": "Botón 3 Ignorado", + "ignoredButton4Text": "Botón 4 Ignorado", + "ignoredButtonDescriptionText": "(usa esto para prevenir que los botones de 'inicio' o 'sincronización' afecten la interfaz visual)", + "ignoredButtonText": "Botón de inicio", + "pressAnyAnalogTriggerText": "Pulsa cualquier gatillo analógico...", + "pressAnyButtonOrDpadText": "Pulsa cualquier botón o DPad...", + "pressAnyButtonText": "Pulsa cualquier botón...", + "pressLeftRightText": "Pulsa izquierda o derecha...", + "pressUpDownText": "Pulsa arriba o abajo...", + "runButton1Text": "Botón para Correr 1", + "runButton2Text": "Botón para Correr 2", + "runTrigger1Text": "Gatillo para Correr 1", + "runTrigger2Text": "Gatillo para Correr 2", + "runTriggerDescriptionText": "(Los gatillos analógicos te dejan correr a velocidades distintas)", + "secondHalfText": "Utiliza esta opción para configurar\nla segunda mitad de un control 2 en 1\nque se muestre como un solo control.", + "secondaryEnableText": "Habilitar", + "secondaryText": "Control Secundario", + "startButtonActivatesDefaultDescriptionText": "(desactívalo si tu botón de inicio es más bien un botón de 'menú')", + "startButtonActivatesDefaultText": "El botón de inicio activa la función por defecto", + "titleText": "Configuración del Control", + "twoInOneSetupText": "Configuración de Controles 2 en 1", + "uiOnlyDescriptionText": "(evitar que este control se una a un juego)", + "uiOnlyText": "Limitar a Uso en Menú", + "unassignedButtonsRunText": "Todos los Botones sin Asignar Corren", + "unassignedButtonsRunTextScale": 0.8, + "unsetText": "", + "vrReorientButtonText": "Boton de Reorientación VR" + }, + "configKeyboardWindow": { + "configuringText": "Configurando ${DEVICE}", + "keyboard2NoteScale": 0.7, + "keyboard2NoteText": "Nota: la mayoría de teclados sólo pueden registrar algunas\npulsaciones a la vez, así que tener un segundo teclado puede\nfuncionar mejor que un teclado compartido por los 2 jugadores.\nTen en cuenta que todavía necesitas asignar teclas únicas a los\ndos jugadores incluso en ese caso." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Tamaño de \"Acciones\"", + "actionsText": "Acciones", + "buttonsText": "botones", + "dragControlsText": "", + "joystickText": "palanca", + "movementControlScaleText": "Tamaño de \"Movimiento\"", + "movementText": "Movimiento", + "resetText": "Reiniciar", + "swipeControlsHiddenText": "Ocultar Icono \"Deslizante\"", + "swipeInfoText": "Controles de estilo 'deslizante' toman un poco de practica, pero\nhacen que sea más fácil jugar sin mirar a los controles.", + "swipeText": "deslizar", + "titleText": "Configura la Pantalla Táctil", + "touchControlsScaleText": "Ampliador de controles táctiles" + }, + "configureItNowText": "Configurar ahora?", + "configureText": "Configurar", + "connectMobileDevicesWindow": { + "amazonText": "Appstore de Amazon", + "appStoreText": "App Store", + "bestResultsScale": 0.65, + "bestResultsText": "Para mejores resultados necesitas una red inalámbrica rápida. Puedes\nreducir el retraso wifi apagando otros dispositivos inalámbricos,\njugando cerca de tu router inalámbrico, y conectando el\njuego anfitrión directamente a la red a través del cable Ethernet.", + "explanationScale": 0.8, + "explanationText": "Para utilizar tus dispositivos móviles como controles, \ninstala la app \"${REMOTE_APP_NAME}\" en ellos. Conecta todos\ntus dispositivos con ${APP_NAME} a través de tu red Wi-fi, ¡es gratis!.", + "forAndroidText": "para Android:", + "forIOSText": "para iOS:", + "getItForText": "Consigue ${REMOTE_APP_NAME} para iOS en el App Store de Apple\no para Android en la Tienda Google Play o Appstore de Amazon", + "googlePlayText": "Google Play", + "titleText": "Utilizando Dispositivos como Controles:" + }, + "continuePurchaseText": "¿Continuar por ${PRICE}?", + "continueText": "Continuar", + "controlsText": "Controles", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Esto no aplica en los rankings de 'Todo el tiempo'.", + "activenessInfoText": "Este multiplicador aumenta en los días que juegas\ny disminuye cuando no juegas.", + "activityText": "Actividad", + "campaignText": "Campaña", + "challengesInfoText": "Gana premios por completar los mini-juegos.\n\nLos premios y la dificultad de los niveles incrementa\ncada vez que se completa y\ndecrece cuando uno expira o no se cumple.", + "challengesText": "Desafíos", + "currentBestText": "Mejor del Momento", + "customText": "Personalizado", + "entryFeeText": "Precio", + "forfeitConfirmText": "¿Renunciar a este desafío?", + "forfeitNotAllowedYetText": "Todavía no puedes abandonar este desafío.", + "forfeitText": "Abandonar", + "multipliersText": "Multiplicadores", + "nextChallengeText": "Siguiente Desafío", + "nextPlayText": "Siguiente Juego", + "ofTotalTimeText": "de ${TOTAL}", + "playNowText": "Juega Ahora", + "pointsText": "Puntos", + "powerRankingFinishedSeasonUnrankedText": "(temporada terminada sin clasificar)", + "powerRankingNotInTopText": "(fuera del top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} pts", + "powerRankingPointsMultText": "(x ${NUMBER} pts)", + "powerRankingPointsText": "${NUMBER} pts", + "powerRankingPointsToRankedText": "(${CURRENT} de ${REMAINING} pts)", + "powerRankingText": "Clasificación de Poder", + "prizesText": "Premios", + "proMultInfoText": "Los jugadores con la mejora ${PRO}\nreciben una bonificación del ${PERCENT}% de puntos.", + "seeMoreText": "Ver más...", + "skipWaitText": "Saltar la Espera", + "timeRemainingText": "Tiempo Restante", + "titleText": "Modo cooperativo", + "toRankedText": "Para Clasificar", + "totalText": "total", + "tournamentInfoText": "Compite por puntajes altos con\notros jugadores en tu liga.\n\nLos premios los obtienen los jugadores\ncon mejor puntaje al acabarse el torneo.", + "welcome1Text": "Bienvenido/a a ${LEAGUE}. Puedes mejorar tu\nposición en la liga ganando estrellas, completando\nlogros, y ganando trofeos en desafíos.", + "welcome2Text": "También puedes ganar tickets desde varias actividades similares.\nLos tickets pueden ser usados para desbloquear nuevos personajes,\nmapas y mini juegos, entrar a torneos, y más.", + "yourPowerRankingText": "Tu Clasificación de Poder:" + }, + "copyOfText": "${NAME} Copiar", + "createAPlayerProfileText": "¿Crear un perfil?", + "createEditPlayerText": "", + "createText": "Crear", + "creditsWindow": { + "additionalAudioArtIdeasText": "Audio Adicional, arte Inicial, e Ideas por ${NAME}", + "additionalMusicFromText": "Música adicional de ${NAME}", + "allMyFamilyText": "Mis amigos y mi familia que ayudaron con las pruebas", + "codingGraphicsAudioText": "Código, gráficas y audio por ${NAME}", + "languageTranslationsText": "Traducciones:", + "legalText": "Legal:", + "publicDomainMusicViaText": "Música de dominio público a través de ${NAME}", + "softwareBasedOnText": "Este software está basado parcialmente en el trabajo de ${NAME}", + "songCreditText": "${TITLE} interpretada por ${PERFORMER}\nCompuesta por ${COMPOSER}, arreglos por ${ARRANGER}, publicada por ${PUBLISHER},\nCortesía de ${SOURCE}", + "soundAndMusicText": "Sonido y música:", + "soundsText": "Sonidos (${SOURCE}):", + "specialThanksText": "Agradecimientos Especiales:", + "thanksEspeciallyToText": "Gracias especialmente a ${NAME}", + "titleText": "Créditos de ${APP_NAME}", + "whoeverInventedCoffeeText": "A quien haya inventado el café" + }, + "currentStandingText": "Tu puesto actual es #${RANK}", + "customizeText": "Personalizar...", + "deathsTallyText": "${COUNT} muertes", + "deathsText": "Muertes", + "debugText": "depurar", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Nota: Es recomendable poner Ajustes->Gráficos->Texturas en 'Alto' al probar esto.", + "runCPUBenchmarkText": "Ejecutar Punto de Referencia del CPU", + "runGPUBenchmarkText": "Ejecutar Punto de Referencia del GPU", + "runMediaReloadBenchmarkText": "Ejecutar Punto de Referencia del Media-Reload", + "runStressTestText": "Ejecutar prueba de resistencia", + "stressTestPlayerCountText": "Conteo de jugadores", + "stressTestPlaylistDescriptionText": "Prueba de Resistencia de Lista de Audio", + "stressTestPlaylistNameText": "Nombre de lista", + "stressTestPlaylistTypeText": "Tipo de lista", + "stressTestRoundDurationText": "Redondear duración", + "stressTestTitleText": "Prueba de resistencia", + "titleText": "Pruebas de Resistencia y Rendimiento.", + "totalReloadTimeText": "Tiempo total de recarga: ${TIME} (Ver log para detalles)", + "unlockCoopText": "Desbloquear niveles cooperativos" + }, + "defaultFreeForAllGameListNameText": "Pelea libre (predeterminado)", + "defaultGameListNameText": "Lista ${PLAYMODE} por Defecto", + "defaultNewFreeForAllGameListNameText": "Mis Matanzas Libres", + "defaultNewGameListNameText": "Mi lista ${PLAYMODE}", + "defaultNewTeamGameListNameText": "Mis Juegos en equipo", + "defaultTeamGameListNameText": "Juego en equipo (predeterminado)", + "deleteText": "Borrar", + "demoText": "Versión de prueba", + "denyText": "Rechazar", + "desktopResText": "Resolución del escritorio", + "difficultyEasyText": "Fácil", + "difficultyHardOnlyText": "Solo Modo Difícil", + "difficultyHardText": "Difícil", + "difficultyHardUnlockOnlyText": "Este nivel solo puede ser desbloqueado en modo difícil.\n¡¿¡¿¡Piensas tener lo que se necesita!?!?!", + "directBrowserToURLText": "Por favor abre la siguiente URL en tu navegador:", + "disableRemoteAppConnectionsText": "Desactivar conexiones remotas de aplicación", + "disableXInputDescriptionText": "Permite más de 4 controladores pero puede que no funcione bien.", + "disableXInputText": "Desactivar XInput", + "doneText": "Hecho", + "drawText": "Empate", + "duplicateText": "Duplicar", + "editGameListWindow": { + "addGameText": "Añadir\nJuego", + "cantOverwriteDefaultText": "¡No puedes sobrescribir la lista predeterminada!", + "cantSaveAlreadyExistsText": "¡Ya existe una lista con ese nombre!", + "cantSaveEmptyListText": "¡No puedes guardar una lista vacía!", + "editGameText": "Editar\nJuego", + "gameListText": "Lista de juegos", + "listNameText": "Nombre de Lista", + "nameText": "Nombre", + "removeGameText": "Remover\nJuego", + "saveText": "Guardar", + "titleText": "Editor de Listas" + }, + "editProfileWindow": { + "accountProfileInfoText": "Este perfil de jugador especial tiene un nombre\ny un icono basado en tu cuenta.\n\n${ICONS}\n\nCrea perfiles personalizados si quieres usar\ndiferentes nombres o iconos personalizados.", + "accountProfileText": "(perfil de cuenta)", + "availableText": "El nombre \"${NAME}\" está disponible.", + "changesNotAffectText": "Nota: los cambios no afectaran los personajes dentro del juego", + "characterText": "personaje", + "checkingAvailabilityText": "Revisando la disponibilidad para \"${NAME}\"...", + "colorText": "color", + "getMoreCharactersText": "Consigue Más Personajes...", + "getMoreIconsText": "Conseguir Más Iconos...", + "globalProfileInfoText": "Los perfiles globales tienen un nombre único\na nivel mundial. También tienen iconos personalizados.", + "globalProfileText": "(perfil global)", + "highlightText": "resalte", + "iconText": "icono", + "localProfileInfoText": "Los perfiles locales no tienen iconos y no hay garantía\nque los nombres sean únicos. Obtén un perfil global\npara reservar un nombre único y tengas un icono personalizado.", + "localProfileText": "(perfil local)", + "nameDescriptionText": "Nombre de Jugador", + "nameText": "Nombre", + "randomText": "aleatorio", + "titleEditText": "Editar Perfil", + "titleNewText": "Nuevo Perfil", + "unavailableText": "\"${NAME}\" no está disponible; intenta otro nombre.", + "upgradeProfileInfoText": "Esto reservará tu nombre de jugador a nivel mundial\ny podrás asignarle un icono personalizado.", + "upgradeToGlobalProfileText": "Actualizar a Perfil Global" + }, + "editProfilesAnyTimeText": "(edita tu perfil en cualquier momento bajo 'ajustes')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "No puedes eliminar la pista de audio predeterminada.", + "cantEditDefaultText": "No puedes editar la pista de audio predeterminada. Duplica o crea una nueva.", + "cantEditWhileConnectedOrInReplayText": "No puedes editar listas de audio mientras juegas con alguien o en repeticiones.", + "cantOverwriteDefaultText": "No puedes sobreescribir la pista de audio predeterminada", + "cantSaveAlreadyExistsText": "¡Ya existe una banda sonora con ese nombre!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Pista de Audio Predeterminada", + "deleteConfirmText": "Eliminar pista de audio:\n\n'${NAME}'?", + "deleteText": "Eliminar\nPista de Audio", + "duplicateText": "Duplicar\nPista de Audio", + "editSoundtrackText": "Editor de Pistas de Audio", + "editText": "Editar\nPista de Audio", + "fetchingITunesText": "buscando listas de reproducción de aplicaciones de música...", + "musicVolumeZeroWarning": "Advertencia: el volumen de la música está en 0", + "nameText": "Nombre", + "newSoundtrackNameText": "Mi Lista de Audio ${COUNT}", + "newSoundtrackText": "Nueva Pista de Audio:", + "newText": "Nueva\nPista de Audio", + "selectAPlaylistText": "Elige una Lista de Reproducción", + "selectASourceText": "Fuente de la Música", + "soundtrackText": "pista de audio", + "testText": "escuchar", + "titleText": "Pistas de Audio", + "useDefaultGameMusicText": "Música por Defecto", + "useITunesPlaylistText": "Lista de reproducción de la aplicación de música", + "useMusicFileText": "Archivo de Audio (MP3, etc)", + "useMusicFolderText": "Carpeta de Archivos de Audio" + }, + "editText": "Editar", + "endText": "Fin", + "enjoyText": "Diviértete!", + "epicDescriptionFilterText": "${DESCRIPTION} En cámara lenta épica.", + "epicNameFilterText": "${NAME} - Modo épico", + "errorAccessDeniedText": "acceso negado", + "errorOutOfDiskSpaceText": "insuficiente espacio en disco", + "errorText": "Error", + "errorUnknownText": "error desconocido", + "exitGameText": "¿Salir de ${APP_NAME}?", + "exportSuccessText": "'${NAME}' exportado.", + "externalStorageText": "Almacenamiento Externo", + "failText": "Fallaste", + "fatalErrorText": "Uh oh, ¡algo está mal por aquí!\nPor favor intenta reinstalar la app o\ncontacta a ${EMAIL} para ayuda.", + "fileSelectorWindow": { + "titleFileFolderText": "Elige un Archivo o Carpeta", + "titleFileText": "Elige un Archivo", + "titleFolderText": "Elige una Carpeta", + "useThisFolderButtonText": "Usar Esta Carpeta" + }, + "filterText": "Filtro", + "finalScoreText": "Puntaje Final", + "finalScoresText": "Puntajes Finales", + "finalTimeText": "Tiempo final", + "finishingInstallText": "Instalación casi lista; espera un momento...", + "fireTVRemoteWarningText": "* Para una mejor experiencia, \nutiliza controles o instala la\napp '${REMOTE_APP_NAME}' en tus\ndispositivos móviles.", + "firstToFinalText": "Primero de ${COUNT} Final", + "firstToSeriesText": "Primero de ${COUNT} Serie", + "fiveKillText": "¡¡¡COMBO QUÍNTUPLE!!!", + "flawlessWaveText": "¡Horda perfecta!", + "fourKillText": "¡¡¡COMBO CUÁDRUPLE!!!", + "freeForAllText": "Pelea libre", + "friendScoresUnavailableText": "Puntaje no disponible.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Líderes del juego ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "No puedes eliminar la lista predeterminada.", + "cantEditDefaultText": "¡No puedes editar la lista predeterminada! Duplicala o crea una nueva.", + "cantShareDefaultText": "No puedes compartir la lista de reproducción por defecto", + "deleteConfirmText": "¿Borrar \"${LIST}\"?", + "deleteText": "Borrar\nLista", + "duplicateText": "Duplicar\nLista", + "editText": "Editar\nLista", + "gameListText": "Lista de juego", + "newText": "Nueva\nLista", + "showTutorialText": "Ver Tutorial", + "shuffleGameOrderText": "Orden de juego al azar", + "titleText": "Personaliza listas de ${TYPE}" + }, + "gameSettingsWindow": { + "addGameText": "Agrega juego" + }, + "gamepadDetectedText": "1 control detectado.", + "gamepadsDetectedText": "${COUNT} controles detectados.", + "gamesToText": "${WINCOUNT} juegos a ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Recuerda: cualquier dispositivo en la fiesta puede\ntener mas de un jugador si tienen controles suficientes.", + "aboutDescriptionText": "Usa estas pestañas para crear una fiesta.\n\nLas fiestas te permiten jugar y competir con\ntus amigos con sus propios dispositivos.\n\nUsa el botón ${PARTY} en la parte superior\nderecha para chatear e interactuar con tu fiesta.\n(En el control, presiona ${BUTTON} mientras estés en el menú)", + "aboutText": "Acerca de", + "addressFetchErrorText": "", + "appInviteInfoText": "Invita amigos a probar BombSquad y recibirán\n${COUNT} ticketes gratis. Tu obtendrás\n${YOU_COUNT} por cada amigo que acepte.", + "appInviteMessageText": "${NAME} te envió ${COUNT} boletos en ${APP_NAME}", + "appInviteSendACodeText": "Enviales un código", + "appInviteTitleText": "Invitación de ${APP_NAME}", + "bluetoothAndroidSupportText": "(Funciona con cualquier dispositivo Android con Bluetooth)", + "bluetoothDescriptionText": "Crea/únete a una fiesta vía Bluetooth:", + "bluetoothHostText": "Crea vía Bluetooth", + "bluetoothJoinText": "Únete vía Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "revisando...", + "copyCodeConfirmText": "Se ha copiado en el portapapeles.", + "copyCodeText": "Copiar", + "dedicatedServerInfoText": "Para mejores resultados, crea un servidor dedicado. Visita bombsquadgame.com/server para aprender cómo hacerlo", + "disconnectClientsText": "Esto desconectará a ${COUNT} jugador(es) de\ntu fiesta. ¿Estás seguro?", + "earnTicketsForRecommendingAmountText": "Tus amigos recibirán ${COUNT} boletos si prueban el juego\n(Y recibirás ${YOU_COUNT} boletos por cada amigo que pruebe el juego)", + "earnTicketsForRecommendingText": "Comparte el juego\na cambio de boletos gratis...", + "emailItText": "Enviar correo", + "favoritesSaveText": "Guardar como favorito", + "favoritesText": "Favoritos", + "freeCloudServerAvailableMinutesText": "El siguiente server estara disponible en ${MINUTES} minutos", + "freeCloudServerAvailableNowText": "Servidor gratuito disponible!", + "freeCloudServerNotAvailableText": "No hay servidores gratis disponibles ahora", + "friendHasSentPromoCodeText": "Recibiste ${COUNT} boletos ${APP_NAME} de ${NAME}", + "friendPromoCodeAwardText": "Recibirás ${COUNT} boletos cada vez que lo uses.", + "friendPromoCodeExpireText": "El código expirará en ${EXPIRE_HOURS} horas y solo lo podrán usar jugadores nuevos.", + "friendPromoCodeInstructionsText": "Para usarlo, abre ${APP_NAME} y ve a \"Ajustes-> Avanzado-> Ingresar código\"\nVisita bombsquadgame.com para enlaces de descarga de todas las plataformas disponibles.", + "friendPromoCodeRedeemLongText": "Se puede canjear por ${COUNT} boletos gratis hasta ${MAX_USES} personas.", + "friendPromoCodeRedeemShortText": "Puede ser canjeado por ${COUNT} boletos en el juego.", + "friendPromoCodeWhereToEnterText": "(en \"Configuración-> Avanzado-> Introducir código\")", + "getFriendInviteCodeText": "Genera código para un amigo", + "googlePlayDescriptionText": "Invita a jugadores de Google Play a tu fiesta:", + "googlePlayInviteText": "Invitar", + "googlePlayReInviteText": "Hay ${COUNT} jugadores de Google Play en tu fiesta\nque se desconectarán si creas una nueva invitación.\nInclúyelos en la nueva invitación para tenerlos de vuelta.", + "googlePlaySeeInvitesText": "Ver invitaciones", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Versión Android / Google Play)", + "hostPublicPartyDescriptionText": "Crear una Fiesta Pública", + "hostingUnavailableText": "Alojamiento No Disponible", + "inDevelopmentWarningText": "Nota:\n\nJugar en Red es una función todavía en desarrollo.\nPor ahora, es altamente recomendado que todos\nlos jugadores estén en la misma red Wi-Fi", + "internetText": "Internet", + "inviteAFriendText": "¿Tus amigos no tienen el juego? Invítalos a\nprobarlo y recibirán ${COUNT} boletos gratis.", + "inviteFriendsText": "Invita a amigos", + "joinPublicPartyDescriptionText": "Entrar en una fiesta publica", + "localNetworkDescriptionText": "Unirse a una fiesta en tu red", + "localNetworkText": "Red Local", + "makePartyPrivateText": "Crea mi Fiesta Privada", + "makePartyPublicText": "Crea mi Fiesta Pública", + "manualAddressText": "Dirección", + "manualConnectText": "Conectar", + "manualDescriptionText": "Únete a una fiesta usando la dirección IP:", + "manualJoinSectionText": "Unirse por dirección", + "manualJoinableFromInternetText": "¿Se te pueden unir vía Internet?", + "manualJoinableNoWithAsteriskText": "NO*", + "manualJoinableYesText": "SI", + "manualRouterForwardingText": "*para arreglarlo, configura en tu router que redireccione el puerto UDP ${PORT} a tu dirección local", + "manualText": "Manual", + "manualYourAddressFromInternetText": "Tu dirección desde internet:", + "manualYourLocalAddressText": "Tu dirección local:", + "nearbyText": "Cerca", + "noConnectionText": "", + "otherVersionsText": "(Otras versiones)", + "partyCodeText": "Código de fiesta", + "partyInviteAcceptText": "Aceptar", + "partyInviteDeclineText": "Denegar", + "partyInviteGooglePlayExtraText": "(Busca la pestaña 'Google Play' en la ventana 'Reúne'", + "partyInviteIgnoreText": "Ignorar", + "partyInviteText": "${NAME} te ha invitado\na unirse a su fiesta!", + "partyNameText": "Nombre de la fiesta", + "partyServerRunningText": "Tu servidor de fiesta está en línea", + "partySizeText": "Tamaño de la fiesta", + "partyStatusCheckingText": "Comprobando estado...", + "partyStatusJoinableText": "ya se pueden conectar a tu fiesta por internet", + "partyStatusNoConnectionText": "Incapaz de conectar al servidor", + "partyStatusNotJoinableText": "No se van a poder conectar a tu fiesta por internet", + "partyStatusNotPublicText": "Tu fiesta no es publica", + "pingText": "Ping", + "portText": "Puerto", + "privatePartyCloudDescriptionText": "Fiestas privadas se ejecutan en la nube; no requiere configuración del router.", + "privatePartyHostText": "Hacer una Fiesta privada", + "privatePartyJoinText": "Únete a una fiesta privada", + "privateText": "Privado", + "publicHostRouterConfigText": "Está opción requiere una configuración de puerto en tu router para una opción más fácil crea una fiesta privada", + "publicText": "Publico", + "requestingAPromoCodeText": "Pidiendo código...", + "sendDirectInvitesText": "Enviar directamente la invitación", + "shareThisCodeWithFriendsText": "Comparte este código con tus amigos:", + "showMyAddressText": "Mostrar mi dirección", + "startHostingPaidText": "Anfitrión Ahora Por ${COST}", + "startHostingText": "Anfitrión", + "startStopHostingMinutesText": "Puede iniciar y detener el alojamiento de forma gratuita durante los próximos ${MINUTES} minutos.", + "stopHostingText": "Dejar de alojar", + "titleText": "Reúne", + "wifiDirectDescriptionBottomText": "Si todo los dispositivos disponen de 'Wi-fi Directo', deberían poder utiizarlo para\nencontrar y conectarse entre ellos. Cuando todos estén coneectados, pudes formar \nfiestas, usando la pestaña 'Red Local', como si estuvieran en la misma red Wi-fi.\n\nPara mejores resultados, el host de Wi-fi Directo también debe de ser el host de la fiesta en ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi Directo puede ser utilizado para conectar dispositivos Android sin\ntener que utilizar una red Wi-Fi. Esto funciona mejor de Android 4.2 o mas nuevo.\n\nPara utilizarlo, abre los ajustes de Wi-Fi y busca 'Wi-Fi Directo' en el menú.", + "wifiDirectOpenWiFiSettingsText": "Abrir Ajustes Wi-Fi", + "wifiDirectText": "Wi-Fi Directo", + "worksBetweenAllPlatformsText": "(funciona con todas las plataformas)", + "worksWithGooglePlayDevicesText": "(funciona con dispositivos que corran la versión Google Play (Android) del juego)", + "youHaveBeenSentAPromoCodeText": "Has enviado un código de ${APP_NAME}:" + }, + "getCoinsWindow": { + "coinDoublerText": "Duplicador de monedas", + "coinsText": "${COUNT} monedas", + "freeCoinsText": "Monedas gratis", + "restorePurchasesText": "Reestablecer compras", + "titleText": "Obtener monedas" + }, + "getTicketsWindow": { + "freeText": "¡GRATIS!", + "freeTicketsText": "Tickets Gratis", + "inProgressText": "Ya hay una transacción en progreso; por favor inténtalo mas tarde.", + "purchasesRestoredText": "Compras Restauradas.", + "receivedTicketsText": "¡${COUNT} boletos recibidos!", + "restorePurchasesText": "Restaurar Compras", + "ticketDoublerText": "Duplicador de Tiquetes", + "ticketPack1Text": "Paquete de Boletos Pequeño", + "ticketPack2Text": "Paquete de Boletos Mediano", + "ticketPack3Text": "Paquete de Boletos Grande", + "ticketPack4Text": "Paquete de Boletos Jumbo", + "ticketPack5Text": "Paquete de Boletos Mamut", + "ticketPack6Text": "Paquete de Boletos Supremo", + "ticketsFromASponsorText": "Obtén ${COUNT} boletos\nde un patrocinador", + "ticketsText": "${COUNT} Boletos", + "titleText": "Obtén Boletos", + "unavailableLinkAccountText": "Lo sentimos, las compras no están disponibles en esta plataforma.\nComo una solución, puedes enlazar esta cuenta a otra\nplataforma y hacer tus compras desde ahí.", + "unavailableTemporarilyText": "No disponible por el momento; por favor inténtalo más tarde.", + "unavailableText": "Lo sentimos, no se encuentra disponible.", + "versionTooOldText": "Lo sentimos, esta versión está demasiado atrasada; por favor actualiza el juego.", + "youHaveShortText": "tienes ${COUNT}", + "youHaveText": "tienes ${COUNT} boletos" + }, + "googleMultiplayerDiscontinuedText": "Lo siento, Google's multijugador servicio ya no esta mas disponible.\nEstoy trabajando en un reemplazo lo mas rapido posible.\nHasta entonces, por favor intente con otro metodo de conexión.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Siempre", + "fullScreenCmdText": "Pantalla completa (Cmd-F)", + "fullScreenCtrlText": "Pantalla completa (Ctrl-F)", + "gammaText": "Gamma", + "highText": "Alta", + "higherText": "Superior", + "lowText": "Baja", + "mediumText": "Mediana", + "neverText": "Nunca", + "resolutionText": "Resolución", + "showFPSText": "Mostrar FPS", + "texturesText": "Texturas", + "titleText": "Gráficas", + "tvBorderText": "Marco de TV", + "verticalSyncText": "Sincronización vertical", + "visualsText": "Visuales" + }, + "helpWindow": { + "bombInfoText": "- Bomba -\nMas fuerte que los golpes, pero puede\nresultar en auto-lesiones bastante graves.\nPara mejores resultados, tírala hacia tus\noponentes antes de que explote.", + "bombInfoTextScale": 0.57, + "canHelpText": "${APP_NAME} te puede ayudar.", + "controllersInfoText": "Puedes jugar ${APP_NAME} con tus amigos en una red, o todos pueden\njugar en el mismo dispositivo si se tienen varios controles.\n${APP_NAME} soporta una gran variedad de ellos; incluso puedes utilizar\ntus dispositivos móviles como controles instalando la app '${REMOTE_APP_NAME}'.\nVe a 'Ajustes-> Controles' para más información.", + "controllersInfoTextFantasia": "Un jugador puede usar el mando a distancia para jugar, pero\nse recomienda usar gamepads. También puedes usar dispositivos\niOS o Android como controles con la app \"BombSquad Remote\".\nRevisa la sección \"Controles\" en los Ajustes para más información.", + "controllersInfoTextMac": "Uno o dos jugadores pueden usar el teclado, pero BombSquad es mejor\ncon controles. BombSquad puede usar controles USB, controles de PS3,\ncontroles de Xbox 360, controles Wii y dispositivos iOS/Android para\ncontrolar los personajes. Espero que tengas algunos de ellos a la mano. \nConsulta 'Controles' bajo 'Ajustes' para más información.", + "controllersInfoTextOuya": "Puedes usar controles OUYA, controles de PS3, controles de Xbox\n360, y muchos de otros controles USB y Bluetooth con BombSquad.\nPuedes usar también dispositivos iOS/Android como controles con la aplicación \n'BombSquad Remote'. Consulta 'Controles' bajo 'Ajustes' para más información.", + "controllersText": "Controles", + "controllersTextScale": 0.67, + "controlsSubtitleText": "Tu personaje de ${APP_NAME} tiene algunas acciones básicas:", + "controlsSubtitleTextScale": 0.7, + "controlsText": "Controles", + "controlsTextScale": 1.4, + "devicesInfoText": "La version VR de ${APP_NAME} se puede jugar en red con la versión \nregular, así que saca tus celulares, tablets y PCs extra y que \nempieze el juego. Es muy útil conectar un dispositivo con la versión\nregular a uno con la versión VR para que todos los que esten fuera\npuedan ver la acción.", + "devicesText": "Dispositivos", + "friendsGoodText": "Cuantos más, mejor. ${APP_NAME} es más divertido con muchos \namigos y soporta hasta 8 a la vez, lo que nos lleva a:", + "friendsGoodTextScale": 0.62, + "friendsText": "Amigos", + "friendsTextScale": 0.67, + "jumpInfoText": "- Salto -\nSalta para cruzar pequeños\nespacios, tirar cosas más lejos,\ny para expresar alegría.", + "jumpInfoTextScale": 0.6, + "orPunchingSomethingText": "O golpear algo, tirarlo por un precipicio, y explotarlo en plena caída con una bomba pegajosa.", + "orPunchingSomethingTextScale": 0.51, + "pickUpInfoText": "- Levanta -\nAlza banderas, enemigos, o cualquier\notra cosa no atornillada al suelo.\nPulsa de nuevo para lanzar.", + "pickUpInfoTextScale": 0.6, + "powerupBombDescriptionText": "Puedes tirar tres bombas de\nun solo tiro en vez de una sola.", + "powerupBombNameText": "Bombas Triple", + "powerupCurseDescriptionText": "Probablemente querrás evitar estos.\n...¿o quizás no?", + "powerupCurseNameText": "Maldición", + "powerupHealthDescriptionText": "Restaura toda la salud.\nNunca habrías imaginado.", + "powerupHealthNameText": "Caja de salud", + "powerupIceBombsDescriptionText": "Más débil que las bombas habituales\npero dejan a tus enemigos congelados\ny particularmente frágiles.", + "powerupIceBombsNameText": "Bombas de hielo", + "powerupImpactBombsDescriptionText": "Levemente más débiles que las bombas\nnormales, pero explotan al impacto.", + "powerupImpactBombsNameText": "Insta-Bombas", + "powerupLandMinesDescriptionText": "Estas vienen en grupos de 3;\nSon buenas para defensa territorial\no para detener enemigos veloces.", + "powerupLandMinesNameText": "Mina Terrestre", + "powerupPunchDescriptionText": "Hace que tus golpes sean más duros,\nmás rápidos, mejores, y más fuertes.", + "powerupPunchNameText": "Guantes de Boxeo", + "powerupShieldDescriptionText": "Absorbe un poco del impacto\npara que tu no tengas que hacerlo.", + "powerupShieldNameText": "Escudo de Energía", + "powerupStickyBombsDescriptionText": "Se adhieren a cualquier cosa.\nEn serio, es demasiado gracioso.", + "powerupStickyBombsNameText": "Bombas Pegajosas", + "powerupsSubtitleText": "Por supuesto, ningún juego está completo sin poderes extra:", + "powerupsSubtitleTextScale": 0.8, + "powerupsText": "Poderes Extra", + "powerupsTextScale": 1.4, + "punchInfoText": "- Golpe -\nEntre mas rápido te muevas más\nimpacto causan tus golpes, así que\ncorre, salta y da vueltas como loco.", + "punchInfoTextScale": 0.6, + "runInfoText": "- Correr -\nSostén CUALQUIER botón para correr. Los botones gatillos o traseros funcionan bien si los tienes.\nCorrer te hace llegar mas rápido pero es mas difícil girar, así que ten cuidado con los barrancos.", + "runInfoTextScale": 0.6, + "someDaysText": "Hay días cuando sientes ganas de romper algo. O explotar algo.", + "someDaysTextScale": 0.66, + "titleText": "Ayuda ${APP_NAME}", + "toGetTheMostText": "Para sacar el máximo partido a este juego, necesitas:", + "toGetTheMostTextScale": 1.0, + "welcomeText": "¡Bienvenido a ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} navega los menús como todo un pro -", + "importPlaylistCodeInstructionsText": "Usa el siguiente código para importar esta lista de reproducción en otra parte", + "importPlaylistSuccessText": "Lista de reproducción '${NAME}' importada ${TYPE}", + "importText": "Importar", + "importingText": "Importando...", + "inGameClippedNameText": "en el juego será\n\"${NAME}\"", + "installDiskSpaceErrorText": "ERROR: Incapaz de completar la instalación\nPuede que te hayas quedado sin espacio.\nLibera un poco de tu espacio e intenta de nuevo.", + "internal": { + "arrowsToExitListText": "pulsa ${LEFT} o ${RIGHT} para salir de la lista", + "buttonText": "botón", + "cantKickHostError": "No puedes expulsar al host", + "chatBlockedText": "A ${NAME} se le ha bloqueado el chat por ${TIME} segundos.", + "connectedToGameText": "'${NAME}' se unió", + "connectedToPartyText": "¡Unido a la fiesta de ${NAME}!", + "connectingToPartyText": "Conectando...", + "connectionFailedHostAlreadyInPartyText": "Conexión fallida; Host esta en otra fiesta.", + "connectionFailedPartyFullText": "Conexión fallida; la fiesta está llena.", + "connectionFailedText": "Conexión fallida.", + "connectionFailedVersionMismatchText": "Conexión fallida; Host corre una versión diferente del juego.\nAsegúrese de que ambos están actualizados y vuelva a intentar.", + "connectionRejectedText": "Conexión rechazada.", + "controllerConnectedText": "${CONTROLLER} conectado.", + "controllerDetectedText": "1 control detectado.", + "controllerDisconnectedText": "${CONTROLLER} desconectado.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} desconectado. Intenta conectarlo de nuevo.", + "controllerForMenusOnlyText": "Este control no puede ser usado para jugar; solo para navegar por menus.", + "controllerReconnectedText": "${CONTROLLER} reconectado.", + "controllersConnectedText": "${COUNT} controles conectados.", + "controllersDetectedText": "${COUNT} controles detectados.", + "controllersDisconnectedText": "${COUNT} controles desconectados.", + "corruptFileText": "Archivos corruptos detectados. Por favor reinstala el juego o contacta ${EMAIL}", + "errorPlayingMusicText": "Error al reproducir música: ${MUSIC}", + "errorResettingAchievementsText": "No se pudieron reiniciar los logros; por favor inténtalo de nuevo.", + "hasMenuControlText": "${NAME} Tiene control del menú.", + "incompatibleNewerVersionHostText": "El host esta corriendo una versión nueva del juego.\nActualiza a la última versión y vuelve a intentarlo.", + "incompatibleVersionHostText": "Host corre una versión diferente del juego; imposible conectar.\nAsegúrese de que ambos están actualizados y vuelva a intentar.", + "incompatibleVersionPlayerText": "${NAME} está corriendo una versión diferente del juego y no se ha podido conectar.\nAsegúrese de que ambos están actualizados y vuelva a intentar.", + "invalidAddressErrorText": "Error: Dirección inválida.", + "invalidNameErrorText": "Error: Nombre invalido", + "invalidPortErrorText": "Error: Puerto inválido.", + "invitationSentText": "Invitación enviada.", + "invitationsSentText": "${COUNT} invitaciones enviadas.", + "joinedPartyInstructionsText": "Alguien se ha unido a tu fiesta\nVe a 'Jugar' para iniciar el juego", + "keyboardText": "Teclado", + "kickIdlePlayersKickedText": "Sacando a ${NAME} por no mostrar movimiento.", + "kickIdlePlayersWarning1Text": "${NAME} será sacado en ${COUNT} segundos si no presenta movimiento.", + "kickIdlePlayersWarning2Text": "(puedes apagar esto en Ajustes -> Avanzado)", + "leftGameText": "'${NAME}' salió", + "leftPartyText": "Dejó la fiesta de ${NAME}.", + "noMusicFilesInFolderText": "La carpeta no contiene archivos de audio.", + "playerJoinedPartyText": "¡${NAME} se unió a la fiesta!", + "playerLeftPartyText": "${NAME} se fue de la fiesta.", + "rejectingInviteAlreadyInPartyText": "Rechazando invitación (Ya en una fiesta).", + "serverRestartingText": "El servidor se está reiniciando. Conectese en un momento...", + "serverShuttingDownText": "El servidor está fuera de servicio...", + "signInErrorText": "Error iniciando sesión.", + "signInNoConnectionText": "Imposible iniciar sesión (¿No hay conexión a internet?)", + "teamNameText": "Equipo ${NAME}", + "telnetAccessDeniedText": "ERROR: El usuario no concedió acceso telnet.", + "timeOutText": "(tiempo fuera en ${TIME} segundos)", + "touchScreenJoinWarningText": "Te uniste con la pantalla táctil.\nSi este fue un error, presiona 'Menú->Dejar juego' con ella.", + "touchScreenText": "Pantalla táctil", + "trialText": "prueba", + "unableToResolveHostText": "Error: no se ha podido resolver el host", + "unavailableNoConnectionText": "No disponible por el momento (¿No tienes conexión a internet?)", + "vrOrientationResetCardboardText": "Usa esto para reiniciarr la orientación del VR.\nPara jugar el juego necesitarás un control externo.", + "vrOrientationResetText": "Orientación VR reseteada.", + "willTimeOutText": "(caducará si está inactivo)" + }, + "jumpBoldText": "SALTA", + "jumpText": "Salta", + "keepText": "Mantener", + "keepTheseSettingsText": "¿Mantener estos ajustes?", + "keyboardChangeInstructionsText": "Presiona dos veces el espacio para cambiar los teclados.", + "keyboardNoOthersAvailableText": "No hay más teclados disponibles.", + "keyboardSwitchText": "Cambiando teclado a \"${NAME}\".", + "kickOccurredText": "${NAME} ha sido expulsado.", + "kickQuestionText": "¿Expulsar a ${NAME}?", + "kickText": "Expulsar", + "kickVoteCantKickAdminsText": "El administrador no puede ser expulsado", + "kickVoteCantKickSelfText": "No puedes expulsarte a ti mismo", + "kickVoteFailedNotEnoughVotersText": "No hay suficientes jugadores para votar.", + "kickVoteFailedText": "Votación de expulsión fallida", + "kickVoteStartedText": "Se ha iniciado una votación para expulsar a '${NAME}'", + "kickVoteText": "Vota para expulsar", + "kickVotingDisabledText": "El voto para expulsar no está disponible", + "kickWithChatText": "Escribe ${YES} en el chat para \"Si\" y ${NO} para \"No\".", + "killsTallyText": "eliminó ${COUNT}", + "killsText": "Víctimas", + "kioskWindow": { + "easyText": "Fácil", + "epicModeText": "Modo Épico", + "fullMenuText": "Menú Completo", + "hardText": "Difícil", + "mediumText": "Medio", + "singlePlayerExamplesText": "Ejemplos 1 Jugador / Varios jugadores", + "versusExamplesText": "Ejemplos Versus" + }, + "languageSetText": "El lenguaje es ahora \"${LANGUAGE}\".", + "lapNumberText": "Vuelta ${CURRENT}/${TOTAL}", + "lastGamesText": "(últimos ${COUNT} juegos)", + "leaderboardsText": "Clasificaciones", + "league": { + "allTimeText": "De todo el tiempo", + "currentSeasonText": "Temporada en curso (${NUMBER})", + "leagueFullText": "Liga ${NAME}", + "leagueRankText": "Liga Rank", + "leagueText": "Liga", + "rankInLeagueText": "#${RANK}, ${NAME} Liga${SUFFIX}", + "seasonEndedDaysAgoText": "La temporada terminó hace ${NUMBER} día(s).", + "seasonEndsDaysText": "La temporada termina en ${NUMBER} día(s).", + "seasonEndsHoursText": "La temporada termina en ${NUMBER} hora(s).", + "seasonEndsMinutesText": "La temporada termina en ${NUMBER} minuto(s).", + "seasonText": "Temporada ${NUMBER}", + "tournamentLeagueText": "Debes alcanzar la liga ${NAME} para entrar a este torneo.", + "trophyCountsResetText": "El conteo de trofeos se reiniciará la próxima temporada." + }, + "levelBestScoresText": "Mejores puntajes en ${LEVEL}", + "levelBestTimesText": "Mejores tiempos en ${LEVEL}", + "levelFastestTimesText": "Mejores tiempos en ${LEVEL}", + "levelHighestScoresText": "Puntajes más altos en ${LEVEL}", + "levelIsLockedText": "${LEVEL} está bloqueado.", + "levelMustBeCompletedFirstText": "${LEVEL} debe completarse primero.", + "levelText": "Nivel ${NUMBER}", + "levelUnlockedText": "¡Nivel desbloqueado!", + "livesBonusText": "Bono de vidas", + "loadingText": "cargando", + "loadingTryAgainText": "Cargando; vuelve a intentarlo en un momento...", + "macControllerSubsystemBothText": "Ambos (no recomendado)", + "macControllerSubsystemClassicText": "Clásico", + "macControllerSubsystemDescriptionText": "(Intenta cambiar esto si tus controladores no están funcionando)", + "macControllerSubsystemMFiNoteText": "Control hecho para iOS/Mac detectado;\nTal vez quieras activar esto en Configuracion -> Controles", + "macControllerSubsystemMFiText": "Creado-para-iOS/Mac", + "macControllerSubsystemTitleText": "Soporte de controladores", + "mainMenu": { + "creditsText": "Créditos", + "demoMenuText": "Menú Demo", + "endGameText": "Salir del juego", + "exitGameText": "Salir del juego", + "exitToMenuText": "¿Salir al menú?", + "howToPlayText": "Cómo jugar", + "justPlayerText": "(Solo ${NAME})", + "leaveGameText": "Dejar Juego", + "leavePartyConfirmText": "¿Seguro que deseas dejar la fiesta?", + "leavePartyText": "Dejar Fiesta", + "leaveText": "Abandonar", + "quitText": "Salir", + "resumeText": "Reanudar", + "settingsText": "Ajustes" + }, + "makeItSoText": "Que así sea", + "mapSelectGetMoreMapsText": "Conseguir más Mapas...", + "mapSelectText": "Seleccionar…", + "mapSelectTitleText": "Pistas: ${GAME}", + "mapText": "Pista", + "maxConnectionsText": "Conexiones maximas", + "maxPartySizeText": "Capacidad máxima de la fiesta", + "maxPlayersText": "Jugadores máximos", + "modeArcadeText": "Modo Arcade", + "modeClassicText": "Modo Clásico", + "modeDemoText": "Modo De Demostración", + "mostValuablePlayerText": "Jugador más valorado", + "mostViolatedPlayerText": "Jugador más Violado", + "mostViolentPlayerText": "Jugador más Violento", + "moveText": "Mover", + "multiKillText": "¡¡¡${COUNT}-COMBO!!!", + "multiPlayerCountText": "${COUNT} jugadores", + "mustInviteFriendsText": "Nota: debes invitar a tus amigos en\nel panel \"${GATHER}\" o conectar\ncontroles para jugar multijugador.", + "nameBetrayedText": "${NAME} traicionó a ${VICTIM}.", + "nameDiedText": "${NAME} ha muerto.", + "nameKilledText": "${NAME} mató a ${VICTIM}.", + "nameNotEmptyText": "¡El nombre no puede quedar vacío!", + "nameScoresText": "¡${NAME} anotó!", + "nameSuicideKidFriendlyText": "${NAME} murió accidentalmente.", + "nameSuicideText": "${NAME} se suicidó.", + "nameText": "Nombre", + "nativeText": "Nativo", + "newPersonalBestText": "¡Nuevo récord personal!", + "newTestBuildAvailableText": "¡Una nueva build de prueba está disponible! (${VERSION} build ${BUILD}).\nObténla en ${ADDRESS}", + "newText": "Nuevo", + "newVersionAvailableText": "¡Una nueva version de ${APP_NAME} está disponible! (${VERSION})", + "nextAchievementsText": "Siguientes Logros:", + "nextLevelText": "Próximo nivel", + "noAchievementsRemainingText": "- ninguno", + "noContinuesText": "(sin re-intentos)", + "noExternalStorageErrorText": "No se encontraron almacenamientos externos en este dispositivo", + "noGameCircleText": "Error: No haz autenticado con GameCircle", + "noJoinCoopMidwayText": "Jugadores no pueden unirse en mitad de un juego.", + "noProfilesErrorText": "No haz creado ningún perfil, por lo que tendrás que llamarte '${NAME}'.\nVe a Ajustes>Perfiles para que te hagas un perfil.", + "noScoresYetText": "Sin puntuaciones aún.", + "noThanksText": "No gracias", + "noTournamentsInTestBuildText": "ADVERTENCIA: los puntajes de los torneos en esta versión de prueba serán ignorados.", + "noValidMapsErrorText": "No hay pistas para este tipo de juego.", + "notEnoughPlayersRemainingText": "Cantidad necesaria de jugadores no coincide; inicia un juego nuevo.", + "notEnoughPlayersText": "¡Necesitas al menos ${COUNT} jugadores para empezar este juego!", + "notNowText": "Ahora no", + "notSignedInErrorText": "Necesitas registrarte para hacer esto.", + "notSignedInGooglePlayErrorText": "Debes iniciar sesión con Google Play para hacer esto.", + "notSignedInText": "No registrado", + "nothingIsSelectedErrorText": "¡No hay nada seleccionado!", + "numberText": "#${NUMBER}", + "offText": "Apagado", + "okText": "Aceptar", + "onText": "Encendido", + "oneMomentText": "Un momento...", + "onslaughtRespawnText": "${PLAYER} reaparecerá en la horda ${WAVE}", + "orText": "${A} o ${B}", + "otherText": "Otros...", + "outOfText": "(#${RANK} de ${ALL})", + "ownFlagAtYourBaseWarning": "Tu propia bandera debe estar\nen su base para anotar!", + "packageModsEnabledErrorText": "Juegos de red no están permitidos mientras paquetes modificadores estén habilitados (ver Ajustes->Avanzado)", + "partyWindow": { + "chatMessageText": "Mensaje del Chat", + "emptyText": "Tu fiesta esta vacia", + "hostText": "(host)", + "sendText": "Enviar", + "titleText": "Tu Fiesta" + }, + "pausedByHostText": "(pausado por host)", + "perfectWaveText": "¡Horda perfecta!", + "pickUpBoldText": "LEVANTAR", + "pickUpText": "Levantar", + "playModes": { + "coopText": "Cooperativo", + "freeForAllText": "Todos contra Todos", + "multiTeamText": "Múltiples Equipos", + "singlePlayerCoopText": "Sólo un Jugador / Co-op", + "teamsText": "Equipos" + }, + "playText": "Jugar", + "playWindow": { + "coopText": "Cooperativo", + "freeForAllText": "Pelea libre", + "oneToFourPlayersText": "1-4 jugadores", + "teamsText": "Equipos", + "titleText": "Jugar", + "twoToEightPlayersText": "2-8 jugadores" + }, + "playerCountAbbreviatedText": "${COUNT}j", + "playerDelayedJoinText": "${PLAYER} entrará al comienzo de la siguiente ronda.", + "playerInfoText": "Información del Jugador", + "playerLeftText": "${PLAYER} abandonó la partida", + "playerLimitReachedText": "Límite de ${COUNT} jugadores alcanzado; no se permiten más.", + "playerLimitReachedUnlockProText": "Mejora a \"${PRO}\" en la tienda para jugar con más de ${COUNT} jugadores.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "No puedes borrar el perfil de tu cuenta.", + "deleteButtonText": "Borrar\nPerfil", + "deleteConfirmText": "Borrar '${PROFILE}'?", + "editButtonText": "Editar\nPerfil", + "explanationText": "(nombres de jugadores personalizados y apariencias para esta cuenta)", + "newButtonText": "Nuevo\nPerfil", + "titleText": "Perfil de jugadores" + }, + "playerText": "Jugador", + "playlistNoValidGamesErrorText": "Esta lista de juegos contiene juegos desbloqueables no válidos.", + "playlistNotFoundText": "lista no encontrada", + "playlistText": "Lista de reproducción", + "playlistsText": "Listas de Juego", + "pleaseRateText": "Si te gusta ${APP_NAME}, por favor tomate un momento\npara calificar o escribir una reseña. Esto proporcionará\ninformación útil y ayuda al soporte del futuro desarrollo del juego.\n\n¡gracias!\n-eric", + "pleaseWaitText": "Por favor, espera...", + "pluginsDetectedText": "Nuevos plugin(s) detectados. Activar/configurarlos en los ajustes.", + "pluginsText": "Plugins", + "practiceText": "Práctica", + "pressAnyButtonPlayAgainText": "Oprime cualquier botón jugar de nuevo...", + "pressAnyButtonText": "Oprime cualquier botón para continuar...", + "pressAnyButtonToJoinText": "Oprime cualquier botón para unirse al juego...", + "pressAnyKeyButtonPlayAgainText": "Oprime cualquier botón/tecla para jugar de nuevo...", + "pressAnyKeyButtonText": "Oprime cualquier botón/tecla para continuar...", + "pressAnyKeyText": "Oprime cualquier tecla...", + "pressJumpToFlyText": "** Salta repetidamente para volar **", + "pressPunchToJoinText": "Pulsa GOLPE para unirse al juego...", + "pressToOverrideCharacterText": "Presiona ${BUTTONS} para cambiar de personaje", + "pressToSelectProfileText": "Presiona ${BUTTONS} para seleccionar tu perfil", + "pressToSelectTeamText": "Presiona ${BUTTONS} para seleccionar un equipo", + "profileInfoText": "Crear perfiles para ti y tus amigos para que\npersonalices sus nombres, personajes y colores.", + "promoCodeWindow": { + "codeText": "Código", + "codeTextDescription": "Código promocional", + "enterText": "Ingresar" + }, + "promoSubmitErrorText": "Error al enviar código; comprueba tu conexión a Internet", + "ps3ControllersWindow": { + "macInstructionsText": "Apaga tu PS3, asegúrate que el Bluetooth esté activado en\ntu Mac, a continuación, conecta el control a tu Mac a través \nde un cable USB para sincronizarlo. A partir de entonces, se \npuede utilizar el botón de inicio del control para conectarlo con\ntu Mac ya sea por cable (USB) o el modo inalámbrico (Bluetooth).\n\nEn algunos Macs es posible que te pida una contraseña al momento de sincronizar.\nSi esto ocurre, consulta la siguiente tutoría o busca en google para obtener ayuda.\n\n\n\n\nLos controles de PS3 conectados inalámbricamente deberían de aparecer en la lista de\ndispositivos en Preferencias de Sistema->Bluetooth. Puede que tengas que eliminarlos\nde esa lista cuando quieras utilizarlos de nuevo con tu PS3.\n\nAsimismo, asegúrate de desconectarlos del Bluetooth cuando no los estés\nusando o las baterías se quedarán sin energía.\n\nEL Bluetooth puede procesar hasta 7 dispositivos,\naunque tu kilometraje puede variar.", + "macInstructionsTextScale": 0.74, + "ouyaInstructionsText": "Para usar un control de PS3 con tu OUYA, tienes que conectar lo una sola vez con\nun cable USB para sincronizarlo. AL hacer esto se pueden desconectar los otros\ncontroles, por lo que debes reiniciar el OUYA y desconectar el cable USB.\n\nA partir de entonces deberías ser capaz de usar el botón INICIO para conectarte de\nforma inalámbrica. Cuando hayas terminado de jugar, mantén pulsado el botón INICIO\ndurante 10 segundos para apagar el control, de lo contrario puede permanecer encendido\ny gastar las baterías.", + "ouyaInstructionsTextScale": 0.74, + "pairingTutorialText": "video tutorial", + "titleText": "Para usar controles de PS3 con ${APP_NAME}:" + }, + "publicBetaText": "BETA PUBLICA", + "punchBoldText": "GOLPE", + "punchText": "Golpe", + "purchaseForText": "Comprar por ${PRICE}", + "purchaseGameText": "Comprar Juego", + "purchasingText": "Comprando...", + "quitGameText": "¿Cerrar ${APP_NAME}?", + "quittingIn5SecondsText": "Abandonando en 5 segundos...", + "randomPlayerNamesText": "DEFAULT_NAMES, Pablo, Fulanito, Menganita, Martín, Franco, Pancho, Tomás, Bruno, Federico, Juan, Joaquín, Huevoduro", + "randomText": "Generar", + "rankText": "Rango", + "ratingText": "Valoración", + "reachWave2Text": "Llega hasta la segunda horda\npara clasificar.", + "readyText": "listo", + "recentText": "Reciente", + "remainingInTrialText": "permaneciendo en prueba", + "remoteAppInfoShortText": "${APP_NAME} es más divertido cuando se juega con la familia y amigos .\nPuede conectar uno o más controladores de hardware o instalar el\n${REMOTE_APP_NAME} aplicación en los teléfonos o tabletas de usarlos\ncomo controladores en el juego.", + "remote_app": { + "app_name": "Control Remoto BombSquad", + "app_name_short": "BSRemoto", + "button_position": "Posición de los botones", + "button_size": "Tamaño de los botones", + "cant_resolve_host": "No se encuentra el host.", + "capturing": "Captando...", + "connected": "Conectado.", + "description": "Usa tu teléfono o tableta como control para BombSquad.\nHasta 8 dispositivos pueden conectarse a la vez para un épico caos multijugador local en un solo TV o tableta", + "disconnected": "Desconectado por el servidor.", + "dpad_fixed": "Fijo", + "dpad_floating": "Flotante", + "dpad_position": "Posición del D-Pad", + "dpad_size": "Tamaño del D-Pad", + "dpad_type": "Tipo de D-Pad", + "enter_an_address": "Ingresa una dirección", + "game_full": "El juego está lleno o no acepta conexiones.", + "game_shut_down": "El juego se ha cerrado.", + "hardware_buttons": "Botones de Hardware", + "join_by_address": "Unirse por dirección...", + "lag": "Lag: ${SECONDS} segundos", + "reset": "Reestablecer a predeterminado", + "run1": "Ejecutar 1", + "run2": "Ejecutar 2", + "searching": "Buscando partidas de BombSquad...", + "searching_caption": "Toca el nombre de la partida para unirte.\nTen en cuenta que debes estar en la misma conexión Wi-Fi de la partida.", + "start": "Empezar", + "version_mismatch": "Las versiones no coinciden.\nAsegurate que BombSquad y BombSquad Remote\nestán actuizados e intenta de nuevo." + }, + "removeInGameAdsText": "Desbloquea \"${PRO}\" en la tienda para remover la publicidad del juego.", + "renameText": "Renombrar", + "replayEndText": "Terminar Repetición", + "replayNameDefaultText": "Repetición Último Juego", + "replayReadErrorText": "Error leyendo archivo de repetición.", + "replayRenameWarningText": "Renombra \"${REPLAY}\" despues de un juego si quieres quedarte con el; o sino será reemplazado.", + "replayVersionErrorText": "Lo sentimos, esta repetición fue hecha en una\nversión diferente del juego y no se puede usar.", + "replayWatchText": "Ver Repetición", + "replayWriteErrorText": "Error creando archivo de repetición", + "replaysText": "Repeticiones", + "reportPlayerExplanationText": "Usa este email para reportar trampas, lenguaje ofensivo, u otro mal comportamiento.\nPor favor, describelo aquí abajo:", + "reportThisPlayerCheatingText": "Trampas", + "reportThisPlayerLanguageText": "Lenguaje Inapropiado", + "reportThisPlayerReasonText": "¿Qué vas a reportar?", + "reportThisPlayerText": "Reportar Este Jugador", + "requestingText": "Solicitando...", + "restartText": "Reiniciar", + "retryText": "Reintentar", + "revertText": "Deshacer", + "runText": "Correr", + "saveText": "Guardar", + "scanScriptsErrorText": "Error (s) escaneando scripts; ver el registro para más detalles.", + "scoreChallengesText": "Desafíos de Puntuación", + "scoreListUnavailableText": "La lista de puntuaciones no está disponible.", + "scoreText": "Puntuación", + "scoreUnits": { + "millisecondsText": "Milisegundos", + "pointsText": "Puntos", + "secondsText": "Segundos" + }, + "scoreWasText": "(era ${COUNT})", + "selectText": "Elegir", + "seriesWinLine1PlayerText": "GANA LA", + "seriesWinLine1TeamText": "GANA LA", + "seriesWinLine1Text": "¡GANA LA", + "seriesWinLine2Text": "SERIE!", + "settingsWindow": { + "accountText": "Cuenta", + "advancedText": "Avanzado", + "audioText": "Sonido", + "controllersText": "Controles", + "graphicsText": "Gráficas", + "playerProfilesMovedText": "Nota: Los Perfiles de Jugadores se han movido a la ventana de Cuenta en el menú principal", + "playerProfilesText": "Perfiles", + "titleText": "Ajustes" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(un teclado simple y amigable con controles para editar textos)", + "alwaysUseInternalKeyboardText": "Siempre Usar el Teclado Interno", + "benchmarksText": "Puntos de Referencia y Pruebas de Resistencia", + "disableCameraGyroscopeMotionText": "Desactivar el movimiento giróscopico de la cámara", + "disableCameraShakeText": "Desactivar el temblor de la cámara", + "disableThisNotice": "(Puedes desactivar este aviso en configuraciones avanzadas)", + "enablePackageModsDescriptionText": "(enciende modificaciones pero apaga juegos vía red)", + "enablePackageModsText": "Encender Modificaciones Locales", + "enterPromoCodeText": "Ingresar Código", + "forTestingText": "Notas: estos datos son solo para pruebas y se reiniciarán cuando se abra la app de nuevo.", + "helpTranslateText": "Las traducciones de ${APP_NAME} son un esfuerzo\napoyado por la comunidad. Si deseas aportar o corregir una\ntraducción, utiliza el siguiente enlace. ¡Gracias de antemano!", + "helpTranslateTextScale": 1.0, + "kickIdlePlayersText": "Expulsar Jugadores Inactivos", + "kidFriendlyModeText": "Modo Niños (reduce violencia, etc)", + "languageText": "Idioma", + "languageTextScale": 1.0, + "moddingGuideText": "Guía de Mods", + "mustRestartText": "Tienes que reiniciar el juego para que tome efecto.", + "netTestingText": "Prueba de Red", + "resetText": "Reiniciar", + "showBombTrajectoriesText": "Mostrar trayectorias", + "showPlayerNamesText": "Mostrar Nombres de los Jugadores", + "showUserModsText": "Mostrar Carpeta de Mods", + "titleText": "Avanzado", + "translationEditorButtonText": "Editor de Traducción de ${APP_NAME}", + "translationFetchErrorText": "estado de traducción no disponible", + "translationFetchingStatusText": "viendo estado de traducción...", + "translationInformMe": "Informarme cuando mi idioma necesite actualizaciones.", + "translationNoUpdateNeededText": "el idioma actual está actualizado; woohoo!", + "translationUpdateNeededText": "** ¡¡el idioma actual necesita actualizarse!! **", + "vrTestingText": "Prueba VR" + }, + "shareText": "Compartir", + "sharingText": "Compartiendo...", + "showText": "Mostrar", + "signInForPromoCodeText": "Debes iniciar sesión con tu cuenta para que el código funcione.", + "signInWithGameCenterText": "Para usar una cuenta de Game Center,\ningresa con la aplicación Game Center.", + "singleGamePlaylistNameText": "Solo ${GAME}", + "singlePlayerCountText": "1 jugador", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Selección de Personajes", + "Chosen One": "El Elegido", + "Epic": "Juegos Épicos", + "Epic Race": "Carrera Épica", + "FlagCatcher": "Captura la Bandera", + "Flying": "Sueños Dulces", + "Football": "Fútbol", + "ForwardMarch": "Carnicería", + "GrandRomp": "Conquista", + "Hockey": "Hockey", + "Keep Away": "Aléjate", + "Marching": "Evasiva", + "Menu": "Menú Principal", + "Onslaught": "Matanza", + "Race": "Carrera", + "Scary": "Rey de la Colina", + "Scores": "Pantalla de Puntuaciones", + "Survival": "Eliminación", + "ToTheDeath": "Combate Mortal", + "Victory": "Pantalla de Puntuaciones Finales" + }, + "spaceKeyText": "espacio", + "statsText": "Estad.", + "storagePermissionAccessText": "Esto requiere acceso de almacenamiento", + "store": { + "alreadyOwnText": "¡Ya compraste ${NAME}!", + "bombSquadProDescriptionText": "• Doble de tickets recibidos en el juego.\n• Remueve los anuncios del juego.\n• Incluye ${COUNT} tickets de bonus.\n• +${PERCENT}% de bonus en la puntuación de la liga.\n• Desbloquea los niveles cooperativos ${INF_ONSLAUGHT}\n y ${INF_RUNAROUND}. ", + "bombSquadProFeaturesText": "Características:", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "•Quita los anuncios y las pantallas molestas\n•Desbloquea más opciones de juego\n•También incluye:", + "buyText": "Comprar", + "charactersText": "Personajes", + "comingSoonText": "Próximamente...", + "extrasText": "Extras", + "freeBombSquadProText": "BombSquad ahora es gratis, pero ya que compraste el juego recibirás\nla mejora de BombSquad Pro y ${COUNT} tickets como agradecimiento.\n¡Disfruta de las nuevas funciones, y gracias por tu soporte!\n-Eric", + "gameUpgradesText": "Mejoras", + "getCoinsText": "Obtén monedas", + "holidaySpecialText": "¡Especial de Temporada!", + "howToSwitchCharactersText": "(ve a \"${SETTINGS} -> ${PLAYER_PROFILES}\" para asignar y personalizar avatares)", + "howToUseIconsText": "(crea perfiles globales de jugador (en la ventana de cuenta) para usarlos)", + "howToUseMapsText": "(usa estos mapas en tus listas de juego por equipos y todos contra todos)", + "iconsText": "Iconos", + "loadErrorText": "No se pudo cargar la página.\nRevisa tu conexión de internet.", + "loadingText": "cargando", + "mapsText": "Mapas", + "miniGamesText": "MiniJuegos", + "oneTimeOnlyText": "(solo una vez)", + "purchaseAlreadyInProgressText": "Una compra de este artículo se encuentra en progreso.", + "purchaseConfirmText": "¿Comprar ${ITEM}?", + "purchaseNotValidError": "Compra inválida.\nContacte ${EMAIL} si esto es un error.", + "purchaseText": "Comprar", + "saleBundleText": "Paquete en Oferta!", + "saleExclaimText": "¡Oferta!", + "salePercentText": "(${PERCENT}% menos)", + "saleText": "OFERTA", + "searchText": "Buscar", + "teamsFreeForAllGamesText": "En Equipos / Todos contra Todos", + "totalWorthText": "*** ¡${TOTAL_WORTH} de valor! ***", + "upgradeQuestionText": "¿Comprar Bombsquad Pro?", + "winterSpecialText": "Especial de Invierno", + "youOwnThisText": "- ya posees esto -" + }, + "storeDescriptionText": "¡Juego en grupo de 8 jugadores!\n\n¡Revienta a tus amigos (o a la computadora) en un torneo de minijuegos explosivos como Captura la Bandera, Hockey-Bomba, y Combate Mortal Épico en cámara lenta!\n\nControles sencillos y un amplio apoyo de controles hacen que sea fácil que 8 personas entren en la acción; ¡incluso puedes usar tus dispositivos móviles como controles a través de la aplicación gratuita 'BombSquad Remote'!\n\n¡Bombas fuera!\n\nEcha un vistazo en www.froemling.net/bombsquad para más información.", + "storeDescriptions": { + "blowUpYourFriendsText": "Revienta a tus amigos.", + "competeInMiniGamesText": "Compite en juegos de carreras, vuelo y mucho más.", + "customize2Text": "Modifica personajes, mini-juegos, e incluso la banda sonora.", + "customizeText": "Modifica personajes y crea tus propias listas de mini-juegos.", + "sportsMoreFunText": "Los deportes son más divertidos con minas.", + "teamUpAgainstComputerText": "Sobrevive contra la computadora." + }, + "storeText": "Tienda", + "submitText": "Enviar", + "submittingPromoCodeText": "Enviando Código...", + "teamNamesColorText": "Nombres de equipo/colores...", + "teamsText": "Equipos", + "telnetAccessGrantedText": "Acceso a Telnet activado.", + "telnetAccessText": "Acceso a Telnet detectado; ¿permitir?", + "testBuildErrorText": "Esta versión de prueba ya no es activa; busca una versión más nueva.", + "testBuildText": "Versión de Prueba", + "testBuildValidateErrorText": "Imposible validar esta versión de prueba (¿no tienes conexión a internet?)", + "testBuildValidatedText": "Versión de Prueba Validada; ¡Disfruta!", + "thankYouText": "¡Gracias por tu ayuda! ¡¡Disfruta el juego!!", + "threeKillText": "¡¡¡Triple Combo!!!", + "timeBonusText": "Bono de Tiempo", + "timeElapsedText": "Tiempo Transcurrido", + "timeExpiredText": "Se Acabó el Tiempo", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}h", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Sugerencia", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Mejores Amigos", + "tournamentCheckingStateText": "Buscando estado del campeonato; espere por favor...", + "tournamentEndedText": "Este torneo ha acabado. Uno nuevo comenzará pronto.", + "tournamentEntryText": "Inscripción al Torneo", + "tournamentResultsRecentText": "Resultados Torneos Recientes", + "tournamentStandingsText": "Puestos del Torneo", + "tournamentText": "Torneo", + "tournamentTimeExpiredText": "Tiempo del Torneo Expirado", + "tournamentsText": "Torneos", + "translations": { + "characterNames": { + "Agent Johnson": "Agente Johnson", + "B-9000": "B-9000", + "Bernard": "Bernard", + "Bones": "Huesos", + "Butch": "Butch", + "Easter Bunny": "Conejo de pascua", + "Flopsy": "Flopsy", + "Frosty": "Frosty", + "Gretel": "Gretel", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Suertudo", + "Mel": "Mel", + "Middle-Man": "Middle-Man", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Papá Noel", + "Snake Shadow": "Serpiente Sombría", + "Spaz": "Spaz", + "Taobao Mascot": "Mascota Taobao", + "Todd": "Todd", + "Todd McBurton": "Todd McBurton", + "Xara": "Xara", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopIconNames": { + "Infinite\nOnslaught": "Matanza\nInfinita", + "Infinite\nRunaround": "Evasiva\nInfinita", + "Onslaught\nTraining": "Entrenamiento\nMatanza", + "Pro\nFootball": "Fútbol\nPro", + "Pro\nOnslaught": "Matanza\nPro", + "Pro\nRunaround": "Evasiva\nPro", + "Rookie\nFootball": "Fútbol\nNovato", + "Rookie\nOnslaught": "Matanza\nNovato", + "The\nLast Stand": "La\nBatalla Final", + "Uber\nFootball": "Fútbol\nUltra", + "Uber\nOnslaught": "Matanza\nUltra", + "Uber\nRunaround": "Evasiva\nUltra" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Entrenamiento", + "Infinite ${GAME}": "${GAME} Infinito/a", + "Infinite Onslaught": "Matanza Infinita", + "Infinite Runaround": "Evasiva Infinita", + "Onslaught": "Matanza Infinita", + "Onslaught Training": "Entrenamiento de Matanza", + "Pro ${GAME}": "Pro ${GAME}", + "Pro Football": "Fútbol Pro", + "Pro Onslaught": "Matanza Pro", + "Pro Runaround": "Evasiva Pro", + "Rookie ${GAME}": "Novato ${GAME}", + "Rookie Football": "Fútbol Novato", + "Rookie Onslaught": "Matanza Novato", + "Runaround": "Evasiva Infinita", + "The Last Stand": "Batalla Final", + "Uber ${GAME}": "Uber ${GAME}", + "Uber Football": "Fútbol Ultra", + "Uber Onslaught": "Matanza Ultra", + "Uber Runaround": "Evasiva Ultra" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Sé el elegido por el tiempo designado para ganar.\nElimina al Elegido para convertirte en El Elegido.", + "Bomb as many targets as you can.": "Lanza bombas a tantos blancos como puedas.", + "Carry the flag for ${ARG1} seconds.": "Carga la bandera por ${ARG1} segundos.", + "Carry the flag for a set length of time.": "Carga la bandera por determinado tiempo.", + "Crush ${ARG1} of your enemies.": "Elimina ${ARG1} de tus enemigos.", + "Defeat all enemies.": "Elimina todos los enemigos.", + "Dodge the falling bombs.": "Esquiva las bombas.", + "Final glorious epic slow motion battle to the death.": "Gloriosa batalla final en cámara lenta épica.", + "Gather eggs!": "¡Recolecta huevos!", + "Get the flag to the enemy end zone.": "Lleva la bandera a la zona enemiga.", + "How fast can you defeat the ninjas?": "¿Cuán rápido puedes derrotar a los ninjas?", + "Kill a set number of enemies to win.": "Elimina a un número determinado \nde enemigos para ganar.", + "Last one standing wins.": "El último de pie gana.", + "Last remaining alive wins.": "El último en quedar vivo gana.", + "Last team standing wins.": "El último equipo en pie gana.", + "Prevent enemies from reaching the exit.": "Evita que los enemigos lleguen a la salida.", + "Reach the enemy flag to score.": "Toca la bandera enemiga para anotar.", + "Return the enemy flag to score.": "Devuelve la bandera enemiga para anotar.", + "Run ${ARG1} laps.": "Da ${ARG1} vueltas.", + "Run ${ARG1} laps. Your entire team has to finish.": "Da ${ARG1} vueltas. Todo tu equipo debe cruzar la meta.", + "Run 1 lap.": "Da 1 vuelta.", + "Run 1 lap. Your entire team has to finish.": "Da 1 vuelta. Todo tu equipo debe cruzar la meta.", + "Run real fast!": "¡Corre muy rápido!", + "Score ${ARG1} goals.": "Anota ${ARG1} goles.", + "Score ${ARG1} touchdowns.": "Anota ${ARG1} touchdowns.", + "Score a goal.": "Anota un gol.", + "Score a touchdown.": "Anota un touchdown.", + "Score some goals.": "Anota unos goles.", + "Secure all ${ARG1} flags.": "Asegura todas la ${ARG1} banderas.", + "Secure all flags on the map to win.": "Asegura todas las banderas en el mapa para ganar.", + "Secure the flag for ${ARG1} seconds.": "Asegura la bandera por ${ARG1} segundos.", + "Secure the flag for a set length of time.": "Asegura la bandera por un tiempo determinado.", + "Steal the enemy flag ${ARG1} times.": "Roba la bandera del enemigo ${ARG1} veces.", + "Steal the enemy flag.": "Roba la bandera del enemigo.", + "There can be only one.": "Solo puede haber uno.", + "Touch the enemy flag ${ARG1} times.": "Toca la bandera del enemigo ${ARG1} veces.", + "Touch the enemy flag.": "Toca la bandera del enemigo.", + "carry the flag for ${ARG1} seconds": "carga la bandera por ${ARG1} segundos.", + "kill ${ARG1} enemies": "elimina a ${ARG1} enemigos", + "last one standing wins": "el último en pie gana", + "last team standing wins": "el último equipo en caer gana", + "return ${ARG1} flags": "devuelve ${ARG1} banderas", + "return 1 flag": "devuelve 1 bandera", + "run ${ARG1} laps": "da ${ARG1} vueltas", + "run 1 lap": "da 1 vuelta", + "score ${ARG1} goals": "anota ${ARG1} goles", + "score ${ARG1} touchdowns": "anota ${ARG1} touchdowns", + "score a goal": "anota un gol", + "score a touchdown": "anota un touchdown", + "secure all ${ARG1} flags": "asegura todas las ${ARG1} banderas", + "secure the flag for ${ARG1} seconds": "asegura la bandera por ${ARG1} segundos", + "touch ${ARG1} flags": "toca ${ARG1} banderas", + "touch 1 flag": "toca 1 bandera" + }, + "gameNames": { + "Assault": "Asalto", + "Capture the Flag": "Captura la Bandera", + "Chosen One": "El Elegido", + "Conquest": "Conquista", + "Death Match": "Combate Mortal", + "Easter Egg Hunt": "Busqueda de Huevos de Pascua", + "Elimination": "Eliminación", + "Football": "Fútbol", + "Hockey": "Hockey", + "Keep Away": "Aléjate", + "King of the Hill": "Rey de la Colina", + "Meteor Shower": "Lluvia de Meteoritos", + "Ninja Fight": "Pelea Ninja", + "Onslaught": "Matanza", + "Race": "Carrera", + "Runaround": "Evasiva", + "Target Practice": "Blanco de Práctica", + "The Last Stand": "Batalla Final" + }, + "inputDeviceNames": { + "Keyboard": "Teclado", + "Keyboard P2": "Teclado P2" + }, + "languages": { + "Arabic": "Árabe", + "Belarussian": "Bielorruso", + "Chinese": "Chino Simplificado", + "ChineseTraditional": "Chino Tradicional", + "Croatian": "Croata", + "Czech": "Checo", + "Danish": "Danés", + "Dutch": "Holandés", + "English": "Inglés", + "Esperanto": "Esperanto", + "Finnish": "Finlandés", + "French": "Francés", + "German": "Alemán", + "Gibberish": "Algarabía", + "Greek": "Griego", + "Hindi": "Hindi", + "Hungarian": "Húngaro", + "Indonesian": "Indonesio", + "Italian": "Italiano", + "Japanese": "Japonés", + "Korean": "Coreano", + "Persian": "Persa", + "Polish": "Polaco", + "Portuguese": "Portugués", + "Romanian": "Rumano", + "Russian": "Ruso", + "Serbian": "Serbio", + "Slovak": "Eslovaco", + "Spanish": "Español", + "Swedish": "Sueco", + "Turkish": "Turco", + "Ukrainian": "Ucraniano", + "Venetian": "Veneciana", + "Vietnamese": "Vietnamita" + }, + "leagueNames": { + "Bronze": "Bronce", + "Diamond": "Diamante", + "Gold": "Oro", + "Silver": "Plata" + }, + "mapsNames": { + "Big G": "La Gran G", + "Bridgit": "Puentecito", + "Courtyard": "Patio Real", + "Crag Castle": "Castillo de Piedra", + "Doom Shroom": "Hongo de la Muerte", + "Football Stadium": "Estadio de Fútbol", + "Happy Thoughts": "Pensamientos felices", + "Hockey Stadium": "Estadio de Hockey", + "Lake Frigid": "Lago Frígido", + "Monkey Face": "Cara de Mono", + "Rampage": "Medio Tubo", + "Roundabout": "Rotonda", + "Step Right Up": "Paso al Frente", + "The Pad": "La Plataforma", + "Tip Top": "La Montaña", + "Tower D": "Torre D", + "Zigzag": "Zigzag" + }, + "playlistNames": { + "Just Epic": "Solo Épico", + "Just Sports": "Solo Deportes" + }, + "promoCodeResponses": { + "invalid promo code": "Código promocional inválido" + }, + "scoreNames": { + "Flags": "Banderas", + "Goals": "Goles", + "Score": "Puntuación", + "Survived": "Sobrevivió", + "Time": "Tiempo", + "Time Held": "Tiempo Mantenido" + }, + "serverResponses": { + "A code has already been used on this account.": "Este código ya ha sido usado en esta cuenta", + "A reward has already been given for that address.": "Ya se le ha dado una recompensa a esa dirección.", + "Account linking successful!": "¡Enlace de Cuenta exitoso!", + "Account unlinking successful!": "¡Desenlace de cuenta realizado!", + "Accounts are already linked.": "Las cuentas ya se encuentran enlazadas.", + "An error has occurred; (${ERROR})": "Se ha producido un error; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Se ha producido un error; por favor contácte con soporte. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Ha ocurrido un error; contacta a support@froemling.net.", + "An error has occurred; please try again later.": "Un error ha ocurrido; por favor intenta más tarde.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Quieres enlazar estas cuentas?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\n¡Esto no se puede deshacer!", + "BombSquad Pro unlocked!": "¡BombSquad Pro desbloqueado!", + "Can't link 2 accounts of this type.": "No puedes enlazar dos cuentas de este tipo.", + "Can't link 2 diamond league accounts.": "No pueden enlazar dos cuentas de liga diamante.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "No se pudo enlazar; sobrepasaría el máximo de ${COUNT} cuentas enlazadas.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Trampa detectada; puntajes y premios suspendidos por ${COUNT} días.", + "Could not establish a secure connection.": "No se pudo establecer una conexión segura.", + "Daily maximum reached.": "Máximo diario conseguido.", + "Entering tournament...": "Entrando a torneo...", + "Invalid code.": "Código inválido.", + "Invalid payment; purchase canceled.": "Pago inválido; compra cancelada.", + "Invalid promo code.": "Código promocional inválido.", + "Invalid purchase.": "Compra inválida.", + "Invalid tournament entry; score will be ignored.": "Entrada de torneo inválida; el puntaje será ignorado.", + "Item unlocked!": "¡Objeto desbloqueado!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "VINCULACÍON DENEGADO. ${ACCOUNT} contiene\ndatos significativos que TODOS SERÍAN PERDIDOS.\nPuede vincular en el orden opuesto si lo desea\n(y pierda los datos de ESTA cuenta en su lugar)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "¿Enlazar ${ACCOUNT} a esta cuenta?\nTodos los datos en ${ACCOUNT} desaparecerán.\nEsto no se puede deshacer. ¿Estás seguro?", + "Max number of playlists reached.": "Número máximo de listas de reproducción alcanzado.", + "Max number of profiles reached.": "Número máximo de perfiles alcanzado.", + "Maximum friend code rewards reached.": "Máximo de premios por códigos de amigos alcanzado.", + "Message is too long.": "El Mensaje es muy largo.", + "Profile \"${NAME}\" upgraded successfully.": "El perfil \"${NAME}\" se ha mejorado satisfactoriamente.", + "Profile could not be upgraded.": "El perfil no pudo ser mejorado.", + "Purchase successful!": "¡Compra exitosa!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Has recibido ${COUNT} tickets recibidos por iniciar sesión.\nRegresa mañana para recibir ${TOMORROW_COUNT} tickets.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "La funcionalidad del servidor ya no es compatible en esta versión del juego;\nActualiza a una versión más reciente.", + "Sorry, there are no uses remaining on this code.": "Perdón, pero no quedan usos disponibles de este código.", + "Sorry, this code has already been used.": "Perdón, este código ya ha sido usado.", + "Sorry, this code has expired.": "Perdón, este código ha expirado.", + "Sorry, this code only works for new accounts.": "Este código solo sirve para cuentas nuevas.", + "Temporarily unavailable; please try again later.": "Temporalmente desactivado; por favor, inténtalo luego.", + "The tournament ended before you finished.": "El torneo terminó antes de que terminaras.", + "This account cannot be unlinked for ${NUM} days.": "Esta cuenta no puede ser desenlazada por ${NUM} días.", + "This code cannot be used on the account that created it.": "Este código no puede ser usado en la misma cuenta que ha sido creado.", + "This is currently unavailable; please try again later.": "Esto Está No Disponible actualmente; por favor inténtelo más tarde", + "This requires version ${VERSION} or newer.": "Esto requiere la versión ${VERSION} o una más nueva.", + "Tournaments disabled due to rooted device.": "Los torneos han sido desactivados debido a que tu dispositivo es root", + "Tournaments require ${VERSION} or newer": "El torneo requiere la versión ${VERSION} o versiones recientes", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "¿Desenlazar ${ACCOUNT} de esta cuenta?\nTodos los datos en ${ACCOUNT} se reiniciarán\n(excepto los logros en algunos casos).", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ADVERTENCIA: se han emitido reclamaciones de piratería contra tu cuenta.\nLas cuentas que se encuentren pirateadas serán prohibidas. Por favor, juega limpio.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "¿Quieres enlazar tu cuenta de dispositivo a esta otra?\n\nTu cuenta de dispositivo es ${ACCOUNT1}\nEsta cuenta es ${ACCOUNT2}\n\nEsto permitirá guardar tu progreso actual.\nAdvertencia: ¡Esto no se puede deshacer!", + "You already own this!": "¡Ya posees esto!", + "You can join in ${COUNT} seconds.": "Puedes unirte en ${COUNT} segundos.", + "You don't have enough tickets for this!": "¡No tienes suficientes tickets para esto!", + "You don't own that.": "No tienes eso.", + "You got ${COUNT} tickets!": "¡Obtuviste ${COUNT} tickets!", + "You got a ${ITEM}!": "¡Recibiste un ${ITEM}!", + "You have been promoted to a new league; congratulations!": "¡Has sido ascendido a una nueva liga! ¡Felicidades!", + "You must update to a newer version of the app to do this.": "Debes actualizar la aplicación a una versión más reciente para hacer esto.", + "You must update to the newest version of the game to do this.": "Necesitas actualizar a la versión más reciente del juego para hacer esto.", + "You must wait a few seconds before entering a new code.": "Debes esperar unos segundos antes de ingresar un código nuevo.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Quedaste en la posición #${RANK} en el campeonato. ¡Gracias por jugar!", + "Your account was rejected. Are you signed in?": "Tu cuenta fue rechazada. ¿Estas registrado?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Tu copia del juego fue modificada.\nPor favor revierte estos cambios e intenta de nuevo", + "Your friend code was used by ${ACCOUNT}": "Tu código de amigo fue usado por ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Minuto", + "1 Second": "1 Segundo", + "10 Minutes": "10 Minutos", + "2 Minutes": "2 Minutos", + "2 Seconds": "2 Segundos", + "20 Minutes": "20 Minutos", + "4 Seconds": "4 Segundos", + "5 Minutes": "5 Minutos", + "8 Seconds": "8 Segundos", + "Allow Negative Scores": "Permitir Puntajes Negativos", + "Balance Total Lives": "Repartir Vidas Totales", + "Bomb Spawning": "Aparición de Bombas", + "Chosen One Gets Gloves": "El Elegido Tiene Guantes de Boxeo", + "Chosen One Gets Shield": "El Elegido Tiene Electro-Escudo", + "Chosen One Time": "Tiempo Del Elegido", + "Enable Impact Bombs": "Permitir Impacto de Bombas", + "Enable Triple Bombs": "Permitir Bombas Triples", + "Entire Team Must Finish": "Todo el Equipo Debe Terminar", + "Epic Mode": "Modo Épico", + "Flag Idle Return Time": "Tiempo de Regreso de Bandera Inactiva", + "Flag Touch Return Time": "Tiempo de Bandera Sin Tocar", + "Hold Time": "Retención", + "Kills to Win Per Player": "Víctimas por Jugador", + "Laps": "Vueltas", + "Lives Per Player": "Vidas por Jugador", + "Long": "Largo", + "Longer": "Más Largo", + "Mine Spawning": "Regenerar Minas", + "No Mines": "Sin Minas", + "None": "Ninguno", + "Normal": "Normal", + "Pro Mode": "Modo Pro", + "Respawn Times": "Tiempo de Reaparición", + "Score to Win": "Puntos para Ganar", + "Short": "Corto", + "Shorter": "Más Corto", + "Solo Mode": "Modo de Un Jugador", + "Target Count": "Número de Objetivos", + "Time Limit": "Límite de Tiempo" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "El equipo ${TEAM} ha sido descalificado porque ${PLAYER} se ha ido.", + "Killing ${NAME} for skipping part of the track!": "¡Matando a ${NAME} por saltarse un pedazo de la pista!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Advertencia para ${NAME}: turbo / El spameo de botones te noqueara." + }, + "teamNames": { + "Bad Guys": "Chicos malos", + "Blue": "Azul", + "Good Guys": "Chicos Buenos", + "Red": "Rojo" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Un 'corre-salta-gira-golpea' puede destrozar de un solo impacto\ny ganarte el respeto de tus amigos por toda la vida.", + "Always remember to floss.": "Siempre acuérdate de cepillar tus dientes.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Crea perfiles para ti y tus amigos con nombres\ny colores personalizados en vez de usar aleatorios.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "La maldición te convierte en una bomba de tiempo.\nLa única cura es coger una caja de salud.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "A pesar de su apariencia, las habilidades de todos los personajes\nson idénticas, así que escoge el que más se parezca a ti.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "No eres invencible con ese Electro-Escudo, todavía te pueden arrojar por un precipicio.", + "Don't run all the time. Really. You will fall off cliffs.": "No corras todo el tiempo. En serio. Te vas a caer.", + "Don't spin for too long; you'll become dizzy and fall.": "No gires por un largo tiempo; puedes marearte y caer.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Sostén cualquier botón para correr. (Los botones de gatillo son para eso)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Mantén pulsado cualquier botón para correr. Llegarás a lugares rápido\npero no girarás muy bien, así que ten cuidado con los acantilados.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Las Bombas de hielo no son muy potentes, pero congelan lo\nque toquen, dejando a tus enemigos vulnerables a romperse.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Si alguien te levanta, golpéalos y ve como te sueltan.\nTambién funciona en la vida real.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Si no tienes suficientes controles, instala la aplicación '${REMOTE_APP_NAME}'\nen tu teléfono celulares para utilizarlos como controles.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Te faltan controles? Instala la aplicación 'BombSquad Remote'\nen tu dispositivo iOS o Android para usarlo como control.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Si te adhieres a una bomba pegajosa, salta y da muchas vueltas. Es posible que sacudas\nla bomba pagada, o sin nada más, tus últimos momentos serán entretenidos.", + "If you kill an enemy in one hit you get double points for it.": "Si eliminas a un enemigo de un golpe ganas doble puntos.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Si levantas una maldición, tu única esperanza es\nencontrar una caja de salud en tus últimos segundos.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Si te quedas quieto, estás perdido. Corre y esquiva para sobrevivir...", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Si tienes muchos jugadores yendo y viniendo, activa 'expulsar jugadores inactivos'\nen ajustes en caso de que alguien se olvide de abandonar el juego.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Si tu dispositivo se pone caliente o te gustaria conservar bateria,\nbaja los \"Visuales\" o \"Resolución\" en configuración->Gráficos", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Si la imagen va lenta, intenta reducir la resolución\no los visuales en los ajustes gráficos del juego.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "En Captura la Bandera, la tuya debe estar en tu base para que anotes.\nSi el otro equipo está a punto de anotar, el arrebatar su bandera evitará que lo hagan.", + "In hockey, you'll maintain more speed if you turn gradually.": "En hockey, mantendrás tu impulso si giras gradualmente.", + "It's easier to win with a friend or two helping.": "Es mas fácil ganar con un amigo.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Brinca antes de lanzar una bomba para que alcance lugares altos.", + "Land-mines are a good way to stop speedy enemies.": "Las minas son una buena manera para detener a los enemigos veloces.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Muchas cosas se pueden recoger y lanzar, incluyendo a otros jugadores.\nArroja a tus enemigos por los precipicios. Te sentirás mejor.", + "No, you can't get up on the ledge. You have to throw bombs.": "No, no puedes subir a la cornisa. Tienes que lanzar bombas.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Jugadores pueden unirse e irse en medio de casi todos los juegos,\ntambién puedes poner o quitar controles en cualquier momento.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Los jugadores pueden unirse y abandonar en el transcurso del juego,\ny también puedes conectar y desconectar controles cuando quieras.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Los poderes sólo tienen tiempo límite en juego cooperativo.\nEn los equipos y Pelea libre son tuyos hasta que seas eliminado.", + "Practice using your momentum to throw bombs more accurately.": "Practica con tu impulso para lanzar bombas con más precisión.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Cuanto más rápido se mueven tu puños, tus golpes tienen\nmás impacto, así que corre, salta y gira como un loco.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Corre de un lado a otro antes de lanzar una\nbomba de 'latigazo' para lanzarla lejos.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Elimina un gran cantidad de enemigos\nal detonar una bomba cerca del TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "La cabeza es la zona más vulnerable, una bomba pegajosa\na la cabeza generalmente significa el fin.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Este nivel no tiene fin, pero un alto puntaje aquí\nte hará ganar el respeto eterno por todo el mundo.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "La fuerza de tiro se basa en la dirección que estás sosteniendo.\nPara arrojar algo justo delante de ti, no sostengas ninguna dirección.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "¿Cansado de la pista de audio? ¡Reemplázala con tu música!\nVe a Ajustes->Audio->Banda Sonora", + "Try 'Cooking off' bombs for a second or two before throwing them.": "'Cocina tus Bombas' por un segundo o dos antes de tirarlas.", + "Try tricking enemies into killing eachother or running off cliffs.": "Engaña a tus enemigos para que se eliminen entre sí o para que corran a los barrancos.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Usa el botón de 'levantar' para llevar la bandera < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Bate de un lado a otro para tirar las bombas más lejos..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Puedes 'dirigir' tus golpes girando a la izquierda o derecha. Esto\nes útil para tirar a los enemigos al vacío o para anotar en el hockey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Puedes juzgar si una bomba va a explotar según el color de\nlas chispas de la mecha: amarillo...naranja...rojo...¡BUM!", + "You can throw bombs higher if you jump just before throwing.": "Puedes lanzar bombas más alto si saltas justo antes de tirarlas.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "No necesitas editar tu perfil para cambiar de personaje; sólo presiona el botón\nsuperior (levantar) cuando te unas a un partido para cambiar el predeterminado.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Te haces daño si golpeas tu cabeza contra cosas, así\nque trata de no golpear tu cabeza contra cosas.", + "Your punches do much more damage if you are running or spinning.": "Tus golpes harán mucho más impacto si estás corriendo o girando." + } + }, + "trialPeriodEndedText": "Tu periodo de prueba se terminó. ¿Deseas\ncomprar BombSquad y seguir jugando?", + "trophiesRequiredText": "Esto requiere al menos ${NUMBER} trofeos.", + "trophiesText": "Trofeos", + "trophiesThisSeasonText": "Trofeos Esta Temporada", + "tutorial": { + "cpuBenchmarkText": "Corriendo tutorial en velocidad ridícula (pruebas de velocidad de CPU)", + "phrase01Text": "¡Hey, hola!", + "phrase02Text": "¡Bienvenido a ${APP_NAME}!", + "phrase03Text": "Aquí van unos consejos para manejar a tu personaje:", + "phrase04Text": "Muchas cosas en ${APP_NAME} se basan en FÍSICAS.", + "phrase05Text": "Por ejemplo, cuando pegas un puñetazo...", + "phrase06Text": "...el daño se basa en la velocidad de tus puños.", + "phrase07Text": "¿Has visto? No nos estábamos moviendo, por eso casi \nno hemos hecho daño a ${NAME}.", + "phrase08Text": "Ahora, saltemos y giremos para conseguir más velocidad.", + "phrase09Text": "Ah, eso está mejor.", + "phrase10Text": "Correr también ayuda.", + "phrase11Text": "Mantén pulsado CUALQUIER botón para correr.", + "phrase12Text": "Para dar superpuñetazos, prueba a correr Y girar.", + "phrase13Text": "¡Ups! Lo siento por eso, ${NAME}.", + "phrase14Text": "Puedes coger y lanzar cosas como banderas... o ${NAME}s.", + "phrase15Text": "Por último, hay bombas.", + "phrase16Text": "Tirar bombas bien requiere práctica.", + "phrase17Text": "¡Vaya! No ha llegado muy lejos.", + "phrase18Text": "Moverte ayuda a lanzarlas más lejos.", + "phrase19Text": "Saltar te permitirá lanzarlas más alto.", + "phrase20Text": "Girar también te permitirá hacer lanzamientos más lejanos.", + "phrase21Text": "Hacer que exploten donde tú quieres puede ser complicado.", + "phrase22Text": "Vaya...", + "phrase23Text": "Probemos a 'cocinarla' dos segundos antes de lanzarla...", + "phrase24Text": "¡Toma ya! Así es como se hace.", + "phrase25Text": "Bueno, eso es todo.", + "phrase26Text": "Ahora, ¡a por ellos, campeón!", + "phrase27Text": "Recuerda tu entrenamiento, ¡y volverás con vida!", + "phrase28Text": "...o a lo mejor...", + "phrase29Text": "¡Buena suerte!", + "randomName1Text": "Fred", + "randomName2Text": "Javi", + "randomName3Text": "Carlos", + "randomName4Text": "Pablo", + "randomName5Text": "Nerea", + "skipConfirmText": "¿Realmente quieres saltar el tutorial? Toca o presiona para confirmar.", + "skipVoteCountText": "votos para saltarnos el tutorial ${COUNT}/${TOTAL}", + "skippingText": "saltándonos el tutorial...", + "toSkipPressAnythingText": "(pulsa cualquier botón para saltarte el tutorial)" + }, + "twoKillText": "¡¡Doble Combo!!", + "unavailableText": "no disponible", + "unconfiguredControllerDetectedText": "Control desconfigurado detectado:", + "unlockThisInTheStoreText": "Esto debe ser desbloqueado en la tienda.", + "unlockThisProfilesText": "Para crear más de ${NUM} cuentas, necesitas:", + "unlockThisText": "Para desbloquear esto, tú necesitas:", + "unsupportedHardwareText": "Lo sentimos, este dispositivo no soporta esta versión del juego.", + "upFirstText": "A continuación:", + "upNextText": "A continuación en juego ${COUNT}:", + "updatingAccountText": "Actualizando tu cuenta...", + "upgradeText": "Mejorar", + "upgradeToPlayText": "Desbloquea \"${PRO}\" en la tienda para jugar esto.", + "useDefaultText": "Usar botón por defecto", + "usesExternalControllerText": "Este juego usa un control externo como entrada.", + "usingItunesText": "Usando la aplicación de música para la banda sonora...", + "usingItunesTurnRepeatAndShuffleOnText": "Asegúrate de que mezclar esté ENCENDIDO y repetir TODOS esté activado en iTunes.", + "validatingBetaText": "Validando Beta…", + "validatingTestBuildText": "Validando versión de prueba...", + "victoryText": "¡Victoria!", + "voteDelayText": "No puedes iniciar otra votación por ${NUMBER} segundos.", + "voteInProgressText": "Ya hay una votación en progreso.", + "votedAlreadyText": "Ya has votado.", + "votesNeededText": "Requiere ${NUMBER} votos", + "vsText": "vs.", + "waitingForHostText": "(esperando a que ${HOST} continúe)", + "waitingForLocalPlayersText": "Esperando jugadores locales...", + "waitingForPlayersText": "esperando a jugadores para unirse...", + "waitingInLineText": "Esperando en línea (la fiesta esta llena)...", + "watchAVideoText": "Ver un Vídeo", + "watchAnAdText": "Mira un Anuncio", + "watchWindow": { + "deleteConfirmText": "¿Borrar \"${REPLAY}\"?", + "deleteReplayButtonText": "Borrar\nRepetición", + "myReplaysText": "Mis Repeticiones", + "noReplaySelectedErrorText": "Ninguna Repetición Seleccionada", + "playbackSpeedText": "Velocidad de reproducción: ${SPEED}", + "renameReplayButtonText": "Renombrar\nRepetición", + "renameReplayText": "Renombrar \"${REPLAY}\" a:", + "renameText": "Renombrar", + "replayDeleteErrorText": "Error borrando la repetición.", + "replayNameText": "Nombre de la Repetición", + "replayRenameErrorAlreadyExistsText": "Una repetición con ese nombre ya existe.", + "replayRenameErrorInvalidName": "No se puede renombrar la repetición: Nombre inválido.", + "replayRenameErrorText": "Error renombrando repetición.", + "sharedReplaysText": "Repeticiones Compartidas", + "titleText": "Ver", + "watchReplayButtonText": "Ver\nRepetición" + }, + "waveText": "Horda", + "wellSureText": "¡Pues claro!", + "wiimoteLicenseWindow": { + "titleText": "Marca registrada DarwiinRemote" + }, + "wiimoteListenWindow": { + "listeningText": "Esperando controles Wii...", + "pressText": "Presiona los botones 1 y 2 simultáneamente.", + "pressText2": "En controles Wii más nuevos con Motion Plus integrado, pulsa mejor el botón 'sinc' rojo en la parte trasera.", + "pressText2Scale": 0.55, + "pressTextScale": 1.0 + }, + "wiimoteSetupWindow": { + "copyrightText": "Marca registrada DarwiinRemote", + "copyrightTextScale": 0.6, + "listenText": "Escuchar", + "macInstructionsText": "Asegúrate de que tu Wii esté apagado y el Bluetooth esté activado en\ntu Mac, a continuación, pulsa 'Escuchar'. El Soporte para controles\nWii puede ser un poco extraño, así que puede que tengas que\nprobar un par de veces antes de obtener una conexión.\n\nEl Bluetooth puede procesar hasta 7 dispositivos conectados,\naunque tu kilometraje puede variar.\n\nBombSquad es compatible con los Controles Wii originales,\nNunchuks, y el Control Clásico.\nEl nuevo Control Wii Plus también funciona\npero sin accesorios.", + "macInstructionsTextScale": 0.7, + "thanksText": "Gracias al equipo DarwiinRemote\nPor hacer esto posible.", + "thanksTextScale": 0.8, + "titleText": "Configuración Wii" + }, + "winsPlayerText": "¡${NAME} Gana!", + "winsTeamText": "¡${NAME} Gana!!", + "winsText": "¡${NAME} Gana!", + "worldScoresUnavailableText": "Puntuaciones mundiales no disponibles.", + "worldsBestScoresText": "Mejores puntuaciones Mundiales", + "worldsBestTimesText": "Mejores tiempos Mundiales", + "xbox360ControllersWindow": { + "getDriverText": "Obtener Driver", + "macInstructions2Text": "Para usar controles inalámbricos, necesitarás el receptor que\nviene con el 'Control de Xbox 360 para Windows'.\nUn receptor te permite conectar hasta cuatro controles.\n\nImportante: Receptores de tercera mano no funcionarán con este controlador;\nasegúrate de que tu receptor tenga el logo de 'Microsoft', no 'XBOX 360'.\nMicrosoft ya no vende receptores por separado, por lo que necesitarás\nel que venía con el control, o si no tendrás que buscar uno por internet.\n\nSi encuentras que esto fue útil, por favor considera donar\nal desarrollador del controlador en su sitio de internet.", + "macInstructions2TextScale": 0.76, + "macInstructionsText": "Para usar controles de Xbox 360, necesitarás instalar\nel controlador para Mac disponible en el siguiente enlace.\nFunciona con controles con cable e inalámbricos.", + "macInstructionsTextScale": 0.8, + "ouyaInstructionsText": "Para usar controles con cable de Xbox 360 con BombSquad, simplemente\nconéctalos al puerto USB de tu dispositivo. Puedes usar un concentrador USB\npara conectar múltiples controles.\n\nPara usar controles inalámbricos, necesitarás un receptor inalámbrico,\ndisponible como parte del paquete “Xbox 360 wireless Controller for\nWindows”, o vendido por separado. Cada receptor se conecta a un puerto\nUSB y permite que conectes hasta cuatro controles inalámbricos.", + "ouyaInstructionsTextScale": 0.8, + "titleText": "Controles de Xbox 360 con ${APP_NAME}:" + }, + "yesAllowText": "¡Si, permitelo!", + "yourBestScoresText": "Tus Mejores Puntuaciones", + "yourBestTimesText": "Tus Mejores Tiempos" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/swedish.json b/dist/ba_data/data/languages/swedish.json new file mode 100644 index 0000000..eab13e6 --- /dev/null +++ b/dist/ba_data/data/languages/swedish.json @@ -0,0 +1,1733 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Kontonamn kan inte innehålla emojis eller andra specialsymboler", + "accountProfileText": "(användarprofil)", + "accountsText": "Konton", + "achievementProgressText": "Prestationer: ${COUNT} avklarade av ${TOTAL}", + "campaignProgressText": "Kampanjutveckling: [svår] ${PROGRESS}", + "changeOncePerSeason": "Du kan enbart ändra detta en gång per säsong.", + "changeOncePerSeasonError": "Du måste vänta tills nästa säsong för att ändra detta igen (${NUM} days)", + "customName": "Anpassat Namn", + "linkAccountsEnterCodeText": "Skriv in kod", + "linkAccountsGenerateCodeText": "Generera kod", + "linkAccountsInfoText": "(dela framsteg över olika plattformar)", + "linkAccountsInstructionsNewText": "För att länka två konton, generera en kod på den första\noch skriv in koden på den andra. Data från det\nandra kontot kommer att delas mellan båda.\n(Data på det första kontot kommer att försvinna)\n\nDu kan länka upp till ${COUNT} konton.\n\nVIKTIGT: länka endast konton du äger;\nOm du länkar med en väns konto kan ni inte\nspela online samtidigt.", + "linkAccountsInstructionsText": "För att länka ihop två konton, generera en kod på\nett av dem och skriv in den på det andra. Framsteg\noch samlingar kommer att föras samman. Du kan länka \nupp till ${COUNT} konton.\n\nVar försiktig, detta kan inte ångras! ", + "linkAccountsText": "Länka konton", + "linkedAccountsText": "Länkade konton:", + "nameChangeConfirm": "Ändra nanmnet på ditt konto till ${NAME}?", + "notLoggedInText": "", + "resetProgressConfirmNoAchievementsText": "Detta kommer att återställa co-op spel och \npoäng (men inte dina värdekuponger). Kan ej ångras.\nÄr du säker?", + "resetProgressConfirmText": "Detta kommer att återställa din co-op kampanj,\nprestationer och poäng (men inte dina värdekuponger).\nDetta kan ej ångras.\nÄr du säker?", + "resetProgressText": "Återställ Framsteg", + "setAccountName": "Välj Konto Namn", + "setAccountNameDesc": "Välj ett namn att visa för ditt konto.\nDu kan använda användarnamnet från något av dina länkade konton eller skapa ett unikt namn.", + "signInInfoText": "Logga in för att samla värdekuponger, tävla \non-line och dela framsteg över olika enheter.", + "signInText": "Logga In", + "signInWithDeviceInfoText": "(endast ett automatiskt konto finns på denna enhet)", + "signInWithDeviceText": "Logga in med ett enhetskonto", + "signInWithGameCircleText": "Logga in med Spel Cirkel", + "signInWithGooglePlayText": "Logga in med Google Play", + "signInWithTestAccountInfoText": "(allmän konto typ; använd enhets konton för att fortsätta)", + "signInWithTestAccountText": "Logga in med ett testkonto", + "signOutText": "Logga Ut", + "signingInText": "Loggar in...", + "signingOutText": "Loggar ut...", + "testAccountWarningCardboardText": "Varning : du loggar in med ett konto \"test\" .\nDessa kommer att ersättas med Google-konton en gång\nde blir stöds i papp appar .\n\nFör nu måste du tjäna alla biljetter i spelet .\n( du gör dock få BombSquad Pro uppgradering gratis )", + "testAccountWarningOculusText": "Varning: du loggar in med ett konto \"test\".\nDessa kommer att ersättas med Oculus konton senare i\n\når som kommer att erbjuda biljettinköp och andra funktioner.\n\nFör nu måste du tjäna alla biljetter i spelet.", + "testAccountWarningText": "Varning: du loggar på med ett \"test\" konto.\nKontot är knutet till en specifik enhet och kan\nkomma att nollställas ibland. (så lägg inte ner\nmycket tid på att samla/låsa upp saker med kontot)\n\nAnvänd betalversionen av spelet för att använda ett\n\"riktigt\" konto (Game-Center, Google Plus, etc.) \nDetta gör även att dina framsteg lagras i molnet \noch delas mellan olika enheter.", + "ticketsText": "Värdekuponger: ${COUNT}", + "titleText": "Konto", + "unlinkAccountsInstructionsText": "Välj ett konto att avlänka", + "unlinkAccountsText": "Avlänka Konton", + "viaAccount": "(via konto ${NAME})", + "youAreLoggedInAsText": "Du är inloggad som:", + "youAreSignedInAsText": "Du är inloggad som:" + }, + "achievementChallengesText": "Prestationsutmaningar", + "achievementText": "Prestation", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Döda 3 fiender med TNT", + "descriptionComplete": "Dödade 3 fiender med TNT", + "descriptionFull": "Döda 3 fiender med TNT på ${LEVEL}", + "descriptionFullComplete": "Dödade 3 fiender med TNT på ${LEVEL}", + "name": "Pang säger dynamiten" + }, + "Boxer": { + "description": "Vinn utan att använda några bomber", + "descriptionComplete": "Vann utan att använda några bomber", + "descriptionFull": "Klara ${LEVEL} utan att använda bomber", + "descriptionFullComplete": "Klarade ${LEVEL} utan att använda bomber", + "name": "Boxare" + }, + "Dual Wielding": { + "descriptionFull": "Koppla 2 kontroller (hårdvara eller app)", + "descriptionFullComplete": "Kopplade 2 kontroller (hårdvara eller app)", + "name": "Dubbel Hantering" + }, + "Flawless Victory": { + "description": "Vinn utan att bli skadad", + "descriptionComplete": "Vann utan att bli skadad", + "descriptionFull": "Vinn ${LEVEL} utan att träffas", + "descriptionFullComplete": "Vann ${LEVEL} utan att bli träffad", + "name": "Perfekt Vinst" + }, + "Free Loader": { + "descriptionFull": "Starta ett Alla-Mot-Alla spel med 2+ spelare", + "descriptionFullComplete": "Börjat ett Alla-Mot-Alla spel med 2+ spelare", + "name": "Gratis Laddare" + }, + "Gold Miner": { + "description": "Döda 6 fiender med landminor", + "descriptionComplete": "Dödade 6 fiender med landminor", + "descriptionFull": "Döda 5 fiender med landminor på ${LEVEL}", + "descriptionFullComplete": "Dödade 6 fiender med landminor i ${LEVEL}", + "name": "Guldgrävare" + }, + "Got the Moves": { + "description": "Vinn utan att använda slag eller bomber", + "descriptionComplete": "Vann utan att använda slag eller bomber", + "descriptionFull": "Vinn ${LEVEL} utan slag eller bomber", + "descriptionFullComplete": "Vann ${LEVEL} utan slag eller bomber", + "name": "Har svinget" + }, + "In Control": { + "descriptionFull": "Ansut en kontroll (Hårdvara eller app)", + "descriptionFullComplete": "Anslöt en kontroll (Hårdvara eller app)", + "name": "I kontroll" + }, + "Last Stand God": { + "description": "Nå 1000 poäng", + "descriptionComplete": "Nådde 1000 poäng", + "descriptionFull": "Få 1000 poäng på ${LEVEL}", + "descriptionFullComplete": "Nådde 1000 poäng i ${LEVEL}", + "name": "${LEVEL} Gud" + }, + "Last Stand Master": { + "description": "Nå 250 poäng", + "descriptionComplete": "Nådde 250 poäng", + "descriptionFull": "Nå 250 poäng på ${LEVEL}", + "descriptionFullComplete": "Nådde 250 poäng i ${LEVEL}", + "name": "${LEVEL} Mästare" + }, + "Last Stand Wizard": { + "description": "Nå 500 poäng", + "descriptionComplete": "Nådde 500 poäng", + "descriptionFull": "Nå 500 poäng i ${LEVEL}", + "descriptionFullComplete": "Nådde 500 poäng i ${LEVEL}", + "name": "${LEVEL} magiker" + }, + "Mine Games": { + "description": "Döda 3 fiender med landminor", + "descriptionComplete": "Dödade 3 fiender med landminor", + "descriptionFull": "Döda 3 fiender med landminor på ${LEVEL}", + "descriptionFullComplete": "Dödade 3 fiender med landminor på ${LEVEL}", + "name": "Minspel" + }, + "Off You Go Then": { + "description": "Kasta ut 3 fiender från kartan", + "descriptionComplete": "Kastade ut 3 fiender från kartan", + "descriptionFull": "Kasta ut 3 fiender från kartan i ${LEVEL}", + "descriptionFullComplete": "Kastade ut 3 fiender från kartan i ${LEVEL}", + "name": "Adjöss med dig" + }, + "Onslaught God": { + "description": "Nå 5000 poäng", + "descriptionComplete": "Nådde 5000 poäng", + "descriptionFull": "Nådde 5000 poäng i ${LEVEL}", + "descriptionFullComplete": "Nådde 5000 poäng i ${LEVEL}", + "name": "${LEVEL} Gud" + }, + "Onslaught Master": { + "description": "Nå 500 poäng", + "descriptionComplete": "Nådde 500 poäng", + "descriptionFull": "Nå 500 poäng på ${LEVEL}", + "descriptionFullComplete": "Nådde 500 poäng i ${LEVEL}", + "name": "${LEVEL} Mästare" + }, + "Onslaught Training Victory": { + "description": "Besegra alla vågor", + "descriptionComplete": "Besegrade alla vågor", + "descriptionFull": "Besegra alla vågor i ${LEVEL}", + "descriptionFullComplete": "Besegrade alla vågor i ${LEVEL}", + "name": "Vinnare av ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Nå 1000 poäng", + "descriptionComplete": "Nådde 1000 poäng", + "descriptionFull": "Nå 1000 poäng i ${LEVEL}", + "descriptionFullComplete": "Nådde 1000 poäng i ${LEVEL}", + "name": "${LEVEL} magiker" + }, + "Precision Bombing": { + "description": "Vinn utan några powerups", + "descriptionComplete": "Vann utan några powerups", + "descriptionFull": "Vinn ${LEVEL} utan några powerups", + "descriptionFullComplete": "Vann ${LEVEL} utan några power-ups", + "name": "Precisionsbombning" + }, + "Pro Boxer": { + "description": "Vinn utan att använda några bomber", + "descriptionComplete": "Vann utan att använda några bomber", + "descriptionFull": "Klara av ${LEVEL} utan att använda några bomber", + "descriptionFullComplete": "Klarade ${LEVEL} utan att använda bomber", + "name": "Proffsboxare" + }, + "Pro Football Shutout": { + "description": "Vinn utan att låta fienden göra mål", + "descriptionComplete": "Vann utan att låta fienden få poäng", + "descriptionFull": "Vinn ${LEVEL} utan att låta fienden få poäng", + "descriptionFullComplete": "Vann ${LEVEL} utan att fienden fick poäng", + "name": "${LEVEL} Shutout" + }, + "Pro Football Victory": { + "description": "Vinn matchen", + "descriptionComplete": "Vann matchen", + "descriptionFull": "Vinn matchen i ${LEVEL}", + "descriptionFullComplete": "Vann matchen i ${LEVEL}", + "name": "Vinnare i ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Klara av alla waves", + "descriptionComplete": "Klarade av alla waves", + "descriptionFull": "Döda alla vågor av ${LEVEL}", + "descriptionFullComplete": "Besegrade alla vågor i ${LEVEL}", + "name": "${LEVEL} Vinst" + }, + "Pro Runaround Victory": { + "description": "Alla vågor avklarade", + "descriptionComplete": "Klarade av alla vågor", + "descriptionFull": "Klarade av alla vågor i ${LEVEL}", + "descriptionFullComplete": "Klarade av alla vågor på ${LEVEL}", + "name": "${LEVEL} Vinst" + }, + "Rookie Football Shutout": { + "description": "Vinn utan att låta fienden få poäng", + "descriptionComplete": "Vann utan att låta fienden få poäng", + "descriptionFull": "Vinn ${LEVEL} utan att låta fienden få poäng", + "descriptionFullComplete": "Vann ${LEVEL} utan att låta fienden få poäng", + "name": "Höll nollan på ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Vinn matchen", + "descriptionComplete": "Vann matchen", + "descriptionFull": "Vinn matchen i ${LEVEL}", + "descriptionFullComplete": "Vann matchen i ${LEVEL}", + "name": "${LEVEL} Vinst" + }, + "Rookie Onslaught Victory": { + "description": "Besegra alla vågor", + "descriptionComplete": "Besegrade alla vågor", + "descriptionFull": "Besegra alla vågor i ${LEVEL}", + "descriptionFullComplete": "Besegrade alla vågor i ${LEVEL}", + "name": "${LEVEL} Vinst" + }, + "Runaround God": { + "description": "Få 2000 poäng", + "descriptionComplete": "Fick 2000 poäng", + "descriptionFull": "Få 2000 poäng i ${LEVEL}", + "descriptionFullComplete": "Fick 2000 poäng i ${LEVEL}", + "name": "${LEVEL}gud" + }, + "Runaround Master": { + "description": "Få 500 poäng", + "descriptionComplete": "Fick 500 poäng", + "descriptionFull": "Få 500 poäng i ${LEVEL}", + "descriptionFullComplete": "Fick 500 poäng i ${LEVEL}", + "name": "Mästare av ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Få 1000 poäng", + "descriptionComplete": "Fick 1000 poäng", + "descriptionFull": "Få 1000 poäng i ${LEVEL}", + "descriptionFullComplete": "Fick 1000 poäng i ${LEVEL}", + "name": "${LEVEL} Magiker" + }, + "Sharing is Caring": { + "descriptionFull": "Lyckas dela spelet med en vän", + "descriptionFullComplete": "Lyckats dela spelet med en vän", + "name": "Att dela är att Bry sig" + }, + "Stayin' Alive": { + "description": "Vinn utan att dö", + "descriptionComplete": "Vann utan att dö", + "descriptionFull": "Vinn ${LEVEL} utan att dö", + "descriptionFullComplete": "Vann ${LEVEL} utan att dö", + "name": "Stayin' Alive" + }, + "Super Mega Punch": { + "description": "Orsaka 100% skada med ett slag", + "descriptionComplete": "Orsakade 100% skada med ett slag", + "descriptionFull": "Orsaka 100% skada med ett slag i ${LEVEL}", + "descriptionFullComplete": "Orsakade 100% skada med ett slag i ${LEVEL}", + "name": "Super Mega Slag" + }, + "Super Punch": { + "description": "Orsaka 50% skada med ett slag", + "descriptionComplete": "Orsakade 50% skada med ett slag", + "descriptionFull": "Orsaka 50% skada med ett slag i ${LEVEL}", + "descriptionFullComplete": "Orsakade 50% skada med ett slag i ${LEVEL}", + "name": "Super Slag" + }, + "TNT Terror": { + "description": "Döda 6 fiender med TNT", + "descriptionComplete": "Dödade 6 fiender med TNT", + "descriptionFull": "Döda 6 fiender med TNT i ${LEVEL}", + "descriptionFullComplete": "Dödade 6 fiender med TNT i ${LEVEL}", + "name": "TNT Terror" + }, + "Team Player": { + "descriptionFull": "Starta ett Team spel med 4+ spelare", + "descriptionFullComplete": "Startade ett Team spel med 4+ spelare", + "name": "Lag Spelare" + }, + "The Great Wall": { + "description": "Stoppa varenda fiende", + "descriptionComplete": "Stoppade varenda fiende", + "descriptionFull": "Stoppa varenda fiende i ${LEVEL}", + "descriptionFullComplete": "Stoppade varenda fiende i ${LEVEL}", + "name": "Den Stora Muren" + }, + "The Wall": { + "description": "Stoppa varenda fiende", + "descriptionComplete": "Stoppade varenda fiende", + "descriptionFull": "Stoppa varenda fiende i ${LEVEL}", + "descriptionFullComplete": "Stoppade varenda fiende i ${LEVEL}", + "name": "Väggen" + }, + "Uber Football Shutout": { + "description": "Vinn utan att låta fienden få poäng", + "descriptionComplete": "Vann utan att låta fienden få poäng", + "descriptionFull": "Vinn ${LEVEL} utan att låta fienden få poäng", + "descriptionFullComplete": "Vann ${LEVEL} utan att låta fienden få poäng", + "name": "Nollan i ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Vinn matchen", + "descriptionComplete": "Vann matchen", + "descriptionFull": "Vinn matchen i ${LEVEL}", + "descriptionFullComplete": "Vann matchen i ${LEVEL}", + "name": "Vinst i ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Besegra alla vågor", + "descriptionComplete": "Besegrade alla vågor", + "descriptionFull": "Besegra alla vågor i ${LEVEL}", + "descriptionFullComplete": "Besegrade alla vågor i ${LEVEL}", + "name": "${LEVEL} Vinst" + }, + "Uber Runaround Victory": { + "description": "Klara av alla vågor", + "descriptionComplete": "Alla vågor avklarade", + "descriptionFull": "Klara av alla vågor på ${LEVEL}", + "descriptionFullComplete": "Alla vågor på ${LEVEL} avklarade", + "name": "${LEVEL} Vinst" + } + }, + "achievementsRemainingText": "Prestationer som återstår:", + "achievementsText": "Prestationer", + "achievementsUnavailableForOldSeasonsText": "Tyvärr, prestation detaljerna är inte tillgängliga för gamla säsonger.", + "addGameWindow": { + "getMoreGamesText": "Hämta Fler Spel...", + "titleText": "Lägg till Spel" + }, + "allowText": "Tillåt", + "alreadySignedInText": "Ditt konto är redan inloggat på en annan enhet;\nvar god byt konto eller stäng spelet på dina\nandra enheter och försök igen.", + "apiVersionErrorText": "Kan inte öppna ${NAME}; den änvänder api-version ${VERSION_USED}; det krävs dock ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Auto\" aktiverar endast detta när hörlurar är inkopplade)", + "headRelativeVRAudioText": "Huvud-Relativt VR Audio", + "musicVolumeText": "Musik Volym", + "soundVolumeText": "Ljud Volym", + "soundtrackButtonText": "Soundtrack", + "soundtrackDescriptionText": "(Lägg till egen musik att lyssna på i spelet)", + "titleText": "Audio" + }, + "autoText": "Automatiskt", + "backText": "Tillbaka", + "banThisPlayerText": "Banna Denna Spelare", + "bestOfFinalText": "Bäst-av-${COUNT} Final", + "bestOfSeriesText": "Bäst av ${COUNT} serier:", + "bestRankText": "Din bästa placering är #${RANK}", + "bestRatingText": "Din bästa rating är ${RATING}", + "betaErrorText": "Denna beta är inte längre aktiv; var snäll kolla efter en ny version.", + "betaValidateErrorText": "Det går inte att validera betan. (Ingen Internetuppkoppling?)", + "betaValidatedText": "Beta Validerad; Njut av spelet!", + "bombBoldText": "BOMB", + "bombText": "Bomb", + "boostText": "Höj", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} är konfigurerad i appen själv.", + "buttonText": "Knapp", + "canWeDebugText": "Vill du att BombSquad automatiskt ska skicka bug, - och crashrapporter och grundläggande användarinfo till utvecklaren?\n\nDenna data innehåller ingen personlig information och\nhjälper till med att låta spelet flyta jämnt och felfritt.", + "cancelText": "Avbryt", + "cantConfigureDeviceText": "Tyvärr, ${DEVICE} är inte konfigurerbar", + "challengeEndedText": "Denna utmaning är avslutad.", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "Du måste klara\ndenna nivå för att fortsätta!", + "completionBonusText": "Slutbonus", + "configControllersWindow": { + "configureControllersText": "Konfigurera Styrenheter", + "configureGamepadsText": "Konfigurera Gamepads", + "configureKeyboard2Text": "Konfigurera Tangentbord P2", + "configureKeyboardText": "Konfigurera Tangentbord", + "configureMobileText": "Mobila Enheter som Styrenheter", + "configureTouchText": "Konfigurera Touchskärm", + "ps3Text": "PS3 Kontroller", + "titleText": "Kontroller", + "wiimotesText": "Wiimotes", + "xbox360Text": "Xbox 360 Kontroller" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Notera: styrenhetsstöd varierar med enhet och Androidversion.", + "pressAnyButtonText": "Tryck valfri knapp på den styrenhet\nsom du vill konfigurera...", + "titleText": "Konfigurera Styrenheter" + }, + "configGamepadWindow": { + "advancedText": "Avancerat", + "advancedTitleText": "Avancerade Inställningar För Styrenheter", + "analogStickDeadZoneDescriptionText": "(öka denna om spelaren 'glider' när du släpper spaken)", + "analogStickDeadZoneText": "Analog spak Dead Zone", + "appliesToAllText": "(gäller alla styrenheter av denna typ)", + "autoRecalibrateDescriptionText": "(slå på denna om din spelare inte springer med full hastighet)", + "autoRecalibrateText": "Autokalibrera analog spak", + "axisText": "axel", + "clearText": "rensa", + "dpadText": "dpad", + "extraStartButtonText": "Extra Start Knapp", + "ifNothingHappensTryAnalogText": "Om ingenting händer, försök tilldela analog spak istället.", + "ifNothingHappensTryDpadText": "Om ingenting händer, försök tilldela dpad istället.", + "ignoreCompletelyDescriptionText": "(förhindra den här kontrollen från att påverka både spelet och menyerna)", + "ignoreCompletelyText": "Ignorera Fullständigt", + "ignoredButton1Text": "Ignorerad Knapp 1", + "ignoredButton2Text": "Ignorerad Knapp 2", + "ignoredButton3Text": "Ignorerad Knapp 3", + "ignoredButton4Text": "Ignorerad Knapp 4", + "ignoredButtonDescriptionText": "(använd denna för att förhindra 'home' och 'sync' knappar från att påverka UI)", + "ignoredButtonText": "Ignorerad Knapp", + "pressAnyAnalogTriggerText": "Tryck på valfri analog trigger", + "pressAnyButtonOrDpadText": "Tryck på valfri knapp eller dpad...", + "pressAnyButtonText": "Tryck på valfri knapp...", + "pressLeftRightText": "Tryck vänster eller höger...", + "pressUpDownText": "Tryck upp eller ned...", + "runButton1Text": "Springknapp 1", + "runButton2Text": "Springknapp 2", + "runTrigger1Text": "Springavtryckare 1", + "runTrigger2Text": "Springavtryckare 2", + "runTriggerDescriptionText": "(analoga avtryckare låter dig springa med varierande hastighet)", + "secondHalfText": "Använd denna för att konfigurera\nandra halvan av 2-i-1 styrenhet\nsom visas som en styrenhet.", + "secondaryEnableText": "Tillåt", + "secondaryText": "Andra Styrenhet", + "startButtonActivatesDefaultDescriptionText": "(stäng av om startknappen är mer som en menyknapp)", + "startButtonActivatesDefaultText": "Startknapp aktiverar standardwidget", + "titleText": "Inställningar För Styrenheter", + "twoInOneSetupText": "Inställningar för 2-i-1 styrenhet", + "uiOnlyDescriptionText": "(förhindra den här kontrollen från att delta i ett spel)", + "uiOnlyText": "Begränsa till Menyer", + "unassignedButtonsRunText": "Alla icke tilldelade knappar - Spring", + "unsetText": "", + "vrReorientButtonText": "VR Omorienterings Knapp" + }, + "configKeyboardWindow": { + "configuringText": "Konfigurerar ${DEVICE}", + "keyboard2NoteText": "Notera: de flesta tangentbord kan endast registrera några få\ntangenttryckningar på en gång. Att låta en andra spelare\nanvända ett separat tangentbord kan fungera bättre. \nNotera att separata knappar måste tilldelas för de bägge\nspelarna även med dubbla tangentbord." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Handlings Kontroll Skala", + "actionsText": "Handlingar", + "buttonsText": "knappar", + "dragControlsText": "< dra kontroller för att återpositionera dem >", + "joystickText": "joystick", + "movementControlScaleText": "Rörelse Kontroll Skala", + "movementText": "Rörelse", + "resetText": "Återställ", + "swipeControlsHiddenText": "Dölj Swipe Ikoner", + "swipeInfoText": "'Swipe' kontroller tar lite tid att vänja sig vid, \nmen gör det lättare att spela utan att titta på skärmen.", + "swipeText": "swipe", + "titleText": "Konfigurera touchscreen", + "touchControlsScaleText": "Touchkontroll skalning" + }, + "configureItNowText": "Konfigurera nu?", + "configureText": "Konfigurera", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "För bästa resultat behöver du ett responsivt wifinätverk.\nDu kan förbättra spelupplevelsen genom att stäng av\nandra wifi-enheter eller spela nära din wifi-router.\nDu kan också koppla upp spelet direkt via ethernet.", + "explanationText": "För att använda en smartphone eller tablet som styrenhet,\ninstallera appen \"${REMOTE_APP_NAME}\". Obegränsat antal enheter \nkan anslutas för ${APP_NAME} spel över wifi, och det är gratis!", + "forAndroidText": "för Android:", + "forIOSText": "för iOS:", + "getItForText": "Hämta ${REMOTE_APP_NAME} för iOS i Apple App Store\neller för Android i Google Play Store eller Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Använder Mobila Enheter som Styrenheter" + }, + "continuePurchaseText": "Fortsätt i ${PRICE}?", + "continueText": "fortsätta", + "controlsText": "Styrenhet", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Detta gäller inte för alla-time ranking.", + "activenessInfoText": "Denna multiplikator stiger på dagar när du\nspela och droppar på dagar när du inte gör det.", + "activityText": "Aktivitet", + "campaignText": "Kampanj", + "challengesInfoText": "Samla priser för avslutade mini-spel\n\nPriser och svårighetsgraden ökar varje \ngång en utmaning slutförs och minskar \nnär den förfaller eller förverkats.", + "challengesText": "Utmaningar", + "currentBestText": "Nuvarande Rekord", + "customText": "Anpassad", + "entryFeeText": "Avgift", + "forfeitConfirmText": "Ge upp denna utmaning?", + "forfeitNotAllowedYetText": "Denna utmaning kan inte förverkas än", + "forfeitText": "Ge upp", + "multipliersText": "Multiplikatorer", + "nextChallengeText": "Nästa utmaning", + "nextPlayText": "Nästa spel", + "ofTotalTimeText": "av ${TOTAL}", + "playNowText": "Spela nu", + "pointsText": "Poäng", + "powerRankingFinishedSeasonUnrankedText": "(Icke rankad avslutad säsong)", + "powerRankingNotInTopText": "(inte i top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} poäng", + "powerRankingPointsMultText": "(x ${NUMBER} poäng)", + "powerRankingPointsText": "${NUMBER} pts", + "powerRankingPointsToRankedText": "(${CURRENT} på ${REMAINING} pts)", + "powerRankingText": "Spelar Rankning", + "prizesText": "Priser", + "proMultInfoText": "Spelare med ${PRO} uppgradering\nfå en ${PERCENT}% punkt uppsving här.", + "seeMoreText": "Mer...", + "skipWaitText": "Skippa väntetiden", + "timeRemainingText": "Tid Kvar", + "titleText": "Co-op", + "toRankedText": "Till Rankat", + "totalText": "totalt", + "tournamentInfoText": "Tävla om att få högsta poäng\nmot andra spelare i din liga.\n\nPriser tilldelas de spelare med högst \npoäng när tiden för turneringen runnit ut.", + "welcome1Text": "Välkommen till ${LEAGUE}. Du kan förbättra din\nTabellplacering genom att tjäna stjärnklassificeringar , slutföra\nprestationer , och vinnande troféer i turneringar .", + "welcome2Text": "Du kan också tjäna biljetter från många av samma verksamhet .\nBiljetter kan användas för att låsa upp nya karaktärer, kartor och\nmini -spel , att delta i turneringar och mycket mer .", + "yourPowerRankingText": "Din Spelar Rankning" + }, + "copyOfText": "${NAME} Kopia", + "copyText": "Kopiera", + "createAPlayerProfileText": "Skapa en spelarprofil?", + "createEditPlayerText": "", + "createText": "Skapa", + "creditsWindow": { + "additionalAudioArtIdeasText": "Tilläggsmusik, tidig grafik och idéer av ${NAME}", + "additionalMusicFromText": "Tilläggsmusik från ${NAME}", + "allMyFamilyText": "Alla mina vänner och familj som hjälpte till att testa", + "codingGraphicsAudioText": "Programmering, grafik och ljud av ${NAME}", + "languageTranslationsText": "Språköversättning:", + "legalText": "Rättsligt:", + "publicDomainMusicViaText": "Public domain musik via ${NAME}", + "softwareBasedOnText": "Denna mjukvara är delvis baserad på verk av ${NAME}", + "songCreditText": "${TITLE} Framförd av ${PERFORMER}\nKomponerad av ${COMPOSER}. Arrangerad av ${ARRANGER}. Publiserad av ${PUBLISHER}.\nMed tillstånd av ${SOURCE}", + "soundAndMusicText": "Ljud och musik:", + "soundsText": "Ljud (${SOURCE}):", + "specialThanksText": "Särskilda tack:", + "thanksEspeciallyToText": "Tack, särskilt, till ${NAME}", + "titleText": "${APP_NAME} Eftertexter", + "whoeverInventedCoffeeText": "Den som uppfann kaffe" + }, + "currentStandingText": "Din nuvarande ställning är #${RANK}", + "customizeText": "Redigera...", + "deathsTallyText": "Dödad ${COUNT} gånger", + "deathsText": "Dödad", + "debugText": "debug", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Notering: rekommendetionen är att man ställer Inställningar->Grafik->Texturer till 'Hög' innan testet körs.", + "runCPUBenchmarkText": "Kör CPU Benchmark", + "runGPUBenchmarkText": "Kör GPU Benchmark", + "runMediaReloadBenchmarkText": "Kör Media-Omladdning Benchmark", + "runStressTestText": "Kör stresstest", + "stressTestPlayerCountText": "Antal spelare", + "stressTestPlaylistDescriptionText": "Stresstest Spelarlista", + "stressTestPlaylistNameText": "Spellista namn", + "stressTestPlaylistTypeText": "Spellista typ", + "stressTestRoundDurationText": "Rundans varaktighet", + "stressTestTitleText": "Stresstest", + "titleText": "Benchmark och Stress Tester", + "totalReloadTimeText": "Total omladdnings tid: ${TIME} (se loggen för detaljer)", + "unlockCoopText": "Lås up co-op nivåer" + }, + "defaultFreeForAllGameListNameText": "Standard fritt-för-alla Spel", + "defaultGameListNameText": "Standard ${PLAYMODE} Spellista", + "defaultNewFreeForAllGameListNameText": "Mina Free-for-All Spel", + "defaultNewGameListNameText": "Min ${PLAYMODE} Spellista", + "defaultNewTeamGameListNameText": "Mina Lagspel", + "defaultTeamGameListNameText": "Standard Lagspel", + "deleteText": "Radera", + "denyText": "Neka", + "desktopResText": "Skrivbordsupplösning", + "difficultyEasyText": "Lätt", + "difficultyHardOnlyText": "Endast Svårt Läge", + "difficultyHardText": "Svår", + "difficultyHardUnlockOnlyText": "Denna bana kan bara låsas up i svårt läge.\nTror du att du har det som krävs!?!?!", + "directBrowserToURLText": "Var god besök följande URL med en webbläsare:", + "disableRemoteAppConnectionsText": "Inaktivera Fjärrapps-Anslutningar", + "disableXInputDescriptionText": "Tillåter mer än 4 kontroller men kanske inte funkar så bra.", + "disableXInputText": "Inaktivera XInput", + "doneText": "Klar", + "drawText": "Oavgjort", + "duplicateText": "Fördubbla", + "editGameListWindow": { + "addGameText": "Lägg till\nSpel", + "cantOverwriteDefaultText": "Kan inte skriva över standardspellistan!", + "cantSaveAlreadyExistsText": "En spellista med det namnet finns redan!", + "cantSaveEmptyListText": "Kan inte spara en tom spellista!", + "editGameText": "Redigera\nSpel", + "gameListText": "Spellista", + "listNameText": "Spellistnamn", + "nameText": "Namn", + "removeGameText": "Ta Bort\nSpel", + "saveText": "Spara lista", + "titleText": "Spelliste Editor" + }, + "editProfileWindow": { + "accountProfileInfoText": "Den här speciella profilen har ett namn\noch en icon baserat på ditt konto.\n\n${ICONS}\n\nSkapa en anpassad profil för att använda\nolika namn eller anpassade ikoner.", + "accountProfileText": "(Kontoprofil)", + "availableText": "Namnet \"${NAME}\" är tillgängligt.", + "changesNotAffectText": "Notera: ändringar påverkar inte spelare inne i ett spel", + "characterText": "karaktär", + "checkingAvailabilityText": "Kontrollerar tillgängligheten för \"${NAME}\"...", + "colorText": "färg", + "getMoreCharactersText": "Skaffa fler karaktärer...", + "getMoreIconsText": "Skaffa fler ikoner...", + "globalProfileInfoText": "Globala spelarprofiler är garanterade att ha unika, \nvärldsomspännande namn. De inkluderar också specialikoner.", + "globalProfileText": "(global profil)", + "highlightText": "highlight", + "iconText": "ikon", + "localProfileInfoText": "Lokala spelarprofiler har inga ikoner och namnet är \ninte garanterat att vara unikt. Uppdatera till en global \nprofil för att kunna använda ett unikt namn och ikon.", + "localProfileText": "(lokal profil)", + "nameDescriptionText": "Spelarnamn", + "nameText": "Namn", + "randomText": "slumpvis", + "titleEditText": "Redigera Profil", + "titleNewText": "Ny Profil", + "unavailableText": "\"${NAME}\" är upptaget; försök med ett annat namn.", + "upgradeProfileInfoText": "Det här kommer att skydda ditt spelar namn världen över \nsamt låta dig tilldela en specialikon till profilen.", + "upgradeToGlobalProfileText": "Uppgradera till en global profil" + }, + "editProfilesAnyTimeText": "(du kan redigera profiler när du vill under 'inställningar')", + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Du kan inte ta bort standardsoundtracket.", + "cantEditDefaultText": "Kan inte redigera standardsoundtrack. Duplicera eller skapa ett nytt.", + "cantOverwriteDefaultText": "Kan inte skriva över standardsoundtrack", + "cantSaveAlreadyExistsText": "Ett soundtrack med det namnet finns redan!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Standardsoundtrack", + "deleteConfirmText": "Ta bort soundtrack:\n'${NAME}'?", + "deleteText": "Ta Bort\nSoundtrack", + "duplicateText": "Duplicera\nSoundtrack", + "editSoundtrackText": "Soundtrack Editor", + "editText": "Redigera\nSoundtrack", + "fetchingITunesText": "hämtar iTunes spellistor...", + "musicVolumeZeroWarning": "Varning: Musikvolym är satt till 0", + "nameText": "Namn", + "newSoundtrackNameText": "Mitt Soundtrack ${COUNT}", + "newSoundtrackText": "Nytt Soundtrack:", + "newText": "Ny\nSoundtrack", + "selectAPlaylistText": "Välj en Spellista", + "selectASourceText": "Musikkälla", + "soundtrackText": "Ljudspår", + "testText": "test", + "titleText": "Soundtrack", + "useDefaultGameMusicText": "Standard Spelmusik", + "useITunesPlaylistText": "iTunes Spellista", + "useMusicFileText": "Musikfil (mp3, etc)", + "useMusicFolderText": "Mappnamn för Musikfiler" + }, + "editText": "Redigera", + "endText": "slut", + "enjoyText": "Ha det så roligt!", + "epicDescriptionFilterText": "${DESCRIPTION} i episk slow moition.", + "epicNameFilterText": "Episk ${NAME}", + "errorAccessDeniedText": "åtkomst nekad", + "errorOutOfDiskSpaceText": "slut på diskutrymme", + "errorText": "Fel", + "errorUnknownText": "okänt fel", + "exitGameText": "Avsluta ${APP_NAME}?", + "exportSuccessText": "'${NAME}' exporterad.", + "externalStorageText": "Extern Lagring", + "failText": "Misslyckades", + "fatalErrorText": "Hoppsan; något saknas eller är trasigt.\nVänligen försök att ominstallera appen\neller kontakta ${EMAIL} för hjälp.", + "fileSelectorWindow": { + "titleFileFolderText": "Välj en Fil eller Mapp", + "titleFileText": "Välj en Fil", + "titleFolderText": "Välj en Mapp", + "useThisFolderButtonText": "Använd Denna Mapp" + }, + "finalScoreText": "Slutgiltig Poäng", + "finalScoresText": "Slutgiltiga Poäng", + "finalTimeText": "Slutgiltig Tid", + "finishingInstallText": "Slutför installationen; ett ögonblick...", + "fireTVRemoteWarningText": "* För en bättre upplevelse, använd\nSpelkontroller eller installera\n'${REMOTE_APP_NAME}' appen på din\ntelefon och surfplatta.", + "firstToFinalText": "Först-till-${COUNT} Final", + "firstToSeriesText": "Först-till-${COUNT} Serie", + "fiveKillText": "FIVE KILL!!!", + "flawlessWaveText": "Felfri Våg", + "fourKillText": "QUAD KILL!!!", + "freeForAllText": "Free-for-All", + "friendScoresUnavailableText": "Vänners poäng otillgängligt", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Spel ${COUNT} Ledare", + "gameListWindow": { + "cantDeleteDefaultText": "Det går inte att ta bort standardspellistan!", + "cantEditDefaultText": "Kan inte redigera standardspellistan! Duplicera eller skapa en ny.", + "cantShareDefaultText": "Du kan inte dela standard spellistan.", + "deleteConfirmText": "Ta Bort \"${LIST}\"?", + "deleteText": "Ta Bort\nSpellista", + "duplicateText": "Duplicera\nSpellista", + "editText": "Redigera\nSpellista", + "gameListText": "Spellista", + "newText": "Ny\nSpellista", + "showTutorialText": "Visa Handledning", + "shuffleGameOrderText": "Blanda spel-ordning", + "titleText": "Redigera ${TYPE} Spellistor" + }, + "gameSettingsWindow": { + "addGameText": "Lägg till spel" + }, + "gamepadDetectedText": "1 gamepad upptäckt.", + "gamepadsDetectedText": "${COUNT} gamepads upptäckta", + "gamesToText": "${WINCOUNT} spel mot ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Kom ihåg: enheter anslutna till ett sällskap kan ha mer \nän en spelare om det finns tillräckligt med styrenheter.", + "aboutDescriptionText": "Använd dessa flikar för att samla ett sällskap.\n\nSällskap gör det möjligt att spela spel och \nturneringar med dina vänner på olika enheter.\n\nAnvänd ${PARTY} knappen i övre högra hörnet \nför att chatta och interagera med sällskapet.\n(på en styrenhet, tryck ${BUTTON} medan en meny visas)", + "aboutText": "Om", + "addressFetchErrorText": "", + "appInviteInfoText": "Bjud in vänner att prova BombSquad så får de \n${COUNT} gratis biljetter och du får ${YOU_COUNT} \nför varje vän som gör det. ", + "appInviteMessageText": "${NAME} skickade ${COUNT} biljetter till dig i ${APP_NAME}", + "appInviteSendACodeText": "Skicka en kod till dem..", + "appInviteTitleText": "Inbjudan att ladda ner ${APP_NAME}", + "bluetoothAndroidSupportText": "(fungerar med alla Android enheter som stöder Bluetooth)", + "bluetoothDescriptionText": "Agera värd/anslut till sällskap över Bluetooth:", + "bluetoothHostText": "Aggera värd över Bluetooth", + "bluetoothJoinText": "Anslut över Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "kontrollerar...", + "dedicatedServerInfoText": "För bästa resultat, skapa en dedikerad server. Gå till bombsquadgame.com/server för att lära dig hur.", + "disconnectClientsText": "Detta gör att ${COUNT} spelare kopplas \nifrån ditt sällskap. Är du säker?", + "earnTicketsForRecommendingAmountText": "Vänner kommer få ${COUNT} biljetter om dom testar spelet\n(och du kommer få ${YOU_COUNT} för varje person som testar)", + "earnTicketsForRecommendingText": "Dela spelet med andra\nOch få fribiljetter...", + "emailItText": "Mejla Det", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME}-biljetter från ${NAME}", + "friendPromoCodeAwardText": "Du kommer få ${COUNT} biljetter varje gång den används.", + "friendPromoCodeExpireText": "Den här koden kommer sluta fungera om ${EXPIRE_HOURS} timmar och fungerar bara för nya spelare.", + "friendPromoCodeInstructionsText": "För att använda den, öppna ${APP_NAME} och gå till \"Inställningar->Avancerat->Skriv in Promo Kod\".\nBesök bombsquadgame.com för nerladdnings-länkar till alla platformar som stöds.", + "friendPromoCodeRedeemLongText": "Den kan lösas in mot ${COUNT} gratis biljetter för upp till ${MAX_USES} personer.", + "friendPromoCodeRedeemShortText": "Den kan lösas in för ${COUNT} biljetter i spelet.", + "friendPromoCodeWhereToEnterText": "(i \"Inställningar->Avancerat->Skriv Promo Kod\")", + "getFriendInviteCodeText": "Få Inbjudnings-Kod för Vänner", + "googlePlayDescriptionText": "Bjud in Google Play spelare till ditt sällskap:", + "googlePlayInviteText": "Bjud In", + "googlePlayReInviteText": "Det finns ${COUNT} Google Play spelare i ditt sällskap\nsom kommer att kopplas ifrån om du gör en ny inbjudan.\nInkludera dem i nya inbjudan för att ha dem kvar.", + "googlePlaySeeInvitesText": "Se Inbjudningar", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play version)", + "hostPublicPartyDescriptionText": "Var värd för Offentligt Spel:", + "inDevelopmentWarningText": "Notera:\n\nSällskap är en ny och ofärdig funktionalitet.\nFör tillfället är det rekommenderat att alla\nspelare är på samma Wi-Fi nätverk.", + "internetText": "Internet", + "inviteAFriendText": "Har dina vänner inte spelet? Bjud in dem att\nprova så får de ${COUNT} fribiljetter.", + "inviteFriendsText": "Bjud in vänner", + "joinPublicPartyDescriptionText": "Anslut till Offentligt Spel:", + "localNetworkDescriptionText": "Anslut till ett sällskap på ditt nätverk:", + "localNetworkText": "Lokalt Nätverk", + "makePartyPrivateText": "Gör Mitt Spel Privat", + "makePartyPublicText": "Gör Mitt Spel Offentligt", + "manualAddressText": "Adress", + "manualConnectText": "Anslut", + "manualDescriptionText": "Anslut till sällskap via adress:", + "manualJoinableFromInternetText": "Kan man ansluta till dig via internet?:", + "manualJoinableNoWithAsteriskText": "NEJ*", + "manualJoinableYesText": "JA", + "manualRouterForwardingText": "*för att åtgärda detta, konfigurera routern så att den vidarebefordrar UDP port ${PORT} till din lokala adress", + "manualText": "Manuellt", + "manualYourAddressFromInternetText": "Din publika internet adress:", + "manualYourLocalAddressText": "Din lokala adress:", + "noConnectionText": "", + "otherVersionsText": "(andra versioner)", + "partyInviteAcceptText": "Acceptera", + "partyInviteDeclineText": "Neka", + "partyInviteGooglePlayExtraText": "(Se 'Google Play' fliken i 'Samla' fönstret)", + "partyInviteIgnoreText": "Ignorera", + "partyInviteText": "${NAME} har bjudit in \ndig till sitt sällskap!", + "partyNameText": "Spelnamn", + "partySizeText": "antal spelare", + "partyStatusCheckingText": "kontrollerar status...", + "partyStatusJoinableText": "ditt spel kan nu anslutas till från internet", + "partyStatusNoConnectionText": "kunde inte koppla till server", + "partyStatusNotJoinableText": "ditt spel kan inte anslutas till från internet", + "partyStatusNotPublicText": "ditt spel är inte offentligt", + "pingText": "ping", + "portText": "Port", + "requestingAPromoCodeText": "Efterfrågar en kod...", + "sendDirectInvitesText": "Skicka inbjudningar direkt", + "sendThisToAFriendText": "Skicka denna koden till en vän:", + "shareThisCodeWithFriendsText": "Dela den här koden med vänner:", + "showMyAddressText": "Visa Min Adress", + "titleText": "Samla", + "wifiDirectDescriptionBottomText": "Om alla enheter har en 'Wi-Fi Direct' panel, borde de kunna använda den för att hitta\noch ansluta till varandra. När alla enheter är anslutna, kan man skapa näverksspel\nhär genom att använda fliken 'Lokalt Nätverk' precis som med ett vanligt Wi-Fi nätverk.\n\nFör bästa resultat bör Wi-Fi Direct värden även vara ${APP_NAME} sällskapsvärd.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct kan användas för att ansluta Android enheter direkt utan\nett Wi-Fi nätverk. Detta funkar bäst på Android 4.2 eller senare.\n\nAktivera genom att öppna Wi-Fi inställingar och hitta 'Wi-Fi Direct' i menyn.", + "wifiDirectOpenWiFiSettingsText": "Öppna Wi-Fi Inställnigar", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(fungerar mellan alla plattformar)", + "worksWithGooglePlayDevicesText": "(fungerar med alla enheter som kör Google Play (android) version av spelet)", + "youHaveBeenSentAPromoCodeText": "Du har fått en promo kod för ${APP_NAME}:" + }, + "getCoinsWindow": { + "coinDoublerText": "Myntfördubblare", + "coinsText": "${COUNT} mynt", + "freeCoinsText": "Gratis Mynt", + "restorePurchasesText": "Återställ Köp", + "titleText": "Skaffa Mynt" + }, + "getTicketsWindow": { + "freeText": "GRATIS!", + "freeTicketsText": "Gratis Värdekuponger", + "inProgressText": "En transaktion är pågående; försök igen om en stund.", + "purchasesRestoredText": "Inköp återställts.", + "receivedTicketsText": "Du har fått ${COUNT} värdekuponger!", + "restorePurchasesText": "Återställ Köp", + "ticketDoublerText": "Värdekupongsdubblerare", + "ticketPack1Text": "Värdekupongspaket - Liten", + "ticketPack2Text": "Värdekupongspaket - Mellan", + "ticketPack3Text": "Värdekupongspaket - Stor", + "ticketPack4Text": "Värdekupongspaket - Jumbo", + "ticketPack5Text": "Värdekupongspaket - Mammut", + "ticketPack6Text": "Ultimata Värdekupongspaketet", + "ticketsFromASponsorText": "Få ${COUNT} värdekuponger\nfrån en sponsor", + "ticketsText": "${COUNT} Värdekuponger", + "titleText": "Skaffa Värdekuponger", + "unavailableLinkAccountText": "Sorry, det går inte att göra inköp på den här plattformen. \nOm du vill kan du länka det här kontot till en annan\nplattform och göra inköpen där.", + "unavailableTemporarilyText": "Detta är inte tillgänglig för tillfället; försök igen senare.", + "unavailableText": "Tyvärr, detta är inte tillgängligt.", + "versionTooOldText": "Tyvärr, denna version av spelet är för gammalt; vänligen uppgradera till en nyare.", + "youHaveShortText": "du har ${COUNT}", + "youHaveText": "du har ${COUNT} värdekuponger" + }, + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Alltid", + "fullScreenCmdText": "Fullskärm (Cmd-F)", + "fullScreenCtrlText": "Fullskärm (Ctrl-F)", + "gammaText": "Gamma", + "highText": "Hög", + "higherText": "Högre", + "lowText": "Låg", + "mediumText": "Medium", + "neverText": "Aldrig", + "resolutionText": "Upplösning", + "showFPSText": "Visa FPS", + "texturesText": "Texturer", + "titleText": "Grafik", + "tvBorderText": "TV-kant", + "verticalSyncText": "Vertical Sync", + "visualsText": "Visuellt" + }, + "helpWindow": { + "bombInfoText": "- Bomb -\nKraftfullare än slag, men \nkastaren riskerar svåra skador\nFör bästa resultat, kasta mot \nfienden innan stubinen brunnit ut.", + "canHelpText": "${APP_NAME} kan hjälpa.", + "controllersInfoText": "Du kan spela ${APP_NAME} med vänner över nätverk, eller så\nkan alla spela på samma enhet om du har tillräckligt med styrenheter.\n${APP_NAME} stöder en uppsjö av dem; du kan till och med använda\nmobiler som enheter via gratisappen '${REMOTE_APP_NAME}'.\nSe Inställningar->Styrenheter för mer info.", + "controllersInfoTextFantasia": "En spelare kan använda fjärrkontrollen som styrenhet, men\ngamepads rekommenderas starkt. Du kan också använda dina\nmobila enheter som styrenheter via appen 'BombSquad Remote'\nsom är gratis. Se 'Styrenheter' under 'Inställningar' för mer info.", + "controllersInfoTextMac": "En eller två spelare kan använda tangentbordet men BombSquad är bäst med gamepads.\nBombSquad kan använda USB gamepads, PS3- eller Xbox 360 styrenheter,\nWiimotes och iOS-/Androidenheter för att kontrollera spelaren. Förhoppningsvis har du \nnågon av dessa att tillgå. Se 'Styrenheter' under 'Inställningar' för mer info.", + "controllersInfoTextOuya": "Du kan använda OUYA kontroller, PS3 kontroller, Xbox 360 kontroller \noch många andra USB och Bluetooth kontroller med BombSquad. \nDu kan även använda iOS och Android enheter som kontroll via \ngratis-appen 'BombSquad Remote'. Se 'Kontroller' under 'Inställningar' för mer info.", + "controllersText": "Styrenheter", + "controlsSubtitleText": "Din vänliga ${APP_NAME}-karaktär har några grundläggande handlingar:", + "controlsText": "Kontroller", + "devicesInfoText": "VR versionen av ${APP_NAME} kan spelas över nätverk med en vanlig\nversion, så ta fram dina extra mobiler, surfplattor och datorer\noch sätt igång. Det kan även vara användbart att koppla upp en \nvanlig version av spelet till VR versionen så att andra kan \nfå se vad som händer i spelet.", + "devicesText": "Enheter", + "friendsGoodText": "Dessa är bra att ha. ${APP_NAME} är roligast med flera spelare\noch stöder upp till åtta åt gången, vilket osökt leder oss till:", + "friendsText": "Vänner", + "jumpInfoText": "- Hoppa -\nHoppa mellan små luckor,\nför att kasta högre och för \natt uttrycka lyckokänslor.", + "orPunchingSomethingText": "Eller slå på någonting, kasta det över kanten eller spräng det på vägen ner med en klisterbomb.", + "pickUpInfoText": "- Plocka upp -\nLyft upp flaggor, fiender eller vad \nsom helst som inte är fastspikat.\nTryck igen för att kasta.", + "powerupBombDescriptionText": "Låter dig kasta ut tre bomber på raken \nistället för bara en i taget.", + "powerupBombNameText": "Tripplebomber", + "powerupCurseDescriptionText": "Du vill antagligen undvika dessa.\n...eller?", + "powerupCurseNameText": "Förbannelse", + "powerupHealthDescriptionText": "Återställer dig till full hälsa.\nDet kunde du inte gissa.", + "powerupHealthNameText": "Första hjälpen", + "powerupIceBombsDescriptionText": "Svagare än vanliga bomber\nmen fryser dina fiender och \ngör dem väldigt sköra", + "powerupIceBombsNameText": "Isbomber", + "powerupImpactBombsDescriptionText": "Något svagare än vanliga bomber,\nmen exploderar vid träff.", + "powerupImpactBombsNameText": "Utlösningbomber", + "powerupLandMinesDescriptionText": "Dessa kommer i trepack;\nAnvändbart för försvar eller\nför att stoppa snabba fiender.", + "powerupLandMinesNameText": "Landminor", + "powerupPunchDescriptionText": "Gör din slag hårdare,\nsnabbare, bättre, starkare.", + "powerupPunchNameText": "Boxarhandskar", + "powerupShieldDescriptionText": "Absorberar lite skada,\nså att du slipper.", + "powerupShieldNameText": "Energisköld", + "powerupStickyBombsDescriptionText": "Fastnar på vad de än träffar.\nFestligheter blir följden.", + "powerupStickyBombsNameText": "Klisterbomber", + "powerupsSubtitleText": "Självklart, inget spel är komplett utan power-ups:", + "powerupsText": "Power-ups", + "punchInfoText": "- Slå -\nSlag gör mer skada ju snabbare\ndina knytnävar rör sig, så\nspring och snurra som en galning", + "runInfoText": "- Spring -\nHåll in VALFRI knapp för att springa. Avtryckare och sidoknappar fungerar också om du har det.\nAtt springa tar dig fram snabbare men gör det svårare att svänga, så akta dig för stupen.", + "someDaysText": "Vissa dagar känner man för att slå något. Eller för att spränga något.", + "titleText": "BombSquad Hjälp", + "toGetTheMostText": "För att få ut mest av detta spel behöver du:", + "welcomeText": "Välkommen till ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} navigerar nu menyerna som chefen -", + "importPlaylistCodeInstructionsText": "Använd följande kod för att importera den här spellistan någon annanstans:", + "importPlaylistSuccessText": "Importerade ${TYPE} spellistan '${NAME}'", + "importText": "Importera", + "importingText": "Importerar...", + "inGameClippedNameText": "i-spel namn kommer att vara\n\"${NAME}\"", + "installDiskSpaceErrorText": "Fel: Kunde inte slutföra installationen.\nDet kan vara slut på utrymme på din enhet.\nFrigör utrymme och försök igen.", + "internal": { + "arrowsToExitListText": "tryck ${LEFT} eller ${RIGHT} för att avsluta listan", + "buttonText": "knapp", + "cantKickHostError": "Du kan inte sparka ut värden.", + "connectedToPartyText": "Ansluten till ${NAME}s sällskap!", + "connectingToPartyText": "Ansluter...", + "connectionFailedHostAlreadyInPartyText": "Anslutning misslyckades; värden är ansluten till annat sällskap.", + "connectionFailedPartyFullText": "Gick inte att ansluta; sällskapet är fullt.", + "connectionFailedText": "Anslutning misslyckades.", + "connectionFailedVersionMismatchText": "Anslutning misslyckades; värden kör en annan version av spelet.\nKontrollera att ni båda har senaste versionen och försök igen.", + "connectionRejectedText": "Anslutning avvisad.", + "controllerConnectedText": "${CONTROLLER} ansluten.", + "controllerDetectedText": "1 kontroll upptäckt.", + "controllerDisconnectedText": "${CONTROLLER} frånkopplad.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} frånkopplad. Försök anslut igen.", + "controllerForMenusOnlyText": "Denna kontrollen kan inte användas för spel;\nenbart för att navigera i menyer.", + "controllerReconnectedText": "${CONTROLLER} återansluten.", + "controllersConnectedText": "${COUNT} kontroller anslutna.", + "controllersDetectedText": "${COUNT} kontroller upptäckta.", + "controllersDisconnectedText": "${COUNT} kontroller frånkopplade.", + "corruptFileText": "Korrupta fil(er) detekterade. Vänligen installera om eller maila ${EMAIL}", + "errorPlayingMusicText": "Fel vid uppspelning av musik: ${MUSIC}", + "errorResettingAchievementsText": "Kunde inte återställa online-prestationer; försök igen senare.", + "hasMenuControlText": "${NAME} har menykontrollen.", + "incompatibleVersionHostText": "Värden kör en annan version av spelet.\nKontrollera att ni båda har senaste versionen.", + "incompatibleVersionPlayerText": "${NAME} kör en annan version av spelet.\nKontrollera att ni båda har senaste versionen och försök igen.", + "invalidAddressErrorText": "Fel: ogiltig adress.", + "invitationSentText": "Inbjudan skickad.", + "invitationsSentText": "${COUNT} inbjudan skickade.", + "joinedPartyInstructionsText": "En vän har anslutit till ditt sällskap.\nGå till 'Spela' för att starta spelet.", + "keyboardText": "Tangentbord", + "kickIdlePlayersKickedText": "Sparkar ${NAME} för att vara inaktiv.", + "kickIdlePlayersWarning1Text": "${NAME} kommer bli sparkad om ${COUNT} sekunder om fortfarande inaktiv.", + "kickIdlePlayersWarning2Text": "(Du kan stänga av detta i Inställningar -> Avancerat)", + "leftPartyText": "Lämnat ${NAME}s sällskap.", + "noMusicFilesInFolderText": "Mappen innehåller ingen musikfil.", + "playerJoinedPartyText": "${NAME} anslöt till sällskapet!", + "playerLeftPartyText": "${NAME} lämnade sällskapet.", + "rejectingInviteAlreadyInPartyText": "Avvisar inbjudan (redan ansluten till ett sällskap).", + "signInErrorText": "Kunde inte logga in.", + "signInNoConnectionText": "Kunde inte logga in. (ingen internet anslutning?)", + "teamNameText": "Lag ${NAME}", + "telnetAccessDeniedText": "ERROR: användare har inte godkänt telnet-åtkomst.", + "timeOutText": "(tiden går ut om ${TIME} sekunder)", + "touchScreenJoinWarningText": "Du har anslutit med touchscreen.\nOm detta var ett misstag, tryck 'Meny->Lämna Spel'.", + "touchScreenText": "Pekskärm", + "trialText": "test", + "unavailableNoConnectionText": "Det är otillgängligt för tillfället (ingen internet anslutning?)", + "vrOrientationResetCardboardText": "Använd detta för att återställa VR orientering.\nFör att spela spelet behöver du en extern styrenhet .", + "vrOrientationResetText": "VR orientering nollställd.", + "willTimeOutText": "(kommer att time:a ut om inaktiv)" + }, + "jumpBoldText": "HOPPA", + "jumpText": "Hoppa", + "keepText": "Behåll", + "keepTheseSettingsText": "Behåll dessa inställningar?", + "killsTallyText": "${COUNT} dödade", + "killsText": "Dödade", + "kioskWindow": { + "easyText": "Lätt", + "epicModeText": "Episkt Läge", + "fullMenuText": "Full Meny", + "hardText": "Svår", + "mediumText": "Medium", + "singlePlayerExamplesText": "En spelar / Co-op Exempel", + "versusExamplesText": "Versus Exempel" + }, + "languageSetText": "Språket är nu \"${LANGUAGE}\".", + "lapNumberText": "Varv ${CURRENT}/${TOTAL}", + "lastGamesText": "(sista ${COUNT} spelen)", + "leaderboardsText": "Ledartavlor", + "league": { + "allTimeText": "All Time", + "currentSeasonText": "Nuvarande säsong (${NUMBER})", + "leagueFullText": "${NAME} League", + "leagueRankText": "League Rank", + "leagueText": "League", + "rankInLeagueText": "#${RANK}, ${NAME} League${SUFFIX}", + "seasonEndedDaysAgoText": "Säsong slutade ${NUMBER} dagar sedan.", + "seasonEndsDaysText": "Säsong slutar i ${NUMBER} dagar.", + "seasonEndsHoursText": "Säsong slutar i ${NUMBER} timmar.", + "seasonEndsMinutesText": "Säsongen slutar i ${NUMBER} minuter.", + "seasonText": "Säsong ${NUMBER}", + "tournamentLeagueText": "Du måste nå ${NAME} ligan att gå in här turneringen.", + "trophyCountsResetText": "Trophy räknas återställs nästa säsong." + }, + "levelFastestTimesText": "Bästa tiderna på ${LEVEL}", + "levelHighestScoresText": "Högsta poäng på ${LEVEL}", + "levelIsLockedText": "${LEVEL} är låst.", + "levelMustBeCompletedFirstText": "${LEVEL} måste klaras av först.", + "levelText": "Nivå ${NUMBER}", + "levelUnlockedText": "Nivå upplåst!", + "livesBonusText": "Livbonus", + "loadingText": "laddar", + "mainMenu": { + "creditsText": "Eftertexter", + "demoMenuText": "Demo Meny", + "endGameText": "Avbryt Spel", + "exitGameText": "Avsluta Spel", + "exitToMenuText": "Återgå till menyn?", + "howToPlayText": "Hur man Spelar", + "justPlayerText": "(Endast ${NAME})", + "leaveGameText": "Lämna Spel", + "leavePartyConfirmText": "Vill du verkligen lämna sällskapet?", + "leavePartyText": "Lämna Sällskap", + "leaveText": "Lämna", + "quitText": "Avsluta", + "resumeText": "Återuppta", + "settingsText": "Inställningar" + }, + "makeItSoText": "Kör hårt", + "mapSelectGetMoreMapsText": "Hämta Fler Banor...", + "mapSelectText": "Välj...", + "mapSelectTitleText": "${GAME} Kartor", + "mapText": "Karta", + "mostValuablePlayerText": "Mest värdefulla spelare", + "mostViolatedPlayerText": "Mest kränkt spelare", + "mostViolentPlayerText": "Mest våldsam spelare", + "moveText": "Flytta", + "multiKillText": "${COUNT}-DÖDA!", + "multiPlayerCountText": "${COUNT} spelare", + "mustInviteFriendsText": "Notera: du måste bjud in vänner i\n\"${GATHER}\" panelen eller ansluta\nstyrenheter för multiplayerspel.", + "nameBetrayedText": "${NAME} svek ${VICTIM}", + "nameDiedText": "${NAME} dog.", + "nameKilledText": "${NAME} dödade ${VICTIM}.", + "nameNotEmptyText": "Namn kan inte vara tomt!", + "nameScoresText": "${NAME} gör mål!", + "nameSuicideKidFriendlyText": "${NAME} dog oavsiktligt.", + "nameSuicideText": "${NAME} begick självmord.", + "nativeText": "Ursprunglig upplösning", + "newPersonalBestText": "Nytt personbästa!", + "newTestBuildAvailableText": "Ett nyare bygge är tillgängligt! (${VERSION} bygge ${BUILD}).\nHämta på ${ADDRESS}", + "newVersionAvailableText": "En nyare version av BombSquad är tillgänglig! (${VERSION})", + "nextLevelText": "Nästa Nivå", + "noAchievementsRemainingText": "- ingen", + "noContinuesText": "(nr fortsätter)", + "noExternalStorageErrorText": "Ingen extern lagring hittad på denna enhet", + "noGameCircleText": "Fel: inte inloggad i GameCircle", + "noJoinCoopMidwayText": "Kan inte ansluta i pågående co-op spel.", + "noProfilesErrorText": "Du har inga spelarprofiler, so du är fast med '${NAME}'.\nGå till Inställningar->Spelarprofiler för att skapa en profil.", + "noScoresYetText": "Inga poäng än.", + "noThanksText": "Nej Tack", + "noValidMapsErrorText": "Ingen giltig karta hittades för denna speltyp.", + "notEnoughPlayersRemainingText": "Otillräckligt antal spelare kvar; avsluta och starta nytt spel.", + "notEnoughPlayersText": "Du behöver minst ${COUNT} spelare för att starta spelet!", + "notNowText": "Inte Nu", + "notSignedInErrorText": "Du måste logga in för att kunna göra det här.", + "notSignedInGooglePlayErrorText": "Du måste logga in på Google Play för att kunna göra det här.", + "notSignedInText": "Inte inloggad", + "nothingIsSelectedErrorText": "Inget är valt!", + "numberText": "#${NUMBER}", + "offText": "Av", + "okText": "Ok", + "onText": "På", + "onslaughtRespawnText": "${PLAYER} återskapas i våg ${WAVE}", + "orText": "${A} eller ${B}", + "outOfText": "(#${RANK} av ${ALL})", + "ownFlagAtYourBaseWarning": "Din flagga måste vara i \ndin bas för poäng!", + "packageModsEnabledErrorText": "Nätverksspel är inte tillåtet medan lokala moddar är aktiverade (se Settings->Avancerat)", + "partyWindow": { + "chatMessageText": "Chat Meddelande", + "emptyText": "Ditt sällskap är tomt", + "hostText": "(värd)", + "sendText": "Skicka", + "titleText": "Ditt Sällskap" + }, + "pausedByHostText": "(pausad av värden)", + "perfectWaveText": "Perfekt Våg!", + "pickUpBoldText": "HÄMTA UPP", + "pickUpText": "Hämta Upp", + "playModes": { + "coopText": "Kooperativt", + "freeForAllText": "Alla mot alla", + "multiTeamText": "Flerlagsspel", + "singlePlayerCoopText": "Single Player / Co - op", + "teamsText": "Lagspel" + }, + "playText": "Spela", + "playWindow": { + "coopText": "Co-op", + "freeForAllText": "Fritt-för-alla", + "oneToFourPlayersText": "1-4 spelare", + "teamsText": "Lag", + "titleText": "Spela", + "twoToEightPlayersText": "2-8 spelare" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerDelayedJoinText": "${PLAYER} kommer att vara med nästa runda", + "playerInfoText": "Spelar info.", + "playerLeftText": "${PLAYER} lämnade spelet.", + "playerLimitReachedText": "Spelargräns på ${COUNT} uppnått; inga fler får ansluta.", + "playerLimitReachedUnlockProText": "Uppgradera till \"${PRO}\" i butiken för att spela med fler än ${COUNT} spelare.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Du kan inte ta bort din kontoprofil.", + "deleteButtonText": "Ta Bort\nProfil", + "deleteConfirmText": "Ta bort '${PROFILE}'?", + "editButtonText": "Redigera\nProfil", + "explanationText": "(Skräddarsy namn och utseende för spelare på denna enhet)", + "newButtonText": "Ny\nProfil", + "titleText": "Spelarprofiler" + }, + "playerText": "Spelare", + "playlistNoValidGamesErrorText": "Denna spellista innehåller inga giltiga olåsta spel.", + "playlistNotFoundText": "spellista hittades inte", + "playlistsText": "Spellistor", + "pleaseRateText": "Om du gillar BombSquad, är vi tacksamma om du\ntar dig tid att ranka spelet eller skriva en recension. \nDetta ger värdefull feedback och hjälper framtida utveckling.\n\nTack på förhand!", + "pressAnyButtonPlayAgainText": "Tryck på valfri knapp för att spela igen...", + "pressAnyButtonText": "Tryck på valfri knapp för att fortsätta...", + "pressAnyButtonToJoinText": "tryck på valfri knapp för att ansluta...", + "pressAnyKeyButtonPlayAgainText": "Tryck på valfri knapp för att spela igen...", + "pressAnyKeyButtonText": "Tryck på valfri knapp för att fortsätta...", + "pressAnyKeyText": "Tryck på valfri knapp...", + "pressJumpToFlyText": "** Tryck upprepat på hoppa för att flyga **", + "pressPunchToJoinText": "tryck SLÅ för att ansluta...", + "pressToOverrideCharacterText": "tryck ${BUTTONS} för att åsidosätta din spelkaraktär", + "pressToSelectProfileText": "tryck ${BUTTONS} för att välja profil", + "pressToSelectTeamText": "tryck ${BUTTONS} för att välja lag", + "profileInfoText": "Skapa profiler för dig och dina vänner för att\nskräddarsy namn, spelkaraktärer och färger.", + "promoCodeWindow": { + "codeText": "Kod", + "codeTextDescription": "Kampanjkod", + "enterText": "Ange" + }, + "promoSubmitErrorText": "Kunde inte skicka in kampanjkod; kontrollera internetförbindelse", + "ps3ControllersWindow": { + "macInstructionsText": "Stäng av strömmen på baksidan av din PS3, säkerställ att \nBluetooth är påslaget på din Mac, anslut sedan din styrenhet \ntill din Mac via USB-kabel för att para ihop dem. Därefter kan du \nanvända din styrenhets hemknapp för att koppla till din Mac\ni antingen trådat (USB) eller trådlöst (Bluetooth) läge.\n\nPå några Macar kan du bli frågad om en passkod.\nOm detta händer, se följande handledning eller google för hjälp.\n\n\n\n\nPS3 styrenheter kopplade trådlöst bör visas i enhetslistan i \nSystem Preferences -> Bluetooth. Du kan behöva ta bort dem\nfrån listan för att använda dem med din PS3 igen.\n\nKoppla ned dem från Bluetooth när du inte använder dem för att \ninte batterierna skall ta slut.\n\nBluetooth bör hantera upp till sju anslutna enheter, men det kan \nvariera något.", + "ouyaInstructionsText": "För att använda PS3 styrenhet med din OUYA, koppla endast in USB kabeln.\nDetta kan få andra styrenheter att koppla ifrån. Då bör du starta om din OUYA\noch koppla ur USB kabeln.\n\nDärefter kan du använda hem knappen för att koppla upp trådlöst. När du \nspelat klart, håll inne hemknappen i 10 sekunder för att stänga av styrenheten.\nI annat fall kan batterierna snabbt ta slut.", + "pairingTutorialText": "handledningsvideo i parkoppling", + "titleText": "Använd PS3-styrenhet med BombSquad:" + }, + "publicBetaText": "PUBLIC BETA", + "punchBoldText": "SLÅ", + "punchText": "Slå", + "purchaseForText": "Köp för ${PRICE}", + "purchaseGameText": "Köp Spel", + "purchasingText": "Köper...", + "quitGameText": "Avsluta BombSquad?", + "quittingIn5SecondsText": "Avslutar om 5 sekunder...", + "randomText": "Slumpad", + "rankText": "Rang", + "ratingText": "Rating", + "reachWave2Text": "Nå våg 2 för att få rank.", + "readyText": "redo", + "recentText": "Senaste", + "remainingInTrialText": "återstår i trial", + "removeInGameAdsText": "Lås up \"${PRO}\" i butiken för att ta bort reklam.", + "renameText": "Döp Om", + "replayEndText": "Avsluta Repris", + "replayNameDefaultText": "Senaste matchens inspelning", + "replayReadErrorText": "Fel vid läsning av reprisfil.", + "replayRenameWarningText": "Döp om \"${REPLAY}\" efter ett spel om du vill behålla det; annars skrivs det över.", + "replayVersionErrorText": "Tyvärr, denna repris skapades med en annan\nversion av spelet och kan inte visas.", + "replayWatchText": "Se inspelning", + "replayWriteErrorText": "Fel vid skrivning av reprisfil.", + "replaysText": "Inspelade matcher", + "reportPlayerExplanationText": "Använd den här e-post adressen för att rapportera fusk, olämpligt språk \neller annat dåligt uppträdande. Vänligen beskriv nedan:", + "reportThisPlayerCheatingText": "Fusk", + "reportThisPlayerLanguageText": "Olämpligt språk", + "reportThisPlayerReasonText": "Vad vill du rapportera?", + "reportThisPlayerText": "Anmäl denna spelare", + "requestingText": "Begäran ...", + "restartText": "Starta om", + "retryText": "Försök Igen", + "revertText": "Återställ", + "runText": "Spring", + "saveText": "Spara", + "scoreChallengesText": "Poängutmaningar", + "scoreListUnavailableText": "Poänglista är otillgänglig", + "scoreText": "Poäng", + "scoreUnits": { + "millisecondsText": "Millisekunder", + "pointsText": "Poäng", + "secondsText": "Sekunder" + }, + "scoreWasText": "(var ${COUNT})", + "selectText": "Välj", + "seriesWinLine1PlayerText": "VINNER", + "seriesWinLine1TeamText": "VINNER", + "seriesWinLine1Text": "VINNER", + "seriesWinLine2Text": "SERIEN!", + "settingsWindow": { + "accountText": "Konto", + "advancedText": "Avancerat", + "audioText": "Ljud", + "controllersText": "Styrenheter", + "graphicsText": "Grafik", + "playerProfilesText": "Spelarprofiler", + "titleText": "Inställningar" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(ett enkelt styrenhetsvänlig skärmtangentbord för text redigering)", + "alwaysUseInternalKeyboardText": "Använd Alltid Inbyggda Tangetbordet", + "benchmarksText": "Benchmarks & Stress Tester", + "enablePackageModsDescriptionText": "(aktiverar moddningsmöjligheter men inaktiverar nätverksspel)", + "enablePackageModsText": "Aktivera Lokala Moddar", + "enterPromoCodeText": "Skriv in Promo Kod", + "forTestingText": "Notera: dessa värden är endast testvärden och återställs när programmet avslutas.", + "helpTranslateText": "BombSquad's språköversättningar skapas gemensamt i\nett community. Om du vill bidra eller rätta en översättning,\nfölj länken nedan. Tack på förhand!", + "kickIdlePlayersText": "Kasta ut inaktiva spelare", + "kidFriendlyModeText": "Barnvänligt Läge (mindre våld, etc)", + "languageText": "Språk", + "moddingGuideText": "Modifieringsguide", + "mustRestartText": "Du måste starta om för att ändringen ska träda i kraft.", + "netTestingText": "Nätverkstester", + "resetText": "Återställ", + "showPlayerNamesText": "Visa spelarnamn", + "showUserModsText": "Visa modifieringsmapp", + "titleText": "Avancerat", + "translationEditorButtonText": "BombSquad Översättningseditor", + "translationFetchErrorText": "översättnings status otillgängligt", + "translationFetchingStatusText": "Kontrollerar översättningsstatus...", + "translationNoUpdateNeededText": "valt språk är up to date; woohoo!", + "translationUpdateNeededText": "** valt språk behöver uppdateras!! **", + "vrTestingText": "VR Tester" + }, + "showText": "Visa", + "signInForPromoCodeText": "Du måste logga in på ett konto för att promotion koder ska aktiveras.", + "signInWithGameCenterText": "För att använda ett Game Center konto, \nlogga in via Game Center appen.", + "singleGamePlaylistNameText": "Endast ${GAME}", + "singlePlayerCountText": "1 spelare", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Val av spelarkaraktär", + "Chosen One": "Den Utvalde", + "Epic": "Episka Spel", + "Epic Race": "Episk kapplöpning", + "FlagCatcher": "Fånga Flaggan", + "Flying": "Glada Tankar", + "Football": "Fotboll", + "ForwardMarch": "Stormning", + "GrandRomp": "Erövring", + "Hockey": "Hockey", + "Keep Away": "Håll dig Borta", + "Marching": "Undanflykt", + "Menu": "Huvudmeny", + "Onslaught": "Anfall", + "Race": "Kapplöpning", + "Scary": "Härre på Täppan", + "Scores": "Poängskärm", + "Survival": "Eliminering", + "ToTheDeath": "Death Match", + "Victory": "Slutpoängsskärm" + }, + "spaceKeyText": "mellanslag", + "store": { + "alreadyOwnText": "Du äger redan ${NAME}!", + "bombSquadProDescriptionText": "Dubblar dina \"achievement ticket rewards\"\n\nTar bort \"in-game\" annonser\n\nInkluderar ${COUNT} bonusbiljetter\n\n+${PERCENT}% ligapoäng bonus\n\nLåser upp \"${INF_ONSLAUGHT}' och\n  \"${INF_RUNAROUND}' co-op nivåer", + "bombSquadProFeaturesText": "Funktionalitet:", + "bombSquadProNameText": "BombSquad Pro", + "buyText": "Köp", + "charactersText": "Karaktärer", + "comingSoonText": "Kommer snart...", + "extrasText": "Extra", + "freeBombSquadProText": "BombSquad är nu gratis, men eftersom du tidigare köpt spelet får du\nBombSquad Pro uppgraderingen och ${COUNT} värdekuponger som tack.\nHoppas du gläds åt all ny funktionalitet, och tack för ditt stöd!\n-Eric", + "gameUpgradesText": "Speluppgraderingar", + "getCoinsText": "Få Mynt", + "holidaySpecialText": "Helgspecial", + "howToSwitchCharactersText": "(Gå till \"${SETTINGS} -> ${PLAYER_PROFILES}\" för att tilldela och anpassa tecken)", + "howToUseIconsText": "(skapa globala spelarprofiler i \"${SETTINGS} -> ${PLAYER_PROFILES}\" för att använda dessa)", + "howToUseMapsText": "(använd dessa kartor i dina egna spellistor för \"lag/alla-mot-alla\")", + "iconsText": "Ikoner", + "loadErrorText": "Kunde inte ladda sida.\nVänligen kontrollera internetförbindelse.", + "loadingText": "Laddar", + "mapsText": "Kartor", + "miniGamesText": "Minispel", + "oneTimeOnlyText": "(endast en gång)", + "purchaseAlreadyInProgressText": "Ett köp av den här produkten är redan påbörjat.", + "purchaseConfirmText": "Bara ${ITEM}?", + "purchaseNotValidError": "Köpet är inte giltigt.\nKontakta ${EMAIL} om du ser detta felmeddelande.", + "purchaseText": "bara", + "saleBundleText": "Paket- försäljning!", + "saleExclaimText": "Rea!", + "salePercentText": "(${PERCENT}% off)", + "saleText": "REA", + "searchText": "Sök", + "teamsFreeForAllGamesText": "Lag / \"Alla mot alla\" Spel", + "winterSpecialText": "Vinterspecial", + "youOwnThisText": "- du äger detta -" + }, + "storeDescriptionText": "Partyspelsgalenskap för åtta spelare!\n\nSpräng dina vänner (eller datorn) i en turnering av exposiva minispel så som Fånga flaggan, Bombarhockey och Episk-Slowmotion-Death-Match!\n\nEnkla kontroller och omfattande stöd för styrenheter gör det enkelt för upp till åtta personer att ta del av all action. Du kan till och med använda dina mobila enheter som styrenheter \nvia gratisappen 'BombSquad Remote' app!\n\nLåt bomberna regna!\n\nKolla in www.froemling.net/bombsquad för mer info.", + "storeDescriptions": { + "blowUpYourFriendsText": "Spräng dina fiender", + "competeInMiniGamesText": "Tävla i minispel - allt från kapplöpning till flygning.", + "customize2Text": "Skräddarsy spelkaraktärer, minispel och soundtracket.", + "customizeText": "Skräddarsy spelkaraktärer och skapa din egen minispel spellista.", + "sportsMoreFunText": "Sport är alltid roligare med sprängdeg.", + "teamUpAgainstComputerText": "Samarbeta mot datorn" + }, + "storeText": "Affär", + "submittingPromoCodeText": "Laddar promotion kod....", + "teamsText": "Lag", + "telnetAccessGrantedText": "Telnetaccess aktiverad", + "telnetAccessText": "Telnetaccess upptäckt; tillåt?", + "testBuildErrorText": "Denna testversion är inte längre aktivt; vänligen kolla om det finns en ny version.", + "testBuildText": "Testversion", + "testBuildValidateErrorText": "Kunde inte validera testversionen. (ingen internet anslutning?)", + "testBuildValidatedText": "Testversion Validerad; Ha Det Så Kul!", + "thankYouText": "Tack för ditt stöd! Ha det så kul!!", + "threeKillText": "TRE DÖDA!", + "timeBonusText": "Tidbonus", + "timeElapsedText": "Förfluten tid", + "timeExpiredText": "Tiden är slut", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}t", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Tips", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Bästa Vänner", + "tournamentCheckingStateText": "Kontrollerar status på turnering; vänligen vänta...", + "tournamentEndedText": "Denna turnering är avslutad. En ny börjar inom kort.", + "tournamentEntryText": "Turneringsavgift", + "tournamentResultsRecentText": "Senaste turneringsresultat", + "tournamentStandingsText": "Turneringsställningar", + "tournamentText": "Turnering", + "tournamentTimeExpiredText": "Turneringstiden Är Slut", + "tournamentsText": "Turneringar", + "translations": { + "characterNames": { + "Bernard": "Bernard", + "Bones": "Bones", + "Butch": "Butch", + "Easter Bunny": "Påskharen", + "Gretel": "Greta", + "Pixel": "Pixel", + "Santa Claus": "Jultomten", + "Snake Shadow": "Ormskugga", + "Zoe": "Zoe" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Övning", + "Infinite ${GAME}": "Oändlig ${GAME}", + "Infinite Onslaught": "Oändligt Anfall", + "Infinite Runaround": "Oändlig Undanflykt", + "Onslaught Training": "Anfallsträning", + "Pro ${GAME}": "Proffs ${GAME}", + "Pro Football": "Proffsfotboll", + "Pro Onslaught": "Proffsanfall", + "Pro Runaround": "Proffsundanflykt", + "Rookie ${GAME}": "Nybörjar ${GAME}", + "Rookie Football": "Nybörjarfotboll", + "Rookie Onslaught": "Nybörjaranfall", + "The Last Stand": "Sista utposten", + "Uber ${GAME}": "Über ${GAME}", + "Uber Football": "Überfotboll", + "Uber Onslaught": "Überanfall", + "Uber Runaround": "Überundanflykt" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Var den utvalde under en tid för att vinna..\nDöda den utvalde för att själv bli utvald.", + "Bomb as many targets as you can.": "Bomba så många mål som möjligt.", + "Bomb as many targets as you can.\n": "Bomba så många mål som möjligt.", + "Carry the flag for ${ARG1} seconds.": "Bär flaggan i ${ARG1} sekunder.", + "Carry the flag for a set length of time.": "Bär flaggan i en viss tid.", + "Crush ${ARG1} of your enemies.": "Krossa ${ARG1} av dina fiender.", + "Defeat all enemies.": "Besegra alla fiender.", + "Dodge the falling bombs.": "Undvik de fallande bomberna.", + "Final glorious epic slow motion battle to the death.": "Slutlig ärofull episk slow motion dödsstrid.", + "Gather eggs!": "Samla ägg!", + "Get the flag to the enemy end zone.": "Få flaggan till fiendens slutzon.", + "How fast can you defeat the ninjas?": "Hur fort kan du besegra ninjorna?", + "Kill a set number of enemies to win.": "Döda ett visst antal fiender för att vinna", + "Last one standing wins.": "Den sista som står upp vinner.", + "Last remaining alive wins.": "Den sista vid liv vinner.", + "Last team standing wins.": "Sista laget som står upp vinner.", + "Prevent enemies from reaching the exit.": "Hindra fienden från att nå utgången.", + "Reach the enemy flag to score.": "Nå fiendens flagga för att få poäng.", + "Return the enemy flag to score.": "Lämna fiendens flagga för att få poäng.", + "Run ${ARG1} laps.": "Spring ${ARG1} varv.", + "Run ${ARG1} laps. Your entire team has to finish.": "Spring ${ARG1} varv. Hela laget måste komma i mål.", + "Run 1 lap.": "Spring ett varv.", + "Run 1 lap. Your entire team has to finish.": "Spring ett varv. Hela laget måste komma i mål.", + "Run real fast!": "Spring riktigt fort!", + "Score ${ARG1} goals.": "Gör ${ARG1} mål.", + "Score ${ARG1} touchdowns.": "Gör ${ARG1} touchdowns.", + "Score a goal.": "Gör ett mål", + "Score a touchdown.": "Gör en touchdown.", + "Score some goals.": "Gör några mål", + "Secure all ${ARG1} flags.": "Säkra alla ${ARG1} flaggor.", + "Secure all flags on the map to win.": "Säkra alla flaggor på kartan för att vinna.", + "Secure the flag for ${ARG1} seconds.": "Säkra flaggan i ${ARG1} sekunder.", + "Secure the flag for a set length of time.": "Säkra flaggan under en viss tid.", + "Steal the enemy flag ${ARG1} times.": "Stjäl fiendens flagga ${ARG1} gånger", + "Steal the enemy flag.": "Stjäl fiendens flagga.", + "There can be only one.": "Det kan endast vara en.", + "Touch the enemy flag ${ARG1} times.": "Rör vid fiendens flagga ${ARG1} gånger", + "Touch the enemy flag.": "Vidrör fiendens flagga.", + "carry the flag for ${ARG1} seconds": "bär flaggen i ${ARG1} sekunder", + "kill ${ARG1} enemies": "Döda ${ARG1} fiender", + "last one standing wins": "den sista som står upp vinner", + "last team standing wins": "Sista laget som står upp vinner", + "return ${ARG1} flags": "lämna ${ARG1} flaggor", + "return 1 flag": "lämna 1 flagga", + "run ${ARG1} laps": "spring ${ARG1} varv", + "run 1 lap": "spring ett varv", + "score ${ARG1} goals": "gör ${ARG1} mål", + "score ${ARG1} touchdowns": "gör ${ARG1} touchdowns", + "score a goal": "Gör ett mål", + "score a touchdown": "Gör en touchdown", + "secure all ${ARG1} flags": "säkra alla ${ARG1} flaggor", + "secure the flag for ${ARG1} seconds": "säkra flaggan i ${ARG1} sekunder", + "touch ${ARG1} flags": "rör vid ${ARG1} flaggor", + "touch 1 flag": "rör en flagga" + }, + "gameNames": { + "Assault": "Angrepp", + "Capture the Flag": "Fånga Flaggan", + "Chosen One": "Den Utvalda", + "Conquest": "Erövring", + "Death Match": "Dödsmatch", + "Easter Egg Hunt": "Påskäggsjakt", + "Elimination": "Eliminering", + "Football": "Fotboll", + "Hockey": "Hockey", + "Keep Away": "Håll Avstånd", + "King of the Hill": "Herre på Täppan", + "Meteor Shower": "Meteorregn", + "Ninja Fight": "Ninja Fight", + "Onslaught": "Anfall", + "Race": "Kapplöpning", + "Runaround": "Undanflykt", + "Target Practice": "Träffövning", + "The Last Stand": "Sista utposten" + }, + "inputDeviceNames": { + "Keyboard": "Tangentbord", + "Keyboard P2": "Tangentbord P2" + }, + "languages": { + "Arabic": "Arabiska", + "Belarussian": "Vitryska", + "Chinese": "Kinesiska", + "Croatian": "Kroatiska", + "Czech": "Tjeckiska", + "Danish": "Danska", + "Dutch": "Holländska", + "English": "Engelska", + "Esperanto": "Esperanto", + "Finnish": "Finska", + "French": "Franska", + "German": "Tyska", + "Gibberish": "Gibberish", + "Hindi": "Hindi", + "Hungarian": "Ungerska", + "Italian": "Italienska", + "Japanese": "Japanska", + "Korean": "Koreanska", + "Persian": "Persiska", + "Polish": "Polska", + "Portuguese": "Portugisiska", + "Romanian": "Rumänska", + "Russian": "Ryska", + "Serbian": "Serbiska", + "Spanish": "Spanska", + "Swedish": "Svenska", + "Turkish": "Turkiska", + "Ukrainian": "Ukrainska" + }, + "leagueNames": { + "Bronze": "Brons", + "Diamond": "Diamant", + "Gold": "Guld", + "Silver": "Silver" + }, + "mapsNames": { + "Big G": "Big G", + "Bridgit": "Bridgit", + "Courtyard": "Courtyard", + "Crag Castle": "Crag Castle", + "Doom Shroom": "Doom Shroom", + "Football Stadium": "Footballs Stadium", + "Happy Thoughts": "Glada tankar", + "Hockey Stadium": "Hockey Stadium", + "Lake Frigid": "Lake Frigid", + "Monkey Face": "Apansikte", + "Rampage": "Framfart", + "Roundabout": "Rondell", + "Step Right Up": "Stig upp", + "The Pad": "The Pad", + "Tip Top": "Tip Top", + "Tower D": "Tower D", + "Zigzag": "Zigzag" + }, + "playlistNames": { + "Just Epic": "Endast Episkt", + "Just Sports": "Endast Sport" + }, + "promoCodeResponses": { + "invalid promo code": "Ogiltig kampanjkod" + }, + "scoreNames": { + "Flags": "Flaggor", + "Goals": "Mål", + "Score": "Poäng", + "Survived": "Överlevde", + "Time": "Tid", + "Time Held": "Tid avklarad" + }, + "serverResponses": { + "A code has already been used on this account.": "En kod har redan använts på det här kontot.", + "An error has occurred; (${ERROR})": "Ett fel har uppstått; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Ett fel har uppstått, vänligen kontakta kundtjänst. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Ett fel har uppstått, vänligen kontakta support@froemling.net.", + "An error has occurred; please try again later.": "Ett fel har uppstått, försök igen senare.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Är du säker på att du vill slå ihop dessa konton?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nDetta kan inte ångras!", + "BombSquad Pro unlocked!": "BombSquad Pro upplåst!", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Fusk upptäckt; resultat och priser kommer att frysas i ${COUNT} dagar", + "Daily maximum reached.": "Dagligt maximum nått", + "Entering tournament...": "Ansluter till turnering...", + "Invalid code.": "Ogiltig kod", + "Invalid payment; purchase canceled.": "Ogiltig betalning, köp avbrutet.", + "Invalid promo code.": "Ogiltig rabbattkod.", + "Invalid purchase.": "Ogiltigt köp.", + "Max number of playlists reached.": "Max antal spellistor nåtts.", + "Max number of profiles reached.": "Max antal profiler nåtts.", + "Profile \"${NAME}\" upgraded successfully.": "Profilen \"${NAME}\" har uppgraderats.", + "Profile could not be upgraded.": "Profilen kunde tyvärr inte uppgraderas.", + "Purchase successful!": "Köp genomfört!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Mottagna ${COUNT} biljetter till att logga in.\nKom tillbaka i morgon för att få ${TOMORROW_COUNT}.", + "Sorry, there are no uses remaining on this code.": "Tyvärr, det finns inga gånger kvar med den här koden.", + "Sorry, this code has already been used.": "Tyvärr, den här koden har redan använts.", + "Sorry, this code has expired.": "Tyvärr, den här koden har gått ut.", + "Sorry, this code only works for new accounts.": "Tyvärr, den här koden fungerar bara för nya konton.", + "Temporarily unavailable; please try again later.": "Tillfälligt otillgängligt, försök igen senare.", + "The tournament ended before you finished.": "Turneringen slutade innan ditt resultat togs emot.", + "This code cannot be used on the account that created it.": "Den här koden kan inte användas i samma konto den skapades i.", + "This requires version ${VERSION} or newer.": "Detta kräver version ${VERSION} eller senare.", + "Tournaments disabled due to rooted device.": "Turnering avbruten på grund av rotad enhet.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Vill du slå ihop ditt enhets konto med detta?\n\nDitt enhets konto är ${ACCOUNT1} \nOch det här kontot är ${ACCOUNT2}\n\nDet här gör att du kan behålla dina framsteg. \nOBS! Detta kan inte ångras!", + "You already own this!": "Du äger redan detta!", + "You don't have enough tickets for this!": "Du har inte tillräckligt med värdekuponger!", + "You got ${COUNT} tickets!": "Du fick ${COUNT} värdekuponger!", + "You got a ${ITEM}!": "Du fick en ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Du har blivit befordrad till en ny liga; grattis!", + "You must update to a newer version of the app to do this.": "Du måste uppdatera till en nyare version av appen för att göra detta.", + "You must update to the newest version of the game to do this.": "Du måste uppdatera till den nyaste versionen av appen för att göra detta.", + "You must wait a few seconds before entering a new code.": "Du måste vänta några sekunder innan du anger en ny kod.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Du rankades #${RANK} i senaste turneringen. Tack för ditt deltagande!", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Din kopia av spelet har modifierats.\nÅterställ förändringarna och försök igen.", + "Your friend code was used by ${ACCOUNT}": "Din kompis-kod användes av ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Minut", + "1 Second": "1 Sekund", + "10 Minutes": "10 Minuter", + "2 Minutes": "2 Minuter", + "2 Seconds": "2 Sekunder", + "20 Minutes": "20 Minuter", + "4 Seconds": "4 Sekunder", + "5 Minutes": "5 Minuter", + "8 Seconds": "8 Sekunder", + "Allow Negative Scores": "Tillåt Negativa Resultat", + "Balance Total Lives": "Balansera totala antalet liv", + "Chosen One Gets Gloves": "Den Utvalda Får Boxningshandskar", + "Chosen One Gets Shield": "Den Utvalda Får Sköld", + "Chosen One Time": "Den Utvalde Tid", + "Enable Impact Bombs": "Aktivera Impact bomber", + "Enable Triple Bombs": "Aktivera Triple bomber", + "Epic Mode": "Episkt Läge", + "Flag Idle Return Time": "Tid innan inaktiv flagga återlämnas", + "Flag Touch Return Time": "Tid innan vidrörd flagga återlämnas", + "Hold Time": "Uthållen tid", + "Kills to Win Per Player": "Antalet döda för att vinna per spelare", + "Laps": "Varv", + "Lives Per Player": "Liv Per Spelare", + "Long": "Lång", + "Longer": "Längre", + "Mine Spawning": "Återkommande minor", + "No Mines": "Inga minor", + "None": "Ingen", + "Normal": "Normal", + "Respawn Times": "Återfödelsetid", + "Score to Win": "Poäng för att vinna", + "Short": "Kort", + "Shorter": "Kortare", + "Solo Mode": "Sololäge", + "Target Count": "Målsumma", + "Time Limit": "Tidsgräns" + }, + "statements": { + "Killing ${NAME} for skipping part of the track!": "Dödar ${NAME} för att hen tog en genväg!" + }, + "teamNames": { + "Bad Guys": "De Onda", + "Blue": "Blå", + "Good Guys": "De Goda", + "Red": "Röd" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "En perfekt timad spring-hopp-spin-slag kan döda i ett enda slag\noch ge dig livslång respekt av dina vänner.", + "Always remember to floss.": "Glöm inte att använda tandtråd.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Skapa spelarprofiler till dig själv och dina vänner med \nförvalda namn och utseenden istället för slumpvalda.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Förbannelse-lådan förvandlar dig till en tickande bomb.\nDet enda botemedlet är att få tag i en hälsolåda.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Oavsett utseendet är alla karaktärers förmågor identiska,\nså välj vilken som än liknar dig mest.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Bli inte för kaxig med den där energiskölden, du kan fortfarande trilla ned för ett stup.", + "Don't run all the time. Really. You will fall off cliffs.": "Spring inte hela tiden. Seriöst. Du kommer att ramla av ett stup.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Håll inne valfri knapp för att springa. (Avtryckarknappar fungerar bra om du har dessa)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Håll ned valfri knapp för att springa. Du tar dig snabbare fram \nmen har svårare att svänga, så se upp för stup.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Isbomber är inte särskilt kraftfulla, men de fryser den som\nträffas och lämnar dem sårbara för förödelse.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Om någon plockar upp dig, slå dem så släpper de taget.\nDetta fungerar i verkliga livet också.", + "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Om du inte har tillräckligt med kontroller kan du installera 'BombSquad Remote' appen\npå din iOS eller Android enhet och använd dem som kontroller.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Om en klisterbomb fastnar på dig, hoppa runt i cirklar.\nDu kanske kan skaka av dig bomben, om inte annat är det kul.", + "If you kill an enemy in one hit you get double points for it.": "Om du dödar en fiende med en träff får du dubbla poäng för det.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Om du tar upp en förbannelse, är ditt enda hopp för att överleva\natt du plockar upp en hälso-powerup inom de närmsta sekunderna.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Om du står på ett ställe är du körd. Spring runt och hoppa undan för att överleva..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Om det är många spelare som ansluter och slutar, ställ in 'kasta ut inaktiva spelare'\nunder inställningar ifall någon glömmer att lämna spelet.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Om din enhet blir för varm eller om du vill spara på batteri,\nvrid ner \"Visuellt\" eller \"Upplösning\" under Inställningar->Grafik", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Om din framerate hoppar, försök sänka upplösningen\neller grafiken i spelet grafikinställningar.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "I Fånga Flaggan måste din egen flagga vara vid basen för att kunna göra poäng, om \nmotståndarna är påväg att göra mål kan ett bra sätt att stoppa dem vara att ta deras flagga.", + "In hockey, you'll maintain more speed if you turn gradually.": "I hockey bibehåller du mer fart om du svänger sakta.", + "It's easier to win with a friend or two helping.": "Det är lättare att vinna med en vän eller två som hjälper till.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Hoppa precis när du kastar en bomb för att få upp den på högre platser.", + "Land-mines are a good way to stop speedy enemies.": "Landminor är ett bra sätt att stoppa snabba fiender.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Många saker kan plockas upp och kastas, inklusive andra spelare. Att kasta dina \nfiender över ett stup kan vara en effektiv och emotionellt tillfredställande strategi.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nej, du kan inte komma upp på avsatsen. Du måste kasta bomber.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Spelare kan ansluta eller lämna mitt i de flesta speltyperna,\noch man kan även koppla i eller ur styrenheter närsomhelst.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug gamepads on the fly.": "Spelare kan ansluta och lämna mitt i de flesta spel\noch du kan koppla in och ur gamepads under spel.", + "Powerups only have time limits in co-op games.\nIn teams and free-for-all they're yours until you die.": "Powerups har endast tidsbegränsning i co-op spel.\nI lag och free-for-all behålls de tills du dör.", + "Practice using your momentum to throw bombs more accurately.": "Öva använda momentum för att kasta bomber mer träffsäkert.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Slag gör mer skada ju snabbare nävarna rör sig,\nså försök springa, hoppa och snurra som en galning.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Spring fram och tillbak innan du kastar en bomb\nför att 'piska' den och kasta längre.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Slå ut en grupp fiender genom att\nspränga en bomb nära en TNT låda.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Huvudet är det mest känsliga området, så en klisterbomb\nmot skallen betyder oftast Game-Over.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Denna nivå tar aldrig slut, men ett high score här\nger dig evig respekt över hela världen.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Kaststryka baseras på den riktning du trycker.\nFör att humpa någonting kort framför dig,\ntryck inte i någon riktning.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Trött på soundtracket? Ersätt den med din egna!\nSe Inställningar->Audio->Soundtrack", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Försök 'bränna av' bombens stubin i några sekunder innan du kastar dem.", + "Try tricking enemies into killing eachother or running off cliffs.": "Försök få fiender att döda varandra eller springa ut ur banan", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Använd plocka-upp knappen för att ta tag i flaggan < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Piska fram och tillbak för att få mer distans i dina kast..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Du kan 'sikta' dina slag genom att snurra till vänster och höger.\nDetta är användbart för att slå ut fiender över kanten eller för att göra mål i hockey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Du kan avgöra när en bomb kommer smälla beroende på\nfärgen på stubintråden: gul..orange..röd..BOOM.", + "You can throw bombs higher if you jump just before throwing.": "Du kan kasta bomber högre om du hoppar precis innan du kastar.", + "You don't need to edit your profile to change characters; Just press the top\nbutton (pick-up) when joining a game to override your default.": "Du behöver inte redigera din profil för att byta spelkaraktär. Tryck bara på \növersta knappen (plocka upp) när du ansluter till ett spel för att byta ut standardinställningen.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Du tar skada när du slår huvudet i saker, \nså försök att inte slå huvudet i saker.", + "Your punches do much more damage if you are running or spinning.": "Dina slag gör mycket mer skada om du springer eller snurrar." + } + }, + "trialPeriodEndedText": "Din testperiod har tagit slut. Skulle du vilja köpa BombSquad och fortsätta spela?", + "trophiesText": "Troféer", + "trophiesThisSeasonText": "Denna säsongs troféer", + "tutorial": { + "cpuBenchmarkText": "Kör handledning vid galen hastighet (främst för att testa processorns hastighet)", + "phrase01Text": "Hejsan!", + "phrase02Text": "Välkommen till BombSquad!", + "phrase03Text": "Här kommer några tips för att kontrollera din spelkaraktär:", + "phrase04Text": "Mycket i ${APP_NAME} är FYSIK baserade.", + "phrase05Text": "Till exempel, när du slår,..", + "phrase06Text": "..skada baseras på hastigheten på dina nävar.", + "phrase07Text": "Ser du? Vi stod nästan stilla, och gjorde knappt illa ${NAME}.", + "phrase08Text": "Låt oss hoppa och snurra för att få mer fart.", + "phrase09Text": "Aha, det var bättre.", + "phrase10Text": "Att springa hjälper också.", + "phrase11Text": "Håll valfri knapp intryckt för att springa.", + "phrase12Text": "För extra feta slag, försök springa OCH snurra.", + "phrase13Text": "Hoppsan, ledsen för det där ${NAME}.", + "phrase14Text": "Du kan plocka upp och kasta saker såsom flaggor.. eller ${NAME}.", + "phrase15Text": "Slutligen finns det bomber.", + "phrase16Text": "Att kasta bomber kräver övning.", + "phrase17Text": "Attans! Ett mindre lyckat kast.", + "phrase18Text": "Att röra sig hjälper till att kasta längre.", + "phrase19Text": "Kasta högre genom att hoppa.", + "phrase20Text": "Piska iväg dina bomber för ännu längre kast.", + "phrase21Text": "Timing av bomber kan vara svårt.", + "phrase22Text": "Dang.", + "phrase23Text": "Försök låta stubinen brinna en sekund eller två.", + "phrase24Text": "Hurra! Snyggt!", + "phrase25Text": "Det var det.", + "phrase26Text": "På dem, tiger!", + "phrase27Text": "Kom ihåg vad du lärt dig så kommer du att överleva!", + "phrase28Text": "...hmm, kanske...", + "phrase29Text": "Lycka till!", + "randomName1Text": "Marina", + "randomName2Text": "Johan", + "randomName3Text": "Kalle", + "randomName4Text": "Nisse", + "randomName5Text": "Anna", + "skipConfirmText": "Hoppa över handledningen? Tryck igen för att bekräfta.", + "skipVoteCountText": "${COUNT}/${TOTAL} röster att hoppa över", + "skippingText": "Hoppar över tutorial...", + "toSkipPressAnythingText": "(tryck på någon knapp för att hoppa över tutorial)" + }, + "twoKillText": "DOUBLE KILL!", + "unavailableText": "Otillgänglig", + "unconfiguredControllerDetectedText": "Okonfigurerad styrenhet upptäckt:", + "unlockThisInTheStoreText": "Detta måste låsas up i butiken.", + "unlockThisText": "För att låsa upp det här behöver du:", + "unsupportedHardwareText": "Tyvärr, hårdvaran stöds inte av detta bygge av spelet.", + "upFirstText": "Först på tur:", + "upNextText": "Nästa på tur i spel ${COUNT}:", + "updatingAccountText": "Uppdaterar ditt konto...", + "upgradeText": "Uppgradera", + "upgradeToPlayText": "Lås \"${PRO}\" i butiken i spelet för att spela detta.", + "useDefaultText": "Använd Standard", + "usesExternalControllerText": "Detta spel använder en extern styrenhet för inmatning.", + "usingItunesText": "Använder iTunes till musik...", + "usingItunesTurnRepeatAndShuffleOnText": "Säkerställ att 'shuffle' är ON och 'repeat' är ALL i iTunes.", + "validatingBetaText": "Bekräftar Beta...", + "validatingTestBuildText": "Validerar Testversionen...", + "victoryText": "Vinst", + "vsText": "vs.", + "waitingForHostText": "(väntar på ${HOST} för att fortsätta)", + "waitingForLocalPlayersText": "väntar på lokala spelare...", + "waitingForPlayersText": "väntar på att spelare ska ansluta...", + "watchAnAdText": "Se en annons", + "watchWindow": { + "deleteConfirmText": "Ta bort \"${REPLAY}\"?", + "deleteReplayButtonText": "Radera\nRepris", + "myReplaysText": "Mina Repriser", + "noReplaySelectedErrorText": "Ingen Repris Markerad", + "renameReplayButtonText": "Döp Om\nRepris", + "renameReplayText": "Döp om från \"${REPLAY}\" till:", + "renameText": "Döp Om", + "replayDeleteErrorText": "Kunde inte radera repris.", + "replayNameText": "Repris Namn", + "replayRenameErrorAlreadyExistsText": "En repris med samma namn finns redan.", + "replayRenameErrorInvalidName": "Kan inte byta namn på repris; ogiltigt namn.", + "replayRenameErrorText": "Fel vid namnändring av repris.", + "sharedReplaysText": "Delade repriser", + "titleText": "Titta", + "watchReplayButtonText": "Se\nRepris" + }, + "waveText": "Våg", + "wellSureText": "Jajamän!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Copyright" + }, + "wiimoteListenWindow": { + "listeningText": "Lyssnar efter Wiimotes...", + "pressText": "Tryck på knapp 1 och 2 samtidigt på din Wiimote.", + "pressText2": "På nyare Wiimotes med Motion Plus inbyggt, tryck på den lilla röda syncknappen på baksidan istället." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote Upphovsrätt", + "listenText": "Lyssna", + "macInstructionsText": "Se till att din Wii är avstäng och Bluetooth är aktiverad \npå din Mac. Tryck sedan på 'Lyssna'. Wiimote support kan \nvara ganska krånglig så du kan behöva försöka ett par gånger\ninnan du får en anslutning.\n\nBluetooth ska kunna hantera upp till 7 anslutna enheter,\nmen det kan variera.\n\nBombSquad stödjer äkta Wiimote, Nunchuks \noch den Klassiska Kontrollen.\nDen nya Wii Remote Plus fungerar nu också\nmen inte med tillbehör.", + "thanksText": "Tack till DarwiinRemote team\nFör att ha gjort detta möjligt.", + "titleText": "Wiimote Inställningar" + }, + "winsPlayerText": "Vinnare är ${NAME}!", + "winsTeamText": "${NAME} Vinner!", + "winsText": "${NAME} Vinner!", + "worldScoresUnavailableText": "Världsmästarpoäng ej tillgängligt.", + "worldsBestScoresText": "Världsmästarpoäng", + "worldsBestTimesText": "Världsmästartider", + "xbox360ControllersWindow": { + "getDriverText": "Skaffa Drivrutin", + "macInstructions2Text": "För att använda kontrollen trådlöst behöver du en mottagare \nsom kommer med 'Xbox 360 kontrollen för Windows'\nEn mottagare gör det möjligt för up till 4 kontroller.\n\nViktigt: 3:e part mottagare fungerar ej med denna drivrutin;\nse till att det står 'Microsoft', inte 'XBOX 360' på mottagaren.\nMicrosoft säljer inte längre dessa separat, så du behöver köpa\nen kontroll eller söka efter en på Ebay.\n\nOm du finner detta användbart, överväg gärna att donera till\nutvecklaren av drivrutinen på hans sida.", + "macInstructionsText": "För att använda Xbox 360 kontroller behöver du\ninstallera Mac drivrutinen som är tillgänglig via\nlänken nedanför. Den fungerar både med och utan sladd", + "ouyaInstructionsText": "Allt du behöver göra för att använda trådade Xbox 360-kontroller är att \nkoppla in dem i USB-porten. Du kan använda en USB-hub \nför att koppla in fler kontroller.\n\nFör att använda trådlösa kontroller behöver du en trådlös mottagare, \ndessa ingår om man köper en \"Xbox 360 Wireless Controller for Windows\" \nmen säljs också separat. Varje mottagare kopplas in i en USB-port \noch möjliggör användandet av upp till fyra stycken trådlösa kontroller.", + "titleText": "Använd Xbox 360-kontroller med BombSquad:" + }, + "yesAllowText": "Ja, Tillåt!", + "yourBestScoresText": "Dina Bästa Poäng", + "yourBestTimesText": "Dina Bästa Tider" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/turkish.json b/dist/ba_data/data/languages/turkish.json new file mode 100644 index 0000000..34c8f5c --- /dev/null +++ b/dist/ba_data/data/languages/turkish.json @@ -0,0 +1,1849 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Hesap isimleri emoji veya diğer özel karakterler içeremez", + "accountProfileText": "(hesap profili) ", + "accountsText": "Hesaplar", + "achievementProgressText": "Başarılar: ${COUNT} / ${TOTAL}", + "campaignProgressText": "Mücadele İlerlemesi [Zor]: ${PROGRESS}", + "changeOncePerSeason": "Bunu sezon başına sadece bir kere değiştirebilirsin", + "changeOncePerSeasonError": "Bunu tekrar değiştirmek için bir sonraki sezona kadar beklemelisin (${NUM}gün)", + "customName": "Özel isim", + "linkAccountsEnterCodeText": "Kod Gir", + "linkAccountsGenerateCodeText": "Kod Oluştur", + "linkAccountsInfoText": "(ilerlemeyi farklı platformlar ile paylaş)", + "linkAccountsInstructionsNewText": "İki hesabı bağlamak için, ilk hesapta kod oluştur\nve bu kodu ikinci hesaba gir. İkinci hesaptaki\nveriler iki hesap arasında paylaşılacaktır. \n(İlk hesaptaki veriler kaybolacaktır) \n\n${COUNT} tane hesap bağlayabilirsin. \n\nÖnemli:Sadece kendi hesaplarını bağla;\nEğer arkadaşlarının hesabını bağlarsan \naynı anda çevrimiçi oynayamayacaksınız.", + "linkAccountsInstructionsText": "İki hesabı birbirine bağlamak için, birinde kod oluşturun\nve o kodu diğerinde girin.\nİlerlemeler ve envanterler birleşecektir.\n${COUNT} taneye kadar hesap bağlayabilirsiniz.\n\nÖNEMLİ: Yalnızca kendi hesaplarını bağla!\nEğer arkadaşınla hesapları bağlarsan\naynı anda oynayamayacaksınız!\n\nAyrıca: bu işlem şu anda geri alınamaz, yani dikkatli ol!", + "linkAccountsText": "Hesapları Bağla", + "linkedAccountsText": "Bağlı Hesaplar:", + "nameChangeConfirm": "Hesap adını ${NAME} olarak değiştir?", + "resetProgressConfirmNoAchievementsText": "Bu co-op ilerlemeyi ve \nlokaldeki yüksek puanlarnı sıfırlar(ama biletlerini değil).\nBu geri alınamaz. Emin misiniz?", + "resetProgressConfirmText": "Bu co-op ilerlemeyi, başarıları ve \nEn yüksek skorları sıfırlayacak \n(ama biletlerini değil). Bu işlem\ngeri alınamaz. Emin misin?", + "resetProgressText": "İlerlemeyi Sıfırla", + "setAccountName": "Hesap adı düzenle", + "setAccountNameDesc": "Hesabın için gösterilecek bir isim seç. \nBağladığın hesaplardan birinin adını kullanabilir\nya da yeni bir tane oluşturabilirsin.", + "signInInfoText": "Bilet toplamak Çevrimiçi olmak ve ilerlemeyi \ndiğer cihazlarla paylaşmak için Giriş yap.", + "signInText": "Giriş Yap", + "signInWithDeviceInfoText": "Otomatik bir hesap sadece bu cihaz için kullanılabilir", + "signInWithDeviceText": "Cihaz hesabı ile Giriş yap", + "signInWithGameCircleText": "Game Circle ile Giriş yap", + "signInWithGooglePlayText": "Google Play ile Giriş yap", + "signInWithTestAccountInfoText": "(miras hesabı;bu cihaz hesaplarını ileride de kullan)", + "signInWithTestAccountText": "Test hesabı ile Giriş yap", + "signOutText": "Çıkış Yap", + "signingInText": "Giriş yapılıyor...", + "signingOutText": "Çıkış yapılıyor...", + "ticketsText": "Biletler: ${COUNT}", + "titleText": "Hesap", + "unlinkAccountsInstructionsText": "Ayırmak için bir hesap seç", + "unlinkAccountsText": "Hesapları ayır", + "viaAccount": "(${NAME}hesabı ile)", + "youAreSignedInAsText": "Aşağıdaki ile giriş yapıldı" + }, + "achievementChallengesText": "Başarı Mücadelesi", + "achievementText": "Başarı", + "achievements": { + "Boom Goes the Dynamite": { + "description": "TNT ile 3 haylaz öldür", + "descriptionComplete": "TNT ile 3 haylaz öldürüldü", + "descriptionFull": "${LEVEL} da TNT ile 3 haylaz öldür", + "descriptionFullComplete": "${LEVEL} da TNT ile 3 haylaz öldürüldü", + "name": "Dinamit Gümledi" + }, + "Boxer": { + "description": "Hiç bomba kullanmadan kazan", + "descriptionComplete": "Hiç bomba kullanmadan kazanıldı", + "descriptionFull": "Hiç bomba kullanmadan ${LEVEL} tamamla", + "descriptionFullComplete": "Hiç bomba kullanmadan ${LEVEL} tamamlandı", + "name": "Boksör" + }, + "Dual Wielding": { + "descriptionFull": "2 kontrolcü bağla (donanım veya uygulama)", + "descriptionFullComplete": "2 kontrolcü bağlandı (donanım veya uygulama)", + "name": "Çifte Kullanım" + }, + "Flawless Victory": { + "description": "Hiç darbe almadan kazan", + "descriptionComplete": "Hiç darbe almadan kazanıldı", + "descriptionFull": "Hiç darbe almadan ${LEVEL} kazan", + "descriptionFullComplete": "Hiç darbe almadan ${LEVEL} kazanıldı", + "name": "Çiziksiz Galibiyet" + }, + "Free Loader": { + "descriptionFull": "2+ oyuncu ile Herkes-Tek oyunu başlat", + "descriptionFullComplete": "2+ oyuncu ile Herkes-Tek oyunu başlatıldı", + "name": "Otlakçı" + }, + "Gold Miner": { + "description": "Kara mayını ile 6 haylaz öldür", + "descriptionComplete": "Kara mayını ile 6 haylaz öldürüldü", + "descriptionFull": "Kara mayını ile ${LEVEL} da 6 haylaz öldür", + "descriptionFullComplete": "Kara mayını ile ${LEVEL} da 6 haylaz öldürüldü", + "name": "Altın Mayıncı" + }, + "Got the Moves": { + "description": "Yumruk veya bomba kullanmadan kazan", + "descriptionComplete": "Yumruk veya bomba kullanmadan kazanıldı", + "descriptionFull": "Yumruk ve bomba kullanmadan ${LEVEL} da kazan", + "descriptionFullComplete": "Yumruk ve bomba kullanmadan ${LEVEL} da kazanıldı", + "name": "Hamleni Yap" + }, + "In Control": { + "descriptionFull": "Bir kontrolcü bağla (donanım veya uygulama)", + "descriptionFullComplete": "Bir kontrolcü bağlandı. (donanım veya uygulama)", + "name": "Kontrol Altında" + }, + "Last Stand God": { + "description": "1000 puan kazan", + "descriptionComplete": "1000 puan kazanıldı", + "descriptionFull": "${LEVEL} da 1000 puan kazan", + "descriptionFullComplete": "${LEVEL} da 1000 puan kazanıldı", + "name": "${LEVEL} Tanrısı" + }, + "Last Stand Master": { + "description": "250 puan kazan", + "descriptionComplete": "250 puan kazanıldı", + "descriptionFull": "${LEVEL} da 250 puan kazan", + "descriptionFullComplete": "${LEVEL} da 250 puan kazanıldı", + "name": "${LEVEL} Ustası" + }, + "Last Stand Wizard": { + "description": "500 puan kazan", + "descriptionComplete": "500 puan kazanıldı", + "descriptionFull": "${LEVEL} da 500 puan kazan", + "descriptionFullComplete": "${LEVEL} da 500 puan kazanıldı", + "name": "${LEVEL} Büyücüsü" + }, + "Mine Games": { + "description": "Kara mayını ile 3 haylaz öldür", + "descriptionComplete": "kara mayını ile 3 haylaz öldürüldü", + "descriptionFull": "kara mayını ile ${LEVEL} da 3 haylaz öldür", + "descriptionFullComplete": "kara mayını ile ${LEVEL} da 3 haylaz öldürüldü", + "name": "Mayın Oyunları" + }, + "Off You Go Then": { + "description": "Haritadan dışarıya 3 haylaz fırlat", + "descriptionComplete": "Haritadan dışarıya 3 haylaz fırlatıldı", + "descriptionFull": "${LEVEL} da haritadan dışarıya 3 haylaz fırlat", + "descriptionFullComplete": "${LEVEL} da haritadan dışarıya 3 haylaz fırlatıldı", + "name": "O Halde Bas Git!" + }, + "Onslaught God": { + "description": "5000 puan kazan", + "descriptionComplete": "5000 puan kazanıldı", + "descriptionFull": "${LEVEL} da 5000 puan kazan", + "descriptionFullComplete": "${LEVEL} da 5000 puan kazanıldı", + "name": "${LEVEL} Tanrısı" + }, + "Onslaught Master": { + "description": "500 puan kazan", + "descriptionComplete": "500 puan kazanıldı", + "descriptionFull": "${LEVEL} da 500 puan kazan", + "descriptionFullComplete": "${LEVEL} da 500 puan kazanıldı", + "name": "${LEVEL} Ustası" + }, + "Onslaught Training Victory": { + "description": "Tüm dalgaları kazan", + "descriptionComplete": "Tüm dalgalar kazanıldı", + "descriptionFull": "${LEVEL} da tüm dalgaları kazan", + "descriptionFullComplete": "${LEVEL} da tüm dalgalar kazanıldı", + "name": "${LEVEL} Galibiyeti" + }, + "Onslaught Wizard": { + "description": "1000 puan kazan", + "descriptionComplete": "1000 puan kazanıldı", + "descriptionFull": "${LEVEL} da 1000 puan kazan", + "descriptionFullComplete": "${LEVEL} da 1000 puan kazan", + "name": "${LEVEL} Büyücüsü" + }, + "Precision Bombing": { + "description": "Hiç güçlendirici kullanmadan kazan", + "descriptionComplete": "Hiç güçlendirici kullanmadan kazanıldı", + "descriptionFull": "Hiç güçlendirici kullanmadan ${LEVEL} da kazan", + "descriptionFullComplete": "Hiç güçlendirici kullanmadan ${LEVEL} da kazanıldı", + "name": "Hassas Bombalama" + }, + "Pro Boxer": { + "description": "Hiç bomba kullanmadan kazan", + "descriptionComplete": "Hiç bomba kullanmadan kazanıldı", + "descriptionFull": "Hiç bomba kullanmadan ${LEVEL} tamamla", + "descriptionFullComplete": "Hiç bomba kullanmadan ${LEVEL} tamamlandı", + "name": "Uzman Boksör" + }, + "Pro Football Shutout": { + "description": "Haylazlara puan yaptırmadan kazan", + "descriptionComplete": "Haylazlara puan yaptırmadan kazanıldı", + "descriptionFull": "Haylazlara puan yaptırmadan ${LEVEL} da kazan", + "descriptionFullComplete": "Haylazlara puan yaptırmadan ${LEVEL} da kazanıldı", + "name": "${LEVEL} Lokavt" + }, + "Pro Football Victory": { + "description": "Oyunu kazan", + "descriptionComplete": "Oyun kazanıldı", + "descriptionFull": "${LEVEL} da oyunu kazan", + "descriptionFullComplete": "${LEVEL} da oyun kazanıldı", + "name": "${LEVEL} Galibiyeti" + }, + "Pro Onslaught Victory": { + "description": "Tüm dalgaları kazan", + "descriptionComplete": "Tüm dalgalar kazanıldı", + "descriptionFull": "${LEVEL} da tüm dalgaları kazan", + "descriptionFullComplete": "${LEVEL} da tüm dalgalar kazanıldı", + "name": "${LEVEL} Galibiyeti" + }, + "Pro Runaround Victory": { + "description": "Tüm dalgaları tamamla", + "descriptionComplete": "Tüm dalgalar tamamlandı", + "descriptionFull": "${LEVEL} da tüm dalgaları tamamla", + "descriptionFullComplete": "${LEVEL} da tüm dalgalar tamamlandı", + "name": "${LEVEL} Galibiyeti" + }, + "Rookie Football Shutout": { + "description": "Haylazlara puan yaptırmadan kazan", + "descriptionComplete": "Haylazlara puan yaptırmadan kazanıldı", + "descriptionFull": "${LEVEL} da haylazlara puan yaptırmadan kazan", + "descriptionFullComplete": "${LEVEL} da haylazlara puan yaptırmadan kazanıldı", + "name": "${LEVEL} Lokavt" + }, + "Rookie Football Victory": { + "description": "Oyunu kazan", + "descriptionComplete": "Oyun kazanıldı", + "descriptionFull": "${LEVEL} da oyunu kazan", + "descriptionFullComplete": "${LEVEL} da oyun kazanıldı", + "name": "${LEVEL} Galibiyeti" + }, + "Rookie Onslaught Victory": { + "description": "Tüm dalgaları kazan", + "descriptionComplete": "Tüm dalgalar kazanıldı", + "descriptionFull": "${LEVEL} da tüm dalgaları kazan", + "descriptionFullComplete": "${LEVEL} da tüm dalgalar kazanıldı", + "name": "${LEVEL} Galibiyeti" + }, + "Runaround God": { + "description": "2000 puan kazan", + "descriptionComplete": "2000 puan kazanıldı", + "descriptionFull": "${LEVEL} da 2000 puan kazan", + "descriptionFullComplete": "${LEVEL} da 2000 puan kazanıldı", + "name": "${LEVEL} Tanrısı" + }, + "Runaround Master": { + "description": "500 puan kazan", + "descriptionComplete": "500 puan kazanıldı", + "descriptionFull": "${LEVEL} da 500 puan kazan", + "descriptionFullComplete": "${LEVEL} da 500 puan kazanıldı", + "name": "${LEVEL} Ustası" + }, + "Runaround Wizard": { + "description": "1000 puan kazan", + "descriptionComplete": "1000 puan kazanıldı", + "descriptionFull": "${LEVEL} da 1000 puan kazan", + "descriptionFullComplete": "${LEVEL} da 1000 puan kazanıldı", + "name": "${LEVEL} Büyücüsü" + }, + "Sharing is Caring": { + "descriptionFull": "Başarılı bir şekilde oyunu bir arkadaş ile paylaş", + "descriptionFullComplete": "Başarılı bir şekilde, oyun bir arkadaş ile paylaşıldı", + "name": "Paylaşmak Önemsemektir" + }, + "Stayin' Alive": { + "description": "Hiç ölmeden Kazan", + "descriptionComplete": "Hiç ölmeden kazanıldı", + "descriptionFull": "Hiç ölmeden ${LEVEL} da kazan", + "descriptionFullComplete": "Hiç ölmeden ${LEVEL} da kazanıldı", + "name": "Hayatta Kal" + }, + "Super Mega Punch": { + "description": "Tek yumruk ile %100 hasar ver", + "descriptionComplete": "Tek yumruk ile %100 hasar verildi", + "descriptionFull": "${LEVEL} da tek yumruk ile %100 hasar ver", + "descriptionFullComplete": "${LEVEL} da tek yumruk ile %100 hasar verildi", + "name": "Super Mega Yumruk" + }, + "Super Punch": { + "description": "Tek yumruk ile %50 hasar ver", + "descriptionComplete": "Tek yumruk ile $50 hasar verildi", + "descriptionFull": "${LEVEL} da tek yumruk ile %50 hasar ver", + "descriptionFullComplete": "${LEVEL} da tek yumruk ile %50 hasar verildi", + "name": "Süper Yumruk" + }, + "TNT Terror": { + "description": "TNT ile 6 haylaz öldür", + "descriptionComplete": "TNT ile 6 haylaz öldürüldü", + "descriptionFull": "${LEVEL} da TNT ile 6 haylaz öldür", + "descriptionFullComplete": "${LEVEL} da TNT ile 6 haylaz öldürüldü", + "name": "TNT Dehşeti" + }, + "Team Player": { + "descriptionFull": "4+ oyuncu ile bir Takımlar oyunu başlat", + "descriptionFullComplete": "4+ oyuncu ile bir Takımlar oyunu başlatıldı", + "name": "Takım Oyuncusu" + }, + "The Great Wall": { + "description": "Bütün haylazları durdur", + "descriptionComplete": "Bütün haylazlar durduruldu", + "descriptionFull": "${LEVEL} da bütün haylazları durdur", + "descriptionFullComplete": "${LEVEL} da bütün haylazlar durduruldu", + "name": "Muhteşem Bariyer" + }, + "The Wall": { + "description": "Bütün haylazları durdur", + "descriptionComplete": "Bütün haylazlar durduruldu", + "descriptionFull": "${LEVEL} da bütün haylazları durdur", + "descriptionFullComplete": "${LEVEL} da bütün haylazlar durduruldu", + "name": "Bariyer" + }, + "Uber Football Shutout": { + "description": "Haylazlara puan yaptırmadan kazan", + "descriptionComplete": "Haylazlara puan yaptırmadan kazanıldı", + "descriptionFull": "${LEVEL} da haylazlara puan yaptırmadan kazan", + "descriptionFullComplete": "${LEVEL} da haylazlara puan yaptırmadan kazanıldı", + "name": "${LEVEL} Lokavt" + }, + "Uber Football Victory": { + "description": "Oyunu kazan", + "descriptionComplete": "Oyun kazanıldı", + "descriptionFull": "${LEVEL} da oyunu kazan", + "descriptionFullComplete": "${LEVEL} da oyun kazanıldı", + "name": "${LEVEL} Galibiyeti" + }, + "Uber Onslaught Victory": { + "description": "Tüm dalgaları kazan", + "descriptionComplete": "Tüm dalgalar kazanıldı", + "descriptionFull": "${LEVEL} da tüm dalgaları kazan", + "descriptionFullComplete": "${LEVEL} da tüm dalgalar kazanıldı", + "name": "${LEVEL} Galibiyeti" + }, + "Uber Runaround Victory": { + "description": "Tüm dalgaları tamamla", + "descriptionComplete": "Tüm dalgalar tamamlandı", + "descriptionFull": "${LEVEL} da tüm dalgaları tamamla", + "descriptionFullComplete": "${LEVEL} da tüm dalgalar tamamlandı", + "name": "${LEVEL} Galibiyeti" + } + }, + "achievementsRemainingText": "Kalan Başarılar;", + "achievementsText": "Başarı", + "achievementsUnavailableForOldSeasonsText": "Üzgünüz, başarı detayları eski sezonlar için kullanılamaz.", + "addGameWindow": { + "getMoreGamesText": "Daha Çok Oyun...", + "titleText": "Oyun Ekle" + }, + "allowText": "Kabul Et", + "alreadySignedInText": "Başka bir cihazda hesabına giriş yapılmış;\nlütfen hesapları değiştir ya da diğer cihazlardaki\noyunu kapat ve tekrar dene.", + "apiVersionErrorText": "Modul yüklenemiyor ${NAME}; hedef api-versiyon ${VERSION_USED}; ihtiyacımız olan ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(\"Oto\" yalnızca kulaklıklar takılı iken etkinleşir)", + "headRelativeVRAudioText": "Head-Relative VR Ses", + "musicVolumeText": "Müzik Seviyesi", + "soundVolumeText": "Ses Seviyesi", + "soundtrackButtonText": "Müzikler", + "soundtrackDescriptionText": "(oyun sırasında çalmak için kendi müziğinizi atayın)", + "titleText": "Ses" + }, + "autoText": "Oto", + "backText": "Geri", + "banThisPlayerText": "Bu oyuncuyu banla", + "bestOfFinalText": "En-İyi-${COUNT} Final", + "bestOfSeriesText": "En-İyi-${COUNT} Seri:", + "bestOfUseFirstToInstead": 0, + "bestRankText": "En İyi #${RANK} oldun", + "bestRatingText": "En iyi derecen ${RATING}", + "bombBoldText": "BOMBA", + "bombText": "Bomba", + "boostText": "Öne Geç", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} Uygulaması kendisini yapılandırdı", + "buttonText": "buton", + "canWeDebugText": "Oyun otomatik olarak hataları, çökmeleri ve basit \nkullanım istatistiklerini geliştiriciye gondersin mi?\n\nBu veri içeriği kişisel bilgilerinizi kullanmaz\noyunun daha iyi çalışmasına yardımcı olur.", + "cancelText": "İptal", + "cantConfigureDeviceText": "Üzgünüz, ${DEVICE} ayarlanabilir değil.", + "challengeEndedText": "Bu mücadele sona erdi.", + "chatMuteText": "Konuşmayı sustur", + "chatMutedText": "Konuşma Susturuldu", + "chatUnMuteText": "Konuşmayı aç", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "İlerlemek için bu \nseviyeyi tamamlamalısınız!", + "completionBonusText": "Tamamlama Bonusu", + "configControllersWindow": { + "configureControllersText": "Kontrolcüleri Ayarla", + "configureKeyboard2Text": "P2 Klavye Ayarla", + "configureKeyboardText": "Klavye Ayarla", + "configureMobileText": "Kontrolcü olarak Mobil Cihaz", + "configureTouchText": "Dokunmatikleri Ayarla", + "ps3Text": "PS3 Kontrolcüsü", + "titleText": "Kontrolcüler", + "wiimotesText": "Wiimote'ler", + "xbox360Text": "Xbox 360 Kontrolcüleri" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Not: kontrolcü desteği cihaz ve android sürümüne göre değişebilir.", + "pressAnyButtonText": "Kontrolcüyü ayarlamak istiyorsan\nherhangi bir tuşuna bas...", + "titleText": "Kontrolcüleri Ayarla" + }, + "configGamepadWindow": { + "advancedText": "Gelişmiş", + "advancedTitleText": "Gelişmiş Kontrolcü Kurulumu", + "analogStickDeadZoneDescriptionText": "(eğer çubuğu bıraktığında karakterin \"sapıtıyorsa,tıkanıyorsa\" bunu aç)", + "analogStickDeadZoneText": "Analog Çubuk Ölü Bölgesi", + "appliesToAllText": "(bu tipi tüm kontrolcülere uygula)", + "autoRecalibrateDescriptionText": "(eğer karakterin tam hızda hareket etmiyorsa bunu etkinleştir)", + "autoRecalibrateText": "Analog Çubuğu Oto-Yeniden Ayarlama", + "axisText": "Eksenler", + "clearText": "Temizle", + "dpadText": "dpad", + "extraStartButtonText": "İlave Başlat Butonu", + "ifNothingHappensTryAnalogText": "Eğer hiçbir şey olmuyorsa, bunun yerine analog çubuk atamayı deneyin.", + "ifNothingHappensTryDpadText": "Eğer hiçbir şey olmuyorasa, bunun yerine d-pad atamayı deneyin.", + "ignoreCompletelyDescriptionText": "(bu kontolcüyü menüden ve oyundan engelleyin)", + "ignoreCompletelyText": "Tamamen Yoksay", + "ignoredButton1Text": "Yoksayılmış Buton 1", + "ignoredButton2Text": "Yoksayılmış Buton 2", + "ignoredButton3Text": "Yoksayılmış Buton 3", + "ignoredButton4Text": "Yoksayılmış Buton 4", + "ignoredButtonDescriptionText": "(UI yi etkilememesi için 'ev' veya 'senktron' butonların kullanımını engelle)", + "pressAnyAnalogTriggerText": "Herhangi bir analoğu hareket ettir...", + "pressAnyButtonOrDpadText": "Herhangi bir dpad veya butona basın...", + "pressAnyButtonText": "Herhangi bir butona basın...", + "pressLeftRightText": "Sağa veya Sola basın...", + "pressUpDownText": "Yukarı veya Aşağıya basın...", + "runButton1Text": "Çalıştır Buton 1", + "runButton2Text": "Çalıştır Buton 2", + "runTrigger1Text": "Çalıştır Tetik 1", + "runTrigger2Text": "Çalıştır Tetik 2", + "runTriggerDescriptionText": "(analog tetikler sana değişken hızlar sunar)", + "secondHalfText": "2'si 1 arada cihaz kontrolcüsünün birini\nyapılandırmak için bunu kullan.\nTek bir kontrolcü gibi görünecek.", + "secondaryEnableText": "Etkinleştir", + "secondaryText": "İkincil Kontrolcü", + "startButtonActivatesDefaultDescriptionText": "(eğer başlat butonun daha çok \"menü\" butonu ise bunu kapat)", + "startButtonActivatesDefaultText": "Başlat Butonu Varsayılan Aktifleştirme Zımpırtısıdır", + "titleText": "Kontrolcü Kurulumu", + "twoInOneSetupText": "2'si 1 arada Kontrolcü Kurulumu", + "uiOnlyDescriptionText": "(bu kontrolcüyü bir oyuna katılmaması için engelle)", + "uiOnlyText": "Menü Kullanımını Kısıtla", + "unassignedButtonsRunText": "Atanmamış Tüm Butonları Çalıştır", + "unsetText": "", + "vrReorientButtonText": "VR Buton Yönünü Değiştir" + }, + "configKeyboardWindow": { + "configuringText": "${DEVICE} Yapılandırılıyor", + "keyboard2NoteText": "Not: Çoğu klavyenin tek seferde yalnızca\nbirkaç tuşu çalışır, bu yüzden ikinci oyuncunun\nikinci klavye ile oynaması daha iyi olacaktır.\nNot:Bunu yapmak için oyunculara farklı \ntuşları atamalısınız." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Eylem Kontrolü Ölçeği", + "actionsText": "Eylemler", + "buttonsText": "Butonlar", + "dragControlsText": "< Kontrol yerini değiştirmek için sürükleyin >", + "joystickText": "joystick", + "movementControlScaleText": "Hareket Kontrolü Ölçeği", + "movementText": "Hareket", + "resetText": "Sıfırla", + "swipeControlsHiddenText": "Sürme Simgelerini Gizle", + "swipeInfoText": "\"Sürme\" kontrol stilleri kullanışlıdır fakat\nkontrollere bakmadan oynamak daha kolaydır.", + "swipeText": "sürme", + "titleText": "Dokunmatikleri Yapılandır" + }, + "configureItNowText": "Şimdi yapılandır?", + "configureText": "Yapılandır", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "En iyi sonuçları gecikmesiz Wifi ağına bağlıyken\nalırsınız. Diğer wifi cihazlarını kapatarak, wifi \nvericisine yakın olarak ve oyunu direkt ethernet \nile kurarak gecikmeyi azaltabilirsiniz.", + "explanationText": "Akıllı Telefon veya Tableti kablosuz kontrolcü olarak kullanmak için\n\"${REMOTE_APP_NAME}\" uygulamasını yükleyin. Çok sayıda cihazı Wi-Fi\nile ${APP_NAME} a bağlayabilirsiniz. Ve bu bedava!", + "forAndroidText": "Android için:", + "forIOSText": "iOS için:", + "getItForText": "${REMOTE_APP_NAME} iOS için Apple App Store ve Android\niçin Google Play Store veya Amazon Appstore da", + "googlePlayText": "Google Play", + "titleText": "Mobil Cihazı Kontrolcü Olarak Kullan:" + }, + "continuePurchaseText": "${PRICE} için devam et?", + "continueText": "Devam Et", + "controlsText": "Kontroller", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Bu Tüm-Zamanlar sıralamalarında uygulanamaz.", + "activenessInfoText": "Bu çarpan oynadığın günlerde yükselir\noynamadığın günlerde düşer.", + "activityText": "Aktiflik", + "campaignText": "Mücadele", + "challengesInfoText": "Mini-oyunları tamamlayarak ödül kazan.\n\nÖdüller ve zorluk seviyesi mücalede\ntamamlandığı her zaman artar süre bittiğinde\nve caydığında azalır.", + "challengesText": "Meydan Okumalar", + "currentBestText": "Şimdiki En İyi", + "customText": "Özel", + "entryFeeText": "Katılım", + "forfeitConfirmText": "Bu meydan okumadan cay?", + "forfeitNotAllowedYetText": "Bu meydan okuma henüz cayılamaz.", + "forfeitText": "Cayma", + "multipliersText": "Çarpanlar", + "nextChallengeText": "Sonraki Meydan Okuma", + "nextPlayText": "Sonraki Oyun", + "ofTotalTimeText": "${TOTAL} üzerinden", + "playNowText": "Şimdi Oyna", + "pointsText": "Puanlar", + "powerRankingFinishedSeasonUnrankedText": "(bitmiş sezon sıralanmaz)", + "powerRankingNotInTopText": "İlk (${NUMBER} içinde değil", + "powerRankingPointsEqualsText": "= ${NUMBER} puan", + "powerRankingPointsMultText": "(x ${NUMBER} puan)", + "powerRankingPointsText": "${NUMBER} puan", + "powerRankingPointsToRankedText": "(${REMAINING} puandan ${CURRENT} puan)", + "powerRankingText": "Oyuncu Sıralaman", + "prizesText": "Ödüller", + "proMultInfoText": "${PRO} yükseltmesi olan oyuncular\n${PERCENT}% daha fazla puan kazanır.", + "seeMoreText": "Daha Fazla...", + "skipWaitText": "Beklemeden Geç", + "timeRemainingText": "Kalan Süre", + "toRankedText": "Sıralamasında", + "totalText": "toplam", + "tournamentInfoText": "Ligindeki diğer oyuncular arasında\nen yüksek skoru yap.\n\nÖdüller En fazla skor yapan oyuncular\narasında turnuva süresi bittiğinde verilir.", + "welcome1Text": "${LEAGUE} hoş geldiniz. Lig sıralamanı turnuvalarda\nyıldız derecesi kazanarak, başarıları tamamlayarak\nve kupalar kazanarak geliştirebilirsin.", + "welcome2Text": "Ayrıca benzer aktivitelerden biletler kazanabilirsin.\nBiletler yeni karakterler, haritalar, mini-oyunlar\nve turnuvalara katılmak için kulanılabilir.", + "yourPowerRankingText": "Oyuncu Sıralaman:" + }, + "copyOfText": "Kopya ${NAME}", + "createEditPlayerText": "", + "createText": "Oluştur", + "creditsWindow": { + "additionalAudioArtIdeasText": "Ek Ses, Early Artwork ve ${NAME} dan fikirler", + "additionalMusicFromText": "${NAME} tarafından ilave müzik", + "allMyFamilyText": "Tüm aile ve arkadaşlarım, kimler yardımcı olduysa", + "codingGraphicsAudioText": "${NAME} tarafından Kodlama, Grafikler ve Ses", + "languageTranslationsText": "Dil Çevirileri:", + "legalText": "Yasal:", + "publicDomainMusicViaText": "${NAME} ile Kullanımı serbest müzik", + "softwareBasedOnText": "Bu yazılım ${NAME} altyapısının bir parçasıdır", + "songCreditText": "${TITLE}, ${PERFORMER} tarafından yapıldı\n${COMPOSER} tarafından bestelendi, ${ARRANGER} tarafından düzenlendi\n${PUBLISHER} tarafından yayımlandı, ${SOURCE} ya iltifaten.", + "soundAndMusicText": "Ses & Müzik:", + "soundsText": "Sesler (${SOURCE}):", + "specialThanksText": "Özel Teşekkürler:", + "thanksEspeciallyToText": "Özellikle ${NAME} a Teşkkürler", + "titleText": "${APP_NAME} Jenerik", + "whoeverInventedCoffeeText": "Kahveyi kim icat ettiyse" + }, + "currentStandingText": "Şu anki mevkin #${RANK}", + "customizeText": "Özelleştir...", + "deathsTallyText": "${COUNT} Ölüm", + "deathsText": "Ölümler", + "debugText": "Hata Ayıklama", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Not: Bunu test ederken Ayarlar->Grafikler->Dokular'ı 'Yüksek' yapmanız önerilir.", + "runCPUBenchmarkText": "CPU Benchmark Çalıştır", + "runGPUBenchmarkText": "GPU Benchmark Çalıştır", + "runMediaReloadBenchmarkText": "Medya-Yükleme Benchmark Çalıştır", + "runStressTestText": "Stres Testi Çalıştır", + "stressTestPlayerCountText": "Oyuncu Sayısı", + "stressTestPlaylistDescriptionText": "Stres Testi ÇalmaListesi", + "stressTestPlaylistNameText": "ÇalmaListesi Adı", + "stressTestPlaylistTypeText": "ÇaşmaListesi Tipi", + "stressTestRoundDurationText": "Tur Süresi", + "stressTestTitleText": "Stres Testi", + "titleText": "Benchmark & Stres Testleri", + "totalReloadTimeText": "Tomplam yükleme zamanı: ${TIME} (detaylar için kayda bak)" + }, + "defaultGameListNameText": "Varsayılan ${PLAYMODE} ÇalmaListesi", + "defaultNewGameListNameText": "Benim ${PLAYMODE} ÇalmaListem", + "deleteText": "Sil", + "demoText": "Tanıtım", + "denyText": "Reddet", + "desktopResText": "PC Çözünürlüğü", + "difficultyEasyText": "Kolay", + "difficultyHardOnlyText": "Sadece Zor Mod", + "difficultyHardText": "Zor", + "difficultyHardUnlockOnlyText": "Bu seviye sadece Zor modda açılır.\nNeler olabileceğini düşünebiliyor musun!?!?", + "directBrowserToURLText": "Lütfen gelen URL yi doğrudan bir web tarayıcıyda kullanın:", + "disableRemoteAppConnectionsText": "Remote-App Bağlantılarını Engelle", + "disableXInputDescriptionText": "4 kontrolcüden fazla kullanılabilir fakat iyi çalışmayabilir.", + "disableXInputText": "XInput etkisizleştir", + "doneText": "Tamam", + "drawText": "Beraberlik", + "duplicateText": "Kopyala", + "editGameListWindow": { + "addGameText": "Oyun\nEkle", + "cantOverwriteDefaultText": "Varsayılan çalmaListesinin üzerine yazılamaz!", + "cantSaveAlreadyExistsText": "Bu isimde bir çalmaListesi zaten var!", + "cantSaveEmptyListText": "Boş çalmaListesi kaydedilemez!", + "editGameText": "Oyun\nDüzenle", + "listNameText": "ÇalmaListesi Adı", + "nameText": "İsim", + "removeGameText": "Oyun\nKaldır", + "saveText": "Listeyi Kaydet", + "titleText": "ÇalmaListesi Editörü" + }, + "editProfileWindow": { + "accountProfileInfoText": "Bu özel profil, ismin ve simgen\nüzerine oluşturulur.\n\n${ICONS}\n\n\nKullanmak için özel profil yarat", + "accountProfileText": "(hesap profili)", + "availableText": "\"${NAME}\" adı kullanılabilir.", + "characterText": "karakter", + "checkingAvailabilityText": "\"${NAME}\" kullanılabilirliği kontrol ediliyor...", + "colorText": "renk", + "getMoreCharactersText": "Daha Fazla Karakter...", + "getMoreIconsText": "Daha Fazla Simge...", + "globalProfileInfoText": "Global oyuncu profilleri dünya çapında benzersiz\nisimleri garanti eder.Ayrıca özel simgeleri kapsar.", + "globalProfileText": "(global profil)", + "highlightText": "vurgu", + "iconText": "simge", + "localProfileInfoText": "Yerel oyuncu profilleri, simgenin ve adının benzersiz\nolmasını garanti etmez.Global profile yükseltirsen\nbenzersiz ad ve simge rezerv edebilirsin.", + "localProfileText": "(yerel profil)", + "nameDescriptionText": "Oyuncu Adı", + "nameText": "Ad", + "randomText": "rastgele", + "titleEditText": "Profil Düzenle", + "titleNewText": "Yeni Profil", + "unavailableText": "\"${NAME}\" kullanılamaz; başka bir tane dene.", + "upgradeProfileInfoText": "Bu senin dünya çapında benzersiz oyuncu adı\nalmanı ve özel simge atamanı sağlar.", + "upgradeToGlobalProfileText": "Global Profile Yükselt" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Varsayılan müzikListesini silemezsin.", + "cantEditDefaultText": "Varsayılan müzikListesini düzenleyemezsin. Birleştirebilir ya da yenisini yapabilirsin.", + "cantOverwriteDefaultText": "Varsayılan müzikListesinin üzerine yazılamaz", + "cantSaveAlreadyExistsText": "Bu isimde bir müzikListesi zaten mevcut!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Varsayılan MüzikListesi", + "deleteConfirmText": "MüzikListesini Sil:\n\n'${NAME}' ?", + "deleteText": "MüzikListesi\nSil", + "duplicateText": "MüzikListesi\nKopyala", + "editSoundtrackText": "MüzikListesi Editörü", + "editText": "MüzikListesi\nDüzenle", + "fetchingITunesText": "iTunes Müzikleri Alınıyor", + "musicVolumeZeroWarning": "Uyarı: müzik seviyesi 0 ayarlanmış", + "nameText": "İsim", + "newSoundtrackNameText": "MüzikListem ${COUNT}", + "newSoundtrackText": "Yeni MüzikListesi:", + "newText": "MüzikListesi\nYarat", + "selectAPlaylistText": "ÇalmaListesi Seç", + "selectASourceText": "Müzik Kaynağı", + "testText": "test", + "titleText": "MüzikListeleri", + "useDefaultGameMusicText": "Varsayılan Oyun Müziği", + "useITunesPlaylistText": "iTunesçalma listesi", + "useMusicFileText": "Müzik Dosyası (mp3, vb)", + "useMusicFolderText": "Müzik Dosyaları Klasörü" + }, + "editText": "Düzenle", + "endText": "Bitir", + "enjoyText": "Keyfini Çıkar!", + "epicDescriptionFilterText": "${DESCRIPTION} epik ağırçekim.", + "epicNameFilterText": "Epik ${NAME}", + "errorAccessDeniedText": "erişim reddedildi", + "errorOutOfDiskSpaceText": "disk alanı doldu", + "errorText": "Hata", + "errorUnknownText": "bilinmeyen hata", + "exitGameText": "Çık ${APP_NAME}?", + "exportSuccessText": "'${NAME}' Dışa Aktarıldı.", + "externalStorageText": "Harici Depolama", + "failText": "Başarısızlık", + "fatalErrorText": "Uh oh; bir şeyler eksik veya hatalı\nLütfen uygulamayı yeniden yükleyin\nveya ${EMAIL} ile yardım alın.", + "fileSelectorWindow": { + "titleFileFolderText": "Bir Dosya veya Klasör Seç", + "titleFileText": "Bir Dosya Seç", + "titleFolderText": "Bir Klasör Seç", + "useThisFolderButtonText": "Bu Klasörü Kullan" + }, + "filterText": "Filtre", + "finalScoreText": "Final Skoru", + "finalScoresText": "Final Skorları", + "finalTimeText": "Final Süresi", + "finishingInstallText": "Yükleme tamamlanıyor; bir dakika...", + "fireTVRemoteWarningText": "* En iyi deneyim için, Oyun\nkontrolcüsü kullanın veya\n'${REMOTE_APP_NAME}' uygulamasını\ntablet ya da telefonunuza yükleyin.", + "firstToFinalText": "İlk-${COUNT} Final", + "firstToSeriesText": "İlk-${COUNT} Seri", + "fiveKillText": "5 Öldürme!!!", + "flawlessWaveText": "Çiziksiz Dalga!", + "fourKillText": "Dörtlü Öldürme!!!", + "friendScoresUnavailableText": "Arkadaş skorları mevcut değil.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Oyun ${COUNT} Liderler", + "gameListWindow": { + "cantDeleteDefaultText": "Varsayılan çalmaListesini silemezsin.", + "cantEditDefaultText": "Varsayılan çalmaListesi düzenlenemez! Kopyalanabilir ya da yenisi yapılabilir.", + "cantShareDefaultText": "Varsayılan ÇalmaListesi paylaşılamaz.", + "deleteConfirmText": "Sil \"${LIST}\"?", + "deleteText": "ÇalmaListesi\nSil", + "duplicateText": "ÇalmaListesi\nKopyala", + "editText": "ÇalmaListesi\nDüzenle", + "newText": "ÇalmaListesi\nYarat", + "showTutorialText": "Öğreticiyi Göster", + "shuffleGameOrderText": "Oyun Sırasını Karıştır", + "titleText": "Özel ${TYPE} ÇalmaListeleri" + }, + "gameSettingsWindow": { + "addGameText": "Oyun Ekle" + }, + "gamesToText": "${WINCOUNT}-${LOSECOUNT} kazandı", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Hatırlatma: Eğer yeterince kontrolcüye sahipsen,\nher cihazda birden fazla oyuncu partiye katılabilir.", + "aboutDescriptionText": "Bu sekmeleri partide toplanmak için kullan\n\nPartiler, oyunları ve turnuvaları başka cihazlar\nile birlikte oynamanı sağlar.\n\nSohbet ve parti ile etkileşim için sağ üstteki\n${PARTY} butonunu kullan.\n(menüdeyken kontrolcüden ${BUTTON} simgesine bas)", + "aboutText": "Hakkında", + "addressFetchErrorText": "", + "appInviteMessageText": "${NAME} sana ${APP_NAME} ile ${COUNT} bilet gönderdi.", + "appInviteSendACodeText": "Bir Kod Gönder", + "appInviteTitleText": "${APP_NAME} Uygulama Daveti", + "bluetoothAndroidSupportText": "(bluetooth destekli tüm adroid cihazla ile çalışır)", + "bluetoothDescriptionText": "Bluetooth ile bir parti Kur/katıl:", + "bluetoothHostText": "Bluetooth ile Kur", + "bluetoothJoinText": "Bluetooth ile Katıl", + "bluetoothText": "Bluetooth", + "checkingText": "denetleniyor...", + "copyCodeConfirmText": "Kod panoya kopyalandı", + "copyCodeText": "Kodu kopyala", + "dedicatedServerInfoText": "En iyi sonuçlar için ozel sunucu ayarlayın. Bakınız; bombsquadgame.com/server", + "disconnectClientsText": "Bu, partindeki ${COUNT} oyuncunun bağlantısını\nkesecek. Emin misin ?", + "earnTicketsForRecommendingAmountText": "Eğer arkadaşların oyunu denerler ise ${COUNT} bilet alacak\n(Sen ise bunu her yapan için ${YOU_COUNT} tane.)", + "earnTicketsForRecommendingText": "Bedave bilet için\nOyunu paylaş...", + "emailItText": "E-Posta Gönder", + "favoritesSaveText": "Favori Olarak Kaydet", + "favoritesText": "Favoriler", + "freeCloudServerAvailableMinutesText": "Sonraki ücretsiz bulut sunucusu ${MINUTES} dakika içinde kullanılabilir olacak", + "freeCloudServerAvailableNowText": "Ücretsiz bulut sunucusu şuan kullanılabilir", + "freeCloudServerNotAvailableText": "Ücretsiz bulut sunucusu şuan mevcut değil.", + "friendHasSentPromoCodeText": "${NAME} tarafından ${COUNT} ${APP_NAME} bileti", + "friendPromoCodeAwardText": "Kullanıldığı her zaman ${COUNT} bilet alacaksın.", + "friendPromoCodeExpireText": "Bu kodun ${EXPIRE_HOURS} saat içinde süresi dolacak ve sadece yeni oyuncularda çalışır.", + "friendPromoCodeInstructionsText": "Kullanmak için ${APP_NAME} uygulamasını açın ve \"Ayarlar->Gelişmiş->Promo Kodu\"\nna gidin. Tüm platformlar arası İndirme bağlantıları için bombsquadgame.com'a gidin.", + "friendPromoCodeRedeemLongText": "Maksimum ${MAX_USES} kişi olmak üzere ${COUNT} bedava bilet alabilirsin.", + "friendPromoCodeRedeemShortText": "Oyun içi ${COUNT} bilet alabilirsin", + "friendPromoCodeWhereToEnterText": "(Ayarlar>Gelişmiş>Promo kodu gir içinde)", + "getFriendInviteCodeText": "Arkadaş Davet Kodu Al", + "googlePlayDescriptionText": "Partine Google Play oyuncularını davet et:", + "googlePlayInviteText": "Davet", + "googlePlayReInviteText": "Partinde şuan ${COUNT} Google Play oyuncusu var.\nEğer yeni Davet başlatırsan bağlantıları kesilecek.\nTerar yeni davet göndermen gerekecek.", + "googlePlaySeeInvitesText": "Davetleri Gör", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Android / Google Play versiyonu)", + "hostPublicPartyDescriptionText": "Herkese açık bir parti kur.", + "hostingUnavailableText": "Sunucu ev sahipliği mevcut değil", + "inDevelopmentWarningText": "Not:\n\nAğ oyunu yeni ve geliştirilebilir bir özellik.\nŞimdilik tüm oyuncuların aynı Wi-Fi ağına bağlı\nolması önerilir.", + "internetText": "İnternet", + "inviteAFriendText": "Bu oyun arkadaşlarında yok mu? Onları davet et.\nDenerler ise ${COUNT} bedava bilet alacaklar.", + "inviteFriendsText": "Arkadaş Davetleri", + "joinPublicPartyDescriptionText": "Herkese açık bir partiye katıl", + "localNetworkDescriptionText": "Ağındaki bir partiye katıl (LAN, Bluetooth, vb.)", + "localNetworkText": "Yerel Ağ", + "makePartyPrivateText": "Partimi Gizle", + "makePartyPublicText": "Partimi Herkese Açık Yap", + "manualAddressText": "Adres", + "manualConnectText": "Bağlan", + "manualDescriptionText": "Adres ile partiye katıl:", + "manualJoinSectionText": "Adresle Katıl", + "manualJoinableFromInternetText": "İnternet üzerinden katılınabilirliğin?:", + "manualJoinableNoWithAsteriskText": "HAYIR*", + "manualJoinableYesText": "EVET", + "manualRouterForwardingText": "*Bunu düzeltmek için, modeminizden yerel adresinizin UDP portunu ${PORT} yapın", + "manualText": "Manuel", + "manualYourAddressFromInternetText": "İnternetteki adresiniz:", + "manualYourLocalAddressText": "Yerel adresiniz:", + "nearbyText": "Yakında", + "noConnectionText": "", + "otherVersionsText": "(diğer sürümler)", + "partyCodeText": "Parti kodu", + "partyInviteAcceptText": "Kabul Et", + "partyInviteDeclineText": "Reddet", + "partyInviteGooglePlayExtraText": "('Lobi' penceresinden 'Google Play' sekmesine bakın)", + "partyInviteIgnoreText": "Gözardı Et", + "partyInviteText": "${NAME} seni partisine\nkatılman için davet etti!", + "partyNameText": "Parti Adı", + "partyServerRunningText": "Parti sunucun çalışıyor.", + "partySizeText": "parti kapasitesi", + "partyStatusCheckingText": "durum denetleniyor...", + "partyStatusJoinableText": "şuan partine internetten katılınabilir", + "partyStatusNoConnectionText": "sunucuya bağlanılamıyor", + "partyStatusNotJoinableText": "partin internetten katılınabilir değil", + "partyStatusNotPublicText": "partin herkese açık değil", + "pingText": "ping", + "portText": "Port", + "privatePartyCloudDescriptionText": "Özel partiler özel bulut sunucularında çalışır; yönlendirici yapılandırması gerekmez", + "privatePartyHostText": "Özel bir partiye ev sahipliği yapın", + "privatePartyJoinText": "Özel Bir Partiye Katılın", + "privateText": "Özel", + "publicHostRouterConfigText": "Bu, yönlendiricinizde bağlantı noktası iletme yapılandırması gerektirebilir. Daha kolay bir seçenek için, özel bir partiye ev sahipliği yapın.", + "publicText": "Herkese Açık", + "requestingAPromoCodeText": "Kod talep ediliyor...", + "sendDirectInvitesText": "Doğrudan Davet Gönder", + "shareThisCodeWithFriendsText": "Bu kodu arkadaşların ile paylaş:", + "showMyAddressText": "Adresimi Göster", + "startHostingPaidText": "${COST} karşılığında ev sahibi olun", + "startHostingText": "Ev sahibi ol", + "startStopHostingMinutesText": "Önümüzdeki ${MINUTES} dakika boyunca ücretsiz ev sahibi özelliğini başlatıp durdurabilirsiniz.", + "stopHostingText": "Ev sahipliğini durdur", + "titleText": "Lobi", + "wifiDirectDescriptionBottomText": "Eğer tüm cihazların 'Wi-Fi Direct' paneli varsa, diğerlerini bulmak ve bağlanmak için\nkullanılabilir. Tüm cihazlar bağlandığında, Aynı Wi-Fi ağına bağlıymış gibi 'Yerel Ağ'\nsekmesini kullanarak partiler oluşturabilirsin. \n\nEn iyi sonuç için, Wi-Fi Direct kurucusu ayrıca ${APP_NAME} parti kurucusu olmalı.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct Android cihazların Wi-Fi ağı ihtiyacı olmadan doğrudan\nbağlanmasını sağlar.Bu en iyi Android 4.2 ve üstünde çalışır.\n\nKullanmak için, Wi-Fi ayarlarını açın ve Menüden 'Wi-Fi Direct' e bakın.", + "wifiDirectOpenWiFiSettingsText": "Wi-Fi Ayarlarını Aç", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(tüm platformlar arasında çalışır)", + "worksWithGooglePlayDevicesText": "(Google Play (android) sürümü kullanan cihazlar arasında çalışır)", + "youHaveBeenSentAPromoCodeText": "${APP_NAME} promo kodu gönderdin:" + }, + "getTicketsWindow": { + "freeText": "ÜCRETSİZ!", + "freeTicketsText": "Ücretsiz Bilet", + "inProgressText": "Ödeme işlemde... lütfen birazdan tekrar deneyin.", + "purchasesRestoredText": "Satın Almalar Geri Yüklendi.", + "receivedTicketsText": "${COUNT} Bilet Alındı!", + "restorePurchasesText": "Satın Almaları Geri Yükle", + "ticketPack1Text": "Ufak Bilet Paketi", + "ticketPack2Text": "Standart Bilet Paketi", + "ticketPack3Text": "Büyük Bilet Paketi", + "ticketPack4Text": "Dev Bilet Paketi", + "ticketPack5Text": "Muazzam Bilet Paketi", + "ticketPack6Text": "En Üst Düzey Bilet Paketi", + "ticketsFromASponsorText": "Bir sponsordan\n${COUNT} Bilet Al", + "ticketsText": "${COUNT} Bilet", + "titleText": "Bilet Al", + "unavailableLinkAccountText": "Üzgünüz, Satın almalar bu patformda kullanılamaz.\nBu geçici oldugu gibi, bu hesabını diğer platformlardaki\nhesablara bağlayabilir oradan satın alma işlemi yapbilirsin.", + "unavailableTemporarilyText": "Bu şu anda müsait değil; lütfen daha sonra tekrar deneyin.", + "unavailableText": "Üzgünüz, Bu mümkün değil.", + "versionTooOldText": "Üzgünüz, Oyunun bu sürümü çok eski; lütfen yeni bir sürüme güncelleyin.", + "youHaveShortText": "${COUNT} Biletin var", + "youHaveText": "${COUNT} biletin var" + }, + "googleMultiplayerDiscontinuedText": "Üzgünüz, Google'ın çok oyunculu servisi şu anda çalışmıyor.\nBir yer değişimi için olabildiğince hızlı çalışıyorum.\nO zamana kadar, lütfen başka bir bağlantı yöntemi deneyin.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Her Zaman", + "fullScreenCmdText": "Tam Ekran (Cmd-F)", + "fullScreenCtrlText": "Tam Ekran (Ctrl-F)", + "gammaText": "Gama", + "highText": "Yüksek", + "higherText": "Çok Yüksek", + "lowText": "Düşük", + "mediumText": "Orta", + "neverText": "Asla", + "resolutionText": "Çözünürlük", + "showFPSText": "FPS göster", + "texturesText": "Dokular", + "titleText": "Grafikler", + "tvBorderText": "TV Ekranı", + "verticalSyncText": "Dikey Senkron", + "visualsText": "Görseller" + }, + "helpWindow": { + "bombInfoText": "- Bomba -\nYumruktan daha güçlüdür, fakat\nkendi bomban ile zarar görebilirsin.\nEn iyi sonuç için fitil bitmeden önce\ndüşmanlarına doğru fırlat.", + "canHelpText": "${APP_NAME} sana yardımcı olabilir.", + "controllersInfoText": "Arkadaşlarınla birlikte ağ üzerinden veya yeterince kontrolcünüz\nvar ise tek bir cihazdan ${APP_NAME} oynayabilirsiniz.\nHatta telefonunuzu bile bedava '${REMOTE_APP_NAME}' uygulaması ile\nkontrolcü olarak kullanabilirsiniz. ${APP_NAME} bunu destekler.\nDaha fazla bilgi için Ayarlar->Kontroller'e bakın.", + "controllersText": "Kontrolcüler", + "controlsSubtitleText": "Dost Canlısı ${APP_NAME} karakterinin birkaç basit özelliği var:", + "controlsText": "Kontroller", + "devicesInfoText": "${APP_NAME}'ın VR sürümü ile normal sürümü ağ\nüzerinde oynanabilir. Yani ekstra telefon, tablet veya\nbilgisayar al ve oyunu aç, Oyunun normal sürümünü\nVR sürümüne bağlamak kullanışlıdır, başkalarının\noyunu izlemesini sağlar.", + "devicesText": "Cihazlar", + "friendsGoodText": "Bunların olması güzel. ${APP_NAME} birkaç oyuncu ile daha\neğlencelidir. Tek seferde 8 oyuncuya kadar destekler.", + "friendsText": "Arkadaşlar", + "jumpInfoText": "- Zıpla -\nZıplamak, ufak çıkıntılardan geçmeni\nve bir şeyleri daha yukarı atmanı\nsağlar. Aşırı eğlencelidir.", + "orPunchingSomethingText": "Bir şeyleri patlatmak, uçurumdan fırlatmak veya yoluna çıkanlara yapışkan bomba atmak.", + "pickUpInfoText": "- Kaldır -\nBayrakları, düşmanları veya etraftaki\nherhangi bir şeyi tutmanı sağlar.\nFırlatmak için tekrar bas.", + "powerupBombDescriptionText": "Tek seferde bir bomba yerine \nPeş peşe üç bomba atmanı sağlar.", + "powerupBombNameText": "Üçlü-Bomba", + "powerupCurseDescriptionText": "Muhtemelen bundan kaçınmak isteyeceksin.\n...Ya da istemez misin?", + "powerupCurseNameText": "Lanet", + "powerupHealthDescriptionText": "Sağlığının tamamını doldurur.\nTahmin etmiş olamazsın...", + "powerupHealthNameText": "Sağlık-Paketi", + "powerupIceBombsDescriptionText": "Normal bombalardan güçsüzdür\nfakat düşmanlarını dondurur\nve özellikle kırılgan yapar.", + "powerupIceBombsNameText": "Buz-Bombaları", + "powerupImpactBombsDescriptionText": "Nispeten normal bombalardan\ngüçsüzdür fakat temas halinde patlarlar.", + "powerupImpactBombsNameText": "Tahrik-Bombaları", + "powerupLandMinesDescriptionText": "Bu 3 paket olarak gelir. Bölgeni\nsavunmak ve hızlı düşmanları\ndurdurmak için kullanışlıdır.", + "powerupLandMinesNameText": "Kara-Mayınları", + "powerupPunchDescriptionText": "Yumruklarını daha sert\nhızlı ve güçlü yapar.", + "powerupPunchNameText": "Boks-Eldivenleri", + "powerupShieldDescriptionText": "Senin için bir parça\nhasarı absorbe eder.", + "powerupShieldNameText": "Enerji-Kalkanı", + "powerupStickyBombsDescriptionText": "Değdiği herhangi bir şeye\nyapışır. Eğlence konusu olur.", + "powerupStickyBombsNameText": "Yapışkan-Bombalar", + "powerupsSubtitleText": "Elbette güçlendiriciler olmadan oyun tamamlanmış değildir:", + "powerupsText": "Güçlendiriciler", + "punchInfoText": "- Yumruk -\nYumruklar, hızlı hareket edersen\ndaha fazla hasar verir.\nYani deli gibi koş ve dön.", + "runInfoText": "- Koş -\nKoşmak için herhangi bir tuşa basılı tut. Koşmak hızlıca yerdeğiştirmeni sağlar\nfakat dönmeni zorlaştırır, yani uçurumlara dikkat et.", + "someDaysText": "Bazı günler canın bir şeyleri yumruklamak ister. Ya da fırlatmak.", + "titleText": "${APP_NAME} Yardımı", + "toGetTheMostText": "Bu oyunu en iyi şekilde oynamak için ihtiyacın olan;", + "welcomeText": "${APP_NAME}'a Hoş Geldiniz!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} Menüde krallar gibi geziniyor -", + "importPlaylistCodeInstructionsText": "ÇalmaListesini bir yerlerde kullanmak için bu kodu kullan:", + "importPlaylistSuccessText": "${TYPE} ÇalmaListesi içe aktarıldı'${NAME}'", + "importText": "İçe Aktar", + "importingText": "İçe Aktarılıyor...", + "inGameClippedNameText": "Şöyle görünecek\n\"${NAME}\"", + "installDiskSpaceErrorText": "HATA: Kurulum tamamlanamıyor.\nCihazınızda boş alan kalmamış olabilir.\nBiraz alan açın ve yeniden deneyin.", + "internal": { + "arrowsToExitListText": "Listeden çıkmak için ${LEFT} ya da ${RIGHT} basın.", + "buttonText": "buton", + "cantKickHostError": "Kurucu'yu atamazsın.", + "chatBlockedText": "${NAME}, ${TIME} saniye sohbette bloklandı.", + "connectedToGameText": "'${NAME}' Sunucusuna girildi", + "connectedToPartyText": "${NAME} Partisine katılındı!", + "connectingToPartyText": "Bağlanılıyor...", + "connectionFailedHostAlreadyInPartyText": "Bağlantı başarısız; Kurucu başka bir partide.", + "connectionFailedPartyFullText": "Bağlantı başarısız; parti dolu", + "connectionFailedText": "Bağlantı başarısız.", + "connectionFailedVersionMismatchText": "Bağlantı başarısız; Kurucu oyunun başka bir sürümünü kullanıyor.\nHer iki cihazın da güncel olduğundan emin olun ve tekrar deneyin.", + "connectionRejectedText": "Bağlantı reddedildi.", + "controllerConnectedText": "${CONTROLLER} bağlandı.", + "controllerDetectedText": "1 kontrolcü algılandı.", + "controllerDisconnectedText": "${CONTROLLER} bağlantısı koptu.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} bağlantısı koptu. Lürfen tekrar bağlamayı deneyin.", + "controllerForMenusOnlyText": "Bu kontrolcü oynamak için kullanılamaz; sadece menüde gezmek için.", + "controllerReconnectedText": "${CONTROLLER} tekrar bağlandı.", + "controllersConnectedText": "${COUNT} kontrolcü bağlandı.", + "controllersDetectedText": "${COUNT} kontrolcü algılandı.", + "controllersDisconnectedText": "${COUNT} kontrolcünün bağlantısı koptu.", + "corruptFileText": "Bozuk dosya algılandı. Lütfen tekrar yükleme yapın, veya E-Posta ${EMAIL}", + "errorPlayingMusicText": "Müzik Çalınırken Hata: ${MUSIC}", + "errorResettingAchievementsText": "Çevrimiçi başarılar sıfırlanamıyor; lütfen daha sonra tekrar deneyin.", + "hasMenuControlText": "${NAME} menü kontrolü yapıyor.", + "incompatibleNewerVersionHostText": "Kurucu oyunun daha yeni bir sürümünü kullanıyor.\nSon sürüme güncelleyin ve tekrar deneyin.", + "incompatibleVersionHostText": "Kurucu oyunun başka bir sürümünü kullanıyor.\nİki cihazın da güncel olduğundan emin olun.", + "incompatibleVersionPlayerText": "${NAME} oyunun başka bir sürümünü kullanıyor.\niki cihazın da güncel olduğundan emin olun.", + "invalidAddressErrorText": "Hata: Geçersiz adres.", + "invalidNameErrorText": "Hata: geçersiz isim.", + "invalidPortErrorText": "Hata: Geçersiz port", + "invitationSentText": "Davet Göderildi.", + "invitationsSentText": "${COUNT} davet gönderildi.", + "joinedPartyInstructionsText": "Biri senin partine katıldı.\nOyunu başlatmak için 'Oyna' ya git", + "keyboardText": "Klavye", + "kickIdlePlayersKickedText": "${NAME} boşta olduğu için atıldı.", + "kickIdlePlayersWarning1Text": "${NAME} boşta durmaya devam ederse ${COUNT} saniye içinde atılacak.", + "kickIdlePlayersWarning2Text": "(Ayarlar-> Gelişmiş den bunu kapatabilirsin)", + "leftGameText": "'${NAME}' Terkedildi", + "leftPartyText": "${NAME} partisini terk et.", + "noMusicFilesInFolderText": "Klasör müzik dosyası içermiyor.", + "playerJoinedPartyText": "${NAME} partiye katıldı!", + "playerLeftPartyText": "${NAME} partiyi terk etti.", + "rejectingInviteAlreadyInPartyText": "Davet reddedildi (zaten partide).", + "serverRestartingText": "Sunucu yeniden başlatılıyor. Lütfen birazdan tekrar girin...", + "serverShuttingDownText": "Sunucu kapatılıyor...", + "signInErrorText": "Giriş yapılırken Hata.", + "signInNoConnectionText": "Giriş yapılamıyor. (internet bağlantısı yok?)", + "telnetAccessDeniedText": "HATA: kullanıcı telnet erişim izni vermedi.", + "timeOutText": "(${TIME} saniye içinde süre dolacak)", + "touchScreenJoinWarningText": "DokunmatikEkran ile katıldın.\nEğer yanlışlıkla olduysa 'Menü->Oyundan Ayrıl' ile düzelt.", + "touchScreenText": "DokunmatikEkran", + "unableToResolveHostText": "Hata: kurucu çözümlenemiyor.", + "unavailableNoConnectionText": "Bu şu an kullanılamaz (internet bağlantısı yok?)", + "vrOrientationResetCardboardText": "Bunu kullanmak için VR oryantasyonunu sıfırla.\nOyunu oynamak için harici kontrolcüye ihtiyacın olacak.", + "vrOrientationResetText": "VR oryantasyonunu sıfırla.", + "willTimeOutText": "(boşta durursa süre dolacak)" + }, + "jumpBoldText": "ZIPLA", + "jumpText": "Zıpla", + "keepText": "Koru", + "keepTheseSettingsText": "Bu ayarları koru?", + "keyboardChangeInstructionsText": "Klavyeleri değiştirmek için iki defa boşluk tuşuna basın.", + "keyboardNoOthersAvailableText": "Başka klavye yok.", + "keyboardSwitchText": "Klavye \"${NAME}\"'a değiştiriliyor.", + "kickOccurredText": "${NAME} atıldı.", + "kickQuestionText": "${NAME} At ?", + "kickText": "At", + "kickVoteCantKickAdminsText": "Yöneticiler atılamaz.", + "kickVoteCantKickSelfText": "Kendini atamazsın.", + "kickVoteFailedNotEnoughVotersText": "Oylama için yeterli oyuncu yok.", + "kickVoteFailedText": "Atma-oylaması başarısız.", + "kickVoteStartedText": "${NAME} için oyundan atma oylaması başlatıldı.", + "kickVoteText": "Atmak için oyla", + "kickVotingDisabledText": "Atma-oylaması devre dışı.", + "kickWithChatText": "Evet için sohbete ${YES} Hayır için ${NO} yazın.", + "killsTallyText": "${COUNT} öldürme", + "killsText": "Öldürme", + "kioskWindow": { + "easyText": "Kolay", + "epicModeText": "Epik Mod", + "fullMenuText": "Tam Menü", + "hardText": "Zor", + "mediumText": "Orta", + "singlePlayerExamplesText": "Tek Oyuncu / Ekip Örnekleri", + "versusExamplesText": "Karşılaşma Örnekleri" + }, + "languageSetText": "Şuan dil \"${LANGUAGE}\".", + "lapNumberText": "Tur ${CURRENT}/${TOTAL}", + "lastGamesText": "(son ${COUNT} oyun)", + "leaderboardsText": "Lider Tahtası", + "league": { + "allTimeText": "Tüm Zamanlarda", + "currentSeasonText": "Şimdiki sezon (${NUMBER})", + "leagueFullText": "${NAME} Ligi", + "leagueRankText": "Lig Sıralaması", + "leagueText": "Lig", + "rankInLeagueText": "${NAME} Ligi${SUFFIX}, #${RANK}", + "seasonEndedDaysAgoText": "Sezon ${NUMBER} gün önce sonlandı.", + "seasonEndsDaysText": "Sezon ${NUMBER} gün içinde sonlanacak.", + "seasonEndsHoursText": "Sezon ${NUMBER} saat içinde sonlanacak.", + "seasonEndsMinutesText": "Sezon ${NUMBER} dakika içinde sonlanacak", + "seasonText": "Sezon ${NUMBER}", + "tournamentLeagueText": "Bu tunuvaya katılmak için ${NAME} olmalısın.", + "trophyCountsResetText": "Kupalar bir sonraki sezon sıfırlanacak." + }, + "levelBestScoresText": "${LEVEL} En-İyi Skorlar", + "levelBestTimesText": "${LEVEL} En-İyi Süreler", + "levelIsLockedText": "${LEVEL} kilitli.", + "levelMustBeCompletedFirstText": "Önce ${LEVEL} tamamlanmalı.", + "levelText": "Seviye ${NUMBER} oldu", + "levelUnlockedText": "Seviye Açıldı!", + "livesBonusText": "Bonus Canlar", + "loadingText": "yükleniyor", + "loadingTryAgainText": "Yükleniyor; Birazdan tekrar deneyin...", + "macControllerSubsystemBothText": "Her ikisi de (önerilmez)", + "macControllerSubsystemClassicText": "Klasik", + "macControllerSubsystemDescriptionText": "(eğer kontrolcülerin çalışmıyorsa bunu değiştirmeyi dene)", + "macControllerSubsystemMFiNoteText": "Made-for-iOS/Mac kontrolcü algılandı;\nAktifleştirmek istiyorsan Ayarlar -> Kontroller'e git.", + "macControllerSubsystemMFiText": "Made-for-iOS/Mac", + "macControllerSubsystemTitleText": "Kontrolcü Desteği", + "mainMenu": { + "creditsText": "Jenerik", + "demoMenuText": "Demo Menü", + "endGameText": "Oyunu Bitir", + "exitGameText": "Oyundan Çık", + "exitToMenuText": "Menüye Git?", + "howToPlayText": "Nasıl Oynanır", + "justPlayerText": "(Sadece ${NAME})", + "leaveGameText": "Oyundan Ayrıl", + "leavePartyConfirmText": "Gerçekten partiden ayrılmak istiyor musunuz?", + "leavePartyText": "Partiden Ayrıl", + "quitText": "Çık", + "resumeText": "Sürdür", + "settingsText": "Ayarlar" + }, + "makeItSoText": "Yap", + "mapSelectGetMoreMapsText": "Daha Fazla Harita Al...", + "mapSelectText": "Seç...", + "mapSelectTitleText": "${GAME} Haritaları", + "mapText": "Harita", + "maxConnectionsText": "Maksimum Bağlantı", + "maxPartySizeText": "Maksimum Parti Kapasitesi", + "maxPlayersText": "Maksimum Oyuncu", + "modeArcadeText": "Arcade Modu", + "modeClassicText": "Klasik Mod", + "modeDemoText": "Demo Modu", + "mostValuablePlayerText": "En Değerli Oyuncu", + "mostViolatedPlayerText": "En Çok Saldırılan Oyuncu", + "mostViolentPlayerText": "En Saldırgan Oyuncu", + "moveText": "Hareket", + "multiKillText": "${COUNT}-ÖLDÜRME!!!", + "multiPlayerCountText": "${COUNT} oyuncu", + "mustInviteFriendsText": "Not: Çok oyunculu oynamak için \"${GATHER}\"\npanelinden arkadaş davet etmelisin veya\nkontrolcü bağlamalısın.", + "nameBetrayedText": "${VICTIM}, ${NAME} tarafından ihanete uğradı.", + "nameDiedText": "${NAME} Öldü.", + "nameKilledText": "${VICTIM}, ${NAME} tarafından öldürüldü.", + "nameNotEmptyText": "Ad boş olamaz.", + "nameScoresText": "${NAME} Sayı Yaptı!", + "nameSuicideKidFriendlyText": "${NAME} kazara öldü.", + "nameSuicideText": "${NAME} intihar etti.", + "nameText": "İsim", + "nativeText": "Gerçek", + "newPersonalBestText": "Yeni kişisel rekor!", + "newTestBuildAvailableText": "Yeni bir test yapısı mevcut! (${VERSION} yapı ${BUILD}).\n${ADDRESS} buradan edinebilirsin", + "newText": "Yeni", + "newVersionAvailableText": "${APP_NAME} uygulamasının yeni bir sürümü mevcut! (${VERSION})", + "nextAchievementsText": "Sıradaki Başarılar:", + "nextLevelText": "Sıradaki Seviye", + "noAchievementsRemainingText": "- hiçbiri", + "noContinuesText": "(sürdürülemiyor)", + "noExternalStorageErrorText": "Bu cihazda harici depolama bulunamadı", + "noGameCircleText": "Hata: GameCircle girişi yapılamadı", + "noScoresYetText": "Henüz skor yok.", + "noThanksText": "Hayır Teşekkürler", + "noTournamentsInTestBuildText": "DİKKAT: Bu test sürümündeki turnuva skorları dikkate alınmayacaktır.", + "noValidMapsErrorText": "Bu oyun tipi için geçerli harita bulunamadı.", + "notEnoughPlayersRemainingText": "Yeterince oyuncu kalmadı; çık ve yeni bir oyun başlat.", + "notEnoughPlayersText": "Bu oyunu başlatmak için en az ${COUNT} oyuncuya ihtiyacın var!", + "notNowText": "Şimdi Değil", + "notSignedInErrorText": "Bunu yapmak için giriş yapmalısın.", + "notSignedInGooglePlayErrorText": "Bunu yapmak için Google Play ile giriş yapmalısın.", + "notSignedInText": "giriş yapılmadı", + "nothingIsSelectedErrorText": "Hiçbir şey seçilmedi!", + "numberText": "#${NUMBER}", + "offText": "Kapalı", + "okText": "Tamam", + "onText": "Açık", + "oneMomentText": "Bir dakika...", + "onslaughtRespawnText": "${PLAYER} oyuncusu ${WAVE}. dalgada yeniden doğacak", + "orText": "${A} veya ${B}", + "otherText": "Diğer...", + "outOfText": "(${ALL} için #${RANK})", + "ownFlagAtYourBaseWarning": "Skor için kendi bayrağın\nkendi bölgende olmalı!", + "packageModsEnabledErrorText": "Ağ-oyunu'na yerel-paket-modları etkinken izin verilmiyor (bak Ayarlar->Gelişmiş)", + "partyWindow": { + "chatMessageText": "Sohbet Mesajı", + "emptyText": "Partin Boş", + "hostText": "(kurucu)", + "sendText": "Gönder", + "titleText": "Partin" + }, + "pausedByHostText": "(kurucu tarafından durduruldu)", + "perfectWaveText": "Mükemmel Dalga!", + "pickUpText": "Kaldır", + "playModes": { + "coopText": "Ekip", + "freeForAllText": "Herkes-Tek", + "multiTeamText": "Çoklu-Takım", + "singlePlayerCoopText": "Tek Oyuncu / Ekip", + "teamsText": "Takımlar" + }, + "playText": "Oyna", + "playWindow": { + "oneToFourPlayersText": "1-4 oyuncu", + "titleText": "Oyna", + "twoToEightPlayersText": "2-8 oyuncu" + }, + "playerCountAbbreviatedText": "${COUNT}p", + "playerDelayedJoinText": "${PLAYER} bir sonraki round başladığında girecek.", + "playerInfoText": "Oyuncu Bilgisi", + "playerLeftText": "${PLAYER} oyunu terk etti.", + "playerLimitReachedText": "${COUNT} oyuncu limitine ulaşıldı; katılımcılar kabul edilmeyecek.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Kendi hesap profilini silemezsin.", + "deleteButtonText": "Profil\nSil", + "deleteConfirmText": "Sil '${PROFILE}' ?", + "editButtonText": "Profil\nDüzenle", + "explanationText": "(bu hesap için özel oyuncu adları ve görünümleri)", + "newButtonText": "Profil\nYarat", + "titleText": "Oyuncu Profilleri" + }, + "playerText": "Oyuncu", + "playlistNoValidGamesErrorText": "Bu ÇalmaListesi içeriği kilitli olmayan oyunlarda geçersiz.", + "playlistNotFoundText": "çalmaListesi bulunamadı", + "playlistText": "Çalma listesi", + "playlistsText": "ÇalmaListesi", + "pleaseRateText": "Eğer ${APP_NAME}'dan zevk aldıysan, lütfen oyunu değerlerdir\nve yorumunu yaz. Bu oyunun daha fazla geliştirilmesine\nyardımcı olur.\n\nteşekkürler!\n-eric", + "pleaseWaitText": "Lütfen bekle...", + "pluginsDetectedText": "Yeni eklenti(ler) tespit edildi. Bunları ayarlardan etkinleştirin / yapılandırın.", + "pluginsText": "Eklentiler", + "practiceText": "Alıştırma", + "pressAnyButtonPlayAgainText": "Tekrar oynamak için bir tuşa basın...", + "pressAnyButtonText": "Devam etmek için bir tuşa basın...", + "pressAnyButtonToJoinText": "Katılmak için bir tuşa basın...", + "pressAnyKeyButtonPlayAgainText": "Tekrar oynamak için herhangi bir tuş/butona basın...", + "pressAnyKeyButtonText": "Devam etmek için bir tuş/buton'a basın...", + "pressAnyKeyText": "Bir tuşa basın...", + "pressJumpToFlyText": "** Uçmak için defalarca zıplaya basın **", + "pressPunchToJoinText": "Katılmak için YUMRUK'a basın...", + "pressToOverrideCharacterText": "karakterini değiştirmek için ${BUTTONS}'ya bas", + "pressToSelectProfileText": "karakter seçmek için ${BUTTONS} bas.", + "pressToSelectTeamText": "takım seçmek için ${BUTTONS} bas.", + "promoCodeWindow": { + "codeText": "Kod", + "enterText": "Gir" + }, + "promoSubmitErrorText": "Kod gönderilirken hata oluştu; internet bağlantını kontrol et", + "ps3ControllersWindow": { + "macInstructionsText": "PS3'ünüzü kapatıp açın. Bluetooth'un Mac'inizde etkin\nolduğundan emin olun. Her ikisini eşlemek için kontrolcünüzü\nUSB kablosu ile Mac'inize bağlayın.Yaptığınızda kontrolcülerin\nhome butonuna basarak Kablolu(USB) ya da Kablosuz(Bluetooth)\nolarak bağlanabilirsiniz.\n\nBazı Macler eşlemek için şifre isterler. Eğer bu olursa;\nTakip eden öğreticiye bakın ya da google dan yardım alın.\n\n\n\n\nBağlı olan PS3 Kontrollerini aygıt listesinden görebilirsiniz;\nTercihler->Bluetooth. PS3'ünü tekrar kullanmak istiyorsan listeden\nkaldırmalısın.\n\nAyrıca kullanmadığın zaman pilinin bitmemesi için bağlantısının\nkesildiğinden emin ol.\n\nBluetooth çeşitli uzaklıklardan 7 tane bağlı cihaza\nkadar destekler", + "ouyaInstructionsText": "OUYA ile PS3 kotnroller kullanmak; basitçe eşlemek için USB kablosunu\nbağlayın. Bunu yapmak diğer kontrolcülerin bağlantısını kesebilir.\nOUYA yeniden başlatıp USB kablosunu çıkarabilirsin \n\nKontrolcüleri kablosuz bağlayıp kullanabilmek için HOME\nbutotnuna basmalısın. Oynadıktan sonra kontrolcüyü kapatmak\niçin HOME butonuna 10 saniye basılı tutmalısın. Bunu yapmamak\nbataryanı harcamana neden olur.", + "pairingTutorialText": "eşleme öğretici video", + "titleText": "PS3 kolu ile ${APP_NAME} kullan:" + }, + "punchBoldText": "YUMRUK", + "punchText": "yumruk", + "purchaseForText": "${PRICE} için Satın Al", + "purchaseGameText": "Oyun Satın Al", + "purchasingText": "Satın Alınıyor...", + "quitGameText": "Çık ${APP_NAME}?", + "quittingIn5SecondsText": "5 saniye içinde çıkılacak...", + "randomPlayerNamesText": "DEFAULT_NAMES", + "randomText": "Rastgele", + "rankText": "Sıralama", + "ratingText": "Derecelendirme", + "reachWave2Text": "Sıralama için dalga 2'ye ulaş.", + "readyText": "hazır", + "recentText": "En Son", + "remoteAppInfoShortText": "${APP_NAME} aile ve arkadaşların ile oynadığında daha eğlencelidir.\nBir veya daha fazla donanım kontrolcüsü bağla veya ${REMOTE_APP_NAME}\nuygulamasını telefon veya tablette kontrolcü olarak kullanmak\niçin indir.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Buton Pozisyonu", + "button_size": "Buton Boyutu", + "cant_resolve_host": "Sunucu Çözülemiyor.", + "capturing": "Yakalanıyor...", + "connected": "Bağlanıldı.", + "description": "Telefonunu veya tabletini BombSquad'da kontrolcü olarak kullan.\nTek seferde 8 oyuncuya kadar Destansı yerel çoklu oyuncu çılgınlığına TV veya tablet ile bağlanılabilir.", + "disconnected": "Sunucu bağlantısı kesildi.", + "dpad_fixed": "Sabit", + "dpad_floating": "Kaydırma", + "dpad_position": "D-Pad Posizyonu", + "dpad_size": "D-Pad Boyutu", + "dpad_type": "D-Pad Tipi", + "enter_an_address": "Bir Adres Gir", + "game_full": "Bu oyun dolu veya bağlantıları kabul etmiyor.", + "game_shut_down": "Oyun Kapalı", + "hardware_buttons": "Fiziksel Butonlar", + "join_by_address": "Adres ile Katıl...", + "lag": "Gecikme: ${SECONDS} saniye", + "reset": "Varsayılana Sıfırla", + "run1": "Koş 1", + "run2": "Koş 2", + "searching": "BombSquad oyunları araştırılıyor...", + "searching_caption": "Katılacağın oyunun adına tıkla.\nOyun ile aynı ağa bağlı olduğundan emin ol.", + "start": "Başlat", + "version_mismatch": "Versiyon uyuşmuyor.\nBombSquad ve BombSquad Remote'nin son \nsürüm olduğundan emin ol ve tekrar dene." + }, + "removeInGameAdsText": "Oyun-içi reklamları kaldırmak için \"${PRO}\" kilidini mağazadan aç.", + "renameText": "Yeniden Adlandır", + "replayEndText": "Tekrarı Kapat", + "replayNameDefaultText": "Son Oyun Tekrarı", + "replayReadErrorText": "Tekrar dosyası okunurken hata.", + "replayRenameWarningText": "Saklamak istiyorsan \"${REPLAY}\"nı yeniden adlandır; aksi halde üzerine yazılacak.", + "replayVersionErrorText": "Üzgünüz, Bu tekrar oyunun eski bir sürümünde\nyapılmış ve kullanılamaz.", + "replayWatchText": "Tekrar İzle", + "replayWriteErrorText": "Tekrar dosyası yazılırken hata.", + "replaysText": "Tekrarlar", + "reportPlayerExplanationText": "Hile, uygunsuz dil ya da diğer kötü şeyleri rapor etmek için bu E-Postayı kullanın.\nLütfen aşağıya belirtin:", + "reportThisPlayerCheatingText": "Hile", + "reportThisPlayerLanguageText": "Uygunsuz Dil", + "reportThisPlayerReasonText": "Ne raporu göndermek istersiniz?", + "reportThisPlayerText": "Bu Oyuncuyu Rapor Et", + "requestingText": "Talep Ediliyor...", + "restartText": "Yeniden Başlat", + "retryText": "Tekrar Dene", + "revertText": "Eski Haline Döndür", + "runText": "Koş", + "saveText": "Kaydet", + "scanScriptsErrorText": "Betikleri tararken hata oluştu; detaylar için logları kontrol edin.", + "scoreChallengesText": "Mücadele Skoru", + "scoreListUnavailableText": "Skor listesi mevcut değil.", + "scoreText": "Skor", + "scoreUnits": { + "millisecondsText": "Milisaniye", + "pointsText": "Puanlar", + "secondsText": "Saniye" + }, + "scoreWasText": "(${COUNT} idi)", + "selectText": "Seç", + "seriesWinLine1PlayerText": "SERİLERİ", + "seriesWinLine1TeamText": "SERİLERİ", + "seriesWinLine1Text": "SERİLERİ", + "seriesWinLine2Text": "KAZANDI!", + "settingsWindow": { + "accountText": "Hesap", + "advancedText": "Gelişmiş", + "audioText": "Ses", + "controllersText": "Kontroller", + "graphicsText": "Grafikler", + "playerProfilesMovedText": "Not: Oyuncu profilleri, ana menüdeki Hesaplar penceresine taşındı.", + "titleText": "Ayarlar" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(Basitçe; yazı yazmak için daha kullanışlı ekran klavyesi)", + "alwaysUseInternalKeyboardText": "Her zaman Dahili Klavye Kullan", + "benchmarksText": "Benchmark & Stres-Testleri", + "disableCameraGyroscopeMotionText": "Kamera Jiroskop Hareketini Devre Dışı Bırak", + "disableCameraShakeText": "Kamera Titremesini Devre Dışı Bırak", + "disableThisNotice": "(bu bildiriyi gelişmiş ayarlardan devre dışı bırakabilirsiniz)", + "enablePackageModsDescriptionText": "(ekstra modlama özelliği ekler fakat ağ-oyununu etkisiz kılar)", + "enablePackageModsText": "Yerel Paket Modlarını Etkinleştir", + "enterPromoCodeText": "Kodu gir", + "forTestingText": "Not: Bu değerler sadece test için ve uygulamadan çıkıldığında sıfırlanacak.", + "helpTranslateText": "${APP_NAME}'ın İngilizce olmayan çevirileri topluluk yardımı ile\nyapılabilir. Katkı veya düzgün çeviri yapmak istiyorsan aşağıdaki\nbağlantıya tıkla. Şimdiden teşekkürler!", + "kickIdlePlayersText": "Boştaki Oyuncuları Çıkar", + "kidFriendlyModeText": "Çoçuk Modu (Azaltılmış şiddet, vb)", + "languageText": "Dil", + "moddingGuideText": "Modlama Rehberi", + "mustRestartText": "Etkisini göstermesi için oyunu yeniden başlatmalısınız.", + "netTestingText": "Ağ Testi", + "resetText": "Sıfırla", + "showBombTrajectoriesText": "Bomba Gidişatını Göster", + "showPlayerNamesText": "Oyuncu Adlarını Göster", + "showUserModsText": "Mod Klasörünü Göster", + "titleText": "Gelişmiş", + "translationEditorButtonText": "${APP_NAME} Çeviri Editörü", + "translationFetchErrorText": "çeviri durumu mevcut değil", + "translationFetchingStatusText": "Çeviri durumu kontrol ediliyor...", + "translationInformMe": "Benim dilimin güncellenmesi gerektiğinde bana haber ver", + "translationNoUpdateNeededText": "Şu anki dil güncel; wohooo!", + "translationUpdateNeededText": "** Şu anki dilin güncellenmeye ihtiyacı var!! **", + "vrTestingText": "VR Testi" + }, + "shareText": "Paylaş", + "sharingText": "Paylaşılıyor...", + "showText": "Göster", + "signInForPromoCodeText": "Kodlar etkisini göstermesi için bir hesap ile giriş yapmalısın.", + "signInWithGameCenterText": "Game Center hesabı kullanmak için\nGame Center uygulaması ile giriş yap.", + "singleGamePlaylistNameText": "Sadece ${GAME}", + "singlePlayerCountText": "1 oyuncu", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Karakter Seçimi", + "Chosen One": "Seçilmiş Kişi", + "Epic": "Epik Mod Oyunları", + "Epic Race": "Epik Yarış", + "FlagCatcher": "Bayrak Kapmaca", + "Flying": "Düşler Vadisi", + "Football": "Futbol", + "ForwardMarch": "Hücum", + "GrandRomp": "İşgal", + "Hockey": "Hokey", + "Keep Away": "Uzak Dur", + "Marching": "Dolambaç", + "Menu": "Ana Menü", + "Onslaught": "Saldırı", + "Race": "Yarış", + "Scary": "Tepelerin Hakimi", + "Scores": "Skor Ekranı", + "Survival": "Eleme", + "ToTheDeath": "Ölüm Müsabakası", + "Victory": "Final Skoru Ekranı" + }, + "spaceKeyText": "boşluk", + "statsText": "İstatistikler", + "storagePermissionAccessText": "Bu depolama erişimine ihtiyaç duyuyor", + "store": { + "alreadyOwnText": "${NAME} zaten sende var!", + "bombSquadProDescriptionText": "• 2 kat başarı ödülü bileti\n• Oyun-içi reklamlar kalkar\n• ${COUNT} bonus bilet içerir\n• +%${PERCENT} lig skoru bonusu\n• '${INF_ONSLAUGHT}' kilidi ve \n '${INF_RUNAROUND}' kilidi açılır\n ", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "•Oyun-içi reklam ve rahatsız edici ekranlar kalkar\n•Daha fazla oyun ayarları açılır\n•Ayrıca şunları içerir:", + "buyText": "Satın Al", + "charactersText": "Karakterler", + "comingSoonText": "Çok Yakında...", + "extrasText": "Ekstralar", + "freeBombSquadProText": "BombSquad şuan bedava fakat orijinal olarak oyunu satın aldığın zaman\nBombSquad Pro yükseltmesi ve bir teşekkür olarak ${COUNT} bilet alırsın.\nYeni özelliklerin keyfini çıkar, ve desteğin için teşekkürler!\n-Eric", + "holidaySpecialText": "Bayram Özel", + "howToSwitchCharactersText": "(karakter atamak ve özelleştirmek için, git: \"${SETTINGS} -> ${PLAYER_PROFILES}\")", + "howToUseIconsText": "(bunu kullanmak için global profil yarat(hesaplar penceresinde))", + "howToUseMapsText": "(bu haritayı takımlar/herkes-tek çalmalistelerinde kullan)", + "iconsText": "Simgeler", + "loadErrorText": "Sayfa yüklenemiyor.\nİnternet bağlantınızı kontrol edin.", + "loadingText": "yükleniyor", + "mapsText": "Haritalar", + "miniGamesText": "MiniOyunlar", + "oneTimeOnlyText": "(tek seferlik)", + "purchaseAlreadyInProgressText": "Bu ögeyi satın alım zaten işlemde.", + "purchaseConfirmText": "${ITEM} Satın Al?", + "purchaseNotValidError": "Satın Alma geçersiz.\nBu bir hata ise ${EMAIL} ile iletişime geç.", + "purchaseText": "Satın Al", + "saleBundleText": "Yığınla İndirim!", + "saleExclaimText": "İndirim!", + "salePercentText": "(%${PERCENT} ucuz)", + "saleText": "İNDİRİM", + "searchText": "Arama", + "teamsFreeForAllGamesText": "Takımlar / Herkes-Tek Oyunları", + "totalWorthText": "*** ${TOTAL_WORTH} değerinde! ***", + "upgradeQuestionText": "Yükselt?", + "winterSpecialText": "Kışa Özel", + "youOwnThisText": "- buna sahipsin -" + }, + "storeDescriptionText": "8 Kişilik Parti Oyunu Çılgınlığı!\n\nArkadaşlarını (ya da bilgisayarı) turnuva içindeki mini-oyunlar olan (Bayrak-Kapmaca, Bombacı-Hokeyi ve Epik-AğırÇekim-Ölüm-Müsabakası) ile çılgına döndür!\n\n8 kişiye kadar basit ve yaygın olan kontrolcüler dekteklenir. Ayrıca bedava 'BombSquad Remote' uygulaması ile de mobil cihazlarını kontrolcü olarak kullanabilirsin!\n\nBombayı Patlat!\n\nDaha fazla bilgi için; www.froemling.net/bombsquad", + "storeDescriptions": { + "blowUpYourFriendsText": "Arkadaşlarını tahrip et.", + "competeInMiniGamesText": "mini-oyunlar'ı yarış'tan uçma'ya kadar tamamla.", + "customize2Text": "Özel karakterler, mini-oyunlar ve tabiki müzik.", + "customizeText": "Karakterini özelleştir ve kendi mini-oyun çalmaListeni yarat.", + "sportsMoreFunText": "Sporlar patlayıcılar ile daha eğlencelidir.", + "teamUpAgainstComputerText": "Bilgisayara karşı takım ol." + }, + "storeText": "Mağaza", + "submitText": "Gönder", + "submittingPromoCodeText": "Kod Gönderiliyor...", + "teamNamesColorText": "Takım isimleri/Renkler...", + "telnetAccessGrantedText": "Telnet Erişimi aktif.", + "telnetAccessText": "Telnet erişimi algılandı; izin ver?", + "testBuildErrorText": "Bu test yapısı uzun süredir aktif değil;yeni sürümü kontrol edin.", + "testBuildText": "Test Yapısı", + "testBuildValidateErrorText": "Test yapısı onaylanamıyor. (ağ bağlantısı yok?)", + "testBuildValidatedText": "Test Yapısı Onaylandı; Keyfinı Çıkar!", + "thankYouText": "Desteğin için teşekkürler! Oyunun tadını çıkar!!", + "threeKillText": "ÜÇLÜ ÖLÜM!!", + "timeBonusText": "Süre Bonusu", + "timeElapsedText": "Geçen Süre", + "timeExpiredText": "Süre Doldu", + "timeSuffixDaysText": "${COUNT}g", + "timeSuffixHoursText": "${COUNT}s", + "timeSuffixMinutesText": "${COUNT}d", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "İpucu", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "En-İyi Arkadaşlar", + "tournamentCheckingStateText": "Turnuva durumu denetleniyor; lütfen bekleyin...", + "tournamentEndedText": "Bu turnuva sonlandı. Yeni bir tanesi yakında başlayacak.", + "tournamentEntryText": "Turnuva Katılımı", + "tournamentResultsRecentText": "Son Turnıva Sonuçları", + "tournamentStandingsText": "Turnuva Kazananları", + "tournamentText": "Turnuva", + "tournamentTimeExpiredText": "Turnuva Sona Erdi", + "tournamentsText": "Turnuvalar", + "translations": { + "characterNames": { + "Agent Johnson": "Ajan Johnson", + "B-9000": "B-9000", + "Bernard": "Bay Pofuduk", + "Bones": "Kemik Torbası", + "Butch": "Butch", + "Easter Bunny": "Paskalya Tavşanı", + "Flopsy": "Tavşik", + "Frosty": "Donak", + "Gretel": "Gretel", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Lucky", + "Mel": "Mel", + "Middle-Man": "Şaşkın-Adam", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Noel Baba", + "Snake Shadow": "Sinsi Gölge", + "Spaz": "Spaz", + "Taobao Mascot": "Toabao Mascot", + "Todd McBurton": "Todd McBurton", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} Alıştırması", + "Infinite ${GAME}": "Sonsuz ${GAME}", + "Infinite Onslaught": "Sonsuz Saldırı", + "Infinite Runaround": "Sonsuz Dolambaç", + "Onslaught Training": "Saldırı Alıştırması", + "Pro ${GAME}": "Pro ${GAME}", + "Pro Football": "Pro Futbol", + "Pro Onslaught": "Pro Saldırı", + "Pro Runaround": "Pro Dolambaç", + "Rookie ${GAME}": "Acemi ${GAME}", + "Rookie Football": "Acemi Futbol", + "Rookie Onslaught": "Acemi Saldırı", + "The Last Stand": "Son Çırpınış", + "Uber ${GAME}": "Üst-Düzey ${GAME}", + "Uber Football": "Üst-Düzey Futbol", + "Uber Onslaught": "Üst-Düzey Saldırı", + "Uber Runaround": "Üst-Düzey Dolambaç" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Kazanmak için uzun süre seçilmiş kişi ol.\nseçilmiş kişi olmak için seçilmiş kişiyi öldür.", + "Bomb as many targets as you can.": "Yapabildiğin kadar hedefi bombala.", + "Carry the flag for ${ARG1} seconds.": "Bayrağı ${ARG1} saniye taşı.", + "Carry the flag for a set length of time.": "Bayrağı en uzun süre taşı.", + "Crush ${ARG1} of your enemies.": "${ARG1} düşmanı ezip geç.", + "Defeat all enemies.": "Tüm düşmanları Mağlup Et.", + "Dodge the falling bombs.": "Düşen bombalardan kaç.", + "Final glorious epic slow motion battle to the death.": "Ölümüne son büyük epik ağır çekim savaşı.", + "Gather eggs!": "Yumurtaları Topla!", + "Get the flag to the enemy end zone.": "Bölgeden ve düşmandan bayrağı al.", + "How fast can you defeat the ninjas?": "Ninjaları ne kadar hızlı mağlup edebilirsin?", + "Kill a set number of enemies to win.": "Kazanmak için öldürülecek düşman sayısını ayarla.", + "Last one standing wins.": "Son kalan kazanır.", + "Last remaining alive wins.": "En son hayatta kalan kazanır.", + "Last team standing wins.": "Sona kalan takım kazanır.", + "Prevent enemies from reaching the exit.": "Düşmanların çıkışa ulaşmalarına engel ol.", + "Reach the enemy flag to score.": "Skor için düşman bayrağına ulaş.", + "Return the enemy flag to score.": "Skor için düşman bayrağını getir.", + "Run ${ARG1} laps.": "${ARG1} tur koş.", + "Run ${ARG1} laps. Your entire team has to finish.": "${ARG1} tur koş. Tüm takım için geçerli.", + "Run 1 lap.": "1 Tur koş.", + "Run 1 lap. Your entire team has to finish.": "1 Tur koş. Tüm takım için geçerli.", + "Run real fast!": "Cidden Hızlı Koş!", + "Score ${ARG1} goals.": "${ARG1} Gol At.", + "Score ${ARG1} touchdowns.": "${ARG1} Sayı Yap.", + "Score a goal.": "Gol At.", + "Score a touchdown.": "Bir sayı yap.", + "Score some goals.": "Birkaç gol at.", + "Secure all ${ARG1} flags.": "${ARG1} bayrağın tümünü ele geçir.", + "Secure all flags on the map to win.": "Kazanmak için haritadaki tüm bayrakları ele geçir.", + "Secure the flag for ${ARG1} seconds.": "Bayrağı ${ARG1} saniye ele geçir.", + "Secure the flag for a set length of time.": "Bayrağı ayarlanmış süre boyunca ele geçir.", + "Steal the enemy flag ${ARG1} times.": "Düşman bayrağını ${ARG1} kere çal.", + "Steal the enemy flag.": "Düşman bayrağını çal.", + "There can be only one.": "Sadece biri kalabilir.", + "Touch the enemy flag ${ARG1} times.": "Düşman bayrağına ${ARG1} kere dokun.", + "Touch the enemy flag.": "Düşman bayrağına dokun.", + "carry the flag for ${ARG1} seconds": "bayrağı ${ARG1} saniye taşı", + "kill ${ARG1} enemies": "${ARG1} düşman öldür", + "last one standing wins": "sona kalan kazanır", + "last team standing wins": "sona kalan takım kazanır", + "return ${ARG1} flags": "${ARG1} bayrak getir", + "return 1 flag": "1 bayrak getir", + "run ${ARG1} laps": "${ARG1} tur koş", + "run 1 lap": "1 tur koş", + "score ${ARG1} goals": "${ARG1} gol at", + "score ${ARG1} touchdowns": "${ARG1} sayı yap", + "score a goal": "gol at", + "score a touchdown": "bir sayı yap", + "secure all ${ARG1} flags": "${ARG1} bayrağın tümünü ele geçir", + "secure the flag for ${ARG1} seconds": "Bayrağı ${ARG1} saniye ele geçir", + "touch ${ARG1} flags": "${ARG1} bayrağa dokun", + "touch 1 flag": "1 bayrağa dokun" + }, + "gameNames": { + "Assault": "Hücum", + "Capture the Flag": "Bayrak Kapmaca", + "Chosen One": "Seçilmiş Kişi", + "Conquest": "İşgal", + "Death Match": "Ölüm Müsabakası", + "Easter Egg Hunt": "Paskalya Yumurtası Avı", + "Elimination": "Eleme", + "Football": "Futbol", + "Hockey": "Hokey", + "Keep Away": "Uzak Dur", + "King of the Hill": "Tepelerin Hakimi", + "Meteor Shower": "Meteor Yağmuru", + "Ninja Fight": "Ninja Kavgası", + "Onslaught": "Saldırı", + "Race": "Yarış", + "Runaround": "Dolambaç", + "Target Practice": "Hedef Alıştırması", + "The Last Stand": "Son Çırpınış" + }, + "inputDeviceNames": { + "Keyboard": "Klavye", + "Keyboard P2": "P2 Klavye" + }, + "languages": { + "Arabic": "Arapça", + "Belarussian": "Beyazrusça", + "Chinese": "Basitleştirilmiş Çince", + "ChineseTraditional": "Geleneksel Çince", + "Croatian": "Hırvatça", + "Czech": "Çekçe", + "Danish": "Danimarkaca", + "Dutch": "Flemenkçe", + "English": "İngilizce", + "Esperanto": "Esperanto", + "Finnish": "Fince", + "French": "Fransızca", + "German": "Almanca", + "Gibberish": "Abuk Sabukça", + "Greek": "Yunan", + "Hindi": "Hintçe", + "Hungarian": "Macarca", + "Indonesian": "Endonezyaca", + "Italian": "İtalyanca", + "Japanese": "Japonca", + "Korean": "Korece", + "Persian": "Farsça", + "Polish": "Polonya Dili", + "Portuguese": "Portekizce", + "Romanian": "Romence", + "Russian": "Rusça", + "Serbian": "Sırpça", + "Slovak": "Slovakça", + "Spanish": "İspanyolca", + "Swedish": "İsveççe", + "Turkish": "Türkçe", + "Ukrainian": "Ukrayna", + "Venetian": "Venedik", + "Vietnamese": "Vietnamca" + }, + "leagueNames": { + "Bronze": "Bronz", + "Diamond": "Elmas", + "Gold": "Altın", + "Silver": "Gümüş" + }, + "mapsNames": { + "Big G": "Büyük G", + "Bridgit": "Köprü", + "Courtyard": "Avlu", + "Crag Castle": "Uçurum Kalesi", + "Doom Shroom": "Lanetli Mantar", + "Football Stadium": "Futbol Stadyumu", + "Happy Thoughts": "Düşler Vadisi", + "Hockey Stadium": "Hokey Stadyumu", + "Lake Frigid": "Donmuş Göl", + "Monkey Face": "Maymun Surat", + "Rampage": "Katliam", + "Roundabout": "Dönemeç", + "Step Right Up": "Uzaktan Bakma Yakına Gel", + "The Pad": "Kutu", + "Tip Top": "Zıp Zıp", + "Tower D": "Kule D", + "Zigzag": "Zikzak" + }, + "playlistNames": { + "Just Epic": "Sadece Epik", + "Just Sports": "Sadece Sporlar" + }, + "scoreNames": { + "Flags": "Bayraklar", + "Goals": "Goller", + "Score": "Skor", + "Survived": "Hayatta Kalındı", + "Time": "Süre", + "Time Held": "Tutulmuş Süre" + }, + "serverResponses": { + "A code has already been used on this account.": "Kod bu hesapta zaten kullanılmış.", + "A reward has already been given for that address.": "Ödül bu adres için zaten verilmiş.", + "Account linking successful!": "Hesap bağlama başarılı!", + "Account unlinking successful!": "Hesap bağlantısı başarıyla kesildi!", + "Accounts are already linked.": "Hesaplar zaten bağlı.", + "An error has occurred; (${ERROR})": "Bir hata oluştu; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Bir hata oluştu; Lütfen destek ile iletişime geçin. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Bir hata meydana geldi; lütfen support@froemling.net ile iletişime geçin.", + "An error has occurred; please try again later.": "Bir hata meydana geldi; Lütfen sonra tekrar deneyin.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Bu hesapları bağlamak istediğine emin misin?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nBu işlem geri alınamaz!", + "BombSquad Pro unlocked!": "BombSquad Pro Kilidi Açıldı!", + "Can't link 2 accounts of this type.": "Bu tipte 2 hesap bağlanamaz.", + "Can't link 2 diamond league accounts.": "2 elmas ligi hesabı birleştirilemez.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Bağlanamıyor; maksimum ${COUNT} bağlı hesap sınırına ulaşıldı.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Hile algılandı; skorlar ve ödüller ${COUNT} gün için askıya alındı.", + "Could not establish a secure connection.": "Güvenli bir bağlantı sağlanamadı.", + "Daily maximum reached.": "Günlük sınıra ulaşıldı.", + "Entering tournament...": "Turnuvaya giriliyor...", + "Invalid code.": "Geçersiz kod.", + "Invalid payment; purchase canceled.": "Geçersiz ödeme; satın alma iptal edildi.", + "Invalid promo code.": "Geçersiz promo kodu.", + "Invalid purchase.": "Geçersiz satın alma.", + "Invalid tournament entry; score will be ignored.": "Geçersiz turnuva girişi; skor yok sayılacak.", + "Item unlocked!": "Öğe kilidi açıldı!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "EŞLEŞTİRME ENGELLENDİ ${ACCOUNT}\nhesabının önemli verilerinin tümü kayıp olacak!\nKarşı taraftan eşleştirme yapabilirsiniz\n(Ve verileriniz kayıp olmaz)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "${ACCOUNT} hesabı bu hesaba bağlansın mı?\n${ACCOUNT} hesabı üzerindeki tüm mevcut veriler kaybolacak.\nBu işlem geri alınamaz. Emin misiniz?", + "Max number of playlists reached.": "Maksimum çalmaListesine ulaşıldı.", + "Max number of profiles reached.": "Profiller için maksimum sayıya ulaşıldı.", + "Maximum friend code rewards reached.": "Arkadaş kodu maksimuma ulaştı.", + "Message is too long.": "Mesaj çok uzun.", + "Profile \"${NAME}\" upgraded successfully.": "\"${NAME}\" Profili başarı ile yükseltildi.", + "Profile could not be upgraded.": "Profil yükseltilemedi.", + "Purchase successful!": "Satın Alma Başarılı!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "${COUNT} Bilet giriş yapıldığı için alındı.\nYarın geri gel ve ${TOMORROW_COUNT} bilet al.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Oyunun bu sürümünde sunucu işlevleri artık desteklenmiyor;\nLütfen daha yeni bir sürüme güncelleyin.", + "Sorry, there are no uses remaining on this code.": "Üzgünüz, bu kodun kullanım bakiyesi doldu.", + "Sorry, this code has already been used.": "Üzgünüz, bu kod zaten kullanılmış.", + "Sorry, this code has expired.": "Üzgünüz, Bu kodun süresi doldu.", + "Sorry, this code only works for new accounts.": "Üzgünüz, bu kod yalnızca yeni hesaplar için çalışır.", + "Temporarily unavailable; please try again later.": "Geçici olarak kullanım dışı; lütfen daha sonra tekrar deneyiniz.", + "The tournament ended before you finished.": "Sen bitiremeden turnuva sona erdi.", + "This account cannot be unlinked for ${NUM} days.": "Bu hesap, ${NUM} gün boyunca kaldırılamaz.", + "This code cannot be used on the account that created it.": "Bu kod onun yaptığı hesapta kullanılamaz.", + "This is currently unavailable; please try again later.": "Bu şu anda kullanılamıyor; lütfen daha sonra tekrar deneyin.", + "This requires version ${VERSION} or newer.": "Bu ${VERSION} veya daha üst sürümüne ihtiyaç duyuyor.", + "Tournaments disabled due to rooted device.": "Rootlu cihaz nedeniyle turnuvalar devre dışı", + "Tournaments require ${VERSION} or newer": "Turnuvalar ${VERSION} veya daha yeni bir sürümü gerektirir", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "${ACCOUNT} hesabının bağlantısını kaldırmak mı istiyorsunuz?\n${ACCOUNT} hesabı üzerindeki tüm veriler sıfırlanacak.\n(bazı durumlarda başarılar hariç)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "UYARI: Hesabınız hile kullanımı nedeniyle şikayet edilmiştir.\nHile kullanıldığı tespit edilen hesaplar yasaklanacaktır. Lütfen adil oyna.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Cihaz hesabınızı bununla bağlamak istermisiniz?\n\nCihaz hesabınız: ${ACCOUNT1}\nBu hesap: ${ACCOUNT2}\n\nUlaştığın ilerlemeler saklanacak.\nUyarı: Bu işlem geri alınamaz!", + "You already own this!": "Buna zaten sahipsin!", + "You can join in ${COUNT} seconds.": "\"${COUNT}\" saniye içinde katılabilirsin.", + "You don't have enough tickets for this!": "Bunun için yeterince biletiniz yok!", + "You don't own that.": "Buna sahip değilsin.", + "You got ${COUNT} tickets!": "${COUNT} Bilet kazandınız!", + "You got a ${ITEM}!": "Bir ${ITEM} kazandınız!", + "You have been promoted to a new league; congratulations!": "Yeni bir lige terfi ettin; tebrikler!", + "You must update to a newer version of the app to do this.": "Bunu yapmak için uygulamayı yeni bir sürüme güncellemelisin.", + "You must update to the newest version of the game to do this.": "Bunu yapmak için oyunun en yeni sürümüne güncellemelisiniz.", + "You must wait a few seconds before entering a new code.": "Yeni bir kod girmeden önce biraz beklemelisin.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Son turnuvada #${RANK}. oldun. Oynadığın için teşekkürler!", + "Your account was rejected. Are you signed in?": "Hesabın reddedildi. Oturum açtın mı?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Oyununun kopyası modifiye edilmiş.\nLütfen eski haline döndürüp tekrar deneyin.", + "Your friend code was used by ${ACCOUNT}": "Arkadaş kodun ${ACCOUNT} tarafından kullanılmış" + }, + "settingNames": { + "1 Minute": "1 Dakika", + "1 Second": "1 Saniye", + "10 Minutes": "10 Dakika", + "2 Minutes": "2 Dakika", + "2 Seconds": "2 Saniye", + "20 Minutes": "20 Dakika", + "4 Seconds": "4 Saniye", + "5 Minutes": "5 Dakika", + "8 Seconds": "8 Saniye", + "Allow Negative Scores": "Negatif Skorlara İzin Ver", + "Balance Total Lives": "Toplam Canları Dengele", + "Bomb Spawning": "Bomba Oluşumu", + "Chosen One Gets Gloves": "Seçilmiş Kişi Eldivenler Alsın", + "Chosen One Gets Shield": "Seçilmiş Kişi Kalkan Alsın", + "Chosen One Time": "Seçilmiş Kişi Süresi", + "Enable Impact Bombs": "Bombalara Çarpmayı Etkinleştir", + "Enable Triple Bombs": "Üçlü Bombaları Etkinleştir", + "Entire Team Must Finish": "Tüm Takım Bitirmeli", + "Epic Mode": "Epik Mod", + "Flag Idle Return Time": "Boştaki Bayrağın Geri Dönme Süresi", + "Flag Touch Return Time": "Dokunulan Bayrağın Geri Dönme Süresi", + "Hold Time": "Tutma Süresi", + "Kills to Win Per Player": "Kazanmak için Öldürülecek Oyuncu", + "Laps": "Turlar", + "Lives Per Player": "Her Oyuncunun Canı", + "Long": "Uzun", + "Longer": "Daha Uzun", + "Mine Spawning": "Mayın Oluşumu", + "No Mines": "Mayın Yok", + "None": "Hiçbiri", + "Normal": "Normal", + "Pro Mode": "Pro Mod", + "Respawn Times": "Yeniden Doğma Süresi", + "Score to Win": "Kazanmak için gereken Skor", + "Short": "Kısa", + "Shorter": "Daha Kısa", + "Solo Mode": "Tek Kişilik Mod", + "Target Count": "Hedef Miktarı", + "Time Limit": "Süre Sınırı" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "\"${PLAYER}\" ayrıldığından \"${TEAM}\" diskalifiye oldu.", + "Killing ${NAME} for skipping part of the track!": "${NAME} oyuncusu yoldan saptığı için öldürüldü!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "${NAME}: için uyarı: Hızlı/gereksiz buton tıklaması seni mahveder." + }, + "teamNames": { + "Bad Guys": "Haylazlar", + "Blue": "Mavi", + "Good Guys": "Kankalar", + "Red": "Kırmızı" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "koşmak-zıplamak-dönmek-vurmak Mükemmel zamanlama ile tek vuruşta öldürür\nve arkadaşlarının hayat boyu sana saygı duymasını sağlar.", + "Always remember to floss.": "Diş iplerini yanına almayı hiçbir zaman unutma.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Rastgele bir tane yerine, tercih ettiğiniz ve\ngörülmesini istediğiniz ad için oyuncu profili yaratın.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Lanet kutuları seni saatli bombaya dönüştürür.\nTek kurtuluş yolu Sağlık-Paketi almaktır.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Görünüşlerinin yanı sıra, tüm karakterlerin yetenekleri aynıdır.\nYani sana en çok benzeyen bir tanesini seç.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Enerji-Kalkanı ileyken kendinden çok emin olma; kendini uçurumdan düşürebilirsin.", + "Don't run all the time. Really. You will fall off cliffs.": "Her zaman koşma. Cidden. Uçurumlardan düşebilirsin.", + "Don't spin for too long; you'll become dizzy and fall.": "Çok uzun süre boyunca dönme; sersemleyip düşersin.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Koşmak için bir tuşa basılı tut. (Eğer varsa tetik butonları da çalışır)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Koşmak için herhangi bir tuşa basılı tut. Hızlıca yerdeğiştirmeni\nsağlar fakat dönmeni zorlaştırır, yani uçurumlara dikkat et.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Buz-Bombaları çok güçlü değildir fakat değdiği şeyi\ndondurur ve kırılmalarını kolaylaştırır.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Birisi seni kaldırırsa, yumrukla ki seni salıversin.\nBu gerçek hayatta da işe yarar.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Eğer kontrolcü için aceleciysen; '${REMOTE_APP_NAME}'\nuygulamasını yükle ve mobil cihazını kontrolcü olarak kullan.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Eğer yapışkan-bomba sana yapıştıysa, etrafta zıpla ve daireler çizerek dön\nbombayı üzerinden atabilirsin. Ya da hiçbir şey yapma, son anların eğlenceli geçsin.", + "If you kill an enemy in one hit you get double points for it.": "Eğer düşmanını tek vuruşta öldürürsen, bunun için çifte puan alırsın.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Eğer Lanetlenirsen, Uğruna savaşılacak tek umudun;\nilerleyen saniyelerde bir sağlık-paketi bulmak olur.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Bir yerde durursan, kızarmış ekmek olursun. Hayatta kalmak için koş ve kaç..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Eğer girip çıkan birçok oyuncun varsa; oyundan çıkmayı unutma ihtimallerine karşı\nayarlardaki 'Boştaki-oyuncuları-çıkar'ı aktifleştir.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Eğer cihazın çok ısındıysa veya güç tasarrufu yapmak istiyorsan\nAyarlar->Grafikler den \"Görseller\" veya \"Çözünürlük\" ayarlarını düşür.", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Eğer FPS dengesiz ise; Oyunun grafik ayarlarından görseller\nveya çözünürlüğü düşürmeyi dene.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "Bayrak-Yakalamaca'da skor yapmak için kendi bayrağın kendi bölgende olmalı. Eğer diğer\ntakım skor yapmak üzereyse; bayraklarını çalmak, onları durdurmak için güzel bir yoldur.", + "In hockey, you'll maintain more speed if you turn gradually.": "Hokey de, yavaşça dönersen topu daha hızlı sürebilirsin.", + "It's easier to win with a friend or two helping.": "Bir arkadaşla yardımlaşarak oynamak, kolayca kazanmanı sağlar.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Zıplamak, bombaları daha yüksek seviyelere atmanı sağlar.", + "Land-mines are a good way to stop speedy enemies.": "Kara-mayınları hızlı düşmanları durdurmak için iyidir.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Çoğu şey kaldırılıp atılabilir, diğer oyuncular da dahil. Düşmanlarını\nuçurumdan fırlatmak, etkili ve seni tatmin edecek bir yöntemdir.", + "No, you can't get up on the ledge. You have to throw bombs.": "Hayır, tümseğe çıkamazsın. Atabileceğin bombaların var.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Oyuncular çoğu oyunun ortasında girip çıkabilir. Ayrıca\nhızlıca kontrolcü bağlayıp da çıkarabilirsin.", + "Practice using your momentum to throw bombs more accurately.": "İvmeni kullanma alıştırması yapman, bombaları daha isabetli atmanı sağlar", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Hızlı hareket edersen yumrukların daha fazla hasar verir\nyani deli gibi koşmayı, zıplamayı ve dönmeyi dene.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Daha uzağa atmak için bomba atmadan önce kırbaç gibi\ngeri-ileri hareket et.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Grupça düşman imha etmek için\nbombayı TNT kutusunun yanına at.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Kafa en zayıf bölgedir, bu yüzden yapışkan-bombayı\nkafaya atman oyun-bitirici bir hamledir.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Bu bölüm hiç bitmez. Fakat yaptığın yüksek-skorlar\ndünyanın sonuna kadar mevcut kalır.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Fırlatma kuvveti senin yöneliminle alakalıdır.\nBir şeyleri nazikçe önüne bırakmak için herhangi bir yöne gitmeden bırak.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Müziklerden sıkıldın mı? Kendininkiyle değiştir!\nAyarlar->Müzikler'e bak", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Bombaları, patlamasına bir ya da iki saniye kala fırlat.", + "Try tricking enemies into killing eachother or running off cliffs.": "Düşmanlarının kandırarak birbirlerini öldürmesini ve uçurumdan fırlatmasını sağla.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Bayrak tutmak için Kaldır butonunu kullan <${PICKUP}>", + "Whip back and forth to get more distance on your throws..": "Geri-ileri gitmek daha uzağa fırlatmanı sağlar.", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Yumruklarınla sağa veya sola dönerken hedef alabilirsin.\nBu haylazları köşelere fırlatmak ve hokeyde skor yapmak için birebirdir.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Bombanın patlama zamanını Fitil kıvılcımlarının rengine bakarak\ntahmin edebilirsin: sarı..turuncu..kırmızı..BOOM.", + "You can throw bombs higher if you jump just before throwing.": "Bombaları atmadan önce zıplarsan, daha yükseğe fırlatabilirsin.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Kafanı bir şeylere çarparsan hasar alırsın\nkafanı bir şeylere çarpmamaya özen göster.", + "Your punches do much more damage if you are running or spinning.": "Dönüyor ve koşuyorsan, yumrukların daha fazla hasar verir." + } + }, + "trophiesRequiredText": "Bunun için en az ${NUMBER} kupa gerekiyor.", + "trophiesText": "Kupa", + "trophiesThisSeasonText": "Bu Sezonki Kupalar", + "tutorial": { + "cpuBenchmarkText": "Öğretici aptalca çalışıyor (öncellikle CPU hızını test et)", + "phrase01Text": "Merhaba !", + "phrase02Text": "${APP_NAME}'a Hoş Geldiniz!", + "phrase03Text": "Karakterini kontrol etmek için birkaç ipucu:", + "phrase04Text": "${APP_NAME}'da çoğu şey FİZİK temellidir.", + "phrase05Text": "Örnek; yumruk attığında...", + "phrase06Text": "...hasar yumruklarının hızına bağlıdır.", + "phrase07Text": "Gördün mü? Hareket etmediğinden ${NAME} zar zor incildi.", + "phrase08Text": "Şimdi daha fazla hızlanmak için zıpla ve Dön.", + "phrase09Text": "Ah, bu daha iyi.", + "phrase10Text": "Koşmak da yardımcı olur.", + "phrase11Text": "Koşmak için herhangi bir butona basılı tut.", + "phrase12Text": "Ekstra-Müthiş yumruklar için koşmayı ve dönmeyi dene.", + "phrase13Text": "Ayy! bunun için üzgünüm ${NAME}.", + "phrase14Text": "Bir şeyleri kaldırıp fırlatabilirsin. Bayraklar gibi.. ya da ${NAME}.", + "phrase15Text": "Son olarak, bombalar var.", + "phrase16Text": "Bombaları fırlatmak, tecrübe işidir.", + "phrase17Text": "Ayy! İyi bir fırlatış değil.", + "phrase18Text": "Koşmak, ileriye fırlatmak için sana yardımcı olur.", + "phrase19Text": "Zıplamak, yükseğe fırlatmak için sana yardımcı olur.", + "phrase20Text": "Bombaları daha uzağa fırlatmak için kamçı vurur gibi at.", + "phrase21Text": "Bombalarını akıllıca zamanlayabilirsin.", + "phrase22Text": "Lanet olsun.", + "phrase23Text": "Fitil bitimine bir ya da iki saniye kala atmayı dene.", + "phrase24Text": "Oley! Güzelce pişmiş.", + "phrase25Text": "Evet, işte tam da böyle.", + "phrase26Text": "Şimdi kaplan gibi olma zamanı!", + "phrase27Text": "Eğitimini hatırla ve buraya canlı DÖN!", + "phrase28Text": "...evet, olabilir...", + "phrase29Text": "İyi şanslar!", + "randomName1Text": "Fred", + "randomName2Text": "Chuck", + "randomName3Text": "Bill", + "randomName4Text": "Myth B.", + "randomName5Text": "Carl", + "skipConfirmText": "Öğreticiyi gerçekten atlamak istiyor musun? Onaylamak için dokun.", + "skipVoteCountText": "geçme oylaması ${COUNT}/${TOTAL}", + "skippingText": "Öğretici Atlanıyor...", + "toSkipPressAnythingText": "(öğreticiyi atlamak için herhangi bir tuşa bas)" + }, + "twoKillText": "ÇİFTE ÖLÜM!", + "unavailableText": "mevcut değil", + "unconfiguredControllerDetectedText": "Yapılandırılmamış kontrolcü tespit edildi:", + "unlockThisInTheStoreText": "Bunun mağazada kilidi açık olmalı.", + "unlockThisProfilesText": "${NUM} taneden daha fazla profil yaratmak için ihtiyacın olan:", + "unlockThisText": "Bu kilidi açmak için ihtiyacın olan:", + "unsupportedHardwareText": "Üzgünüz, bu donanım oyunun bu sürümü tarafından desteklenmiyor.", + "upFirstText": "İlki:", + "upNextText": "Sıradaki oyun ${COUNT}:", + "updatingAccountText": "Hesabınız güncelleniyor...", + "upgradeText": "Yükselt", + "upgradeToPlayText": "Bunu oynamak için \"${PRO}\" kilidini oyun-içi mağazadan aç.", + "useDefaultText": "Varsayılanı Kullan", + "usesExternalControllerText": "Bu oyun girdi olarak harici kontrolcü kullanıyor.", + "usingItunesText": "Müzikler için iTunes kullanılıyor...", + "usingItunesTurnRepeatAndShuffleOnText": "Lütfen iTunes da karıştırmanın KAPALI oldugundan ve Yinelemenin TÜM oldugundan emin olun. ", + "validatingTestBuildText": "Test Yapısı Onaylanıyor...", + "victoryText": "Galibiyet!", + "voteDelayText": "${NUMBER} saniye boyunca başka oylama başlatamazsın.", + "voteInProgressText": "Oylama zaten işlemde.", + "votedAlreadyText": "Zaten oyladın", + "votesNeededText": "${NUMBER} oy gerekli", + "vsText": "vs.", + "waitingForHostText": "(devam etmesi için ${HOST} bekleniyor)", + "waitingForPlayersText": "Oyuncuların katılması bekleniyor...", + "waitingInLineText": "Sırada bekleniyor (parti dolu)...", + "watchAVideoText": "Bir Video İzle", + "watchAnAdText": "Reklam İzle", + "watchWindow": { + "deleteConfirmText": "Sil \"${REPLAY}\"?", + "deleteReplayButtonText": "Tekrar\nSil", + "myReplaysText": "Tekrarlarım", + "noReplaySelectedErrorText": "Tekrar Seçilmedi", + "playbackSpeedText": "Oynatma hızı: ${SPEED}", + "renameReplayButtonText": "Tekrar\nYeniden Adlandır", + "renameReplayText": "\"${REPLAY}\" Yeniden Adlandır:", + "renameText": "Yeniden Adlandır", + "replayDeleteErrorText": "Tekrar Silinirken Hata", + "replayNameText": "Tekrar Adı", + "replayRenameErrorAlreadyExistsText": "Bu isimde bir tekrar zaten mevcut.", + "replayRenameErrorInvalidName": "Tekrar yeniden adlandırılamıyor; geçersiz ad.", + "replayRenameErrorText": "Tekrar Yeniden Adlandırılırken Hata", + "sharedReplaysText": "Paylaşılmış Tekrarlar", + "titleText": "İzle", + "watchReplayButtonText": "Tekrar\nİzle" + }, + "waveText": "Dalga", + "wellSureText": "Elbette!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Telif Hakkı" + }, + "wiimoteListenWindow": { + "listeningText": "Wiimote dinleniyor...", + "pressText": "Lütfen Wiimote 1 ve 2 butonlarına aynı anda basın.", + "pressText2": "Motion Plus içerisinde hiç Wiimote yok, geri yerine kırmızı 'senkron' butonuna basın." + }, + "wiimoteSetupWindow": { + "copyrightText": "DarwiinRemote TelifHakkı", + "listenText": "Dinle", + "macInstructionsText": "Wii nin kapalı olduğundan ve 'Dinle'ye bastığında\nMac'inde Bluetooth'un açık olduğundan emin ol.Wii\ndesteği biraz tuhaftır bu yüzden bağlanmadan önce\nbirkaç kere denemelisin.\n\nBluetooth çeşitli uzaklıklardan 7 cihaza\nkadar destek verir.\n\nBombSquad Orijinal Wiimoteleri Nunchukslari ve\nKlasik Kontrolcüleri destekler.\nYeni Wii Remote Plus'da şuanda çalışıyor\nfakat eklenmiş değil.", + "thanksText": "DarwiinRemote takımına bunu mümkün\nkıldığı için teşekkürler.", + "titleText": "Wiimote Kurulumu" + }, + "winsPlayerText": "${NAME} Kazandı!", + "winsTeamText": "${NAME} Kazandı!", + "winsText": "${NAME} Kazandı!", + "worldScoresUnavailableText": "Global Skorlar Mevcut Değil.", + "worldsBestScoresText": "Global En-İyi Skorlar", + "worldsBestTimesText": "Global En-İyi Süreler", + "xbox360ControllersWindow": { + "getDriverText": "Sürücüyü İndir", + "macInstructions2Text": "Kablosuz olarak kullanmak için; ayrıca 'Xbox 360 Kontroller for Windows'\nile gelen bir alıcı kullanman lazım. \nBir alıcı sana 4 taneye kadar bağlantı sağlar.\n\nÖnemli: 3.şahıs alıcılar bu sürücü ile çalışmaz;\nAlıcının 'Microsoft' için olduğuna dikkat et 'XBOX 360' değil.\nMicrosoft bunu uzun süredir ayrı olarak satmıyor. Kontrolcüleri ile \nbirlikte alabilirsin ya da ebay de araştırabilirsin.\n\nEğer bunu kullanışlı bulduysan, lütfen değerlendirerek sürücü\ngeliştirilmesine bu siteden bağış yap.", + "macInstructionsText": "Xbox 360 kolunu kullanmak için aşağıdaki bağlantıdaki\nMac sürücüsü yüklemen gerekiyor.\nKablolu ve Kablosuz kontroller de çalışır.", + "ouyaInstructionsText": "BombSquad ile kablolu Xbox 360 kontrolcüsü kullanmak için basitçe\ncihazının USB girişine takın. Çoklu kontrolcü kullanmak için USB\nçoklayıcı kullanabilirsiniz.\n\nKablosuz kontrolcü için \"Xbox 360 Wireless Controller for Windows\"\nkablosuz alıcısına ihtiyacınız var. Her alıcıya\nUSB girişi ile bağlanır ve 4 kablosuz kontrolcüye\nkadar desteklenir.", + "titleText": "Xbox 360 Kontrolcüsünü ${APP_NAME} ile kullan:" + }, + "yesAllowText": "Evet, Kabul!", + "yourBestScoresText": "En-İyi Skorların", + "yourBestTimesText": "En-İyi Sürelerin" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/ukrainian.json b/dist/ba_data/data/languages/ukrainian.json new file mode 100644 index 0000000..c5ea652 --- /dev/null +++ b/dist/ba_data/data/languages/ukrainian.json @@ -0,0 +1,1850 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Імена акаунтів не можуть містити емоджі або інші спеціальні символи", + "accountProfileText": "(профіль аккаунта)", + "accountsText": "Акаунти", + "achievementProgressText": "Досягнення: ${COUNT} з ${TOTAL}", + "campaignProgressText": "Прогрес кампанії [Тяжко]: ${PROGRESS}", + "changeOncePerSeason": "Ви можете змінити це тільки раз в сезон.", + "changeOncePerSeasonError": "Ви повинні дочекатися наступного сезону, щоб змінити це знову (${NUM} днів)", + "customName": "Ім'я акаунта", + "linkAccountsEnterCodeText": "Ввести Код", + "linkAccountsGenerateCodeText": "Створити Код", + "linkAccountsInfoText": "(поділитися прогресом на різних платформах)", + "linkAccountsInstructionsNewText": "Щоб зв'язати два облікові записи, згенеруйте код на\nпершому і введіть цей код на другому. Дані з другого\nоблікового запису будуть розподілені між ними.\n(Дані з першого облікового запису буде втрачено)\n\nВи можете зв'язати акаунтів: ${COUNT}.\n\nВАЖЛИВО: зв'язуйте тільки власні облікові записи;\nЯкщо ви зв'яжетесь з акаунтами друзів, ви не зможете\nодночасно грати онлайн.", + "linkAccountsInstructionsText": "Щоб зв'язати дви обликови записи, створювати код на одному з них и ввести цей код на инший. Прогрес та инвентаризации будуць об'эднанни. Ви можете зв'язати ${COUNT} рахунки.", + "linkAccountsText": "Зв'язати акаунти", + "linkedAccountsText": "Зв'язані акаунти:", + "nameChangeConfirm": "Ви впевнені що хочете змінити ім'я на ${NAME}?", + "resetProgressConfirmNoAchievementsText": "Це скине весь ваш кооперативний прогрес\nі локальні кращі результати (крім квитків).\nЦей процес є незворотнім. Ви впевнені?", + "resetProgressConfirmText": "Це скине весь ваш кооперативний\nпрогрес, досягнення і локальні результати\n(Крім квитків). Цей процес є незворотнім.\nВи впевнені?", + "resetProgressText": "Скинути прогрес", + "setAccountName": "Задайте ім'я акаунта", + "setAccountNameDesc": "Виберіть ім'я для відображення свого акаунта.\nВи можете використовувати ім'я однієї з ваших пов'язаних\nакаунтів або створити унікальне ім'я акаунта.", + "signInInfoText": "Увійдіть, щоб збирати квитки, змагатися онлайн,\nі синхронізувати прогрес на всіх пристроях.", + "signInText": "Увійти", + "signInWithDeviceInfoText": "(стандартний акаунт тільки для цього пристрою)", + "signInWithDeviceText": "Увійдіть, використовуючи акаунт пристрою", + "signInWithGameCircleText": "Увійти через Game Circle", + "signInWithGooglePlayText": "Увійти через Google Play", + "signInWithTestAccountInfoText": "(тест-аккаунт; надалі використовуйте акаунт пристрою)", + "signInWithTestAccountText": "Увійти через тестовий акаунт", + "signOutText": "Вийти", + "signingInText": "Вхід…", + "signingOutText": "Вихід…", + "testAccountWarningOculusText": "Увага: ви входите під \"тестовим\" аккаунтом.\nВін буде замінений \"справжнім\" акаунтом пізніше в цьому\nроці, що дозволить здійснювати покупки квитків і багато іншого.\nНа даний момент Вам доведеться заробляти квитки в грі.", + "testAccountWarningText": "Увага: ви увійшли під \"тестовим\" аккаунтом.\nЦей аккаунт зв'язанний з певним девайсом та може\nчас від часу скидатися. (так що не марнуйте час для\nзбирання/розблокування добра для нього)\n\nДля використання \"реального\" аккаунту (Game-Center,\nGoogle Plus та інші) запустіть платну версію гри. Це\nнадасть вам можливість зберігати прогрес у хмарі та\nробити його доступним для різних девайсів.", + "ticketsText": "Квитки: ${COUNT}", + "titleText": "Акаунт", + "unlinkAccountsInstructionsText": "Виберіть акаунт, який хочете відв'язати", + "unlinkAccountsText": "Відв'язати акаунт", + "viaAccount": "(через акаунт ${NAME})", + "youAreSignedInAsText": "Ви увійшли як:" + }, + "achievementChallengesText": "Досягнення", + "achievementText": "Досягнення", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Вбийте 3 поганих хлопців за допомогою TNT", + "descriptionComplete": "Вбито 3 поганих хлопців за допомогою TNT", + "descriptionFull": "Вбийте 3 поганих хлопців за допомогою TNT на рівні ${LEVEL}", + "descriptionFullComplete": "Вбито 3 поганих хлопців за допомогою TNT на рівні ${LEVEL}", + "name": "Зараз бабахне" + }, + "Boxer": { + "description": "Виграйте без використання бомб", + "descriptionComplete": "Перемога без використання бомб", + "descriptionFull": "Завершіть рівень ${LEVEL} без використання бомб", + "descriptionFullComplete": "Рівень ${LEVEL} завершений без використання бомб", + "name": "Боксер" + }, + "Dual Wielding": { + "descriptionFull": "Підключіть 2 контролери (реальний або додаток)", + "descriptionFullComplete": "Підключено 2 контролери (реальний або додаток)", + "name": "Подвійна зброя" + }, + "Flawless Victory": { + "description": "Виграйте не отримавши ушкоджень", + "descriptionComplete": "Перемога без пошкоджень", + "descriptionFull": "Пройдіть рівень ${LEVEL} не отримавши ушкоджень", + "descriptionFullComplete": "Рівень ${LEVEL} пройдений без ушкоджень", + "name": "Бездоганна перемога" + }, + "Free Loader": { + "descriptionFull": "Почати вільну для всіх гру з 2 і більш гравцями", + "descriptionFullComplete": "Почав вільну для всіх гру з 2 і більш гравцями", + "name": "Вільний завантажувач" + }, + "Gold Miner": { + "description": "Вбийте 6 поганих хлопців за допомогою мін", + "descriptionComplete": "Вбито 6 поганих хлопців за допомогою мін", + "descriptionFull": "Вбийте 6 поганих хлопців за допомогою мін на рівні ${LEVEL}", + "descriptionFullComplete": "Вбито 6 поганих хлопців за допомогою мін на рівні ${LEVEL}", + "name": "Золотий мінер" + }, + "Got the Moves": { + "description": "Виграйте без ударів і бомб", + "descriptionComplete": "Перемога без ударів і бомб", + "descriptionFull": "Виграйте рівень ${LEVEL} без ударів і бомб", + "descriptionFullComplete": "Перемога на рівні ${LEVEL} без ударів і бомб", + "name": "Точні рухи" + }, + "In Control": { + "descriptionFull": "Підключіть контролер (реальний або додаток)", + "descriptionFullComplete": "Підключений контролер. (реальний або додаток)", + "name": "Під контролем" + }, + "Last Stand God": { + "description": "Наберіть 1000 очок", + "descriptionComplete": "Набрано 1000 очок", + "descriptionFull": "Наберіть 1000 очок на рівні ${LEVEL}", + "descriptionFullComplete": "Набрано 1000 очок на рівні ${LEVEL}", + "name": "Бог рівня ${LEVEL}" + }, + "Last Stand Master": { + "description": "Наберіть 250 очок", + "descriptionComplete": "Набрано 250 очок", + "descriptionFull": "Наберіть 250 очок на рівні ${LEVEL}", + "descriptionFullComplete": "Набрано 250 очок на рівні ${LEVEL}", + "name": "Майстер рівня ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Наберіть 500 очок", + "descriptionComplete": "Набрано 500 очок", + "descriptionFull": "Наберіть 500 очок на рівні ${LEVEL}", + "descriptionFullComplete": "Набрано 500 очок на рівні ${LEVEL}", + "name": "Чарівник рівня ${LEVEL}" + }, + "Mine Games": { + "description": "Вбийте 3 поганих хлопців за допомогою мін", + "descriptionComplete": "Вбито 3 поганих хлопців за допомогою мін", + "descriptionFull": "Вбийте 3 поганих хлопців за допомогою мін на рівні ${LEVEL}", + "descriptionFullComplete": "Вбито 3 поганих хлопців за допомогою мін на рівні ${LEVEL}", + "name": "Ігри з мінами" + }, + "Off You Go Then": { + "description": "Скиньте 3 поганих хлопців з карти", + "descriptionComplete": "Скинуто 3 поганих хлопців з карти", + "descriptionFull": "Скиньте 3 поганих хлопців з карти на рівні ${LEVEL}", + "descriptionFullComplete": "Скинуто 3 поганих хлопців з карти на рівні ${LEVEL}", + "name": "Давай звідси" + }, + "Onslaught God": { + "description": "Наберіть 5000 очок", + "descriptionComplete": "Набрано 5000 очок", + "descriptionFull": "Наберіть 5000 очок на рівні ${LEVEL}", + "descriptionFullComplete": "Набрано 5000 очок на рівні ${LEVEL}", + "name": "Бог рівня ${LEVEL}" + }, + "Onslaught Master": { + "description": "Наберіть 500 очок", + "descriptionComplete": "Набрано 500 очок", + "descriptionFull": "Наберіть 500 очок на рівні ${LEVEL}", + "descriptionFullComplete": "Набрано 500 очок на рівні ${LEVEL}", + "name": "Майстер рівня ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Переможіть всі хвилі", + "descriptionComplete": "Переможені всі хвилі", + "descriptionFull": "Переможіть всі хвилі на рівні ${LEVEL}", + "descriptionFullComplete": "Переможені всі хвилі на рівні ${LEVEL}", + "name": "Перемога на рівні ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Наберіть 1000 очок", + "descriptionComplete": "Набрано 1000 очок", + "descriptionFull": "Наберіть 1000 очок на рівні ${LEVEL}", + "descriptionFullComplete": "Набрано 1000 очок на рівні ${LEVEL}", + "name": "Чарівник рівня ${LEVEL}" + }, + "Precision Bombing": { + "description": "Виграйте без підсилювачів", + "descriptionComplete": "Перемога без підсилювачів", + "descriptionFull": "Виграйте без підсилювачів на рівні ${LEVEL}", + "descriptionFullComplete": "Перемога без підсилювачів на рівні ${LEVEL}", + "name": "Прицільне бомбометання" + }, + "Pro Boxer": { + "description": "Виграйте без використання бомб", + "descriptionComplete": "Перемога без використання бомб", + "descriptionFull": "Виграйте без використання бомб на рівні ${LEVEL}", + "descriptionFullComplete": "Перемога без використання бомб на рівні ${LEVEL}", + "name": "Боксер профі" + }, + "Pro Football Shutout": { + "description": "Виграйте в суху", + "descriptionComplete": "Переміг в суху", + "descriptionFull": "Виграйте в суху на рівні ${LEVEL}", + "descriptionFullComplete": "Переміг в суху на рівні ${LEVEL}", + "name": "${LEVEL} в суху" + }, + "Pro Football Victory": { + "description": "Виграйте матч", + "descriptionComplete": "Матч виграно", + "descriptionFull": "Виграйте матч ${LEVEL}", + "descriptionFullComplete": "Матч виграно ${LEVEL}", + "name": "Перемога на рівні ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Переможіть всі хвилі", + "descriptionComplete": "Переможені всі хвилі", + "descriptionFull": "Переможіть всі хвилі на рівні ${LEVEL}", + "descriptionFullComplete": "Переможені всі хвилі на рівні ${LEVEL}", + "name": "Перемога на рівні ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Пройдіть всі хвилі", + "descriptionComplete": "Пройдені всі хвилі", + "descriptionFull": "Пройдіть всі хвилі на рівні ${LEVEL}", + "descriptionFullComplete": "Пройдені всі хвилі на рівні ${LEVEL}", + "name": "Перемога на рівні ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Виграйте, не давши поганим хлопцям забити", + "descriptionComplete": "Перемога, не давши поганим хлопцям забити", + "descriptionFull": "Виграйте матч ${LEVEL}, не давши поганим хлопцям забити", + "descriptionFullComplete": "Перемога в матчі ${LEVEL}, не давши поганим хлопцям забити", + "name": "${LEVEL} в суху" + }, + "Rookie Football Victory": { + "description": "Виграйте матч", + "descriptionComplete": "Матч виграний", + "descriptionFull": "Виграйте матч ${LEVEL}", + "descriptionFullComplete": "Виграний матч ${LEVEL}", + "name": "Перемога на рівні ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Переможіть всі хвилі", + "descriptionComplete": "Переможені всі хвилі", + "descriptionFull": "Переможіть всі хвилі на рівні ${LEVEL}", + "descriptionFullComplete": "Переможені всі хвилі на рівні ${LEVEL}", + "name": "Перемога на рівні ${LEVEL}" + }, + "Runaround God": { + "description": "Наберіть 2000 очок", + "descriptionComplete": "Набрано 2000 очок", + "descriptionFull": "Наберіть 2000 очок на рівні ${LEVEL}", + "descriptionFullComplete": "Набрано 2000 очок на рівні ${LEVEL}", + "name": "Бог рівня ${LEVEL}" + }, + "Runaround Master": { + "description": "Наберіть 500 очок", + "descriptionComplete": "Набрано 500 очок", + "descriptionFull": "Наберіть 500 очок на рівні ${LEVEL}", + "descriptionFullComplete": "Набрано 500 очок на рівні ${LEVEL}", + "name": "Майстер рівня ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Наберіть 1000 очок", + "descriptionComplete": "Набрано 1000 очок", + "descriptionFull": "Наберіть 1000 очок на рівні ${LEVEL}", + "descriptionFullComplete": "Набрано 1000 очок на рівні ${LEVEL}", + "name": "Чарівник рівня ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Успішно поділитися грою з другом", + "descriptionFullComplete": "Успішно поділився грою з другом", + "name": "Ділитися - значить піклуватися" + }, + "Stayin' Alive": { + "description": "Виграйте без смертей", + "descriptionComplete": "Перемога без смертей", + "descriptionFull": "Виграйте рівень ${LEVEL} не вмираючи", + "descriptionFullComplete": "Переміг на рівні ${LEVEL} не вмерши", + "name": "Залишитися в живих" + }, + "Super Mega Punch": { + "description": "Нанесіть 100% шкоди одним ударом", + "descriptionComplete": "Нанесено 100% шкоди одним ударом", + "descriptionFull": "Нанесіть 100% шкоди одним ударом на рівні ${LEVEL}", + "descriptionFullComplete": "Нанесено 100% шкоди одним ударом на рівні ${LEVEL}", + "name": "Супер-мега-удар" + }, + "Super Punch": { + "description": "Нанесіть 50% пошкодження одним ударом", + "descriptionComplete": "Нанесено 50% шкоди одним ударом", + "descriptionFull": "Нанесіть 50% шкоди одним ударом на рівні ${LEVEL}", + "descriptionFullComplete": "Нанесено 50% шкоди одним ударом на рівні ${LEVEL}", + "name": "Супер-удар" + }, + "TNT Terror": { + "description": "Вбийте 6 поганих хлопців з допомогою TNT", + "descriptionComplete": "Вбито 6 поганих хлопців з допомогою TNT", + "descriptionFull": "Вбийте 6 поганих хлопців з допомогою TNT на рівні ${LEVEL}", + "descriptionFullComplete": "Вбито 6 поганих хлопців з допомогою TNT на рівні ${LEVEL}", + "name": "Тротиловий жах" + }, + "Team Player": { + "descriptionFull": "Почати гру в командах з 4 і більш гравцями", + "descriptionFullComplete": "Почата гра в командах з 4 і більш гравцями", + "name": "Командний гравець" + }, + "The Great Wall": { + "description": "Зупиніть кожного поганого хлопця", + "descriptionComplete": "Зупинений кожен поганий хлопець", + "descriptionFull": "Зупиніть кожного поганого хлопця на рівні ${LEVEL}", + "descriptionFullComplete": "Зупинений кожен поганий хлопець на рівні ${LEVEL}", + "name": "Велика стіна" + }, + "The Wall": { + "description": "Зупиніть кожного поганого хлопця", + "descriptionComplete": "Зупинений кожен поганий хлопець", + "descriptionFull": "Зупиніть кожного поганого хлопця на рівні ${LEVEL}", + "descriptionFullComplete": "Зупинений кожен поганий хлопець на рівні ${LEVEL}", + "name": "Стіна" + }, + "Uber Football Shutout": { + "description": "Виграйте в суху", + "descriptionComplete": "Переміг в суху", + "descriptionFull": "Виграйте в суху на рівні ${LEVEL}", + "descriptionFullComplete": "Переміг в суху на рівні ${LEVEL}", + "name": "${LEVEL} в суху" + }, + "Uber Football Victory": { + "description": "Виграйте матч", + "descriptionComplete": "Матч виграно", + "descriptionFull": "Виграйте матч ${LEVEL}", + "descriptionFullComplete": "Матч виграно ${LEVEL}", + "name": "Перемога на рівні ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Переможіть всі хвилі", + "descriptionComplete": "Переможені всі хвилі", + "descriptionFull": "Переможіть всі хвилі на рівні ${LEVEL}", + "descriptionFullComplete": "Переможені всі хвилі на рівні ${LEVEL}", + "name": "Перемога на рівні ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Пройдіть всі хвилі", + "descriptionComplete": "Пройдені всі хвилі", + "descriptionFull": "Пройдіть всі хвилі на рівні ${LEVEL}", + "descriptionFullComplete": "Пройдені всі хвилі на рівні ${LEVEL}", + "name": "Перемога на рівні ${LEVEL}" + } + }, + "achievementsRemainingText": "Досягнень залишилось:", + "achievementsText": "Досягнення", + "achievementsUnavailableForOldSeasonsText": "На жаль, специфіка досягнення недоступна для старих сезонів.", + "addGameWindow": { + "getMoreGamesText": "Ще ігри...", + "titleText": "Додати гру" + }, + "allowText": "Дозволити", + "alreadySignedInText": "На вашому акаунті грають на іншому пристрої;\nбудь ласка зайдіть з іншого акаунта або закрийте гру на\nіншому пристрої та спробуйте знову.", + "apiVersionErrorText": "Неможливо завантажити модуль ${NAME}; він призначений для API версії ${VERSION_USED}; потрібна версія ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(Режим \"Auto\" включайте його тільки коли підключені навушники)", + "headRelativeVRAudioText": "Позиційно-залежне VR-аудіо", + "musicVolumeText": "Гучність музики", + "soundVolumeText": "Гучність звуку", + "soundtrackButtonText": "Саундтреки", + "soundtrackDescriptionText": "(виберіть свою власну музику, яка буде звучати під час гри)", + "titleText": "Аудіо" + }, + "autoText": "Авто", + "backText": "Назад", + "banThisPlayerText": "Забанити гравця", + "bestOfFinalText": "Фінал: кращий з ${COUNT}", + "bestOfSeriesText": "Кращий в ${COUNT} партіях:", + "bestRankText": "Ваш найкращий ранг: ${RANK}", + "bestRatingText": "Ваш найкращий рейтинг: ${RATING}", + "bombBoldText": "БОМБА", + "bombText": "Бомба", + "boostText": "Підсилення", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} налаштовується в самому додатку.", + "buttonText": "кнопка", + "canWeDebugText": "Хочете, щоб BombSquad автоматично повідомляв розробнику\nпро помилки, збої і основну інформацію про використання?\n\nЦі дані не містять ніякої особистої інформації і допомагають\nпідтримувати гру в робочому стані без збоїв і помилок.", + "cancelText": "Скасувати", + "cantConfigureDeviceText": "Вибачте, ${DEVICE} неможливо налаштувати.", + "challengeEndedText": "Це змагання завершено.", + "chatMuteText": "Приглушити чат", + "chatMutedText": "Чат приглушений", + "chatUnMuteText": "Включити чат", + "choosingPlayerText": "<вибір гравця>", + "completeThisLevelToProceedText": "Щоб продовжити, потрібно\nпройти цей рівень!", + "completionBonusText": "Бонус за проходження", + "configControllersWindow": { + "configureControllersText": "Налаштування контролерів", + "configureKeyboard2Text": "Налаштування клавіатури P2", + "configureKeyboardText": "Налаштування клавіатури", + "configureMobileText": "Мобільні пристрої як контролери", + "configureTouchText": "Налаштування сенсорного екрану", + "ps3Text": "Контролери PS3", + "titleText": "Контролери", + "wiimotesText": "Wiimotes", + "xbox360Text": "Контролери Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Увага: підтримка контролерів різниться в залежності від пристрою і версії Android.", + "pressAnyButtonText": "Натисніть будь-яку кнопку на контролері,\n  який хочете налаштувати...", + "titleText": "Налаштування контролерів" + }, + "configGamepadWindow": { + "advancedText": "Додатково", + "advancedTitleText": "Додаткові настройки контролера", + "analogStickDeadZoneDescriptionText": "(включіть, якщо персонаж продовжує рухатися після того, як стік відпущений)", + "analogStickDeadZoneText": "Мертва Зона Аналоговогу Стіку", + "appliesToAllText": "(застосовується до всіх контроллерів такого типу)", + "autoRecalibrateDescriptionText": "(увімкніть цю опцію якщо ваш персонаж не рухається на повній швидкості)", + "autoRecalibrateText": "Авто-калібрування аналогових стіків", + "axisText": "вісь", + "clearText": "очистити", + "dpadText": "хрестовина", + "extraStartButtonText": "Додаткова кнопка Start", + "ifNothingHappensTryAnalogText": "Якщо нічого не відбувається, спробуйте назначити це аналоговому стіку.", + "ifNothingHappensTryDpadText": "Якщо нічого не відбувається, спробуйте назначити це на хрестовину", + "ignoreCompletelyDescriptionText": "(не дайте цьому контролеру впливати на гру, або меню)", + "ignoreCompletelyText": "Ігнорувати повністю", + "ignoredButton1Text": "Ігнорована кнопка 1", + "ignoredButton2Text": "Ігнорована кнопка 2", + "ignoredButton3Text": "Ігнорована кнопка 3", + "ignoredButton4Text": "Ігнорована кнопка 4", + "ignoredButtonDescriptionText": "(використовуйте це, щоб кнопки 'home' або 'sync' не вплинули на користувацький інтерфейс)", + "pressAnyAnalogTriggerText": "Натисніть будь-який аналоговий тригер...", + "pressAnyButtonOrDpadText": "Натисніть будь-яку кнопку або хрестовину...", + "pressAnyButtonText": "Натисніть будь-яку кнопку", + "pressLeftRightText": "Натисніть вліво або вправо...", + "pressUpDownText": "Натисніть вгору або вниз...", + "runButton1Text": "Кнопка для бігу 1", + "runButton2Text": "Кнопка для бігу 2", + "runTrigger1Text": "Тригер для бігу 1", + "runTrigger2Text": "Тригер для бігу 2", + "runTriggerDescriptionText": "(аналогові тригери дозволяють бігати з різною швидкістю)", + "secondHalfText": "Використовуйте це для настройки другої половини\nпристрою 'два контролера в одному',\nяке показується як один контролер.", + "secondaryEnableText": "Увімкнути", + "secondaryText": "Вторинний контролер", + "startButtonActivatesDefaultDescriptionText": "(вимкніть, якщо ваша кнопка \"старт\" працює як кнопки \"меню\")", + "startButtonActivatesDefaultText": "Кнопка Старт активує стандартний віджет", + "titleText": "Налаштування контролера", + "twoInOneSetupText": "Налаштування контролера 2-в-1", + "uiOnlyDescriptionText": "(відключити цей контролер приєднуючись до гри)", + "uiOnlyText": "Обмежуватись навігацією в меню", + "unassignedButtonsRunText": "Всі не присвоєні кнопки для бігу", + "unsetText": "<не задано>", + "vrReorientButtonText": "Кнопка переорієнтації VR" + }, + "configKeyboardWindow": { + "configuringText": "Налаштування ${DEVICE}", + "keyboard2NoteText": "Примітка: більшість клавіатур можуть зареєструвати тільки кілька \nнатискань клавіш за раз, так що другому гравцеві з клавіатури \nкраще грати з окремою підключеної для них клавіатури. \nЗауважте, що навіть в цьому випадку все одно потрібно \nбуде присвоїти унікальні клавіші для двох гравців." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Розміри кнопок дії", + "actionsText": "Дії", + "buttonsText": "кнопки", + "dragControlsText": "< щоб пересунути елементи управління, перетягніть їх >", + "joystickText": "джойстик", + "movementControlScaleText": "Розміри кнопок руху", + "movementText": "Рух", + "resetText": "Скидання", + "swipeControlsHiddenText": "Сховати іконки змахування", + "swipeInfoText": "До контролерів, які працюють зі змахування, потрібно звикнути,\nзате з ними можна грати, не дивлячись на контролер.", + "swipeText": "змахнути", + "titleText": "Налаштування сенсорного екрану" + }, + "configureItNowText": "Налаштувати зараз?", + "configureText": "Налаштувати", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Appstore", + "appStoreText": "App Store", + "bestResultsText": "Для найкращого результату вам знадобиться мережу Wi-Fi без затримок.\nВи можете зменшити затримки Wi-Fi, вимкнувши інші бездротові\nпристрою, граючи близько до маршрутизатора Wi-Fi, і підключивши\nхост гри безпосередньо до мережі через Ethernet.", + "explanationText": "Для налаштування смартфона або планшета в якості контролера\nвстановіть на ньому додаток \"${REMOTE_APP_NAME}\". До гри ${APP_NAME}\nможна підключити будь-яку кількість пристроїв через Wi-Fi, безкоштовно!", + "forAndroidText": "для Android:", + "forIOSText": "для iOS:", + "getItForText": "Завантажте додаток ${REMOTE_APP_NAME} для iOS в Apple App Store\nабо для Android в Google Play Store або Amazon Appstore", + "googlePlayText": "Google Play", + "titleText": "Використання мобільних пристроїв в якості контролерів:" + }, + "continuePurchaseText": "Продовжити за ${PRICE}?", + "continueText": "Продовжити", + "controlsText": "Управління", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Це не відноситься до абсолютних рекордів.", + "activenessInfoText": "Цей множник підвищується коли ви граєте,\nі знижується коли ви не граєте.", + "activityText": "Активність", + "campaignText": "Компанія", + "challengesInfoText": "Вигравай призи за міні-ігри.\n\nПризи та складність збільшуються\nкожен раз після перемоги і\nзменшуються в разі провалу.", + "challengesText": "Випробування", + "currentBestText": "Останній рекорд", + "customText": "Інше", + "entryFeeText": "Участь", + "forfeitConfirmText": "Так просто здаєтеся?", + "forfeitNotAllowedYetText": "Ви не можете покинути це змагання.", + "forfeitText": "Здатися", + "multipliersText": "Множники", + "nextChallengeText": "Наступне випробування", + "nextPlayText": "Наступна гра", + "ofTotalTimeText": "З ${TOTAL}", + "playNowText": "Грати", + "pointsText": "Бали", + "powerRankingFinishedSeasonUnrankedText": "Сезон завершений не в лізі", + "powerRankingNotInTopText": "(не в ${NUMBER} кращих)", + "powerRankingPointsEqualsText": "= ${NUMBER} очко", + "powerRankingPointsMultText": "(x ${NUMBER} балів)", + "powerRankingPointsText": "${NUMBER} балів", + "powerRankingPointsToRankedText": "(${CURRENT} з ${REMAINING} балів)", + "powerRankingText": "Ранг гравця", + "prizesText": "Призи", + "proMultInfoText": "Гравці з ${PRO} покращенням\nОтримують ${PERCENT}% множника тут.", + "seeMoreText": "Детальніше...", + "skipWaitText": "Прискорити очікування", + "timeRemainingText": "Часу залишився", + "toRankedText": "Отримано", + "totalText": "всього", + "tournamentInfoText": "Добийтеся високого результату з\nіншими гравцями твоєї ліги.\n\nНагороди вручаються найкрутішим\nпісля закінчення турніру.", + "welcome1Text": "Ласкаво просимо в ${LEAGUE}. Ви можете\nпідвищити свою лігу отримуючи зірки, отримуючи\nдосягнення і виграючи трофеї в турнірах.", + "welcome2Text": "Ви також можете заробити квитки від багатьох однакових видів діяльності.\nКвитки можуть бути використані, щоб розблокувати нових персонажів, карти та\nміні-ігри, щоб увійти турніри, і багато іншого.", + "yourPowerRankingText": "Ваш ранг:" + }, + "copyOfText": "Копія ${NAME}", + "createEditPlayerText": "<Створення / редагування гравця>", + "createText": "Створити", + "creditsWindow": { + "additionalAudioArtIdeasText": "Додаткове аудіо, попередні ілюстрації та ідеї: ${NAME}", + "additionalMusicFromText": "Додаткова музика: ${NAME}", + "allMyFamilyText": "Всім моїм друзям і родині, котрі допомогли грати тестову версію", + "codingGraphicsAudioText": "Програмування, графіка і аудіо: ${NAME}", + "languageTranslationsText": "Мовні переклади:", + "legalText": "Юридична інформація:", + "publicDomainMusicViaText": "Загальнодоступна музика через ${NAME}", + "softwareBasedOnText": "Це програмне забезпечення частково базується на роботі ${NAME}", + "songCreditText": "${TITLE} виконує ${PERFORMER}\nКомпозитор ${COMPOSER}, Аранжування ${ARRANGER}, Видано ${PUBLISHER},\nНадано ${SOURCE}", + "soundAndMusicText": "Звук і музика:", + "soundsText": "Звуки (${SOURCE}):", + "specialThanksText": "Особлива подяка:", + "thanksEspeciallyToText": "Окреме спасибі ${NAME}", + "titleText": "Брали участь в створенні ${APP_NAME}", + "whoeverInventedCoffeeText": "Тому, хто винайшов каву" + }, + "currentStandingText": "Ваша поточна позиція: ${RANK}", + "customizeText": "Змінити...", + "deathsTallyText": "${COUNT} смертей", + "deathsText": "Смерті", + "debugText": "налагодження", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Увага: для цього тесту рекомендується встановити Настройки->графіка->Текстури на 'Високий.'", + "runCPUBenchmarkText": "Запустити тест продуктивності CPU", + "runGPUBenchmarkText": "Запустити тест продуктивності GPU", + "runMediaReloadBenchmarkText": "Запустити тест продуктивності завантаження медіа", + "runStressTestText": "Виконати тест-навантаження", + "stressTestPlayerCountText": "Кількість гравців", + "stressTestPlaylistDescriptionText": "Плейлист навантажувального випробування", + "stressTestPlaylistNameText": "Назва плейлиста", + "stressTestPlaylistTypeText": "Тип плейлиста", + "stressTestRoundDurationText": "Тривалість раунду", + "stressTestTitleText": "Тест-навантаження", + "titleText": "Тести продуктивності і тести-навантаження", + "totalReloadTimeText": "Загальний час перезавантаження: ${TIME} (подробиці див. в лозі)" + }, + "defaultGameListNameText": "Стандартний плейлист режиму ${PLAYMODE}", + "defaultNewGameListNameText": "Мій плейлист ${PLAYMODE}", + "deleteText": "Видалити", + "demoText": "Демо", + "denyText": "Відхилити", + "desktopResText": "Розширення екрану", + "difficultyEasyText": "Легкий", + "difficultyHardOnlyText": "Тільки в складному режимі", + "difficultyHardText": "Складний", + "difficultyHardUnlockOnlyText": "Цей рівень може бути відкритий тільки у складному режимі.\nВи цього не зробили !!!!!", + "directBrowserToURLText": "Будь ласка, перейдіть у веб-браузері за наступною адресою:", + "disableRemoteAppConnectionsText": "Відключення з'єднання RemoteApp", + "disableXInputDescriptionText": "Підключення більше 4 контролерів, але може не працювати.", + "disableXInputText": "Відключити XInput", + "doneText": "Готово", + "drawText": "Нічия", + "duplicateText": "Дублювати", + "editGameListWindow": { + "addGameText": "Додати\nГру", + "cantOverwriteDefaultText": "Неможливо перезаписати стандартний плейлист!", + "cantSaveAlreadyExistsText": "Плейлист з таким ім'ям вже існує!", + "cantSaveEmptyListText": "Неможливо зберегти порожній плейлист!", + "editGameText": "Змінити\nГру", + "listNameText": "Назва плейлиста", + "nameText": "Назва плейлиста", + "removeGameText": "Видалити\nгру", + "saveText": "Зберегти список", + "titleText": "Редактор плейлиста" + }, + "editProfileWindow": { + "accountProfileInfoText": "Цей спеціальний профіль має\nім'я та іконку, основані на вашому акаунті.\n\n${ICONS}\n\nСтворіть додаткові профілі, щоб\nВикористовувати різні імена і іконки.", + "accountProfileText": "(профіль акаунта)", + "availableText": "Ім'я \"${NAME}\" доступне.", + "characterText": "персонаж", + "checkingAvailabilityText": "Перевірка доступності для \"${NAME}\"...", + "colorText": "колір", + "getMoreCharactersText": "Ще персонажів...", + "getMoreIconsText": "Отримати додаткові іконки...", + "globalProfileInfoText": "Глобальні профілі гравців гарантовано мають унікальні\nІмена. Вони також включають додаткові іконки.", + "globalProfileText": "(глобальний профіль)", + "highlightText": "акцент", + "iconText": "іконка", + "localProfileInfoText": "Локальні ігрові профілі не мають іконки і їхні імена\nне гарантовано унікальні. Підніміть до глобального профілю\nщоб зарезервувати унікальне ім'я і додати додаткову іконку.", + "localProfileText": "(локальний профіль)", + "nameDescriptionText": "Ім'я гравця", + "nameText": "Ім'я", + "randomText": "випадкове", + "titleEditText": "Змінити профіль", + "titleNewText": "Новий профіль", + "unavailableText": "\"${NAME}\" недоступно; спробуйте інше ім'я.", + "upgradeProfileInfoText": "Це зарезервує ваше ім'я гравця\nІ дозволить вам змінити іконкую.", + "upgradeToGlobalProfileText": "Оновити до глобального профілю" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Неможливо видалити стандартний саундтрек.", + "cantEditDefaultText": "Неможливо змінити стандартний саундтрек. Створіть копію або новий саундтрек.", + "cantOverwriteDefaultText": "Неможливо перезаписати стандартний саундтрек", + "cantSaveAlreadyExistsText": "Саундтрек з таким ім'ям вже існує!", + "defaultGameMusicText": "<стандартна музика гри>", + "defaultSoundtrackNameText": "Стандартний саундтрек", + "deleteConfirmText": "Видалити саундтрек:\n\n'${NAME}?", + "deleteText": "Видалити\nсаундтрек", + "duplicateText": "Копіювати\nсаундтрек", + "editSoundtrackText": "Редактор саундтрека", + "editText": "Змінити\nсаундтрек", + "fetchingITunesText": "завантаження музикальних плейлистів...", + "musicVolumeZeroWarning": "Увага: гучність музики встановлена на 0", + "nameText": "Назва", + "newSoundtrackNameText": "Мій саундтрек ${COUNT}", + "newSoundtrackText": "Новий саундтрек:", + "newText": "Новий", + "selectAPlaylistText": "Виберіть плейлист", + "selectASourceText": "Джерело музики", + "testText": "тест", + "titleText": "Саундтреки", + "useDefaultGameMusicText": "Стандартна музика", + "useITunesPlaylistText": "Музикальний плейлист", + "useMusicFileText": "Музичні файли (mp3 і т.д.)", + "useMusicFolderText": "Тека з музикою" + }, + "editText": "Редагувати", + "endText": "Кінець", + "enjoyText": "Успіхів!", + "epicDescriptionFilterText": "${DESCRIPTION} в епічному сповільненій дії.", + "epicNameFilterText": "${NAME} в епічному режимі", + "errorAccessDeniedText": "доступ заборонено", + "errorOutOfDiskSpaceText": "немає місця на диску", + "errorText": "Помилка", + "errorUnknownText": "невідома помилка", + "exitGameText": "Вийти з ${APP_NAME}?", + "exportSuccessText": "'${NAME}' експортовано.", + "externalStorageText": "Зовнішня пам'ять", + "failText": "Провал", + "fatalErrorText": "Ой, щось загубилося або зламалося.\nСпробуйте перевстановити додаток або\nзверніться до ${EMAIL} за допомогою.", + "fileSelectorWindow": { + "titleFileFolderText": "Виберіть файл або папку", + "titleFileText": "Виберіть файл", + "titleFolderText": "Виберіть папку", + "useThisFolderButtonText": "Використовувати цю папку" + }, + "filterText": "Фiльтр", + "finalScoreText": "Фінальні очки", + "finalScoresText": "Фінальні очки", + "finalTimeText": "Фінальний час", + "finishingInstallText": "Завершується установка, хвилинку ...", + "fireTVRemoteWarningText": "* Для кращого результату використовуйте\nігрові контролери або встановіть\nдодаток '${REMOTE_APP_NAME}'\nна ваших телефонах і планшетах.", + "firstToFinalText": "Фінал до ${COUNT} очок", + "firstToSeriesText": "Серія до ${COUNT} очок", + "fiveKillText": "П'ЯТЬОХ ЗА РАЗ!!!", + "flawlessWaveText": "Бездоганна хвиля!", + "fourKillText": "ЧОТИРЬОХ ЗА РАЗ!!!", + "friendScoresUnavailableText": "Очки друзів недоступні.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Лідери гри ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "Неможливо видалити стандартний плейлист!", + "cantEditDefaultText": "Неможливо змінити стандартний плейлист! Створіть копію або новий список.", + "cantShareDefaultText": "Ви не можете ділитися стандартним плейлистом.", + "deleteConfirmText": "Видалити \"${LIST}\"?", + "deleteText": "Видалити\nплейлист", + "duplicateText": "Дублювати\nплейлист", + "editText": "Змінити\nплейлист", + "newText": "Новий\nплейлист", + "showTutorialText": "Показати туторіал", + "shuffleGameOrderText": "Змішати порядок ігор", + "titleText": "Налаштувати плейлисти '${TYPE}'" + }, + "gameSettingsWindow": { + "addGameText": "Додати гру" + }, + "gamesToText": "${WINCOUNT}: ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Пам'ятайте: будь-який пристрій у спільній грі підтримує\nкілька гравців, якщо у вас вистачає контролерів.", + "aboutDescriptionText": "Використовуйте ці закладки, щоб створити лобі.\n\nЛобі дозволяє грати в ігри і турніри\nз друзями на різних пристроях.\n\nНатисніть кнопку ${PARTY} в правому верхньому куті,\nщоб спілкуватися з друзями в вашому лобі.\n(На контролері, натисніть ${BUTTON} перебуваючи в меню)", + "aboutText": "Інфо", + "addressFetchErrorText": "<помилка запиту адрес>", + "appInviteMessageText": "${NAME} відправив вам ${COUNT} квитків в ${APP_NAME}", + "appInviteSendACodeText": "Надішліть їм код", + "appInviteTitleText": "Запрошення в ${APP_NAME}", + "bluetoothAndroidSupportText": "(працює з будь-яким пристроєм, що підтримує Bluetooth)", + "bluetoothDescriptionText": "Створити/увійти в лобі через Bluetooth:", + "bluetoothHostText": "Створити лобі через Bluetooth", + "bluetoothJoinText": "Приєднатися через Bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "Перевірка ...", + "copyCodeConfirmText": "Код скопійовано до буферу обміну", + "copyCodeText": "Скопіювати код", + "dedicatedServerInfoText": "Для кращого результату створіть окремий сервер. Дивись bombsquadgame.com/server", + "disconnectClientsText": "Це відключить ${COUNT} гравців\nУ вашому лобі. Ви впевнені?", + "earnTicketsForRecommendingAmountText": "Друзі отримають ${COUNT} квитки, якщо вони спробують гру \n(і ви будете отримувати ${YOU_COUNT} для кожного, це зробить)", + "earnTicketsForRecommendingText": "Поділися грою\nОтримай квитки ...", + "emailItText": "Надіслати Email", + "favoritesSaveText": "Встановити як улюблене", + "favoritesText": "Улюблене", + "freeCloudServerAvailableMinutesText": "Наступний безкоштовний сервер у хмарі буде доступним через ${MINUTES} хвилин.", + "freeCloudServerAvailableNowText": "Безкоштовний сервер у хмарі тепер достуний!", + "freeCloudServerNotAvailableText": "Не знайдено доступних серверів у хмарі.", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} квитків від ${NAME}", + "friendPromoCodeAwardText": "Ви отримаєте ${COUNT} квитків за кожну активацію.", + "friendPromoCodeExpireText": "Код дійсний протягом ${EXPIRE_HOURS} годин і тільки для нових гравців.", + "friendPromoCodeInstructionsText": "Для активації в ${APP_NAME} зайдіть в \"Налаштування->Додатково->Ввести код\".\nЗнайди на bombsquadgame.com версію гри для своєї платформи.", + "friendPromoCodeRedeemLongText": "Кожна активація приносить ${COUNT} квитків до ${MAX_USES} гравцям.", + "friendPromoCodeRedeemShortText": "Він принесе ${COUNT} квитків в грі.", + "friendPromoCodeWhereToEnterText": "(В \"Налаштування->Додатково->Ввести код\")", + "getFriendInviteCodeText": "Отримати промо-код", + "googlePlayDescriptionText": "Запросити гравців Google Play в ваше лобі:", + "googlePlayInviteText": "Запросити", + "googlePlayReInviteText": "У вашому лобі ${COUNT} гравців Google Play.\nЯкщо ви почнете нове запрошення, вони будуть відключені.\nДодайте їх в нове запрошення, щоб їх повернути.", + "googlePlaySeeInvitesText": "Подивитися запрошення", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Тільки для Android / Google Play)", + "hostPublicPartyDescriptionText": "Створити публічну групу", + "hostingUnavailableText": "Хостинг не доступний", + "inDevelopmentWarningText": "Примітка:\n\nМережева гра є новою і ще розвивається.\nВ даний час, дуже рекомендується, щоб усі\nгравці були в тій же мережі Wi-Fi.", + "internetText": "Інтернет", + "inviteAFriendText": "Друзі ще не грають? Запроси їх\nспробувати і вони отримають ${COUNT} квитків.", + "inviteFriendsText": "Запросити друзів", + "joinPublicPartyDescriptionText": "Приєднатися до публічної групи", + "localNetworkDescriptionText": "Увійти в лобі у вашій мережі (через LAN, Bluetooth, т. д. )", + "localNetworkText": "Локальна мережа", + "makePartyPrivateText": "Зробити моє лобі приватним", + "makePartyPublicText": "Зробити моє лобі публічним", + "manualAddressText": "Адреса", + "manualConnectText": "Підключитися", + "manualDescriptionText": "Увійти в лобі за адресою:", + "manualJoinSectionText": "Приєднатися за адресою", + "manualJoinableFromInternetText": "До вас можна підключатися через інтернет?:", + "manualJoinableNoWithAsteriskText": "НІ*", + "manualJoinableYesText": "ТАК", + "manualRouterForwardingText": "*щоб виправити, спробуйте налаштувати маршрутизатор для перенаправлення UDP-порта ${PORT} на ваш локальний адресу", + "manualText": "Вручну", + "manualYourAddressFromInternetText": "Ваш інтернет адрес:", + "manualYourLocalAddressText": "Ваш локальний адрес:", + "nearbyText": "Поблизу", + "noConnectionText": "<немає з'єднання>", + "otherVersionsText": "(інші версії)", + "partyCodeText": "Код групи", + "partyInviteAcceptText": "Прийняти", + "partyInviteDeclineText": "Відхилити", + "partyInviteGooglePlayExtraText": "(дивіться вкладку 'Google Play' в розділі 'Зібрати')", + "partyInviteIgnoreText": "Ігнорувати", + "partyInviteText": "${NAME} запросив\nвас в його лобі!", + "partyNameText": "Ім'я команди", + "partyServerRunningText": "Ваш груповий сервер запущений.", + "partySizeText": "розмір групи", + "partyStatusCheckingText": "перевірка...", + "partyStatusJoinableText": "ваша команда доступна через інтернет", + "partyStatusNoConnectionText": "сервер не доступний", + "partyStatusNotJoinableText": "ваша команда недоступна через інтернет", + "partyStatusNotPublicText": "ваша команда не для всіх", + "pingText": "пінг", + "portText": "порт", + "privatePartyCloudDescriptionText": "Приватні групи працюють на серверах хмари; конфігурація роутера не потрібна.", + "privatePartyHostText": "Створити приватну групу", + "privatePartyJoinText": "Приєднатися до публічної групи", + "privateText": "Приватний", + "publicHostRouterConfigText": "Це може потребувати конфігурацію спрямування порту на вашому роутері. Легшим вибором є хостинг приватної групи.", + "publicText": "Публічний", + "requestingAPromoCodeText": "Запитуємо код...", + "sendDirectInvitesText": "Послати запрошення", + "shareThisCodeWithFriendsText": "Поділися кодом з друзями:", + "showMyAddressText": "Показати мою адресу", + "startHostingPaidText": "Запустити зараз за ${COST}", + "startHostingText": "Запустити", + "startStopHostingMinutesText": "Ви можете розпочати хостинг безкоштовно через наступних ${MINUTES} хвилин.", + "stopHostingText": "Зупинити хостинг", + "titleText": "Зібрати", + "wifiDirectDescriptionBottomText": "Якщо у всіх пристроїв є панель 'Wi-Fi Direct', вони можуть використовувати її, щоб знайти\nі з'єднатися один з одним. Коли всі пристрої підключені, ви можете створити тут лобі,\nвикористовуючи вкладку 'Локальна мережа', так само, як і зі звичайною Wi-Fi мережею.\n\nДля кращих результатів, хост Wi-Fi Direct також повинен бути хостом групи ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct може бути використаний для з'єднання пристроїв Android безпосередньо без\nWi-Fi мережі. Це найкраще працює на Android 4.2 або новіше.\n\nЩоб використовувати, відкрийте налаштування Wi-Fi і пошукайте Wi-Fi Direct в меню.", + "wifiDirectOpenWiFiSettingsText": "Відкрити налаштування Wi-Fi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(працює між всіма платформами)", + "worksWithGooglePlayDevicesText": "(працює з пристроями, що використовують версію гри Google Play (Android)", + "youHaveBeenSentAPromoCodeText": "Вам був висланий ${APP_NAME} промо-код:" + }, + "getTicketsWindow": { + "freeText": "БЕЗКОШТОВНО!", + "freeTicketsText": "Безкоштовні квитки", + "inProgressText": "Виконується транзакція; спробуйте ще раз через кілька секунд.", + "purchasesRestoredText": "Покупки перевірені", + "receivedTicketsText": "Отримано ${COUNT} квитків!", + "restorePurchasesText": "Відновити покупки", + "ticketPack1Text": "Маленька пачка квитків", + "ticketPack2Text": "Середня пачка квитків", + "ticketPack3Text": "Велика пачка квитків", + "ticketPack4Text": "Величезна пачка квитків", + "ticketPack5Text": "Слонова пачка квитків", + "ticketPack6Text": "Максимальна пачка квитків", + "ticketsFromASponsorText": "Отримати ${COUNT} квитків\nвід спонсора", + "ticketsText": "Квитків: ${COUNT}", + "titleText": "Отримати квитки", + "unavailableLinkAccountText": "Вибачте, але на цій платформі покупки недоступні.\nВ якості вирішення, ви можете прив'язати цей акаунт\nдо акаунту на іншій платформі, і здійснювати покупки там.", + "unavailableTemporarilyText": "Зараз недоступно; Спробуйте ще раз пізніше.", + "unavailableText": "На жаль це не доступно.", + "versionTooOldText": "Вибачте, ваша версія гри застаріла; будь ласка, завантажте оновлення.", + "youHaveShortText": "у вас ${COUNT}", + "youHaveText": "У вас ${COUNT} квитків" + }, + "googleMultiplayerDiscontinuedText": "Пробачте, але сервіс мультіплеєра від Google тепер не доступний.\nЯ працюю над зміною сервіса як можно скоріше.\nДо цього, будь ласка, подивіться інакші способи гри в мультіплеєр. \n-Ерік", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Завжди", + "fullScreenCmdText": "Повноекранний (Cmd-F)", + "fullScreenCtrlText": "Повноекранний (Ctrl-F)", + "gammaText": "Гамма", + "highText": "Високий", + "higherText": "Вище", + "lowText": "Низький", + "mediumText": "Середній", + "neverText": "Ніколи", + "resolutionText": "Розширення", + "showFPSText": "Показувати FPS", + "texturesText": "Текстури", + "titleText": "Графіка", + "tvBorderText": "Границя телевізора", + "verticalSyncText": "Вертикальна синхронізація", + "visualsText": "Відеоряд" + }, + "helpWindow": { + "bombInfoText": "- Бомба -\nСильніше ударів, але може привести\nдо смертельних ушкоджень. для\nнайкращих результатів кидати в\nпротивника поки не догорів гніт.", + "canHelpText": "${APP_NAME} може допомогти.", + "controllersInfoText": "Ви можете грати в ${APP_NAME} з друзями по мережі, або ви всі можете\nграти на одному пристрої, якщо у вас досить контролерів.\n${APP_NAME} підтримує будь-які контролери; можна навіть використовувати телефони\nв якості контролерів через безкоштовний додаток '${REMOTE_APP_NAME}.\nДивіться Налаштування->Контролери для отримання додаткової інформації.", + "controllersText": "Контролери", + "controlsSubtitleText": "У вашого дружнього персонажа з ${APP_NAME} є кілька простих дій:", + "controlsText": "Управління", + "devicesInfoText": "У VR-версію ${APP_NAME} можна грати по мережі зі звичайною \nверсією, так що витягуйте свої додаткові телефони, планшети\nі комп'ютери, і грайте на них. Можна навіть підключити\nзвичайну версію гри до VR-версії, щоб дозволити\nіншим спостерігати за діями.", + "devicesText": "Пристрої", + "friendsGoodText": "Бувають корисні. В ${APP_NAME} веселіше грати з кількома гравцями;\nпідтримується до 8 гравців одночасно, що призводить нас до:", + "friendsText": "Друзі", + "jumpInfoText": "- Стрибок -\nСтрибайте для перескакування,\nкидання предметів подалі\nабо для вираження радості.", + "orPunchingSomethingText": "Або вдарити, скинути з обриву і підірвати бомбою-липучкою по дорозі вниз.", + "pickUpInfoText": "- Захоплення -\nХапайте прапори, ворогів\nі все, що не прикручено до підлоги.\nЩоб кинути, натисніть ще раз.", + "powerupBombDescriptionText": "Дозволяє жбурнути три бомби\nпоспіль, замість однієї.", + "powerupBombNameText": "Потрійні бомби", + "powerupCurseDescriptionText": "Цих, напевно, краще уникати.\n  ...чи ні?", + "powerupCurseNameText": "Прокляття", + "powerupHealthDescriptionText": "Нізащо не здогадаєтеся.\nПовертає повне здоров'я.", + "powerupHealthNameText": "Аптечка", + "powerupIceBombsDescriptionText": "Слабші, ніж звичайні бомби\nале робить ворогів замороженими\nі особливо крихкими.", + "powerupIceBombsNameText": "Крижані бомби", + "powerupImpactBombsDescriptionText": "Трохи слабші звичайних бомб,\nале вибухають при ударі.", + "powerupImpactBombsNameText": "Ударні бомби", + "powerupLandMinesDescriptionText": "Видаються по 3 штуки.\nКорисні для захисту бази або\nупокорення прудконогих ворогів.", + "powerupLandMinesNameText": "Міни", + "powerupPunchDescriptionText": "Роблять ваші удари швидше,\nкраще, сильніше.", + "powerupPunchNameText": "Боксерські рукавички", + "powerupShieldDescriptionText": "Трохи поглинає пошкодження,\nщоб вам не довелося.", + "powerupShieldNameText": "Енергетичний щит", + "powerupStickyBombsDescriptionText": "Липнуть до всього, чого торкаються.\nІ починаються веселощі.", + "powerupStickyBombsNameText": "Бомби-липучки", + "powerupsSubtitleText": "Звичайно, жодна гра не обходиться без підсилювачів:", + "powerupsText": "Підсилювачі", + "punchInfoText": "- Удар -\nЧим швидше рухаються кулаки -\nтим сильніше удар. Так що бігайте\nі крутіться як ненормальні.", + "runInfoText": "- Біг -\nДля бігу утримуйте БУДЬ-ЯКУ кнопку. Для цього добрі верхні тригери\nабо плечові кнопки, якщо вони у вас є. Бігом пересуватися швидше,\nале важче повертати, так що обережно з обривами.", + "someDaysText": "Іноді просто хочеться що-небудь вдарити. Або підірвати.", + "titleText": "Довідка ${APP_NAME}", + "toGetTheMostText": "Щоб вичавити максимум з цієї гри, вам необхідно:", + "welcomeText": "Ласкаво просимо в ${APP_NAME}!" + }, + "holdAnyButtonText": "<тримати будь-яку кнопку>", + "holdAnyKeyText": "<тримати будь-яку клавішу>", + "hostIsNavigatingMenusText": "- ${HOST} в меню навігації як бос -", + "importPlaylistCodeInstructionsText": "Використовуйте вказаний код щоб імпортувати цей плейлист ще десь:", + "importPlaylistSuccessText": "Імпортовано плейлист ${TYPE} '${NAME}'", + "importText": "Імпорт", + "importingText": "Імпортую...", + "inGameClippedNameText": "У грі буде\n\"${NAME}\"", + "installDiskSpaceErrorText": "ПОМИЛКА: не вдалося завершити встановлення. \nМоже не вистачає вільного місця на вашому \nпристрої. Звільніть місце та спробуйте ще раз.", + "internal": { + "arrowsToExitListText": "Щоб вийти зі списку натисніть ${LEFT} або ${RIGHT}", + "buttonText": "кнопка", + "cantKickHostError": "Неможливо вигнати творця.", + "chatBlockedText": "${NAME} заблокований на ${TIME} секунд.", + "connectedToGameText": "Увійшов в гру '${NAME}'", + "connectedToPartyText": "Увійшов в лобі ${NAME}!", + "connectingToPartyText": "Йде з'єднання...", + "connectionFailedHostAlreadyInPartyText": "З'єднання невдале; хост знаходиться в іншому лобі.", + "connectionFailedPartyFullText": "З'єднання невдале; група повна.", + "connectionFailedText": "З'єднання невдале.", + "connectionFailedVersionMismatchText": "З'єднання невдале; хост використовує іншу версію гри.\nПереконайтеся, що версії обох сторін оновлені, і спробуйте знову.", + "connectionRejectedText": "З'єднання відхилено.", + "controllerConnectedText": "${CONTROLLER} підключено.", + "controllerDetectedText": "Знайдено 1 контролер.", + "controllerDisconnectedText": "${CONTROLLER} від'єднано.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} від'єднано. Спробуйте підключитися ще раз.", + "controllerForMenusOnlyText": "Цей контролер не використовується в грі; тільки в навігаційному меню", + "controllerReconnectedText": "${CONTROLLER} знову підключений.", + "controllersConnectedText": "Підключено ${COUNT} контролерів.", + "controllersDetectedText": "Виявлено ${COUNT} контролерів.", + "controllersDisconnectedText": "Від'єднано ${COUNT} контролерів.", + "corruptFileText": "Виявлено пошкоджені файли. Спробуйте перевстановити або зверніться до ${EMAIL}", + "errorPlayingMusicText": "Помилка відтворення музики: ${MUSIC}", + "errorResettingAchievementsText": "Неможливо скинути онлайн-досягнення, будь ласка, спробуйте пізніше.", + "hasMenuControlText": "${NAME} контролює меню", + "incompatibleNewerVersionHostText": "Хост використовує новішу версію гри.\nОновіться до останньої версії і спробуйте знову.", + "incompatibleVersionHostText": "Хост використовує іншу версію гри.\nПереконайтеся, що обидві ваші версії оновлені, і спробуйте знову.", + "incompatibleVersionPlayerText": "${NAME} використовує іншу версію гри і не може з'єднається.\nПереконайтеся, що обидві ваші версії оновлені, і спробуйте знову.", + "invalidAddressErrorText": "Помилка: неправильний адрес.", + "invalidNameErrorText": "Помилка: некоректне ім'я.", + "invalidPortErrorText": "Помилка: невірний порт.", + "invitationSentText": "Запрошення відправлено.", + "invitationsSentText": "Відправлено ${COUNT} запрошень.", + "joinedPartyInstructionsText": "Хтось увійшов у ваше лобі.\nНатисніть 'Грати' щоб почати гру.", + "keyboardText": "Клавіатура", + "kickIdlePlayersKickedText": "${NAME} викинутий за бездіяльність.", + "kickIdlePlayersWarning1Text": "${NAME} буде викинутий через ${COUNT} секунд при бездіяльності.", + "kickIdlePlayersWarning2Text": "(це можна вимкнути в Налаштування->Додатково)", + "leftGameText": "Покинув '${NAME}'", + "leftPartyText": "Вийшов з лобі ${NAME}.", + "noMusicFilesInFolderText": "У теці немає музичних файлів.", + "playerJoinedPartyText": "${NAME} увійшов в лобі!", + "playerLeftPartyText": "${NAME} покинув лобі.", + "rejectingInviteAlreadyInPartyText": "Запрошення відхилено (вже в лобі).", + "serverRestartingText": "Сервер перезавантажуєтся. Спробуй пізніше...", + "serverShuttingDownText": "Сервер вимикаєтся...", + "signInErrorText": "Помилка входу", + "signInNoConnectionText": "Неможливо увійти. (немає інтернет з'єднання?)", + "telnetAccessDeniedText": "ПОМИЛКА: користувач не надав доступ Telnet.", + "timeOutText": "(залишилося ${TIME} секунд)", + "touchScreenJoinWarningText": "Ви приєдналися з сенсорним екраном.\nЯкщо це була помилка, натисніть 'Меню->Покинути гру'.", + "touchScreenText": "Сенсорний екран", + "unableToResolveHostText": "Помилка: неможливо досягти хоста.", + "unavailableNoConnectionText": "Зараз це недоступно (немає інтернет з'єднання?)", + "vrOrientationResetCardboardText": "Використовуйте це, щоб скинути орієнтацію VR.\nЩоб грати в гру вам буде потрібен зовнішній контролер.", + "vrOrientationResetText": "Скидання орієнтації VR.", + "willTimeOutText": "(час вийде при бездіяльності)" + }, + "jumpBoldText": "СТРИБОК", + "jumpText": "Стрибнути", + "keepText": "Залишити", + "keepTheseSettingsText": "Залишити ці налаштування?", + "keyboardChangeInstructionsText": "Натисни двічи для зміни клавіатури.", + "keyboardNoOthersAvailableText": "Немає інших контроллерів.", + "keyboardSwitchText": "Перемикання контролю на \"${NAME}\".", + "kickOccurredText": "${NAME} вигнали.", + "kickQuestionText": "Вигнати ${NAME}?", + "kickText": "Вигнати", + "kickVoteCantKickAdminsText": "Адміни не можуть бути вигнані.", + "kickVoteCantKickSelfText": "Ви не можете вигнати себе.", + "kickVoteFailedNotEnoughVotersText": "Недостатньо гравців для голосування", + "kickVoteFailedText": "Вигнання не вдалося.", + "kickVoteStartedText": "Голосування за вигнання ${NAME}", + "kickVoteText": "Вигнати", + "kickVotingDisabledText": "Вигнання вимкненно.", + "kickWithChatText": "Наберіть ${YES} для згоди або ${NO} для відмови.", + "killsTallyText": "Убито ${COUNT}", + "killsText": "Убито", + "kioskWindow": { + "easyText": "Легкий", + "epicModeText": "Епічний режим", + "fullMenuText": "Повне меню", + "hardText": "Важкий", + "mediumText": "Середній", + "singlePlayerExamplesText": "Приклади одиночної гри / кооперативу", + "versusExamplesText": "Приклади гри один проти одного" + }, + "languageSetText": "Мова тепер \"${LANGUAGE}\".", + "lapNumberText": "Коло ${CURRENT} / ${TOTAL}", + "lastGamesText": "(останні ${COUNT} ігор)", + "leaderboardsText": "Таблиці лідерів", + "league": { + "allTimeText": "Абсолютні", + "currentSeasonText": "Поточний сезон (${NUMBER})", + "leagueFullText": "Ліга ${NAME}", + "leagueRankText": "Ранг ліги", + "leagueText": "Ліга", + "rankInLeagueText": "#${RANK}, ${NAME} League ${SUFFIX}", + "seasonEndedDaysAgoText": "Сезон завершився ${NUMBER} днів тому.", + "seasonEndsDaysText": "Сезон завершиться через ${NUMBER} днів.", + "seasonEndsHoursText": "Сезон завершиться через ${NUMBER} годин.", + "seasonEndsMinutesText": "Сезон завершиться через ${NUMBER} хвилин.", + "seasonText": "Сезон ${NUMBER}", + "tournamentLeagueText": "Щоб брати участь в цьому турнірі, ви повинні досягти ліги ${NAME}.", + "trophyCountsResetText": "Трофеї будуть скинуті в наступному сезоні." + }, + "levelBestScoresText": "Кращий рекорд на ${LEVEL}", + "levelBestTimesText": "Кращий час на ${LEVEL}", + "levelIsLockedText": "${LEVEL} заблокований.", + "levelMustBeCompletedFirstText": "Спочатку повинен бути пройдений ${LEVEL}.", + "levelText": "Рівень ${NUMBER}", + "levelUnlockedText": "Рівень розблоковано!", + "livesBonusText": "Бонус життів", + "loadingText": "завантаження", + "loadingTryAgainText": "Завантаження; спробуйте знову через кілька секунд ...", + "macControllerSubsystemBothText": "Обидва (не рекомендується)", + "macControllerSubsystemClassicText": "Класичний", + "macControllerSubsystemDescriptionText": "(спробуйте змінити це, якщо ваші контролери неробочі)", + "macControllerSubsystemMFiNoteText": "Made-for-iOS/Mac контролер виявлено;\nМожливо, ви захочете включити це в Налаштування -> Контролери", + "macControllerSubsystemMFiText": "Made-for-iOS/Mac", + "macControllerSubsystemTitleText": "Підтримка контролера", + "mainMenu": { + "creditsText": "Подяки", + "demoMenuText": "Меню прикладів", + "endGameText": "Завершити гру", + "exitGameText": "Вийти з гри", + "exitToMenuText": "Вийти в меню?", + "howToPlayText": "Як грати", + "justPlayerText": "(Тільки ${NAME})", + "leaveGameText": "Покинути гру", + "leavePartyConfirmText": "Дійсно покинути лобі?", + "leavePartyText": "Покинути лобі", + "quitText": "Вийти", + "resumeText": "Продовжити", + "settingsText": "Налаштування" + }, + "makeItSoText": "Поїхали!", + "mapSelectGetMoreMapsText": "Ще карт...", + "mapSelectText": "Вибрати...", + "mapSelectTitleText": "Карти гри ${GAME}", + "mapText": "Карта", + "maxConnectionsText": "Максимум з'єднань", + "maxPartySizeText": "Розмір групи", + "maxPlayersText": "Максимум гравців", + "modeArcadeText": "Аркадний режим", + "modeClassicText": "Класичний режим", + "modeDemoText": "Демо режим", + "mostValuablePlayerText": "Найцінніший гравець", + "mostViolatedPlayerText": "Самий побитий гравець", + "mostViolentPlayerText": "Самий буйний гравець", + "moveText": "Рух", + "multiKillText": "${COUNT} ЗА РАЗ!!!", + "multiPlayerCountText": "${COUNT} гравця", + "mustInviteFriendsText": "Примітка: ви повинні запросити друзів\nна панелі \"${GATHER}\" або приєднати\nконтролери для спільної гри.", + "nameBetrayedText": "${NAME} зрадив ${VICTIM}.", + "nameDiedText": "${NAME} помер.", + "nameKilledText": "${NAME} вбив ${VICTIM}.", + "nameNotEmptyText": "Ім'я не може бути порожнім!", + "nameScoresText": "${NAME} веде!", + "nameSuicideKidFriendlyText": "${NAME} вбився.", + "nameSuicideText": "${NAME} скоїв суїцид.", + "nameText": "Ім'я", + "nativeText": "Власний", + "newPersonalBestText": "Новий особистий рекорд!", + "newTestBuildAvailableText": "Доступна нова тестова версія! (${VERSION} збірка ${BUILD}).\nОновити: ${ADDRESS}", + "newText": "Новий", + "newVersionAvailableText": "Доступна нова версія ${APP_NAME}! (${VERSION})", + "nextAchievementsText": "Наступні досягнення:", + "nextLevelText": "Наступний рівень", + "noAchievementsRemainingText": "- немає", + "noContinuesText": "(без продовжень)", + "noExternalStorageErrorText": "На цьому пристрої не знайдено зовнішньої пам'яті", + "noGameCircleText": "Помилка: не ввійшли в GameCircle", + "noScoresYetText": "Рахунка поки немає.", + "noThanksText": "Ні дякую", + "noTournamentsInTestBuildText": "ВАЖЛИВО: Турнірні Очки на цій тестовій версії будуть ігноровані.", + "noValidMapsErrorText": "Для даного типу гри не знайдено коректних карт.", + "notEnoughPlayersRemainingText": "Залишилося недостатньо гравців; вийдіть і почніть нову гру.", + "notEnoughPlayersText": "Для початку цієї гри потрібно як мінімум ${COUNT} гравця!", + "notNowText": "Не зараз", + "notSignedInErrorText": "Увійдіть щоб зробити це.", + "notSignedInGooglePlayErrorText": "Увійдіть спочатку в Google Play, а там подивимося.", + "notSignedInText": "(ви не ввійшли)", + "nothingIsSelectedErrorText": "Нічого не вибрано!", + "numberText": "#${NUMBER}", + "offText": "Викл", + "okText": "Ок", + "onText": "Вкл", + "oneMomentText": "Хвилинку...", + "onslaughtRespawnText": "${PLAYER} відродиться в ${WAVE} хвилі", + "orText": "${A} або ${B}", + "otherText": "Інші...", + "outOfText": "(${RANK} з ${ALL})", + "ownFlagAtYourBaseWarning": "Щоб набрати очки, ваш власний\nпрапор повинен бути на вашій базі!", + "packageModsEnabledErrorText": "Мережева гра заборонена коли включені моди локального пакета (див. Налаштування->Додатково)", + "partyWindow": { + "chatMessageText": "Повідомлення чату", + "emptyText": "Ваше лобі порожнє", + "hostText": "(хост)", + "sendText": "Відправити", + "titleText": "Ваше лобі" + }, + "pausedByHostText": "(зупинено хостом)", + "perfectWaveText": "Ідеальна хвиля!", + "pickUpText": "Підняти", + "playModes": { + "coopText": "Кооператив", + "freeForAllText": "Кожен сам за себе", + "multiTeamText": "Мультикомандний", + "singlePlayerCoopText": "Самітня гра / Кооператив", + "teamsText": "Команди" + }, + "playText": "Грати", + "playWindow": { + "oneToFourPlayersText": "1-4 гравця", + "titleText": "Грати", + "twoToEightPlayersText": "2-8 гравців" + }, + "playerCountAbbreviatedText": "${COUNT}г", + "playerDelayedJoinText": "${PLAYER} приєднається в наступному раунді.", + "playerInfoText": "Про Гравця", + "playerLeftText": "${PLAYER} покинув гру.", + "playerLimitReachedText": "Досягнуто ліміт ${COUNT} гравців. Більше додати не можна.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Ви не можете видалити профіль акаунта.", + "deleteButtonText": "Видалити\nпрофіль", + "deleteConfirmText": "Видалити '${PROFILE}'?", + "editButtonText": "Змінити\nпрофіль", + "explanationText": "(налаштовуйте імена і зовнішній вигляд гравця)", + "newButtonText": "Новий\nпрофіль", + "titleText": "Профілі гравців" + }, + "playerText": "Гравець", + "playlistNoValidGamesErrorText": "Цей плейлист містить невідкриті ігри.", + "playlistNotFoundText": "Плейлист не знайдено", + "playlistText": "Плейлист", + "playlistsText": "Плейлисти", + "pleaseRateText": "Якщо вам подобається гра ${APP_NAME}, будь ласка, подумайте про те,\nщоб оцінити її або написати рецензію. Це забезпечує корисну\nзворотний зв'язок і допомагає підтримати подальшу розробку.\n\nДякуємо!\n-Ерік", + "pleaseWaitText": "Будь ласка зачекайте...", + "pluginsDetectedText": "Новий плагін(и) виявлені.Ввімкніть/підтвердіть це в налаштуваннях.", + "pluginsText": "Плагіни", + "practiceText": "Тренування", + "pressAnyButtonPlayAgainText": "Натисніть будь-яку кнопку щоб грати знову...", + "pressAnyButtonText": "Натисніть будь-яку кнопку щоб продовжити...", + "pressAnyButtonToJoinText": "натисніть будь-яку кнопку щоб приєднатися...", + "pressAnyKeyButtonPlayAgainText": "Натисніть будь-яку клавішу/кнопку щоб грати знову...", + "pressAnyKeyButtonText": "Натисніть будь-яку клавішу/кнопку щоб продовжити...", + "pressAnyKeyText": "Натисніть будь-яку клавішу...", + "pressJumpToFlyText": "** Щоб летіти, продовжуйте натискати стрибок **", + "pressPunchToJoinText": "натисніть УДАР щоб приєднатися...", + "pressToOverrideCharacterText": "натисніть ${BUTTONS} щоб перевизначити свого персонажа", + "pressToSelectProfileText": "натисніть ${BUTTONS} щоб вибрати гравця", + "pressToSelectTeamText": "натисніть ${BUTTONS} щоб вибрати команду", + "promoCodeWindow": { + "codeText": "Код", + "enterText": "Відправити" + }, + "promoSubmitErrorText": "Помилка відправки коду, перевірте своє інтернет-з'єднання", + "ps3ControllersWindow": { + "macInstructionsText": "Вимкніть живлення на задній панелі PS3, переконайтеся, що Bluetooth\nвключений на вашому комп'ютері, а потім підключіть контролер до Mac\nза допомогою кабелю USB для синхронізації. Тепер можна використовувати\nкнопку контролера 'PS' щоб підключити його до Mac\nв дротовому (USB) або бездротовому (Bluetooth) режимі.\n\nНа деяких Mac при синхронізації може знадобитися введення пароля.\nВ цьому випадку зверніться до наступної інструкції або до Google.\n\n\n\n\nКонтролери PS3, пов'язані по бездротовій мережі, повинні з'явитися\nв списку пристроїв в Налаштуваннях системи->Bluetooth. Можливо, \nвам доведеться видалити їх з цього списку, якщо ви хочете знову \nвикористовувати їх з PS3.\n\nТакож завжди відключайте їх від Bluetooth, коли він не використовується,\nінакше будуть розряджатися батарейки.\n\nBluetooth повинен обробляти до 7 підключених пристроїв,\nхоча у вас може вийти по-іншому.", + "ouyaInstructionsText": "Щоб використовувати контролер PS3 з OUYA, просто підключіть його один раз\nза допомогою кабелю USB для синхронізації. Це може відключити інші\nконтролери, тоді потрібно перезавантажити OUYA і від'єднати кабель USB.\n\nПісля цього можна використовувати кнопку 'PS' контролера для бездротового\nпідключення. Після гри натисніть і утримуйте кнопку 'PS' протягом\n10 секунд щоб вимкнути контролер, в іншому випадку він може\nзалишитися включеним і розрядить батарейки.", + "pairingTutorialText": "відео-інструкції по синхронізації", + "titleText": "Використання контролерів PS3 з ${APP_NAME}:" + }, + "punchBoldText": "УДАР", + "punchText": "Вдарити", + "purchaseForText": "Купити за ${PRICE}", + "purchaseGameText": "Купити гру", + "purchasingText": "Покупка...", + "quitGameText": "Вийти з ${APP_NAME}?", + "quittingIn5SecondsText": "Вихід через 5 секунд...", + "randomPlayerNamesText": "Петро, Василь, Микола, Дацюк, Андрюха, Козак, Сашка, Володька, Вітальон, Сака, Пупчик, Олька", + "randomText": "Випадковий", + "rankText": "Ранг", + "ratingText": "Рейтинг", + "reachWave2Text": "Досягніть другої хвилі, щоб отримати ранг.", + "readyText": "готовий", + "recentText": "останній", + "remoteAppInfoShortText": "Грати в ${APP_NAME} з сім'єю або друзями набагато веселіше.\nПідключіть один або кілька контролерів або встановіть\n${REMOTE_APP_NAME} на свої пристрої, щоб використовувати\nїх в якості контролерів.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Положення кнопки", + "button_size": "Розмір кнопки", + "cant_resolve_host": "Сервер не знайдений.", + "capturing": "Слухаю...", + "connected": "Пов'язано.", + "description": "Використовуйте Ваш телефон або планшет як контролер BombSquad.\nДо 8 пристроїв можуть бути одночасно підключені для епічних битв в мультиплеєрі на одному TV або планшеті.", + "disconnected": "Викинутий сервером.", + "dpad_fixed": "нерухомий", + "dpad_floating": "плаваючий", + "dpad_position": "Розташування D-Pad", + "dpad_size": "Розмір D-Pad", + "dpad_type": "Тип D-Pad", + "enter_an_address": "Введіть адресу", + "game_full": "Кімната заповнена або з'єднання не приймаються.", + "game_shut_down": "Сервер відключився.", + "hardware_buttons": "Апаратні кнопки.", + "join_by_address": "Приєднатися за адресою...", + "lag": "Лаг: ${SECONDS} секунд", + "reset": "Скидання", + "run1": "Біг 1", + "run2": "Біг 2", + "searching": "Шукаємо ігри BombSquad...", + "searching_caption": "Натисніть на назву гри для входу.\nПереконайтеся, що ви перебуваєте в тій же мережі, що і гра.", + "start": "Старт", + "version_mismatch": "Розбіжність версій.\nПереконайтеся, що BombSquad і контролер BombSquad Remote\nоновлені до останньої версії і спробуйте ще раз." + }, + "removeInGameAdsText": "Розблокуйте \"${PRO}\" в магазині, щоб прибрати рекламу в грі.", + "renameText": "Перейменувати", + "replayEndText": "Завершити перегляд повтору", + "replayNameDefaultText": "Повтор останньої гри", + "replayReadErrorText": "Помилка читання файлу повтору.", + "replayRenameWarningText": "Перейменуйте повтор \"${REPLAY}\" щоб його зберегти; інакше він буде переписаний.", + "replayVersionErrorText": "На жаль, цей повтор було зроблено в інший\nверсії гри і не може бути використана.", + "replayWatchText": "Дивитися повтор", + "replayWriteErrorText": "Помилка запису файлу повтору.", + "replaysText": "Повтори", + "reportPlayerExplanationText": "Поскаржіться за цією адресою на чітера, грубіяна або того хто погано поводиться.\nОпишіть чим ви незадоволені:", + "reportThisPlayerCheatingText": "Нечесний гравець", + "reportThisPlayerLanguageText": "Грубіян", + "reportThisPlayerReasonText": "На що хочете поскаржитися?", + "reportThisPlayerText": "Скарга на Гравця", + "requestingText": "Запит даних...", + "restartText": "Почати заново", + "retryText": "Повтор", + "revertText": "Відновити", + "runText": "Бігти", + "saveText": "Зберегти", + "scanScriptsErrorText": "Помилка(и) сканування скриптів; дивіться лог для деталей.", + "scoreChallengesText": "Медалі за очки", + "scoreListUnavailableText": "Список очок недоступний.", + "scoreText": "Очки", + "scoreUnits": { + "millisecondsText": "Мілісекунди", + "pointsText": "Бали", + "secondsText": "Секунди" + }, + "scoreWasText": "(було ${COUNT})", + "selectText": "Вибрати", + "seriesWinLine1PlayerText": "ПЕРЕМІГ У", + "seriesWinLine1TeamText": "ПЕРЕМОГЛИ У", + "seriesWinLine1Text": "ПЕРЕМІГ У", + "seriesWinLine2Text": "СЕРІЇ!", + "settingsWindow": { + "accountText": "Акаунт", + "advancedText": "Додатково", + "audioText": "Аудіо", + "controllersText": "Контролери", + "graphicsText": "Графіка", + "playerProfilesMovedText": "Примітка: Користувачі гравців були переміщені у вікно Акаунти в головному меню.", + "titleText": "Налаштування" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(проста, зручна для контролера віртуальна клавіатура для введення тексту)", + "alwaysUseInternalKeyboardText": "Завжди використовувати вбудовану клавіатуру", + "benchmarksText": "Тест продуктивності і навантаження", + "disableCameraGyroscopeMotionText": "Вимкнути Рух Гіроскопу Камери", + "disableCameraShakeText": "Вимкнути Тремтіння Камери", + "disableThisNotice": "(ви можете відключити це повідомлення в налаштуваннях)", + "enablePackageModsDescriptionText": "(включає додаткові можливості для модінгу, але відключає мережеву гру)", + "enablePackageModsText": "Включити моди локального пакета", + "enterPromoCodeText": "Ввести код", + "forTestingText": "Примітка: ці значення використовуються тільки для тестування і будуть втрачені, коли додаток завершить роботу.", + "helpTranslateText": "Переклади гри ${APP_NAME} з англійської зроблені спільнотою. \nЯкщо ви хочете запропонувати або виправити переклад, \nПерейдіть за посиланню нижче. Заздалегідь дякую!", + "kickIdlePlayersText": "Викидати не активних гравців", + "kidFriendlyModeText": "Сімейний режим (менше насильства, і т.д.)", + "languageText": "Мова", + "moddingGuideText": "Інструкція по модінгу", + "mustRestartText": "Необхідно перезапустити гру, щоб зміни вступили в силу.", + "netTestingText": "Тестування мережі", + "resetText": "Скинути", + "showBombTrajectoriesText": "Показувати траєкторію бомби", + "showPlayerNamesText": "Показувати імена гравців", + "showUserModsText": "Показати теку модів", + "titleText": "Додатково", + "translationEditorButtonText": "Редактор перекладу ${APP_NAME}", + "translationFetchErrorText": "статус перекладу недоступний", + "translationFetchingStatusText": "перевірка статусу перекладу...", + "translationInformMe": "Повідомляйте мене, коли моя мова потребує оновлення", + "translationNoUpdateNeededText": "дана мова повністю оновлена, ура!", + "translationUpdateNeededText": "** дана мова потребує оновлення!! **", + "vrTestingText": "Тестування VR" + }, + "shareText": "Поділитися", + "sharingText": "Ділимося...", + "showText": "Показати", + "signInForPromoCodeText": "Ви повинні увійти, для активації коду.", + "signInWithGameCenterText": "Щоб використовувати аккаунт GameCenter,\nувійдіть через GameCenter.", + "singleGamePlaylistNameText": "Просто ${GAME}", + "singlePlayerCountText": "1 гравець", + "soloNameFilterText": "${NAME} соло", + "soundtrackTypeNames": { + "CharSelect": "Вибір персонажа", + "Chosen One": "Обраний", + "Epic": "Ігри в епічному режимі", + "Epic Race": "Епічна гонка", + "FlagCatcher": "Захоплення прапору", + "Flying": "Щасливі думки", + "Football": "Регбі", + "ForwardMarch": "Напад", + "GrandRomp": "Завоювання", + "Hockey": "Хокей", + "Keep Away": "Не підходити!", + "Marching": "Маневр", + "Menu": "Головне меню", + "Onslaught": "Атака", + "Race": "Гонка", + "Scary": "Король гори", + "Scores": "Результати", + "Survival": "Ліквідація", + "ToTheDeath": "Смертельний бій", + "Victory": "Табло фінального рахунку" + }, + "spaceKeyText": "пробіл", + "statsText": "Статистика", + "storagePermissionAccessText": "Це вимагає доступу до сховища", + "store": { + "alreadyOwnText": "У вас вже є ${NAME}!", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• Прибирає рекламу\n• Відкриє міні-ігри\n• Також включає:", + "buyText": "Купити", + "charactersText": "Персонажі", + "comingSoonText": "Скоро в ефірі...", + "extrasText": "Додатково", + "freeBombSquadProText": "BombSquad тепер безкоштовна гра, але якщо ви придбали її раніше, то ви\nотримуєте оновлення BombSquad Pro і ${COUNT} квитків в якості подяки.\nНасолоджуйтесь новими можливостями, і спасибі за вашу підтримку!\n-Ерік", + "holidaySpecialText": "Святкова акція", + "howToSwitchCharactersText": "(зайдіть в \"${SETTINGS} -> ${PLAYER_PROFILES}\", щоб вибрати і змінити персонажів)", + "howToUseIconsText": "(створіть глобальні профілі гравців (у вікні акаунт) щоб використовувати це)", + "howToUseMapsText": "(використовуйте ці карти в ваших власних командних/всі-проти-всіх плейлистах)", + "iconsText": "Іконки", + "loadErrorText": "Неможливо завантажити сторінку.\nПеревірте з'єднання з Інтернетом.", + "loadingText": "завантаження", + "mapsText": "Карти", + "miniGamesText": "Міні-ігри", + "oneTimeOnlyText": "(тільки один раз)", + "purchaseAlreadyInProgressText": "Ця покупка вже виконується.", + "purchaseConfirmText": "Купити ${ITEM}?", + "purchaseNotValidError": "Купівля недійсна.\nЯкщо це помилка, зверніться до ${EMAIL}.", + "purchaseText": "Купити", + "saleBundleText": "Розпродаж в комплекті!", + "saleExclaimText": "Розпродаж!", + "salePercentText": "(Знижка ${PERCENT}%)", + "saleText": "ЗНИЖКА", + "searchText": "Пошук", + "teamsFreeForAllGamesText": "Командні ігри / Кожен сам за себе", + "totalWorthText": "*** ${TOTAL_WORTH} значення! ***", + "upgradeQuestionText": "Оновити?", + "winterSpecialText": "Зимова акція", + "youOwnThisText": "- у вас це вже є -" + }, + "storeDescriptionText": "Ігрове безумство з 8 гравцями!\n\nПідривайте своїх друзів (або ботів) в турнірі вибухових міні-ігор, таких як Захоплення прапора і Епічний смертельний бій уповільненої дії!\n\nЗ простим управлінням і розширеною підтримкою контролерів 8 осіб можуть приєднатися до гри, можна навіть використовувати мобільні пристрої як контролери через безкоштовний додаток 'BombSquad Remote'!\n\nДо бою!\n\nДив. www.froemling.net/BombSquad для додаткової інформації.", + "storeDescriptions": { + "blowUpYourFriendsText": "Підірви друзів.", + "competeInMiniGamesText": "Змагайтесь в міні-іграх від гонок до левітації.", + "customize2Text": "Налаштування персонажів, міні-ігор і навіть саундтрека.", + "customizeText": "Налаштування персонажів і створення своїх власних плейлистів міні-ігор.", + "sportsMoreFunText": "Спорт веселіший з вибухівкою.", + "teamUpAgainstComputerText": "Команда проти комп'ютера." + }, + "storeText": "Магазин", + "submitText": "Відправити", + "submittingPromoCodeText": "Активація коду...", + "teamNamesColorText": "імена/кольори команд", + "telnetAccessGrantedText": "Доступ Telnet включений.", + "telnetAccessText": "Виявлено доступ Telnet. Дозволити?", + "testBuildErrorText": "Ця версія застаріла, будь ласка, перевірте оновлення.", + "testBuildText": "Тестова версія", + "testBuildValidateErrorText": "Не вдається перевірити тестову збірку. (немає з'єднання з мережею?)", + "testBuildValidatedText": "Тестова збірка перевірена. Насолоджуйтесь!", + "thankYouText": "Дякую за вашу підтримку! Веселої гри!!", + "threeKillText": "ТРЬОХ ЗА РАЗ!!", + "timeBonusText": "Бонус часу", + "timeElapsedText": "Пройшло часу", + "timeExpiredText": "Час вийшов", + "timeSuffixDaysText": "${COUNT}д", + "timeSuffixHoursText": "${COUNT}г", + "timeSuffixMinutesText": "${COUNT}х", + "timeSuffixSecondsText": "${COUNT}с", + "tipText": "Підказка", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Топ друзів", + "tournamentCheckingStateText": "Перевірка статусу турніру, будь ласка, зачекайте...", + "tournamentEndedText": "Турнір закінчився. Скоро почнеться новий.", + "tournamentEntryText": "Вхід в турнір", + "tournamentResultsRecentText": "Останні Результати турніру", + "tournamentStandingsText": "Позиції в турнірі", + "tournamentText": "Турнір", + "tournamentTimeExpiredText": "Час турніру минув", + "tournamentsText": "Турніри", + "translations": { + "characterNames": { + "Agent Johnson": "Агент Джонсон", + "B-9000": "B-9000", + "Bernard": "Бернард", + "Bones": "Кістячок", + "Butch": "Силач", + "Easter Bunny": "Великодній Кролик", + "Flopsy": "Флопс", + "Frosty": "Морозний", + "Gretel": "Гретель", + "Grumbledorf": "Грамблдорф", + "Jack Morgan": "Джек Морган", + "Kronk": "Кронк", + "Lee": "Лі", + "Lucky": "Щасливчик", + "Mel": "Мел", + "Middle-Man": "Середняк", + "Minimus": "Мінімус", + "Pascal": "Паскаль", + "Pixel": "Піксель", + "Sammy Slam": "Семі Слем", + "Santa Claus": "Санта Клаус", + "Snake Shadow": "Тінь Змії", + "Spaz": "Спаз", + "Taobao Mascot": "Талісман Таобао", + "Todd McBurton": "Тод МакБартон", + "Zoe": "Зої", + "Zola": "Зола" + }, + "coopLevelNames": { + "${GAME} Training": "Тренування ${GAME}", + "Infinite ${GAME}": "Нескінченний рівень ${GAME}", + "Infinite Onslaught": "Нескінченна атака", + "Infinite Runaround": "Нескінченний маневр", + "Onslaught Training": "Атака: тренування", + "Pro ${GAME}": "${GAME} Про", + "Pro Football": "Регбі профі", + "Pro Onslaught": "Атака профі", + "Pro Runaround": "Маневр профі", + "Rookie ${GAME}": "${GAME} для новачків", + "Rookie Football": "Регбі для новачків", + "Rookie Onslaught": "Атака для новачків", + "The Last Stand": "Останній рубіж", + "Uber ${GAME}": "Убер ${GAME}", + "Uber Football": "Убер регбі", + "Uber Onslaught": "Убер атака", + "Uber Runaround": "Убер маневр" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Щоб перемогти, стань обраним на деякий час.\nЩоб стати обраним, вбий обраного.", + "Bomb as many targets as you can.": "Підірвіть стільки мішеней, скільки зможете.", + "Carry the flag for ${ARG1} seconds.": "Проносіть прапор протягом ${ARG1} секунд.", + "Carry the flag for a set length of time.": "Проносіть прапор протягом заданого часу.", + "Crush ${ARG1} of your enemies.": "Розбийте ${ARG1} ворогів.", + "Defeat all enemies.": "Переможіть всіх ворогів.", + "Dodge the falling bombs.": "Ухиліться від падаючих бомб.", + "Final glorious epic slow motion battle to the death.": "Фінальна епічна смертельна битва уповільненої дії.", + "Gather eggs!": "Зберіть яйця!", + "Get the flag to the enemy end zone.": "Віднесіть прапор в зону захисту супротивника.", + "How fast can you defeat the ninjas?": "Як швидко ви зможете перемогти ніндзя?", + "Kill a set number of enemies to win.": "Вбийте заданий число ворогів, щоб виграти.", + "Last one standing wins.": "Перемагає останній стоячий на ногах.", + "Last remaining alive wins.": "Перемагає останній живий.", + "Last team standing wins.": "Перемагає остання стояща на ногах команда.", + "Prevent enemies from reaching the exit.": "Не дайте ворогам дійти до виходу.", + "Reach the enemy flag to score.": "Доберіться до ворожого прапора щоб набрати очки.", + "Return the enemy flag to score.": "Принесіть ворожий прапор на базу, щоб набрати очки.", + "Run ${ARG1} laps.": "Пробіжіть кіл: ${ARG1}.", + "Run ${ARG1} laps. Your entire team has to finish.": "Пробіжіть кіл: ${ARG1}. Фінішувати повинна вся команда.", + "Run 1 lap.": "Пробіжіть 1 коло.", + "Run 1 lap. Your entire team has to finish.": "Пробіжіть 1 коло. Фінішувати повинна вся команда.", + "Run real fast!": "Біжіть дуже швидко!", + "Score ${ARG1} goals.": "Забийте голів: ${ARG1}.", + "Score ${ARG1} touchdowns.": "Зробіть тачдаунів: ${ARG1}.", + "Score a goal.": "Забийте гол.", + "Score a touchdown.": "Зробіть тачдаун.", + "Score some goals.": "Забийте кілька голів.", + "Secure all ${ARG1} flags.": "Захопіть всі прапори: ${ARG1}.", + "Secure all flags on the map to win.": "Захопіть всі прапори на карті, щоб виграти.", + "Secure the flag for ${ARG1} seconds.": "захопіть прапор на ${ARG1} секунд.", + "Secure the flag for a set length of time.": "Захопіть прапор на певний час.", + "Steal the enemy flag ${ARG1} times.": "Вкрадіть ворожий прапор ${ARG1} раз.", + "Steal the enemy flag.": "Вкрадіть ворожий прапор.", + "There can be only one.": "Може бути тільки один.", + "Touch the enemy flag ${ARG1} times.": "Торкніться ворожого прапора ${ARG1} раз.", + "Touch the enemy flag.": "Торкніться ворожого прапора.", + "carry the flag for ${ARG1} seconds": "проносіть прапор протягом ${ARG1} секунд", + "kill ${ARG1} enemies": "Вбийте ${ARG1} ворогів", + "last one standing wins": "перемагає останній стоячий на ногах", + "last team standing wins": "перемагає остання стояща на ногах команда", + "return ${ARG1} flags": "принесіть ${ARG1} прапорів на базу", + "return 1 flag": "принесіть 1 прапор на базу", + "run ${ARG1} laps": "пробіжіть кіл: ${ARG1}", + "run 1 lap": "пробіжіть 1 коло", + "score ${ARG1} goals": "забийте голів: ${ARG1}", + "score ${ARG1} touchdowns": "зробіть тачдаунів: ${ARG1}", + "score a goal": "забийте гол", + "score a touchdown": "зробіть тачдаун", + "secure all ${ARG1} flags": "захопіть всі прапори: ${ARG1}", + "secure the flag for ${ARG1} seconds": "захопіть прапор на ${ARG1} секунд", + "touch ${ARG1} flags": "торкніться прапорів: ${ARG1}", + "touch 1 flag": "торкніться 1 прапора" + }, + "gameNames": { + "Assault": "Напад", + "Capture the Flag": "Захоплення прапору", + "Chosen One": "Обраний", + "Conquest": "Завоювання", + "Death Match": "Смертельний бій", + "Easter Egg Hunt": "Полювання на крашанки", + "Elimination": "Ліквідація", + "Football": "Регбі", + "Hockey": "Хокей", + "Keep Away": "Не підходити!", + "King of the Hill": "Король гори", + "Meteor Shower": "Метеоритний дощ", + "Ninja Fight": "Бій з ніндзя", + "Onslaught": "Атака", + "Race": "Гонка", + "Runaround": "Обхід", + "Target Practice": "Стрілянина по мішенях", + "The Last Stand": "Останній рубіж" + }, + "inputDeviceNames": { + "Keyboard": "Клавіатура", + "Keyboard P2": "Клавіатура P2" + }, + "languages": { + "Arabic": "Арабська", + "Belarussian": "Білоруська", + "Chinese": "Китайська", + "ChineseTraditional": "Китайська традиційна", + "Croatian": "Хорватська", + "Czech": "Чеська", + "Danish": "Данська", + "Dutch": "Голландська", + "English": "Англійська", + "Esperanto": "Есперанто", + "Finnish": "Фінська", + "French": "Французька", + "German": "Німецька", + "Gibberish": "Гіберіш", + "Greek": "Грецький", + "Hindi": "Хінді", + "Hungarian": "Угорська", + "Indonesian": "Індонезійська", + "Italian": "Італійська", + "Japanese": "Японська", + "Korean": "Корейська", + "Persian": "Перська", + "Polish": "Польська", + "Portuguese": "Португальська", + "Romanian": "Румунська", + "Russian": "Російська", + "Serbian": "Сербська", + "Slovak": "Словацька", + "Spanish": "Іспанська", + "Swedish": "Шведська", + "Turkish": "Турецька", + "Ukrainian": "Українська", + "Venetian": "Венеціанський", + "Vietnamese": "В'єьнамська" + }, + "leagueNames": { + "Bronze": "Бронзова", + "Diamond": "Діамантова", + "Gold": "Золота", + "Silver": "Срібна" + }, + "mapsNames": { + "Big G": "Велика G", + "Bridgit": "Містки", + "Courtyard": "Внутрішній двір", + "Crag Castle": "Замок на скелі", + "Doom Shroom": "Фатальний гриб", + "Football Stadium": "Стадіон регбі", + "Happy Thoughts": "Щасливі думки", + "Hockey Stadium": "Хокейний стадіон", + "Lake Frigid": "Крижане озеро", + "Monkey Face": "Мавпа", + "Rampage": "Беззаконня", + "Roundabout": "Кільцева", + "Step Right Up": "Проходьте", + "The Pad": "Подушка", + "Tip Top": "Тіп-Топ", + "Tower D": "Вежа D", + "Zigzag": "Зигзаг" + }, + "playlistNames": { + "Just Epic": "Тільки епічний", + "Just Sports": "Тільки спорт" + }, + "scoreNames": { + "Flags": "Прапори", + "Goals": "Голи", + "Score": "Очки", + "Survived": "Вижив", + "Time": "Час", + "Time Held": "Час утримання" + }, + "serverResponses": { + "A code has already been used on this account.": "Код вже був активований на цьому акаунті.", + "A reward has already been given for that address.": "Ця нагорода вже була видана на цей ip адреса", + "Account linking successful!": "Акаунт успішно прив'язаний!", + "Account unlinking successful!": "Акаунт роз'єднаному успішно!", + "Accounts are already linked.": "Акаунти вже прив'язані.", + "An error has occurred; (${ERROR})": "Сталася помилка; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Сталася помилка; будь ласка, зв'яжіться зі службою підтримки (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Сталася помилка; будь ласка, зв'яжіться з support@froemling.net.", + "An error has occurred; please try again later.": "Сталася помилка, будь ласка, спробуйте ще раз пізніше.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Точно хочете зв'язати акаунти?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nЦе не може бути скасоване!", + "BombSquad Pro unlocked!": "BombSquad Pro розблоковано!", + "Can't link 2 accounts of this type.": "Неможливо зв'язати 2 акаунти цього типу.", + "Can't link 2 diamond league accounts.": "Неможливо зв'язати 2 акаунти діамантовою ліги.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Неможливо зв'язати; буде перевищено максимум ${COUNT} зв'язаних акаунтів.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Ах ти, чітер; Очки і нагороди заморожені на ${COUNT} днів.", + "Could not establish a secure connection.": "Не вдалося встановити безпечне з'єднання.", + "Daily maximum reached.": "Досить на сьогодні.", + "Entering tournament...": "Вхід в турнір...", + "Invalid code.": "Невірний код.", + "Invalid payment; purchase canceled.": "Щось пішло не так. Купівля скасована.", + "Invalid promo code.": "Невірний промокод.", + "Invalid purchase.": "Помилка транзакції.", + "Invalid tournament entry; score will be ignored.": "Невірна заявка на турнір; рахунок буде проігнорований.", + "Item unlocked!": "Предмет разблоковано!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "Об'єднання не дозволено. ${ACCOUNT} містить \nважливі дані і вони ВСІ ЗНИКНУТЬ.\nТи можеш об'єднанати у зворотньому порядку, якщо ти цього захочеш\n(І втратити дані ЦЬОГО акаунта)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Зв'язати ${ACCOUNT} акаунт з цим?\nВсі існуючі дані на ${ACCOUNT} будуть втрачені.\nЦя дія не може бути скасовано. Ви впевнені?", + "Max number of playlists reached.": "Досягнуто максимальну кількість плейлистів.", + "Max number of profiles reached.": "Максимальна кількість профілів досягнута.", + "Maximum friend code rewards reached.": "Досягнуто ліміт кодів", + "Message is too long.": "Повідомлення занадто довге.", + "Profile \"${NAME}\" upgraded successfully.": "Профіль \"${NAME}\" оновлений успішно.", + "Profile could not be upgraded.": "Профіль не може бути оновлений.", + "Purchase successful!": "Успішна транзакція!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Отримано ${COUNT} квитків за вхід.\nПриходьте завтра, щоб отримати ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Функціональність сервера більше не підтримується в цій версії гри;\nБудь ласка оновіть гру.", + "Sorry, there are no uses remaining on this code.": "Ой, код більше не може бути активований.", + "Sorry, this code has already been used.": "Ой, цей код вже використаний.", + "Sorry, this code has expired.": "Ой, час дії коду минув.", + "Sorry, this code only works for new accounts.": "Ой, цей код працює тільки для нових акаунтів.", + "Temporarily unavailable; please try again later.": "Тимчасово недоступний; будь ласка, спробуйте ще раз пізніше.", + "The tournament ended before you finished.": "Турнір закінчився раніше, ніж ви закінчили.", + "This account cannot be unlinked for ${NUM} days.": "Цей обліковий запис неможливо відв'язати протягом ${NUM} днів.", + "This code cannot be used on the account that created it.": "Цей код не може бути використаний його творцем.", + "This is currently unavailable; please try again later.": "Це зараз не доступно; будь ласка, спробуйте ще раз пізніше.", + "This requires version ${VERSION} or newer.": "Для цього необхідна версія ${VERSION} або новіша.", + "Tournaments disabled due to rooted device.": "Турніри вимкнені через рутований пристрій.", + "Tournaments require ${VERSION} or newer": "Для турнірів потрібна версія ${VERSION} або вище.", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Відв'язати ${ACCOUNT} від цього акаунта?\nВсі дані на ${ACCOUNT} будуть скинуті.\n(За винятком досягнень в деяких випадках)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ПОПЕРЕДЖЕННЯ: скарги на хакерство були видані на ваш обліковий запис.\nОблікові записи, які вважаються зламаними, будуть заблоковані. Будь ласка, грайте чесно.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Бажаєте пов'язати обліковий запис пристрою з цим?\n\nАкаунт пристрою ${ACCOUNT1}\nПоточний акаунт ${ACCOUNT2}\n\nЦе дозволить зберегти ваші нинішні досягнення.\nУвага: скасування неможлива!", + "You already own this!": "Ви це вже придбали!", + "You can join in ${COUNT} seconds.": "Ти зможеш увійти через ${COUNT} секунд", + "You don't have enough tickets for this!": "У вас недостатньо квитків для цієї покупки!", + "You don't own that.": "Вам це не належить", + "You got ${COUNT} tickets!": "Ви отримали ${COUNT} квитків!", + "You got a ${ITEM}!": "Ви отримали ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Вас підвищили і перевели в нову лігу; вітаємо!", + "You must update to a newer version of the app to do this.": "Щоб це зробити, ви повинні оновити додаток.", + "You must update to the newest version of the game to do this.": "Ви повинні оновитися до нової версії гри, щоб зробити це.", + "You must wait a few seconds before entering a new code.": "Зачекайте кілька секунд, перш ніж вводити новий код.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Ваш ранг в останньому турнірі: ${RANK}! Дякуємо за гру!", + "Your account was rejected. Are you signed in?": "Вашу обліковку відхилено. Ви ввійшли?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Ваша версія гри була модифікована.\nПриберіть всі зміни і спробуйте знову", + "Your friend code was used by ${ACCOUNT}": "Ваш код був використаний ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 хвилина", + "1 Second": "1 секунда", + "10 Minutes": "10 хвилин", + "2 Minutes": "2 хвилини", + "2 Seconds": "2 секунди", + "20 Minutes": "20 хвилин", + "4 Seconds": "4 секунди", + "5 Minutes": "5 хвилин", + "8 Seconds": "8 секунд", + "Allow Negative Scores": "Дозволити негативний рахунок", + "Balance Total Lives": "Розподіляти всі життя", + "Bomb Spawning": "Поява бомб", + "Chosen One Gets Gloves": "Обраний отримує рукавички", + "Chosen One Gets Shield": "Обраний отримує щит", + "Chosen One Time": "Час обраного", + "Enable Impact Bombs": "Включити розривні бомби", + "Enable Triple Bombs": "Включити потрійні бомби", + "Entire Team Must Finish": "Вся команда повинна фінішувати", + "Epic Mode": "Епічний режим", + "Flag Idle Return Time": "Час повернення кинутого прапора", + "Flag Touch Return Time": "Час захоплення прапора", + "Hold Time": "Час утримання", + "Kills to Win Per Player": "Вбивств що перемогти", + "Laps": "Кола", + "Lives Per Player": "Життів на гравця", + "Long": "Довго", + "Longer": "Довше", + "Mine Spawning": "Мінування", + "No Mines": "Без мін", + "None": "Немає", + "Normal": "Нормальний", + "Pro Mode": "Професійний режим", + "Respawn Times": "Час до воскресіння", + "Score to Win": "Очок для перемоги", + "Short": "Коротко", + "Shorter": "Коротше", + "Solo Mode": "Режим соло", + "Target Count": "Кількість цілей", + "Time Limit": "Ліміт часу" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} дискваліфікована бо ${PLAYER} вийшов", + "Killing ${NAME} for skipping part of the track!": "Ліквідація ${NAME} за зрізання траси!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Попередження для ${NAME}: турбо / спам-кнопок викидує вас." + }, + "teamNames": { + "Bad Guys": "Погані хлопці", + "Blue": "Сині", + "Good Guys": "Хороші хлопці", + "Red": "Червоні" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Своєчасна послідовність біг-стрибок-поворот-удар може вбити\nодним ударом і заробити вам довічну повагу друзів.", + "Always remember to floss.": "Не забувайте користуватися зубною ниткою.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Створіть профілі гравців для себе і своїх друзів з власними\nіменами і зовнішністю замість випадкових.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Ящики з прокляттям перетворюють вас в цокаючу бомбу.\nЄдині ліки - швидко схопити аптечку.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Незалежно від зовнішності, здатності всіх персонажів ідентичні,\nтак що просто вибирайте того, на кого ви більше схожі.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Не зазнавайстеся з цим енергетичним щитом, вас все ще можуть скинути з обриву.", + "Don't run all the time. Really. You will fall off cliffs.": "Не бігай весь час. Серйозно. Впадеш з обриву.", + "Don't spin for too long; you'll become dizzy and fall.": "Не крутись довго; у тебе закрутиться голова і ти впадеш.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Для бігу натисніть і тримайте будь-яку кнопку. (Для цього зручні тригери, якщо вони є)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Для бігу утримуйте будь-яку кнопку. Бігати, звичайно, швидше,\nзате важче повертати, так що не забувайте про обриви.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Крижані бомби не дуже потужні, але вони заморожують\nвсіх навколо, роблячи їх крихкими.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Якщо хтось вас схопив, бийте, і вас відпустять.\nУ реальному житті це теж працює.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Якщо вам не вистачає контролерів, встановіть програму '${REMOTE_APP_NAME}'\nна ваші мобільні пристрої, щоб використовувати їх в якості контролерів.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Якщо до вас прилипла липка бомба, стрибайте і крутитесь. може пощастить\nструсити бомбу або, на худий кінець, повеселити оточуючих.", + "If you kill an enemy in one hit you get double points for it.": "Якщо вбиваєш ворога з одного удару, то отримуєш подвійні очки.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Якщо підхопили прокляття, то єдина надія на виживання\n- це знайти аптечку в найближчі кілька секунд.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Не стій на місці — підсмажижся. Біжи і ухилятися щоб вижити..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Якщо у вас багато гравців, які приходять і йдуть, включіть \"автоматично викидати \nне активних гравців \"в налаштуваннях на випадок, якщо хтось забуде вийти з гри.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Якщо ваш пристрій нагрівається або ви хочете зберегти заряд батареї,\nзменшіть \"Візуальні ефекти\" або \"Розширення\" в Налаштуваннях->Графіка", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Якщо картинка переривчаста, спробуйте зменшити розширення\nабо візуальні ефекти в налаштуваннях графіки гри.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "У Захопленні прапору, щоб набрати очки, свій власний прапор повинен бути на базі.\nЯкщо інша команда ось-ось заб'є, можна вкрасти їх прапор, щоб їх зупинити.", + "In hockey, you'll maintain more speed if you turn gradually.": "У хокеї можна підтримувати більш високу швидкість, якщо повертати поступово.", + "It's easier to win with a friend or two helping.": "Виграти легше з допомогою одного або двох друзів.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Стрибни прямо перед кидком щоб закинути бомбу якомога вище.", + "Land-mines are a good way to stop speedy enemies.": "Міни - непоганий спосіб зупинити прудконогих ворогів.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Можна багато чого підняти і кинути, включаючи інших гравців. Жбурляння ворогів\nз обриву буває ефективною і підбадьорливою стратегією.", + "No, you can't get up on the ledge. You have to throw bombs.": "Ні, ви не зможете залізти на виступ. Кидайте бомби.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Гравці можуть приєднуватися і йти посеред гри,\nтакож можна підключати і відключати контролери прямо на льоту.", + "Practice using your momentum to throw bombs more accurately.": "Тренуйтеся використовувати інерцію для більш точних кидків.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Удари наносять більше шкоди, чим швидше рухаються кулаки,\nтак що намагайтеся бігати, стрибати і крутитися, як ненормальні.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Пробіжіться туди-назад перед кидком, щоб\n'підкрутити' бомбу і кинути її далі.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Зметіть групу ворогів, підірвавши\nбомбу біля коробки TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Голова - найвразливіше місце, так що липка бомба\nпо чайнику, як правило, означає капут.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Цей рівень безкінечний, але високі очки тут\nзароблять вам вічну пошану у всьому світі.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Сила кидка залежить від напрямку. Щоб акуратно кинути \nщось прямо перед собою, не натискайте ні в якому напрямку.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Набрид саундтрек? Замініть його власним!\nДивіться Налаштування->Аудіо->Саундтрек", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Спробуйте 'підігрівати' бомби секунду або дві, перш ніж їх кинути.", + "Try tricking enemies into killing eachother or running off cliffs.": "Спробуйте обдурити ворогів, щоб вони вбили один одного або стрибнули з обрива.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Використовуйте кнопку вибору (трикутник), щоб схопити прапор <${PICKUP}>", + "Whip back and forth to get more distance on your throws..": "Змахни туди-сюди, щоб закинути подалі..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Ви можете 'націлювати' удари, крутись вліво або вправо.\nЦе корисно для зіштовхування поганих хлопців з краю або для голів в хокеї.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Коли вибухне бомба можна визначити за кольором іскор гніту:\nжовтий..оранжевий..червоний..БАБАХ.", + "You can throw bombs higher if you jump just before throwing.": "Бомбу можна кинути вище, якщо підстрибнути прямо перед кидком.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Ви отримуєте пошкодження, коли вдаряється головою,\nтак що бережіть голову.", + "Your punches do much more damage if you are running or spinning.": "Від ударів набагато більше шкоди, коли біжиш або крутишся." + } + }, + "trophiesRequiredText": "Для цього треба мінімум ${NUMBER} трофеїв.", + "trophiesText": "Трофеїв", + "trophiesThisSeasonText": "Трофеї за цей Сезон", + "tutorial": { + "cpuBenchmarkText": "Прогін туторіала на шаленій швидкості (перевіряє швидкість процесора)", + "phrase01Text": "Привіт!", + "phrase02Text": "Ласкаво просимо в ${APP_NAME}!", + "phrase03Text": "Кілька порад з управління персонажем:", + "phrase04Text": "Багато чого в ${APP_NAME} засноване на законах фізики.", + "phrase05Text": "Наприклад, при ударі кулаком..", + "phrase06Text": "..пошкодження залежать від швидкості кулака.", + "phrase07Text": "Бачите? Ми не рухалися, тому ${NAME} майже в повному порядку.", + "phrase08Text": "Тепер підстрибніть і крутаніться для швидкості.", + "phrase09Text": "Ага, так-то краще.", + "phrase10Text": "Також допомагає біг.", + "phrase11Text": "Для бігу утримуйте будь-яку кнопку.", + "phrase12Text": "Для супер-крутих ударів спробуйте бігти і крутитися.", + "phrase13Text": "Ой .. Вибач, ${NAME}.", + "phrase14Text": "Можна піднімати і кидати речі, наприклад прапори .. або ${NAME}.", + "phrase15Text": "І, зрештою, бомби.", + "phrase16Text": "Кидання бомб потребує тренування.", + "phrase17Text": "Ай! Не дуже хороший кидок.", + "phrase18Text": "У русі кинути виходить далі.", + "phrase19Text": "У стрибку кинути виходить вище.", + "phrase20Text": "\"Підкручені\" бомби летять ще далі.", + "phrase21Text": "\"Вичікування\" бомби робиться досить складно.", + "phrase22Text": "Чорт.", + "phrase23Text": "Спробуйте \"підігріти\" гніт секунду або дві.", + "phrase24Text": "Ура! Добре \"підігріто\".", + "phrase25Text": "Ну на цьому, мабуть, все.", + "phrase26Text": "Вперед, на міни!", + "phrase27Text": "Не забувай ці поради, і ТОЧНО повернешся живим!", + "phrase28Text": "...може бути...", + "phrase29Text": "Успіхів!", + "randomName1Text": "Петя", + "randomName2Text": "Вася", + "randomName3Text": "Коля", + "randomName4Text": "Ігор", + "randomName5Text": "Сашка", + "skipConfirmText": "Пропустити туторіал? Торкніться або натисніть кнопку для підтвердження.", + "skipVoteCountText": "${COUNT}/${TOTAL} голосів за пропуск", + "skippingText": "пропуск навчання ...", + "toSkipPressAnythingText": "(торкніться або натисніть що-небудь щоб пропустити туторіал)" + }, + "twoKillText": "ДВОХ ЗА РАЗ!", + "unavailableText": "недоступно", + "unconfiguredControllerDetectedText": "Виявлено налаштований контролер:", + "unlockThisInTheStoreText": "Це повинно бути розблоковано в магазині.", + "unlockThisProfilesText": "Щоб створити більш ${NUM} профіль, Вам необхідно:", + "unlockThisText": "Щоб розблокувати це, вам потрібно:", + "unsupportedHardwareText": "На жаль, це устаткування не підтримується в цій збірці гри.", + "upFirstText": "Для початку:", + "upNextText": "Далі в грі ${COUNT}:", + "updatingAccountText": "Оновлення вашого акаунту...", + "upgradeText": "Оновлення", + "upgradeToPlayText": "Розблокуйте \"${PRO}\" в магазині щоб грати в це.", + "useDefaultText": "Використовувати стандартні", + "usesExternalControllerText": "Ця гра може використовувати зовнішній контролер для управління.", + "usingItunesText": "Використання музикального додатку для саундтрека...", + "usingItunesTurnRepeatAndShuffleOnText": "Будь ласка, переконайтеся, що випадковий порядок і повтор усіх пісень включений в Itunes.", + "validatingTestBuildText": "Перевірка тестової збірки...", + "victoryText": "Перемога!", + "voteDelayText": "Ви не можете виганяти ще ${NUMBER} секунд", + "voteInProgressText": "Голосування ще триває.", + "votedAlreadyText": "Ви вже голосували", + "votesNeededText": "Потрібно ${NUMBER} голосів", + "vsText": "vs.", + "waitingForHostText": "(очікування ${HOST} щоб продовжити)", + "waitingForPlayersText": "очікування гравців для початку...", + "waitingInLineText": "Почекай трохи (кімната заповнена)...", + "watchAVideoText": "Дивитися відео", + "watchAnAdText": "Дивитись рекламу", + "watchWindow": { + "deleteConfirmText": "Видалити \"${REPLAY}\"?", + "deleteReplayButtonText": "Видалити\nповтор", + "myReplaysText": "Мої повтори", + "noReplaySelectedErrorText": "Повтор не обрано", + "playbackSpeedText": "Швидкість відтворення: ${SPEED}", + "renameReplayButtonText": "Перейменувати\nповтор", + "renameReplayText": "Перейменувати \"${REPLAY}\" на:", + "renameText": "Перейменувати", + "replayDeleteErrorText": "Помилка видалення повтору.", + "replayNameText": "Ім'я повтору", + "replayRenameErrorAlreadyExistsText": "Повтор з таким ім'ям вже існує.", + "replayRenameErrorInvalidName": "Неможливо перейменувати повтор; невірне ім'я.", + "replayRenameErrorText": "Помилка перейменування повтору.", + "sharedReplaysText": "Загальні повтори", + "titleText": "Дивитися", + "watchReplayButtonText": "Дивитися\nповтор" + }, + "waveText": "Хвиля", + "wellSureText": "Зійде!", + "wiimoteLicenseWindow": { + "titleText": "Авторські права DarwiinRemote" + }, + "wiimoteListenWindow": { + "listeningText": "Очікування контролерів Wii...", + "pressText": "Натисніть кнопки 1 та 2 на Wiimote одночасно.", + "pressText2": "На нових Wiimote з вбудованим Motion Plus натисніть червону кнопку 'sync' на задній частині." + }, + "wiimoteSetupWindow": { + "copyrightText": "Авторські права DarwiinRemote", + "listenText": "Слухати", + "macInstructionsText": "Переконайтеся, що ваш Wii вимкнений, а на вашому Mac включений\nBluetooth, потім натисніть 'Слухати'. Підтримка контролерів Wii\nбуває трохи примхлива, так що, можливо, доведеться\nспробувати кілька разів, поки вийде під'єднатися.\n\nBluetooth повинен обробляти до 7 підключених пристроїв,\nхоча всяке буває.\n\nBombSquad підтримує оригінальні контролери Wiimote,\nнунчаки і класичні контролери.\nТакож тепер працює і новий Wii Remote Plus,\nПравда, без аксесуарів.", + "macInstructionsTextScale": 0.7, + "thanksText": "Це стало можливим завдяки\nкоманді DarwiinRemote.", + "titleText": "Налаштування контролера Wii" + }, + "winsPlayerText": "Переміг ${NAME}!", + "winsTeamText": "Перемогли ${NAME}!", + "winsText": "${NAME} виграв!", + "worldScoresUnavailableText": "Світові результати недоступні.", + "worldsBestScoresText": "Кращі в світі результати", + "worldsBestTimesText": "Кращий світовий час", + "xbox360ControllersWindow": { + "getDriverText": "Завантажити драйвер", + "macInstructions2Text": "Для використання контролерів по бездротовому зв'язку, вам також\nбуде потрібно ресивер, який поставляється з \"бездротовим контролером\nXbox 360 для Windows\". Один ресивер дозволяє підключити до 4 контролерів.\n\nУвага: ресивери сторонніх виробників не будуть працювати з цим драйвером,\nпереконайтеся, що на вашому ресівері написано \"Microsoft\", а не \"XBOX 360\".\nMicrosoft більше не продає їх окремо, так що вам потрібно буде знайти\nресивер в комплекті з контролером, або шукати на ebay.\n\nЯкщо ви вважаєте це за необхідне, можете відправити грошей розробнику\nдрайвера на його сайті.", + "macInstructionsText": "Для використання контролерів Xbox 360 необхідно\nвстановити драйвер Mac, доступний за посиланням нижче.\nВін працює і з провідними і бездротовими контролерами.", + "ouyaInstructionsText": "Для використання провідних контролерів Xbox 360 в BombSquad,\nпросто підключіть їх до USB-порту вашого пристрою. для кількох\nконтролерів можна використовувати концентратор USB.\n\nДля використання бездротових контролерів вам знадобиться бездротовий\nресивер який поставляється в наборі \"бездротового контролера Xbox 360\nдля Windows \"або продається окремо. Кожен ресивер підключається\nдо порту USB і дозволяє підключати до 4 бездротових контролерів.", + "titleText": "Використання контролерів Xbox 360 в ${APP_NAME}:" + }, + "yesAllowText": "Так, дозволити!", + "yourBestScoresText": "Ваші кращі результати", + "yourBestTimesText": "Ваш кращий час" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/venetian.json b/dist/ba_data/data/languages/venetian.json new file mode 100644 index 0000000..87a1cd4 --- /dev/null +++ b/dist/ba_data/data/languages/venetian.json @@ -0,0 +1,1819 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "I nomi de i account no i połe miga ver rento emoji o altri caràtari spesiałi", + "accountsText": "Account", + "achievementProgressText": "Obietivi: ${COUNT} de ${TOTAL}", + "campaignProgressText": "Progreso canpagna [Defìsiłe]: ${PROGRESS}", + "changeOncePerSeason": "A te połi canbiar ’sto dato soło na volta par stajon.", + "changeOncePerSeasonError": "Par modifegar (${NUM} days) A te ghè da spetar ła stajon pròsema.", + "customName": "Nome parsonałizà", + "linkAccountsEnterCodeText": "Insarisi còdaze", + "linkAccountsGenerateCodeText": "Jènara còdaze", + "linkAccountsInfoText": "(sparpagna progresi infrà dispozitivi defarenti)", + "linkAccountsInstructionsNewText": "Par cołegar do account, jènara un còdaze inte'l primo\ndispozitivo e insarìseło inte’l segondo. I dati de’l\nsegondo account i vegnarà sparpagnài so tuti do i account\n(i dati de’l primo account i ndarà perdesti).\n\nA te połi cołegar fin a ${COUNT} account.\n\nINPORTANTE: cołega soło i account de to propiedà.\nSe A te cołeghi account de calche to amigo, A no sarè\npì boni de zugar online inte’l mèdemo momento.", + "linkAccountsText": "Cołega account", + "linkedAccountsText": "Account cołegài:", + "nameChangeConfirm": "Vutu canbiar el nome de’l to account co ${NAME}?", + "resetProgressConfirmNoAchievementsText": "A te si drio ełimenar i to progresi so ła modałidà\ncooparadiva e i to punteji łogałi (ma miga i to biłieti).\n’Sta asion no ła połe mìa èsar anułada. Vutu ndar vanti?", + "resetProgressConfirmText": "A te si drio ełimenar i to progresi so ła\nmodałidà cooparadiva, i to obietivi e i to punteji\nłogałi (ma miga i to biłieti). ’Sta asion\nno ła połe mìa èsar anułada. Vutu ndar vanti?", + "resetProgressText": "Ełìmena progresi", + "setAccountName": "Inposta un nome utente", + "setAccountNameDesc": "Sełesiona el nome da vizuałizar so’l to account.\nA te połi doparar el nome da uno de i to account\ncołegài o crear un nome parsonałizà ma ùnivogo.", + "signInInfoText": "Conétate par tirar sù biłieti, batajar online e\nsparpagnar i to progresi infrà dispozitivi defarenti.", + "signInText": "Conétate", + "signInWithDeviceInfoText": "(par 'sto dispozitivo A ze disponìbiłe un soło account automàtego)", + "signInWithDeviceText": "Conétate co l'account de’l dispozitivo", + "signInWithGameCircleText": "Conétate co Game Circle", + "signInWithGooglePlayText": "Conétate co Google Play", + "signInWithTestAccountInfoText": "(account de proa vecio: in fuduro dòpara i account de’l dispozitivo)", + "signInWithTestAccountText": "Conétate co un account de proa", + "signOutText": "Sortisi da l'account", + "signingInText": "Conesion in corso...", + "signingOutText": "Sortìa in corso...", + "ticketsText": "Biłieti: ${COUNT}", + "titleText": "Account", + "unlinkAccountsInstructionsText": "Sełesiona un account da descołegar", + "unlinkAccountsText": "Descołega account", + "viaAccount": "(doparando ${NAME})", + "youAreSignedInAsText": "A te zugarè cofà:" + }, + "achievementChallengesText": "Obietivi sfide", + "achievementText": "Obietivo", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Copa 3 cataràdeghi co ła TNT", + "descriptionComplete": "Copài co ła TNT 3 cataràdeghi", + "descriptionFull": "Copa 3 cataràdeghi co ła TNT so ${LEVEL}", + "descriptionFullComplete": "Copài co ła TNT 3 cataràdeghi so ${LEVEL}", + "name": "Parla ła dimanite: Boom!" + }, + "Boxer": { + "description": "Vinsi sensa doparar bonbe", + "descriptionComplete": "Vinsesto sensa doparar bonbe", + "descriptionFull": "Vinsi so ${LEVEL} sensa doparar bonbe", + "descriptionFullComplete": "Vinsesto so ${LEVEL} sensa doparar bonbe", + "name": "Boxador" + }, + "Dual Wielding": { + "descriptionFull": "Coneti 2 controładori (fìzeghi o apl)", + "descriptionFullComplete": "Conetesti 2 controładori (fìzeghi o apl)", + "name": "Tegnùa dupia" + }, + "Flawless Victory": { + "description": "Vinsi sensa èsar becà", + "descriptionComplete": "Vinsesto sensa èsar becà", + "descriptionFull": "Fenisi ${LEVEL} sensa mai èsar becà", + "descriptionFullComplete": "${LEVEL} fenìo sensa mai èsar becà", + "name": "Capoto!" + }, + "Free Loader": { + "descriptionFull": "Taca na partìa \"Tuti contro tuti\" co pì de 2 zugadori", + "descriptionFullComplete": "Partìa \"Tuti contro tuti\" co pì de 2 zugadori fata!", + "name": "Bote da orbi" + }, + "Gold Miner": { + "description": "Copa 6 cataràdeghi co łe mine", + "descriptionComplete": "6 cataràdeghi copài co łe mine", + "descriptionFull": "Copa 6 cataràdeghi co łe mine so ${LEVEL}", + "descriptionFullComplete": "6 cataràdeghi copài co łe mine so ${LEVEL}", + "name": "Mina-d’or" + }, + "Got the Moves": { + "description": "Vinsi sensa tirar crogni o bonbe", + "descriptionComplete": "Vinsesto sensa tirar crogni o bonbe", + "descriptionFull": "Vinsi so ${LEVEL} sensa tirar crogni o bonbe", + "descriptionFullComplete": "Vinsesto so ${LEVEL} sensa tirar crogni o bonbe", + "name": "Forest Gunp" + }, + "In Control": { + "descriptionFull": "Coneti un controłador (fìzego o apl)", + "descriptionFullComplete": "Controłador conetesto (fìzego o apl)", + "name": "Soto controło" + }, + "Last Stand God": { + "description": "Ingruma sù 1000 punti", + "descriptionComplete": "1000 punti ingrumài sù", + "descriptionFull": "Ingruma sù 1000 punti so ${LEVEL}", + "descriptionFullComplete": "1000 punti ingrumài sù so ${LEVEL}", + "name": "Łejenda de ${LEVEL}" + }, + "Last Stand Master": { + "description": "Ingruma sù 250 punti", + "descriptionComplete": "250 punti ingrumài sù", + "descriptionFull": "Ingruma sù 250 punti so ${LEVEL}", + "descriptionFullComplete": "250 punti ingrumài sù so ${LEVEL}", + "name": "Aso de ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Ingruma sù 500 punti", + "descriptionComplete": "500 punti ingrumài sù", + "descriptionFull": "Ingruma sù 500 punti so ${LEVEL}", + "descriptionFullComplete": "500 punti ingrumài sù so ${LEVEL}", + "name": "Mago de ${LEVEL}" + }, + "Mine Games": { + "description": "Copa co łe mine 3 cataràdeghi", + "descriptionComplete": "3 cataràdeghi copài co łe mine", + "descriptionFull": "Copa co łe mine 3 tacaràdeghi so ${LEVEL}", + "descriptionFullComplete": "3 cataràdeghi copài co łe mine so ${LEVEL}", + "name": "Canpo minà" + }, + "Off You Go Then": { + "description": "Trà zó da ła mapa 3 cataràdeghi", + "descriptionComplete": "Trato zó da ła mapa 3 cataràdeghi", + "descriptionFull": "Trà zó da ła mapa 3 cataràdeghi so ${LEVEL}", + "descriptionFullComplete": "Trato zó da ła mapa 3 cataràdeghi so ${LEVEL}", + "name": "Aerosaltador" + }, + "Onslaught God": { + "description": "Ingruma sù 5000 punti", + "descriptionComplete": "5000 punti ingrumài sù", + "descriptionFull": "Ingruma sù 5000 punti so ${LEVEL}", + "descriptionFullComplete": "5000 punti ingrumài sù so ${LEVEL}", + "name": "Łejenda de ${LEVEL}" + }, + "Onslaught Master": { + "description": "Ingruma sù 500 punti", + "descriptionComplete": "500 punti ingrumài sù", + "descriptionFull": "Ingruma sù 500 punti so ${LEVEL}", + "descriptionFullComplete": "500 punti ingrumài sù so ${LEVEL}", + "name": "Aso de ${LEVEL}" + }, + "Onslaught Training Victory": { + "description": "Fà fora tute łe ondàe", + "descriptionComplete": "Fazeste fora tute łe ondàe", + "descriptionFull": "Fà fora tute łe ondàe so ${LEVEL}", + "descriptionFullComplete": "Fazeste fora tute łe ondàe so ${LEVEL}", + "name": "Vitoria so ${LEVEL}" + }, + "Onslaught Wizard": { + "description": "Ingruma sù 1000 punti", + "descriptionComplete": "1000 punti ingrumài sù", + "descriptionFull": "Ingruma sù 1000 punti so ${LEVEL}", + "descriptionFullComplete": "1000 punti ingrumài sù so ${LEVEL}", + "name": "Mago de ${LEVEL}" + }, + "Precision Bombing": { + "description": "Vinsi sensa doparar potensiadori", + "descriptionComplete": "Vinsesto sensa doparar potensiadori", + "descriptionFull": "Vinsi so ${LEVEL} sensa doparar potensiadori", + "descriptionFullComplete": "Vinsesto so ${LEVEL} sensa doparar potensiadori", + "name": "Tirador de presizion" + }, + "Pro Boxer": { + "description": "Vinsi sensa doparar bonbe", + "descriptionComplete": "Vinsesto sensa doparar bonbe", + "descriptionFull": "Vinsi so ${LEVEL} sensa doparar bonbe", + "descriptionFullComplete": "Vinsesto so ${LEVEL} sensa doparar bonbe", + "name": "Boxador profesionałe" + }, + "Pro Football Shutout": { + "description": "Vinsi sensa łasar che i cataràdeghi i fasa punti", + "descriptionComplete": "Vinsesto sensa łasar che i cataràdeghi i fasa punti", + "descriptionFull": "Vinsi so ${LEVEL} sensa łasar che i cataràdeghi i fasa punti", + "descriptionFullComplete": "Vinsesto so ${LEVEL} sensa łasar che i cataràdeghi i fasa punti", + "name": "Vanpadora so ${LEVEL}" + }, + "Pro Football Victory": { + "description": "Vinsi ła partìa", + "descriptionComplete": "Partìa vinsesta", + "descriptionFull": "Vinsi ła partìa so ${LEVEL}", + "descriptionFullComplete": "Partìa vinsesta so ${LEVEL}", + "name": "Vitoria so ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Fà fora tute łe ondàe", + "descriptionComplete": "Fazeste fora tute łe ondàe", + "descriptionFull": "Fà fora tute łe ondàe de ${LEVEL}", + "descriptionFullComplete": "Fazeste fora tute łe ondàe de ${LEVEL}", + "name": "Vitoria so ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Bloca tute łe ondàe", + "descriptionComplete": "Blocàe tute łe ondàe", + "descriptionFull": "Bloca tute łe ondàe so ${LEVEL}", + "descriptionFullComplete": "Blocàe tute łe ondàe so ${LEVEL}", + "name": "Vitoria so ${LEVEL}" + }, + "Rookie Football Shutout": { + "description": "Vinsi sensa łasar che i cataràdeghi i fasa punti", + "descriptionComplete": "Vinsesto sensa łasar che i cataràdeghi i fasa punti", + "descriptionFull": "Vinsi so ${LEVEL} sensa łasar che i cataràdeghi i fasa punti", + "descriptionFullComplete": "Vinsesto so ${LEVEL} sensa łasar che i cataràdeghi i fasa punti", + "name": "Vanpadora so ${LEVEL}" + }, + "Rookie Football Victory": { + "description": "Vinsi ła partìa", + "descriptionComplete": "Partìa vinsesta", + "descriptionFull": "Vinsi ła partìa so ${LEVEL}", + "descriptionFullComplete": "Partìa vinsesta so ${LEVEL}", + "name": "Vitoria so ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Fà fora tute łe ondàe", + "descriptionComplete": "Fazeste fora tute łe ondàe", + "descriptionFull": "Fà fora tute łe ondàe so ${LEVEL}", + "descriptionFullComplete": "Fazeste fora tute łe ondàe so ${LEVEL}", + "name": "Vitoria so ${LEVEL}" + }, + "Runaround God": { + "description": "Ingruma sù 2000 punti", + "descriptionComplete": "2000 punti ingrumài sù", + "descriptionFull": "Ingruma sù 2000 punti so ${LEVEL}", + "descriptionFullComplete": "2000 punti ingrumài sù so ${LEVEL}", + "name": "Łejenda de ${LEVEL}" + }, + "Runaround Master": { + "description": "Ingruma sù 500 punti", + "descriptionComplete": "500 punti ingrumài sù", + "descriptionFull": "Ingruma sù 500 punti so ${LEVEL}", + "descriptionFullComplete": "500 punti ingrumài sù so ${LEVEL}", + "name": "Aso de ${LEVEL}" + }, + "Runaround Wizard": { + "description": "Ingruma sù 1000 punti", + "descriptionComplete": "1000 punti ingrumài sù", + "descriptionFull": "Ingruma sù 1000 punti so ${LEVEL}", + "descriptionFullComplete": "1000 punti ingrumài sù so ${LEVEL}", + "name": "Mago de ${LEVEL}" + }, + "Sharing is Caring": { + "descriptionFull": "Sparpagna el zugo co un amigo co suceso", + "descriptionFullComplete": "Zugo sparpagnà co suceso co un amigo", + "name": "Sparpagnar A vołe dir tegnerghe" + }, + "Stayin' Alive": { + "description": "Vinsi sensa mai crepar", + "descriptionComplete": "Vinsesto sensa mai crepar", + "descriptionFull": "Vinsi so ${LEVEL} sensa mai crepar", + "descriptionFullComplete": "Vinsesto so ${LEVEL} sensa mai crepar", + "name": "Ła scanpada!" + }, + "Super Mega Punch": { + "description": "Cava el 100% de vita co un crogno soło", + "descriptionComplete": "100% de vita cavada vìa co un crogno soło", + "descriptionFull": "Cava vìa el 100% de vita co un crogno soło so ${LEVEL}", + "descriptionFullComplete": "100% de vita cavada vìa co un crogno soło so ${LEVEL}", + "name": "Super mega crogno" + }, + "Super Punch": { + "description": "Cava vìa el 50% de vita co un crogno soło", + "descriptionComplete": "50% de vita cavada vìa co un crogno soło", + "descriptionFull": "Cava vìa el 50% de vita co un crogno soło so ${LEVEL}", + "descriptionFullComplete": "50% de vita cavada vìa co un crogno soło so ${LEVEL}", + "name": "Super crogno" + }, + "TNT Terror": { + "description": "Copa 6 cataràdeghi co ła TNT", + "descriptionComplete": "Copài co ła TNT 6 cataràdeghi", + "descriptionFull": "Copa 6 cataràdeghi co ła TNT so ${LEVEL}", + "descriptionFullComplete": "Copài co ła TNT 6 cataràdeghi so ${LEVEL}", + "name": "Dinamitaro" + }, + "Team Player": { + "descriptionFull": "Taca na partìa a \"Scuadre\" co pì de 4 zugadori", + "descriptionFullComplete": "Partìa a \"Scuadre\" co pì de 4 zugadori fata!", + "name": "Spirido cołaboradivo" + }, + "The Great Wall": { + "description": "Bloca tuti i cataràdeghi", + "descriptionComplete": "Blocài tuti i cataràdeghi", + "descriptionFull": "Bloca tuti i cataràdeghi so ${LEVEL}", + "descriptionFullComplete": "Blocài tuti i cataràdeghi so ${LEVEL}", + "name": "Muraja Cineze" + }, + "The Wall": { + "description": "Bloca tuti i cataràdeghi", + "descriptionComplete": "Blocài tuti i cataràdeghi", + "descriptionFull": "Bloca tuti i cataràdeghi so ${LEVEL}", + "descriptionFullComplete": "Blocài tuti i cataràdeghi so ${LEVEL}", + "name": "El muro" + }, + "Uber Football Shutout": { + "description": "Vinsi sensa łasar che i cataràdeghi i fasa punti", + "descriptionComplete": "Vinsesto sensa łasar che i cataràdeghi i gapia fazesto punti", + "descriptionFull": "Vinsi so ${LEVEL} sensa łasar che i cataràdeghi i fasa punti", + "descriptionFullComplete": "Vinsesto so ${LEVEL} sensa łasar che i cataràdeghi i gapia fazesto punti", + "name": "Vanpadora so ${LEVEL}" + }, + "Uber Football Victory": { + "description": "Vinsi ła partìa", + "descriptionComplete": "Partìa vinsesta", + "descriptionFull": "Vinsi ła partìa so ${LEVEL}", + "descriptionFullComplete": "Partìa vinsesta so ${LEVEL}", + "name": "Vitoria so ${LEVEL}" + }, + "Uber Onslaught Victory": { + "description": "Fà fora tute łe ondàe", + "descriptionComplete": "Fazeste fora tute łe ondàe", + "descriptionFull": "Fà fora tute łe ondàe so ${LEVEL}", + "descriptionFullComplete": "Fazeste fora tute łe ondàe so ${LEVEL}", + "name": "Vitoria so ${LEVEL}" + }, + "Uber Runaround Victory": { + "description": "Bloca tute łe ondàe", + "descriptionComplete": "Blocàe tute łe ondàe", + "descriptionFull": "Bloca tute łe ondàe so ${LEVEL}", + "descriptionFullComplete": "Blocàe tute łe ondàe so ${LEVEL}", + "name": "Vitoria so ${LEVEL}" + } + }, + "achievementsRemainingText": "Obietivi restanti:", + "achievementsText": "Obietivi", + "achievementsUnavailableForOldSeasonsText": "Ne despiaze, i obietivi de łe stajon pasàe no i ze mìa disponìbiłe.", + "addGameWindow": { + "getMoreGamesText": "Otien pì łevełi...", + "titleText": "Zonta zugo" + }, + "allowText": "Parmeti", + "alreadySignedInText": "El to account el ze in dòparo inte n’antro\ndispozitivo: canbia account o sara sù el zugo\ninte cheł’altro to dispozitivo e proa danovo.", + "apiVersionErrorText": "A no ze mìa posìbiłe cargar el mòduło ${NAME}, el se refarise a ła varsion ${VERSION_USED}. A serve invese ła ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(Ativa \"Auto\" soło co A te tachi sù łe fonarołe par ła realtà virtuałe)", + "headRelativeVRAudioText": "Àudio par fonarołe VR", + "musicVolumeText": "Vołume mùzega", + "soundVolumeText": "Vołume son", + "soundtrackButtonText": "Son de fondo", + "soundtrackDescriptionText": "(scolta ła to mùzega fin che A te zughi)", + "titleText": "Àudio" + }, + "autoText": "Automàtega", + "backText": "Indrìo", + "banThisPlayerText": "Bloca 'sto zugador", + "bestOfFinalText": "Fenałe de \"El mejo de i ${COUNT}\"", + "bestOfSeriesText": "El mejo de i ${COUNT}", + "bestRankText": "Ła to pozision pì alta: #${RANK}", + "bestRatingText": "Ła to vałutasion pì alta: ${RATING}", + "bombBoldText": "BONBA", + "bombText": "Bonba", + "boostText": "Potensiador", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} el se configura rento l'apl mèdema.", + "buttonText": "boton", + "canWeDebugText": "Ghetu caro che BombSquad el reporte in automàtego bai,\nblochi e informasion baze só'l so dòparo a'l dezviłupador?\n\n'Sti dati no i contien miga informasion parsonałi ma i ło\njuta a far ndar ben el zugo sensa blochi o bai.", + "cancelText": "Anuła", + "cantConfigureDeviceText": "Ne despiaze, ${DEVICE} no'l ze mìa configuràbiłe.", + "challengeEndedText": "'Sta sfida ła ze fenìa.", + "chatMuteText": "Siłensia ciacołada", + "chatMutedText": "Ciacołada siłensiada", + "chatUnMuteText": "Reativa son", + "choosingPlayerText": "", + "completeThisLevelToProceedText": "A te ghè da conpletar 'sto\nłeveło par ndar vanti!", + "completionBonusText": "Premio de concruzion", + "configControllersWindow": { + "configureControllersText": "Configura controładori", + "configureKeyboard2Text": "Configura botonera P2", + "configureKeyboardText": "Configura botonera", + "configureMobileText": "Dòpara dispozitivi cofà controładori", + "configureTouchText": "Configura schermo tàtiłe", + "ps3Text": "Controładori PS3", + "titleText": "Controładori", + "wiimotesText": "Controładori Wii", + "xbox360Text": "Controładori Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Nota: ła conpatibiłidà par i controładori ła muda drio dispozitivo e varsion de Android.", + "pressAnyButtonText": "Struca un boton calsìase de'l controłador\nche A te vołi configurar...", + "titleText": "Configura controładori" + }, + "configGamepadWindow": { + "advancedText": "Avansàe", + "advancedTitleText": "Configurasion avansàe controładori", + "analogStickDeadZoneDescriptionText": "(da alsar se el to parsonajo el \"zbanda\" anca co no te tochi gnente)", + "analogStickDeadZoneText": "Pozision baze łeviera nałòzega", + "appliesToAllText": "(aplegàe a tuti i controładori de 'sto tipo)", + "autoRecalibrateDescriptionText": "(da ativar se'l to parsonajo no'l se move a ła vełosidà màsema)", + "autoRecalibrateText": "Autocàłebra ła łeviera nałòzega", + "axisText": "ase", + "clearText": "reinposta", + "dpadText": "Croze deresionałe", + "extraStartButtonText": "Boton Start in pì", + "ifNothingHappensTryAnalogText": "Se no càpita gnente, proa inpostarghe a'l só posto ła łeviera nałòzega.", + "ifNothingHappensTryDpadText": "Se no càpita gnente, proa inpostarghe a'l só posto ła croze deresionałe.", + "ignoreCompletelyDescriptionText": "(par evitar che 'sto controłador el posa influir so'l zugo o so i menù)", + "ignoreCompletelyText": "Ignora de'l tuto", + "ignoredButton1Text": "Boton ignorà 1", + "ignoredButton2Text": "Boton ignorà 2", + "ignoredButton3Text": "Boton ignorà 3", + "ignoredButton4Text": "Boton ignorà 4", + "ignoredButtonDescriptionText": "(par evitar anca che i botoni \"cao\" e \"sincro\" i interfarisa co l'interfasa utente)", + "pressAnyAnalogTriggerText": "Struca un griłeto nałòzego calsìase...", + "pressAnyButtonOrDpadText": "Struca un boton o croze deresionałe calsìase...", + "pressAnyButtonText": "Struca un boton calsìase...", + "pressLeftRightText": "Struca el boton sanco o drito...", + "pressUpDownText": "Struca el boton sù o zó...", + "runButton1Text": "Boton par córar 1", + "runButton2Text": "Boton par córar 2", + "runTrigger1Text": "Griłeto par córar 1", + "runTrigger2Text": "Griłeto par córar 2", + "runTriggerDescriptionText": "(i griłeti nałòzeghi i te parmete de variar ła vełosidà de corsa)", + "secondHalfText": "Configura ła segonda serie de inpostasion\nde'l controłador 2 in 1 che'l vegnarà\nmostrà cofà un controłador ùgnoło.", + "secondaryEnableText": "Ativa", + "secondaryText": "Controłador segondaro", + "startButtonActivatesDefaultDescriptionText": "(da dezativar se'l to boton Start el ze considarà pì cofà un boton menù)", + "startButtonActivatesDefaultText": "El boton Start l'ativa el menù predefenìo", + "titleText": "Configurasion controłador", + "twoInOneSetupText": "Configurasion controłador 2 so 1", + "uiOnlyDescriptionText": "(par evitar che 'sto controłador el posa zontarse inte na partìa)", + "uiOnlyText": "Łìmida el dòparo de'l menù", + "unassignedButtonsRunText": "Tuti i botoni mìa defenìi i fà córar", + "unsetText": "", + "vrReorientButtonText": "Boton de repozision VR" + }, + "configKeyboardWindow": { + "configuringText": "Configurasion ${DEVICE}", + "keyboard2NoteText": "Nota: ła parte pì granda de łe botonere łe połe rejistrar\nsoło un fià de comandi a'l colpo. Donca, par zugar in du,\nA se ndarìa mejo doparar do botonere defarenti. Tien daconto\nche A te gavarè da defenir in tuti i cazi botoni unìvoghi\npar i tuti do i zugadori." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Grandesa controło", + "actionsText": "Asion a", + "buttonsText": "botoni", + "dragControlsText": "< strasina i controłi par repozisionarli >", + "joystickText": "łeviera", + "movementControlScaleText": "Grandesa controło", + "movementText": "Movimento a", + "resetText": "Reinposta", + "swipeControlsHiddenText": "Scondi i comandi a zlizo", + "swipeInfoText": "Par bituarse co i controłi a \"zlizo\" A serve un fià de pì tenpo.\nMa sensa dover vardar i controłi, el zugo el deventarà pì senpio.", + "swipeText": "zlizo", + "titleText": "Configura schermo tàtiłe" + }, + "configureItNowText": "Vutu configurarlo deso?", + "configureText": "Configura", + "connectMobileDevicesWindow": { + "amazonText": "Amazon Store", + "appStoreText": "App Store", + "bestResultsText": "Par zugar co rezultài pì boni A te serve na rede wifi ràpida.\nA te połi redùzar el retardo (lag) de ła rede zugando visin a’l\ntrazmetidor de’l segnałe, desconetendo altri dispozitivi sensa\nfiło o conetendo l’ospitador de’l zugo co un cavo ethernet.", + "explanationText": "Par doparar un tełèfono o un tołeto cofà un controłador sensa fiło,\ninstàłaghe rento l’apl \"${REMOTE_APP_NAME}\". Par zugar a ${APP_NAME}\nA te połi conétar co’l wifi tuti i dispozitivi che A te vołi! A ze gratis!", + "forAndroidText": "par Android:", + "forIOSText": "par iOS:", + "getItForText": "Descarga ${REMOTE_APP_NAME} par iOS so l'AppStore de\nApple o par Android so'l Store de Google Play o l'Amazon Store", + "googlePlayText": "Google Play", + "titleText": "Doparar dispozitivi mòbiłi cofà controładori:" + }, + "continuePurchaseText": "Vutu ndar vanti par ${PRICE}?", + "continueText": "Sì!", + "controlsText": "Comandi", + "coopSelectWindow": { + "activenessAllTimeInfoText": "'Sto chive no'l vien aplegà inte ła clasìfega globałe.", + "activenessInfoText": "'Sto moltiplegador el và sù inte i dì co\nA te zughi e el và zó inte cheł'altri.", + "activityText": "Costansa", + "campaignText": "Canpagna", + "challengesInfoText": "Vadagna i premi conpletando i minizughi.\n\nI premi e ła defegoltà de i łevełi i ndarà\nsù par cauna sfida conpletada e i ndarà zó\nco łe vien perdeste o miga zugàe.", + "challengesText": "Sfide", + "currentBestText": "El mejo par deso", + "customText": "E in pì...", + "entryFeeText": "Costo", + "forfeitConfirmText": "Vutu dalvero dàrgheła vinta?", + "forfeitNotAllowedYetText": "Deso A no te połi miga dàrgheła vinta suito.", + "forfeitText": "Dàgheła vinta", + "multipliersText": "Moltiplegadori", + "nextChallengeText": "Sfida pròsema", + "nextPlayText": "Partìa pròsema", + "ofTotalTimeText": "de ${TOTAL}", + "playNowText": "Zuga suito", + "pointsText": "Punti", + "powerRankingFinishedSeasonUnrankedText": "(stajon fenìa sensa ver partesipà)", + "powerRankingNotInTopText": "(miga inte i primi ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} punti", + "powerRankingPointsMultText": "(x ${NUMBER} punti)", + "powerRankingPointsText": "${NUMBER} punti", + "powerRankingPointsToRankedText": "${CURRENT} punti so ${REMAINING}", + "powerRankingText": "Clasìfega globałe", + "prizesText": "Premi", + "proMultInfoText": "I zugadori co'l mejoramento ${PRO}\ni beca el ${PERCENT}% de punti in pì.", + "seeMoreText": "Clasìfega conpleta", + "skipWaitText": "Vanti suito", + "timeRemainingText": "Tenpo remanente", + "toRankedText": "par ndar sù de pozision", + "totalText": "totałe", + "tournamentInfoText": "Conpeti co cheł'altri zugadori par\nndar sù de puntejo inte ła to łega.\n\nCo'l tornèo el fenirà, i zugadori pì\nbrai i vegnarà reconpensài co i premi.", + "welcome1Text": "Benrivài inte ła ${LEAGUE}. A te połi mejorar ła to\npozision vadagnando stełe inte i łevełi, conpletando\ni obietivi o vinsendo i trofèi inte i tornèi.", + "welcome2Text": "Fazendo racuante atividà de 'sto tipo A te połi anca vadagnar biłieti.\nI biłieti i połe èsar doparài par dezblocar parsonaji novi, łevełi e\nminizughi ma anca par ndar rento a tornèi o ver funsion in pì.", + "yourPowerRankingText": "Ła to pozision:" + }, + "copyOfText": "Copia de ${NAME}", + "createEditPlayerText": "", + "createText": "Crea", + "creditsWindow": { + "additionalAudioArtIdeasText": "Soni adisionałi, gràfega inisiałe e idee de ${NAME}", + "additionalMusicFromText": "Mùzega adisionałe de ${NAME}", + "allMyFamilyText": "ła me fameja e tuti i me amighi che i gà jutà a testar el zugo", + "codingGraphicsAudioText": "Tradusion in łengua veneta: Còdaze Veneto\nMail: codazeveneto@gmail.com - Telegram: @LenguaVeneta\n\nProgramasion, gràfega e àudio de ${NAME}", + "languageTranslationsText": "Tradusion inte cheł'altre łengue:", + "legalText": "Informasion łegałi:", + "publicDomainMusicViaText": "Mùzega a dòparo pùblego de ${NAME}", + "softwareBasedOnText": "'Sta aplegasion ła ze in parte bazada so'l łaoro de ${NAME}", + "songCreditText": "${TITLE} sonada da ${PERFORMER}\nFata sù da ${COMPOSER}, intarpretada da ${ARRANGER}, piovegada da ${PUBLISHER},\nPar grasia de ${SOURCE}", + "soundAndMusicText": "Son e mùzega:", + "soundsText": "Soni (${SOURCE}):", + "specialThanksText": "Rengrasiamenti spesiałi a:", + "thanksEspeciallyToText": "Un grasie partegołar a ${NAME}", + "titleText": "Reconosimenti", + "whoeverInventedCoffeeText": "chichesipia che'l ghese inventà el cafè" + }, + "currentStandingText": "Ła to pozision par deso: #${RANK}", + "customizeText": "Parsonałiza...", + "deathsTallyText": "crepà ${COUNT} 'olte", + "deathsText": "Crepà", + "debugText": "dezbao", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Nota: par 'sta proa A sarìa mejo inpostar ła Defenision so 'Alta' so Inpostasion > Gràfega > Defenision.", + "runCPUBenchmarkText": "Saja prestasion CPU", + "runGPUBenchmarkText": "Saja prestasion GPU", + "runMediaReloadBenchmarkText": "Saja prestasion recargamento gràfega", + "runStressTestText": "Saja zugo soto sforso", + "stressTestPlayerCountText": "Nùmaro zugadori", + "stressTestPlaylistDescriptionText": "Łista de zugo par proa soto sforso", + "stressTestPlaylistNameText": "Nome łista de zugo", + "stressTestPlaylistTypeText": "Tipo łista de zugo", + "stressTestRoundDurationText": "Durada turno", + "stressTestTitleText": "Proa soto sforso", + "titleText": "Prestasion e Proe soto sforso", + "totalReloadTimeText": "Tenpo totałe recargamento: ${TIME} (par i detaji varda el rejistro)" + }, + "defaultGameListNameText": "Łista de zugo \"${PLAYMODE}\" predefenìa", + "defaultNewGameListNameText": "Ła me łista de zugo ${PLAYMODE}", + "deleteText": "Ełìmena", + "demoText": "Demo", + "denyText": "Refuda", + "desktopResText": "Resołusion PC", + "difficultyEasyText": "Fàsiłe", + "difficultyHardOnlyText": "Soło modałità defìsiłe", + "difficultyHardText": "Defìsiłe", + "difficultyHardUnlockOnlyText": "'Sto łeveło el połe èsar dezblocà soło in modałità defìsiłe.\nPénsitu de fàrgheła!?", + "directBrowserToURLText": "Verzi 'sto cołegamento so un browser:", + "disableRemoteAppConnectionsText": "Dezativa conesion co BombSquad Remote", + "disableXInputDescriptionText": "Dòpara pì de 4 controładori (ma co'l riscio che no i funsione ben).", + "disableXInputText": "Dezativa XInput", + "doneText": "Fato", + "drawText": "Pata", + "duplicateText": "Zdopia", + "editGameListWindow": { + "addGameText": "Zonta\nłeveło", + "cantOverwriteDefaultText": "A no te połi miga sorascrìvar ła łista de zugo predefenìa!", + "cantSaveAlreadyExistsText": "Eziste dezà na łista de zugo co 'sto nome!", + "cantSaveEmptyListText": "A no te połi miga salvar na łista de zugo voda!", + "editGameText": "Muda\nłeveło", + "listNameText": "Nome łista de zugo", + "nameText": "Nome", + "removeGameText": "Ełìmena\nłeveło", + "saveText": "Salva łista de zugo", + "titleText": "Mudador łiste de zugo" + }, + "editProfileWindow": { + "accountProfileInfoText": "'Sto profiło zugador spesiałe el gà nome e icona\nautomàteghi, inpostài drio el to account conetesto.\n\n${ICONS}\n\nCrea un profiło parsonałizà par\ndoparar nomi o icone defarenti.", + "accountProfileText": "(profiło account)", + "availableText": "El nome \"${NAME}\" el ze disponìbiłe.", + "characterText": "parsonajo", + "checkingAvailabilityText": "Controło disponibiłidà par \"${NAME}\"...", + "colorText": "cołor", + "getMoreCharactersText": "Otien pì parsonaji...", + "getMoreIconsText": "Otien pì icone...", + "globalProfileInfoText": "I profiłi zugador globałi i gà nomi ùgnołi garantìi.\nIn pì A se połe zontarghe anca icone parsonałizàe.", + "globalProfileText": "(profiło globałe)", + "highlightText": "detaji", + "iconText": "icona", + "localProfileInfoText": "I profiłi zugador łocałi no i połe ver icone e i nomi no i\nze garantìi cofà ùgnołi. Mejòrełi a profiłi globałi par\nreservarte un nome ùnego e zontarghe na icona parsonałizada.", + "localProfileText": "(profiło łogałe)", + "nameDescriptionText": "Nome zugador", + "nameText": "Nome", + "randomText": "", + "titleEditText": "Muda profiło", + "titleNewText": "Profiło novo", + "unavailableText": "\"${NAME}\" no'l ze miga disponìbiłe: proa n'antro nome.", + "upgradeProfileInfoText": "Ndando vanti A te reservarè el to nome zugador in tuto\nel mondo e te podarè zontarghe na icona parsonałizada.", + "upgradeToGlobalProfileText": "Mejora a profiło globałe" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "A no te połi miga ełimenar el son de fondo predefenìo.", + "cantEditDefaultText": "A no te połi miga mudar el son de fondo predefenìo. Zdópieło o crèaghine uno novo.", + "cantOverwriteDefaultText": "A no te połi miga sorascrìvar el son de fondo predefenìo.", + "cantSaveAlreadyExistsText": "Eziste dezà un son de fondo co 'sto nome!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Son de fondo predefenìo", + "deleteConfirmText": "Vutu ełimenar el son de fondo:\n\n'${NAME}'?", + "deleteText": "Ełìmena\nson de fondo", + "duplicateText": "Zdopia\nson de fondo", + "editSoundtrackText": "Mudador son de fondo", + "editText": "Muda\nson de fondo", + "fetchingITunesText": "rancurasion łiste de scolto de l'apl...", + "musicVolumeZeroWarning": "Avertensa: el vołume de ła mùzega el ze inpostà a 0", + "nameText": "Nome", + "newSoundtrackNameText": "El me son de fondo ${COUNT}", + "newSoundtrackText": "Son de fondo novo:", + "newText": "Novo\nson de fondo", + "selectAPlaylistText": "Sełesiona na łista de scolto", + "selectASourceText": "Fóntego mùzega", + "testText": "proa", + "titleText": "Soni de fondo", + "useDefaultGameMusicText": "Mùzega zugo predefenìa", + "useITunesPlaylistText": "Łista de scolto de l'apl", + "useMusicFileText": "File mùzegałe (mp3, evc)", + "useMusicFolderText": "Carteła de file muzegałi" + }, + "editText": "Muda", + "endText": "Moła", + "enjoyText": "Profìtaghine e gòdateła!", + "epicDescriptionFilterText": "${DESCRIPTION} in movensa camoma.", + "epicNameFilterText": "${NAME} in movensa camoma", + "errorAccessDeniedText": "aceso refudà", + "errorOutOfDiskSpaceText": "spasio so'l disco fenìo", + "errorText": "Eror", + "errorUnknownText": "eror mìa conosesto", + "exitGameText": "Vutu ndar fora da ${APP_NAME}?", + "exportSuccessText": "'${NAME}' esportà.", + "externalStorageText": "Memoria esterna", + "failText": "Desfata", + "fatalErrorText": "Orpo! Calcosa el manca o el ze danejà.\nProa a reinstałar l'apl o contata\n${EMAIL} par ver juto.", + "fileSelectorWindow": { + "titleFileFolderText": "Sełesiona un file o na carteła", + "titleFileText": "Sełesiona un file", + "titleFolderText": "Sełesiona na carteła", + "useThisFolderButtonText": "Dòpara 'sta carteła" + }, + "filterText": "Tamizo", + "finalScoreText": "Puntejo fenałe", + "finalScoresText": "Punteji fenałi", + "finalTimeText": "Tenpo fenałe", + "finishingInstallText": "Conpletamento instałasion: n'àtemo soło...", + "fireTVRemoteWarningText": "* Par mejorar l'espariensa, dòpara\ni controładori o instała l'app\n'${REMOTE_APP_NAME}' so i to\ntełèfoni o tołeti.", + "firstToFinalText": "Fenałe de Brinca ${COUNT} punti", + "firstToSeriesText": "Brinca ${COUNT} punti", + "fiveKillText": "BEN 5 SASINÀI!!!!", + "flawlessWaveText": "Ondada parfeta!", + "fourKillText": "EŁIMENÀI: 4!!!", + "friendScoresUnavailableText": "Punteji de i amighi mìa disponìbiłi.", + "gameCenterText": "GameCenter", + "gameCircleText": "GameCircle", + "gameLeadersText": "Clasìfega de'l łeveło ${COUNT}", + "gameListWindow": { + "cantDeleteDefaultText": "A no te połi miga ełimenar ła łista de zugo predefenìa.", + "cantEditDefaultText": "A no te połi miga modifegar ła łista de zugo predefenìa. Zdòpieła o crèaghine una nova.", + "cantShareDefaultText": "A no te połi miga sparpagnar ła łista de zugo predefenìa.", + "deleteConfirmText": "Vutu ełimenar \"${LIST}\"?", + "deleteText": "Ełìmena\nłista de zugo", + "duplicateText": "Zdopia\nłista de zugo", + "editText": "Muda\nłista de zugo", + "newText": "Nova\nłista de zugo", + "showTutorialText": "Demostrasion", + "shuffleGameOrderText": "Zmisia òrdene łevełi", + "titleText": "Parsonałiza łiste de zugo \"${TYPE}\"" + }, + "gameSettingsWindow": { + "addGameText": "Zonta partìa" + }, + "gamesToText": "${WINCOUNT} a ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Recòrdate: se A te ghè bastansa controładori, caun\ndispozitivo de un grupo el połe ospitar pì zugadori.", + "aboutDescriptionText": "Dòpara ’ste sesion par far sù un grupo.\n\nI grupi i te parmete de zugar partìe e tornèi\nco i to amighi infrà i vari dispozitivi.\n\nDòpara el boton ${PARTY} insima a drita par\nciacołar e interajir co’l to grupo.\n(so un controłador, struca ${BUTTON} co A te si inte un menù)", + "aboutText": "Info", + "addressFetchErrorText": "", + "appInviteMessageText": "L'utente ${NAME} el te ga mandà ${COUNT} biłieti so ${APP_NAME}", + "appInviteSendACodeText": "Màndaghe un còdaze", + "appInviteTitleText": "Invido a proar ${APP_NAME}", + "bluetoothAndroidSupportText": "(el funsiona so tuti i dispozitivi Android co el bluetooth)", + "bluetoothDescriptionText": "Òspita/zóntate inte un grupo co'l bluetooth:", + "bluetoothHostText": "Òspita", + "bluetoothJoinText": "Zóntate", + "bluetoothText": "Bluetooth", + "checkingText": "controło...", + "dedicatedServerInfoText": "Inposta un server dedegà par rezultài pì boni. Daghe un ocio so bombsquadgame.com/server par capir come far.", + "disconnectClientsText": "'Sta oparasion ła desconetarà ${COUNT} zugador/i\nda'l to grupo. Vutu ndar vanti?", + "earnTicketsForRecommendingAmountText": "Se i provarà el zugo, i to amighi i resevarà ${COUNT} biłieti\n(e ti A te ghin resevarè ${YOU_COUNT} par caun de łori che'l ło dopararà)", + "earnTicketsForRecommendingText": "Sparpagna el zugo par\nver biłieti gratùidi...", + "emailItText": "Màndeło par mail", + "friendHasSentPromoCodeText": "Par ti ${COUNT} biłieti de ${APP_NAME} da ${NAME}!", + "friendPromoCodeAwardText": "Tute łe 'olte che'l vegnarà doparà ti A te resevarè ${COUNT} biłieti.", + "friendPromoCodeExpireText": "El còdaze el ze soło par i zugaduri novi e el terminarà tenpo ${EXPIRE_HOURS} ore.", + "friendPromoCodeInstructionsText": "Par dopararlo, verzi ${APP_NAME} e và so \"Inpostasion > Avansàe > Insarisi còdaze\".\nDaghe un ocio so bombsquadgame.com par i link de descargamento de'l zugo par tute łe piataforme conpatìbiłi.", + "friendPromoCodeRedeemLongText": "El połe èsar scanbià par ${COUNT} biłieti gratùidi da ${MAX_USES} parsone.", + "friendPromoCodeRedeemShortText": "El połe èsar scanbià inte'l zugo par ${COUNT} biłieti gratùidi.", + "friendPromoCodeWhereToEnterText": "(so \"Inpostasion > Avansàe > Insarisi còdaze\")", + "getFriendInviteCodeText": "Jènara còdaze de invido", + "googlePlayDescriptionText": "Invida zugadori de Google Play inte'l to grupo:", + "googlePlayInviteText": "Invida", + "googlePlayReInviteText": "Se A te mandi un invido novo el/i ${COUNT} zugador(i) de\nGoogle Play inte'l to grupo el/i vegnarà desconetesto/i.\nPar tegnerlo/i in grupo màndaghe anca a łù/łori l'invido novo.", + "googlePlaySeeInvitesText": "Mostra invidi", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(soło inte ła varsion Android / Google Play)", + "hostPublicPartyDescriptionText": "Òspita un grupo pùblego:", + "inDevelopmentWarningText": "Nota:\n\nEl zugo in rede ła ze na funsion nova e in dezviłupo.\nPar deso, A ze dalvero racomandà che tuti i\nzugadori i sie tuti so ła mèdema rede wifi.", + "internetText": "Internet", + "inviteAFriendText": "I to amighi zełi sensa zugo? Invìdełi a\nproarlo e i resevarà ${COUNT} biłieti gratùidi.", + "inviteFriendsText": "Invida amighi", + "joinPublicPartyDescriptionText": "Zóntate inte un grupo pùblego:", + "localNetworkDescriptionText": "Zóntate inte un grupo so ła to rede:", + "localNetworkText": "Rede łogałe", + "makePartyPrivateText": "Canbia el me grupo in privà", + "makePartyPublicText": "Canbia el me grupo in pùblego", + "manualAddressText": "Ndariso", + "manualConnectText": "Coneti", + "manualDescriptionText": "Zóntate inte un grupo par ndariso:", + "manualJoinableFromInternetText": "Pòsitu èsar catà da internet?:", + "manualJoinableNoWithAsteriskText": "NÒ*", + "manualJoinableYesText": "SÌ", + "manualRouterForwardingText": "*par sistemar, proa configurar el to router in magnera che'l rederesione ła porta UDP ${PORT} a'l to ndariso łogałe", + "manualText": "A man", + "manualYourAddressFromInternetText": "El to ndariso da internet:", + "manualYourLocalAddressText": "El to ndariso łogałe:", + "noConnectionText": "", + "otherVersionsText": "(par altre varsion)", + "partyInviteAcceptText": "Bona", + "partyInviteDeclineText": "Anca nò", + "partyInviteGooglePlayExtraText": "(varda el paneło 'Google Play' inte ła fenestra 'Gregasion')", + "partyInviteIgnoreText": "Ignora", + "partyInviteText": "A te ze rivà un invido de ${NAME}\npar zontarte inte'l só grupo!", + "partyNameText": "Nome grupo", + "partySizeText": "grandesa", + "partyStatusCheckingText": "Varìfega condision...", + "partyStatusJoinableText": "deso el to grupo el połe èsar catà da internet", + "partyStatusNoConnectionText": "A no ze miga posìbiłe conétarse a'l server", + "partyStatusNotJoinableText": "el to grupo no'l połe mìa èsar catà da internet", + "partyStatusNotPublicText": "el to grupo no'l ze miga pùblego", + "pingText": "łatensa", + "portText": "Porta", + "requestingAPromoCodeText": "Drio far domanda de un còdaze...", + "sendDirectInvitesText": "Manda invidi dereti", + "shareThisCodeWithFriendsText": "Sparpagna 'sto còdaze co i amighi:", + "showMyAddressText": "Mostra el me ndariso", + "titleText": "Gregasion", + "wifiDirectDescriptionBottomText": "Se tuti i dispozitivi i gà ła sesion 'Wifi dereto', i sarà boni de dopararlo par\ncatarse e conétarse infrà de łori. Na volta che tuti i dispozitivi i sarà conetesti,\nA te podarè crear grupi inte ła sesion 'Rede łogałe', cofà ła fuse na rede wifi normałe.\n\nPar rezultài pì boni, mejo che l’ospitador de’l wifi dereto el sipie l’òspite anca de’l grupo so ${APP_NAME}.", + "wifiDirectDescriptionTopText": "El Wifi dereto el połe èsar doparà par conétar deretamente dispozitivi Android\nsensa pasar par ła rede wifi. El funsiona mejo so varsion Android 4.2 o pì resenti.\n\nPar dopararlo, verzi łe inpostasion wifi e controła ła funsion 'Wifi dereto'.", + "wifiDirectOpenWiFiSettingsText": "Verzi inpostasion wifi", + "wifiDirectText": "Wifi dereto", + "worksBetweenAllPlatformsText": "(el funsiona intrà tute łe piataforme)", + "worksWithGooglePlayDevicesText": "(el funsiona co tuti i dispozitivi co ła varsion de’l zugo de Google Play par Android)", + "youHaveBeenSentAPromoCodeText": "A te ze stà mandà un còdaze promosionałe de ${APP_NAME}:" + }, + "getTicketsWindow": { + "freeText": "GRATIS!", + "freeTicketsText": "Biłieti gratùidi", + "inProgressText": "Na tranzasion ła ze dezà in ełaborasion: proa danovo infrà na scianta.", + "purchasesRestoredText": "Cronpade repristenàe.", + "receivedTicketsText": "${COUNT} biłieti resevesti!", + "restorePurchasesText": "Reprìstena cronpade", + "ticketPack1Text": "Pacheto de biłieti ceło", + "ticketPack2Text": "Pacheto de biłieti mezan", + "ticketPack3Text": "Pacheto de biłieti grando", + "ticketPack4Text": "Pacheto de biłieti ultra", + "ticketPack5Text": "Pacheto de biłieti despropozità", + "ticketPack6Text": "Pacheto de biłieti defenidivo", + "ticketsFromASponsorText": "Vadagna ${COUNT} biłieti\nco na reclan", + "ticketsText": "${COUNT} biłieti", + "titleText": "Otien biłieti", + "unavailableLinkAccountText": "Ne despiaze, A no se połe miga cronpar so 'sta piataforma.\nVołendo, cofà sołusion, A te połi cołegar 'sto account co\nuno inte n'antra piataforma e cronpar calcosa da łà.", + "unavailableTemporarilyText": "'Sta funsion no ła ze mìa disponìbiłe par deso: proa danovo pì tardi.", + "unavailableText": "Ne despiaze, 'sta funsion no ła ze mìa disponìbiłe.", + "versionTooOldText": "Ne despiaze, 'sta varsion ła ze masa vecia: ajorna el zugo co cheła nova.", + "youHaveShortText": "A te ghè ${COUNT}", + "youHaveText": "A te ghè ${COUNT} biłieti" + }, + "googleMultiplayerDiscontinuedText": "Me despiaze, el sarviso multizugador de Google no'l ze miga pì disponìbiłe.\nA sò drio łaorar a un renpiaso pì in presa che se połe.\nIntanto proa n'antro mètodo de conesion.\n-Eric", + "googlePlayText": "Google Play", + "graphicsSettingsWindow": { + "alwaysText": "Senpre", + "fullScreenCmdText": "Schermo pien (Cmd-F)", + "fullScreenCtrlText": "Schermo pien (Ctrl-F)", + "gammaText": "Gama", + "highText": "Alta", + "higherText": "Màsema", + "lowText": "Basa", + "mediumText": "Mezana", + "neverText": "Mai", + "resolutionText": "Resołusion", + "showFPSText": "Mostra FPS", + "texturesText": "Defenision", + "titleText": "Gràfega", + "tvBorderText": "Bordaura TV", + "verticalSyncText": "Sincronizasion vertegałe", + "visualsText": "Prospetiva" + }, + "helpWindow": { + "bombInfoText": "- Bonbe -\nPì forti de i crogni, ma łe połe\nfarte małe anca a ti. Dopàrełe\nben tràndoghełe doso a i nemighi\nprima che łe salte par aria.", + "canHelpText": "${APP_NAME} el połe jutarte!", + "controllersInfoText": "A te połi zugar a ${APP_NAME} co i amighi co na rede o, se gavì\ncontroładori che basta, połì zugar tuti insenbre so el mèdemo\ndispozitivo: ${APP_NAME} el ghin suporta racuanti. Połì senpre doparar\ni tełèfoni cofà controładori co l’apl gratùida '${REMOTE_APP_NAME}'.\nPar info in pì varda so Inpostasion > Controładori.", + "controllersText": "Controładori", + "controlsSubtitleText": "El to amighévołe parsonajo de ${APP_NAME} el gà un fià de funsion de baze:", + "controlsText": "Controłi", + "devicesInfoText": "Ła varsion VR de ${APP_NAME} ła połe èsar zugada so na rede anca\nco ła varsion normałe de l'app, donca tira fora tełèfoni, tołeti\ne computers e taca a zugar! Podarìa èsar ùtiłe conétar na varsion\nnormałe de'l zugo a cheła VR anca soło par parmétarghe a łe parsone\nde poder ndarghe drio a'l zugo da fora.", + "devicesText": "Dispozitivi", + "friendsGoodText": "A ze senpre bona roba vèrghene. Se se ła gode de pì co pì zugadori\ne ${APP_NAME} el ghin suporta fin a 8, e 'sta roba ła ne mena a:", + "friendsText": "Amighi", + "jumpInfoText": "- Salto -\nSalta par traversar buzi cełi,\npar trar robe pì alte, o par\nfar védar tuta ła to ałegresa.", + "orPunchingSomethingText": "O de ciaparla a crogni, trarla zó par na croda, e fin che ła casca zó, farla saltar par aria co na bonba petaisa.", + "pickUpInfoText": "- Łeva sù -\nBrinca sù bandiere, nemighi o calsìase\naltra roba miga inciodada par tera.\nStruca danovo par trarla in jiro.", + "powerupBombDescriptionText": "El te parmete de trar in jiro fin\na 3 bonbe drioman invese che 1.", + "powerupBombNameText": "Bonbe triple", + "powerupCurseDescriptionText": "Probabilmente A te vorè schivarla\n'sta chive. ...o dìzitu de nò?", + "powerupCurseNameText": "Małedision", + "powerupHealthDescriptionText": "El te recarga de'l tuto ła vida.\nA no te ło gavarisi mai pensà, ahn?", + "powerupHealthNameText": "Parecio mèdego", + "powerupIceBombsDescriptionText": "Pì débołi de łe bonbe normałi\nma łe injasa i to nemighi e\nłi fà deventar pì fràjiłi.", + "powerupIceBombsNameText": "Bonbe jaso", + "powerupImpactBombsDescriptionText": "Un peło pì débołi de łe bonbe normałi,\nma łe sciopa suito co łe brinca calcosa.", + "powerupImpactBombsNameText": "Bonbe a inpato", + "powerupLandMinesDescriptionText": "In pacheti còmodi da 3!\nÙtiłi par na defeza de baze o\npar fermar nemighi ràpidi.", + "powerupLandMinesNameText": "Mine", + "powerupPunchDescriptionText": "I to crogni i deventarà uncora\nmejo: pì fisi, gajardi, ràpidi.", + "powerupPunchNameText": "Guantoni da boxe", + "powerupShieldDescriptionText": "El se sorbise un fià de dano in \nmagnera che no'l te rive a ti.", + "powerupShieldNameText": "Scudo nerjètego", + "powerupStickyBombsDescriptionText": "Łe se peta so cauna roba che\nłe toca. Zganasàe seguràe!", + "powerupStickyBombsNameText": "Bonbe petaise", + "powerupsSubtitleText": "Par forsa, gnaun zugo el sarìe conpleto sensa potensiadori:", + "powerupsText": "Potensiadori", + "punchInfoText": "- Crogno -\nPì che te te movi ràpidamente,\npì i to crogni i farà małe, donca\ncori, salta e và torno cofà un mato.", + "runInfoText": "- Corsa -\nTien strucà CALSÌASE boton par córar. I botoni de testa o i griłeti nałòzeghi i ze i pì adati se\nA te łi ghè. Corendo A te rivarè prima inte i posti ma sarà pì dura voltar, donca ocio a łe crode.", + "someDaysText": "In serti dì A se gà soło voja de tirarghe crogni a calcosa. O de farla saltar par aria.", + "titleText": "Istrusion de ${APP_NAME}", + "toGetTheMostText": "Par tirar fora el mejo da 'sto zugo A te serve:", + "welcomeText": "Benrivài so ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} l'è drio navegar intrà i menù a zbregabałon! -", + "importPlaylistCodeInstructionsText": "Dòpara 'sto còdaze par inportar 'sta łista de zugo ndove che te vołi:", + "importPlaylistSuccessText": "Łista de zugo '${NAME}' a ${TYPE} inportada", + "importText": "Inporta", + "importingText": "Inportasion...", + "inGameClippedNameText": "rento el zugo:\n\"${NAME}\"", + "installDiskSpaceErrorText": "EROR: A no ze mìa posìbiłe fenir l’instałasion.\nEl to dispozitivo el podarìa èsar sensa spasio.\nŁìbara un fià de memoria e proa danovo.", + "internal": { + "arrowsToExitListText": "struca ${LEFT} o ${RIGHT} par ndar fora da ła serie", + "buttonText": "boton", + "cantKickHostError": "A no te połi miga parar vìa l'ospitador.", + "chatBlockedText": "${NAME} l'è stà tajà fora da ła chat par ${TIME} segondi.", + "connectedToGameText": "A te te si zontà so '${NAME}'", + "connectedToPartyText": "A te te si zontà so'l grupo de ${NAME}!", + "connectingToPartyText": "Conesion...", + "connectionFailedHostAlreadyInPartyText": "Conesion fałìa: l'ospitador el ze inte n'antro grupo.", + "connectionFailedPartyFullText": "Conesion fałìa: el grupo el ze pien.", + "connectionFailedText": "Conesion fałìa.", + "connectionFailedVersionMismatchText": "Conesion fałìa: l'ospitador el gà na varsion de'l zugo defarente.\nSegureve de ver tuti do l'apl ajornada e provè danovo.", + "connectionRejectedText": "Conesion refudada.", + "controllerConnectedText": "${CONTROLLER} conetesto.", + "controllerDetectedText": "Rełevà 1 controłador.", + "controllerDisconnectedText": "${CONTROLLER} desconetesto.", + "controllerDisconnectedTryAgainText": "${CONTROLLER} desconetesto. Proa conétarlo danovo.", + "controllerForMenusOnlyText": "'Sto controłador no'l połe miga èsar doparà par zugar, ma soło par navegar intrà i menù.", + "controllerReconnectedText": "${CONTROLLER} reconetesto.", + "controllersConnectedText": "${COUNT} controładori conetesti.", + "controllersDetectedText": "${COUNT} controładori rełevài.", + "controllersDisconnectedText": "${COUNT} controładori desconetesti.", + "corruptFileText": "A ze stà catài fora file coronpesti. Proa reinstałar el zugo o manda na mail a: ${EMAIL}", + "errorPlayingMusicText": "Eror de reprodusion muzegałe: ${MUSIC}", + "errorResettingAchievementsText": "A no ze miga posìbiłe reinpostar i obietivi in łinea. Proa danovo pì tardi.", + "hasMenuControlText": "${NAME} l'à el controło de'l menù", + "incompatibleNewerVersionHostText": "L'ospitador el gà na varsion de'l zugo pì resente.\nAjórneło anca ti a ła varsion ùltema e proa danovo.", + "incompatibleVersionHostText": "L'ospitador el gà na varsion de'l zugo defarente.\nSegùreve de ver tuti do l'apl ajornada e provè danovo.", + "incompatibleVersionPlayerText": "${NAME} l'à na varsion de'l zugo defarente.\nSegùreve de ver tuti do l'apl ajornada e provè danovo.", + "invalidAddressErrorText": "Eror: ndariso miga vàłido.", + "invalidNameErrorText": "Eror: nome miga vàłido.", + "invalidPortErrorText": "Eror: porta miga vàłida.", + "invitationSentText": "Invido mandà.", + "invitationsSentText": "Mandài ${COUNT} invidi.", + "joinedPartyInstructionsText": "Calchedun el se gà zontà inte'l to grupo.\nVà so \"Zuga\" par tacar na partìa.", + "keyboardText": "Botonera", + "kickIdlePlayersKickedText": "A ze stà parà vìa ${NAME} par masa sonera.", + "kickIdlePlayersWarning1Text": "Se ła só sonera ła sèvita, ${NAME} l'vegnarà parà vìa tenpo ${COUNT} segondi.", + "kickIdlePlayersWarning2Text": "(A te połi dezativarlo so Inpostasion > Avansàe)", + "leftGameText": "A te si ndà fora da '${NAME}'.", + "leftPartyText": "A te si ndà fora da'l grupo de ${NAME}.", + "noMusicFilesInFolderText": "Ła carteła no ła gà rento gnaun file muzegałe.", + "playerJoinedPartyText": "${NAME} l'se gà zontà inte'l grupo!", + "playerLeftPartyText": "${NAME} l'è ndà fora da'l grupo!", + "rejectingInviteAlreadyInPartyText": "Invido refudà (dezà inte un grupo).", + "serverRestartingText": "El server el ze drio retacarse. Reconétate infrà na scianta...", + "serverShuttingDownText": "El server el ze drio stuarse...", + "signInErrorText": "Eror in entrada.", + "signInNoConnectionText": "A no ze mìa posìbiłe ndar rento. (gnauna conesion a internet?)", + "telnetAccessDeniedText": "EROR: l'utente no'l gà mìa parmeso l'aceso co telnet.", + "timeOutText": "(tocarà a ti tenpo ${TIME} segondi)", + "touchScreenJoinWarningText": "A te te si zontà co'l touchscreen.\nSe ła ze stà na capeła, struca 'Menù > Moła łeveło'.", + "touchScreenText": "TouchScreen", + "unableToResolveHostText": "Eror: A no ze mìa posìbiłe conétarse co l'ospitador.", + "unavailableNoConnectionText": "Par deso mìa disponìbiłe (gnauna conesion a internet?)", + "vrOrientationResetCardboardText": "Dopàreło par reinpostar l'orientasion de'l VR.\nA te servirà un controłador esterno par zugar.", + "vrOrientationResetText": "Reinposta orientasion VR.", + "willTimeOutText": "(ma se in sonera el ghe vegnarà cavà)." + }, + "jumpBoldText": "SALTO", + "jumpText": "Salto", + "keepText": "Tien", + "keepTheseSettingsText": "Vutu tegner 'ste inpostasion?", + "keyboardChangeInstructionsText": "Struca spasio do 'olte par mudar botonera.", + "keyboardNoOthersAvailableText": "A no ghe ze miga altre botonere disponìbiłi.", + "keyboardSwitchText": "Pasajo a ła botonera \"${NAME}\".", + "kickOccurredText": "${NAME} l'è stà parà vìa.", + "kickQuestionText": "Vutu parar vìa ${NAME}?", + "kickText": "Para vìa", + "kickVoteCantKickAdminsText": "I aministradori no i połe mìa èsar parài vìa.", + "kickVoteCantKickSelfText": "A no te połi mìa pararte vìa da soło.", + "kickVoteFailedNotEnoughVotersText": "A ghe ze masa pochi zugadori par na votasion.", + "kickVoteFailedText": "Votasion de zlontanamento fałìa.", + "kickVoteStartedText": "A se gà tacà na votasion par parar vìa ${NAME}.", + "kickVoteText": "Vota par parar vìa", + "kickVotingDisabledText": "El voto de zlontanamento el ze dezativà.", + "kickWithChatText": "Scrivi inte ła chat ${YES} par sì e ${NO} par nò.", + "killsTallyText": "${COUNT} fati fora", + "killsText": "Fati fora", + "kioskWindow": { + "easyText": "Fàsiłe", + "epicModeText": "Movensa camoma", + "fullMenuText": "Menù intiero", + "hardText": "Defìsiłe", + "mediumText": "Medhan", + "singlePlayerExamplesText": "Ezenpi de Zugador ùgnoło / Cooperadiva", + "versusExamplesText": "Ezenpi de Scontro" + }, + "languageSetText": "Łengua inpostada: \"${LANGUAGE}\".", + "lapNumberText": "Jiro ${CURRENT}/${TOTAL}", + "lastGamesText": "(${COUNT} partìe ùlteme)", + "leaderboardsText": "Clasìfeghe", + "league": { + "allTimeText": "Clasìfega globałe", + "currentSeasonText": "Stajon in corso (${NUMBER})", + "leagueFullText": "Łega ${NAME}", + "leagueRankText": "Pozision łega", + "leagueText": "Łega", + "rankInLeagueText": "#${RANK}, ${NAME} Łega${SUFFIX}", + "seasonEndedDaysAgoText": "Stajon fenìa da ${NUMBER} dì.", + "seasonEndsDaysText": "Ła stajon ła fenirà tenpo ${NUMBER} dì.", + "seasonEndsHoursText": "Ła stajon ła fenirà tenpo ${NUMBER} ore.", + "seasonEndsMinutesText": "Ła stajon ła fenirà tenpo ${NUMBER} menuti.", + "seasonText": "Stajon ${NUMBER}", + "tournamentLeagueText": "A te ghè da rivar inte ła łega ${NAME} par zugar inte 'sto tornèo.", + "trophyCountsResetText": "Par ła stajon che ła vien el puntejo\nde i trofèi el se zerarà." + }, + "levelBestScoresText": "I punteji mejo so ${LEVEL}", + "levelBestTimesText": "I tenpi mejo so ${LEVEL}", + "levelIsLockedText": "${LEVEL} el ze blocà.", + "levelMustBeCompletedFirstText": "Prima A te ghè da conpletar ${LEVEL}.", + "levelText": "Łeveło ${NUMBER}", + "levelUnlockedText": "Łeveło dezblocà!", + "livesBonusText": "Vite bonus", + "loadingText": "cargamento", + "loadingTryAgainText": "Cargamento: proa danovo infrà na scianta...", + "macControllerSubsystemBothText": "Tuti do (ma miga racomandà)", + "macControllerSubsystemClassicText": "Clàsego", + "macControllerSubsystemDescriptionText": "(se i to controładori no i funsiona, proa mudar 'sta inpostasion)", + "macControllerSubsystemMFiNoteText": "Rełevà controłador 'Fato par iOS/Mac';\nSe te ghè caro ativarlo, và so Inpostasion > Controładori", + "macControllerSubsystemMFiText": "Fato par iOS/Mac", + "macControllerSubsystemTitleText": "Suporto controłador", + "mainMenu": { + "creditsText": "Mension", + "demoMenuText": "Menù demo", + "endGameText": "Sara sù partìa", + "exitGameText": "Sortisi da'l zugo", + "exitToMenuText": "Vutu tornar inte'l menù?", + "howToPlayText": "Come zugar", + "justPlayerText": "(Soło par ${NAME})", + "leaveGameText": "Moła łeveło", + "leavePartyConfirmText": "Vutu dalvero ndar fora da'l grupo?", + "leavePartyText": "Va fora da'l grupo", + "quitText": "Sortisi", + "resumeText": "Continua", + "settingsText": "Inpostasion" + }, + "makeItSoText": "Àplega", + "mapSelectGetMoreMapsText": "Otien pì łevełi...", + "mapSelectText": "Sełesiona...", + "mapSelectTitleText": "Łevełi ${GAME}", + "mapText": "Łeveło", + "maxConnectionsText": "Łìmite conesion", + "maxPartySizeText": "Łìmite grandesa grupo", + "maxPlayersText": "Łìmite zugadori", + "modeArcadeText": "Modałidà arcade", + "modeClassicText": "Modałidà clàsega", + "modeDemoText": "Modałidà demo", + "mostValuablePlayerText": "Zugador pì zgajo", + "mostViolatedPlayerText": "Zugador pì sacagnà", + "mostViolentPlayerText": "Zugador pì viołento", + "moveText": "Movi", + "multiKillText": "CAENA DE ${COUNT} FATI FORA!!!!!", + "multiPlayerCountText": "${COUNT} zugadori", + "mustInviteFriendsText": "Nota: A te ghè da invidar i amighi\nso'l paneło \"${GATHER}\" o picar pì\ncontroładori par zugar in multizugador.", + "nameBetrayedText": "${NAME} l'à tradìo ${VICTIM}.", + "nameDiedText": "${NAME} l'è crepà.", + "nameKilledText": "${NAME} l'à copà ${VICTIM}.", + "nameNotEmptyText": "El nome no'l połe miga star vodo!", + "nameScoresText": "Un ponto par ${NAME}!", + "nameSuicideKidFriendlyText": "${NAME} par zbałio L ze crepà.", + "nameSuicideText": "${NAME} l'se gà eutanà.", + "nameText": "Nome", + "nativeText": "Nadiva", + "newPersonalBestText": "Novo record parsonałe!", + "newTestBuildAvailableText": "A ze disponìbiłe na varsion de proa nova. (${VERSION} beta ${BUILD}).\nDescàrgheła da ${ADDRESS}", + "newText": "Novo", + "newVersionAvailableText": "A ze disponìbiłe na varsion nova de ${APP_NAME}! (${VERSION})", + "nextAchievementsText": "Obietivi pròsemi:", + "nextLevelText": "Łeveło seguente", + "noAchievementsRemainingText": "- gnaun", + "noContinuesText": "(sensa continui)", + "noExternalStorageErrorText": "Inte ’sto dispozitivo A no ze stà catada gnauna memoria esterna", + "noGameCircleText": "Eror: A no te si miga conetesto co GameCircle", + "noScoresYetText": "Gnancora gnaun puntejo.", + "noThanksText": "Nò, grasie", + "noTournamentsInTestBuildText": "AVERTENSA: i punteji de'l tornèo de 'sta varsion de proa i vegnarà ignorài.", + "noValidMapsErrorText": "A no ze stà catà gnaun łeveło vàłido par 'sto tipo de zugo.", + "notEnoughPlayersRemainingText": "A no ghe ze pì zugadori che basta: sortisi e taca na partìa nova.", + "notEnoughPlayersText": "A serve almanco ${COUNT} zugadori par tacar 'sta partìa!", + "notNowText": "Miga deso", + "notSignedInErrorText": "Par partesipar A te ghè da conétarte.", + "notSignedInGooglePlayErrorText": "Far farlo A te ghè da conétarte co Google Play.", + "notSignedInText": "gnancora cołegà", + "nothingIsSelectedErrorText": "A no ze sełesionà gnente!", + "numberText": "#${NUMBER}", + "offText": "Dezativa", + "okText": "Và ben", + "onText": "Ativa", + "onslaughtRespawnText": "Rejenerasion de ${PLAYER} inte l'ondada ${WAVE}", + "orText": "${A} o ${B}", + "otherText": "Altro...", + "outOfText": "(#${RANK} de ${ALL})", + "ownFlagAtYourBaseWarning": "Par far punti ła to bandiera\nła gà da èsar so ła to baze!", + "packageModsEnabledErrorText": "El zugo in rede no'l ze miga parmeso co A ze ativi i pacheti mod łogałi (varda so Inpostasion > Avansàe)", + "partyWindow": { + "chatMessageText": "Mesaji", + "emptyText": "El to grupo el ze vodo", + "hostText": "(ospitador)", + "sendText": "Manda", + "titleText": "El to grupo" + }, + "pausedByHostText": "(sospezo da l'ospitador)", + "perfectWaveText": "Ondada parfeta!", + "pickUpText": "Łeva sù", + "playModes": { + "coopText": "Cooperadiva", + "freeForAllText": "Tuti contro tuti", + "multiTeamText": "Multi-scuadra", + "singlePlayerCoopText": "Un zugador / Cooperadiva", + "teamsText": "Scuadre" + }, + "playText": "Zuga", + "playWindow": { + "oneToFourPlayersText": "1-4 zugadori", + "titleText": "Zuga", + "twoToEightPlayersText": "2-8 zugadori" + }, + "playerCountAbbreviatedText": "${COUNT}z", + "playerDelayedJoinText": "Co tacarà el turno che'l vien A se zontarà anca ${PLAYER}.", + "playerInfoText": "Informasion zugador", + "playerLeftText": "${PLAYER} l'à mołà el łeveło.", + "playerLimitReachedText": "Nùmaro màsemo de ${COUNT} zugadori: A no połe zontarse pì nesun.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "A no te połi miga ełimenar el profiło prinsipałe de'l to account.", + "deleteButtonText": "Ełìmena\nprofiło", + "deleteConfirmText": "Vutu ełimenar '${PROFILE}'?", + "editButtonText": "Muda\nprofiło", + "explanationText": "(nomi e parense parsonałizàe par 'sto account)", + "newButtonText": "Novo\nprofiło", + "titleText": "Profiłi zugador" + }, + "playerText": "Zugador", + "playlistNoValidGamesErrorText": "'Sta łista de zugo ła gà rento łevełi dezblocài mìa vàłidi.", + "playlistNotFoundText": "łista de zugo mìa catada", + "playlistsText": "Łiste de zugo", + "pleaseRateText": "Se ${APP_NAME} el ze drio piazerte, tote un àtemo par\nłasarghe zó na vałudasion o scrìvarghe zó un comento. 'Ste\nopinion łe tornarà còmode par dezviłupi fuduri de'l zugo.\n\ngrasie!\n-eric", + "pleaseWaitText": "Speta n'àtemo...", + "pluginsDetectedText": "Estension nove rełevàe. Atìvełe/configùrełe inte łe inpostasion.", + "pluginsText": "Estension", + "practiceText": "Pràtega", + "pressAnyButtonPlayAgainText": "Struca un boton calsìase par zugar danovo...", + "pressAnyButtonText": "Struca un boton calsìase par ndar vanti...", + "pressAnyButtonToJoinText": "struca un boton calsìase par zontarte...", + "pressAnyKeyButtonPlayAgainText": "Struca un boton calsìase par zugar danovo...", + "pressAnyKeyButtonText": "Struca un boton calsìase par ndar vanti...", + "pressAnyKeyText": "Struca un boton calsìase...", + "pressJumpToFlyText": "** Sèvita strucar SALTO par zvołar **", + "pressPunchToJoinText": "struca CROGNO par zontarte...", + "pressToOverrideCharacterText": "struca ${BUTTONS} par renpiasar el to parsonajo", + "pressToSelectProfileText": "struca ${BUTTONS} par sernir un profiło zugador", + "pressToSelectTeamText": "struca ${BUTTONS} par sernir na scuadra", + "promoCodeWindow": { + "codeText": "Còdaze", + "enterText": "Dòpara" + }, + "promoSubmitErrorText": "Eror de trazmision de'l còdaze: controła ła to conesion internet", + "ps3ControllersWindow": { + "macInstructionsText": "Stua ła corente so'l dadrìo de ła to PS3, segùrate che'l\nbluetooth de'l to Mac el sipie ativà, daspò coneti el to controłador\na'l to Mac co un cavo USB par cubiarli. Da deso in vanti A te\npołi doparar el boton \"cao\" de'l controłador par conétarlo co'l to\nMac in modałidà USB (co'l fiło) o bluetooth (sensa fiło).\n\nPar el cubiamento calche Mac el podarìa dimandarte un còdaze.\nSe càpida, varda ła demostrasion seguente o serca juto so Google.\n\n\n\n\nI controładori PS3 conetesti sensa fiło i gavarìa da védarse inte ła łista\nde'l dispozitivo so Prefarense de sistema > Bluetooth. Co te vorè\ndopararli danovo so ła to PS3, te podarisi ver da cavarli vìa da 'sta łista.\n\nSegùrate anca de desconétarli da'l bluetooth co A no te łi\ndòpari o łe só batarìe łe sevitarà a sconsumarse.\n\nEl bluetooth el gavarìa da suportar fin a 7 dispozitivi conetesti,\nanca se ła capasidà ła podarìa variar.", + "ouyaInstructionsText": "Par doparar un controłador PS3 co ła to OUYA, conéteło co un cavo USB par\ncubiarlo. 'Sta asion ła podarìa desconétar cheł'altri to controładori, donca\nA podarìa servirte retacar ła to OUYA e destacar el cavo USB.\n\nDa deso in vanti A te dovarisi èsar bon de doparar el boton \"cao\" de'l\ncontrołador par conétarlo sensa fiło. Co A te ghè fenìo de zugar, tien\nstrucà el boton \"cao\" par 10 segondi par stuar el controłador, el\npodarìa restar inpisà e stracar ła batarìa.", + "pairingTutorialText": "demostrasion video pa'l cubiamento", + "titleText": "Doparar un controłador PS3 co ${APP_NAME}:" + }, + "punchBoldText": "CROGNO", + "punchText": "Crogno", + "purchaseForText": "Cronpa par ${PRICE}", + "purchaseGameText": "Cronpa el zugo", + "purchasingText": "Cronpa in corso...", + "quitGameText": "Vutu sortir da ${APP_NAME}?", + "quittingIn5SecondsText": "Sortìa tenpo 5 segondi...", + "randomPlayerNamesText": "Toni, Bepi, Ciano, Ico, Tisio, Senpronio, Cajo, Ciorci, Ucio, Tòio, Stełio, Duiłio, Mariza, Sunta, Tano, Tilde, Piero, Neno, Nena, Momi, Ménego, Łełe, Jani, Jaco, Cicio, Giza, Checo, Bice, Beta, Gneze, Àndoło, Jijo, Aneta, Bórtoło, Metrio, Gidio, Gnegno, Baldi, Icio, Nane, Tano, Jema, Maria", + "randomText": "A stin", + "rankText": "Pozision", + "ratingText": "Vałudasion", + "reachWave2Text": "Riva so ła segonda ondada par clasifegarte.", + "readyText": "aposto", + "recentText": "Resenti", + "remoteAppInfoShortText": "Co A se zuga a ${APP_NAME} co fameja e amighi A se se ła\ngode de pì. Coneti uno o pì controładori fìzeghi o instała l'apl\n${REMOTE_APP_NAME} so tełèfoni o tołeti par dopararli cofà\ncontroładori.", + "remote_app": { + "app_name": "BombSquad Remote", + "app_name_short": "BSRemote", + "button_position": "Pozision botoni", + "button_size": "Grandesa botoni", + "cant_resolve_host": "A no ze mìa posìbiłe catar fora l'ospitador.", + "capturing": "Drio spetar i zugadori…", + "connected": "Conetesto.", + "description": "Dòpara el to tełèfono o tołeto cofà controłador par BombSquad.\nA połe conétarse so un schermo ùgnoło fin a 8 dispozitivi insenbre, par un feston bueło multizugador!", + "disconnected": "Desconetesto da'l server.", + "dpad_fixed": "fiso", + "dpad_floating": "mòbiłe", + "dpad_position": "Pozision croze deresionałe", + "dpad_size": "Grandesa croze deresionałe", + "dpad_type": "Tipo de croze deresionałe", + "enter_an_address": "Insarisi ndariso", + "game_full": "El zugo el ze pien o no l'aceta conesion.", + "game_shut_down": "El zugo el se gà stuà.", + "hardware_buttons": "Botoni fìzeghi", + "join_by_address": "Zóntate par ndariso...", + "lag": "Lag: ${SECONDS} segondi", + "reset": "Reinposta", + "run1": "Corsa 1", + "run2": "Corsa 2", + "searching": "Reserca partìe de BombSquad…", + "searching_caption": "Toca el nome de ła partìa par zontarte.\nSegùrate de èsar so ła mèdema rede wifi de ła partìa.", + "start": "Taca", + "version_mismatch": "Varsion zbałiada.\nSegùrate che BombSquad e BombSquad Remote i\nsipie ajornài a ła varsion ùltema e proa danovo." + }, + "removeInGameAdsText": "Par cavar vìa łe reclan, dezbloca \"${PRO}\" inte ła botega.", + "renameText": "Renòmena", + "replayEndText": "Sara sù Revardo", + "replayNameDefaultText": "Revardo partìa ùltema", + "replayReadErrorText": "Eror de łedura de'l file de revardo.", + "replayRenameWarningText": "Par salvar na partìa fenìa, renòmena \"${REPLAY}\". Senò te sevitarè sorascrìvarle.", + "replayVersionErrorText": "Ne despiaze, 'sto revardo el ze stà fato co na varsion\nde'l zugo defarente e no'l połe mìa èsar doparà.", + "replayWatchText": "Varda revardo", + "replayWriteErrorText": "Eror de salvatajo de'l revardo.", + "replaysText": "Revardi", + "reportPlayerExplanationText": "Dòpara 'sta mail par segnałar inbroji, łenguaji ofansivi o altri conportamenti bruti.\nZonta na descrision soto chive:", + "reportThisPlayerCheatingText": "Inbrojo", + "reportThisPlayerLanguageText": "Łenguajo ofansivo", + "reportThisPlayerReasonText": "'Sa ghetu caro segnałar?", + "reportThisPlayerText": "Segnała 'sto zugador", + "requestingText": "Drio far domanda...", + "restartText": "Retaca", + "retryText": "Reproa", + "revertText": "Anuła", + "runText": "Cori", + "saveText": "Salva", + "scanScriptsErrorText": "Tirando sù i script A se gà catà erori: varda el rejistro eventi par i detaji.", + "scoreChallengesText": "Puntejo sfide", + "scoreListUnavailableText": "Łista de puntejo mìa disponìbiłe.", + "scoreText": "Puntejo", + "scoreUnits": { + "millisecondsText": "Miłesegondi", + "pointsText": "Punti", + "secondsText": "Segondi" + }, + "scoreWasText": "(prima ${COUNT})", + "selectText": "Sełesiona", + "seriesWinLine1PlayerText": "L'À VINTO ŁA", + "seriesWinLine1TeamText": "L'À VINTO ŁA", + "seriesWinLine1Text": "L'À VINTO ŁA", + "seriesWinLine2Text": "DESFIDA!", + "settingsWindow": { + "accountText": "Account", + "advancedText": "Avansàe", + "audioText": "Àudio", + "controllersText": "Controładori", + "graphicsText": "Gràfega", + "playerProfilesMovedText": "Nota: \"Profiłi zugador\" el ze stà spostà rento ła sesion \"Account\" inte'l menù prinsipałe.", + "titleText": "Inpostasion" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(na botonera so schermo, senpia e còmoda, par scrìvar testi)", + "alwaysUseInternalKeyboardText": "Dòpara botonera integrada", + "benchmarksText": "Prestasion & Proe soto sforso", + "disableCameraGyroscopeMotionText": "Dezativa movimento de ła prospetiva łigada a'l jiroscopio", + "disableCameraShakeText": "Dezativa i zgorlamenti de ła videocàmara", + "disableThisNotice": "(A te połi dezativar 'sta notìfega inte łe inpostasion avansàe)", + "enablePackageModsDescriptionText": "(l'ativasion de ła capasidà de modifegasion ła dezativa el zugo in rede)", + "enablePackageModsText": "Ativa pacheti mod łogałi", + "enterPromoCodeText": "Insarisi còdaze", + "forTestingText": "Nota: vałori vàłidi soło par proe. Sortendo da l'apl łi vegnarà perdesti.", + "helpTranslateText": "Łe tradusion de ${APP_NAME} łe ze curàe da vołontari.\nSe A te vol darghe na ociada a cheła veneta, struca so'l boton\ncuà soto. Curada da Veneto Còdaze: codazeveneto@gmail.com", + "kickIdlePlayersText": "Para vìa zugadori in sonera", + "kidFriendlyModeText": "Modałidà bocia (viołensa reduzesta, evc)", + "languageText": "Łengua", + "moddingGuideText": "Guida par modifegasion", + "mustRestartText": "Par rèndar efetive łe modìfeghe, A te ghè da retacar el zugo.", + "netTestingText": "Proa de rede", + "resetText": "Reinposta", + "showBombTrajectoriesText": "Mostra trajetore bonbe", + "showPlayerNamesText": "Mostra nomi zugadori", + "showUserModsText": "Mostra carteła modifegasion", + "titleText": "Avansàe", + "translationEditorButtonText": "Piataforma de tradusion de ${APP_NAME}", + "translationFetchErrorText": "stado de ła tradusion mìa disponìbiłe", + "translationFetchingStatusText": "controło stado de ła tradusion...", + "translationInformMe": "Infòrmame co A ghe ze tochi novi da tradùzar", + "translationNoUpdateNeededText": "ła tradusion in veneto ła ze aposto: un aereo!", + "translationUpdateNeededText": "** A ghe ze testi novi da tradùzar!! **", + "vrTestingText": "Proa VR" + }, + "shareText": "Sparpagna", + "sharingText": "Sparpagnasion...", + "showText": "Mostra", + "signInForPromoCodeText": "Conétate co un account par poder far funsionar el còdaze.", + "signInWithGameCenterText": "Par doparar un account Game Center,\nconétate da rento l'apl Game Center.", + "singleGamePlaylistNameText": "Tuti \"${GAME}\"", + "singlePlayerCountText": "1 zugador", + "soloNameFilterText": "\"${NAME}\" 1 VS 1", + "soundtrackTypeNames": { + "CharSelect": "Sełesion parsonajo", + "Chosen One": "L'ełeto", + "Epic": "Zughi in movensa camoma", + "Epic Race": "Corsa camoma", + "FlagCatcher": "Brinca ła bandiera", + "Flying": "Pensieri contenti", + "Football": "Football", + "ForwardMarch": "Asalto", + "GrandRomp": "Conchista", + "Hockey": "Hockey", + "Keep Away": "Tien distante", + "Marching": "Mena torno", + "Menu": "Menù prinsipałe", + "Onslaught": "Dezìo", + "Race": "Corsa", + "Scary": "Re de'l col", + "Scores": "Schermada puntejo", + "Survival": "Ełimenasion", + "ToTheDeath": "Scontro mortałe", + "Victory": "Schermada puntejo fenałe" + }, + "spaceKeyText": "spasio", + "statsText": "Statìsteghe", + "storagePermissionAccessText": "A serve el parmeso de aceso a ła memoria", + "store": { + "alreadyOwnText": "${NAME} dezà cronpà!", + "bombSquadProNameText": "${APP_NAME} Pro", + "bombSquadProNewDescriptionText": "• El cava vìa i anunsi e łe reclan\n• El dezbloca tute łe funsion de'l zugo\n• El te dà anca:", + "buyText": "Cronpa", + "charactersText": "Parsonaji", + "comingSoonText": "E presto...", + "extrasText": "Extra", + "freeBombSquadProText": "Daromài BombSquad el ze gratis, ma visto che A te ło ghivi dezà cronpà prima,\nA te vegnarà dezblocà el mejoramento BombSquad Pro e A te vegnarà zontài ${COUNT}\nbiłieti cofà rengrasiamento. Gòdate łe funsion nove, e grasie de'l to suporto!\n-Eric", + "holidaySpecialText": "Spesiałe feste", + "howToSwitchCharactersText": "(và so \"${SETTINGS} > ${PLAYER_PROFILES}\" par sernir i to parsonaji e parsonałizarli)", + "howToUseIconsText": "(par dopararle, crea un profiło zugador globałe inte ła sesion account)", + "howToUseMapsText": "(dòpara 'sti łevełi inte łe to łiste de zugo: scuadre/tuti contro tuti)", + "iconsText": "Icone", + "loadErrorText": "A no ze mìa posìbiłe cargar ła pàjina.\nDaghe na ociada a ła to conesion internet.", + "loadingText": "cargamento", + "mapsText": "Łevełi", + "miniGamesText": "Minizughi", + "oneTimeOnlyText": "(ocazion ùgnoła)", + "purchaseAlreadyInProgressText": "Cronpa de 'sto ojeto dezà drio conpirse.", + "purchaseConfirmText": "Vutu cronpar ${ITEM}?", + "purchaseNotValidError": "Cronpa mìa vàłida.\nSe'l ze un eror, contata ${EMAIL}.", + "purchaseText": "Cronpa", + "saleBundleText": "Pacheto in oferta!", + "saleExclaimText": "Oferta!", + "salePercentText": "(${PERCENT}% de manco)", + "saleText": "SCONTI", + "searchText": "Serca", + "teamsFreeForAllGamesText": "Par zughi a Scuadre / Tuti contro tuti", + "totalWorthText": "*** Un vałor de ${TOTAL_WORTH} a soło ***", + "upgradeQuestionText": "Mejorar BombSquad?", + "winterSpecialText": "Spesiałe inverno", + "youOwnThisText": "- dezà tuo -" + }, + "storeDescriptionText": "Feston bueło a 8 zugadori!\n\nFà saltar par aria i to amighi (o el computer) inte un tornèo de minizughi cofà 'Brinca ła bandiera', 'Scravaso de stełe' o 'Scontri mortałi' in movensa camoma!\n\nFin a 8 parsone łe sarà bone de batajar insenbre, grasie a comandi senpi e a ła conpatibiłidà co tanti controładori. Co l'apl gratùida 'BombSquad Remote' A te połi parfin doparar i to dispozitivi mòbiłi cofà controładori!\n\nE deso... tira fora łe bonbe!\n\nPar informasion in pì daghe na ociada so www.froemling.net/bombsquad.", + "storeDescriptions": { + "blowUpYourFriendsText": "Fà sciopar par aria i to amighi.", + "competeInMiniGamesText": "Zuga co tanti minizughi: da corse a zvołi.", + "customize2Text": "Parsonałiza parsonaji, minizughi, o anca i son de fondo.", + "customizeText": "Parsonałiza i parsonaji e crea łe to łiste de minizughi.", + "sportsMoreFunText": "Inte i sport A se se ła gode de pì co i esprozivi.", + "teamUpAgainstComputerText": "Cołàbora contro el computer." + }, + "storeText": "Botega", + "submitText": "Manda", + "submittingPromoCodeText": "Trazmision còdaze...", + "teamNamesColorText": "Nome/Cołor scuadra...", + "telnetAccessGrantedText": "Aceso a telnet ativà.", + "telnetAccessText": "Rełevà aceso a telnet: vutu autorizarlo?", + "testBuildErrorText": "'Sta varsion de proa no ła ze miga pì ativa. Controła se A ghin ze una pì resente.", + "testBuildText": "Varsion de proa", + "testBuildValidateErrorText": "A no ze mìa posìbiłe varifegar ła varsion de proa. (gnauna conesion a internet?)", + "testBuildValidatedText": "Varsion de proa aposto. Gòdateła!", + "thankYouText": "Grasie par el to suporto! Gùstate el zugo!", + "threeKillText": "NO GHE N'È 2 SENSA 3!!", + "timeBonusText": "Tenpo bonus", + "timeElapsedText": "Tenpo pasà", + "timeExpiredText": "Tenpo fenìo", + "timeSuffixDaysText": "${COUNT}d", + "timeSuffixHoursText": "${COUNT}o", + "timeSuffixMinutesText": "${COUNT}m", + "timeSuffixSecondsText": "${COUNT}s", + "tipText": "Drita", + "titleText": "BombSquad", + "titleVRText": "BombSquad VR", + "topFriendsText": "Mejori amighi", + "tournamentCheckingStateText": "Verìfega stado de'l tornèo: speta n'àtemo...", + "tournamentEndedText": "'Sto tornèo el ze fenìo. A ghin tacarà presto uno novo.", + "tournamentEntryText": "Entrada tornèo", + "tournamentResultsRecentText": "Rezultài tornèi resenti", + "tournamentStandingsText": "Clasìfega tornèo", + "tournamentText": "Tornèo", + "tournamentTimeExpiredText": "Tenpo de'l tornèo fenìo!", + "tournamentsText": "Tornèi", + "translations": { + "characterNames": { + "Agent Johnson": "Ajente Johnson", + "B-9000": "B-9000", + "Bernard": "Pùpoło", + "Bones": "Oseto", + "Butch": "Vacher", + "Easter Bunny": "Cunicio de Pascua", + "Flopsy": "Jévare", + "Frosty": "Zgrìzoło", + "Gretel": "Reitia", + "Grumbledorf": "Silendalf", + "Jack Morgan": "Cap. Canaja", + "Kronk": "Sciavon", + "Lee": "Tàng", + "Lucky": "Masaroło", + "Mel": "Cogo", + "Middle-Man": "Gnagno", + "Minimus": "Antènore", + "Pascal": "Pinghin", + "Pixel": "Fada", + "Sammy Slam": "Tentori", + "Santa Claus": "Pupà Nadal", + "Snake Shadow": "Bison", + "Spaz": "Fagin", + "Taobao Mascot": "Mascot Taobao", + "Todd McBurton": "Mc Tresà", + "Zoe": "Sone", + "Zola": "Màsone" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} - łenamento", + "Infinite ${GAME}": "${GAME} - sensa fine", + "Infinite Onslaught": "Dezìo - sensa fine", + "Infinite Runaround": "Mena torno - sensa fine", + "Onslaught Training": "Dezìo - łenamento", + "Pro ${GAME}": "${GAME} - pro", + "Pro Football": "Football - pro", + "Pro Onslaught": "Dezìo - pro", + "Pro Runaround": "Mena torno - pro", + "Rookie ${GAME}": "${GAME} - novisià", + "Rookie Football": "Football - novisià", + "Rookie Onslaught": "Dezìo - novisià", + "The Last Stand": "Resta in pie", + "Uber ${GAME}": "${GAME} - ultra", + "Uber Football": "Football - ultra", + "Uber Onslaught": "Dezìo - ultra", + "Uber Runaround": "Mena torno - ultra" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Copa l'ełeto par ciapar el só posto!\nPar vìnsar resta l'ełeto par un serto tenpo.", + "Bomb as many targets as you can.": "Bonbarda tuti i sentri che A te pol!", + "Carry the flag for ${ARG1} seconds.": "Tiente ła bandiera par ${ARG1} segondi.", + "Carry the flag for a set length of time.": "Tiente ła bandiera par un serto tenpo.", + "Crush ${ARG1} of your enemies.": "Copa ${ARG1} nemighi.", + "Defeat all enemies.": "Copa tuti i nemighi.", + "Dodge the falling bombs.": "A piove bonbe: schìvełe tute.", + "Final glorious epic slow motion battle to the death.": "Bataja fenałe, èpega e mortałe, in movensa camoma.", + "Gather eggs!": "Rancura sù i ovi!", + "Get the flag to the enemy end zone.": "Roba ła bandiera da ła baze nemiga.", + "How fast can you defeat the ninjas?": "A che vełosidà coparetu tuti i ninja?", + "Kill a set number of enemies to win.": "Copa un serto nùmaro de nemighi par vìnsar.", + "Last one standing wins.": "L'ùltemo a restar in pie el vinse.", + "Last remaining alive wins.": "L'ùltemo a restar vivo el vinse.", + "Last team standing wins.": "L'ùltema scuadra in pie ła vinse.", + "Prevent enemies from reaching the exit.": "Inpedisi che i nemighi i rive so ła sortìa.", + "Reach the enemy flag to score.": "Riva a ła bandiera de i nemighi par far ponto.", + "Return the enemy flag to score.": "Torna co ła bandiera nemiga par far ponto.", + "Run ${ARG1} laps.": "Cori par ${ARG1} jiri.", + "Run ${ARG1} laps. Your entire team has to finish.": "Cori par ${ARG1} jiri. Tuta ła to scuadra ła gà da fenirli.", + "Run 1 lap.": "Cori par 1 jiro.", + "Run 1 lap. Your entire team has to finish.": "Cori par 1 jiro. Tuta ła to scuadra ła gà da fenirlo.", + "Run real fast!": "Cori fà na foina!", + "Score ${ARG1} goals.": "Piasa ${ARG1} gol.", + "Score ${ARG1} touchdowns.": "Piasa ${ARG1} touchdown.", + "Score a goal.": "Piasa un gol.", + "Score a touchdown.": "Piasa un touchdown.", + "Score some goals.": "Piasa racuanti gol.", + "Secure all ${ARG1} flags.": "Proteji tute ${ARG1} łe bandiere.", + "Secure all flags on the map to win.": "Proteji tute łe bandiere de'l łeveło par vìnsar.", + "Secure the flag for ${ARG1} seconds.": "Proteji ła bandiera par ${ARG1} segondi.", + "Secure the flag for a set length of time.": "Proteji ła bandiera par un serto tenpo.", + "Steal the enemy flag ${ARG1} times.": "Roba ${ARG1} 'olte ła bandiera nemiga.", + "Steal the enemy flag.": "Roba ła bandiera nemiga.", + "There can be only one.": "A połe èsarghine soło un.", + "Touch the enemy flag ${ARG1} times.": "Toca ${ARG1} 'olte ła bandiera nemiga.", + "Touch the enemy flag.": "Toca ła bandiera nemiga.", + "carry the flag for ${ARG1} seconds": "tiente ła bandiera par ${ARG1} segondi", + "kill ${ARG1} enemies": "Copa ${ARG1} nemighi", + "last one standing wins": "l'ùltemo a restar in pie el vinse", + "last team standing wins": "l'ùltema scuadra a restar in pie ła vinse", + "return ${ARG1} flags": "torna co ${ARG1} bandiere", + "return 1 flag": "torna co 1 bandiera", + "run ${ARG1} laps": "cori par ${ARG1} jiri", + "run 1 lap": "cori par 1 jiro", + "score ${ARG1} goals": "piasa ${ARG1} gol", + "score ${ARG1} touchdowns": "piasa ${ARG1} touchdown", + "score a goal": "piasa un gol", + "score a touchdown": "piasa un touchdown", + "secure all ${ARG1} flags": "proteji tute ${ARG1} łe bandiere", + "secure the flag for ${ARG1} seconds": "proteji ła bandiera par ${ARG1} segondi", + "touch ${ARG1} flags": "toca ${ARG1} bandiere", + "touch 1 flag": "toca 1 bandiera" + }, + "gameNames": { + "Assault": "Asalto", + "Capture the Flag": "Brinca ła bandiera", + "Chosen One": "L'ełeto", + "Conquest": "Conchista", + "Death Match": "Scontro mortałe", + "Easter Egg Hunt": "Casador de ovi", + "Elimination": "Ełimenasion", + "Football": "Football", + "Hockey": "Hockey", + "Keep Away": "Tien distante", + "King of the Hill": "Re de'l col", + "Meteor Shower": "Scravaso de stełe", + "Ninja Fight": "Petuche ninja", + "Onslaught": "Dezìo", + "Race": "Corsa", + "Runaround": "Mena torno", + "Target Practice": "Pràtega de tiro", + "The Last Stand": "Resta in pie" + }, + "inputDeviceNames": { + "Keyboard": "Botonera", + "Keyboard P2": "Botonera Z2" + }, + "languages": { + "Arabic": "Àrabo", + "Belarussian": "Bełoruso", + "Chinese": "Sineze senplifegà", + "ChineseTraditional": "Sineze tradisionałe", + "Croatian": "Croà", + "Czech": "Ceco", + "Danish": "Daneze", + "Dutch": "Ołandeze", + "English": "Ingleze", + "Esperanto": "Esperanto", + "Finnish": "Fiłandeze", + "French": "Franseze", + "German": "Todesco", + "Gibberish": "Tabascà", + "Greek": "Grego", + "Hindi": "Hindi", + "Hungarian": "Ongareze", + "Indonesian": "Indonezian", + "Italian": "Itałian", + "Japanese": "Japoneze", + "Korean": "Corean", + "Persian": "Persian", + "Polish": "Połaco", + "Portuguese": "Portogheze", + "Romanian": "Romen", + "Russian": "Ruso", + "Serbian": "Serbo", + "Slovak": "Zlovaco", + "Spanish": "Spagnoło", + "Swedish": "Zvedeze", + "Turkish": "Turco", + "Ukrainian": "Ucrain", + "Venetian": "Veneto", + "Vietnamese": "Vietnamita" + }, + "leagueNames": { + "Bronze": "Bronzo", + "Diamond": "Damante", + "Gold": "Oro", + "Silver": "Arzento" + }, + "mapsNames": { + "Big G": "G granda", + "Bridgit": "Pontezeło", + "Courtyard": "Corte", + "Crag Castle": "Tori zemełe", + "Doom Shroom": "Fongo de ła danasion", + "Football Stadium": "Stadio de football", + "Happy Thoughts": "Antigravidà", + "Hockey Stadium": "Stadio de hockey", + "Lake Frigid": "Łago injasà", + "Monkey Face": "Testa de simia", + "Rampage": "Ła ranpa", + "Roundabout": "Tranpołini", + "Step Right Up": "Sù e Zó", + "The Pad": "L'altopian", + "Tip Top": "Fortesa", + "Tower D": "Muraja", + "Zigzag": "Zigzag" + }, + "playlistNames": { + "Just Epic": "Ndemo camomi", + "Just Sports": "Ndemo de sport" + }, + "scoreNames": { + "Flags": "Bandiere", + "Goals": "Gol", + "Score": "Puntejo", + "Survived": "Soravivesto", + "Time": "Tenpo", + "Time Held": "Tenpo tegnùa" + }, + "serverResponses": { + "A code has already been used on this account.": "So 'sto profiło A ze dezà stà doparà un còdaze.", + "A reward has already been given for that address.": "Par 'sto ndariso A ze dezà stà dà na reconpensa.", + "Account linking successful!": "Profiło cołegà co suceso!", + "Account unlinking successful!": "Profiło descołegà co suceso!", + "Accounts are already linked.": "I profiłi i ze dezà cołegài.", + "An error has occurred; (${ERROR})": "A se gà verifegà un eror: (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "A se gà verifegà un eror: contata l'asistensa. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "A se gà verifegà un eror: contata support@froemling.net.", + "An error has occurred; please try again later.": "A se gà verifegà un eror: proa danovo pì tardi.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Vutu dalvero cołegar 'sti profiłi?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\n'Sta asion no ła połe mìa èsar anułada!", + "BombSquad Pro unlocked!": "BombSquad Pro dezblocà!", + "Can't link 2 accounts of this type.": "A no se połe mìa cołegar 2 profiłi de 'sto tipo.", + "Can't link 2 diamond league accounts.": "A no se połe mìa cołegar 2 profiłi de ła łega de damante.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "A no se połe mìa cołegarlo: A ze dezà stà cołegài un màsemo de ${COUNT} profiłi.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Rełevà un inbrojo: punteji e premi sospendesti par ${COUNT} dì.", + "Could not establish a secure connection.": "A no ze mìa posìbiłe stabiłir na conesion segura.", + "Daily maximum reached.": "Màsemo jornałiero pasà.", + "Entering tournament...": "Entrada inte'l tornèo...", + "Invalid code.": "Còdaze mìa vàłido.", + "Invalid payment; purchase canceled.": "Pagamento miga vàłido: cronpa anułada.", + "Invalid promo code.": "Còdaze promosionałe mìa vàłido.", + "Invalid purchase.": "Cronpa mìa vàłida.", + "Invalid tournament entry; score will be ignored.": "Entrada inte'l tornèo mìa vàłida: el puntejo el vegnarà ignorà.", + "Item unlocked!": "Ojeto dezblocà!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "COŁEGAMENTO REFUDÀ. ${ACCOUNT} el contien dati\ninportanti che i ndarà PERDESTI.\nSe te vołi A te połi cołegarli in òrdane raverso\n(e pèrdar a'l só posto i dati de 'STO account cuà).", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Vutu dalvero cołegar l'account ${ACCOUNT} a 'sto account?\nTuti i dati so ${ACCOUNT} i ndarà perdesti.\n'Sta asion no ła połe mìa èsar anułada: situ seguro?", + "Max number of playlists reached.": "Nùmaro màsemo de łiste de scolto pasà.", + "Max number of profiles reached.": "Nùmaro màsemo de profiłi pasà.", + "Maximum friend code rewards reached.": "Brincà el nùmaro màsemo de premi da'l còdaze amigo.", + "Message is too long.": "Mesajo masa łongo.", + "Profile \"${NAME}\" upgraded successfully.": "Profiło \"${NAME}\" mejorà co suceso.", + "Profile could not be upgraded.": "El profiło no'l połe mìa èsar mejorà.", + "Purchase successful!": "Cronpà co suceso!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "A te ghè resevesto ${COUNT} biłieti par ver verto el zugo.\nTorna anca doman par brincàrghine ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Ła funsionałidà de'l server no ła ze mìa pì suportada inte 'sta varsion de'l zugo.\nAjòrneło a ła varsion nova.", + "Sorry, there are no uses remaining on this code.": "Ne despiaze, 'sto còdaze el ze dezà stà doparà a'l màsemo.", + "Sorry, this code has already been used.": "Ne despiaze, 'sto còdaze el ze dezà stà doparà.", + "Sorry, this code has expired.": "Ne despiaze, ła vałidità de 'sto còdaze ła ze terminada.", + "Sorry, this code only works for new accounts.": "Ne despiaze, 'sto còdaze el funsiona soło so i account novi.", + "Temporarily unavailable; please try again later.": "Par deso miga disponìbiłe: proa danovo pì tardi.", + "The tournament ended before you finished.": "El tornèo el ze terminà prima che te ghesi fenìo.", + "This account cannot be unlinked for ${NUM} days.": "'Sto account no'l połe mìa èsar descołegà prima de ${NUM} dì.", + "This code cannot be used on the account that created it.": "'Sto còdaze no'l połe mìa èsar doparà inte l'account che'l ło gà creà.", + "This requires version ${VERSION} or newer.": "A ghe serve ła varsion ${VERSION} o una pì nova.", + "Tournaments disabled due to rooted device.": "Tornèi dezativài parvìa de'l dispozitivo co'l root.", + "Tournaments require ${VERSION} or newer": "Par i tornèi A serve ła varsion ${VERSION} o una pì resente", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Vutu descołegar l'account ${ACCOUNT} da 'sto account?\nTuti i dati de ${ACCOUNT} i vegnarà ełimenài.\n(obietivi a parte in calche cazo)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "AVERTENSA: el to account el ze stà segnałà par el dòparo de truchi.\nI zugaduri catài a doparar truchi i vegnarà blocài. Zuga da gałantomo.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Ghetu caro cołegar l'account de'l to dispozitivo co 'sto cuà?\n\nL'account de'l to dispozitivo el ze ${ACCOUNT1}\n'Sto account el ze ${ACCOUNT2}\n\n'Sta oparasion ła te parmetarà de mantegner i to progresi ezistenti.\nAvertensa: 'sta asion no ła połe mìa èsar anułada!", + "You already own this!": "Dezà cronpà!", + "You can join in ${COUNT} seconds.": "A te połi zontarte tenpo ${COUNT} segondi.", + "You don't have enough tickets for this!": "A no te ghè miga biłieti che basta par cronparlo!", + "You don't own that.": "Gnancora cronpà!", + "You got ${COUNT} tickets!": "A te ghè otegnesto ${COUNT} biłieti!", + "You got a ${ITEM}!": "A te ghè otegnesto un ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Promosion a ła łega suparior: congratułasion!", + "You must update to a newer version of the app to do this.": "Par ndar vanti A te ghè da ajornar l'apl a ła varsion pì resente.", + "You must update to the newest version of the game to do this.": "Par ndar vanti A te ghè da ajornar el zugo a ła varsion pì resente.", + "You must wait a few seconds before entering a new code.": "A te ghè da spetar calche segondo prima de insarir un còdaze novo.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Inte'l tornèo ùltemo A te si rivà #${RANK}. Grasie par ver zugà!", + "Your account was rejected. Are you signed in?": "El to account el ze stà refudà. Ghetu fato l'aceso?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Ła to copia de'l zugo ła ze stada modifegada.\nPar piaser, anuła łe modìfeghe e proa danovo.", + "Your friend code was used by ${ACCOUNT}": "El to còdaze amigo el ze stà doparà da ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 menuto", + "1 Second": "1 segondo", + "10 Minutes": "10 menuti", + "2 Minutes": "2 menuti", + "2 Seconds": "2 segondi", + "20 Minutes": "20 menuti", + "4 Seconds": "4 segondi", + "5 Minutes": "5 menuti", + "8 Seconds": "8 segondi", + "Allow Negative Scores": "Parmeti anca i punteji negadivi", + "Balance Total Lives": "Bałansa el nùmaro totałe de vide", + "Bomb Spawning": "Jenerasion bonbe", + "Chosen One Gets Gloves": "L'ełeto l'otien i guantoni", + "Chosen One Gets Shield": "L'ełeto l'otien el scudo", + "Chosen One Time": "Tenpo de l'ełeto", + "Enable Impact Bombs": "Ativa bonbe a inpato", + "Enable Triple Bombs": "Ativa bonbe triple", + "Entire Team Must Finish": "Tuta ła scuadra ła gà da fenir", + "Epic Mode": "Movensa camoma", + "Flag Idle Return Time": "Tenpo de retorno a bandiera ferma", + "Flag Touch Return Time": "Tenpo de retorno a bandiera tocada", + "Hold Time": "Tenpo de tegnùa", + "Kills to Win Per Player": "Copài par vìnsar par zugador", + "Laps": "Jiri", + "Lives Per Player": "Vide par zugador", + "Long": "Łonga", + "Longer": "Pì łonga", + "Mine Spawning": "Jenerasion mine", + "No Mines": "Sensa mine", + "None": "Gnaun", + "Normal": "Normałe", + "Pro Mode": "Modałidà pro", + "Respawn Times": "Tenpo de rejenerasion", + "Score to Win": "Puntejo par vìnsar", + "Short": "Curta", + "Shorter": "Pì curta", + "Solo Mode": "Modałidà zugador ùgnoło", + "Target Count": "Nùmaro de obietivi", + "Time Limit": "Łìmide de tenpo" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "Ła scuadra ${TEAM} ła ze scuałifegada parché ${PLAYER} l'à mołà.", + "Killing ${NAME} for skipping part of the track!": "Copà ${NAME} par ver saltà un toco de'l parcorso!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Avertensa par ${NAME}: el turbo / spam co i botoni el te gà fato trar fora." + }, + "teamNames": { + "Bad Guys": "Cataràdeghi", + "Blue": "Blè", + "Good Guys": "Ałeài", + "Red": "Rosi" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Co'l justo tenpo de corsa, salto e rodasion, un crogno el połe copar inte un\ncolpo soło e farte vadagnar el respeto de i to amighi par tuta ła vida.", + "Always remember to floss.": "Recòrdate senpre de doparar el fiło intardentałe.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Invese de doparàrghine de fati a cazo, par ti e i to\namighi crea profiłi zugador co nomi e parense parsonałizàe.", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Łe case małedision łe te transforma inte na bonba a tenpo.\nA te połi curarte soło tołendo in presa un pacheto sałude.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Anca se drio ła siera A no par, łe abiłidà de tuti i parsonaji\nłe ze conpagne, donca tote sù cheło che'l te połe somejar de pì.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Mai far masa i gałeti co'l scudo nerjètego: A te połi uncora farte trar baso da na croda.", + "Don't run all the time. Really. You will fall off cliffs.": "No stà córar tuto el tenpo. Dalvero. A te fenirè zó par na croda.", + "Don't spin for too long; you'll become dizzy and fall.": "No stà ndar torno par masa tenpo: te vegnarà na storniroła e te cascarè.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Tien strucà un boton calsìase par córar. (I griłeti nałòzeghi i ze i pì adati, se A te łi ghè)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Tien strucà un boton calsìase par córar. A te te movarè pì in\npresa ma no te voltarè miga masa ben, donca ocio a łe crode.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Łe bonbe jaso no łe ze miga tanto potenti, ma łe injasa tuto cheło\nche łe brinca, e cheło che'l ze injasà el ze anca fàsiłe da fracasar.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Se calchedun el te łeva sù, daghe un crogno che'l te\nmołe zó. 'Sta roba ła funsiona anca inte ła vida vera.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Se A te si a curto de controładori, instała l'apl '${REMOTE_APP_NAME}'\nso i to dispozitivi mòbiłi e dopàrełi cofà controładori.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Se na bonba petaisa ła te se taca doso, salta in volta e và torno: ła gavarìa da cavarse.\nE se no funsionase, i to momenti ùltemi de vida i gavarà almanco dà spetàgoło!", + "If you kill an enemy in one hit you get double points for it.": "Se A te copi un nemigo co un colpo soło A te ciapi el dupio de i punti.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Se A te ciapi na małedision, A te ghè soło na sparansa de salvesa:\ncatar fora un pacheto sałude inte i puchi segondi che A te resta.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Se A se stà fermi inte un posto, A se ze friti. Cori e schiva par soravìvar...", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Se A te te cati tanti zugadori che i và e vien, e inte'l cazo che calchedun el se dezménteghe de\nmołar el zugo, ativa ła funsion automàtega 'Para vìa zugadori in sonera' inte łe inpostasion.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Se'l to dispozitivo el taca scotar o se A te ghè caro sparagnar batarìa,\ncała ła \"Prospetiva\" o ła \"Resołusion\" so Inpostasion > Gràfega", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Se el zugo el và a scati, proa całar ła resołusion\no ła prospetiva inte l'inpostasion gràfega.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "Par far ponto so 'Brinca ła bandiera', A te ghè da portar ła bandiera fin so ła to\nbaze. Se cheł'altra scuadra ła ze drio far ponto, A te połi fermarla anca robàndogheła.", + "In hockey, you'll maintain more speed if you turn gradually.": "Inte l'hockey, voltando gradualmente A te mantien alta ła vełosidà.", + "It's easier to win with a friend or two helping.": "A ze pì fàsiłe vìnsar se un amigo o do i te dà na man.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Co A te si drio tirar na bonba, salta par farla rivar pì alta posìbiłe.", + "Land-mines are a good way to stop speedy enemies.": "Łe mine łe ze fantàsteghe par fermar i nemighi pì ràpidi.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "A te połi łevar sù e tirar racuante robe, anca cheł'altri zugadori. Trar zó da na croda\ni to nemighi ła połe èsar na stratejìa efisente che ła połe cavarte anca calche spisa.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nò, A no te połi mìa montar so'l bordo. A te ghè da tirar bonbe.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "I zugadori i połe zontarse e ndar vìa inte'l medo de ła majornasa de łe\npartìe. Ti A te połi anca tacar e destacar un controłador a'l voło.", + "Practice using your momentum to throw bombs more accurately.": "Fà pràtega tirardo bonbe corendo par far crèsar ła to presizion.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Pì vełosi che se move łe to man, pì dano i farà i to\ncrogni! Donca proa córar, saltar e ndar torno cofà un mato.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Prima de tirar na bonba cori prima indrìo e daspò in\nvanti par darghe na \"scuriatada\" e trarla pì distante.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Fà fora un grupo intiero de nemighi\npiasando na bonba visin a na TNT.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Ła testa ła ze el posto pì vulneràbiłe, donca na bonba\npetaisa inte ła suca de sòłito ła vołe dir fine de i zughi.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "'Sto łeveło no'l fenise mai, ma un puntejo alto cuà\nel vołe dir el respeto eterno de tuto el mondo.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Ła potensa de tiro ła ze bazada so ła diresion che A te si drio strucar. Par\nmołar zó calcosa davanti de ti dolsemente, no stà tegner strucà gnauna diresion.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Stufo de ła mùzega de'l zugo? Meti sù ła tua!\nVarda so Inpostasion > Àudio > Son de fondo", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Prima de tirarle, proa 'łasar so'l fogo' łe bonbe par un segondo o do.", + "Try tricking enemies into killing eachother or running off cliffs.": "Frega i to nemighi parché i se cope infrà de łori o i cora zó par na croda.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Dòpara el boton 'łeva sù' par brincar ła bandiera < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Cori prima indrìo e daspò in vanti par rivar pì distante co i to tiri...", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "A te połi 'mirar' i to crogni ndando torno par drita o sanca. A torna\ncòmodo par trar baso i cataràdeghi da łe crode o par far ponto so l'hockey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "A te połi capir el tenpo de esplozion de na bonba drio el cołor\nde łe fałive de ła micia: zało.. naranson.. roso.. e BUM!!", + "You can throw bombs higher if you jump just before throwing.": "A te połi tirar łe bonbe pì alte, se te salti pena prima de tirarle.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Se A te bati ła testa da calche parte A te te fè małe,\ndonca proa a no zbàtar da gnauna parte.", + "Your punches do much more damage if you are running or spinning.": "I to crogni i fà pì małe se A te si drio córar o ndar torno." + } + }, + "trophiesRequiredText": "Par zugarghe A serve almanco ${NUMBER} trofèi.", + "trophiesText": "Trofèi", + "trophiesThisSeasonText": "Trofèi de 'sta stajon", + "tutorial": { + "cpuBenchmarkText": "El fà ndar ła demostrasion a vełosidà redìgoła (par proar prinsipalmente ła rapidità de'l procesor)", + "phrase01Text": "Ciao!", + "phrase02Text": "Benrivài so ${APP_NAME}!", + "phrase03Text": "Eco calche sujarimento so come far móvar el to parsonajo:", + "phrase04Text": "Rento ${APP_NAME} tante robe łe ze bazàe so ła FÌZEGA.", + "phrase05Text": "Par dir, co te tiri un crogno...", + "phrase06Text": "...el dano el ze bazà so ła vełosidà de i to pugni.", + "phrase07Text": "Visto? A te si fermo, donca ${NAME} el ło gà sentìo a cico.", + "phrase08Text": "Deso salta e và torno par ciapar pì vełosidà.", + "phrase09Text": "Eco, cusì A ze mejo.", + "phrase10Text": "Anca córar juta.", + "phrase11Text": "Tien strucà un boton CALSÌASE par córar.", + "phrase12Text": "Par crogni uncora pì fisi, proa córar e ndar torno INSENBRE.", + "phrase13Text": "Orpo! Colpa mia ${NAME}.", + "phrase14Text": "A te połi łevar sù e tirar robe cofà łe bandiere o anca cofà... ${NAME}.", + "phrase15Text": "In ùltema, A ghe ze łe bonbe.", + "phrase16Text": "A serve pràtega par tirar ben łe bonbe.", + "phrase17Text": "Òstrega! Un tiro miga masa beło...", + "phrase18Text": "Móvarte el te juta a tirarle pì distante.", + "phrase19Text": "Móvarte el te juta a tirarle pì alte.", + "phrase20Text": "\"Scuriata\" łe bonbe par tiri uncora pì łonghi.", + "phrase21Text": "Calcołar el tenpo de esplozion de łe bonbe A połe èsar defìsiłe.", + "phrase22Text": "Canaja!", + "phrase23Text": "Proa 'łasar so'l fogo' ła micia par un segondo o do.", + "phrase24Text": "Grande! Cotura spesiałe!", + "phrase25Text": "Benon, A ghemo fenìo.", + "phrase26Text": "Deso và e dàghine na rata a tuti!", + "phrase27Text": "Recòrdate el to łenamento... e A te tornarè caza tut'un toco!", + "phrase28Text": "...beh, forse...", + "phrase29Text": "Bona fortuna!", + "randomName1Text": "Fede", + "randomName2Text": "Enry", + "randomName3Text": "Teo", + "randomName4Text": "Cesco", + "randomName5Text": "Stè", + "skipConfirmText": "Vutu dalvero saltar ła demostrasion? Toca o struca par confermar.", + "skipVoteCountText": "Voti par saltar ła demostrasion: ${COUNT}/${TOTAL}", + "skippingText": "demostrasion saltada...", + "toSkipPressAnythingText": "(toca o struca un boton par saltar ła demostrasion)" + }, + "twoKillText": "E 2 DE COPÀI!", + "unavailableText": "miga disponìbiłe", + "unconfiguredControllerDetectedText": "Rełevà controłador miga configurà:", + "unlockThisInTheStoreText": "Da dezblocar inte ła botega.", + "unlockThisProfilesText": "Par crear pì de ${NUM} profiłi, A te serve:", + "unlockThisText": "Par dezblocar 'sta funsion A te serve:", + "unsupportedHardwareText": "Ne despiaze, 'sto hardware no'l ze mìa conpatìbiłe co 'sta varsion de'l zugo.", + "upFirstText": "Par tacar:", + "upNextText": "Inte'l łeveło n°${COUNT}:", + "updatingAccountText": "Ajornamento de'l to account...", + "upgradeText": "Mejora", + "upgradeToPlayText": "Par zugarghe, dezbloca \"${PRO}\" inte ła botega.", + "useDefaultText": "Reinposta", + "usesExternalControllerText": "'Sto zugo el dòpara un controłador esterno cofà dispozitivo de entrada.", + "usingItunesText": "Doparar l'apl de mùzega par el son de fondo...", + "validatingTestBuildText": "Confermasion varsion de proa...", + "victoryText": "Vitoria!", + "voteDelayText": "A no te połi tacar n'antra votasion par ${NUMBER} segondi", + "voteInProgressText": "A ze dezà in corso na votasion.", + "votedAlreadyText": "A te ghè zà votà", + "votesNeededText": "A serve ${NUMBER} voti", + "vsText": "vs.", + "waitingForHostText": "(drio spetar ${HOST} par ndar vanti)", + "waitingForPlayersText": "drio spetar che A se zonte altri zugadori...", + "waitingInLineText": "Drio spetar in coa (el grupo el ze pien)...", + "watchAVideoText": "Varda un video", + "watchAnAdText": "Varda na reclan", + "watchWindow": { + "deleteConfirmText": "Vutu ełimenar \"${REPLAY}\"?", + "deleteReplayButtonText": "Ełìmena\nrevardo", + "myReplaysText": "I me revardi", + "noReplaySelectedErrorText": "Gnaun revardo sełesionà", + "playbackSpeedText": "Vełosidà de reprodusion: ${SPEED}", + "renameReplayButtonText": "Renòmena\nrevardo", + "renameReplayText": "Renòmena \"${REPLAY}\" co:", + "renameText": "Renòmena", + "replayDeleteErrorText": "A se gà verifegà un eror ełimenando el revardo.", + "replayNameText": "Nome de'l revardo", + "replayRenameErrorAlreadyExistsText": "A eziste dezà un revardo co 'sto nome.", + "replayRenameErrorInvalidName": "A no ze mìa posìbiłe renomenar el revardo: nome mìa vàłido.", + "replayRenameErrorText": "A se gà verifegà un eror renomenando el revardo.", + "sharedReplaysText": "Revardi sparpagnài", + "titleText": "Varda", + "watchReplayButtonText": "Varda\nrevardo" + }, + "waveText": "Ondada", + "wellSureText": "Ciaro!", + "wiimoteLicenseWindow": { + "titleText": "Deriti d'autor DarwiinRemote" + }, + "wiimoteListenWindow": { + "listeningText": "Reserca de controładori Wiimotes...", + "pressText": "Struca simultaneamente i botoni 1 e 2 de'l Wiimote.", + "pressText2": "Invese, so i Wiimotes novi co'l Motion Plus integrà, struca el boton roso 'sync' che A ghe ze dadrìo." + }, + "wiimoteSetupWindow": { + "copyrightText": "Deriti d'autor DarwiinRemote", + "listenText": "Scolta", + "macInstructionsText": "Segùrate che ła to Wii ła sipie stuada e el bluetooth de'l\nto Mac ativà, donca stuca so 'Scolta'. Ła conpatibiłidà co'l\nWiimote ła podarìa èsar un fià inserta, donca te podarisi\ncatarte a proar pì 'olte prima de otegner na conesion.\n\nEl bluetooth el gavarìa da suportar fin a 7 dispozitivi conetesti,\nanca se ła capasidà ła podarìa variar.\n\nBombSquad el suporta el controłador Clàsego e i controładori\norijinałi Wiimotes, Nunchuks.\nAnca el novo Wii Remote Plus el funsiona\nma soło sensa acesori.", + "thanksText": "Grasie a ła scuadra DarwiinRemote\npar verlo rendesto posìbiłe.", + "titleText": "Configurasion Wiimote" + }, + "winsPlayerText": "Vitoria de ${NAME}!", + "winsTeamText": "Vitoria de i ${NAME}!", + "winsText": "Vitoria de ${NAME}!", + "worldScoresUnavailableText": "Punteji mondiałi miga disponìbiłi.", + "worldsBestScoresText": "I punteji mejo de'l mondo", + "worldsBestTimesText": "I tenpi mejo de'l mondo", + "xbox360ControllersWindow": { + "getDriverText": "Descarga el driver", + "macInstructions2Text": "Par doparar sensa fiło i controładori, A te serve anca el resevidor\nche'l riva co l''Xbox 360 Wireless Controller par Windows'.\nUn resevidor el te parmete de conétar fin a 4 controładori.\n\nInportante: i resevidori de terse parti no i funsionarà mìa co 'sto driver;\nsegùrate che'l to resevidor el sipia 'Microsoft' e miga 'XBOX 360'.\nŁa Microsoft no łi vende pì destacài, donca A te servirà par forsa cheło\nvendesto insebre co'l controłador, o senò, proa sercar so Ebay.\n\nSe A te cati ùtiłe el driver, ciapa in considerasion de farghe na\ndonasion a'l só dezviłupador so 'sto sito.", + "macInstructionsText": "Per doparar i controładori co'l fiło de ła Xbox 360, A te ghè\nda instałar el driver Mac disponìbiłe so'l link cuà soto.\nEl funsiona co anbo i controładori, co'l fiło o sensa.", + "ouyaInstructionsText": "Par doparar so Bombsquad un controłador de l'Xbox 360 co'l fiło,\ntàcheło sù inte ła porta USB de’l to dispozitivo. A te połi anca\ntacar sù pì controładori insenbre doparando un hub USB.\n\nPar doparar i controładori sensa fiło invese, A te serve un resevidor\nde segnałe. A te połi catarlo, o rento ła scàtoła \"Controładori sensa fiło\nXbox 360 par Windows\", o vendesto a parte. Caun resevidor el và tacà so\nna porta USB e el te parmete de conétar fin a 4 controładori.", + "titleText": "Doparar un controłador Xbox 360 co ${APP_NAME}:" + }, + "yesAllowText": "Sì, parmeti!", + "yourBestScoresText": "I to punteji mejo", + "yourBestTimesText": "I to tenpi mejo" +} \ No newline at end of file diff --git a/dist/ba_data/data/languages/vietnamese.json b/dist/ba_data/data/languages/vietnamese.json new file mode 100644 index 0000000..a315c45 --- /dev/null +++ b/dist/ba_data/data/languages/vietnamese.json @@ -0,0 +1,1820 @@ +{ + "accountSettingsWindow": { + "accountNameRules": "Tên tài khoản không được chứa biểu tượng cảm xúc và kí tự đặc biệt", + "accountProfileText": "Tài Khoản", + "accountsText": "Tài khoản", + "achievementProgressText": "Huy Hiệu:${COUNT} trong ${TOTAL}", + "campaignProgressText": "Tiến trình(khó) :${PROGRESS}", + "changeOncePerSeason": "Bạn chỉ có thể thay đổi một lần mỗi mùa.", + "changeOncePerSeasonError": "Bạn có thể đổi nó vào mùa sau (${NUM} days)", + "customName": "Tên tùy chỉnh", + "linkAccountsEnterCodeText": "Nhập Mã", + "linkAccountsGenerateCodeText": "Tạo mã", + "linkAccountsInfoText": "(Chia sẽ dữ liệu giữa các máy)", + "linkAccountsInstructionsNewText": "Để kết nối hai tài khoản, tạo mã trên tài khoản thứ\nnhất rồi nhập vào tài khoản thứ hai. Dữ liệu trên tài\nkhoản thứ hai sẽ được đồng bộ.\n(Dữ liệu trên tài khoản thứ nhất sẽ bị xóa vĩnh viễn)\n\nBạn có thể kết nối lên đến ${COUNT} tài khoản.\n\nLƯU Ý: chỉ kết nối tài khoản của bạn;\nNếu bạn kết nối tài khoản của người khác thì\ncả hai người sẽ không thể trực tuyến cùng một lúc.", + "linkAccountsInstructionsText": "Để kết nối 2 tài khoản khác nhau, tạo mã ở\nmột máy và nhập mã ở máy còn lại.\nDữ liệu sẽ được liên kết giữa các máy\nBạn có thể kết nối đến ${COUNT} thiết bị.\n\nQUAN TRỌNG: Chỉ kết nối tài khoản của bạn!\nNếu bạn kết nối tài khoản với bạn bè\nbạn không thể chơi cùng một lúc!\n\nNgoài ra: nó không thể hoàn tác, nên hãy cẩn thận!", + "linkAccountsText": "kết nối máy khác", + "linkedAccountsText": "Tài khoản kết nối:", + "nameChangeConfirm": "Đổi tên tài khoản thành ${NAME}?", + "resetProgressConfirmNoAchievementsText": "Việc này sẽ đặt lại toàn bộ dữ liệu của bạn\nngoại trừ số tiền của ban\nđiều này không thể hoàn tác. Chắc chắn?", + "resetProgressConfirmText": "Việc này sẽ đặt lại toàn bộ dữ liệu của bạn\nvà cả huy hiệu\nngoại trừ số tiền của ban\nđiều này không thể hoàn tác. Chắc chắn?", + "resetProgressText": "Xóa dữ liệu", + "setAccountName": "Đặt lại tên tài khoản", + "setAccountNameDesc": "Chọn tên cho tài khoản của bạn.\nBạn có thể dùng tên từ tài khoản đã kết nối\nhoặc tạo tên khác.", + "signInInfoText": "Đăng nhập để lưu dữ liệu giữa các máy \nchơi online và tham gia giải đấu", + "signInText": "Đăng Nhập", + "signInWithDeviceInfoText": "(một tài khoản ở máy khác sẽ tự động đăng nhập)", + "signInWithDeviceText": "đăng nhập bằng tài khoản của máy tính", + "signInWithGameCircleText": "Đăng nhập với Game Circle", + "signInWithGooglePlayText": "đăng nhập bằng google chơi trò chơi", + "signInWithTestAccountInfoText": "(loại tài khoản đặc biệt; chỉ đăng nhập trên máy này)", + "signInWithTestAccountText": "Đăng nhập bằng tài khoản máy tính", + "signOutText": "Đăng Xuất", + "signingInText": "Đang đăng nhập...", + "signingOutText": "Đang Đăng xuất...", + "testAccountWarningCardboardText": "Cảnh Báo: bạn đang Đang Chơi tài Khoản 'Thử'\ndữ liệu sẽ bị thay thế bởi tài khoản\nvà nó sẽ đè lên TK thử\n\nBây giờ bạn sẽ có tất cả tiền trong Game\n(Bạn hãy chơi Boomsquad Pro miẽn phí", + "testAccountWarningOculusText": "Cảnh báo:bạn đang đăng nhập với kí tự chữ.Chúng sẽ được thay thế bằng kí tự ô.\nTài khoản này sẽ được dùng để mua vé và các tính năng khác.\n\n\nVà bây giờ bạn sẽ được học cách thu thập các vé trong game.\n(Mách nhỏ bạn có thể cập nhật BombSquad Pro miễn phí bằng vé)", + "testAccountWarningText": "Cảnh báo:bạn đang đăng nhập bằng tài khoản thử nghiệm.\nTài khoản này sẽ xác thưc thiết bị của bạn và sẽ xóa mất dữ liệu.\n(vì vậy chúng tôi khuyên bạn đừng bỏ nhiều thời gian thu thập \nhoặc mở các vật phẩm trong game.\n\nHãy dùng phiên bản bày bán để có tài khoản thực(vd:Game-Center,Google Plus,...)\nViệc này sẽ giúp bạn lưu trữ các quá trình \nkhi chơi bằng dữ liệu đám mây \nvà bạn có thể chơi trên các thiết bị khác.", + "ticketsText": "Số Tiền: ${COUNT}", + "titleText": "Tài Khoản", + "unlinkAccountsInstructionsText": "Chọn tài khoản để hủy kết nối", + "unlinkAccountsText": "Hủy kết nối", + "viaAccount": "(qua tài khoản ${NAME})", + "youAreSignedInAsText": "Bạn đang đăng nhập tài khoản:" + }, + "achievementChallengesText": "Các thành tựu đạt được.", + "achievementText": "Thành tựu", + "achievements": { + "Boom Goes the Dynamite": { + "description": "Tiêu diệt 3 kẻ xấu bằng TNT", + "descriptionComplete": "Giết 3 kẻ xấu với TNT", + "descriptionFull": "Tiêu diệt 3 kẻ xấu bằng TNT với ${LEVEL}", + "descriptionFullComplete": "Tiêu diệt 3 kẻ xấu bằng TNT với ${LEVEL}", + "name": "Bom tạo ra sức mạnh" + }, + "Boxer": { + "description": "Chiến thắng mà không sử dụng bom", + "descriptionComplete": "Đã chiến thắng mà không sử dụng bom", + "descriptionFull": "Hoàn thành ${LEVEL} mà không sử dụng bom", + "descriptionFullComplete": "Hoàn thành ${LEVEL} mà không sử dụng bom", + "name": "Tay đấm xuất sắc" + }, + "Dual Wielding": { + "descriptionFull": "Kết nối 2 điều khiển (thiết bị hoặc ứng dụng)", + "descriptionFullComplete": "Kết nối 2 điều khiển (thiết bị hoặc ứng dụng)", + "name": "Gấp đôi điều khiển" + }, + "Flawless Victory": { + "description": "Chiến thắng mà không đánh đối thủ", + "descriptionComplete": "Đã chiến thắng mà không đánh đối thủ", + "descriptionFull": "Nhận được ${LEVEL} mà không đánh đối thủ", + "descriptionFullComplete": "Đã thắng ${LEVEL} mà không đánh đối thủ", + "name": "Chiến thắng vinh quang" + }, + "Free Loader": { + "descriptionFull": "Chơi Đánh đơn với 2+ người chơi", + "descriptionFullComplete": "Chơi Đánh đơn với 2+ người chơi", + "name": "Chơi Vô Hạn" + }, + "Gold Miner": { + "description": "Tiêu diệt 6 kẻ xấu bằng mìn", + "descriptionComplete": "Đã tiêu diệt 6 kẻ xấu bằng mìn", + "descriptionFull": "Tiêu diệt 6 kẻ xấu bằng mìn trên ${LEVEL}", + "descriptionFullComplete": "Đã tiêu diệt 6 kẻ xấu bằng mìn trên ${LEVEL}", + "name": "Người đào vàng" + }, + "Got the Moves": { + "description": "Chiến thắng mà không đấm đối thủ hoặc sử dụng bom", + "descriptionComplete": "Đã thắng mà không đấm đối thủ hoặc sử dụng bom", + "descriptionFull": "Nhận được ${LEVEL} mà không đấm đối thủ hoặc sử dụng bom", + "descriptionFullComplete": "Đã thắng ${LEVEL} mà không đắm đối thủ hoặc không sử đụng bom", + "name": "Hãy di chuyển" + }, + "In Control": { + "descriptionFull": "Kết nối với điều khiển (thiết bị hoặc ứng dụng)", + "descriptionFullComplete": "Kết nối với điều khiển (thiết bị hoặc ứng dụng)", + "name": "Trong điều khiển" + }, + "Last Stand God": { + "description": "Ghi 1000 điểm", + "descriptionComplete": "Đã ghi được 1000 điểm", + "descriptionFull": "Ghi 1000 điểm trên ${LEVEL}", + "descriptionFullComplete": "Đã ghi 1000 điểm trên ${LEVEL}", + "name": "${LEVEL} Thánh" + }, + "Last Stand Master": { + "description": "Ghi 250 điểm", + "descriptionComplete": "Đạt 250 điểm", + "descriptionFull": "Điểm 250 trong vòng ${LEVEL}", + "descriptionFullComplete": "Đạt 250 điểm trong vòng ${LEVEL}", + "name": "Sư phụ của vòng ${LEVEL}" + }, + "Last Stand Wizard": { + "description": "Điểm đạt 500", + "descriptionComplete": "Đạt 500 điểm", + "descriptionFull": "Đạt 500 điểm tại ${LEVEL}", + "descriptionFullComplete": "Đạt 500 điểm tại ${LEVEL}", + "name": "${LEVEL} Wizard" + }, + "Mine Games": { + "description": "Giết 3 kẻ xấu vứi land-mines", + "descriptionComplete": "Giết 3 kẻ xấu với land-mines", + "descriptionFull": "Giết 3 kẻ xấu với land-mines tại ${LEVEL}", + "descriptionFullComplete": "Giết 3 kẻ xấu với land-mines tại ${LEVEL}", + "name": "Trò chơi Mine" + }, + "Off You Go Then": { + "description": "Ném 3 đối thủ khỏi bản đồ", + "descriptionComplete": "Ném 3 kẻ xấu ra khỏi bản đồ", + "descriptionFull": "Ném 3 kẻ xấu ra khỏi bản đồ tại ${LEVEL}", + "descriptionFullComplete": "Ném 3 kẻ xấu ra khỏi bản đồ tại ${LEVEL}", + "name": "Đến Ai Tiếp Theo" + }, + "Onslaught God": { + "description": "Đạt 5000 điểm", + "descriptionComplete": "Đạt 5000 điểm", + "descriptionFull": "Đạt 5000 điểm tại ${LEVEL}", + "descriptionFullComplete": "Đạt 5000 điểm tại ${LEVEL}", + "name": "${LEVEL} thánh" + }, + "Onslaught Master": { + "description": "Đạt 500 điểm", + "descriptionComplete": "Đạt 500 điểm", + "descriptionFull": "Đạt 500 điểm tại ${LEVEL}", + "descriptionFullComplete": "Đạt 500 điểm tại ${LEVEL}", + "name": "${LEVEL} Sư phụ" + }, + "Onslaught Training Victory": { + "description": "Thắng tất cả các ải", + "descriptionComplete": "Thắng tất cả các ải", + "descriptionFull": "Thắng tất cả các ải tại ${LEVEL}", + "descriptionFullComplete": "Thắng tất cả các ải tại ${LEVEL}", + "name": "${LEVEL} Chiến thắng" + }, + "Onslaught Wizard": { + "description": "Đạt 1000 điểm", + "descriptionComplete": "Đạt 1000 điểm", + "descriptionFull": "Đạt 1000 điểm tại ${LEVEL}", + "descriptionFullComplete": "Đạt 1000 điểm tại ${LEVEL}", + "name": "${LEVEL} phù thủy" + }, + "Precision Bombing": { + "description": "Thắng mà không cần powerups", + "descriptionComplete": "Thắng mà không cần powerups", + "descriptionFull": "Thắng tại ${LEVEL} mà không cần powerups", + "descriptionFullComplete": "Hoàn thành ${LEVEL} mà không sử dụng Power-ups", + "name": "Tự Tin Ném Bom" + }, + "Pro Boxer": { + "description": "Thắng mà không sử dụng bom", + "descriptionComplete": "Thắng mà không sử dụng bom", + "descriptionFull": "Hoàn thành ${LEVEL} không sử dụng bom", + "descriptionFullComplete": "Hoàn thành ${LEVEL} không sử dụng bom", + "name": "Đấm Bốc Chuyên Nhiệp" + }, + "Pro Football Shutout": { + "description": "Thắng mà không để đội bên ghi điểm", + "descriptionComplete": "Thắng mà không để đội bên ghi điểm", + "descriptionFull": "Hoàn thành ${LEVEL} mà không để đội bên ghi điểm", + "descriptionFullComplete": "Hoàn thành ${LEVEL} mà không để đội bên ghi điểm", + "name": "Xuất Sắc ${LEVEL}" + }, + "Pro Football Victory": { + "description": "Hoàn thành vòng", + "descriptionComplete": "Hoàn thành vòng", + "descriptionFull": "Hoàn thành vòng ${LEVEL}", + "descriptionFullComplete": "Hoàn thành vòng ${LEVEL}", + "name": "Chiến thắng ${LEVEL}" + }, + "Pro Onslaught Victory": { + "description": "Đánh bại tất cả", + "descriptionComplete": "Đánh bại tất cả", + "descriptionFull": "Đánh bại tất cả ở ${LEVEL}", + "descriptionFullComplete": "Đánh bại tất cả ở ${LEVEL}", + "name": "Chiến thang ${LEVEL}" + }, + "Pro Runaround Victory": { + "description": "Hoàn thành tất cả các vòng", + "descriptionComplete": "Đã hoàn thành tất cả các vòng", + "descriptionFull": "Hoàn thành tất cả các vòng tại ${LEVEL}", + "descriptionFullComplete": "Đã hoàn thành tất cả các vòng tại ${LEVEL}", + "name": "${LEVEL} Chiến thắng" + }, + "Rookie Football Shutout": { + "description": "Thắng mà không để đối phương ghi bàn nào", + "descriptionComplete": "Chiến thắng mà không để kẻ xấu ghi điểm", + "descriptionFull": "Chiến thắng ${LEVEL} mà không để kẻ xấu ghi điểm", + "descriptionFullComplete": "Chiến thắng ${LEVEL} mà không để kẻ xấu ghi điểm", + "name": "${LEVEL} Bắn ra" + }, + "Rookie Football Victory": { + "description": "Hoàn Thành Trò Chơi", + "descriptionComplete": "Hoàn Thành Trò Chơi", + "descriptionFull": "Hoàn Thành Trò Chơi ở ${LEVEL}", + "descriptionFullComplete": "Hoàn Thành Trò Chơi ở ${LEVEL}", + "name": "Chiến Thắng ${LEVEL}" + }, + "Rookie Onslaught Victory": { + "description": "Vượt qua tất cả vòng", + "descriptionComplete": "Vượt qua tất cả vòng", + "descriptionFull": "Vượt qua tất cả vòng ở ${LEVEL}", + "descriptionFullComplete": "Vượt qua tất cả vòng ở ${LEVEL}", + "name": "${LEVEL} Chiến thắng" + }, + "Runaround God": { + "description": "Đạt 2000 điểm", + "descriptionComplete": "Đạt 2000 điểm", + "descriptionFull": "Đạt 2000 điểm tại ${LEVEL}", + "descriptionFullComplete": "Đạt 2000 điểm tại ${LEVEL}", + "name": "${LEVEL} Thánh" + }, + "Runaround Master": { + "description": "Đạt 500 điểm", + "descriptionComplete": "Đạt 500 điểm", + "descriptionFull": "Đạt 500 điểm tại ${LEVEL}", + "descriptionFullComplete": "Đạt 500 điểm tại ${LEVEL}", + "name": "${LEVEL} Sư phụ" + }, + "Runaround Wizard": { + "description": "Đạt 1000 điểm", + "descriptionComplete": "Đạt 1000 điểm", + "descriptionFull": "Đạt 1000 điểm tại ${LEVEL}", + "descriptionFullComplete": "Đạt 1000 điểm tại ${LEVEL}", + "name": "${LEVEL} Phù thủy" + }, + "Sharing is Caring": { + "descriptionFull": "Chia sẻ thành công trò chơi này với bạn bè", + "descriptionFullComplete": "Đã chia sẻ trò chơi này với bạn bè", + "name": "Chia sẻ là Quan tâm" + }, + "Stayin' Alive": { + "description": "Thắng mà không chết", + "descriptionComplete": "Thắng mà không chết", + "descriptionFull": "Thắng ${LEVEL} mà không chết", + "descriptionFullComplete": "Đã thắng ${LEVEL} mà không chết", + "name": "Sống sót" + }, + "Super Mega Punch": { + "description": "Gây sát thương 100% với một cú đấm", + "descriptionComplete": "Gây sát thương 100% với một cú đấm", + "descriptionFull": "Gây sát thương 100% với một cú đấm trong ${LEVEL}", + "descriptionFullComplete": "Gây sát thương 100% với một cú đấm trong ${LEVEL}", + "name": "Cú đấm siêu hạng" + }, + "Super Punch": { + "description": "Gây sát thương 50% với một cú đấm", + "descriptionComplete": "Gây sát thương 50% với một cú đấm", + "descriptionFull": "Gây sát thương 50% với một cú đấm trong ${LEVEL}", + "descriptionFullComplete": "Gây sát thương 50% với một cú đấm trong ${LEVEL}", + "name": "Siêu đấm" + }, + "TNT Terror": { + "description": "Giết 6 kẻ địch với TNT", + "descriptionComplete": "Giết 6 kẻ địch với TNT", + "descriptionFull": "Giết 6 kẻ địch với TNT trong ${LEVEL}", + "descriptionFullComplete": "Giết 6 kẻ địch với TNT trong ${LEVEL}", + "name": "Nỗi khiếp sợ TNT" + }, + "Team Player": { + "descriptionFull": "Chơi cùng với 4+ người khác", + "descriptionFullComplete": "Chơi cùng với 4+ người khác", + "name": "Đồng đội" + }, + "The Great Wall": { + "description": "Chặn toàn bộ kẻ địch", + "descriptionComplete": "Chặn toàn bộ kẻ địch", + "descriptionFull": "Chặn toàn bộ kẻ địch trong ${LEVEL}", + "descriptionFullComplete": "Chặn toàn bộ kẻ địch trong ${LEVEL}", + "name": "Bức tường vĩ đại" + }, + "The Wall": { + "description": "Chặn tất cả kẻ địch", + "descriptionComplete": "Chặn tất cả kẻ địch", + "descriptionFull": "Chặn tất cả kẻ địch trong ${LEVEL}", + "descriptionFullComplete": "Chặn tất cả kẻ địch trong ${LEVEL}", + "name": "Bức tường" + }, + "Uber Football Shutout": { + "description": "Chiến thấng mà không cho kẻ địch ghi điểm", + "descriptionComplete": "Chiến thấng mà không cho kẻ địch ghi điểm", + "descriptionFull": "Chiến thấng ${LEVEL} mà không cho kẻ địch ghi điểm", + "descriptionFullComplete": "Chiến thấng ${LEVEL} mà không cho kẻ địch ghi điểm", + "name": "${LEVEL} Áp đảo" + }, + "Uber Football Victory": { + "description": "Chiến thắng", + "descriptionComplete": "Chiến thắng", + "descriptionFull": "Chiến thắng trong ${LEVEL}", + "descriptionFullComplete": "Chiến thấng trong ${LEVEL}", + "name": "${LEVEL} Chiến thắng" + }, + "Uber Onslaught Victory": { + "description": "Thắng tất cả các vòng", + "descriptionComplete": "Thấng tất cả các ải", + "descriptionFull": "Thấng tất cả các ải trong ${LEVEL}", + "descriptionFullComplete": "Thấng tất cả các ải trong ${LEVEL}", + "name": "${LEVEL} Chiến thấng" + }, + "Uber Runaround Victory": { + "description": "Thấng tất cả các ải", + "descriptionComplete": "Thấng tất cả các ải", + "descriptionFull": "Thấng tất cả các ải trong ${LEVEL}", + "descriptionFullComplete": "Thấng tất cả các ải trong ${LEVEL}", + "name": "${LEVEL} Chiến thắng" + } + }, + "achievementsRemainingText": "Các thành tựu tiếp theo:", + "achievementsText": "Các thành tựu", + "achievementsUnavailableForOldSeasonsText": "Xin lỗi , huy hiệu này không có ở mùa trước", + "addGameWindow": { + "getMoreGamesText": "Thêm các thể loại chơi", + "titleText": "Thêm trận đấu" + }, + "allowText": "Cho phép", + "alreadySignedInText": "Tài khoản của bạn đã được đăng nhập trên thiết bị khác;\nhãy đổi tài khoản hoặc đăng xuất khỏi thiết bị khác\nvà thử lại.", + "apiVersionErrorText": "Không thể tải ${NAME}; phiên bản ${VERSION_USED}; yêu cầu ${VERSION_REQUIRED}.", + "audioSettingsWindow": { + "headRelativeVRAudioInfoText": "(tự động bật chức năng này khi cắm tai phone vào)", + "headRelativeVRAudioText": "Tai nghe VR Audio", + "musicVolumeText": "Mức âm lượng", + "soundVolumeText": "Mức độ âm thanh nền", + "soundtrackButtonText": "Các bản nhạc khác", + "soundtrackDescriptionText": "(sử dụng bài nhạc của bạn khi đang chơi game)", + "titleText": "Âm thanh" + }, + "autoText": "Tự động", + "backText": "Quay lại", + "banThisPlayerText": "Khóa người chơi này", + "bestOfFinalText": "Đứng đầu ${COUNT} ván", + "bestOfSeriesText": "Đứng đầu ${COUNT} ván", + "bestRankText": "Thành tích cao nhất của bạn #${RANK}", + "bestRatingText": "Đánh giá cao nhất của bạn ${RATING}", + "bombBoldText": "BOM", + "bombText": "Bom", + "boostText": "Tăng Lực", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} sẽ từ được điều chỉnh.", + "buttonText": "Nút", + "canWeDebugText": "Bạn có muốn BombSquad tự động gửi bug,lỗi \ngame và thồn tin cơ bản cho nhà phát triển?\n\nNhững dữ liệu này không chứa thông tin \ncá nhân và chúng giúp game chạy mượt và không gặp bug.", + "cancelText": "Hủy", + "cantConfigureDeviceText": "Rất tiếc,${DEVICE} không định cấu hình được", + "challengeEndedText": "Thử thách này đã kết thúc.", + "chatMuteText": "Ẩn trò chuyện", + "chatMutedText": "Đã ẩn trò chuyện", + "chatUnMuteText": "Khôi phục trò chuyện", + "choosingPlayerText": "<đang chọn nhân vật>", + "completeThisLevelToProceedText": "Bạn phải hoàn thành \ncấp độ này để tiếp tục!", + "completionBonusText": "Hoàn thành phần thưởng", + "configControllersWindow": { + "configureControllersText": "Điều chỉnh bộ điều khiển", + "configureKeyboard2Text": "Điều chỉnh bàn phím P2", + "configureKeyboardText": "Điều chỉnh bàn phím", + "configureMobileText": "Thiết bị di động đang là bộ điều khiển", + "configureTouchText": "Điều chỉnh màn hình cảm ứng", + "ps3Text": "Bộ điều khiển PS3", + "titleText": "Các bộ điều khiển", + "wiimotesText": "Bộ điều khiển Wii", + "xbox360Text": "Bộ điều khiển Xbox 360" + }, + "configGamepadSelectWindow": { + "androidNoteText": "Lưu ý:bộ điều khiển sẽ hỗ trợ khác nhau tùy theo thiết bị và hệ thống Android", + "pressAnyButtonText": "Nhấn phím bất kỳ trên bộ điều khiển \nmà bạn muốn điều chỉnh....", + "titleText": "Điều chỉnh bộ điều khiển" + }, + "configGamepadWindow": { + "advancedText": "Phát triển", + "advancedTitleText": "Cài đặt bộ điều khiển", + "analogStickDeadZoneDescriptionText": "(Bật cái này nếu nhân vật của bạn trượt khi bạn nhấn cần điều hướng)", + "analogStickDeadZoneText": "Điểm chết trên Điều khiển Analog", + "appliesToAllText": "(áp dụng cho tất cả các bộ điều khiển với kiểu này)", + "autoRecalibrateDescriptionText": "(bật cái này nếu nhân vật không chạy được)", + "autoRecalibrateText": "Tự Đông Hiệu Chỉnh Điều Khiển Analog", + "axisText": "Trục", + "clearText": "xóa", + "dpadText": "Màn Hình", + "extraStartButtonText": "Thêm nút Start", + "ifNothingHappensTryAnalogText": "Nếu không sử dụng được,thử thay thế nút điều hướng khác", + "ifNothingHappensTryDpadText": "Nếu không sử dụng được,thử thay thế D-pad", + "ignoreCompletelyDescriptionText": "(ngăn chặn bộ điều khiển ảnh hưởng tới trò chơi hay menus)", + "ignoreCompletelyText": "Bỏ qua toàn bộ", + "ignoredButton1Text": "Bỏ qua Nút 1", + "ignoredButton2Text": "Bỏ qua Nút 2", + "ignoredButton3Text": "Bỏ qua Nút 3", + "ignoredButton4Text": "Bỏ qua Nút 4", + "ignoredButtonDescriptionText": "(dùng để ngăn nút 'home' hoặc 'sync' ảnh hưởng đến giao diện)", + "pressAnyAnalogTriggerText": "Nhấn bất kì nút trigger", + "pressAnyButtonOrDpadText": "Nhấn bất kì nút hay dpad nào...", + "pressAnyButtonText": "Nhấn nút bất kỳ...", + "pressLeftRightText": "Nhấn nút trái hay phải...", + "pressUpDownText": "Nhấn nut lên hoặc xuống...", + "runButton1Text": "Bật nút 1", + "runButton2Text": "Bật nút 2", + "runTrigger1Text": "Bật nút Trigger 1", + "runTrigger2Text": "Bật nút Trigger 2", + "runTriggerDescriptionText": "(nút Triggers có thể chạy với tốc độ khác nhau)", + "secondHalfText": "Sử dụng cái này để cấu hình nửa thứ hai\ncủa thiết bị 2 bộ điều khiển trong 1\nhiển thị như một bộ điều khiển duy nhất.", + "secondaryEnableText": "Kích hoạt", + "secondaryText": "Bộ điều khiển thứ hai", + "startButtonActivatesDefaultDescriptionText": "(tắt cái này đi nếu nút bắt đầu của bạn có nhiều nút 'menu' hơn)", + "startButtonActivatesDefaultText": "Nút bắt đầu kích hoạt widget mặc định", + "titleText": "Thiết lập điều khiển", + "twoInOneSetupText": "Thiết lập cần điều khiển 2-trong-1", + "uiOnlyDescriptionText": "(ngăn bộ điều khiển này thực sự tham gia một trò chơi)", + "uiOnlyText": "Giới hạn sử dụng Menu", + "unassignedButtonsRunText": "Tất cả các nút chưa được gắn Chạy", + "unsetText": "", + "vrReorientButtonText": "Nút định hướng VR" + }, + "configKeyboardWindow": { + "configuringText": "Cấu hình ${DEVICE}", + "keyboard2NoteText": "Lưu ý: hầu hết các bàn phím chỉ có thể đăng ký một vài phím bấm tại\nmột lần, vì vậy có một trình phát bàn phím thứ hai có thể hoạt động tốt hơn\nnếu có một bàn phím riêng kèm theo để họ sử dụng.\nLưu ý rằng bạn vẫn sẽ cần gán các khóa duy nhất cho\nHai người chơi ngay cả trong trường hợp đó." + }, + "configTouchscreenWindow": { + "actionControlScaleText": "Độ lớn phím hành động", + "actionsText": "Các hành động", + "buttonsText": "các nút", + "dragControlsText": "< di chuyển phím điều khiển để thay đổi vi trí chúng>", + "joystickText": "Phím xoay", + "movementControlScaleText": "Độ lớn phím di chuyển", + "movementText": "Di chuyển", + "resetText": "Chơi lại từ đầu", + "swipeControlsHiddenText": "Ân nút di chuyển", + "swipeInfoText": "Kiểu điều khiển 'Swipe'làm cho việc điều khiển dễ dàng hơn \nmà không cần nhìn phím", + "swipeText": "Trượt", + "titleText": "Điều chinh màn hình cảm ứng" + }, + "configureItNowText": "Điều chỉnh nó ngay ?", + "configureText": "Điều chỉnh", + "connectMobileDevicesWindow": { + "amazonText": "Cửa hàng Amazon", + "appStoreText": "Cửa hàng", + "bestResultsText": "Để chơi game tốt hơn bạn nên sử dụng wifi mạnh.\nBạn cũng có thể tắt các thiết bị đang sử dụng \nwifi khác để giảm lag,chơi game gần người chơi \nkhác trong vong phát sóng wifi và có thể kết nối wifi direct.", + "explanationText": "Để sử dụng điên thoại thông minh hoặc máy tính bảng để làm thiết bị điều khiển,\ncài đặt ứng dụng \"${REMOTE_APP_NAME}\" lên cấc thiết bị đó.\nTất cả các thiêt bị đều có thể kết nối với ${APP_NAME} game thông qua wifi và hoàn toàn miễn phí", + "forAndroidText": "Cho Adndroid:", + "forIOSText": "cho iOS:", + "getItForText": "Tải ${REMOTE_APP_NAME} cho IOS và Android tại App Store \nhoặc cho Android tại cửa hàng Google Play hoặc Amazone Appstore", + "googlePlayText": "Google play", + "titleText": "Sử dụng thiết bị di động như bộ điều khiển." + }, + "continuePurchaseText": "Tiếp tục cho ${PRICE}?", + "continueText": "Tiếp tục", + "controlsText": "Các bộ điều khiển", + "coopSelectWindow": { + "activenessAllTimeInfoText": "Nó không được áp dụng cho tất cả các xếp hạng", + "activenessInfoText": "Những người chơi cùng sẽ tăng khi \nbạn chơi hoặc giảm khi bạn nghỉ.", + "activityText": "Hoạt động", + "campaignText": "Đi cảnh", + "challengesInfoText": "Nhận giải thưởng khi hoàn thành xong các minigames.\n\nPhần thưởng và độ khó tăng \nmỗi khi hoàn thành thử thách và \ngiảm xuống khi thử thách đó bị hết hạn hoặc hủy bỏ", + "challengesText": "Thử thách", + "currentBestText": "Cao nhất hiện tại", + "customText": "Tùy chỉnh", + "entryFeeText": "Mục", + "forfeitConfirmText": "Thím muốn Hủy bỏ thử thách này ?", + "forfeitNotAllowedYetText": "Thím chưa hủy bỏ đc cái thử thách này đâu.", + "forfeitText": "Hủy bỏ", + "multipliersText": "Người chơi khác", + "nextChallengeText": "Thử thách tiếp", + "nextPlayText": "Lần chơi tiếp theo", + "ofTotalTimeText": "của ${TOTAL}", + "playNowText": "Chơi bây giờ", + "pointsText": "Điểm", + "powerRankingFinishedSeasonUnrankedText": "(Hoàn thành mùa không đánh giá)", + "powerRankingNotInTopText": "(không ở top ${NUMBER})", + "powerRankingPointsEqualsText": "= ${NUMBER} Điểm", + "powerRankingPointsMultText": "(x ${NUMBER} Điểm)", + "powerRankingPointsText": "${NUMBER} Điểm", + "powerRankingPointsToRankedText": "(${CURRENT} của ${REMAINING} Điểm)", + "powerRankingText": "Xếp hạng sức mạnh", + "prizesText": "Thưởng", + "proMultInfoText": "Người chơi với ${PRO} nâng cấp\nnhận thêm ${PERCENT}% điểm", + "seeMoreText": "Thêm...", + "skipWaitText": "Bỏ qua việc chờ đợi", + "timeRemainingText": "Thời gian kết thúc", + "toRankedText": "Xêp hạng", + "totalText": "tổng cộng", + "tournamentInfoText": "Hoàn thành điểm cao để cùng thi\nđấu với người chơi khác cùng giải.\n\nGiải thưởng được trao cho những người\ncó điểm cao nhất khi giải đấu kết thúc.", + "welcome1Text": "Chào mừng đến ${LEAGUE}.Bạn có thể nâng cao hạng giải đấu của bạn \nbằng cách đánh giá,hoàn thành các thành tựu và giành các cúp ở \nphần chơi hướng dẫn.", + "welcome2Text": "Bạn có thể kiếm vé từ các hoạt động giống nhau.\nVé có thể sử dụng để mở khóa nhân vật,màn chơi mới \nvà mini games các hướng dẫn mới và nhiều cái khác", + "yourPowerRankingText": "Xếp hạng sức mạnh của bạn" + }, + "copyOfText": "${NAME} copy", + "createEditPlayerText": "", + "createText": "Tạo", + "creditsWindow": { + "additionalAudioArtIdeasText": "Thêm vào âm thanh,hình ảnh mới và ý kiến bởi ${NAME}", + "additionalMusicFromText": "Thêm âm nhạc từ ${NAME}", + "allMyFamilyText": "Người thân và bạn bè tôi đã thử nghiệm", + "codingGraphicsAudioText": "Mã hóa,đồ họa và âm thanh bởi ${NAME}", + "languageTranslationsText": "Phiên dịch:", + "legalText": "Pháp lý:", + "publicDomainMusicViaText": "Nhạc của nhóm cộng đồng qua ${NAME}", + "softwareBasedOnText": "Phần mềm này được phát triển trong một dự án của ${NAME}", + "songCreditText": "${TITLE} Phát triển bởi ${PERFORMER}\nTác giả ${COMPOSER}, Sắp xếp bởi ${ARRANGER},Phát hành bởi ${PUBLISHER},\nNguồn ${SOURCE}", + "soundAndMusicText": "Âm thanh và nhạc:", + "soundsText": "Âm thanh (${SOURCE}):", + "specialThanksText": "Dành lời cảm ơn đặc biệt cho :", + "thanksEspeciallyToText": "Vô cùng cảm ơn đến ${NAME}", + "titleText": "Nhóm Sáng Tạo ${APP_NAME}", + "whoeverInventedCoffeeText": "Ai đó tạo ra cà phê" + }, + "currentStandingText": "Vị trí hiện tại của bạn là #${RANK}", + "customizeText": "Tùy chinh...", + "deathsTallyText": "${COUNT} lần chết", + "deathsText": "Chết", + "debugText": "Sửa lỗi", + "debugWindow": { + "reloadBenchmarkBestResultsText": "Lưu ý:khuyến nghị bật Cài Đặt->Đồ Họa->Khung Cảnh thành cao khi thử nghiệm", + "runCPUBenchmarkText": "Chấm điểm CPU benchmark", + "runGPUBenchmarkText": "Chấm điểm đồ họa", + "runMediaReloadBenchmarkText": "Chạy lại nội dung chuẩn", + "runStressTestText": "Chạy thử nghiệm", + "stressTestPlayerCountText": "Đếm người chơi", + "stressTestPlaylistDescriptionText": "Danh sách thử độ trễ", + "stressTestPlaylistNameText": "Danh sách tên", + "stressTestPlaylistTypeText": "Kiểu danh sách", + "stressTestRoundDurationText": "Thời hạn Vòng", + "stressTestTitleText": "Thử độ trễ", + "titleText": "Chấm điểm và đo độ trễ", + "totalReloadTimeText": "Tổng cộng thời gian lặp lại :${TIME} (xem danh sách để biết chi tiết)" + }, + "defaultGameListNameText": "Danh Sách Mặc Định ${PLAYMODE}", + "defaultNewGameListNameText": "Danh Sách ${PLAYMODE} Của Tôi", + "deleteText": "Xóa", + "demoText": "Giới thiệu", + "denyText": "Từ chối", + "desktopResText": "Độ phân giải", + "difficultyEasyText": "Dễ", + "difficultyHardOnlyText": "Chỉ chế độ khó", + "difficultyHardText": "Khó", + "difficultyHardUnlockOnlyText": "Vòng này chỉ mở ở chế độ khó\nBạn nghĩ mình có thể dễ dàng vào được vòng này sao ?!?", + "directBrowserToURLText": "Hãy cho phép trình duyệt theo đường dẫn:", + "disableRemoteAppConnectionsText": "Tắt Trình Điều Khiên", + "disableXInputDescriptionText": "Cho phép trên 4 thiết bị điều khiển nhưng nó có thể hoạt động không tốt", + "disableXInputText": "Ngắt Kết NỐi XInput", + "doneText": "Xong", + "drawText": "Hòa", + "duplicateText": "Nhân Đôi", + "editGameListWindow": { + "addGameText": "Thêm\ngame", + "cantOverwriteDefaultText": "Không thể ghi đè danh sách mặc định", + "cantSaveAlreadyExistsText": "Tên danh sach đã tồn tại", + "cantSaveEmptyListText": "Không thể lưu danh sách trống", + "editGameText": "Chỉnh\nsửa game", + "listNameText": "Tên danh sách", + "nameText": "Tên", + "removeGameText": "Bỏ \ngame", + "saveText": "Lưu danh sách", + "titleText": "Chỉnh sửa danh sách" + }, + "editProfileWindow": { + "accountProfileInfoText": "Tài khoản đặc biệt này có tên \nvà biểu tượng dựa vào máy.\n\n${ICONS}", + "accountProfileText": "(thông tin tài khoản)", + "availableText": "Tên \"${NAME}\" khả dụng", + "changesNotAffectText": "Lưu ý:những thay đổi sẽ không có tác dụng với các nhân vật có sẵn trong game", + "characterText": "Nhân vật", + "checkingAvailabilityText": "Đang kiểm tra tên \"${NAME}\"...", + "colorText": "Màu sắc", + "getMoreCharactersText": "Thêm nhân vật mới", + "getMoreIconsText": "Nhận được nhiều icons hơn...", + "globalProfileInfoText": "Tài khoản toàn cầu được khuyến nghị để có \ntên riêng biệt. Nó củng có kí hiệu đặc biệt", + "globalProfileText": "(tài khoản toàn cầu)", + "highlightText": "Màu tóc", + "iconText": "Kí hiệu", + "localProfileInfoText": "Tài khoản nội bộ không có kí hiệu và tên của nó\ncủng không được bảo lưu để được độc nhất. Nâng\ncấp lên tài khoản toàn cầu để được nhận ưu đãi.", + "localProfileText": "(tài khoản cố định)", + "nameDescriptionText": "Tên nhân vật", + "nameText": "Tên", + "randomText": "Ngẫu nhiên", + "titleEditText": "Chỉnh sửa thông tin", + "titleNewText": "Tạo mới", + "unavailableText": "${NAME} không khả dụng; hãy thử tên khác", + "upgradeProfileInfoText": "Nó sẽ bảo lưu tên của bạn toàn cầu\nvà bạn có thể gắn được kí hiệu.", + "upgradeToGlobalProfileText": "Nâng lên Tài Khoản Toàn Cầu" + }, + "editSoundtrackWindow": { + "cantDeleteDefaultText": "Bạ không thể xóa file nhạc mặc định", + "cantEditDefaultText": "Không thể chỉnh sửa file nhạc gốc.Nhân đôi lên hoặc tạo mới", + "cantOverwriteDefaultText": "Không thể ghi đè file nhạc mặc định", + "cantSaveAlreadyExistsText": "Tên file nhạc đó đã tồn tại", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Nhạc nền mặc định", + "deleteConfirmText": "Xóa nhạc nền: \n\n'${NAME}'?", + "deleteText": "Xóa bỏ\nNhạc nền", + "duplicateText": "Nhân đôi \nNhạc nền", + "editSoundtrackText": "Chỉnh sửa nhạc", + "editText": "Sửa\nÂm thanh", + "fetchingITunesText": "tìm nạp danh sách phát Ứng dụng Âm nhạc ...", + "musicVolumeZeroWarning": "Cảnh báo: Âm lượng nhạc được chỉnh về 0", + "nameText": "Tên", + "newSoundtrackNameText": "Nhạc nền của tôi ${COUNT}", + "newSoundtrackText": "Nhạc nền mới:", + "newText": "Nhạc nền\nMới", + "selectAPlaylistText": "Chọn một Playlist", + "selectASourceText": "Nguồn nhạc", + "testText": "Kiểm tra", + "titleText": "Nhạc nền", + "useDefaultGameMusicText": "Nhạc nền mặc định trò chơi", + "useITunesPlaylistText": "Danh sách phát ứng dụng âm nhạc", + "useMusicFileText": "Tập tin nhạc (mp3, khác)", + "useMusicFolderText": "Thư mục của files nhạc" + }, + "editText": "Sửa", + "endText": "kết thúc", + "enjoyText": "Chúc vui vẻ!", + "epicDescriptionFilterText": "${DESCRIPTION} trong chế độ quay chậm.", + "epicNameFilterText": "${NAME} Quay Chậm", + "errorAccessDeniedText": "từ chối kết nối", + "errorOutOfDiskSpaceText": "hết bộ nhớ", + "errorText": "Lỗi", + "errorUnknownText": "Không rõ lỗi", + "exitGameText": "Thoát ${APP_NAME}?", + "exportSuccessText": "'${NAME}' Đã xuất", + "externalStorageText": "Bộ nhớ ngoài", + "failText": "Thất bại", + "fatalErrorText": "Ôi không; dữ liệu bị thiếu hoặc hỏng.\nHãy thử cài lại ứng dụng hoặc liên hệ\nvới chúng tôi ${EMAIL}để được giúp đỡ", + "fileSelectorWindow": { + "titleFileFolderText": "Chọn file hoặc thư mục", + "titleFileText": "Chon một file", + "titleFolderText": "Chọn thư mục", + "useThisFolderButtonText": "Sử dụng thư mục này" + }, + "finalScoreText": "Điểm hoàn thành", + "finalScoresText": "Điểm cuối cùng", + "finalTimeText": "Thời gian hoàn thành", + "finishingInstallText": "Hoàn thành cài đặt; vui lòng chờ...", + "fireTVRemoteWarningText": "*Đê có trải nghiệm tốt nhất,\nsử dụng điều khiển hoặc cài\nđặt '${REMOTE_APP_NAME}' trên\nđiện thoại hay máy tính bảng.", + "firstToFinalText": "Dẫn đầu đến ${COUNT}", + "firstToSeriesText": "Dẫn đầu đến ${COUNT}", + "fiveKillText": "NĂM MẠNG", + "flawlessWaveText": "Chiến Thắng Hoàn Hảo!!", + "fourKillText": "BỐN MẠNG", + "friendScoresUnavailableText": "Điểm số bạn bè không có sẵn.", + "gameCenterText": "Trung tâm trò chơi", + "gameCircleText": "Vòng tròn trò chơi", + "gameLeadersText": "Quán quân ${COUNT} Trò chơi", + "gameListWindow": { + "cantDeleteDefaultText": "Bạn không thể xóa danh sách mặc định", + "cantEditDefaultText": "Không thể sửa danh sách mặc định! Nhân đôi nó hoặc tạo một cái mới.", + "cantShareDefaultText": "Bạn không thể chia sẻ danh sách mặc định", + "deleteConfirmText": "Xóa bỏ \"${LIST}\"?", + "deleteText": "Xóa \nDanh sách", + "duplicateText": "Nhân đôi \nDanh sách", + "editText": "Đặt lại \nDanh sách", + "newText": "Danh sách\nMới", + "showTutorialText": "Hiện Hướng dẫn", + "shuffleGameOrderText": "Xáo trộn thứ tự trò chơi", + "titleText": "Tùy chỉnh ${TYPE} Danh sách phát" + }, + "gameSettingsWindow": { + "addGameText": "Thêm trò chơi" + }, + "gamesToText": "${WINCOUNT} Các trò chơi thành ${LOSECOUNT}", + "gatherWindow": { + "aboutDescriptionLocalMultiplayerExtraText": "Lưu ý: Bất kỳ thiết bị nào trong tổ đội có nhiều \nhơn một thành viên nếu bạn có đủ tay cầm.", + "aboutDescriptionText": "Sử dụng các tab này để tập hợp một tổ đội.\n\nCác tổ đội cho phép bạn chơi các trò chơi và giải đấu\nvới bạn bè của bạn trên các thiết bị khác nhau.\n\nSử dụng nút ${PARTY} ở trên cùng bên phải để\ntrò chuyện và tương tác với tổ đội của bạn.\n(trên bộ điều khiển, nhấn ${BUTTON} trong khi ở trong menu)", + "aboutText": "Hướng Dẫn", + "addressFetchErrorText": "", + "appInviteMessageText": "${NAME} đã gửi cho bạn vé ${COUNT} bằng ${APP_NAME}", + "appInviteSendACodeText": "Gửi cho họ mã này", + "appInviteTitleText": "${APP_NAME} Ứng dụng mời", + "bluetoothAndroidSupportText": "(hoạt động với mọi thiết bị Android hỗ trợ Bluetooth)", + "bluetoothDescriptionText": "Tổ chức/tham gia một tổ đội qua Bluetooth:", + "bluetoothHostText": "Tổ chức qua Bluetooth", + "bluetoothJoinText": "Vào qua bluetooth", + "bluetoothText": "Bluetooth", + "checkingText": "Đang kiểm tra...", + "dedicatedServerInfoText": "Để có kết quả tốt nhất, hãy thiết lập một máy chủ chuyên dụng.Vào bombsquadgame.com/server để tìm hiểu cách.", + "disconnectClientsText": "Điều này sẽ ngắt kết nối (các) người chơi ${COUNT}\n trong tổ đội của bạn.Bạn có chắc không?", + "earnTicketsForRecommendingAmountText": "Bạn bè sẽ nhận được vé ${COUNT} nếu họ thử trò chơi\n (và bạn sẽ nhận được ${YOU_COUNT} cho mỗi người làm)", + "earnTicketsForRecommendingText": "Chia sẻ trò chơi này với bạn bè\nđể có thêm vé miễn phí...", + "emailItText": "Gửi nó", + "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} vé từ ${NAME}", + "friendPromoCodeAwardText": "Bạn sẽ nhận được ${COUNT} vé mỗi lần nó được sử dụng", + "friendPromoCodeExpireText": "Mã code sẽ hết hạn trong ${EXPIRE_HOURS} và chỉ hoạt động với người chơi mới.", + "friendPromoCodeInstructionsText": "Để dùng nó,mở ${APP_NAME} và tới \"Cài đặt ->Nâng cao->Nhập mã\".\nXem bombsquadgame.com cho đường dẫn tải xuống cho tất cả hệ điều hành hỗ trợ.", + "friendPromoCodeRedeemLongText": "Nó có thể được đổi lấy ${COUNT} vé miễn phí cho tối đa ${MAX_USES} người", + "friendPromoCodeRedeemShortText": "Nó có thể được đổi lấy vé ${COUNT} trong trò chơi.", + "friendPromoCodeWhereToEnterText": "(trong \"Cài đặt-> Nâng cao-> Nhập mã\")", + "getFriendInviteCodeText": "Nhận mã mời bạn bè", + "googlePlayDescriptionText": "Mời bạn bè trên google play đến party của bạn:", + "googlePlayInviteText": "Mời", + "googlePlayReInviteText": "Có ${COUNT} người chơi Google Play trong nhóm của bạn\n ai sẽ bị ngắt kết nối nếu bạn bắt đầu một lời mời mới.\n Bao gồm họ trong lời mời mới để họ quay lại .", + "googlePlaySeeInvitesText": "Xem lời mời", + "googlePlayText": "Google Play", + "googlePlayVersionOnlyText": "(Phiên bản Android / Google Play)", + "hostPublicPartyDescriptionText": "Tổ chức một tổ đội công cộng:", + "inDevelopmentWarningText": "Ghi chú:\n\n Chơi mạng là một tính năng mới và vẫn đang phát triển.\n Hiện tại, rất khuyến khích tất cả\n Người chơi ở trên cùng một mạng Wi-Fi.", + "internetText": "Internet", + "inviteAFriendText": "Bạn bè không có game này? Mời họ để\nthử nó và họ sẽ nhận được ${COUNT} tickets miễn phí", + "inviteFriendsText": "Mời bạn bè", + "joinPublicPartyDescriptionText": "Tham gia một bữa tiệc công cộng:", + "localNetworkDescriptionText": "Tham gia một bữa tiệc trên mạng của bạn:", + "localNetworkText": "Mạng nội bộ", + "makePartyPrivateText": "Làm cho bữa tiệc của tôi riêng tư", + "makePartyPublicText": "Công khai bữa tiệc của tôi", + "manualAddressText": "Địa chỉ", + "manualConnectText": "Kết nối", + "manualDescriptionText": "Vào một party bằng địa chỉ IP:", + "manualJoinableFromInternetText": "Bạn có thể dược tham gia từ internet?:", + "manualJoinableNoWithAsteriskText": "KHÔNG*", + "manualJoinableYesText": "CÓ", + "manualRouterForwardingText": "*Hãy thử vào router và tạo cổng UDP ${PORT} trên IP của bạn", + "manualText": "Tự chọn", + "manualYourAddressFromInternetText": "Địa chỉ IP của bạn ở trên internet:", + "manualYourLocalAddressText": "Địa chỉ nội bộ:", + "noConnectionText": "(không có kết nối)", + "otherVersionsText": "(phiên bản khác)", + "partyInviteAcceptText": "Chấp nhận", + "partyInviteDeclineText": "Từ chối", + "partyInviteGooglePlayExtraText": "(Ở bảng 'Google Play' trong 'Nhiều Người')", + "partyInviteIgnoreText": "Bỏ qua", + "partyInviteText": "${NAME} đã mời bạn\nvào party của họ!", + "partyNameText": "Tên Phòng", + "partySizeText": "kích cỡ tiệc", + "partyStatusCheckingText": "Kiểm tra trạng thái...", + "partyStatusJoinableText": "Phòng của bạn có thể tham gia", + "partyStatusNoConnectionText": "không thể kết nối đến máy chủ", + "partyStatusNotJoinableText": "Phòng của bạn không thể tham gia", + "partyStatusNotPublicText": "phòng của bạn không thể tham gia", + "pingText": "ping", + "portText": "Cổng", + "requestingAPromoCodeText": "Yêu cầu mã...", + "sendDirectInvitesText": "Gửi trực tiếp lời mời", + "shareThisCodeWithFriendsText": "Chia sẻ mã này với bạn bè:", + "showMyAddressText": "Hiển thị địa chỉ", + "titleText": "Nhiều Người", + "wifiDirectDescriptionBottomText": "Nếu tất cả các thiết bị có bảng điều khiển 'Wi-Fi Direct', họ sẽ có thể sử dụng nó để tìm\n và kết nối với nhau. Khi tất cả các thiết bị được kết nối, bạn có thể tạo thành các bữa tiệc\n ở đây sử dụng tab 'Mạng cục bộ', giống như với mạng Wi-Fi thông thường.\n\n Để có kết quả tốt nhất, máy chủ Wi-Fi Direct cũng phải là máy chủ của bữa tiệc ${APP_NAME}.", + "wifiDirectDescriptionTopText": "Wi-Fi Direct có thể được sử dụng để kết nối trực tiếp các thiết bị Android mà không cần\n một mạng Wi-Fi. Điều này hoạt động tốt nhất trên Android 4.2 trở lên.\n\n Để sử dụng nó, hãy mở cài đặt Wi-Fi và tìm 'Wi-Fi Direct' trong menu.", + "wifiDirectOpenWiFiSettingsText": "Mở cài đặt Wi-Fi", + "wifiDirectText": "Wi-Fi Direct", + "worksBetweenAllPlatformsText": "(Hoạt động trên tất cả hệ điều hành)", + "worksWithGooglePlayDevicesText": "(hoạt động với thiết bị chạy Google Trò chơi (Android) phiên bản của trò chơi)", + "youHaveBeenSentAPromoCodeText": "Bạn đã nhận một ${APP_NAME} mã code thưởng:" + }, + "getTicketsWindow": { + "freeText": "Miễn phí!", + "freeTicketsText": "Vé miễn phí", + "inProgressText": "Đang giao dịch; vui lòng thử lại sau một lát.", + "purchasesRestoredText": "Khôi phục hoàn tất.", + "receivedTicketsText": "Nhận được ${COUNT} vé!", + "restorePurchasesText": "Khôi phục Mua hàng", + "ticketPack1Text": "Gói vé nhỏ", + "ticketPack2Text": "Gói vé vừa", + "ticketPack3Text": "Gói vé lớn", + "ticketPack4Text": "Gói vé siêu lớn", + "ticketPack5Text": "Gói vé khổng lồ", + "ticketPack6Text": "Gói vé siêu khổng lồ", + "ticketsFromASponsorText": "Lấy ${COUNT} vé \ntừ một nhà tài trợ", + "ticketsText": "${COUNT} Vé", + "titleText": "Lấy vé", + "unavailableLinkAccountText": "Xin lỗi, không thể mua hàng trên hệ điều hành này.\nBạn có thể, liên kết tài khoản này\ntới một tài khoản trên hệ điều hành khác và mua hàng ở đó.", + "unavailableTemporarilyText": "Hiện tại không có sẵn; vui lòng thử lại sau", + "unavailableText": "Xin lỗi,cái này không có sẵn", + "versionTooOldText": "Xin lỗi,phiên bản trò chơi quá cũ; vui lòng nâng cấp lên phiên bản mới hơn.", + "youHaveShortText": "Bạn có ${COUNT}", + "youHaveText": "Bạn có ${COUNT} vé" + }, + "googleMultiplayerDiscontinuedText": "Xin lỗi, dịch vụ Google nhiều người chơi không còn nữa.\nTôi đang cố gắng thay thế nhanh nhất có thể.\nTrong lúc đó, vui lòng thử cách kết nối khác.\n-Eric", + "googlePlayText": "Google Trò chơi", + "graphicsSettingsWindow": { + "alwaysText": "Luôn Luôn", + "fullScreenCmdText": "Toàn màn hình (Cmd-F)", + "fullScreenCtrlText": "Toàn màn hình (Ctrl-F)", + "gammaText": "Gam-ma", + "highText": "Cao", + "higherText": "Cao hơn", + "lowText": "Thấp", + "mediumText": "Vừa", + "neverText": "Không bao giờ", + "resolutionText": "Độ phân giải", + "showFPSText": "Hiển thị tốc độ khung hình", + "texturesText": "Bề mặt", + "titleText": "Đồ họa", + "tvBorderText": "Màn hình TV", + "verticalSyncText": "Đồng bộ dọc", + "visualsText": "Hình ảnh" + }, + "helpWindow": { + "bombInfoText": "- Bom -\nMạnh hơn nắm đấm, nhưng\nbạn có thể tự làm hại mình.\nĐể có kết quả tốt nhất, ném\nvề phía đói phương trước khi nổ", + "canHelpText": "${APP_NAME} Có thể giúp ban.", + "controllersInfoText": "Bạn có thể chơi ${APP_NAME} với bạn bè hay qua mạng, hoặc tất\ncả các bạn có thể chơi trên một thiết bị nếu đủ bộ điều khiển.\n${APP_NAME} hổ trợ một lượng lớn các thiết bị; bạn có thể sử dụng\nđiện thoại như một bộ điều khiển qua ứng dụng '${REMOTE_APP_NAME}'.\nĐến Cài Đặt -> bộ điều khiển để xem thêm.", + "controllersText": "Bộ điều khiển", + "controlsSubtitleText": "Nhân vật ${APP_NAME} của bạn có một vài động tác cơ bản:", + "controlsText": "Điều khiển", + "devicesInfoText": "Phiên bản ${APP_NAME} VR có thể chơi cùng với phiên bản gốc,\nnên hãy lấy hết tất cả các điện thoại, máy tính bảng vả máy tính\ncá nhân rồi bắt đầu trận chiến! Nó còn có ích khi kết nối với\nphiên bản gốc đến phiên bản VR để một người khác có thể\ntheo dõi trận chiến.", + "devicesText": "Thiết bị", + "friendsGoodText": "Rất tốt nếu bạn có. ${APP_NAME} vui hơn khi có nhiều người chơi\nhơn và có thể hỗ trợ lên đến 8 người chơi cùng một lúc, Vì vậy:", + "friendsText": "Bạn bè", + "jumpInfoText": "- Nhảy -\nNhảy qua các chướng ngại vật,\nđể ném cao hơn, và cảm nhận\nđược niềm vi của bay lượn.", + "orPunchingSomethingText": "Hoặc đấm một vật nào đó, ném nó xuống vực, và làm nổ tung nó khi nó bay xuống bằng bom dính.", + "pickUpInfoText": "- Nhặt -\nNhặt cờ, đối thủ, hoặc bất cứ\nthứ gì khác không dính vào sàn.\nNhấn lại để ném.", + "powerupBombDescriptionText": "Cho phép bạn ném ba bom cùng\nmột lúc thay vì chỉ một.", + "powerupBombNameText": "Ba Lần Bom", + "powerupCurseDescriptionText": "Có thể bạn nên tránh nó.\n ...hoặc không?", + "powerupCurseNameText": "Lời Nguyền", + "powerupHealthDescriptionText": "Hồi đầy máu cho bạn.\nKhông cần phải lo lắng.", + "powerupHealthNameText": "Hộp Cứu Thương", + "powerupIceBombsDescriptionText": "Yếu hon bom cơ bản nhưng\nlàm kẻ thù đóng băng và\nhoàn toàn bất động.", + "powerupIceBombsNameText": "Bom Băng Giá", + "powerupImpactBombsDescriptionText": "Yếu hơn bom cơ bản một chút, nhưng\nnó có thể nổ ngay khi chạm vào.", + "powerupImpactBombsNameText": "Bom Hạt Nhân", + "powerupLandMinesDescriptionText": "Nó đi theo gói 3; bảo \nPhù hợp để bảo vệ hoặc\nchặn kẻ thù tốc độ", + "powerupLandMinesNameText": "Mìn", + "powerupPunchDescriptionText": "Làm bạn đấm tốt hơn,\nnhanh hơn và mạnh hơn.", + "powerupPunchNameText": "Găng Tay", + "powerupShieldDescriptionText": "Hấp thụ một lượng sát thương\nđể bạn không phải chịu.", + "powerupShieldNameText": "Khiên Chắn", + "powerupStickyBombsDescriptionText": "Dính vào mọi thứ nó chạm.\nHoàn toàn chắc chắn.", + "powerupStickyBombsNameText": "Bom dính", + "powerupsSubtitleText": "Dĩ nhiên, không trò chơi nào có thể thiếu :", + "powerupsText": "Năng Lục", + "punchInfoText": "- Đấm -\nCú đấm gây ra nhiều sát thương hơn\nkhi bạn đang di chuyển, nên hãy\ncứ chạy và xoay như kẻ điên.", + "runInfoText": "- Chạy -\nGiữ BẤT KÌ nút nào để chạy. Các nút khác đều hoạt động nếu bạn có nó.\nChạy giúp bạn di chuyển nhanh hơn nhưng cũng làm bạn khó điều khiển hơn.", + "someDaysText": "Một ngày bạn muốn đấm thứ gì đó. Hoặc làm nổ tung nó.", + "titleText": "Giúp Đỡ Từ ${APP_NAME}", + "toGetTheMostText": "Để tối ưu hóa trò chơi, bạn sẽ cần:", + "welcomeText": "Chào mừng đến ${APP_NAME}!" + }, + "holdAnyButtonText": "", + "holdAnyKeyText": "", + "hostIsNavigatingMenusText": "- ${HOST} đang điều hướng màn hình chính như một ông trùm", + "importPlaylistCodeInstructionsText": "Sử dụng mã đi kèm để nhập danh sách này ở chỗ khác:", + "importPlaylistSuccessText": "Đã nhập ${TYPE} danh sách '${NAME}'", + "importText": "Xuất", + "importingText": "Đang nhập...", + "inGameClippedNameText": "trong game sẽ là \n\"${NAME}\"", + "installDiskSpaceErrorText": "Lỗi: Không thể hoàn thành tải xuống.\nBạn có thể hết dung lượng trên thiết bị của bạn.\nDọn một ít chỗ trống và thử lại.", + "internal": { + "arrowsToExitListText": "nhấn ${LEFT} hoặc ${RIGHT} để rời khỏi danh sách", + "buttonText": "Nút", + "cantKickHostError": "Bạn không thể đuổi chủ phòng.", + "chatBlockedText": "${NAME} bị chặn nhắn tin trong ${TIME} giây.", + "connectedToGameText": "'${NAME}' đã tham gia", + "connectedToPartyText": "Đã tham gia tổ đội của ${NAME}!", + "connectingToPartyText": "Đang kết nối...", + "connectionFailedHostAlreadyInPartyText": "Kết nối thất bại; chủ phòng đã ở tổ đội khác.", + "connectionFailedPartyFullText": "Kết nối thất bại; tổ đội đã đủ người.", + "connectionFailedText": "Kết nối thất bại.", + "connectionFailedVersionMismatchText": "Kết nối thất bại; người tổ chức đang chạy một phiên bản khác của trò chơi.\nChắc chắn rằng cả hai đều là phiên bản mới nhất và thử lại.", + "connectionRejectedText": "Kết nối bị từ chối.", + "controllerConnectedText": "Đã kết nối ${CONTROLLER}.", + "controllerDetectedText": "Phát hiện 1 cần điều khiển.", + "controllerDisconnectedText": "${CONTROLLER} đã ngắt kết nối.", + "controllerDisconnectedTryAgainText": "Đã ngắt kết nối ${CONTROLLER}. Vui lòng thử kết nối lại.", + "controllerForMenusOnlyText": "Cần điều khiển này chỉ dùng để điều khiển.", + "controllerReconnectedText": "${CONTROLLER} đã kết nối lại.", + "controllersConnectedText": "${COUNT} cần điều khiển đã kết nối.", + "controllersDetectedText": "${COUNT} cần điều khiển đã phát hiện.", + "controllersDisconnectedText": "${COUNT} cần điều khiển đã ngắt kết nối.", + "corruptFileText": "Phát hiện tập tin bị hỏng. Vui lòng tải lại trò chơi, hoặc tương tác với email ${EMAIL}", + "errorPlayingMusicText": "Lỗi phát nhạc: ${MUSIC}", + "errorResettingAchievementsText": "Không thể làm mới thành tựu trực tuyến; vui lòng thử lại sau.", + "hasMenuControlText": "", + "incompatibleNewerVersionHostText": "Chủ phòng đang chạy một phiên bản mới hơn của trò chơi.\nCập nhật trò chơi lên phiên bản mới nhất và thử lại.", + "incompatibleVersionHostText": "Chủ phòng đang chạy một phiên bản khác của trò chơi.\nChắc chắn cả hai đều cập nhật phiên bản mới nhất và thử lại.", + "incompatibleVersionPlayerText": "${NAME} đang chạy một phiên bản khác của trò chơi.\nChắc chắn rằng cả hai đều cập nhật lên phiên bản mới nhất và thử lại.", + "invalidAddressErrorText": "Lỗi: địa chỉ không hợp lệ.", + "invalidNameErrorText": "Lỗi: tên không hợp lệ.", + "invalidPortErrorText": "Lỗi: cổng không hợp lệ.", + "invitationSentText": "Đã gửi lời mời", + "invitationsSentText": "Đã gửi ${COUNT} lời mời.", + "joinedPartyInstructionsText": "Một ai đó đã gia nhập tổ đội của bạn.\nNhấn nút 'Chơi' để bắt đầu trò chơi.", + "keyboardText": "Bàn phím", + "kickIdlePlayersKickedText": "Đuổi ${NAME} bởi vì người chơi không hoạt động.", + "kickIdlePlayersWarning1Text": "${NAME} sẽ bị đuổi trong ${COUNT} giây nếu tiếp tục không hoạt động.", + "kickIdlePlayersWarning2Text": "(bạn có thể tắt nó ở trong Cài đặt -> Nâng cao)", + "leftGameText": "Đã rời '${NAME}'.", + "leftPartyText": "Đã rời khỏi tổ đội của ${NAME}.", + "noMusicFilesInFolderText": "Thư mục không chứa tập tin nhạc.", + "playerJoinedPartyText": "${NAME} đã tham gia tổ đội!", + "playerLeftPartyText": "${NAME} đã rời khỏi tổ đội.", + "rejectingInviteAlreadyInPartyText": "Đang từ chối lời mời (đã ở trong một tổ đội).", + "serverRestartingText": "Máy chủ đang reset. Hãy vào trong giây lát", + "serverShuttingDownText": "Máy chủ đang đóng", + "signInErrorText": "Lỗi đăng nhập.", + "signInNoConnectionText": "Không thể đăng nhập. (không có kết nối mạng?)", + "telnetAccessDeniedText": "LỖI: người dùng không có quyền truy cập giao thức.", + "timeOutText": "(hết giờ trong ${TIME} giây)", + "touchScreenJoinWarningText": "Bạn đã gia nhập với màn hình cảm ứng.\nNếu đây là lỗi, nhấn 'Menu-> Rời trò chơi '.", + "touchScreenText": "Màn hình Cảm ứng", + "unableToResolveHostText": "Lỗi: không thể giải quyết máy chủ.", + "unavailableNoConnectionText": "Hiện tại không có sẵn (không có kết nối mạng?)", + "vrOrientationResetCardboardText": "Sử dụng cái này để cài lại VR định hướng.\nĐể chơi trò chơi bạn sẽ cần một cần điều khiển bên ngoài.", + "vrOrientationResetText": "Cài lại định hướng VR.", + "willTimeOutText": "(sẽ hết giờ nếu không hoạt động)" + }, + "jumpBoldText": "NHẢY", + "jumpText": "Nhảy", + "keepText": "Giữ", + "keepTheseSettingsText": "Giữ các cài đặt?", + "keyboardChangeInstructionsText": "Nhấn phím cách hai lần để đổi bàn phím.", + "keyboardNoOthersAvailableText": "Không có bàn phím khác sẵn có.", + "keyboardSwitchText": "Đổi sang bàn phím \"${NAME}\".", + "kickOccurredText": "${NAME} đã bị đuổi.", + "kickQuestionText": "Đuổi ${NAME}?", + "kickText": "Đuổi", + "kickVoteCantKickAdminsText": "Không thể kick Admin", + "kickVoteCantKickSelfText": "Bạn không thể đuổi chính bạn", + "kickVoteFailedNotEnoughVotersText": "Không đủ người chơi để bỏ phiếu.", + "kickVoteFailedText": "Bầu chọn đuổi người chơi thất bại.", + "kickVoteStartedText": "Một cuộc bầu chọn người chơi đã bắt đầu bởi ${NAME}.", + "kickVoteText": "Bầu chọn để Đuổi", + "kickVotingDisabledText": "Tính năng bầu chọn để đuổi đã tắt", + "kickWithChatText": "Nhấn ${YES} trong cuộc trò chuyện để đồng ý và ${NO} để phản đối.", + "killsTallyText": "Giết ${COUNT}", + "killsText": "Số mạng", + "kioskWindow": { + "easyText": "Dễ", + "epicModeText": "Chế độ chậm", + "fullMenuText": "Menu đầy đủ", + "hardText": "Khó", + "mediumText": "Bình thường", + "singlePlayerExamplesText": "Ví dụ người chơi đơn / Co-op", + "versusExamplesText": "Ví dụ Versus" + }, + "languageSetText": "Ngôn ngữ hiện là \"${LANGUAGE}\".", + "lapNumberText": "Vòng ${CURRENT}/${TOTAL}", + "lastGamesText": "(trò chơi ${COUNT} mới nhất)", + "leaderboardsText": "Bảng xếp hạng", + "league": { + "allTimeText": "Mọi lúc", + "currentSeasonText": "Mùa hiện tại (${NUMBER})", + "leagueFullText": "${NAME} Giải đấu", + "leagueRankText": "Hạng giải đấu", + "leagueText": "Giải đấu", + "rankInLeagueText": "#${RANK},${NAME} Giải đấu${SUFFIX}", + "seasonEndedDaysAgoText": "Mùa kết thúc ${NUMBER} ngày trước.", + "seasonEndsDaysText": "Mùa kết thúc ${NUMBER} ngày.", + "seasonEndsHoursText": "Mùa kết thúc ${NUMBER} giờ trước.", + "seasonEndsMinutesText": "Mùa kết thúc ${NUMBER} phúc trước.", + "seasonText": "Mùa ${NUMBER}", + "tournamentLeagueText": "Bạn phải đạt giải đấu ${NAME} để tham gia giải đấu này.", + "trophyCountsResetText": "Số lượng cúp sẽ thiết lập lại vào mùa giải tới." + }, + "levelBestScoresText": "Điểm số tốt nhất trên ${LEVEL}", + "levelBestTimesText": "Thời gian tốt nhất trên ${LEVEL}", + "levelIsLockedText": "${LEVEL} đã bị khóa.", + "levelMustBeCompletedFirstText": "${LEVEL} phải được hoàn thành trước.", + "levelText": "Màn chơi ${NUMBER}", + "levelUnlockedText": "Mở khóa cấp độ!", + "livesBonusText": "Tiền thưởng sống sót", + "loadingText": "Đang tải", + "loadingTryAgainText": "Đang tải; thử lại sau một lúc ...", + "macControllerSubsystemBothText": "Cả hai (không nên dùng)", + "macControllerSubsystemClassicText": "Cổ điển", + "macControllerSubsystemDescriptionText": "(thử thay đổi điều này nếu bộ điều khiển của bạn không hoạt động)", + "macControllerSubsystemMFiNoteText": "Đã phát hiện bộ điều khiển dành cho iOS / Mac;\nBạn có thể muốn bật những thứ này trong Cài đặt -> Bộ điều khiển", + "macControllerSubsystemMFiText": "Được tạo cho iOS / Mac", + "macControllerSubsystemTitleText": "Hỗ trợ điều khiển", + "mainMenu": { + "creditsText": "Giới thiệu", + "demoMenuText": "Cài đặt Demo", + "endGameText": "kết thúc game", + "exitGameText": "Thoat game", + "exitToMenuText": "Quay về Menu?", + "howToPlayText": "Hướng dẫn chơi", + "justPlayerText": "(Chỉ có ${NAME})", + "leaveGameText": "Rời trận", + "leavePartyConfirmText": "Có thật sự thoát game?", + "leavePartyText": "Rời nhóm", + "quitText": "Thoát", + "resumeText": "Trở lại", + "settingsText": "Tùy chỉnh" + }, + "makeItSoText": "Làm cho nó như vậy", + "mapSelectGetMoreMapsText": "Nhận thêm bản đồ ...", + "mapSelectText": "Chọn...", + "mapSelectTitleText": "Bản đồ ${GAME}", + "mapText": "Bản đồ", + "maxConnectionsText": "Kết nối tối đa", + "maxPartySizeText": "Số người trong bữa tiệc tối đa", + "maxPlayersText": "Người chơi tối đa", + "mostValuablePlayerText": "Người chơi đáng giá nhất", + "mostViolatedPlayerText": "Người chơi bị chết nhiều nhất", + "mostViolentPlayerText": "Người chơi bạo lực nhất", + "moveText": "Di chuyển", + "multiKillText": "${COUNT}-KILL!!!", + "multiPlayerCountText": "${COUNT} người chơi", + "mustInviteFriendsText": "Lưu ý: bạn phải mời bạn bè vào\nbảng điều khiển \"${GATHER}\" hoặc đính kèm\nbộ điều khiển để chơi nhiều người chơi.", + "nameBetrayedText": "${NAME} đã phản bội ${VICTIM}.", + "nameDiedText": "${NAME} đã hy sinh.", + "nameKilledText": "${NAME} đã tiêu diệt ${VICTIM}.", + "nameNotEmptyText": "Tên không thể để trống!", + "nameScoresText": "${NAME} Ghi điểm!", + "nameSuicideKidFriendlyText": "${NAME} vô tình chết.", + "nameSuicideText": "${NAME} tự tử.", + "nameText": "Tên", + "nativeText": "Tự nhiên", + "newPersonalBestText": "Mới tốt nhất cá nhân!", + "newTestBuildAvailableText": "Một bản dựng thử nghiệm mới hơn có sẵn! (${VERSION} xây dựng ${BUILD}).\nNhận nó tại ${ADDRESS}", + "newText": "Mới", + "newVersionAvailableText": "Đã có phiên bản mới hơn của ${APP_NAME}! (${VERSION})", + "nextAchievementsText": "Thành tựu tiếp theo:", + "nextLevelText": "Cấp độ tiếp theo", + "noAchievementsRemainingText": "không có gì hết", + "noContinuesText": "(không tiếp tục)", + "noExternalStorageErrorText": "Không tìm thấy bộ nhớ ngoài trên thiết bị này", + "noGameCircleText": "Lỗi: không đăng nhập vào GameCircle", + "noScoresYetText": "Chưa có điểm nào.", + "noThanksText": "Không cảm ơn", + "noTournamentsInTestBuildText": "CẢNH BÁO: Điểm thi đấu từ bản dựng thử nghiệm này sẽ bị bỏ qua.", + "noValidMapsErrorText": "Không tìm thấy bản đồ hợp lệ cho loại trò chơi này.", + "notEnoughPlayersRemainingText": "Không đủ người chơi còn lại; thoát ra và bắt đầu một trò chơi mới", + "notEnoughPlayersText": "Bạn cần ít nhất ${COUNT} người chơi để bắt đầu trò chơi này!", + "notNowText": "Không phải bây giờ", + "notSignedInErrorText": "Bạn phải đăng nhập để làm điều này.", + "notSignedInGooglePlayErrorText": "Bạn phải đăng nhập bằng Google Play để làm điều này.", + "notSignedInText": "chưa đăng nhập", + "nothingIsSelectedErrorText": "Bạn chưa chọn cái nào!", + "numberText": "#${NUMBER}", + "offText": "Tắt", + "okText": "Ok", + "onText": "Bật", + "onslaughtRespawnText": "${PLAYER} sẽ hồi sinh trong ải ${WAVE}", + "orText": "${A} hoặc ${B}", + "otherText": "Khác ...", + "outOfText": "(#${RANK} ra khỏi ${ALL})", + "ownFlagAtYourBaseWarning": "Cờ của riêng bạn phải là\ntại chỗ của bạn để ghi điểm!", + "packageModsEnabledErrorText": "Không cho phép chơi qua mạng khi gói mods nội bộ đang bật (mở Cài đặt->Nâng cao)", + "partyWindow": { + "chatMessageText": "Nhắn tin", + "emptyText": "Tổ đội của bạn không có ai hết", + "hostText": "(chủ phòng)", + "sendText": "Gửi", + "titleText": "Tổ đội của bạn" + }, + "pausedByHostText": "(Dừng lại bởi chủ phòng)", + "perfectWaveText": "Ải hoàn hảo!", + "pickUpText": "Nhặt", + "playModes": { + "coopText": "Chế độ co-op", + "freeForAllText": "Đấu đơn", + "multiTeamText": "Nhiều đội", + "singlePlayerCoopText": "Chơi đơn hoặc co-op", + "teamsText": "Đội" + }, + "playText": "Chơi", + "playWindow": { + "oneToFourPlayersText": "1-4 người chơi", + "titleText": "Chơi", + "twoToEightPlayersText": "2-8 người chơi" + }, + "playerCountAbbreviatedText": "${COUNT} người chơi", + "playerDelayedJoinText": "${PLAYER} sẽ tham gia vào đầu ván tiếp theo.", + "playerInfoText": "Thông tin Người chơi", + "playerLeftText": "${PLAYER} rời khỏi trò chơi.", + "playerLimitReachedText": "Đạt giới hạn ${COUNT} người chơi; không cho phép thêm người chơi.", + "playerProfilesWindow": { + "cantDeleteAccountProfileText": "Bạn không thể xóa hồ sơ tài khoản của bạn.", + "deleteButtonText": "Xóa\nHồ sơ", + "deleteConfirmText": "Xóa '${PROFILE}'?", + "editButtonText": "Sửa\nHồ sơ", + "explanationText": "(chỉnh sửa tên người chơi và hình đại diện cho tải khoản này)", + "newButtonText": "Hồ sơ \nMới", + "titleText": "Hồ sơ người chơi" + }, + "playerText": "Người chơi", + "playlistNoValidGamesErrorText": "Danh sách này không chứa trò chơi nào đã mở khóa.", + "playlistNotFoundText": "không tìm thấy danh sách", + "playlistsText": "Danh sách", + "pleaseRateText": "Nếu bạn thấy ${APP_NAME} vui lòng \nđánh giá hoặc viết \ncảm nhận.\nĐiều này giúp hỗ trợ phát triển trong tương lai.\ncảm ơn!\n-eric", + "pleaseWaitText": "Vui lòng chờ...", + "pluginsDetectedText": "Đã phát hiện plugin mới. Kích hoạt / cấu hình chúng trong cài đặt.", + "pluginsText": "Cắm", + "practiceText": "Luyện tập", + "pressAnyButtonPlayAgainText": "Nhấn nút bất kỳ để chơi lại...", + "pressAnyButtonText": "Nhấn nút bất kỳ để tiếp tục...", + "pressAnyButtonToJoinText": "nhấn nút bất kỳ để gia nhập...", + "pressAnyKeyButtonPlayAgainText": "Nhấn phím/nút bất kỳ để chơi lại...", + "pressAnyKeyButtonText": "Nhấn phím/nút bất kỳ để tiếp tục...", + "pressAnyKeyText": "Nhấn nút bất kỳ...", + "pressJumpToFlyText": "** nhấn nhảy liên tục để bay **", + "pressPunchToJoinText": "Nhấn ĐẤM để gia nhập...", + "pressToOverrideCharacterText": "nhấn ${BUTTONS} để đổi nhân vật", + "pressToSelectProfileText": "nhấn ${BUTTONS} để chọn người chơi", + "pressToSelectTeamText": "nhấn ${BUTTONS} để chọn đội", + "promoCodeWindow": { + "codeText": "Mã code", + "enterText": "Nhập" + }, + "promoSubmitErrorText": "Lỗi gửi code; kiểm tra kết nối mạng", + "ps3ControllersWindow": { + "macInstructionsText": "Tắt nguồn ở mặt sau của PS3, đảm bảo\nBluetooth được bật trên máy Mac của bạn, sau đó kết nối bộ điều khiển của bạn\nđến máy Mac của bạn thông qua cáp USB để ghép nối hai. Từ đó về sau\ncó thể sử dụng nút home của bộ điều khiển để kết nối nó với máy Mac của bạn\nở chế độ có dây (USB) hoặc không dây (Bluetooth).\n\nTrên một số máy Mac, bạn có thể được nhắc nhập mật mã khi ghép nối.\nNếu điều này xảy ra, hãy xem hướng dẫn hoặc google sau đây để được giúp đỡ.\n\n\n\n\nBộ điều khiển PS3 được kết nối không dây sẽ hiển thị trong thiết bị\ndanh sách trong Tùy chọn hệ thống-> Bluetooth. Bạn có thể cần phải loại bỏ chúng\ntừ danh sách đó khi bạn muốn sử dụng lại chúng với PS3.\n\nĐồng thời đảm bảo ngắt kết nối chúng khỏi Bluetooth khi không ở trong\nsử dụng hoặc pin của họ sẽ tiếp tục cạn kiệt.\n\nBluetooth nên xử lý tối đa 7 thiết bị được kết nối,\nmặc dù số dặm của bạn có thể thay đổi.", + "ouyaInstructionsText": "Để sử dụng cần điều khiển PS3 với OUYA, \nchỉ cần kết nối nó với một \ncổng USB để ghép đôi nó. \nLàm điều này có thể ngắt kết nối các cần điều khiển khác của bạn, vì vậy nên khởi động lại OUYA và ngắt cổng kết nối USB.", + "pairingTutorialText": "Đang ghép video hướng dẫn", + "titleText": "Sử dụng cần điều khiển PS3 với ${APP_NAME}:" + }, + "punchBoldText": "ĐẤM", + "punchText": "Đấm", + "purchaseForText": "Mua với ${PRICE}", + "purchaseGameText": "Mua trò chơi", + "purchasingText": "Đang mua...", + "quitGameText": "Rời ${APP_NAME}?", + "quittingIn5SecondsText": "Rời đi trong 5 giây...", + "randomPlayerNamesText": "Tên_Mặcđịnh", + "randomText": "Ngẫu nhiên", + "rankText": "Xếp hạng", + "ratingText": "Đánh giá", + "reachWave2Text": "Tới màn 2 để xếp hạng.", + "readyText": "sẵn sàng", + "recentText": "Gần đây", + "remoteAppInfoShortText": "${APP_NAME} sẽ vui hơn khi chơi cùng gia đình và bạn bè.\nKết nối một hoặc nhiều \ntay cầm phần cứng hoặc tải ứng dụng \n${REMOTE_APP_NAME} trên điện thoại hoặc máy tính bảng để sử dụng chúng như tay cầm.", + "remote_app": { + "app_name": "Điều khiển Bombsquad", + "app_name_short": "Điều khiển BS", + "button_position": "Vị trí Nút", + "button_size": "Kích cỡ Nút", + "cant_resolve_host": "Không thể tìm thấy máy chủ.", + "capturing": "Đang chờ chấp thuận...", + "connected": "Đã kết nối", + "description": "Sử dụng điện thoại của bạn hoặc máy tính bảng như cần điều khiển với Bombsquad.\nTới 8 thiết bị có thể kết nối một lần với trò chơi nội bộ nhiều người chơi điên cuồng trên một màn hình TV hoặc máy tính bảng.", + "disconnected": "Đã bị ngắt kết nối bởi máy chủ.", + "dpad_fixed": "Cố định", + "dpad_floating": "Di chuyển", + "dpad_position": "Vị trí D-pad", + "dpad_size": "Kích cỡ D-pad", + "dpad_type": "Loại D-pad", + "enter_an_address": "Nhập địa chỉ", + "game_full": "Trò chơi đã đủ người chơi hoặc không chấp nhận kết nối.", + "game_shut_down": "Trò chơi đã tắt.", + "hardware_buttons": "Nút Phần cứng", + "join_by_address": "Gia nhập qua địa chỉ...", + "lag": "Trễ: ${SECONDS} giây", + "reset": "Cài lại mặc định", + "run1": "Chạy 1", + "run2": "Chạy 2", + "searching": "Đang tìm trò chơi Bombsquad...", + "searching_caption": "Nhấn vào trò chơi để gia nhập.\nHãy chắc chắn rằng bạn có cùng mạng wifi.", + "start": "Bắt đầu", + "version_mismatch": "Không trùng phiên bản.\nChắc chắn rằng Bombsquad và\nBombsquad Remote là phiên bản mới nhất và thử lại." + }, + "removeInGameAdsText": "Mở khóa \"${PRO}\" trong cửa hàng để loại bỏ quảng cáo.", + "renameText": "Đặt lại tên", + "replayEndText": "Kết thúc phát lại", + "replayNameDefaultText": "Xem lại trò chơi vừa rồi", + "replayReadErrorText": "Lỗi trong lúc đọc tập tin phát lại.", + "replayRenameWarningText": "Đặt tên \"${REPLAY}\" sau trò chơi nếu bạn muốn giữ nó; nếu không nó sẽ bị viết đè.", + "replayVersionErrorText": "Xin lỗi, phát lại này đã được tạo ra\nbởi phiên bản khác của trò chơi và không thể sử dụng.", + "replayWatchText": "Xem phát lại", + "replayWriteErrorText": "Lỗi viết tập tin phát lại.", + "replaysText": "Phát lại", + "reportPlayerExplanationText": "Sử dụng email này để tố cáo gian lận, ngôn ngữ thô tục, hoặc các hành vi xấu khác.\nLàm ơn mô tả dưới đây:", + "reportThisPlayerCheatingText": "Gian lận", + "reportThisPlayerLanguageText": "Ngôn ngữ thô tục", + "reportThisPlayerReasonText": "Lý do tố cáo?", + "reportThisPlayerText": "Tố cáo người chơi này", + "requestingText": "Đang yêu cầu...", + "restartText": "Khởi động lại", + "retryText": "Thử lại", + "revertText": "Hủy bỏ", + "runText": "Chạy", + "saveText": "Lưu", + "scanScriptsErrorText": "Lỗi trong lúc quét mã bản thảo; xem bản thông báo cho chi tiết.", + "scoreChallengesText": "Thử thách Điểm số", + "scoreListUnavailableText": "Danh sách điểm số không có sẵn.", + "scoreText": "Điểm số", + "scoreUnits": { + "millisecondsText": "Mi-li-giây", + "pointsText": "Điểm", + "secondsText": "Giây" + }, + "scoreWasText": "(Cao nhất ${COUNT})", + "selectText": "Chọn", + "seriesWinLine1PlayerText": "ĐÃ THẮNG", + "seriesWinLine1TeamText": "ĐÃ THẮNG", + "seriesWinLine1Text": "ĐÃ THẮNG", + "seriesWinLine2Text": "LOẠT GAME!", + "settingsWindow": { + "accountText": "Tài khoản", + "advancedText": "Nâng cao", + "audioText": "Âm thanh", + "controllersText": "Điều khiển", + "graphicsText": "Đồ họa", + "playerProfilesMovedText": "Ghi chú: Hồ sơ nhân vật đã được chuyển tới cửa sổ Tài khoản ở màn hình chính.", + "titleText": "Cài đặt" + }, + "settingsWindowAdvanced": { + "alwaysUseInternalKeyboardDescriptionText": "(một bàn phím trên màn hình đơn giản, thân thiện với tay cầm cho nhập văn bản)", + "alwaysUseInternalKeyboardText": "Luôn sử dụng bàn phím nội bộ", + "benchmarksText": "Điểm chuẩn & Kiểm tra độ trễ", + "disableCameraGyroscopeMotionText": "Vô hiệu hóa Camera Gyroscope Motion", + "disableCameraShakeText": "Vô hiệu hóa Camera Shake", + "disableThisNotice": "(bạn có thể tắt thông báo này trong cài đặt nâng cao)", + "enablePackageModsDescriptionText": "(thêm tối đa chỗ mod nhưng tắt chơi qua mạng)", + "enablePackageModsText": "Bật Gói Mod Cục bộ", + "enterPromoCodeText": "Nhập mã", + "forTestingText": "Ghi chú: những giá trị này chỉ dành cho kiểm tra và sẽ bị mất khi rời khỏi ứng dụng.", + "helpTranslateText": "Cộng đồng dịch ${APP_NAME} là một cộng đồng được hỗ trợ.\nNếu bạn muốn góp phần hoặc sửa bản dịch,\nđi theo đường dẫn bên dưới.", + "kickIdlePlayersText": "Đuổi người chơi không hoạt động", + "kidFriendlyModeText": "Chế độ thân thiện với trẻ em (giảm bạo lực , vân vân)", + "languageText": "Ngôn ngữ", + "moddingGuideText": "Hướng dẫn mod", + "mustRestartText": "Bạn phải khởi động lại trò chơi để áp dụng cài đặt", + "netTestingText": "Kiểm tra mạng", + "resetText": "Cài lại", + "showBombTrajectoriesText": "Hiển thị quỹ đạo của bom", + "showPlayerNamesText": "Hiển thị tên người chơi", + "showUserModsText": "Hiển thị Thư mục Mod", + "titleText": "Nâng cao", + "translationEditorButtonText": "${APP_NAME} Thiết lập ngôn ngữ", + "translationFetchErrorText": "trạng thái bản dịch không có sẵn", + "translationFetchingStatusText": "kiểm tra trạng thái bản dịch...", + "translationInformMe": "Thông báo tôi khi ngôn ngữ cần được nâng cấp", + "translationNoUpdateNeededText": "Ngôn ngữ hiện tại đã được nâng cấp; quẩy đi!", + "translationUpdateNeededText": "** ngôn ngữ hiện tại cần được nâng cấp!! **", + "vrTestingText": "Thử nghiệm VR" + }, + "shareText": "Chia sẻ", + "sharingText": "Đang chia sẻ...", + "showText": "Xem", + "signInForPromoCodeText": "Bạn phải đăng nhập vào một tài khoản để nhập code", + "signInWithGameCenterText": "Để sử dụng một tài khoản Trung tâm trò chơi,\n đăng nhập với ứng dụng Trung Tâm Trò Chơi", + "singleGamePlaylistNameText": "Chỉ ${GAME}", + "singlePlayerCountText": "1 người chơi", + "soloNameFilterText": "Solo ${NAME}", + "soundtrackTypeNames": { + "CharSelect": "Chọn nhân vật", + "Chosen One": "Người được chọn", + "Epic": "Những trò chơi chế độ chậm", + "Epic Race": "Chạy đua chậm", + "FlagCatcher": "Cướp cờ", + "Flying": "Mảnh đất vui vẻ", + "Football": "Bóng bầu dục", + "ForwardMarch": "Công kích", + "GrandRomp": "Chinh phục", + "Hockey": "Khúc côn cầu", + "Keep Away": "Tránh xa", + "Marching": "Thủ thành", + "Menu": "Màn hình chính", + "Onslaught": "Tử chiến (Máy)", + "Race": "Chạy đua", + "Scary": "Chiếm đồi", + "Scores": "Màn hình điểm số", + "Survival": "Loại trừ", + "ToTheDeath": "Tử chiến", + "Victory": "Màn hình điểm số cuối cùng" + }, + "spaceKeyText": "dấu cách", + "statsText": "Thống kê", + "storagePermissionAccessText": "Yêu cầu truy cập lưu trữ", + "store": { + "alreadyOwnText": "Bạn đã có ${NAME}!", + "bombSquadProNameText": "${APP_NAME} Chuyên nghiệp", + "bombSquadProNewDescriptionText": "• Loại bỏ quảng cáo\n• Mở khóa thêm cài đặt trò chơi\n• Bao gồm cả:", + "buyText": "Mua", + "charactersText": "Nhân Vật", + "comingSoonText": "Sắp có mặt...", + "extrasText": "Nâng cao", + "freeBombSquadProText": "Hiện tại Bombsquad miễn phí, nhưng khi bạn mua nó trước đây thì không. Bây giờ bạn sẽ nhận được Bombsquad Chuyên nghiệp và ${COUNT} vé \nnhư một lời cảm ơn.\nTận hưởng những điều mới, và cảm ơn vì sự hỗ trợ của bạn!\n-Eric", + "holidaySpecialText": "Đặc biệt mùa lễ hội", + "howToSwitchCharactersText": "(đi tới \"${SETTINGS} -> ${PLAYER_PROFILES}\" để thay và chỉnh sửa nhân vật)", + "howToUseIconsText": "(tạo hồ sơ người chơi toàn cầu (trong cửa sổ tài khoản) để sử dụng)", + "howToUseMapsText": "Sử dụng ở danh sách đội/đơn của bạn)", + "iconsText": "Biểu tượng", + "loadErrorText": "Không thể tải trang.\nKiểm tra kết nối mạng của bạn.", + "loadingText": "đang tải", + "mapsText": "Bản đồ", + "miniGamesText": "Trò chơi", + "oneTimeOnlyText": "(một lần duy nhất)", + "purchaseAlreadyInProgressText": "Vật phẩm này đang được mua.", + "purchaseConfirmText": "Mua ${ITEM}?", + "purchaseNotValidError": "Không thể mua.\nLiên hệ ${EMAIL} nếu đây là lỗi.", + "purchaseText": "Mua", + "saleBundleText": "Combo giảm giá!", + "saleExclaimText": "Giảm giá!", + "salePercentText": "(giảm ${PERCENT}%)", + "saleText": "Giảm giá", + "searchText": "Tìm kiếm", + "teamsFreeForAllGamesText": "Trò chơi Đội/Đơn", + "totalWorthText": "*** Đáng giá ${TOTAL_WORTH}! ***", + "upgradeQuestionText": "Nâng cấp?", + "winterSpecialText": "Mùa đông đặc biệt", + "youOwnThisText": "- bạn đã có cái này -" + }, + "storeDescriptionText": "Trò chơi tổ đội 8 người chơi điên cuồng!\n\nThổi tung bạn bè (hoặc máy) trong một giải đấu mini-games toàn chất nổ như Cướp cờ, \n\nkhúc côn cầu và tử chiến tốc độ chậm!\nĐiều khiển đơn giản và hỗ trợ nhiều loại\ncần điều khiển làm cho dễ dàng chơi tới 8 người; bạn còn có thể sử dụng thiết bị di động của bạn như cần điều khiển thông qua ứng dụng miễn phí 'Điều khiển BombSquad'!\nNém bom đi!\nTới www.froemling.net/bombsquad để thêm thông tin.", + "storeDescriptions": { + "blowUpYourFriendsText": "Thổi bay bạn bè của bạn.", + "competeInMiniGamesText": "Cạnh tranh trong các trò chơi khác nhau từ chạy đua tới bay lượn.", + "customize2Text": "Điều chỉnh nhân vật, trò chơi, và thậm chí cả nhạc nền.", + "customizeText": "Tùy chỉnh nhân vật và tạo danh sách trò chơi của riêng bạn.", + "sportsMoreFunText": "Các môn thể thao sẽ vui hơn với chất nổ.", + "teamUpAgainstComputerText": "Lập đội chống lại máy." + }, + "storeText": "Cửa hàng", + "submitText": "Gửi", + "submittingPromoCodeText": "Đang xác nhận mã code...", + "teamNamesColorText": "Tên/Màu sắc đội...", + "telnetAccessGrantedText": "Đã kích hoạt truy cập giao thức.", + "telnetAccessText": "Phát hiện truy cập giao thức; cho phép?", + "testBuildErrorText": "Bản dựng thử nghiệm này không còn hoạt động; Vui lòng kiểm tra phiên bản mới.", + "testBuildText": "Thử nghiệm xây dựng", + "testBuildValidateErrorText": "Không thể xác nhận xây dựng thử nghiệm. (không có kết nối mạng?)", + "testBuildValidatedText": "Kiểm tra bản dựng được xác thực; Tận hưởng!", + "thankYouText": "Cảm ơn vì sự hỗ trợ của bạn! Hãy tận hưởng trò chơi!!", + "threeKillText": "Tam Sát!!", + "timeBonusText": "Thưởng thời gian", + "timeElapsedText": "Thời gian còn lại", + "timeExpiredText": "Hết giờ", + "timeSuffixDaysText": "${COUNT} ngày", + "timeSuffixHoursText": "${COUNT} giờ", + "timeSuffixMinutesText": "${COUNT} phút", + "timeSuffixSecondsText": "${COUNT} giây", + "tipText": "Gợi ý", + "titleText": "Bombsquad", + "titleVRText": "Bombsquad Thực tế ảo", + "topFriendsText": "Bạn bè đứng đầu", + "tournamentCheckingStateText": "Đang kiểm tra trạng thái giải đấu; vui lòng chờ...", + "tournamentEndedText": "Giải đấu này đã kết thúc. Giải đấu mới sẽ bắt đầu sớm.", + "tournamentEntryText": "Lệ phí", + "tournamentResultsRecentText": "Kết quả giải đấu gần đây", + "tournamentStandingsText": "Bảng xếp hạng giải đấu", + "tournamentText": "Giải đấu", + "tournamentTimeExpiredText": "Giải đấu đã hết thời gian.", + "tournamentsText": "Giải đấu", + "translations": { + "characterNames": { + "Agent Johnson": "Điệp Viên Johnson", + "B-9000": "B-9000", + "Bernard": "Bernard", + "Bones": "Xương", + "Butch": "Butch", + "Easter Bunny": "Thỏ Phục Sinh", + "Flopsy": "Flopsy", + "Frosty": "Người Tuyết", + "Gretel": "Gretel", + "Grumbledorf": "Grumbledorf", + "Jack Morgan": "Jack Morgan", + "Kronk": "Kronk", + "Lee": "Lee", + "Lucky": "Lucky", + "Mel": "Mel", + "Middle-Man": "Middle-Man", + "Minimus": "Minimus", + "Pascal": "Pascal", + "Pixel": "Pixel", + "Sammy Slam": "Sammy Slam", + "Santa Claus": "Santa Claus", + "Snake Shadow": "Snake Shadow", + "Spaz": "Spaz", + "Taobao Mascot": "Taobao Mascot", + "Todd McBurton": "Todd McBurton", + "Zoe": "Zoe", + "Zola": "Zola" + }, + "coopLevelNames": { + "${GAME} Training": "${GAME} (Luyện tập)", + "Infinite ${GAME}": "${GAME} (Vô tận)", + "Infinite Onslaught": "Tử chiến (Vô tận)", + "Infinite Runaround": "Thủ thành (Vô tận)", + "Onslaught Training": "Tử chiến (Luyện tập)", + "Pro ${GAME}": "${GAME} Chuyên nghiệp", + "Pro Football": "Bóng bầu dục (Chuyên nghiệp)", + "Pro Onslaught": "Tử chiến (Chuyên nghiệp)", + "Pro Runaround": "Thủ thành (Chuyên nghiệp)", + "Rookie ${GAME}": "${GAME} (Tân binh)", + "Rookie Football": "Bóng bầu dục (Tân binh)", + "Rookie Onslaught": "Tử chiến (Tân Binh)", + "The Last Stand": "Người cuối cùng", + "Uber ${GAME}": "${GAME} (Bậc thầy)", + "Uber Football": "Bóng bầu dục (Bậc thầy)", + "Uber Onslaught": "Tử chiến (Bậc thầy)", + "Uber Runaround": "Thủ thành (Bậc thầy)" + }, + "gameDescriptions": { + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Làm người được chọn trong thời gian nhất định để chiến thắng.\nGiết người được chọn để trở thành họ.", + "Bomb as many targets as you can.": "Dội bom nhiều mục tiêu nhất có thể.", + "Carry the flag for ${ARG1} seconds.": "Cầm cờ trong ${ARG1} giây.", + "Carry the flag for a set length of time.": "Cầm cờ trong thời gian nhất định.", + "Crush ${ARG1} of your enemies.": "Đè bẹp ${ARG1} kẻ địch.", + "Defeat all enemies.": "Đánh bại tất cả kẻ địch.", + "Dodge the falling bombs.": "Tránh những quả Bomb đang rơi.", + "Final glorious epic slow motion battle to the death.": "Cuối cùng vinh quang chiến đấu chậm đến cái chết.", + "Gather eggs!": "Lấy trứng!", + "Get the flag to the enemy end zone.": "Lấy cờ tới khu vực kết thúc của kẻ địch.", + "How fast can you defeat the ninjas?": "Tốc độ tiêu diệt ninja của bạn nhanh bao nhiêu?", + "Kill a set number of enemies to win.": "Giết một số lượng địch nhất định sẽ thắng.", + "Last one standing wins.": "Người cuối cùng còn sống thắng.", + "Last remaining alive wins.": "Người còn sống cuối cùng thắng.", + "Last team standing wins.": "Đội sống sót cuối cùng chiến thắng.", + "Prevent enemies from reaching the exit.": "Chặn địch đi tới lối thoát.", + "Reach the enemy flag to score.": "Chạm cờ địch để ghi điểm.", + "Return the enemy flag to score.": "Lấy cờ địch về căn cứ của bạn để ghi điểm.", + "Run ${ARG1} laps.": "Chạy ${ARG1} vòng.", + "Run ${ARG1} laps. Your entire team has to finish.": "Chạy ${ARG1} vòng. Toàn bộ thành viên của đội phải hoàn thành.", + "Run 1 lap.": "Chạy 1 vòng.", + "Run 1 lap. Your entire team has to finish.": "Chạy 1 vòng. Toàn bộ thành viên của đội phải hoàn thành.", + "Run real fast!": "Chạy nhanh như hack đi! (Cấm hack)", + "Score ${ARG1} goals.": "Ghi ${ARG1} bàn.", + "Score ${ARG1} touchdowns.": "Ghi ${ARG1} bàn.", + "Score a goal.": "Ghi một bàn.", + "Score a touchdown.": "Ghi một bàn.", + "Score some goals.": "Ghi vài bàn thắng.", + "Secure all ${ARG1} flags.": "Chiếm giữ tất cả {ARG1} cờ.", + "Secure all flags on the map to win.": "Chiếm giữ tất cả cờ trên bản đồ để chiến thắng.", + "Secure the flag for ${ARG1} seconds.": "Chiếm giữ cờ trong ${ARG1} giây.", + "Secure the flag for a set length of time.": "Chiếm giữ cờ trong một khoảng thời gian cài sẵn.", + "Steal the enemy flag ${ARG1} times.": "Cướp cờ địch ${ARG1} lần.", + "Steal the enemy flag.": "Cướp cờ địch.", + "There can be only one.": "Chỉ có thể có một.", + "Touch the enemy flag ${ARG1} times.": "Chạm cờ địch ${ARG1} lần.", + "Touch the enemy flag.": "Chạm cờ địch", + "carry the flag for ${ARG1} seconds": "cầm cờ trong ${ARG1} giây.", + "kill ${ARG1} enemies": "giết ${ARG1} địch", + "last one standing wins": "người cuối cùng còn sống thắng", + "last team standing wins": "Đội sống sót cuối cùng chiến thắng.", + "return ${ARG1} flags": "Lấy ${ARG1} cờ", + "return 1 flag": "Lấy 1 lá cờ", + "run ${ARG1} laps": "chạy ${ARG1} vòng", + "run 1 lap": "chạy 1 vòng", + "score ${ARG1} goals": "ghi ${ARG1} bàn", + "score ${ARG1} touchdowns": "ghi ${ARG1} bàn.", + "score a goal": "ghi một bàn.", + "score a touchdown": "ghi một bàn.", + "secure all ${ARG1} flags": "Chiếm giữ tất cả ${ARG1} cờ.", + "secure the flag for ${ARG1} seconds": "bảo vệ cờ trong ${ARG1} giây.", + "touch ${ARG1} flags": "chạm ${ARG1} cờ", + "touch 1 flag": "chạm 1 lá cờ" + }, + "gameNames": { + "Assault": "Chiếm lĩnh", + "Capture the Flag": "Lấy cờ", + "Chosen One": "Người Được Chọn", + "Conquest": "Chiếm Đóng", + "Death Match": "Đối Đầu", + "Easter Egg Hunt": "Săn Trứng", + "Elimination": "Loại Bỏ", + "Football": "Bóng Bầu Dục", + "Hockey": "Hockey", + "Keep Away": "Tránh Xa", + "King of the Hill": "Vua Của Đỉnh Núi", + "Meteor Shower": "Mưa Bom", + "Ninja Fight": "Cuộc đấu của Ninja", + "Onslaught": "Onslaught", + "Race": "Đua", + "Runaround": "Chạy Một Vòng", + "Target Practice": "Luyện Ném", + "The Last Stand": "Người cuối cùng" + }, + "inputDeviceNames": { + "Keyboard": "Bàn Phím S1", + "Keyboard P2": "Bàn Phím S2" + }, + "languages": { + "Arabic": "Tiếng Ả Rập", + "Belarussian": "Tiếng Belarus", + "Chinese": "Tiếng Trung Giản thể", + "ChineseTraditional": "Tiếng Trung Quốc truyền thống", + "Croatian": "Tiếng Croatia", + "Czech": "Tiếng Séc", + "Danish": "Tiếng Đan Mạch", + "Dutch": "Tiếng Hà Lan", + "English": "Tiếng Anh", + "Esperanto": "Quốc tế ngữ", + "Finnish": "Tiếng Phần Lan", + "French": "Tiếng Pháp", + "German": "Tiếng Đức", + "Gibberish": "Tiếng vô nghia", + "Greek": "Tiếng Greeks", + "Hindi": "Tiếng Hin-ddi", + "Hungarian": "Tiếng Hungary", + "Indonesian": "Tiếng Indonesia", + "Italian": "Tiếng Ý", + "Japanese": "Tiếng Nhật", + "Korean": "Tiếng Hàn", + "Persian": "Tiếng Ba Tư", + "Polish": "Tiếng Polish", + "Portuguese": "Tiếng Bồ Đào Nha", + "Romanian": "Tiếng Rumani", + "Russian": "Tiếng Nga", + "Serbian": "Tiếng Serbia", + "Slovak": "Tiếng Slovakia", + "Spanish": "Tiếng Tây Ban Nha", + "Swedish": "Tiếng Thụy Điển", + "Turkish": "Tiếng Thổ Nhĩ Kỳ", + "Ukrainian": "Tiếng Ukraina", + "Vietnamese": "Tiếng Việt" + }, + "leagueNames": { + "Bronze": "Đồng", + "Diamond": "Kim Cương", + "Gold": "Vàng", + "Silver": "Bạc" + }, + "mapsNames": { + "Big G": "Chữ G siêu to khổng lồ", + "Bridgit": "Vực gió hú", + "Courtyard": "Khu đất trống", + "Crag Castle": "Lâu đài cua", + "Doom Shroom": "Cây nấm chết chóc", + "Football Stadium": "Sân thi đấu bóng bầu dục", + "Happy Thoughts": "Mảnh đất vui vẻ", + "Hockey Stadium": "Sân thi đấu khúc khôn cầu", + "Lake Frigid": "Hồ thiên nga", + "Monkey Face": "Mặt khỉ", + "Rampage": "Điên cuồng", + "Roundabout": "Roundabout", + "Step Right Up": "Step Right Up", + "The Pad": "The Pad", + "Tip Top": "Tip Top", + "Tower D": "Tòa tháp D", + "Zigzag": "Zigzag" + }, + "playlistNames": { + "Just Epic": "Chỉ gồm trò chơi chuyển động chậm", + "Just Sports": "Chỉ gồm thể thao" + }, + "scoreNames": { + "Flags": "Số cờ", + "Goals": "Bàn thắng", + "Score": "Điểm số", + "Survived": "Đã sống sót", + "Time": "Thời gian", + "Time Held": "Thời gian tổ chức" + }, + "serverResponses": { + "A code has already been used on this account.": "Một mã code đã được sử dụng trên tài khoản này.", + "A reward has already been given for that address.": "Phần thưởng đã được gửi cho địa chỉ đó.", + "Account linking successful!": "Liên kết tài khoản hoàn tất!", + "Account unlinking successful!": "Hủy liên kết tài khoản hoàn tất!", + "Accounts are already linked.": "Tài khoản đã liên kết.", + "An error has occurred; (${ERROR})": "Lỗi xảy ra; (${ERROR})", + "An error has occurred; please contact support. (${ERROR})": "Một lỗi đã xảy ra; vui lòng liên hệ bộ phận hỗ trợ. (${ERROR})", + "An error has occurred; please contact support@froemling.net.": "Lỗi xảy ra; vui lòng báo cáo tại support@froemling.net.", + "An error has occurred; please try again later.": "Lỗi xảy ra,vui lòng thử lại sau.", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Bạn có muốn liên kết những tài khoản\nnày?\n\n${ACCOUNT1} \n\n${ACCOUNT2}", + "BombSquad Pro unlocked!": "Đã mở khóa Bombsquad Pro", + "Can't link 2 accounts of this type.": "Không thể liên kết 2 tài khoản loại này.", + "Can't link 2 diamond league accounts.": "Không thể liên kết 2 tài khoản ở giải đấu kim cương.", + "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Không thể liên kết; đã vượt qua tối đa ${COUNT} liên kết tài khoản.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Phát hiện gian lận; không được ghi điểm số và nhận giải thưởng trong ${COUNT} ngày.", + "Could not establish a secure connection.": "Không thể thiết lập kết nối an toàn.", + "Daily maximum reached.": "Đã đạt tối đa số lượng hàng ngày.", + "Entering tournament...": "Gia nhập giải đấu...", + "Invalid code.": "Mã code không hợp lệ.", + "Invalid payment; purchase canceled.": "Thành toán không hợp lệ, mua thất bại.", + "Invalid promo code.": "Mã code thưởng không hợp lệ.", + "Invalid purchase.": "Mua sắm không hợp lệ.", + "Invalid tournament entry; score will be ignored.": "Lối vào giải đấu không hợp lệ; điểm số ghi được không công nhận.", + "Item unlocked!": "Vật phẩm đã mở khóa!", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "LIÊN KẾT TỪ CHỐI. ${ACCOUNT} chứa\ndữ liệu quan trọng mà TẤT CẢ ĐƯỢC.\nBạn có thể liên kết theo thứ tự ngược lại nếu bạn muốn\n(và mất dữ liệu của tài khoản NÀY thay thế)", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Liên kết tài khoản ${ACCOUNT} với tài khoản này?\nTất cả dữ liệu tồn tại trên ${ACCOUNT} sẽ bị mất.\nKhông thể hủy. Bạn chắc chứ?", + "Max number of playlists reached.": "Đã đạt tối đa danh sách.", + "Max number of profiles reached.": "Đã đạt tối đa số lượng hồ sơ.", + "Maximum friend code rewards reached.": "Đã đạt tối đa thưởng từ mã code bạn bè.", + "Message is too long.": "Tin nhắn quá dài.", + "Profile \"${NAME}\" upgraded successfully.": "Hồ sơ \"${NAME}\" nâng cấp thành công.", + "Profile could not be upgraded.": "Hồ sơ không thể nâng cấp.", + "Purchase successful!": "Mua thành công!", + "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Nhận được ${COUNT} vé nhờ đăng nhập.\nQuay lại ngày mai để nhận ${TOMORROW_COUNT}.", + "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Chức năng máy chủ không còn hỗ trợ trong phiên bản này của trò chơi;\nVui lòng nâng cấp lên phiên bản mới hơn.", + "Sorry, there are no uses remaining on this code.": "Xin lỗi,không còn lần sử dụng nào trên mã code này.", + "Sorry, this code has already been used.": "Xin lỗi bạn, mã code này đã đc sử dụng", + "Sorry, this code has expired.": "Xin lỗi, mã code này đã hết hạn.", + "Sorry, this code only works for new accounts.": "Xin lỗi,mã code này chỉ dành cho người chơi mới.", + "Temporarily unavailable; please try again later.": "Tạm thời không có; vui lòng thử lại sau.", + "The tournament ended before you finished.": "Giải đấu đã kết thúc trước khi bạn hoàn thành.", + "This account cannot be unlinked for ${NUM} days.": "Tài khoản này không thể ngừng liên kết trong ${NUM} ngày.", + "This code cannot be used on the account that created it.": "Mã code này không thể sử dụng đc ở tài khoản đã tạo ra nó.", + "This requires version ${VERSION} or newer.": "Yêu cầu phiên bản ${VERSION} hoặc mới hơn.", + "Tournaments disabled due to rooted device.": "Giải đấu đã tắt bởi vì thiết bị đã root.", + "Tournaments require ${VERSION} or newer": "Giải đấu yêu cầu phiên bản ${VERSION} hoặc mới hơn.", + "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Ngừng liên kết ${ACCOUNT} từ tài khoản này? \nTất cả dữ liệu trên ${ACCOUNT} sẽ bị cài lại.\n(ngoại trừ thành tựu trong một số trường hợp)", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "Cảnh báo: Khiếu nại về hack đã được đưa ra về tài khoản của bạn.\nTài khoản phát hiện hack sẽ bị cấm chơi vĩnh viễn. Vui lòng chơi trung thực.", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Bạn có muốn lên kết tài khoản thiết bị của bạn với cái này?\n\nTài khoản thiết bị là\n ${ACCOUNT1}\nTài khoản này là ${ACCOUNT2}", + "You already own this!": "Bạn đã có cái này!", + "You can join in ${COUNT} seconds.": "Bạn có thể tham gia trong ${COUNT} giây.", + "You don't have enough tickets for this!": "Bạn không đủ vé để mua!", + "You don't own that.": "Bạn không có cái đó.", + "You got ${COUNT} tickets!": "Bạn đã nhận được ${COUNT} vé!", + "You got a ${ITEM}!": "Bạn đã nhận được một ${ITEM}!", + "You have been promoted to a new league; congratulations!": "Bạn đã được thăng hạng lên một giải đấu mới; xin chúc mừng!", + "You must update to a newer version of the app to do this.": "Bạn phải nâng cấp lên phiên bản mới hơn của ứng dụng để làm điều này.", + "You must update to the newest version of the game to do this.": "Bạn phải nâng cấp trò chơi lên phiên bản mới nhất để làm điều này.", + "You must wait a few seconds before entering a new code.": "Bạn phải chờ một vài giây trước khi nhập một mã mới.", + "You ranked #${RANK} in the last tournament. Thanks for playing!": "Bạn đứng thứ #${RANK} trong giải đấu vừa rồi. Cảm ơn vì đã tham gia!", + "Your account was rejected. Are you signed in?": "Tài khoản của bạn đã bị từ chối. Bạn đã đăng nhập chưa?", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Bản sao của trò chơi đã bị sửa đổi.\nVui lòng sửa lại bất kỳ thay đổi lại như cũ và thử lại.", + "Your friend code was used by ${ACCOUNT}": "Mã của bạn bè của bạn đã được dùng bởi ${ACCOUNT}" + }, + "settingNames": { + "1 Minute": "1 Phút", + "1 Second": "1 giây", + "10 Minutes": "10 phút", + "2 Minutes": "2 phút", + "2 Seconds": "2 giây", + "20 Minutes": "20 phút", + "4 Seconds": "4 giây", + "5 Minutes": "5 phút", + "8 Seconds": "8 giây", + "Allow Negative Scores": "Cho phép điểm số gián tiếp", + "Balance Total Lives": "Cân bằng tổng mạng", + "Bomb Spawning": "Xuất hiện bom", + "Chosen One Gets Gloves": "Người được chọn nhận găng tay", + "Chosen One Gets Shield": "Người được chọn nhận khiên", + "Chosen One Time": "Thời gian người được chọn", + "Enable Impact Bombs": "Kích hoạt bom tác động", + "Enable Triple Bombs": "Bật ba bom", + "Entire Team Must Finish": "Toàn đội phải hoàn thành", + "Epic Mode": "Chế độ sử thi", + "Flag Idle Return Time": "Thời gian chờ cờ quay trở lại", + "Flag Touch Return Time": "Thời gian cờ quay trở lại", + "Hold Time": "Thời gian giữ", + "Kills to Win Per Player": "Số mạng để thắng trên mỗi người chơi", + "Laps": "Vòng", + "Lives Per Player": "Mạng mỗi người chơi", + "Long": "Dài", + "Longer": "Dài hơn", + "Mine Spawning": "Xuất hiện mìn", + "No Mines": "Không mìn", + "None": "Không có", + "Normal": "Bình thường", + "Pro Mode": "Chế độ chuyên nghiệp", + "Respawn Times": "Thời gian hồi sinh", + "Score to Win": "Ghi điểm để Chiến thắng", + "Short": "Ngắn", + "Shorter": "Ngắn hơn", + "Solo Mode": "Chế độ đánh đơn", + "Target Count": "Số lượng mục tiêu", + "Time Limit": "Giới hạn thời gian" + }, + "statements": { + "${TEAM} is disqualified because ${PLAYER} left": "${TEAM} đã bị loại bởi vì ${PLAYER} rời đi", + "Killing ${NAME} for skipping part of the track!": "Giết ${NAME} để bỏ qua một phần đường đua!", + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Cảnh báo đến ${NAME}: spam / nút spam khiến bạn bị loại." + }, + "teamNames": { + "Bad Guys": "Kẻ xấu", + "Blue": "Xanh", + "Good Guys": "Người tốt", + "Red": "Đỏ" + }, + "tips": { + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Một cú đấm chạy-nhảy-xoay có thể một phát chết luôn\nvà cho bạn sự ngưỡng mộ từ bạn bè.", + "Always remember to floss.": "Luôn luôn nhớ Vinahouse.", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Tạo hồ sơ người chơi cho bản thân và bạn bè \nvới tên và hình dáng yêu thích thay vì dùng ngẫu nhiên", + "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Hộp nguyền rủa biến bạn trở thành quả bom hẹn giờ.\nThuốc duy nhất là bạn phải ăn hộp máu.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Ngoại hình khác nhau nhưng khả năng là như nhau,\nvì vậy hãy chọn nhân vật giống với tính cách của bạn nhất.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Đừng quá tự tin với chiếc lá chắn của mình; bạn vẫn có thể bị ném xuống vực đó.", + "Don't run all the time. Really. You will fall off cliffs.": "Đừng chạy nhiều quá. Thề . bạn sẽ bị ngã.", + "Don't spin for too long; you'll become dizzy and fall.": "Đừng xoay quá nhiều, bạn sẽ choáng váng và té.", + "Hold any button to run. (Trigger buttons work well if you have them)": "Giữ bất kì nút nào để chạy. (Cần xoay cũng hoạt động nếu bạn có)", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Giữ bất kì nút nào để chạy . bạn sẽ đến được chỗ bạn muốn rất nhanh , \nnhưng khó điều khiển , cẩn thận với vực nhé", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Bom băng yếu,nhưng nó đóng băng \nbất kỳ ai trong tầm nổ, làm chúng có khả năng bị vỡ thành mảnh vụn.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Nếu ai đó nhặt bạn lên, đấm họ và họ sẽ thả ra.\nHoạt động trong cả đời thật.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Nếu bạn không đủ cần điều khiển, tải '${REMOTE_APP_NAME}' \nứng dụng trên thiết bị di động của bạn để sử dụng như cần điều khiển.", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Nếu bạn dính bom dính, \nnhảy vòng quanh và xoay tròn. Bạn có thể hất bay bom, nếu không thì khoảng khắc cuối cùng của bạn sẽ rất thú vị.", + "If you kill an enemy in one hit you get double points for it.": "Nếu bạn giết một phát chết luôn một kẻ địch bạn nhận gấp đôi điểm.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Nếu bạn nhặt một lời nguyền, hi vọng để sinh tồn là\ntìm một túi hồi máu trong một vài giây tiếp theo.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Nếu bạn ở một chỗ,bạn gà.Chạy và né để sinh tồn.", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Nếu bạn thấy rất nhiều người chơi ra vào\n liên tục, bật 'tự-động-đuổi-người-chơi-không-hoạt-động' trong cài đặt trong trường hợp có người quên rời khỏi trò chơi.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Nếu thiết bị của bạn quá nóng hoặc muốn tiết kiệm pin, giảm \"Trực quan\" hoặc \"Độ phân giải\" trong\nCài đặt->Đồ họa", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Nếu tỷ lệ khung hình của bạn hay thay đổi, thử giảm độ phân giải \nhoặc trực quan trong cài đặt đồ họa trò chơi.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "Trong cướp cờ, cờ của bạn phải ở căn cứ để ghi điểm, Nếu đội khác chuẩn bị ghi điểm,\ncướp cờ của họ là một cách tốt để chặn lại.", + "In hockey, you'll maintain more speed if you turn gradually.": "Trong khúc côn cầu,bạn sẽ duy trì tốc độ nếu bạn quay dần.", + "It's easier to win with a friend or two helping.": "Dễ thắng hơn khi có một người bạn hoặc hai người giúp.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Nhảy giống như bạn đang ném để đưa bom lên đến cấp cao nhất.", + "Land-mines are a good way to stop speedy enemies.": "Mìn là một cách để dừng kẻ địch đang chạy.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Rất nhiều thứ có thể cầm lên và ném, \nbao gồm người chơi khác. Ném kẻ địch bay khỏi khu vực chơi là một cách hiệu lực và cảm xúc nhất.", + "No, you can't get up on the ledge. You have to throw bombs.": "Không, bạn không thể lên trên đó. Bạn cần phải ném bom.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Người chơi có thể tham gia và rời đi giữa trận trong đa số trò chơi,\nvà bạn cũng có thể cắm và rút cần điều khiển một cách nhanh chóng.", + "Practice using your momentum to throw bombs more accurately.": "Luyện tập sử dụng quán tính của bạn để ném bom chuẩn xác hơn.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Đấm gây sát thương phụ thuộc vào tốc độ nấm đấm,\nvì vậy thử chạy, nhảy, và xoay như một thằng điên.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Chạy tới chạy lui trước khi ném bom\nđể 'whiplash' nó và ném nó xa hơn.", + "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Hất bay một nhóm kẻ địch bằng cách \ncài một quả bom gần thùng Thuốc-Nổ-Tung.", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Đầu là vùng dễ bị tổn thương nhất, vì vậy một quả bom dính \nvào đầu thường có nghĩa tèo.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Màn này không bao giờ hết, \nnhưng điểm số cao ở đây sẽ giúp bạn nhận được sự tôn trọng khắp thế giới.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Lực ném phụ thuộc vào hướng bạn đang giữ.\nĐể ném thứ gì ngay trước mặt bạn, đừng giữ bất kỳ hướng nào.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Chán nhạc nền? Thay nó với cái của riêng bạn!\nXem Cài đặt->Âm thanh->Nhạc nền", + "Try 'Cooking off' bombs for a second or two before throwing them.": "Thử 'Ngâm' bom một hoặc hai giây trước khi ném chúng.", + "Try tricking enemies into killing eachother or running off cliffs.": "Thử lừa kẻ địch tiêu diệt lẫn nhau hoặc chạy khỏi khu vực chơi.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Sử dụng nút nhặt để lấy cờ < ${PICKUP} >", + "Whip back and forth to get more distance on your throws..": "Đi tới lui để có thêm khoảng cách trên những cú ném của bạn ..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Bạn có thể 'nhắm' cú đấm của bạn bằng cách xoay trái hoặc phải.\nĐiều này rất có ích cho hất kẻ xấu bay khỏi cạnh hoặc ghi bàn trong khúc côn cầu.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Bạn có thể quan sát khi nào quả bom sắp phát nổ nhờ màu sắc của tia lửa từ ngòi nổ của nó:\nVàng..cam..đỏ..Bùm.", + "You can throw bombs higher if you jump just before throwing.": "Bạn có thể ném bom bay cao hơn nếu bạn nhảy chỉ trước khi ném.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Bạn nhận sát thương khi bạn đập đầu vào vật thể,\nvì vậy cố gắng đừng đập đầu vào vật thể.", + "Your punches do much more damage if you are running or spinning.": "Đấm của bạn sẽ nhiều sát thương hơn nếu bạn chạy hoặc xoay" + } + }, + "trophiesRequiredText": "Yêu cầu ít nhất ${NUMBER} cúp.", + "trophiesText": "Cúp", + "trophiesThisSeasonText": "Cúp mùa hiện tại", + "tutorial": { + "cpuBenchmarkText": "Chạy hướng dẫn ở tốc độ lố bịch (chủ yếu kiểm tra tốc độ CPU)", + "phrase01Text": "Chào bạn!", + "phrase02Text": "Chào mừng đến ${APP_NAME}!", + "phrase03Text": "Đây là một vài cách về điều khiển nhân vật của bạn:", + "phrase04Text": "Nhiều thứ trên ${APP_NAME} dựa trên VẬT LÝ HỌC.", + "phrase05Text": "Ví dụ, khi bạn đấm,..", + "phrase06Text": "..sát thương dựa trên tốc độ của bạn.", + "phrase07Text": "Thấy chưa? Chúng ta không di chuyển, nên không làm bị thương ${NAME}.", + "phrase08Text": "Nào, hãy nhảy lên và chạy để có thêm tốc độ.", + "phrase09Text": "À, được hơn rồi đó.", + "phrase10Text": "Chạy cũng rất tốt.", + "phrase11Text": "Giữ bất cứ nút action nào và di chuyển để chạy.", + "phrase12Text": "Để có cú đấm Tuyệt vời và đặc biệt, thử vừa nhảy VÀ vừa chạy.", + "phrase13Text": "Ôi; xin lỗi về chuyện đó ${NAME}.", + "phrase14Text": "Bạn có thể nhặt lên và ném những thứ như cờ.. hoặc ${NAME}.", + "phrase15Text": "Cuối cùng, những trái Bom.", + "phrase16Text": "Luyện tập tạo nên thành công.", + "phrase17Text": "Ối!!! Đó không phải là một cú ném tốt.", + "phrase18Text": "Vừa di chuyển vừa ném giúp bạn ném xa hơn.", + "phrase19Text": "Vừa nhảy vừa ném giúp bạn ném cao hơn.", + "phrase20Text": "\"Whiplast\" còn giúp bạn ném xa hơn nữa.", + "phrase21Text": "Định thời gian ném có hơi khó khăn", + "phrase22Text": "Trượt.", + "phrase23Text": "Thử \"định giờ\" trái bom một hoặc hai giây", + "phrase24Text": "Tuyệt! Quá chính xác.", + "phrase25Text": "Và, chỉ có thế thôi.", + "phrase26Text": "Bây giờ thì tống cổ chúng nào !", + "phrase27Text": "Hãy nhớ buổi luyện tập, và bạn SẼ sống sót trở về!", + "phrase28Text": "...ummm, có thể...", + "phrase29Text": "Chúc may mắn", + "randomName1Text": "Fred", + "randomName2Text": "Harry", + "randomName3Text": "Bill", + "randomName4Text": "Chuck", + "randomName5Text": "Phil", + "skipConfirmText": "Bạn muốn skip cái tutorial? Bấm hoặc ấn để skip.", + "skipVoteCountText": "${COUNT}/${TOTAL} người muốn bỏ qua.", + "skippingText": "Đang bỏ qua...", + "toSkipPressAnythingText": "(chạm hoặc nhấn bất kì đâu để bỏ qua hướng dẩn)" + }, + "twoKillText": "Hai Mạng!", + "unavailableText": "Không Khả Dụng", + "unconfiguredControllerDetectedText": "Tay cầm không cấu hình được phát hiện:", + "unlockThisInTheStoreText": "Nó phải được mở khóa trong cửa hàng.", + "unlockThisProfilesText": "Để tạo nhiều hơn ${NUM} hồ sơ,bạn cần:", + "unlockThisText": "Để mở khóa bạn cần", + "unsupportedHardwareText": "Xin lỗi, phần cứng này không được hỗ trợ bởi bản dựng này của trò chơi.", + "upFirstText": "Game đầu:", + "upNextText": "Trò chơi tiếp theo ${COUNT}:", + "updatingAccountText": "Đang nâng cấp tài khoản của bạn...", + "upgradeText": "Nâng cấp", + "upgradeToPlayText": "Mở khóa \"${PRO}\" trong cửa hàng để chơi.", + "useDefaultText": "Sử dụng mặc định", + "usesExternalControllerText": "Trò chơi này sử dụng tay cầm bên ngoài cho đầu vào.", + "usingItunesText": "Sử dụng Ứng dụng âm nhạc cho nhạc nền", + "validatingTestBuildText": "Kiểm tra xác thực Xây dựng ...", + "victoryText": "Chiến thắng!", + "voteDelayText": "Bạn không thể bắt đầu cuộc bầu chọn khác trong ${NUMBER} giây", + "voteInProgressText": "Một cuộc bầu chọn đang tiến hành.", + "votedAlreadyText": "Bạn đã bầu chọn", + "votesNeededText": "${NUMBER} phiếu cần", + "vsText": "vs.", + "waitingForHostText": "(Đang chờ ${HOST} để tiếp tục)", + "waitingForPlayersText": "Đang chờ người chơi để tham gia...", + "waitingInLineText": "Chờ xếp hàng (tiệc đã đầy) ...", + "watchAVideoText": "Xem Video", + "watchAnAdText": "Xem Quảng cáo", + "watchWindow": { + "deleteConfirmText": "Xóa \"${REPLAY}\"?", + "deleteReplayButtonText": "Xóa\nPhát lại", + "myReplaysText": "Phát lại của tôi", + "noReplaySelectedErrorText": "Không phát lại được chọn", + "playbackSpeedText": "Tốc độ phát lại: ${SPEED}", + "renameReplayButtonText": "Đặt tên\nPhát lại", + "renameReplayText": "Đặt lại tên \"${REPLAY}\" thành:", + "renameText": "Đặt tên", + "replayDeleteErrorText": "Lỗi xóa phát lại.", + "replayNameText": "Đặt tên phát lại", + "replayRenameErrorAlreadyExistsText": "Một phát lại với cái tên đó đã tồn tại.", + "replayRenameErrorInvalidName": "Không thể đổi tên phát lại; tên không hợp lệ.", + "replayRenameErrorText": "Lỗi đổi tên phát lại.", + "sharedReplaysText": "Chia sẻ Phát lại", + "titleText": "Xem", + "watchReplayButtonText": "Xem \nLại" + }, + "waveText": "Vòng", + "wellSureText": "Chắc chắn rồi!", + "wiimoteLicenseWindow": { + "titleText": "DarwiinRemote Bản quyền" + }, + "wiimoteListenWindow": { + "listeningText": "Lắng nghe Wiimote ...", + "pressText": "Press Wiimote buttons 1 and 2 simultaneously.", + "pressText2": "Trên các Wiimote mới hơn có tích hợp Motion Plus, thay vào đó hãy nhấn nút 'đồng bộ hóa' màu đỏ ở mặt sau." + }, + "wiimoteSetupWindow": { + "copyrightText": "Bản quyền của DarwiinRemote", + "listenText": "Lắng nghe", + "macInstructionsText": "Hãy chắc chắn rằng Wii của bạn là tắt và Bluetooth được kích hoạt\ntrên máy Mac của bạn, sau đó nhấn 'Nghe'. Wiimote hỗ trợ có thể\nmột chút flaky, do đó, bạn có thể phải thử một vài lần\ntrước khi bạn nhận được một kết nối.\n\nBluetooth nên xử lý lên đến 7 thiết bị kết nối,\nmặc dù mileage của bạn có thể khác nhau.\n\nBombSquad hỗ trợ Wiimotes gốc, Nunchuk,\nvà bộ điều khiển cổ điển.\nCác mới Wii Remote Plus bây giờ hoạt động quá\nnhưng không phải với tập tin đính kèm", + "thanksText": "Cảm ơn nhóm DarwiinRemote\nĐã làm thứ này có thật.", + "titleText": "Thiết lập Wiimote" + }, + "winsPlayerText": "${NAME} Chiến thắng!", + "winsTeamText": "${NAME} Chiến thắng!", + "winsText": "${NAME} Chiến thắng!", + "worldScoresUnavailableText": "Điểm trên thế giới không có sẵn.", + "worldsBestScoresText": "Điểm số thế giới cao nhất", + "worldsBestTimesText": "Thời gian tốt nhất thế giới", + "xbox360ControllersWindow": { + "getDriverText": "Lấy Driver", + "macInstructions2Text": "Để sử dụng bộ điều khiển không dây, bạn cũng sẽ cần người nhận đó\nđi kèm với các 'Xbox 360 Wireless Controller for Windows'.\nMột người nhận cho phép bạn kết nối bộ điều khiển lên đến 4.\n\nQuan trọng: bộ thu bên thứ 3 sẽ không làm việc với các trình điều khiển này;\nđảm bảo rằng bạn nhận nói 'Microsoft' vào nó, không phải 'Xbox360'.\nMicrosoft không còn bán chúng một cách riêng biệt, do đó bạn sẽ cần để có được\nmột trong những đi kèm với ebay điều khiển hoặc người nào khác tìm.\n\nNếu bạn thấy điều này hữu ích, hãy xem xét một tài trợ cho các\ntrình điều khiển các phát triển tại trang web của mình.", + "macInstructionsText": "Để sử dụng bộ điều khiển Xbox 360, bạn cần phải cài đặt\nTrình điều khiển Mac có sẵn tại liên kết bên dưới.\nNó hoạt động với cả bộ điều khiển có dây và không dây.", + "ouyaInstructionsText": "Sử dụng bộ điều khiển Xbox 360 có dây với BombSquad, chỉ đơn giản là\ncắm vào cổng USB của điện thoại. Bạn có thể sử dụng một USB hub\nđể kết nối nhiều bộ điều khiển.\n\nSử dụng bộ điều khiển không dây, bạn sẽ cần thu không dây\ncó sẵn như là một phần của \"Xbox 360 wireless Controller cho Windows\"\nđóng gói hoặc được bán rời. Mỗi người nhận cắm vào cổng USB và\ncho phép bạn kết nối bộ điều khiển không dây lên đến 4.", + "titleText": "Sử dụng Điều Khiển Xbox 360 với ${APP_NAME}:" + }, + "yesAllowText": "Có, chấp nhận !", + "yourBestScoresText": "Điểm kỉ lục của bạn", + "yourBestTimesText": "Thời gian kỉ lục của bạn" +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/big_g.json b/dist/ba_data/data/maps/big_g.json new file mode 100644 index 0000000..a7de281 --- /dev/null +++ b/dist/ba_data/data/maps/big_g.json @@ -0,0 +1,85 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [-0.4, 2.33, -0.54], "size": [19.12, 10.2, 23.5]} + ], + "ffa_spawn": [ + {"center": [3.14, 1.17, 6.17], "size": [4.74, 1.0, 1.03]}, + {"center": [5.42, 1.18, -0.17], "size": [2.95, 0.62, 0.5]}, + {"center": [-0.37, 2.89, -6.91], "size": [7.58, 0.62, 0.5]}, + {"center": [-2.39, 1.12, -3.42], "size": [2.93, 0.62, 0.98]}, + {"center": [-7.46, 2.86, 4.94], "size": [0.87, 0.62, 2.23]} + ], + "flag": [ + {"center": [7.56, 2.89, -7.21]}, + {"center": [7.7, 1.1, 6.1]}, + {"center": [-8.12, 2.84, 6.1]}, + {"center": [-8.02, 2.84, -6.2]} + ], + "flag_default": [ + {"center": [-7.56, 2.85, 0.09]} + ], + "map_bounds": [ + {"center": [-0.19, 8.76, 0.2], "size": [27.42, 18.47, 22.17]} + ], + "powerup_spawn": [ + {"center": [7.83, 2.12, -0.05]}, + {"center": [-5.19, 1.48, -3.8]}, + {"center": [-8.54, 3.76, -7.28]}, + {"center": [7.37, 3.76, -3.09]}, + {"center": [-8.69, 3.69, 6.63]} + ], + "race_mine": [ + {"center": [-0.06, 1.12, 4.97]}, + {"center": [-0.06, 1.12, 7.0]}, + {"center": [-0.73, 1.12, -2.83]}, + {"center": [-3.29, 1.12, 0.85]}, + {"center": [5.08, 2.85, -5.25]}, + {"center": [6.29, 2.85, -5.25]}, + {"center": [0.97, 2.85, -7.89]}, + {"center": [-2.98, 2.85, -6.24]}, + {"center": [-6.96, 2.85, -2.12]}, + {"center": [-6.87, 2.85, 2.72]} + ], + "race_point": [ + {"center": [2.28, 1.17, 6.02], "size": [0.71, 4.67, 1.32]}, + {"center": [4.85, 1.17, 6.04], "size": [0.39, 4.58, 1.35]}, + {"center": [6.91, 1.17, 1.14], "size": [1.61, 3.52, 0.11]}, + {"center": [2.68, 1.17, 0.77], "size": [0.65, 3.6, 0.11]}, + {"center": [-0.38, 1.23, 1.92], "size": [0.11, 4.25, 0.59]}, + {"center": [-4.37, 1.17, -0.36], "size": [1.63, 4.55, 0.11]}, + {"center": [0.41, 1.17, -3.39], "size": [0.11, 4.95, 1.31]}, + {"center": [4.27, 2.2, -3.34], "size": [0.11, 4.39, 1.2]}, + {"center": [2.55, 2.88, -7.12], "size": [0.11, 5.51, 1.0]}, + {"center": [-4.2, 2.88, -7.11], "size": [0.11, 5.5, 1.03]}, + {"center": [-7.63, 2.88, -3.62], "size": [1.44, 5.16, 0.06]}, + {"center": [-7.54, 2.88, 3.29], "size": [1.67, 5.52, 0.06]} + ], + "shadow_lower_bottom": [ + {"center": [-0.22, 0.29, 2.68]} + ], + "shadow_lower_top": [ + {"center": [-0.22, 0.88, 2.68]} + ], + "shadow_upper_bottom": [ + {"center": [-0.22, 6.31, 2.68]} + ], + "shadow_upper_top": [ + {"center": [-0.22, 9.47, 2.68]} + ], + "spawn": [ + {"center": [7.18, 2.86, -4.41], "size": [0.76, 1.0, 1.82]}, + {"center": [5.88, 1.14, 6.17], "size": [1.82, 1.0, 0.77]} + ], + "spawn_by_flag": [ + {"center": [7.18, 2.86, -4.41], "size": [0.76, 1.0, 1.82]}, + {"center": [5.88, 1.14, 6.17], "size": [1.82, 1.0, 0.77]}, + {"center": [-6.67, 3.55, 5.82], "size": [1.1, 1.0, 1.29]}, + {"center": [-6.84, 3.55, -6.17], "size": [0.82, 1.0, 1.29]} + ], + "tnt": [ + {"center": [-3.4, 2.07, -1.9]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/bridgit.json b/dist/ba_data/data/maps/bridgit.json new file mode 100644 index 0000000..9a30122 --- /dev/null +++ b/dist/ba_data/data/maps/bridgit.json @@ -0,0 +1,95 @@ +{ + "format": ["stdmap", 1], + "globals": { + "ambient_color": [1.1, 1.2, 1.3], + "tint": [1.1, 1.2, 1.3], + "vignette_inner": [0.9, 0.9, 0.93], + "vignette_outer": [0.65, 0.6, 0.55] + }, + "locations": { + "area_of_interest_bounds": [ + {"center": [-0.25, 3.83, -1.53], "size": [19.15, 7.31, 8.44]} + ], + "ffa_spawn": [ + {"center": [-5.87, 3.72, -1.62], "size": [0.94, 1.0, 1.82]}, + {"center": [5.16, 3.76, -1.44], "size": [0.77, 1.0, 1.82]}, + {"center": [-0.43, 3.76, -1.56], "size": [4.03, 1.0, 0.27]} + ], + "flag": [ + {"center": [-7.35, 3.77, -1.62]}, + {"center": [6.89, 3.77, -1.44]} + ], + "flag_default": [ + {"center": [-0.22, 3.8, -1.56]} + ], + "map_bounds": [ + {"center": [-0.19, 7.48, -1.31], "size": [27.42, 18.47, 19.52]} + ], + "powerup_spawn": [ + {"center": [6.83, 4.66, 0.19]}, + {"center": [-7.25, 4.73, 0.25]}, + {"center": [6.83, 4.66, -3.46]}, + {"center": [-7.25, 4.73, -3.4]} + ], + "shadow_lower_bottom": [ + {"center": [-0.22, 2.83, 2.68]} + ], + "shadow_lower_top": [ + {"center": [-0.22, 3.5, 2.68]} + ], + "shadow_upper_bottom": [ + {"center": [-0.22, 6.31, 2.68]} + ], + "shadow_upper_top": [ + {"center": [-0.22, 9.47, 2.68]} + ], + "spawn": [ + {"center": [-5.87, 3.72, -1.62], "size": [0.94, 1.0, 1.82]}, + {"center": [5.16, 3.76, -1.44], "size": [0.77, 1.0, 1.82]} + ] + }, + "name": "Bridgit", + "play_types": ["melee", "team_flag", "keep_away"], + "preview_texture": "bridgitPreview", + "terrain_nodes": [ + { + "collide_model": "bridgitLevelBottom", + "color_texture": "bridgitLevelColor", + "comment": "Top portion of bridge.", + "materials": ["footing"], + "model": "bridgitLevelTop" + }, + { + "color_texture": "bridgitLevelColor", + "comment": "Bottom portion of bridge with no lighting effects.", + "lighting": false, + "model": "bridgitLevelBottom" + }, + { + "background": true, + "color_texture": "natureBackgroundColor", + "comment": "Visible background.", + "lighting": false, + "model": "natureBackground" + }, + { + "background": true, + "color_texture": "model_bg_tex", + "comment": "360 degree bg for vr.", + "lighting": false, + "model": "bg_vr_fill_model", + "vr_only": true + }, + { + "bumper": true, + "collide_model": "railing_collide_model", + "comment": "Invisible railing to help prevent falls.", + "materials": ["railing"] + }, + { + "collide_model": "collide_bg", + "comment": "Collision shape for bg", + "materials": ["footing", "friction@10", "death"] + } + ] +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/courtyard.json b/dist/ba_data/data/maps/courtyard.json new file mode 100644 index 0000000..c485c95 --- /dev/null +++ b/dist/ba_data/data/maps/courtyard.json @@ -0,0 +1,130 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.35, 3.96, -2.18], "size": [16.38, 7.76, 13.39]} + ], + "bot_spawn_bottom": [ + {"center": [-0.06, 2.81, 1.95]} + ], + "bot_spawn_bottom_half_left": [ + {"center": [-2.05, 2.81, 1.95]} + ], + "bot_spawn_bottom_half_right": [ + {"center": [1.86, 2.81, 1.95]} + ], + "bot_spawn_bottom_left": [ + {"center": [-3.68, 2.81, 1.95]} + ], + "bot_spawn_bottom_right": [ + {"center": [3.59, 2.81, 1.95]} + ], + "bot_spawn_left": [ + {"center": [-6.45, 2.81, -2.32]} + ], + "bot_spawn_left_lower": [ + {"center": [-6.45, 2.81, -1.51]} + ], + "bot_spawn_left_lower_more": [ + {"center": [-6.45, 2.81, -0.48]} + ], + "bot_spawn_left_upper": [ + {"center": [-6.45, 2.81, -3.18]} + ], + "bot_spawn_left_upper_more": [ + {"center": [-6.45, 2.81, -4.01]} + ], + "bot_spawn_right": [ + {"center": [6.54, 2.81, -2.32]} + ], + "bot_spawn_right_lower": [ + {"center": [6.54, 2.81, -1.4]} + ], + "bot_spawn_right_lower_more": [ + {"center": [6.54, 2.81, -0.36]} + ], + "bot_spawn_right_upper": [ + {"center": [6.54, 2.81, -3.13]} + ], + "bot_spawn_right_upper_more": [ + {"center": [6.54, 2.81, -3.98]} + ], + "bot_spawn_top": [ + {"center": [-0.06, 2.81, -5.83]} + ], + "bot_spawn_top_half_left": [ + {"center": [-1.49, 2.81, -5.83]} + ], + "bot_spawn_top_half_right": [ + {"center": [1.6, 2.81, -5.83]} + ], + "bot_spawn_top_left": [ + {"center": [-3.12, 2.81, -5.95]} + ], + "bot_spawn_top_right": [ + {"center": [3.4, 2.81, -5.95]} + ], + "bot_spawn_turret_bottom_left": [ + {"center": [-6.13, 3.33, 1.91]} + ], + "bot_spawn_turret_bottom_right": [ + {"center": [6.37, 3.33, 1.8]} + ], + "bot_spawn_turret_top_left": [ + {"center": [-6.13, 3.33, -6.57]} + ], + "bot_spawn_turret_top_middle": [ + {"center": [0.08, 4.27, -8.52]} + ], + "bot_spawn_turret_top_middle_left": [ + {"center": [-1.27, 4.27, -8.52]} + ], + "bot_spawn_turret_top_middle_right": [ + {"center": [1.13, 4.27, -8.52]} + ], + "bot_spawn_turret_top_right": [ + {"center": [6.37, 3.33, -6.6]} + ], + "edge_box": [ + {"center": [0.0, 1.04, -2.14], "size": [12.02, 11.41, 7.81]} + ], + "ffa_spawn": [ + {"center": [-6.23, 3.77, -5.16], "size": [1.48, 1.0, 0.07]}, + {"center": [6.29, 3.77, -4.92], "size": [1.42, 1.0, 0.07]}, + {"center": [-0.02, 4.4, -6.96], "size": [1.51, 1.0, 0.25]}, + {"center": [-0.02, 3.79, 3.45], "size": [4.99, 1.0, 0.15]} + ], + "flag": [ + {"center": [-5.97, 2.82, -2.43]}, + {"center": [5.91, 2.8, -2.22]} + ], + "flag_default": [ + {"center": [0.25, 2.78, -2.64]} + ], + "map_bounds": [ + {"center": [0.26, 4.9, -3.54], "size": [29.24, 14.2, 29.93]} + ], + "powerup_spawn": [ + {"center": [-3.56, 3.17, 0.37]}, + {"center": [3.63, 3.17, 0.41]}, + {"center": [3.63, 3.17, -4.99]}, + {"center": [-3.56, 3.17, -5.02]} + ], + "shadow_lower_bottom": [ + {"center": [0.52, 0.02, 5.34]} + ], + "shadow_lower_top": [ + {"center": [0.52, 1.21, 5.34]} + ], + "shadow_upper_bottom": [ + {"center": [0.52, 6.36, 5.34]} + ], + "shadow_upper_top": [ + {"center": [0.52, 10.12, 5.34]} + ], + "spawn": [ + {"center": [-7.51, 3.8, -2.1], "size": [0.09, 1.0, 2.2]}, + {"center": [7.46, 3.77, -1.84], "size": [0.03, 1.0, 2.22]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/crag_castle.json b/dist/ba_data/data/maps/crag_castle.json new file mode 100644 index 0000000..28cd287 --- /dev/null +++ b/dist/ba_data/data/maps/crag_castle.json @@ -0,0 +1,48 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.7, 6.56, -3.15], "size": [16.74, 14.95, 11.6]} + ], + "ffa_spawn": [ + {"center": [-4.04, 7.55, -3.54], "size": [2.47, 1.16, 0.18]}, + {"center": [5.43, 7.58, -3.5], "size": [2.42, 1.13, 0.18]}, + {"center": [4.86, 9.31, -6.01], "size": [1.62, 1.13, 0.18]}, + {"center": [-3.63, 9.31, -6.01], "size": [1.62, 1.13, 0.18]}, + {"center": [-2.41, 5.93, 0.03], "size": [1.62, 1.13, 0.18]}, + {"center": [3.52, 5.93, 0.03], "size": [1.62, 1.13, 0.18]} + ], + "flag": [ + {"center": [-1.9, 9.36, -6.44]}, + {"center": [3.24, 9.32, -6.39]}, + {"center": [-6.88, 7.48, 0.21]}, + {"center": [8.19, 7.48, 0.15]} + ], + "flag_default": [ + {"center": [0.63, 6.22, -0.04]} + ], + "map_bounds": [ + {"center": [0.48, 9.09, -3.27], "size": [22.96, 9.91, 14.18]} + ], + "powerup_spawn": [ + {"center": [7.92, 7.84, -5.99]}, + {"center": [-0.7, 7.88, -6.07]}, + {"center": [1.86, 7.89, -6.08]}, + {"center": [-6.67, 7.99, -6.12]} + ], + "spawn": [ + {"center": [-5.17, 7.55, -3.54], "size": [1.06, 1.16, 0.18]}, + {"center": [6.2, 7.58, -3.5], "size": [1.01, 1.13, 0.18]} + ], + "spawn_by_flag": [ + {"center": [-2.87, 9.36, -6.04]}, + {"center": [4.31, 9.36, -6.04]}, + {"center": [-6.63, 7.51, -0.59]}, + {"center": [7.87, 7.51, -0.59]} + ], + "tnt": [ + {"center": [-5.04, 10.01, -6.16]}, + {"center": [6.2, 10.01, -6.16]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/doom_shroom.json b/dist/ba_data/data/maps/doom_shroom.json new file mode 100644 index 0000000..539de5e --- /dev/null +++ b/dist/ba_data/data/maps/doom_shroom.json @@ -0,0 +1,46 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.47, 2.32, -3.22], "size": [21.35, 10.26, 14.67]} + ], + "ffa_spawn": [ + {"center": [-5.83, 2.3, -3.45], "size": [1.0, 1.0, 2.68]}, + {"center": [6.5, 2.4, -3.57], "size": [1.0, 1.0, 2.68]}, + {"center": [0.88, 2.31, -0.36], "size": [4.46, 1.0, 0.27]}, + {"center": [0.88, 2.31, -7.12], "size": [4.46, 1.0, 0.27]} + ], + "flag": [ + {"center": [-7.15, 2.25, -3.43]}, + {"center": [8.1, 2.32, -3.55]} + ], + "flag_default": [ + {"center": [0.6, 2.37, -4.24]} + ], + "map_bounds": [ + {"center": [0.46, 1.33, -3.81], "size": [27.75, 14.45, 22.99]} + ], + "powerup_spawn": [ + {"center": [5.18, 4.28, -7.28]}, + {"center": [-3.24, 4.16, -0.32]}, + {"center": [5.08, 4.16, -0.32]}, + {"center": [-3.4, 4.28, -7.43]} + ], + "shadow_lower_bottom": [ + {"center": [0.6, -0.23, 3.37]} + ], + "shadow_lower_top": [ + {"center": [0.6, 0.7, 3.37]} + ], + "shadow_upper_bottom": [ + {"center": [0.6, 5.41, 3.37]} + ], + "shadow_upper_top": [ + {"center": [0.6, 7.89, 3.37]} + ], + "spawn": [ + {"center": [-5.83, 2.3, -3.45], "size": [1.0, 1.0, 2.68]}, + {"center": [6.5, 2.4, -3.57], "size": [1.0, 1.0, 2.68]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/football_stadium.json b/dist/ba_data/data/maps/football_stadium.json new file mode 100644 index 0000000..0441bd5 --- /dev/null +++ b/dist/ba_data/data/maps/football_stadium.json @@ -0,0 +1,42 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.0, 1.19, 0.43], "size": [29.82, 11.57, 18.89]} + ], + "edge_box": [ + {"center": [-0.1, 0.41, 0.43], "size": [22.48, 1.29, 8.99]} + ], + "ffa_spawn": [ + {"center": [-0.08, 0.02, -4.37], "size": [8.9, 1.0, 0.44]}, + {"center": [-0.08, 0.02, 4.08], "size": [8.9, 1.0, 0.44]} + ], + "flag": [ + {"center": [-10.99, 0.06, 0.11]}, + {"center": [11.01, 0.04, 0.11]} + ], + "flag_default": [ + {"center": [-0.1, 0.04, 0.11]} + ], + "goal": [ + {"center": [12.22, 1.0, 0.11], "size": [2.0, 2.0, 12.97]}, + {"center": [-12.16, 1.0, 0.11], "size": [2.0, 2.0, 13.12]} + ], + "map_bounds": [ + {"center": [0.0, 1.19, 0.43], "size": [42.1, 22.81, 29.77]} + ], + "powerup_spawn": [ + {"center": [5.41, 0.95, -5.04]}, + {"center": [-5.56, 0.95, -5.04]}, + {"center": [5.41, 0.95, 5.15]}, + {"center": [-5.74, 0.95, 5.15]} + ], + "spawn": [ + {"center": [-10.04, 0.02, 0.0], "size": [0.5, 1.0, 4.0]}, + {"center": [9.82, 0.01, 0.0], "size": [0.5, 1.0, 4.0]} + ], + "tnt": [ + {"center": [-0.08, 0.95, -0.78]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/happy_thoughts.json b/dist/ba_data/data/maps/happy_thoughts.json new file mode 100644 index 0000000..d6c82ba --- /dev/null +++ b/dist/ba_data/data/maps/happy_thoughts.json @@ -0,0 +1,44 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [-1.05, 12.68, -5.4], "size": [34.46, 20.94, 0.69]} + ], + "ffa_spawn": [ + {"center": [-9.3, 8.01, -5.44], "size": [1.56, 1.45, 0.12]}, + {"center": [7.48, 8.17, -5.61], "size": [1.55, 1.45, 0.04]}, + {"center": [9.56, 11.31, -5.61], "size": [1.34, 1.45, 0.04]}, + {"center": [-11.56, 10.99, -5.61], "size": [1.34, 1.45, 0.04]}, + {"center": [-1.88, 9.46, -5.61], "size": [1.34, 1.45, 0.04]}, + {"center": [-0.49, 5.08, -5.52], "size": [1.88, 1.45, 0.01]} + ], + "flag": [ + {"center": [-11.75, 8.06, -5.52]}, + {"center": [9.84, 8.19, -5.52]}, + {"center": [-0.22, 5.01, -5.52]}, + {"center": [-0.05, 12.73, -5.52]} + ], + "flag_default": [ + {"center": [-0.04, 12.72, -5.52]} + ], + "map_bounds": [ + {"center": [-0.87, 9.21, -5.73], "size": [36.1, 26.2, 7.9]} + ], + "powerup_spawn": [ + {"center": [1.16, 6.75, -5.47]}, + {"center": [-1.9, 10.56, -5.51]}, + {"center": [10.56, 12.25, -5.58]}, + {"center": [-12.34, 12.25, -5.58]} + ], + "spawn": [ + {"center": [-9.3, 8.01, -5.44], "size": [1.56, 1.45, 0.12]}, + {"center": [7.48, 8.17, -5.61], "size": [1.55, 1.45, 0.04]} + ], + "spawn_by_flag": [ + {"center": [-9.3, 8.01, -5.44], "size": [1.56, 1.45, 0.12]}, + {"center": [7.48, 8.17, -5.61], "size": [1.55, 1.45, 0.04]}, + {"center": [-1.46, 5.04, -5.54], "size": [0.95, 0.67, 0.09]}, + {"center": [0.49, 12.74, -5.6], "size": [0.52, 0.52, 0.02]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/hockey_stadium.json b/dist/ba_data/data/maps/hockey_stadium.json new file mode 100644 index 0000000..e5c6e51 --- /dev/null +++ b/dist/ba_data/data/maps/hockey_stadium.json @@ -0,0 +1,39 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.0, 0.8, 0.0], "size": [30.8, 0.6, 13.88]} + ], + "ffa_spawn": [ + {"center": [-0.0, 0.02, -3.82], "size": [7.83, 1.0, 0.16]}, + {"center": [-0.0, 0.02, 3.56], "size": [7.83, 1.0, 0.06]} + ], + "flag": [ + {"center": [-11.22, 0.1, -0.08]}, + {"center": [11.08, 0.04, -0.08]} + ], + "flag_default": [ + {"center": [-0.02, 0.06, -0.08]} + ], + "goal": [ + {"center": [8.45, 1.0, 0.0], "size": [0.43, 1.6, 3.0]}, + {"center": [-8.45, 1.0, 0.0], "size": [0.43, 1.6, 3.0]} + ], + "map_bounds": [ + {"center": [0.0, 0.8, -0.47], "size": [35.16, 12.19, 21.53]} + ], + "powerup_spawn": [ + {"center": [-3.65, 1.08, -4.77]}, + {"center": [-3.65, 1.08, 4.6]}, + {"center": [2.88, 1.08, -4.77]}, + {"center": [2.88, 1.08, 4.6]} + ], + "spawn": [ + {"center": [-6.84, 0.02, 0.0], "size": [1.0, 1.0, 3.0]}, + {"center": [6.86, 0.04, 0.0], "size": [1.0, 1.0, 3.0]} + ], + "tnt": [ + {"center": [-0.06, 1.08, -4.77]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/lake_frigid.json b/dist/ba_data/data/maps/lake_frigid.json new file mode 100644 index 0000000..a4cd90f --- /dev/null +++ b/dist/ba_data/data/maps/lake_frigid.json @@ -0,0 +1,84 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.62, 3.96, -2.49], "size": [20.62, 7.76, 12.33]} + ], + "ffa_spawn": [ + {"center": [-5.78, 2.6, -2.12], "size": [0.49, 1.0, 2.99]}, + {"center": [8.33, 2.56, -2.36], "size": [0.49, 1.0, 2.59]}, + {"center": [-0.02, 2.62, -6.52], "size": [4.45, 1.0, 0.25]}, + {"center": [-0.02, 2.62, 2.15], "size": [4.99, 1.0, 0.15]} + ], + "flag": [ + {"center": [-5.97, 2.61, -2.43]}, + {"center": [7.47, 2.6, -2.22]} + ], + "flag_default": [ + {"center": [0.58, 2.59, -6.08]} + ], + "map_bounds": [ + {"center": [0.67, 6.09, -2.48], "size": [26.78, 12.5, 19.09]} + ], + "powerup_spawn": [ + {"center": [-3.18, 3.17, 1.53]}, + {"center": [3.63, 3.17, 1.56]}, + {"center": [3.63, 3.17, -5.77]}, + {"center": [-3.18, 3.17, -5.81]} + ], + "race_mine": [ + {"center": [-5.3, 2.52, 1.96]}, + {"center": [-5.29, 2.52, -5.87]}, + {"center": [6.49, 2.52, 1.53]}, + {"center": [6.78, 2.52, -4.81]}, + {"center": [1.53, 2.52, -7.24]}, + {"center": [-1.55, 2.52, -6.39]}, + {"center": [-4.36, 2.52, -2.05]}, + {"center": [-0.71, 2.52, -0.13]}, + {"center": [-0.71, 2.52, 1.28]}, + {"center": [-0.71, 2.52, 3.11]}, + {"center": [9.39, 2.52, -1.65]}, + {"center": [5.75, 2.52, -2.3]}, + {"center": [6.21, 2.52, -0.81]}, + {"center": [5.38, 2.52, -4.17]}, + {"center": [1.5, 2.52, -5.82]}, + {"center": [-1.69, 2.52, -5.24]}, + {"center": [-3.87, 2.52, -4.15]}, + {"center": [-7.41, 2.52, -1.5]}, + {"center": [-2.19, 2.52, 1.86]}, + {"center": [8.03, 2.52, -0.01]}, + {"center": [7.38, 2.52, -5.78]}, + {"center": [-4.57, 2.52, -5.03]}, + {"center": [-5.87, 2.52, -0.28]}, + {"center": [2.79, 2.52, -7.9]}, + {"center": [5.82, 2.52, -6.59]}, + {"center": [-3.97, 2.52, -0.04]} + ], + "race_point": [ + {"center": [0.59, 2.54, 1.54], "size": [0.28, 3.95, 2.29]}, + {"center": [4.75, 2.49, 1.1], "size": [0.28, 3.95, 2.39]}, + {"center": [7.45, 2.6, -2.25], "size": [2.17, 3.95, 0.26]}, + {"center": [5.06, 2.49, -5.82], "size": [0.28, 3.95, 2.39]}, + {"center": [0.59, 2.68, -6.17], "size": [0.28, 3.95, 2.16]}, + {"center": [-3.06, 2.49, -6.11], "size": [0.28, 3.95, 2.32]}, + {"center": [-5.81, 2.58, -2.25], "size": [2.04, 3.95, 0.26]}, + {"center": [-2.96, 2.49, 1.36], "size": [0.28, 3.95, 2.53]} + ], + "shadow_lower_bottom": [ + {"center": [0.52, 1.52, 5.34]} + ], + "shadow_lower_top": [ + {"center": [0.52, 2.52, 5.34]} + ], + "shadow_upper_bottom": [ + {"center": [0.52, 4.54, 5.34]} + ], + "shadow_upper_top": [ + {"center": [0.52, 5.92, 5.34]} + ], + "spawn": [ + {"center": [-5.95, 2.52, -2.1], "size": [0.09, 1.0, 2.2]}, + {"center": [8.08, 2.51, -2.36], "size": [0.03, 1.0, 2.22]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/monkey_face.json b/dist/ba_data/data/maps/monkey_face.json new file mode 100644 index 0000000..b2daf11 --- /dev/null +++ b/dist/ba_data/data/maps/monkey_face.json @@ -0,0 +1,46 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [-1.66, 4.13, -1.58], "size": [17.36, 10.49, 12.31]} + ], + "ffa_spawn": [ + {"center": [-8.03, 3.35, -2.54], "size": [0.95, 0.95, 1.18]}, + {"center": [4.73, 3.31, -2.76], "size": [0.93, 1.0, 1.22]}, + {"center": [-1.91, 3.33, -6.57], "size": [4.08, 1.0, 0.29]}, + {"center": [-1.67, 3.33, 2.41], "size": [3.87, 1.0, 0.29]} + ], + "flag": [ + {"center": [-8.97, 3.36, -2.8]}, + {"center": [5.95, 3.35, -2.66]} + ], + "flag_default": [ + {"center": [-1.69, 3.39, -2.24]} + ], + "map_bounds": [ + {"center": [-1.62, 6.83, -2.2], "size": [22.52, 12.21, 15.91]} + ], + "powerup_spawn": [ + {"center": [-6.86, 4.43, -6.59]}, + {"center": [-5.42, 4.23, 2.8]}, + {"center": [3.15, 4.43, -6.59]}, + {"center": [1.83, 4.23, 2.8]} + ], + "shadow_lower_bottom": [ + {"center": [-1.88, 0.99, 5.5]} + ], + "shadow_lower_top": [ + {"center": [-1.88, 2.88, 5.5]} + ], + "shadow_upper_bottom": [ + {"center": [-1.88, 6.17, 5.5]} + ], + "shadow_upper_top": [ + {"center": [-1.88, 10.25, 5.5]} + ], + "spawn": [ + {"center": [-8.03, 3.35, -2.54], "size": [0.95, 0.95, 1.18]}, + {"center": [4.73, 3.31, -2.76], "size": [0.93, 1.0, 1.22]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/rampage.json b/dist/ba_data/data/maps/rampage.json new file mode 100644 index 0000000..5efa306 --- /dev/null +++ b/dist/ba_data/data/maps/rampage.json @@ -0,0 +1,45 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.35, 5.62, -4.07], "size": [19.9, 10.34, 8.16]} + ], + "edge_box": [ + {"center": [0.35, 5.44, -4.1], "size": [12.58, 4.65, 3.61]} + ], + "ffa_spawn": [ + {"center": [0.5, 5.05, -5.79], "size": [6.63, 1.0, 0.34]}, + {"center": [0.5, 5.05, -2.44], "size": [6.63, 1.0, 0.34]} + ], + "flag": [ + {"center": [-5.89, 5.11, -4.25]}, + {"center": [6.7, 5.1, -4.26]} + ], + "flag_default": [ + {"center": [0.32, 5.11, -4.29]} + ], + "map_bounds": [ + {"center": [0.45, 4.9, -3.54], "size": [23.55, 14.2, 12.08]} + ], + "powerup_spawn": [ + {"center": [-2.65, 6.43, -4.23]}, + {"center": [3.54, 6.55, -4.2]} + ], + "shadow_lower_bottom": [ + {"center": [5.58, 3.14, 5.34]} + ], + "shadow_lower_top": [ + {"center": [5.58, 4.32, 5.34]} + ], + "shadow_upper_bottom": [ + {"center": [5.27, 8.43, 5.34]} + ], + "shadow_upper_top": [ + {"center": [5.27, 11.93, 5.34]} + ], + "spawn": [ + {"center": [-4.75, 5.05, -4.25], "size": [0.92, 1.0, 0.52]}, + {"center": [5.84, 5.05, -4.26], "size": [0.92, 1.0, 0.52]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/roundabout.json b/dist/ba_data/data/maps/roundabout.json new file mode 100644 index 0000000..3205bf7 --- /dev/null +++ b/dist/ba_data/data/maps/roundabout.json @@ -0,0 +1,46 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [-1.55, 3.19, -2.41], "size": [11.96, 8.86, 9.53]} + ], + "ffa_spawn": [ + {"center": [-4.06, 3.86, -4.61], "size": [0.94, 1.0, 1.42]}, + {"center": [0.91, 3.85, -4.67], "size": [0.92, 1.0, 1.42]}, + {"center": [-1.5, 1.5, -0.73], "size": [5.73, 1.0, 0.19]} + ], + "flag": [ + {"center": [-3.02, 3.85, -6.7]}, + {"center": [-0.01, 3.83, -6.68]} + ], + "flag_default": [ + {"center": [-1.51, 1.45, -1.44]} + ], + "map_bounds": [ + {"center": [-1.62, 8.76, -2.66], "size": [20.49, 18.92, 13.8]} + ], + "powerup_spawn": [ + {"center": [-6.79, 2.66, 0.01]}, + {"center": [3.61, 2.66, 0.01]} + ], + "shadow_lower_bottom": [ + {"center": [-1.85, 0.63, 2.27]} + ], + "shadow_lower_top": [ + {"center": [-1.85, 1.08, 2.27]} + ], + "shadow_upper_bottom": [ + {"center": [-1.85, 6.05, 2.27]} + ], + "shadow_upper_top": [ + {"center": [-1.85, 9.19, 2.27]} + ], + "spawn": [ + {"center": [-4.06, 3.86, -4.61], "size": [0.94, 1.0, 1.42]}, + {"center": [0.91, 3.85, -4.67], "size": [0.92, 1.0, 1.42]} + ], + "tnt": [ + {"center": [-1.51, 2.46, 0.23]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/step_right_up.json b/dist/ba_data/data/maps/step_right_up.json new file mode 100644 index 0000000..5486d45 --- /dev/null +++ b/dist/ba_data/data/maps/step_right_up.json @@ -0,0 +1,59 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.35, 6.08, -2.27], "size": [22.55, 10.15, 14.66]} + ], + "ffa_spawn": [ + {"center": [-6.99, 5.82, -4.0], "size": [0.41, 1.0, 3.63]}, + {"center": [7.31, 5.87, -4.0], "size": [0.41, 1.0, 3.63]}, + {"center": [2.64, 4.79, -4.0], "size": [0.41, 1.0, 3.63]}, + {"center": [-2.36, 4.79, -4.0], "size": [0.41, 1.0, 3.63]} + ], + "flag": [ + {"center": [-6.01, 5.82, -8.18]}, + {"center": [6.67, 5.82, -0.32]}, + {"center": [-2.11, 4.79, -3.94]}, + {"center": [2.69, 4.79, -3.94]} + ], + "flag_default": [ + {"center": [0.25, 4.16, -3.69]} + ], + "map_bounds": [ + {"center": [0.26, 4.9, -3.54], "size": [29.24, 14.2, 29.93]} + ], + "powerup_spawn": [ + {"center": [-5.25, 4.73, 2.82]}, + {"center": [5.69, 4.73, 2.82]}, + {"center": [7.9, 6.31, -0.72]}, + {"center": [-7.22, 6.31, -7.94]}, + {"center": [-1.83, 5.25, -7.96]}, + {"center": [2.53, 5.25, -0.41]} + ], + "shadow_lower_bottom": [ + {"center": [0.52, 2.6, 5.34]} + ], + "shadow_lower_top": [ + {"center": [0.52, 3.78, 5.34]} + ], + "shadow_upper_bottom": [ + {"center": [0.52, 7.32, 5.34]} + ], + "shadow_upper_top": [ + {"center": [0.52, 11.09, 5.34]} + ], + "spawn": [ + {"center": [-4.27, 5.46, -4.0], "size": [0.41, 1.0, 2.2]}, + {"center": [5.07, 5.44, -4.06], "size": [0.35, 1.0, 2.22]} + ], + "spawn_by_flag": [ + {"center": [-6.66, 5.98, -6.17], "size": [0.75, 1.0, 0.85]}, + {"center": [7.39, 5.98, -1.71], "size": [0.75, 1.0, 0.85]}, + {"center": [-2.11, 4.8, -3.95], "size": [0.75, 1.0, 0.85]}, + {"center": [2.7, 4.8, -3.95], "size": [0.75, 1.0, 0.85]} + ], + "tnt": [ + {"center": [0.26, 4.83, -4.31]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/the_pad.json b/dist/ba_data/data/maps/the_pad.json new file mode 100644 index 0000000..359420d --- /dev/null +++ b/dist/ba_data/data/maps/the_pad.json @@ -0,0 +1,49 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.35, 4.49, -2.52], "size": [16.65, 8.06, 18.5]} + ], + "ffa_spawn": [ + {"center": [-3.81, 4.38, -8.96], "size": [2.37, 1.0, 0.87]}, + {"center": [4.47, 4.41, -9.01], "size": [2.71, 1.0, 0.87]}, + {"center": [6.97, 4.38, -7.42], "size": [0.49, 1.0, 1.6]}, + {"center": [-6.37, 4.38, -7.42], "size": [0.49, 1.0, 1.6]} + ], + "flag": [ + {"center": [-7.03, 4.31, -6.3]}, + {"center": [7.63, 4.37, -6.29]} + ], + "flag_default": [ + {"center": [0.46, 4.38, 3.68]} + ], + "map_bounds": [ + {"center": [0.26, 4.9, -3.54], "size": [29.24, 14.2, 29.93]} + ], + "powerup_spawn": [ + {"center": [-4.17, 5.28, -6.43]}, + {"center": [4.43, 5.34, -6.33]}, + {"center": [-4.2, 5.12, 0.44]}, + {"center": [4.76, 5.12, 0.35]} + ], + "shadow_lower_bottom": [ + {"center": [-0.29, 2.02, 5.34]} + ], + "shadow_lower_top": [ + {"center": [-0.29, 3.21, 5.34]} + ], + "shadow_upper_bottom": [ + {"center": [-0.29, 6.06, 5.34]} + ], + "shadow_upper_top": [ + {"center": [-0.29, 9.83, 5.34]} + ], + "spawn": [ + {"center": [-3.9, 4.38, -8.96], "size": [1.66, 1.0, 0.87]}, + {"center": [4.78, 4.41, -9.01], "size": [1.66, 1.0, 0.87]} + ], + "tnt": [ + {"center": [0.46, 4.04, -6.57]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/tip_top.json b/dist/ba_data/data/maps/tip_top.json new file mode 100644 index 0000000..d25695b --- /dev/null +++ b/dist/ba_data/data/maps/tip_top.json @@ -0,0 +1,49 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [0.0, 7.14, -0.02], "size": [21.13, 4.96, 16.69]} + ], + "ffa_spawn": [ + {"center": [-4.21, 6.97, -3.79], "size": [0.39, 1.16, 0.3]}, + {"center": [7.38, 5.21, -2.79], "size": [1.16, 1.16, 1.16]}, + {"center": [-7.26, 5.46, -3.1], "size": [1.16, 1.16, 1.16]}, + {"center": [0.02, 5.37, 4.08], "size": [1.88, 1.16, 0.2]}, + {"center": [-1.57, 7.04, -0.48], "size": [0.39, 1.16, 0.3]}, + {"center": [1.69, 7.04, -0.48], "size": [0.39, 1.16, 0.3]}, + {"center": [4.4, 6.97, -3.8], "size": [0.39, 1.16, 0.3]} + ], + "flag": [ + {"center": [-7.01, 5.42, -2.72]}, + {"center": [7.17, 5.17, -2.65]} + ], + "flag_default": [ + {"center": [0.07, 8.87, -4.99]} + ], + "map_bounds": [ + {"center": [-0.21, 7.75, -0.38], "size": [23.81, 13.86, 16.38]} + ], + "powerup_spawn": [ + {"center": [1.66, 8.05, -1.22]}, + {"center": [-1.49, 7.91, -1.23]}, + {"center": [2.63, 6.36, 1.4]}, + {"center": [-2.7, 6.36, 1.43]} + ], + "shadow_lower_bottom": [ + {"center": [0.07, 4.0, 6.32]} + ], + "shadow_lower_top": [ + {"center": [0.07, 4.75, 6.32]} + ], + "shadow_upper_bottom": [ + {"center": [0.07, 9.15, 6.32]} + ], + "shadow_upper_top": [ + {"center": [0.07, 13.82, 6.32]} + ], + "spawn": [ + {"center": [-7.26, 5.46, -3.1], "size": [1.16, 1.16, 1.16]}, + {"center": [7.38, 5.21, -2.79], "size": [1.16, 1.16, 1.16]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/tower_d.json b/dist/ba_data/data/maps/tower_d.json new file mode 100644 index 0000000..c1bc74f --- /dev/null +++ b/dist/ba_data/data/maps/tower_d.json @@ -0,0 +1,76 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [-0.47, 2.89, -1.51], "size": [17.9, 6.19, 15.96]} + ], + "b": [ + {"center": [-4.83, 2.58, -2.35], "size": [0.94, 2.75, 7.27]}, + {"center": [4.74, 2.58, -3.34], "size": [0.82, 2.75, 7.22]}, + {"center": [6.42, 2.58, -4.42], "size": [0.83, 4.53, 9.17]}, + {"center": [-6.65, 3.23, -3.06], "size": [0.94, 2.75, 8.93]}, + {"center": [3.54, 2.58, -2.37], "size": [0.63, 2.75, 5.33]}, + {"center": [5.76, 2.58, 1.15], "size": [2.3, 2.18, 0.5]}, + {"center": [-2.87, 2.58, -1.56], "size": [0.94, 2.75, 5.92]}, + {"center": [-0.69, 4.76, -5.32], "size": [24.86, 9.0, 13.43]}, + {"center": [-0.02, 2.86, -0.95], "size": [4.9, 3.04, 6.14]} + ], + "bot_spawn_bottom_left": [ + {"center": [-7.4, 1.62, 5.38], "size": [1.66, 1.0, 0.87]} + ], + "bot_spawn_bottom_right": [ + {"center": [6.49, 1.62, 5.38], "size": [1.66, 1.0, 0.87]} + ], + "bot_spawn_start": [ + {"center": [-9.0, 3.15, 0.31], "size": [1.66, 1.0, 0.87]} + ], + "edge_box": [ + {"center": [-0.65, 3.19, 4.1], "size": [14.24, 1.47, 2.9]}, + {"center": [-0.14, 2.3, 0.35], "size": [1.74, 1.06, 4.91]} + ], + "ffa_spawn": [ + {"center": [0.1, 2.71, -0.36], "size": [1.66, 1.0, 0.87]} + ], + "flag": [ + {"center": [-7.75, 3.14, 0.27]}, + {"center": [6.85, 2.25, 0.38]} + ], + "flag_default": [ + {"center": [0.02, 2.23, -6.31]} + ], + "map_bounds": [ + {"center": [0.26, 4.9, -3.54], "size": [29.24, 14.2, 29.93]} + ], + "powerup_region": [ + {"center": [0.35, 4.04, 3.41], "size": [12.49, 0.38, 1.62]} + ], + "powerup_spawn": [ + {"center": [-4.93, 2.4, 2.88]}, + {"center": [-1.96, 2.4, 3.75]}, + {"center": [1.65, 2.4, 3.75]}, + {"center": [4.4, 2.4, 2.88]} + ], + "score_region": [ + {"center": [8.38, 3.05, 0.51], "size": [1.28, 1.37, 0.69]} + ], + "shadow_lower_bottom": [ + {"center": [-0.29, 0.95, 5.34]} + ], + "shadow_lower_top": [ + {"center": [-0.29, 2.13, 5.34]} + ], + "shadow_upper_bottom": [ + {"center": [-0.45, 6.06, 5.34]} + ], + "shadow_upper_top": [ + {"center": [-0.29, 9.83, 5.34]} + ], + "spawn": [ + {"center": [0.1, 2.71, -0.36], "size": [1.66, 1.0, 0.87]}, + {"center": [0.14, 2.74, -0.34], "size": [1.66, 1.0, 0.87]} + ], + "tnt_loc": [ + {"center": [0.01, 2.78, 3.89]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/data/maps/zig_zag.json b/dist/ba_data/data/maps/zig_zag.json new file mode 100644 index 0000000..407591a --- /dev/null +++ b/dist/ba_data/data/maps/zig_zag.json @@ -0,0 +1,57 @@ +{ + "format": ["stdmap", 1], + "locations": { + "area_of_interest_bounds": [ + {"center": [-1.81, 3.94, -1.61], "size": [23.01, 13.28, 10.01]} + ], + "ffa_spawn": [ + {"center": [-9.52, 4.65, -3.19], "size": [0.85, 1.0, 1.3]}, + {"center": [6.22, 4.63, -3.19], "size": [0.88, 1.0, 1.3]}, + {"center": [-4.43, 3.01, -4.91], "size": [1.53, 1.0, 0.76]}, + {"center": [1.46, 3.01, -4.91], "size": [1.53, 1.0, 0.76]} + ], + "flag": [ + {"center": [-9.97, 4.65, -4.97]}, + {"center": [6.84, 4.65, -4.95]}, + {"center": [1.16, 2.97, -4.89]}, + {"center": [-4.22, 3.01, -4.89]} + ], + "flag_default": [ + {"center": [-1.43, 3.02, 0.88]} + ], + "map_bounds": [ + {"center": [-1.57, 8.76, -1.31], "size": [28.77, 17.65, 19.52]} + ], + "powerup_spawn": [ + {"center": [2.56, 4.37, -4.8]}, + {"center": [-6.02, 4.37, -4.8]}, + {"center": [5.56, 5.38, -4.8]}, + {"center": [-8.79, 5.38, -4.82]} + ], + "shadow_lower_bottom": [ + {"center": [-1.43, 1.68, 4.79]} + ], + "shadow_lower_top": [ + {"center": [-1.43, 2.55, 4.79]} + ], + "shadow_upper_bottom": [ + {"center": [-1.43, 6.8, 4.79]} + ], + "shadow_upper_top": [ + {"center": [-1.43, 8.78, 4.79]} + ], + "spawn": [ + {"center": [-9.52, 4.65, -3.19], "size": [0.85, 1.0, 1.3]}, + {"center": [6.22, 4.63, -3.19], "size": [0.88, 1.0, 1.3]} + ], + "spawn_by_flag": [ + {"center": [-9.52, 4.65, -3.19], "size": [0.85, 1.0, 1.3]}, + {"center": [6.22, 4.63, -3.19], "size": [0.88, 1.0, 1.3]}, + {"center": [1.46, 3.01, -4.91], "size": [1.53, 1.0, 0.76]}, + {"center": [-4.43, 3.01, -4.91], "size": [1.53, 1.0, 0.76]} + ], + "tnt": [ + {"center": [-1.43, 4.05, 0.04]} + ] + } +} \ No newline at end of file diff --git a/dist/ba_data/fonts/fontSmall0.fdata b/dist/ba_data/fonts/fontSmall0.fdata new file mode 100644 index 0000000..b9429c0 Binary files /dev/null and b/dist/ba_data/fonts/fontSmall0.fdata differ diff --git a/dist/ba_data/fonts/fontSmall1.fdata b/dist/ba_data/fonts/fontSmall1.fdata new file mode 100644 index 0000000..a0c2393 Binary files /dev/null and b/dist/ba_data/fonts/fontSmall1.fdata differ diff --git a/dist/ba_data/fonts/fontSmall2.fdata b/dist/ba_data/fonts/fontSmall2.fdata new file mode 100644 index 0000000..5d0330b Binary files /dev/null and b/dist/ba_data/fonts/fontSmall2.fdata differ diff --git a/dist/ba_data/fonts/fontSmall3.fdata b/dist/ba_data/fonts/fontSmall3.fdata new file mode 100644 index 0000000..70ba858 Binary files /dev/null and b/dist/ba_data/fonts/fontSmall3.fdata differ diff --git a/dist/ba_data/fonts/fontSmall4.fdata b/dist/ba_data/fonts/fontSmall4.fdata new file mode 100644 index 0000000..efc89bb Binary files /dev/null and b/dist/ba_data/fonts/fontSmall4.fdata differ diff --git a/dist/ba_data/fonts/fontSmall5.fdata b/dist/ba_data/fonts/fontSmall5.fdata new file mode 100644 index 0000000..884d3c4 Binary files /dev/null and b/dist/ba_data/fonts/fontSmall5.fdata differ diff --git a/dist/ba_data/fonts/fontSmall6.fdata b/dist/ba_data/fonts/fontSmall6.fdata new file mode 100644 index 0000000..1a26a92 Binary files /dev/null and b/dist/ba_data/fonts/fontSmall6.fdata differ diff --git a/dist/ba_data/fonts/fontSmall7.fdata b/dist/ba_data/fonts/fontSmall7.fdata new file mode 100644 index 0000000..a8ae082 Binary files /dev/null and b/dist/ba_data/fonts/fontSmall7.fdata differ diff --git a/dist/ba_data/models/alwaysLandLevelCollide.cob b/dist/ba_data/models/alwaysLandLevelCollide.cob new file mode 100644 index 0000000..12d0dae Binary files /dev/null and b/dist/ba_data/models/alwaysLandLevelCollide.cob differ diff --git a/dist/ba_data/models/bigGBumper.cob b/dist/ba_data/models/bigGBumper.cob new file mode 100644 index 0000000..295e22c Binary files /dev/null and b/dist/ba_data/models/bigGBumper.cob differ diff --git a/dist/ba_data/models/bigGCollide.cob b/dist/ba_data/models/bigGCollide.cob new file mode 100644 index 0000000..95fa187 Binary files /dev/null and b/dist/ba_data/models/bigGCollide.cob differ diff --git a/dist/ba_data/models/bridgitLevelCollide.cob b/dist/ba_data/models/bridgitLevelCollide.cob new file mode 100644 index 0000000..07e9304 Binary files /dev/null and b/dist/ba_data/models/bridgitLevelCollide.cob differ diff --git a/dist/ba_data/models/bridgitLevelRailingCollide.cob b/dist/ba_data/models/bridgitLevelRailingCollide.cob new file mode 100644 index 0000000..e376abf Binary files /dev/null and b/dist/ba_data/models/bridgitLevelRailingCollide.cob differ diff --git a/dist/ba_data/models/courtyardLevelCollide.cob b/dist/ba_data/models/courtyardLevelCollide.cob new file mode 100644 index 0000000..3c01a78 Binary files /dev/null and b/dist/ba_data/models/courtyardLevelCollide.cob differ diff --git a/dist/ba_data/models/courtyardPlayerWall.cob b/dist/ba_data/models/courtyardPlayerWall.cob new file mode 100644 index 0000000..b444f6d Binary files /dev/null and b/dist/ba_data/models/courtyardPlayerWall.cob differ diff --git a/dist/ba_data/models/cragCastleLevelBumper.cob b/dist/ba_data/models/cragCastleLevelBumper.cob new file mode 100644 index 0000000..c8d81aa Binary files /dev/null and b/dist/ba_data/models/cragCastleLevelBumper.cob differ diff --git a/dist/ba_data/models/cragCastleLevelCollide.cob b/dist/ba_data/models/cragCastleLevelCollide.cob new file mode 100644 index 0000000..31c4bf0 Binary files /dev/null and b/dist/ba_data/models/cragCastleLevelCollide.cob differ diff --git a/dist/ba_data/models/doomShroomLevelCollide.cob b/dist/ba_data/models/doomShroomLevelCollide.cob new file mode 100644 index 0000000..1fc6640 Binary files /dev/null and b/dist/ba_data/models/doomShroomLevelCollide.cob differ diff --git a/dist/ba_data/models/doomShroomStemCollide.cob b/dist/ba_data/models/doomShroomStemCollide.cob new file mode 100644 index 0000000..e6da3d1 Binary files /dev/null and b/dist/ba_data/models/doomShroomStemCollide.cob differ diff --git a/dist/ba_data/models/footballStadiumCollide.cob b/dist/ba_data/models/footballStadiumCollide.cob new file mode 100644 index 0000000..92f42c5 Binary files /dev/null and b/dist/ba_data/models/footballStadiumCollide.cob differ diff --git a/dist/ba_data/models/hockeyStadiumCollide.cob b/dist/ba_data/models/hockeyStadiumCollide.cob new file mode 100644 index 0000000..5f8b2f2 Binary files /dev/null and b/dist/ba_data/models/hockeyStadiumCollide.cob differ diff --git a/dist/ba_data/models/lakeFrigidCollide.cob b/dist/ba_data/models/lakeFrigidCollide.cob new file mode 100644 index 0000000..f8eba15 Binary files /dev/null and b/dist/ba_data/models/lakeFrigidCollide.cob differ diff --git a/dist/ba_data/models/monkeyFaceLevelBumper.cob b/dist/ba_data/models/monkeyFaceLevelBumper.cob new file mode 100644 index 0000000..ced1141 Binary files /dev/null and b/dist/ba_data/models/monkeyFaceLevelBumper.cob differ diff --git a/dist/ba_data/models/monkeyFaceLevelCollide.cob b/dist/ba_data/models/monkeyFaceLevelCollide.cob new file mode 100644 index 0000000..175433c Binary files /dev/null and b/dist/ba_data/models/monkeyFaceLevelCollide.cob differ diff --git a/dist/ba_data/models/natureBackgroundCollide.cob b/dist/ba_data/models/natureBackgroundCollide.cob new file mode 100644 index 0000000..570fdd8 Binary files /dev/null and b/dist/ba_data/models/natureBackgroundCollide.cob differ diff --git a/dist/ba_data/models/rampageBumper.cob b/dist/ba_data/models/rampageBumper.cob new file mode 100644 index 0000000..c39698f Binary files /dev/null and b/dist/ba_data/models/rampageBumper.cob differ diff --git a/dist/ba_data/models/rampageLevelCollide.cob b/dist/ba_data/models/rampageLevelCollide.cob new file mode 100644 index 0000000..7dc3431 Binary files /dev/null and b/dist/ba_data/models/rampageLevelCollide.cob differ diff --git a/dist/ba_data/models/roundaboutLevelBumper.cob b/dist/ba_data/models/roundaboutLevelBumper.cob new file mode 100644 index 0000000..a26cd03 Binary files /dev/null and b/dist/ba_data/models/roundaboutLevelBumper.cob differ diff --git a/dist/ba_data/models/roundaboutLevelCollide.cob b/dist/ba_data/models/roundaboutLevelCollide.cob new file mode 100644 index 0000000..e373044 Binary files /dev/null and b/dist/ba_data/models/roundaboutLevelCollide.cob differ diff --git a/dist/ba_data/models/stepRightUpLevelCollide.cob b/dist/ba_data/models/stepRightUpLevelCollide.cob new file mode 100644 index 0000000..4077edf Binary files /dev/null and b/dist/ba_data/models/stepRightUpLevelCollide.cob differ diff --git a/dist/ba_data/models/thePadLevelBumper.cob b/dist/ba_data/models/thePadLevelBumper.cob new file mode 100644 index 0000000..6d7774d Binary files /dev/null and b/dist/ba_data/models/thePadLevelBumper.cob differ diff --git a/dist/ba_data/models/thePadLevelCollide.cob b/dist/ba_data/models/thePadLevelCollide.cob new file mode 100644 index 0000000..a4a5cfd Binary files /dev/null and b/dist/ba_data/models/thePadLevelCollide.cob differ diff --git a/dist/ba_data/models/tipTopLevelBumper.cob b/dist/ba_data/models/tipTopLevelBumper.cob new file mode 100644 index 0000000..109ff12 Binary files /dev/null and b/dist/ba_data/models/tipTopLevelBumper.cob differ diff --git a/dist/ba_data/models/tipTopLevelCollide.cob b/dist/ba_data/models/tipTopLevelCollide.cob new file mode 100644 index 0000000..b0d94dc Binary files /dev/null and b/dist/ba_data/models/tipTopLevelCollide.cob differ diff --git a/dist/ba_data/models/towerDLevelCollide.cob b/dist/ba_data/models/towerDLevelCollide.cob new file mode 100644 index 0000000..0d49496 Binary files /dev/null and b/dist/ba_data/models/towerDLevelCollide.cob differ diff --git a/dist/ba_data/models/towerDPlayerWall.cob b/dist/ba_data/models/towerDPlayerWall.cob new file mode 100644 index 0000000..d563368 Binary files /dev/null and b/dist/ba_data/models/towerDPlayerWall.cob differ diff --git a/dist/ba_data/models/zigZagLevelBumper.cob b/dist/ba_data/models/zigZagLevelBumper.cob new file mode 100644 index 0000000..3dba9c5 Binary files /dev/null and b/dist/ba_data/models/zigZagLevelBumper.cob differ diff --git a/dist/ba_data/models/zigZagLevelCollide.cob b/dist/ba_data/models/zigZagLevelCollide.cob new file mode 100644 index 0000000..3d0ec27 Binary files /dev/null and b/dist/ba_data/models/zigZagLevelCollide.cob differ diff --git a/dist/ba_data/python-site-packages/__pycache__/typing_extensions.cpython-38.opt-1.pyc b/dist/ba_data/python-site-packages/__pycache__/typing_extensions.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7abfad5 Binary files /dev/null and b/dist/ba_data/python-site-packages/__pycache__/typing_extensions.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python-site-packages/typing_extensions.py b/dist/ba_data/python-site-packages/typing_extensions.py new file mode 100644 index 0000000..a6f4281 --- /dev/null +++ b/dist/ba_data/python-site-packages/typing_extensions.py @@ -0,0 +1,2168 @@ +import abc +import collections +import contextlib +import sys +import typing +import collections.abc as collections_abc +import operator + +# These are used by Protocol implementation +# We use internal typing helpers here, but this significantly reduces +# code duplication. (Also this is only until Protocol is in typing.) +from typing import Generic, Callable, TypeVar, Tuple + +# After PEP 560, internal typing API was substantially reworked. +# This is especially important for Protocol class which uses internal APIs +# quite extensivelly. +PEP_560 = sys.version_info[:3] >= (3, 7, 0) + +if PEP_560: + GenericMeta = TypingMeta = type +else: + from typing import GenericMeta, TypingMeta +OLD_GENERICS = False +try: + from typing import _type_vars, _next_in_mro, _type_check +except ImportError: + OLD_GENERICS = True +try: + from typing import _subs_tree # noqa + SUBS_TREE = True +except ImportError: + SUBS_TREE = False +try: + from typing import _tp_cache +except ImportError: + def _tp_cache(x): + return x +try: + from typing import _TypingEllipsis, _TypingEmpty +except ImportError: + class _TypingEllipsis: + pass + + class _TypingEmpty: + pass + + +# The two functions below are copies of typing internal helpers. +# They are needed by _ProtocolMeta + + +def _no_slots_copy(dct): + dict_copy = dict(dct) + if '__slots__' in dict_copy: + for slot in dict_copy['__slots__']: + dict_copy.pop(slot, None) + return dict_copy + + +def _check_generic(cls, parameters): + if not cls.__parameters__: + raise TypeError("%s is not a generic class" % repr(cls)) + alen = len(parameters) + elen = len(cls.__parameters__) + if alen != elen: + raise TypeError("Too %s parameters for %s; actual %s, expected %s" % + ("many" if alen > elen else "few", repr(cls), alen, elen)) + + +if hasattr(typing, '_generic_new'): + _generic_new = typing._generic_new +else: + # Note: The '_generic_new(...)' function is used as a part of the + # process of creating a generic type and was added to the typing module + # as of Python 3.5.3. + # + # We've defined '_generic_new(...)' below to exactly match the behavior + # implemented in older versions of 'typing' bundled with Python 3.5.0 to + # 3.5.2. This helps eliminate redundancy when defining collection types + # like 'Deque' later. + # + # See https://github.com/python/typing/pull/308 for more details -- in + # particular, compare and contrast the definition of types like + # 'typing.List' before and after the merge. + + def _generic_new(base_cls, cls, *args, **kwargs): + return base_cls.__new__(cls, *args, **kwargs) + +# See https://github.com/python/typing/pull/439 +if hasattr(typing, '_geqv'): + from typing import _geqv + _geqv_defined = True +else: + _geqv = None + _geqv_defined = False + +if sys.version_info[:2] >= (3, 6): + import _collections_abc + _check_methods_in_mro = _collections_abc._check_methods +else: + def _check_methods_in_mro(C, *methods): + mro = C.__mro__ + for method in methods: + for B in mro: + if method in B.__dict__: + if B.__dict__[method] is None: + return NotImplemented + break + else: + return NotImplemented + return True + + +# Please keep __all__ alphabetized within each category. +__all__ = [ + # Super-special typing primitives. + 'ClassVar', + 'Final', + 'Type', + + # ABCs (from collections.abc). + # The following are added depending on presence + # of their non-generic counterparts in stdlib: + # 'Awaitable', + # 'AsyncIterator', + # 'AsyncIterable', + # 'Coroutine', + # 'AsyncGenerator', + # 'AsyncContextManager', + # 'ChainMap', + + # Concrete collection types. + 'ContextManager', + 'Counter', + 'Deque', + 'DefaultDict', + 'TypedDict', + + # Structural checks, a.k.a. protocols. + 'SupportsIndex', + + # One-off things. + 'final', + 'IntVar', + 'Literal', + 'NewType', + 'overload', + 'Text', + 'TYPE_CHECKING', +] + +# Annotated relies on substitution trees of pep 560. It will not work for +# versions of typing older than 3.5.3 +HAVE_ANNOTATED = PEP_560 or SUBS_TREE + +if PEP_560: + __all__.extend(["get_args", "get_origin", "get_type_hints"]) + +if HAVE_ANNOTATED: + __all__.append("Annotated") + +# Protocols are hard to backport to the original version of typing 3.5.0 +HAVE_PROTOCOLS = sys.version_info[:3] != (3, 5, 0) + +if HAVE_PROTOCOLS: + __all__.extend(['Protocol', 'runtime', 'runtime_checkable']) + + +# TODO +if hasattr(typing, 'NoReturn'): + NoReturn = typing.NoReturn +elif hasattr(typing, '_FinalTypingBase'): + class _NoReturn(typing._FinalTypingBase, _root=True): + """Special type indicating functions that never return. + Example:: + + from typing import NoReturn + + def stop() -> NoReturn: + raise Exception('no way') + + This type is invalid in other positions, e.g., ``List[NoReturn]`` + will fail in static type checkers. + """ + __slots__ = () + + def __instancecheck__(self, obj): + raise TypeError("NoReturn cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("NoReturn cannot be used with issubclass().") + + NoReturn = _NoReturn(_root=True) +else: + class _NoReturnMeta(typing.TypingMeta): + """Metaclass for NoReturn""" + def __new__(cls, name, bases, namespace, _root=False): + return super().__new__(cls, name, bases, namespace, _root=_root) + + def __instancecheck__(self, obj): + raise TypeError("NoReturn cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("NoReturn cannot be used with issubclass().") + + class NoReturn(typing.Final, metaclass=_NoReturnMeta, _root=True): + """Special type indicating functions that never return. + Example:: + + from typing import NoReturn + + def stop() -> NoReturn: + raise Exception('no way') + + This type is invalid in other positions, e.g., ``List[NoReturn]`` + will fail in static type checkers. + """ + __slots__ = () + + +# Some unconstrained type variables. These are used by the container types. +# (These are not for export.) +T = typing.TypeVar('T') # Any type. +KT = typing.TypeVar('KT') # Key type. +VT = typing.TypeVar('VT') # Value type. +T_co = typing.TypeVar('T_co', covariant=True) # Any type covariant containers. +V_co = typing.TypeVar('V_co', covariant=True) # Any type covariant containers. +VT_co = typing.TypeVar('VT_co', covariant=True) # Value type covariant containers. +T_contra = typing.TypeVar('T_contra', contravariant=True) # Ditto contravariant. + + +if hasattr(typing, 'ClassVar'): + ClassVar = typing.ClassVar +elif hasattr(typing, '_FinalTypingBase'): + class _ClassVar(typing._FinalTypingBase, _root=True): + """Special type construct to mark class variables. + + An annotation wrapped in ClassVar indicates that a given + attribute is intended to be used as a class variable and + should not be set on instances of that class. Usage:: + + class Starship: + stats: ClassVar[Dict[str, int]] = {} # class variable + damage: int = 10 # instance variable + + ClassVar accepts only types and cannot be further subscribed. + + Note that ClassVar is not a class itself, and should not + be used with isinstance() or issubclass(). + """ + + __slots__ = ('__type__',) + + def __init__(self, tp=None, **kwds): + self.__type__ = tp + + def __getitem__(self, item): + cls = type(self) + if self.__type__ is None: + return cls(typing._type_check(item, + '{} accepts only single type.'.format(cls.__name__[1:])), + _root=True) + raise TypeError('{} cannot be further subscripted' + .format(cls.__name__[1:])) + + def _eval_type(self, globalns, localns): + new_tp = typing._eval_type(self.__type__, globalns, localns) + if new_tp == self.__type__: + return self + return type(self)(new_tp, _root=True) + + def __repr__(self): + r = super().__repr__() + if self.__type__ is not None: + r += '[{}]'.format(typing._type_repr(self.__type__)) + return r + + def __hash__(self): + return hash((type(self).__name__, self.__type__)) + + def __eq__(self, other): + if not isinstance(other, _ClassVar): + return NotImplemented + if self.__type__ is not None: + return self.__type__ == other.__type__ + return self is other + + ClassVar = _ClassVar(_root=True) +else: + class _ClassVarMeta(typing.TypingMeta): + """Metaclass for ClassVar""" + + def __new__(cls, name, bases, namespace, tp=None, _root=False): + self = super().__new__(cls, name, bases, namespace, _root=_root) + if tp is not None: + self.__type__ = tp + return self + + def __instancecheck__(self, obj): + raise TypeError("ClassVar cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("ClassVar cannot be used with issubclass().") + + def __getitem__(self, item): + cls = type(self) + if self.__type__ is not None: + raise TypeError('{} cannot be further subscripted' + .format(cls.__name__[1:])) + + param = typing._type_check( + item, + '{} accepts only single type.'.format(cls.__name__[1:])) + return cls(self.__name__, self.__bases__, + dict(self.__dict__), tp=param, _root=True) + + def _eval_type(self, globalns, localns): + new_tp = typing._eval_type(self.__type__, globalns, localns) + if new_tp == self.__type__: + return self + return type(self)(self.__name__, self.__bases__, + dict(self.__dict__), tp=self.__type__, + _root=True) + + def __repr__(self): + r = super().__repr__() + if self.__type__ is not None: + r += '[{}]'.format(typing._type_repr(self.__type__)) + return r + + def __hash__(self): + return hash((type(self).__name__, self.__type__)) + + def __eq__(self, other): + if not isinstance(other, ClassVar): + return NotImplemented + if self.__type__ is not None: + return self.__type__ == other.__type__ + return self is other + + class ClassVar(typing.Final, metaclass=_ClassVarMeta, _root=True): + """Special type construct to mark class variables. + + An annotation wrapped in ClassVar indicates that a given + attribute is intended to be used as a class variable and + should not be set on instances of that class. Usage:: + + class Starship: + stats: ClassVar[Dict[str, int]] = {} # class variable + damage: int = 10 # instance variable + + ClassVar accepts only types and cannot be further subscribed. + + Note that ClassVar is not a class itself, and should not + be used with isinstance() or issubclass(). + """ + + __type__ = None + +# On older versions of typing there is an internal class named "Final". +if hasattr(typing, 'Final') and sys.version_info[:2] >= (3, 7): + Final = typing.Final +elif sys.version_info[:2] >= (3, 7): + class _FinalForm(typing._SpecialForm, _root=True): + + def __repr__(self): + return 'typing_extensions.' + self._name + + def __getitem__(self, parameters): + item = typing._type_check(parameters, + '{} accepts only single type'.format(self._name)) + return _GenericAlias(self, (item,)) + + Final = _FinalForm('Final', + doc="""A special typing construct to indicate that a name + cannot be re-assigned or overridden in a subclass. + For example: + + MAX_SIZE: Final = 9000 + MAX_SIZE += 1 # Error reported by type checker + + class Connection: + TIMEOUT: Final[int] = 10 + class FastConnector(Connection): + TIMEOUT = 1 # Error reported by type checker + + There is no runtime checking of these properties.""") +elif hasattr(typing, '_FinalTypingBase'): + class _Final(typing._FinalTypingBase, _root=True): + """A special typing construct to indicate that a name + cannot be re-assigned or overridden in a subclass. + For example: + + MAX_SIZE: Final = 9000 + MAX_SIZE += 1 # Error reported by type checker + + class Connection: + TIMEOUT: Final[int] = 10 + class FastConnector(Connection): + TIMEOUT = 1 # Error reported by type checker + + There is no runtime checking of these properties. + """ + + __slots__ = ('__type__',) + + def __init__(self, tp=None, **kwds): + self.__type__ = tp + + def __getitem__(self, item): + cls = type(self) + if self.__type__ is None: + return cls(typing._type_check(item, + '{} accepts only single type.'.format(cls.__name__[1:])), + _root=True) + raise TypeError('{} cannot be further subscripted' + .format(cls.__name__[1:])) + + def _eval_type(self, globalns, localns): + new_tp = typing._eval_type(self.__type__, globalns, localns) + if new_tp == self.__type__: + return self + return type(self)(new_tp, _root=True) + + def __repr__(self): + r = super().__repr__() + if self.__type__ is not None: + r += '[{}]'.format(typing._type_repr(self.__type__)) + return r + + def __hash__(self): + return hash((type(self).__name__, self.__type__)) + + def __eq__(self, other): + if not isinstance(other, _Final): + return NotImplemented + if self.__type__ is not None: + return self.__type__ == other.__type__ + return self is other + + Final = _Final(_root=True) +else: + class _FinalMeta(typing.TypingMeta): + """Metaclass for Final""" + + def __new__(cls, name, bases, namespace, tp=None, _root=False): + self = super().__new__(cls, name, bases, namespace, _root=_root) + if tp is not None: + self.__type__ = tp + return self + + def __instancecheck__(self, obj): + raise TypeError("Final cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("Final cannot be used with issubclass().") + + def __getitem__(self, item): + cls = type(self) + if self.__type__ is not None: + raise TypeError('{} cannot be further subscripted' + .format(cls.__name__[1:])) + + param = typing._type_check( + item, + '{} accepts only single type.'.format(cls.__name__[1:])) + return cls(self.__name__, self.__bases__, + dict(self.__dict__), tp=param, _root=True) + + def _eval_type(self, globalns, localns): + new_tp = typing._eval_type(self.__type__, globalns, localns) + if new_tp == self.__type__: + return self + return type(self)(self.__name__, self.__bases__, + dict(self.__dict__), tp=self.__type__, + _root=True) + + def __repr__(self): + r = super().__repr__() + if self.__type__ is not None: + r += '[{}]'.format(typing._type_repr(self.__type__)) + return r + + def __hash__(self): + return hash((type(self).__name__, self.__type__)) + + def __eq__(self, other): + if not isinstance(other, Final): + return NotImplemented + if self.__type__ is not None: + return self.__type__ == other.__type__ + return self is other + + class Final(typing.Final, metaclass=_FinalMeta, _root=True): + """A special typing construct to indicate that a name + cannot be re-assigned or overridden in a subclass. + For example: + + MAX_SIZE: Final = 9000 + MAX_SIZE += 1 # Error reported by type checker + + class Connection: + TIMEOUT: Final[int] = 10 + class FastConnector(Connection): + TIMEOUT = 1 # Error reported by type checker + + There is no runtime checking of these properties. + """ + + __type__ = None + + +if hasattr(typing, 'final'): + final = typing.final +else: + def final(f): + """This decorator can be used to indicate to type checkers that + the decorated method cannot be overridden, and decorated class + cannot be subclassed. For example: + + class Base: + @final + def done(self) -> None: + ... + class Sub(Base): + def done(self) -> None: # Error reported by type checker + ... + @final + class Leaf: + ... + class Other(Leaf): # Error reported by type checker + ... + + There is no runtime checking of these properties. + """ + return f + + +def IntVar(name): + return TypeVar(name) + + +if hasattr(typing, 'Literal'): + Literal = typing.Literal +elif sys.version_info[:2] >= (3, 7): + class _LiteralForm(typing._SpecialForm, _root=True): + + def __repr__(self): + return 'typing_extensions.' + self._name + + def __getitem__(self, parameters): + return _GenericAlias(self, parameters) + + Literal = _LiteralForm('Literal', + doc="""A type that can be used to indicate to type checkers + that the corresponding value has a value literally equivalent + to the provided parameter. For example: + + var: Literal[4] = 4 + + The type checker understands that 'var' is literally equal to + the value 4 and no other value. + + Literal[...] cannot be subclassed. There is no runtime + checking verifying that the parameter is actually a value + instead of a type.""") +elif hasattr(typing, '_FinalTypingBase'): + class _Literal(typing._FinalTypingBase, _root=True): + """A type that can be used to indicate to type checkers that the + corresponding value has a value literally equivalent to the + provided parameter. For example: + + var: Literal[4] = 4 + + The type checker understands that 'var' is literally equal to the + value 4 and no other value. + + Literal[...] cannot be subclassed. There is no runtime checking + verifying that the parameter is actually a value instead of a type. + """ + + __slots__ = ('__values__',) + + def __init__(self, values=None, **kwds): + self.__values__ = values + + def __getitem__(self, values): + cls = type(self) + if self.__values__ is None: + if not isinstance(values, tuple): + values = (values,) + return cls(values, _root=True) + raise TypeError('{} cannot be further subscripted' + .format(cls.__name__[1:])) + + def _eval_type(self, globalns, localns): + return self + + def __repr__(self): + r = super().__repr__() + if self.__values__ is not None: + r += '[{}]'.format(', '.join(map(typing._type_repr, self.__values__))) + return r + + def __hash__(self): + return hash((type(self).__name__, self.__values__)) + + def __eq__(self, other): + if not isinstance(other, _Literal): + return NotImplemented + if self.__values__ is not None: + return self.__values__ == other.__values__ + return self is other + + Literal = _Literal(_root=True) +else: + class _LiteralMeta(typing.TypingMeta): + """Metaclass for Literal""" + + def __new__(cls, name, bases, namespace, values=None, _root=False): + self = super().__new__(cls, name, bases, namespace, _root=_root) + if values is not None: + self.__values__ = values + return self + + def __instancecheck__(self, obj): + raise TypeError("Literal cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("Literal cannot be used with issubclass().") + + def __getitem__(self, item): + cls = type(self) + if self.__values__ is not None: + raise TypeError('{} cannot be further subscripted' + .format(cls.__name__[1:])) + + if not isinstance(item, tuple): + item = (item,) + return cls(self.__name__, self.__bases__, + dict(self.__dict__), values=item, _root=True) + + def _eval_type(self, globalns, localns): + return self + + def __repr__(self): + r = super().__repr__() + if self.__values__ is not None: + r += '[{}]'.format(', '.join(map(typing._type_repr, self.__values__))) + return r + + def __hash__(self): + return hash((type(self).__name__, self.__values__)) + + def __eq__(self, other): + if not isinstance(other, Literal): + return NotImplemented + if self.__values__ is not None: + return self.__values__ == other.__values__ + return self is other + + class Literal(typing.Final, metaclass=_LiteralMeta, _root=True): + """A type that can be used to indicate to type checkers that the + corresponding value has a value literally equivalent to the + provided parameter. For example: + + var: Literal[4] = 4 + + The type checker understands that 'var' is literally equal to the + value 4 and no other value. + + Literal[...] cannot be subclassed. There is no runtime checking + verifying that the parameter is actually a value instead of a type. + """ + + __values__ = None + + +def _overload_dummy(*args, **kwds): + """Helper for @overload to raise when called.""" + raise NotImplementedError( + "You should not call an overloaded function. " + "A series of @overload-decorated functions " + "outside a stub module should always be followed " + "by an implementation that is not @overload-ed.") + + +def overload(func): + """Decorator for overloaded functions/methods. + + In a stub file, place two or more stub definitions for the same + function in a row, each decorated with @overload. For example: + + @overload + def utf8(value: None) -> None: ... + @overload + def utf8(value: bytes) -> bytes: ... + @overload + def utf8(value: str) -> bytes: ... + + In a non-stub file (i.e. a regular .py file), do the same but + follow it with an implementation. The implementation should *not* + be decorated with @overload. For example: + + @overload + def utf8(value: None) -> None: ... + @overload + def utf8(value: bytes) -> bytes: ... + @overload + def utf8(value: str) -> bytes: ... + def utf8(value): + # implementation goes here + """ + return _overload_dummy + + +# This is not a real generic class. Don't use outside annotations. +if hasattr(typing, 'Type'): + Type = typing.Type +else: + # Internal type variable used for Type[]. + CT_co = typing.TypeVar('CT_co', covariant=True, bound=type) + + class Type(typing.Generic[CT_co], extra=type): + """A special construct usable to annotate class objects. + + For example, suppose we have the following classes:: + + class User: ... # Abstract base for User classes + class BasicUser(User): ... + class ProUser(User): ... + class TeamUser(User): ... + + And a function that takes a class argument that's a subclass of + User and returns an instance of the corresponding class:: + + U = TypeVar('U', bound=User) + def new_user(user_class: Type[U]) -> U: + user = user_class() + # (Here we could write the user object to a database) + return user + joe = new_user(BasicUser) + + At this point the type checker knows that joe has type BasicUser. + """ + + __slots__ = () + + +# Various ABCs mimicking those in collections.abc. +# A few are simply re-exported for completeness. + +def _define_guard(type_name): + """ + Returns True if the given type isn't defined in typing but + is defined in collections_abc. + + Adds the type to __all__ if the collection is found in either + typing or collection_abc. + """ + if hasattr(typing, type_name): + __all__.append(type_name) + globals()[type_name] = getattr(typing, type_name) + return False + elif hasattr(collections_abc, type_name): + __all__.append(type_name) + return True + else: + return False + + +class _ExtensionsGenericMeta(GenericMeta): + def __subclasscheck__(self, subclass): + """This mimics a more modern GenericMeta.__subclasscheck__() logic + (that does not have problems with recursion) to work around interactions + between collections, typing, and typing_extensions on older + versions of Python, see https://github.com/python/typing/issues/501. + """ + if sys.version_info[:3] >= (3, 5, 3) or sys.version_info[:3] < (3, 5, 0): + if self.__origin__ is not None: + if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: + raise TypeError("Parameterized generics cannot be used with class " + "or instance checks") + return False + if not self.__extra__: + return super().__subclasscheck__(subclass) + res = self.__extra__.__subclasshook__(subclass) + if res is not NotImplemented: + return res + if self.__extra__ in subclass.__mro__: + return True + for scls in self.__extra__.__subclasses__(): + if isinstance(scls, GenericMeta): + continue + if issubclass(subclass, scls): + return True + return False + + +if _define_guard('Awaitable'): + class Awaitable(typing.Generic[T_co], metaclass=_ExtensionsGenericMeta, + extra=collections_abc.Awaitable): + __slots__ = () + + +if _define_guard('Coroutine'): + class Coroutine(Awaitable[V_co], typing.Generic[T_co, T_contra, V_co], + metaclass=_ExtensionsGenericMeta, + extra=collections_abc.Coroutine): + __slots__ = () + + +if _define_guard('AsyncIterable'): + class AsyncIterable(typing.Generic[T_co], + metaclass=_ExtensionsGenericMeta, + extra=collections_abc.AsyncIterable): + __slots__ = () + + +if _define_guard('AsyncIterator'): + class AsyncIterator(AsyncIterable[T_co], + metaclass=_ExtensionsGenericMeta, + extra=collections_abc.AsyncIterator): + __slots__ = () + + +if hasattr(typing, 'Deque'): + Deque = typing.Deque +elif _geqv_defined: + class Deque(collections.deque, typing.MutableSequence[T], + metaclass=_ExtensionsGenericMeta, + extra=collections.deque): + __slots__ = () + + def __new__(cls, *args, **kwds): + if _geqv(cls, Deque): + return collections.deque(*args, **kwds) + return _generic_new(collections.deque, cls, *args, **kwds) +else: + class Deque(collections.deque, typing.MutableSequence[T], + metaclass=_ExtensionsGenericMeta, + extra=collections.deque): + __slots__ = () + + def __new__(cls, *args, **kwds): + if cls._gorg is Deque: + return collections.deque(*args, **kwds) + return _generic_new(collections.deque, cls, *args, **kwds) + + +if hasattr(typing, 'ContextManager'): + ContextManager = typing.ContextManager +elif hasattr(contextlib, 'AbstractContextManager'): + class ContextManager(typing.Generic[T_co], + metaclass=_ExtensionsGenericMeta, + extra=contextlib.AbstractContextManager): + __slots__ = () +else: + class ContextManager(typing.Generic[T_co]): + __slots__ = () + + def __enter__(self): + return self + + @abc.abstractmethod + def __exit__(self, exc_type, exc_value, traceback): + return None + + @classmethod + def __subclasshook__(cls, C): + if cls is ContextManager: + # In Python 3.6+, it is possible to set a method to None to + # explicitly indicate that the class does not implement an ABC + # (https://bugs.python.org/issue25958), but we do not support + # that pattern here because this fallback class is only used + # in Python 3.5 and earlier. + if (any("__enter__" in B.__dict__ for B in C.__mro__) and + any("__exit__" in B.__dict__ for B in C.__mro__)): + return True + return NotImplemented + + +if hasattr(typing, 'AsyncContextManager'): + AsyncContextManager = typing.AsyncContextManager + __all__.append('AsyncContextManager') +elif hasattr(contextlib, 'AbstractAsyncContextManager'): + class AsyncContextManager(typing.Generic[T_co], + metaclass=_ExtensionsGenericMeta, + extra=contextlib.AbstractAsyncContextManager): + __slots__ = () + + __all__.append('AsyncContextManager') +elif sys.version_info[:2] >= (3, 5): + exec(""" +class AsyncContextManager(typing.Generic[T_co]): + __slots__ = () + + async def __aenter__(self): + return self + + @abc.abstractmethod + async def __aexit__(self, exc_type, exc_value, traceback): + return None + + @classmethod + def __subclasshook__(cls, C): + if cls is AsyncContextManager: + return _check_methods_in_mro(C, "__aenter__", "__aexit__") + return NotImplemented + +__all__.append('AsyncContextManager') +""") + + +if hasattr(typing, 'DefaultDict'): + DefaultDict = typing.DefaultDict +elif _geqv_defined: + class DefaultDict(collections.defaultdict, typing.MutableMapping[KT, VT], + metaclass=_ExtensionsGenericMeta, + extra=collections.defaultdict): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if _geqv(cls, DefaultDict): + return collections.defaultdict(*args, **kwds) + return _generic_new(collections.defaultdict, cls, *args, **kwds) +else: + class DefaultDict(collections.defaultdict, typing.MutableMapping[KT, VT], + metaclass=_ExtensionsGenericMeta, + extra=collections.defaultdict): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if cls._gorg is DefaultDict: + return collections.defaultdict(*args, **kwds) + return _generic_new(collections.defaultdict, cls, *args, **kwds) + + +if hasattr(typing, 'Counter'): + Counter = typing.Counter +elif (3, 5, 0) <= sys.version_info[:3] <= (3, 5, 1): + assert _geqv_defined + _TInt = typing.TypeVar('_TInt') + + class _CounterMeta(typing.GenericMeta): + """Metaclass for Counter""" + def __getitem__(self, item): + return super().__getitem__((item, int)) + + class Counter(collections.Counter, + typing.Dict[T, int], + metaclass=_CounterMeta, + extra=collections.Counter): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if _geqv(cls, Counter): + return collections.Counter(*args, **kwds) + return _generic_new(collections.Counter, cls, *args, **kwds) + +elif _geqv_defined: + class Counter(collections.Counter, + typing.Dict[T, int], + metaclass=_ExtensionsGenericMeta, extra=collections.Counter): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if _geqv(cls, Counter): + return collections.Counter(*args, **kwds) + return _generic_new(collections.Counter, cls, *args, **kwds) + +else: + class Counter(collections.Counter, + typing.Dict[T, int], + metaclass=_ExtensionsGenericMeta, extra=collections.Counter): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if cls._gorg is Counter: + return collections.Counter(*args, **kwds) + return _generic_new(collections.Counter, cls, *args, **kwds) + + +if hasattr(typing, 'ChainMap'): + ChainMap = typing.ChainMap + __all__.append('ChainMap') +elif hasattr(collections, 'ChainMap'): + # ChainMap only exists in 3.3+ + if _geqv_defined: + class ChainMap(collections.ChainMap, typing.MutableMapping[KT, VT], + metaclass=_ExtensionsGenericMeta, + extra=collections.ChainMap): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if _geqv(cls, ChainMap): + return collections.ChainMap(*args, **kwds) + return _generic_new(collections.ChainMap, cls, *args, **kwds) + else: + class ChainMap(collections.ChainMap, typing.MutableMapping[KT, VT], + metaclass=_ExtensionsGenericMeta, + extra=collections.ChainMap): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if cls._gorg is ChainMap: + return collections.ChainMap(*args, **kwds) + return _generic_new(collections.ChainMap, cls, *args, **kwds) + + __all__.append('ChainMap') + + +if _define_guard('AsyncGenerator'): + class AsyncGenerator(AsyncIterator[T_co], typing.Generic[T_co, T_contra], + metaclass=_ExtensionsGenericMeta, + extra=collections_abc.AsyncGenerator): + __slots__ = () + + +if hasattr(typing, 'NewType'): + NewType = typing.NewType +else: + def NewType(name, tp): + """NewType creates simple unique types with almost zero + runtime overhead. NewType(name, tp) is considered a subtype of tp + by static type checkers. At runtime, NewType(name, tp) returns + a dummy function that simply returns its argument. Usage:: + + UserId = NewType('UserId', int) + + def name_by_id(user_id: UserId) -> str: + ... + + UserId('user') # Fails type check + + name_by_id(42) # Fails type check + name_by_id(UserId(42)) # OK + + num = UserId(5) + 1 # type: int + """ + + def new_type(x): + return x + + new_type.__name__ = name + new_type.__supertype__ = tp + return new_type + + +if hasattr(typing, 'Text'): + Text = typing.Text +else: + Text = str + + +if hasattr(typing, 'TYPE_CHECKING'): + TYPE_CHECKING = typing.TYPE_CHECKING +else: + # Constant that's True when type checking, but False here. + TYPE_CHECKING = False + + +def _gorg(cls): + """This function exists for compatibility with old typing versions.""" + assert isinstance(cls, GenericMeta) + if hasattr(cls, '_gorg'): + return cls._gorg + while cls.__origin__ is not None: + cls = cls.__origin__ + return cls + + +if OLD_GENERICS: + def _next_in_mro(cls): # noqa + """This function exists for compatibility with old typing versions.""" + next_in_mro = object + for i, c in enumerate(cls.__mro__[:-1]): + if isinstance(c, GenericMeta) and _gorg(c) is Generic: + next_in_mro = cls.__mro__[i + 1] + return next_in_mro + + +_PROTO_WHITELIST = ['Callable', 'Awaitable', + 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator', + 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', + 'ContextManager', 'AsyncContextManager'] + + +def _get_protocol_attrs(cls): + attrs = set() + for base in cls.__mro__[:-1]: # without object + if base.__name__ in ('Protocol', 'Generic'): + continue + annotations = getattr(base, '__annotations__', {}) + for attr in list(base.__dict__.keys()) + list(annotations.keys()): + if (not attr.startswith('_abc_') and attr not in ( + '__abstractmethods__', '__annotations__', '__weakref__', + '_is_protocol', '_is_runtime_protocol', '__dict__', + '__args__', '__slots__', + '__next_in_mro__', '__parameters__', '__origin__', + '__orig_bases__', '__extra__', '__tree_hash__', + '__doc__', '__subclasshook__', '__init__', '__new__', + '__module__', '_MutableMapping__marker', '_gorg')): + attrs.add(attr) + return attrs + + +def _is_callable_members_only(cls): + return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls)) + + +if hasattr(typing, 'Protocol'): + Protocol = typing.Protocol +elif HAVE_PROTOCOLS and not PEP_560: + class _ProtocolMeta(GenericMeta): + """Internal metaclass for Protocol. + + This exists so Protocol classes can be generic without deriving + from Generic. + """ + if not OLD_GENERICS: + def __new__(cls, name, bases, namespace, + tvars=None, args=None, origin=None, extra=None, orig_bases=None): + # This is just a version copied from GenericMeta.__new__ that + # includes "Protocol" special treatment. (Comments removed for brevity.) + assert extra is None # Protocols should not have extra + if tvars is not None: + assert origin is not None + assert all(isinstance(t, TypeVar) for t in tvars), tvars + else: + tvars = _type_vars(bases) + gvars = None + for base in bases: + if base is Generic: + raise TypeError("Cannot inherit from plain Generic") + if (isinstance(base, GenericMeta) and + base.__origin__ in (Generic, Protocol)): + if gvars is not None: + raise TypeError( + "Cannot inherit from Generic[...] or" + " Protocol[...] multiple times.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + raise TypeError( + "Some type variables (%s) " + "are not listed in %s[%s]" % + (", ".join(str(t) for t in tvars if t not in gvarset), + "Generic" if any(b.__origin__ is Generic + for b in bases) else "Protocol", + ", ".join(str(g) for g in gvars))) + tvars = gvars + + initial_bases = bases + if (extra is not None and type(extra) is abc.ABCMeta and + extra not in bases): + bases = (extra,) + bases + bases = tuple(_gorg(b) if isinstance(b, GenericMeta) else b + for b in bases) + if any(isinstance(b, GenericMeta) and b is not Generic for b in bases): + bases = tuple(b for b in bases if b is not Generic) + namespace.update({'__origin__': origin, '__extra__': extra}) + self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, + _root=True) + super(GenericMeta, self).__setattr__('_gorg', + self if not origin else + _gorg(origin)) + self.__parameters__ = tvars + self.__args__ = tuple(... if a is _TypingEllipsis else + () if a is _TypingEmpty else + a for a in args) if args else None + self.__next_in_mro__ = _next_in_mro(self) + if orig_bases is None: + self.__orig_bases__ = initial_bases + elif origin is not None: + self._abc_registry = origin._abc_registry + self._abc_cache = origin._abc_cache + if hasattr(self, '_subs_tree'): + self.__tree_hash__ = (hash(self._subs_tree()) if origin else + super(GenericMeta, self).__hash__()) + return self + + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + if not cls.__dict__.get('_is_protocol', None): + cls._is_protocol = any(b is Protocol or + isinstance(b, _ProtocolMeta) and + b.__origin__ is Protocol + for b in cls.__bases__) + if cls._is_protocol: + for base in cls.__mro__[1:]: + if not (base in (object, Generic) or + base.__module__ == 'collections.abc' and + base.__name__ in _PROTO_WHITELIST or + isinstance(base, TypingMeta) and base._is_protocol or + isinstance(base, GenericMeta) and + base.__origin__ is Generic): + raise TypeError('Protocols can only inherit from other' + ' protocols, got %r' % base) + + def _no_init(self, *args, **kwargs): + if type(self)._is_protocol: + raise TypeError('Protocols cannot be instantiated') + cls.__init__ = _no_init + + def _proto_hook(other): + if not cls.__dict__.get('_is_protocol', None): + return NotImplemented + if not isinstance(other, type): + # Same error as for issubclass(1, int) + raise TypeError('issubclass() arg 1 must be a class') + for attr in _get_protocol_attrs(cls): + for base in other.__mro__: + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + annotations = getattr(base, '__annotations__', {}) + if (isinstance(annotations, typing.Mapping) and + attr in annotations and + isinstance(other, _ProtocolMeta) and + other._is_protocol): + break + else: + return NotImplemented + return True + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook + + def __instancecheck__(self, instance): + # We need this method for situations where attributes are + # assigned in __init__. + if ((not getattr(self, '_is_protocol', False) or + _is_callable_members_only(self)) and + issubclass(instance.__class__, self)): + return True + if self._is_protocol: + if all(hasattr(instance, attr) and + (not callable(getattr(self, attr, None)) or + getattr(instance, attr) is not None) + for attr in _get_protocol_attrs(self)): + return True + return super(GenericMeta, self).__instancecheck__(instance) + + def __subclasscheck__(self, cls): + if self.__origin__ is not None: + if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: + raise TypeError("Parameterized generics cannot be used with class " + "or instance checks") + return False + if (self.__dict__.get('_is_protocol', None) and + not self.__dict__.get('_is_runtime_protocol', None)): + if sys._getframe(1).f_globals['__name__'] in ['abc', + 'functools', + 'typing']: + return False + raise TypeError("Instance and class checks can only be used with" + " @runtime protocols") + if (self.__dict__.get('_is_runtime_protocol', None) and + not _is_callable_members_only(self)): + if sys._getframe(1).f_globals['__name__'] in ['abc', + 'functools', + 'typing']: + return super(GenericMeta, self).__subclasscheck__(cls) + raise TypeError("Protocols with non-method members" + " don't support issubclass()") + return super(GenericMeta, self).__subclasscheck__(cls) + + if not OLD_GENERICS: + @_tp_cache + def __getitem__(self, params): + # We also need to copy this from GenericMeta.__getitem__ to get + # special treatment of "Protocol". (Comments removed for brevity.) + if not isinstance(params, tuple): + params = (params,) + if not params and _gorg(self) is not Tuple: + raise TypeError( + "Parameter list to %s[...] cannot be empty" % self.__qualname__) + msg = "Parameters to generic types must be types." + params = tuple(_type_check(p, msg) for p in params) + if self in (Generic, Protocol): + if not all(isinstance(p, TypeVar) for p in params): + raise TypeError( + "Parameters to %r[...] must all be type variables" % self) + if len(set(params)) != len(params): + raise TypeError( + "Parameters to %r[...] must all be unique" % self) + tvars = params + args = params + elif self in (Tuple, Callable): + tvars = _type_vars(params) + args = params + elif self.__origin__ in (Generic, Protocol): + raise TypeError("Cannot subscript already-subscripted %s" % + repr(self)) + else: + _check_generic(self, params) + tvars = _type_vars(params) + args = params + + prepend = (self,) if self.__origin__ is None else () + return self.__class__(self.__name__, + prepend + self.__bases__, + _no_slots_copy(self.__dict__), + tvars=tvars, + args=args, + origin=self, + extra=self.__extra__, + orig_bases=self.__orig_bases__) + + class Protocol(metaclass=_ProtocolMeta): + """Base class for protocol classes. Protocol classes are defined as:: + + class Proto(Protocol): + def meth(self) -> int: + ... + + Such classes are primarily used with static type checkers that recognize + structural subtyping (static duck-typing), for example:: + + class C: + def meth(self) -> int: + return 0 + + def func(x: Proto) -> int: + return x.meth() + + func(C()) # Passes static type check + + See PEP 544 for details. Protocol classes decorated with + @typing_extensions.runtime act as simple-minded runtime protocol that checks + only the presence of given attributes, ignoring their type signatures. + + Protocol classes can be generic, they are defined as:: + + class GenProto({bases}): + def meth(self) -> T: + ... + """ + __slots__ = () + _is_protocol = True + + def __new__(cls, *args, **kwds): + if _gorg(cls) is Protocol: + raise TypeError("Type Protocol cannot be instantiated; " + "it can be used only as a base class") + if OLD_GENERICS: + return _generic_new(_next_in_mro(cls), cls, *args, **kwds) + return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) + if Protocol.__doc__ is not None: + Protocol.__doc__ = Protocol.__doc__.format(bases="Protocol, Generic[T]" if + OLD_GENERICS else "Protocol[T]") + + +elif PEP_560: + from typing import _type_check, _GenericAlias, _collect_type_vars # noqa + + class _ProtocolMeta(abc.ABCMeta): + # This metaclass is a bit unfortunate and exists only because of the lack + # of __instancehook__. + def __instancecheck__(cls, instance): + # We need this method for situations where attributes are + # assigned in __init__. + if ((not getattr(cls, '_is_protocol', False) or + _is_callable_members_only(cls)) and + issubclass(instance.__class__, cls)): + return True + if cls._is_protocol: + if all(hasattr(instance, attr) and + (not callable(getattr(cls, attr, None)) or + getattr(instance, attr) is not None) + for attr in _get_protocol_attrs(cls)): + return True + return super().__instancecheck__(instance) + + class Protocol(metaclass=_ProtocolMeta): + # There is quite a lot of overlapping code with typing.Generic. + # Unfortunately it is hard to avoid this while these live in two different + # modules. The duplicated code will be removed when Protocol is moved to typing. + """Base class for protocol classes. Protocol classes are defined as:: + + class Proto(Protocol): + def meth(self) -> int: + ... + + Such classes are primarily used with static type checkers that recognize + structural subtyping (static duck-typing), for example:: + + class C: + def meth(self) -> int: + return 0 + + def func(x: Proto) -> int: + return x.meth() + + func(C()) # Passes static type check + + See PEP 544 for details. Protocol classes decorated with + @typing_extensions.runtime act as simple-minded runtime protocol that checks + only the presence of given attributes, ignoring their type signatures. + + Protocol classes can be generic, they are defined as:: + + class GenProto(Protocol[T]): + def meth(self) -> T: + ... + """ + __slots__ = () + _is_protocol = True + + def __new__(cls, *args, **kwds): + if cls is Protocol: + raise TypeError("Type Protocol cannot be instantiated; " + "it can only be used as a base class") + return super().__new__(cls) + + @_tp_cache + def __class_getitem__(cls, params): + if not isinstance(params, tuple): + params = (params,) + if not params and cls is not Tuple: + raise TypeError( + "Parameter list to {}[...] cannot be empty".format(cls.__qualname__)) + msg = "Parameters to generic types must be types." + params = tuple(_type_check(p, msg) for p in params) + if cls is Protocol: + # Generic can only be subscripted with unique type variables. + if not all(isinstance(p, TypeVar) for p in params): + i = 0 + while isinstance(params[i], TypeVar): + i += 1 + raise TypeError( + "Parameters to Protocol[...] must all be type variables." + " Parameter {} is {}".format(i + 1, params[i])) + if len(set(params)) != len(params): + raise TypeError( + "Parameters to Protocol[...] must all be unique") + else: + # Subscripting a regular Generic subclass. + _check_generic(cls, params) + return _GenericAlias(cls, params) + + def __init_subclass__(cls, *args, **kwargs): + tvars = [] + if '__orig_bases__' in cls.__dict__: + error = Generic in cls.__orig_bases__ + else: + error = Generic in cls.__bases__ + if error: + raise TypeError("Cannot inherit from plain Generic") + if '__orig_bases__' in cls.__dict__: + tvars = _collect_type_vars(cls.__orig_bases__) + # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn]. + # If found, tvars must be a subset of it. + # If not found, tvars is it. + # Also check for and reject plain Generic, + # and reject multiple Generic[...] and/or Protocol[...]. + gvars = None + for base in cls.__orig_bases__: + if (isinstance(base, _GenericAlias) and + base.__origin__ in (Generic, Protocol)): + # for error messages + the_base = 'Generic' if base.__origin__ is Generic else 'Protocol' + if gvars is not None: + raise TypeError( + "Cannot inherit from Generic[...]" + " and/or Protocol[...] multiple types.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + s_vars = ', '.join(str(t) for t in tvars if t not in gvarset) + s_args = ', '.join(str(g) for g in gvars) + raise TypeError("Some type variables ({}) are" + " not listed in {}[{}]".format(s_vars, + the_base, s_args)) + tvars = gvars + cls.__parameters__ = tuple(tvars) + + # Determine if this is a protocol or a concrete subclass. + if not cls.__dict__.get('_is_protocol', None): + cls._is_protocol = any(b is Protocol for b in cls.__bases__) + + # Set (or override) the protocol subclass hook. + def _proto_hook(other): + if not cls.__dict__.get('_is_protocol', None): + return NotImplemented + if not getattr(cls, '_is_runtime_protocol', False): + if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: + return NotImplemented + raise TypeError("Instance and class checks can only be used with" + " @runtime protocols") + if not _is_callable_members_only(cls): + if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: + return NotImplemented + raise TypeError("Protocols with non-method members" + " don't support issubclass()") + if not isinstance(other, type): + # Same error as for issubclass(1, int) + raise TypeError('issubclass() arg 1 must be a class') + for attr in _get_protocol_attrs(cls): + for base in other.__mro__: + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + annotations = getattr(base, '__annotations__', {}) + if (isinstance(annotations, typing.Mapping) and + attr in annotations and + isinstance(other, _ProtocolMeta) and + other._is_protocol): + break + else: + return NotImplemented + return True + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook + + # We have nothing more to do for non-protocols. + if not cls._is_protocol: + return + + # Check consistency of bases. + for base in cls.__bases__: + if not (base in (object, Generic) or + base.__module__ == 'collections.abc' and + base.__name__ in _PROTO_WHITELIST or + isinstance(base, _ProtocolMeta) and base._is_protocol): + raise TypeError('Protocols can only inherit from other' + ' protocols, got %r' % base) + + def _no_init(self, *args, **kwargs): + if type(self)._is_protocol: + raise TypeError('Protocols cannot be instantiated') + cls.__init__ = _no_init + + +if hasattr(typing, 'runtime_checkable'): + runtime_checkable = typing.runtime_checkable +elif HAVE_PROTOCOLS: + def runtime_checkable(cls): + """Mark a protocol class as a runtime protocol, so that it + can be used with isinstance() and issubclass(). Raise TypeError + if applied to a non-protocol class. + + This allows a simple-minded structural check very similar to the + one-offs in collections.abc such as Hashable. + """ + if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: + raise TypeError('@runtime_checkable can be only applied to protocol classes,' + ' got %r' % cls) + cls._is_runtime_protocol = True + return cls + + +if HAVE_PROTOCOLS: + # Exists for backwards compatibility. + runtime = runtime_checkable + + +if hasattr(typing, 'SupportsIndex'): + SupportsIndex = typing.SupportsIndex +elif HAVE_PROTOCOLS: + @runtime_checkable + class SupportsIndex(Protocol): + __slots__ = () + + @abc.abstractmethod + def __index__(self) -> int: + pass + + +if sys.version_info[:2] >= (3, 9): + # The standard library TypedDict in Python 3.8 does not store runtime information + # about which (if any) keys are optional. See https://bugs.python.org/issue38834 + TypedDict = typing.TypedDict +else: + def _check_fails(cls, other): + try: + if sys._getframe(1).f_globals['__name__'] not in ['abc', + 'functools', + 'typing']: + # Typed dicts are only for static structural subtyping. + raise TypeError('TypedDict does not support instance and class checks') + except (AttributeError, ValueError): + pass + return False + + def _dict_new(*args, **kwargs): + if not args: + raise TypeError('TypedDict.__new__(): not enough arguments') + _, args = args[0], args[1:] # allow the "cls" keyword be passed + return dict(*args, **kwargs) + + _dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)' + + def _typeddict_new(*args, total=True, **kwargs): + if not args: + raise TypeError('TypedDict.__new__(): not enough arguments') + _, args = args[0], args[1:] # allow the "cls" keyword be passed + if args: + typename, args = args[0], args[1:] # allow the "_typename" keyword be passed + elif '_typename' in kwargs: + typename = kwargs.pop('_typename') + import warnings + warnings.warn("Passing '_typename' as keyword argument is deprecated", + DeprecationWarning, stacklevel=2) + else: + raise TypeError("TypedDict.__new__() missing 1 required positional " + "argument: '_typename'") + if args: + try: + fields, = args # allow the "_fields" keyword be passed + except ValueError: + raise TypeError('TypedDict.__new__() takes from 2 to 3 ' + 'positional arguments but {} ' + 'were given'.format(len(args) + 2)) + elif '_fields' in kwargs and len(kwargs) == 1: + fields = kwargs.pop('_fields') + import warnings + warnings.warn("Passing '_fields' as keyword argument is deprecated", + DeprecationWarning, stacklevel=2) + else: + fields = None + + if fields is None: + fields = kwargs + elif kwargs: + raise TypeError("TypedDict takes either a dict or keyword arguments," + " but not both") + + ns = {'__annotations__': dict(fields), '__total__': total} + try: + # Setting correct module is necessary to make typed dict classes pickleable. + ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + pass + + return _TypedDictMeta(typename, (), ns) + + _typeddict_new.__text_signature__ = ('($cls, _typename, _fields=None,' + ' /, *, total=True, **kwargs)') + + class _TypedDictMeta(type): + def __new__(cls, name, bases, ns, total=True): + # Create new typed dict class object. + # This method is called directly when TypedDict is subclassed, + # or via _typeddict_new when TypedDict is instantiated. This way + # TypedDict supports all three syntaxes described in its docstring. + # Subclasses and instances of TypedDict return actual dictionaries + # via _dict_new. + ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new + tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns) + + annotations = {} + own_annotations = ns.get('__annotations__', {}) + own_annotation_keys = set(own_annotations.keys()) + msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" + own_annotations = { + n: typing._type_check(tp, msg) for n, tp in own_annotations.items() + } + required_keys = set() + optional_keys = set() + + for base in bases: + annotations.update(base.__dict__.get('__annotations__', {})) + required_keys.update(base.__dict__.get('__required_keys__', ())) + optional_keys.update(base.__dict__.get('__optional_keys__', ())) + + annotations.update(own_annotations) + if total: + required_keys.update(own_annotation_keys) + else: + optional_keys.update(own_annotation_keys) + + tp_dict.__annotations__ = annotations + tp_dict.__required_keys__ = frozenset(required_keys) + tp_dict.__optional_keys__ = frozenset(optional_keys) + if not hasattr(tp_dict, '__total__'): + tp_dict.__total__ = total + return tp_dict + + __instancecheck__ = __subclasscheck__ = _check_fails + + TypedDict = _TypedDictMeta('TypedDict', (dict,), {}) + TypedDict.__module__ = __name__ + TypedDict.__doc__ = \ + """A simple typed name space. At runtime it is equivalent to a plain dict. + + TypedDict creates a dictionary type that expects all of its + instances to have a certain set of keys, with each key + associated with a value of a consistent type. This expectation + is not checked at runtime but is only enforced by type checkers. + Usage:: + + class Point2D(TypedDict): + x: int + y: int + label: str + + a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK + b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check + + assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') + + The type info can be accessed via the Point2D.__annotations__ dict, and + the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. + TypedDict supports two additional equivalent forms:: + + Point2D = TypedDict('Point2D', x=int, y=int, label=str) + Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) + + The class syntax is only supported in Python 3.6+, while two other + syntax forms work for Python 2.7 and 3.2+ + """ + + +# Python 3.9+ has PEP 593 (Annotated and modified get_type_hints) +if hasattr(typing, 'Annotated'): + Annotated = typing.Annotated + get_type_hints = typing.get_type_hints + # Not exported and not a public API, but needed for get_origin() and get_args() + # to work. + _AnnotatedAlias = typing._AnnotatedAlias +elif PEP_560: + class _AnnotatedAlias(typing._GenericAlias, _root=True): + """Runtime representation of an annotated type. + + At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't' + with extra annotations. The alias behaves like a normal typing alias, + instantiating is the same as instantiating the underlying type, binding + it to types is also the same. + """ + def __init__(self, origin, metadata): + if isinstance(origin, _AnnotatedAlias): + metadata = origin.__metadata__ + metadata + origin = origin.__origin__ + super().__init__(origin, origin) + self.__metadata__ = metadata + + def copy_with(self, params): + assert len(params) == 1 + new_type = params[0] + return _AnnotatedAlias(new_type, self.__metadata__) + + def __repr__(self): + return "typing_extensions.Annotated[{}, {}]".format( + typing._type_repr(self.__origin__), + ", ".join(repr(a) for a in self.__metadata__) + ) + + def __reduce__(self): + return operator.getitem, ( + Annotated, (self.__origin__,) + self.__metadata__ + ) + + def __eq__(self, other): + if not isinstance(other, _AnnotatedAlias): + return NotImplemented + if self.__origin__ != other.__origin__: + return False + return self.__metadata__ == other.__metadata__ + + def __hash__(self): + return hash((self.__origin__, self.__metadata__)) + + class Annotated: + """Add context specific metadata to a type. + + Example: Annotated[int, runtime_check.Unsigned] indicates to the + hypothetical runtime_check module that this type is an unsigned int. + Every other consumer of this type can ignore this metadata and treat + this type as int. + + The first argument to Annotated must be a valid type (and will be in + the __origin__ field), the remaining arguments are kept as a tuple in + the __extra__ field. + + Details: + + - It's an error to call `Annotated` with less than two arguments. + - Nested Annotated are flattened:: + + Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3] + + - Instantiating an annotated type is equivalent to instantiating the + underlying type:: + + Annotated[C, Ann1](5) == C(5) + + - Annotated can be used as a generic type alias:: + + Optimized = Annotated[T, runtime.Optimize()] + Optimized[int] == Annotated[int, runtime.Optimize()] + + OptimizedList = Annotated[List[T], runtime.Optimize()] + OptimizedList[int] == Annotated[List[int], runtime.Optimize()] + """ + + __slots__ = () + + def __new__(cls, *args, **kwargs): + raise TypeError("Type Annotated cannot be instantiated.") + + @_tp_cache + def __class_getitem__(cls, params): + if not isinstance(params, tuple) or len(params) < 2: + raise TypeError("Annotated[...] should be used " + "with at least two arguments (a type and an " + "annotation).") + msg = "Annotated[t, ...]: t must be a type." + origin = typing._type_check(params[0], msg) + metadata = tuple(params[1:]) + return _AnnotatedAlias(origin, metadata) + + def __init_subclass__(cls, *args, **kwargs): + raise TypeError( + "Cannot subclass {}.Annotated".format(cls.__module__) + ) + + def _strip_annotations(t): + """Strips the annotations from a given type. + """ + if isinstance(t, _AnnotatedAlias): + return _strip_annotations(t.__origin__) + if isinstance(t, typing._GenericAlias): + stripped_args = tuple(_strip_annotations(a) for a in t.__args__) + if stripped_args == t.__args__: + return t + res = t.copy_with(stripped_args) + res._special = t._special + return res + return t + + def get_type_hints(obj, globalns=None, localns=None, include_extras=False): + """Return type hints for an object. + + This is often the same as obj.__annotations__, but it handles + forward references encoded as string literals, adds Optional[t] if a + default value equal to None is set and recursively replaces all + 'Annotated[T, ...]' with 'T' (unless 'include_extras=True'). + + The argument may be a module, class, method, or function. The annotations + are returned as a dictionary. For classes, annotations include also + inherited members. + + TypeError is raised if the argument is not of a type that can contain + annotations, and an empty dictionary is returned if no annotations are + present. + + BEWARE -- the behavior of globalns and localns is counterintuitive + (unless you are familiar with how eval() and exec() work). The + search order is locals first, then globals. + + - If no dict arguments are passed, an attempt is made to use the + globals from obj (or the respective module's globals for classes), + and these are also used as the locals. If the object does not appear + to have globals, an empty dictionary is used. + + - If one dict argument is passed, it is used for both globals and + locals. + + - If two dict arguments are passed, they specify globals and + locals, respectively. + """ + hint = typing.get_type_hints(obj, globalns=globalns, localns=localns) + if include_extras: + return hint + return {k: _strip_annotations(t) for k, t in hint.items()} + +elif HAVE_ANNOTATED: + + def _is_dunder(name): + """Returns True if name is a __dunder_variable_name__.""" + return len(name) > 4 and name.startswith('__') and name.endswith('__') + + # Prior to Python 3.7 types did not have `copy_with`. A lot of the equality + # checks, argument expansion etc. are done on the _subs_tre. As a result we + # can't provide a get_type_hints function that strips out annotations. + + class AnnotatedMeta(typing.GenericMeta): + """Metaclass for Annotated""" + + def __new__(cls, name, bases, namespace, **kwargs): + if any(b is not object for b in bases): + raise TypeError("Cannot subclass " + str(Annotated)) + return super().__new__(cls, name, bases, namespace, **kwargs) + + @property + def __metadata__(self): + return self._subs_tree()[2] + + def _tree_repr(self, tree): + cls, origin, metadata = tree + if not isinstance(origin, tuple): + tp_repr = typing._type_repr(origin) + else: + tp_repr = origin[0]._tree_repr(origin) + metadata_reprs = ", ".join(repr(arg) for arg in metadata) + return '%s[%s, %s]' % (cls, tp_repr, metadata_reprs) + + def _subs_tree(self, tvars=None, args=None): # noqa + if self is Annotated: + return Annotated + res = super()._subs_tree(tvars=tvars, args=args) + # Flatten nested Annotated + if isinstance(res[1], tuple) and res[1][0] is Annotated: + sub_tp = res[1][1] + sub_annot = res[1][2] + return (Annotated, sub_tp, sub_annot + res[2]) + return res + + def _get_cons(self): + """Return the class used to create instance of this type.""" + if self.__origin__ is None: + raise TypeError("Cannot get the underlying type of a " + "non-specialized Annotated type.") + tree = self._subs_tree() + while isinstance(tree, tuple) and tree[0] is Annotated: + tree = tree[1] + if isinstance(tree, tuple): + return tree[0] + else: + return tree + + @_tp_cache + def __getitem__(self, params): + if not isinstance(params, tuple): + params = (params,) + if self.__origin__ is not None: # specializing an instantiated type + return super().__getitem__(params) + elif not isinstance(params, tuple) or len(params) < 2: + raise TypeError("Annotated[...] should be instantiated " + "with at least two arguments (a type and an " + "annotation).") + else: + msg = "Annotated[t, ...]: t must be a type." + tp = typing._type_check(params[0], msg) + metadata = tuple(params[1:]) + return self.__class__( + self.__name__, + self.__bases__, + _no_slots_copy(self.__dict__), + tvars=_type_vars((tp,)), + # Metadata is a tuple so it won't be touched by _replace_args et al. + args=(tp, metadata), + origin=self, + ) + + def __call__(self, *args, **kwargs): + cons = self._get_cons() + result = cons(*args, **kwargs) + try: + result.__orig_class__ = self + except AttributeError: + pass + return result + + def __getattr__(self, attr): + # For simplicity we just don't relay all dunder names + if self.__origin__ is not None and not _is_dunder(attr): + return getattr(self._get_cons(), attr) + raise AttributeError(attr) + + def __setattr__(self, attr, value): + if _is_dunder(attr) or attr.startswith('_abc_'): + super().__setattr__(attr, value) + elif self.__origin__ is None: + raise AttributeError(attr) + else: + setattr(self._get_cons(), attr, value) + + def __instancecheck__(self, obj): + raise TypeError("Annotated cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("Annotated cannot be used with issubclass().") + + class Annotated(metaclass=AnnotatedMeta): + """Add context specific metadata to a type. + + Example: Annotated[int, runtime_check.Unsigned] indicates to the + hypothetical runtime_check module that this type is an unsigned int. + Every other consumer of this type can ignore this metadata and treat + this type as int. + + The first argument to Annotated must be a valid type, the remaining + arguments are kept as a tuple in the __metadata__ field. + + Details: + + - It's an error to call `Annotated` with less than two arguments. + - Nested Annotated are flattened:: + + Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3] + + - Instantiating an annotated type is equivalent to instantiating the + underlying type:: + + Annotated[C, Ann1](5) == C(5) + + - Annotated can be used as a generic type alias:: + + Optimized = Annotated[T, runtime.Optimize()] + Optimized[int] == Annotated[int, runtime.Optimize()] + + OptimizedList = Annotated[List[T], runtime.Optimize()] + OptimizedList[int] == Annotated[List[int], runtime.Optimize()] + """ + +# Python 3.8 has get_origin() and get_args() but those implementations aren't +# Annotated-aware, so we can't use those, only Python 3.9 versions will do. +if sys.version_info[:2] >= (3, 9): + get_origin = typing.get_origin + get_args = typing.get_args +elif PEP_560: + from typing import _GenericAlias # noqa + + def get_origin(tp): + """Get the unsubscripted version of a type. + + This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar + and Annotated. Return None for unsupported types. Examples:: + + get_origin(Literal[42]) is Literal + get_origin(int) is None + get_origin(ClassVar[int]) is ClassVar + get_origin(Generic) is Generic + get_origin(Generic[T]) is Generic + get_origin(Union[T, int]) is Union + get_origin(List[Tuple[T, T]][int]) == list + """ + if isinstance(tp, _AnnotatedAlias): + return Annotated + if isinstance(tp, _GenericAlias): + return tp.__origin__ + if tp is Generic: + return Generic + return None + + def get_args(tp): + """Get type arguments with all substitutions performed. + + For unions, basic simplifications used by Union constructor are performed. + Examples:: + get_args(Dict[str, int]) == (str, int) + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + get_args(Callable[[], T][int]) == ([], int) + """ + if isinstance(tp, _AnnotatedAlias): + return (tp.__origin__,) + tp.__metadata__ + if isinstance(tp, _GenericAlias): + res = tp.__args__ + if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + return () + + +if hasattr(typing, 'TypeAlias'): + TypeAlias = typing.TypeAlias +elif sys.version_info[:2] >= (3, 9): + class _TypeAliasForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + @_TypeAliasForm + def TypeAlias(self, parameters): + """Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example above. + """ + raise TypeError("{} is not subscriptable".format(self)) + +elif sys.version_info[:2] >= (3, 7): + class _TypeAliasForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + TypeAlias = _TypeAliasForm('TypeAlias', + doc="""Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example + above.""") + +elif hasattr(typing, '_FinalTypingBase'): + class _TypeAliasMeta(typing.TypingMeta): + """Metaclass for TypeAlias""" + + def __repr__(self): + return 'typing_extensions.TypeAlias' + + class _TypeAliasBase(typing._FinalTypingBase, metaclass=_TypeAliasMeta, _root=True): + """Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example above. + """ + __slots__ = () + + def __instancecheck__(self, obj): + raise TypeError("TypeAlias cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("TypeAlias cannot be used with issubclass().") + + def __repr__(self): + return 'typing_extensions.TypeAlias' + + TypeAlias = _TypeAliasBase(_root=True) +else: + class _TypeAliasMeta(typing.TypingMeta): + """Metaclass for TypeAlias""" + + def __instancecheck__(self, obj): + raise TypeError("TypeAlias cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("TypeAlias cannot be used with issubclass().") + + def __call__(self, *args, **kwargs): + raise TypeError("Cannot instantiate TypeAlias") + + class TypeAlias(metaclass=_TypeAliasMeta, _root=True): + """Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example above. + """ + __slots__ = () diff --git a/dist/ba_data/python-site-packages/yaml/__init__.py b/dist/ba_data/python-site-packages/yaml/__init__.py new file mode 100644 index 0000000..13d687c --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/__init__.py @@ -0,0 +1,427 @@ + +from .error import * + +from .tokens import * +from .events import * +from .nodes import * + +from .loader import * +from .dumper import * + +__version__ = '5.3.1' +try: + from .cyaml import * + __with_libyaml__ = True +except ImportError: + __with_libyaml__ = False + +import io + +#------------------------------------------------------------------------------ +# Warnings control +#------------------------------------------------------------------------------ + +# 'Global' warnings state: +_warnings_enabled = { + 'YAMLLoadWarning': True, +} + +# Get or set global warnings' state +def warnings(settings=None): + if settings is None: + return _warnings_enabled + + if type(settings) is dict: + for key in settings: + if key in _warnings_enabled: + _warnings_enabled[key] = settings[key] + +# Warn when load() is called without Loader=... +class YAMLLoadWarning(RuntimeWarning): + pass + +def load_warning(method): + if _warnings_enabled['YAMLLoadWarning'] is False: + return + + import warnings + + message = ( + "calling yaml.%s() without Loader=... is deprecated, as the " + "default Loader is unsafe. Please read " + "https://msg.pyyaml.org/load for full details." + ) % method + + warnings.warn(message, YAMLLoadWarning, stacklevel=3) + +#------------------------------------------------------------------------------ +def scan(stream, Loader=Loader): + """ + Scan a YAML stream and produce scanning tokens. + """ + loader = Loader(stream) + try: + while loader.check_token(): + yield loader.get_token() + finally: + loader.dispose() + +def parse(stream, Loader=Loader): + """ + Parse a YAML stream and produce parsing events. + """ + loader = Loader(stream) + try: + while loader.check_event(): + yield loader.get_event() + finally: + loader.dispose() + +def compose(stream, Loader=Loader): + """ + Parse the first YAML document in a stream + and produce the corresponding representation tree. + """ + loader = Loader(stream) + try: + return loader.get_single_node() + finally: + loader.dispose() + +def compose_all(stream, Loader=Loader): + """ + Parse all YAML documents in a stream + and produce corresponding representation trees. + """ + loader = Loader(stream) + try: + while loader.check_node(): + yield loader.get_node() + finally: + loader.dispose() + +def load(stream, Loader=None): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + """ + if Loader is None: + load_warning('load') + Loader = FullLoader + + loader = Loader(stream) + try: + return loader.get_single_data() + finally: + loader.dispose() + +def load_all(stream, Loader=None): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + """ + if Loader is None: + load_warning('load_all') + Loader = FullLoader + + loader = Loader(stream) + try: + while loader.check_data(): + yield loader.get_data() + finally: + loader.dispose() + +def full_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + + Resolve all tags except those known to be + unsafe on untrusted input. + """ + return load(stream, FullLoader) + +def full_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + + Resolve all tags except those known to be + unsafe on untrusted input. + """ + return load_all(stream, FullLoader) + +def safe_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + + Resolve only basic YAML tags. This is known + to be safe for untrusted input. + """ + return load(stream, SafeLoader) + +def safe_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + + Resolve only basic YAML tags. This is known + to be safe for untrusted input. + """ + return load_all(stream, SafeLoader) + +def unsafe_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + + Resolve all tags, even those known to be + unsafe on untrusted input. + """ + return load(stream, UnsafeLoader) + +def unsafe_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + + Resolve all tags, even those known to be + unsafe on untrusted input. + """ + return load_all(stream, UnsafeLoader) + +def emit(events, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None): + """ + Emit YAML parsing events into a stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + stream = io.StringIO() + getvalue = stream.getvalue + dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + try: + for event in events: + dumper.emit(event) + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def serialize_all(nodes, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + """ + Serialize a sequence of representation trees into a YAML stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + if encoding is None: + stream = io.StringIO() + else: + stream = io.BytesIO() + getvalue = stream.getvalue + dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end) + try: + dumper.open() + for node in nodes: + dumper.serialize(node) + dumper.close() + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def serialize(node, stream=None, Dumper=Dumper, **kwds): + """ + Serialize a representation tree into a YAML stream. + If stream is None, return the produced string instead. + """ + return serialize_all([node], stream, Dumper=Dumper, **kwds) + +def dump_all(documents, stream=None, Dumper=Dumper, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + """ + Serialize a sequence of Python objects into a YAML stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + if encoding is None: + stream = io.StringIO() + else: + stream = io.BytesIO() + getvalue = stream.getvalue + dumper = Dumper(stream, default_style=default_style, + default_flow_style=default_flow_style, + canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end, sort_keys=sort_keys) + try: + dumper.open() + for data in documents: + dumper.represent(data) + dumper.close() + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def dump(data, stream=None, Dumper=Dumper, **kwds): + """ + Serialize a Python object into a YAML stream. + If stream is None, return the produced string instead. + """ + return dump_all([data], stream, Dumper=Dumper, **kwds) + +def safe_dump_all(documents, stream=None, **kwds): + """ + Serialize a sequence of Python objects into a YAML stream. + Produce only basic YAML tags. + If stream is None, return the produced string instead. + """ + return dump_all(documents, stream, Dumper=SafeDumper, **kwds) + +def safe_dump(data, stream=None, **kwds): + """ + Serialize a Python object into a YAML stream. + Produce only basic YAML tags. + If stream is None, return the produced string instead. + """ + return dump_all([data], stream, Dumper=SafeDumper, **kwds) + +def add_implicit_resolver(tag, regexp, first=None, + Loader=None, Dumper=Dumper): + """ + Add an implicit scalar detector. + If an implicit scalar value matches the given regexp, + the corresponding tag is assigned to the scalar. + first is a sequence of possible initial characters or None. + """ + if Loader is None: + loader.Loader.add_implicit_resolver(tag, regexp, first) + loader.FullLoader.add_implicit_resolver(tag, regexp, first) + loader.UnsafeLoader.add_implicit_resolver(tag, regexp, first) + else: + Loader.add_implicit_resolver(tag, regexp, first) + Dumper.add_implicit_resolver(tag, regexp, first) + +def add_path_resolver(tag, path, kind=None, Loader=None, Dumper=Dumper): + """ + Add a path based resolver for the given tag. + A path is a list of keys that forms a path + to a node in the representation tree. + Keys can be string values, integers, or None. + """ + if Loader is None: + loader.Loader.add_path_resolver(tag, path, kind) + loader.FullLoader.add_path_resolver(tag, path, kind) + loader.UnsafeLoader.add_path_resolver(tag, path, kind) + else: + Loader.add_path_resolver(tag, path, kind) + Dumper.add_path_resolver(tag, path, kind) + +def add_constructor(tag, constructor, Loader=None): + """ + Add a constructor for the given tag. + Constructor is a function that accepts a Loader instance + and a node object and produces the corresponding Python object. + """ + if Loader is None: + loader.Loader.add_constructor(tag, constructor) + loader.FullLoader.add_constructor(tag, constructor) + loader.UnsafeLoader.add_constructor(tag, constructor) + else: + Loader.add_constructor(tag, constructor) + +def add_multi_constructor(tag_prefix, multi_constructor, Loader=None): + """ + Add a multi-constructor for the given tag prefix. + Multi-constructor is called for a node if its tag starts with tag_prefix. + Multi-constructor accepts a Loader instance, a tag suffix, + and a node object and produces the corresponding Python object. + """ + if Loader is None: + loader.Loader.add_multi_constructor(tag_prefix, multi_constructor) + loader.FullLoader.add_multi_constructor(tag_prefix, multi_constructor) + loader.UnsafeLoader.add_multi_constructor(tag_prefix, multi_constructor) + else: + Loader.add_multi_constructor(tag_prefix, multi_constructor) + +def add_representer(data_type, representer, Dumper=Dumper): + """ + Add a representer for the given type. + Representer is a function accepting a Dumper instance + and an instance of the given data type + and producing the corresponding representation node. + """ + Dumper.add_representer(data_type, representer) + +def add_multi_representer(data_type, multi_representer, Dumper=Dumper): + """ + Add a representer for the given type. + Multi-representer is a function accepting a Dumper instance + and an instance of the given data type or subtype + and producing the corresponding representation node. + """ + Dumper.add_multi_representer(data_type, multi_representer) + +class YAMLObjectMetaclass(type): + """ + The metaclass for YAMLObject. + """ + def __init__(cls, name, bases, kwds): + super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds) + if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None: + if isinstance(cls.yaml_loader, list): + for loader in cls.yaml_loader: + loader.add_constructor(cls.yaml_tag, cls.from_yaml) + else: + cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml) + + cls.yaml_dumper.add_representer(cls, cls.to_yaml) + +class YAMLObject(metaclass=YAMLObjectMetaclass): + """ + An object that can dump itself to a YAML stream + and load itself from a YAML stream. + """ + + __slots__ = () # no direct instantiation, so allow immutable subclasses + + yaml_loader = [Loader, FullLoader, UnsafeLoader] + yaml_dumper = Dumper + + yaml_tag = None + yaml_flow_style = None + + @classmethod + def from_yaml(cls, loader, node): + """ + Convert a representation node to a Python object. + """ + return loader.construct_yaml_object(node, cls) + + @classmethod + def to_yaml(cls, dumper, data): + """ + Convert a Python object to a representation node. + """ + return dumper.represent_yaml_object(cls.yaml_tag, data, cls, + flow_style=cls.yaml_flow_style) + diff --git a/dist/ba_data/python-site-packages/yaml/composer.py b/dist/ba_data/python-site-packages/yaml/composer.py new file mode 100644 index 0000000..6d15cb4 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/composer.py @@ -0,0 +1,139 @@ + +__all__ = ['Composer', 'ComposerError'] + +from .error import MarkedYAMLError +from .events import * +from .nodes import * + +class ComposerError(MarkedYAMLError): + pass + +class Composer: + + def __init__(self): + self.anchors = {} + + def check_node(self): + # Drop the STREAM-START event. + if self.check_event(StreamStartEvent): + self.get_event() + + # If there are more documents available? + return not self.check_event(StreamEndEvent) + + def get_node(self): + # Get the root node of the next document. + if not self.check_event(StreamEndEvent): + return self.compose_document() + + def get_single_node(self): + # Drop the STREAM-START event. + self.get_event() + + # Compose a document if the stream is not empty. + document = None + if not self.check_event(StreamEndEvent): + document = self.compose_document() + + # Ensure that the stream contains no more documents. + if not self.check_event(StreamEndEvent): + event = self.get_event() + raise ComposerError("expected a single document in the stream", + document.start_mark, "but found another document", + event.start_mark) + + # Drop the STREAM-END event. + self.get_event() + + return document + + def compose_document(self): + # Drop the DOCUMENT-START event. + self.get_event() + + # Compose the root node. + node = self.compose_node(None, None) + + # Drop the DOCUMENT-END event. + self.get_event() + + self.anchors = {} + return node + + def compose_node(self, parent, index): + if self.check_event(AliasEvent): + event = self.get_event() + anchor = event.anchor + if anchor not in self.anchors: + raise ComposerError(None, None, "found undefined alias %r" + % anchor, event.start_mark) + return self.anchors[anchor] + event = self.peek_event() + anchor = event.anchor + if anchor is not None: + if anchor in self.anchors: + raise ComposerError("found duplicate anchor %r; first occurrence" + % anchor, self.anchors[anchor].start_mark, + "second occurrence", event.start_mark) + self.descend_resolver(parent, index) + if self.check_event(ScalarEvent): + node = self.compose_scalar_node(anchor) + elif self.check_event(SequenceStartEvent): + node = self.compose_sequence_node(anchor) + elif self.check_event(MappingStartEvent): + node = self.compose_mapping_node(anchor) + self.ascend_resolver() + return node + + def compose_scalar_node(self, anchor): + event = self.get_event() + tag = event.tag + if tag is None or tag == '!': + tag = self.resolve(ScalarNode, event.value, event.implicit) + node = ScalarNode(tag, event.value, + event.start_mark, event.end_mark, style=event.style) + if anchor is not None: + self.anchors[anchor] = node + return node + + def compose_sequence_node(self, anchor): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == '!': + tag = self.resolve(SequenceNode, None, start_event.implicit) + node = SequenceNode(tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style) + if anchor is not None: + self.anchors[anchor] = node + index = 0 + while not self.check_event(SequenceEndEvent): + node.value.append(self.compose_node(node, index)) + index += 1 + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node + + def compose_mapping_node(self, anchor): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == '!': + tag = self.resolve(MappingNode, None, start_event.implicit) + node = MappingNode(tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style) + if anchor is not None: + self.anchors[anchor] = node + while not self.check_event(MappingEndEvent): + #key_event = self.peek_event() + item_key = self.compose_node(node, None) + #if item_key in node.value: + # raise ComposerError("while composing a mapping", start_event.start_mark, + # "found duplicate key", key_event.start_mark) + item_value = self.compose_node(node, item_key) + #node.value[item_key] = item_value + node.value.append((item_key, item_value)) + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node + diff --git a/dist/ba_data/python-site-packages/yaml/constructor.py b/dist/ba_data/python-site-packages/yaml/constructor.py new file mode 100644 index 0000000..1948b12 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/constructor.py @@ -0,0 +1,748 @@ + +__all__ = [ + 'BaseConstructor', + 'SafeConstructor', + 'FullConstructor', + 'UnsafeConstructor', + 'Constructor', + 'ConstructorError' +] + +from .error import * +from .nodes import * + +import collections.abc, datetime, base64, binascii, re, sys, types + +class ConstructorError(MarkedYAMLError): + pass + +class BaseConstructor: + + yaml_constructors = {} + yaml_multi_constructors = {} + + def __init__(self): + self.constructed_objects = {} + self.recursive_objects = {} + self.state_generators = [] + self.deep_construct = False + + def check_data(self): + # If there are more documents available? + return self.check_node() + + def check_state_key(self, key): + """Block special attributes/methods from being set in a newly created + object, to prevent user-controlled methods from being called during + deserialization""" + if self.get_state_keys_blacklist_regexp().match(key): + raise ConstructorError(None, None, + "blacklisted key '%s' in instance state found" % (key,), None) + + def get_data(self): + # Construct and return the next document. + if self.check_node(): + return self.construct_document(self.get_node()) + + def get_single_data(self): + # Ensure that the stream contains a single document and construct it. + node = self.get_single_node() + if node is not None: + return self.construct_document(node) + return None + + def construct_document(self, node): + data = self.construct_object(node) + while self.state_generators: + state_generators = self.state_generators + self.state_generators = [] + for generator in state_generators: + for dummy in generator: + pass + self.constructed_objects = {} + self.recursive_objects = {} + self.deep_construct = False + return data + + def construct_object(self, node, deep=False): + if node in self.constructed_objects: + return self.constructed_objects[node] + if deep: + old_deep = self.deep_construct + self.deep_construct = True + if node in self.recursive_objects: + raise ConstructorError(None, None, + "found unconstructable recursive node", node.start_mark) + self.recursive_objects[node] = None + constructor = None + tag_suffix = None + if node.tag in self.yaml_constructors: + constructor = self.yaml_constructors[node.tag] + else: + for tag_prefix in self.yaml_multi_constructors: + if tag_prefix is not None and node.tag.startswith(tag_prefix): + tag_suffix = node.tag[len(tag_prefix):] + constructor = self.yaml_multi_constructors[tag_prefix] + break + else: + if None in self.yaml_multi_constructors: + tag_suffix = node.tag + constructor = self.yaml_multi_constructors[None] + elif None in self.yaml_constructors: + constructor = self.yaml_constructors[None] + elif isinstance(node, ScalarNode): + constructor = self.__class__.construct_scalar + elif isinstance(node, SequenceNode): + constructor = self.__class__.construct_sequence + elif isinstance(node, MappingNode): + constructor = self.__class__.construct_mapping + if tag_suffix is None: + data = constructor(self, node) + else: + data = constructor(self, tag_suffix, node) + if isinstance(data, types.GeneratorType): + generator = data + data = next(generator) + if self.deep_construct: + for dummy in generator: + pass + else: + self.state_generators.append(generator) + self.constructed_objects[node] = data + del self.recursive_objects[node] + if deep: + self.deep_construct = old_deep + return data + + def construct_scalar(self, node): + if not isinstance(node, ScalarNode): + raise ConstructorError(None, None, + "expected a scalar node, but found %s" % node.id, + node.start_mark) + return node.value + + def construct_sequence(self, node, deep=False): + if not isinstance(node, SequenceNode): + raise ConstructorError(None, None, + "expected a sequence node, but found %s" % node.id, + node.start_mark) + return [self.construct_object(child, deep=deep) + for child in node.value] + + def construct_mapping(self, node, deep=False): + if not isinstance(node, MappingNode): + raise ConstructorError(None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark) + mapping = {} + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + if not isinstance(key, collections.abc.Hashable): + raise ConstructorError("while constructing a mapping", node.start_mark, + "found unhashable key", key_node.start_mark) + value = self.construct_object(value_node, deep=deep) + mapping[key] = value + return mapping + + def construct_pairs(self, node, deep=False): + if not isinstance(node, MappingNode): + raise ConstructorError(None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark) + pairs = [] + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + value = self.construct_object(value_node, deep=deep) + pairs.append((key, value)) + return pairs + + @classmethod + def add_constructor(cls, tag, constructor): + if not 'yaml_constructors' in cls.__dict__: + cls.yaml_constructors = cls.yaml_constructors.copy() + cls.yaml_constructors[tag] = constructor + + @classmethod + def add_multi_constructor(cls, tag_prefix, multi_constructor): + if not 'yaml_multi_constructors' in cls.__dict__: + cls.yaml_multi_constructors = cls.yaml_multi_constructors.copy() + cls.yaml_multi_constructors[tag_prefix] = multi_constructor + +class SafeConstructor(BaseConstructor): + + def construct_scalar(self, node): + if isinstance(node, MappingNode): + for key_node, value_node in node.value: + if key_node.tag == 'tag:yaml.org,2002:value': + return self.construct_scalar(value_node) + return super().construct_scalar(node) + + def flatten_mapping(self, node): + merge = [] + index = 0 + while index < len(node.value): + key_node, value_node = node.value[index] + if key_node.tag == 'tag:yaml.org,2002:merge': + del node.value[index] + if isinstance(value_node, MappingNode): + self.flatten_mapping(value_node) + merge.extend(value_node.value) + elif isinstance(value_node, SequenceNode): + submerge = [] + for subnode in value_node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing a mapping", + node.start_mark, + "expected a mapping for merging, but found %s" + % subnode.id, subnode.start_mark) + self.flatten_mapping(subnode) + submerge.append(subnode.value) + submerge.reverse() + for value in submerge: + merge.extend(value) + else: + raise ConstructorError("while constructing a mapping", node.start_mark, + "expected a mapping or list of mappings for merging, but found %s" + % value_node.id, value_node.start_mark) + elif key_node.tag == 'tag:yaml.org,2002:value': + key_node.tag = 'tag:yaml.org,2002:str' + index += 1 + else: + index += 1 + if merge: + node.value = merge + node.value + + def construct_mapping(self, node, deep=False): + if isinstance(node, MappingNode): + self.flatten_mapping(node) + return super().construct_mapping(node, deep=deep) + + def construct_yaml_null(self, node): + self.construct_scalar(node) + return None + + bool_values = { + 'yes': True, + 'no': False, + 'true': True, + 'false': False, + 'on': True, + 'off': False, + } + + def construct_yaml_bool(self, node): + value = self.construct_scalar(node) + return self.bool_values[value.lower()] + + def construct_yaml_int(self, node): + value = self.construct_scalar(node) + value = value.replace('_', '') + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + if value == '0': + return 0 + elif value.startswith('0b'): + return sign*int(value[2:], 2) + elif value.startswith('0x'): + return sign*int(value[2:], 16) + elif value[0] == '0': + return sign*int(value, 8) + elif ':' in value: + digits = [int(part) for part in value.split(':')] + digits.reverse() + base = 1 + value = 0 + for digit in digits: + value += digit*base + base *= 60 + return sign*value + else: + return sign*int(value) + + inf_value = 1e300 + while inf_value != inf_value*inf_value: + inf_value *= inf_value + nan_value = -inf_value/inf_value # Trying to make a quiet NaN (like C99). + + def construct_yaml_float(self, node): + value = self.construct_scalar(node) + value = value.replace('_', '').lower() + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + if value == '.inf': + return sign*self.inf_value + elif value == '.nan': + return self.nan_value + elif ':' in value: + digits = [float(part) for part in value.split(':')] + digits.reverse() + base = 1 + value = 0.0 + for digit in digits: + value += digit*base + base *= 60 + return sign*value + else: + return sign*float(value) + + def construct_yaml_binary(self, node): + try: + value = self.construct_scalar(node).encode('ascii') + except UnicodeEncodeError as exc: + raise ConstructorError(None, None, + "failed to convert base64 data into ascii: %s" % exc, + node.start_mark) + try: + if hasattr(base64, 'decodebytes'): + return base64.decodebytes(value) + else: + return base64.decodestring(value) + except binascii.Error as exc: + raise ConstructorError(None, None, + "failed to decode base64 data: %s" % exc, node.start_mark) + + timestamp_regexp = re.compile( + r'''^(?P[0-9][0-9][0-9][0-9]) + -(?P[0-9][0-9]?) + -(?P[0-9][0-9]?) + (?:(?:[Tt]|[ \t]+) + (?P[0-9][0-9]?) + :(?P[0-9][0-9]) + :(?P[0-9][0-9]) + (?:\.(?P[0-9]*))? + (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) + (?::(?P[0-9][0-9]))?))?)?$''', re.X) + + def construct_yaml_timestamp(self, node): + value = self.construct_scalar(node) + match = self.timestamp_regexp.match(node.value) + values = match.groupdict() + year = int(values['year']) + month = int(values['month']) + day = int(values['day']) + if not values['hour']: + return datetime.date(year, month, day) + hour = int(values['hour']) + minute = int(values['minute']) + second = int(values['second']) + fraction = 0 + tzinfo = None + if values['fraction']: + fraction = values['fraction'][:6] + while len(fraction) < 6: + fraction += '0' + fraction = int(fraction) + if values['tz_sign']: + tz_hour = int(values['tz_hour']) + tz_minute = int(values['tz_minute'] or 0) + delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute) + if values['tz_sign'] == '-': + delta = -delta + tzinfo = datetime.timezone(delta) + elif values['tz']: + tzinfo = datetime.timezone.utc + return datetime.datetime(year, month, day, hour, minute, second, fraction, + tzinfo=tzinfo) + + def construct_yaml_omap(self, node): + # Note: we do not check for duplicate keys, because it's too + # CPU-expensive. + omap = [] + yield omap + if not isinstance(node, SequenceNode): + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark) + for subnode in node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark) + if len(subnode.value) != 1: + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark) + key_node, value_node = subnode.value[0] + key = self.construct_object(key_node) + value = self.construct_object(value_node) + omap.append((key, value)) + + def construct_yaml_pairs(self, node): + # Note: the same code as `construct_yaml_omap`. + pairs = [] + yield pairs + if not isinstance(node, SequenceNode): + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark) + for subnode in node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark) + if len(subnode.value) != 1: + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark) + key_node, value_node = subnode.value[0] + key = self.construct_object(key_node) + value = self.construct_object(value_node) + pairs.append((key, value)) + + def construct_yaml_set(self, node): + data = set() + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_yaml_str(self, node): + return self.construct_scalar(node) + + def construct_yaml_seq(self, node): + data = [] + yield data + data.extend(self.construct_sequence(node)) + + def construct_yaml_map(self, node): + data = {} + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_yaml_object(self, node, cls): + data = cls.__new__(cls) + yield data + if hasattr(data, '__setstate__'): + state = self.construct_mapping(node, deep=True) + data.__setstate__(state) + else: + state = self.construct_mapping(node) + data.__dict__.update(state) + + def construct_undefined(self, node): + raise ConstructorError(None, None, + "could not determine a constructor for the tag %r" % node.tag, + node.start_mark) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:null', + SafeConstructor.construct_yaml_null) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:bool', + SafeConstructor.construct_yaml_bool) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:int', + SafeConstructor.construct_yaml_int) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:float', + SafeConstructor.construct_yaml_float) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:binary', + SafeConstructor.construct_yaml_binary) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:timestamp', + SafeConstructor.construct_yaml_timestamp) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:omap', + SafeConstructor.construct_yaml_omap) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:pairs', + SafeConstructor.construct_yaml_pairs) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:set', + SafeConstructor.construct_yaml_set) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:str', + SafeConstructor.construct_yaml_str) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:seq', + SafeConstructor.construct_yaml_seq) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:map', + SafeConstructor.construct_yaml_map) + +SafeConstructor.add_constructor(None, + SafeConstructor.construct_undefined) + +class FullConstructor(SafeConstructor): + # 'extend' is blacklisted because it is used by + # construct_python_object_apply to add `listitems` to a newly generate + # python instance + def get_state_keys_blacklist(self): + return ['^extend$', '^__.*__$'] + + def get_state_keys_blacklist_regexp(self): + if not hasattr(self, 'state_keys_blacklist_regexp'): + self.state_keys_blacklist_regexp = re.compile('(' + '|'.join(self.get_state_keys_blacklist()) + ')') + return self.state_keys_blacklist_regexp + + def construct_python_str(self, node): + return self.construct_scalar(node) + + def construct_python_unicode(self, node): + return self.construct_scalar(node) + + def construct_python_bytes(self, node): + try: + value = self.construct_scalar(node).encode('ascii') + except UnicodeEncodeError as exc: + raise ConstructorError(None, None, + "failed to convert base64 data into ascii: %s" % exc, + node.start_mark) + try: + if hasattr(base64, 'decodebytes'): + return base64.decodebytes(value) + else: + return base64.decodestring(value) + except binascii.Error as exc: + raise ConstructorError(None, None, + "failed to decode base64 data: %s" % exc, node.start_mark) + + def construct_python_long(self, node): + return self.construct_yaml_int(node) + + def construct_python_complex(self, node): + return complex(self.construct_scalar(node)) + + def construct_python_tuple(self, node): + return tuple(self.construct_sequence(node)) + + def find_python_module(self, name, mark, unsafe=False): + if not name: + raise ConstructorError("while constructing a Python module", mark, + "expected non-empty name appended to the tag", mark) + if unsafe: + try: + __import__(name) + except ImportError as exc: + raise ConstructorError("while constructing a Python module", mark, + "cannot find module %r (%s)" % (name, exc), mark) + if name not in sys.modules: + raise ConstructorError("while constructing a Python module", mark, + "module %r is not imported" % name, mark) + return sys.modules[name] + + def find_python_name(self, name, mark, unsafe=False): + if not name: + raise ConstructorError("while constructing a Python object", mark, + "expected non-empty name appended to the tag", mark) + if '.' in name: + module_name, object_name = name.rsplit('.', 1) + else: + module_name = 'builtins' + object_name = name + if unsafe: + try: + __import__(module_name) + except ImportError as exc: + raise ConstructorError("while constructing a Python object", mark, + "cannot find module %r (%s)" % (module_name, exc), mark) + if module_name not in sys.modules: + raise ConstructorError("while constructing a Python object", mark, + "module %r is not imported" % module_name, mark) + module = sys.modules[module_name] + if not hasattr(module, object_name): + raise ConstructorError("while constructing a Python object", mark, + "cannot find %r in the module %r" + % (object_name, module.__name__), mark) + return getattr(module, object_name) + + def construct_python_name(self, suffix, node): + value = self.construct_scalar(node) + if value: + raise ConstructorError("while constructing a Python name", node.start_mark, + "expected the empty value, but found %r" % value, node.start_mark) + return self.find_python_name(suffix, node.start_mark) + + def construct_python_module(self, suffix, node): + value = self.construct_scalar(node) + if value: + raise ConstructorError("while constructing a Python module", node.start_mark, + "expected the empty value, but found %r" % value, node.start_mark) + return self.find_python_module(suffix, node.start_mark) + + def make_python_instance(self, suffix, node, + args=None, kwds=None, newobj=False, unsafe=False): + if not args: + args = [] + if not kwds: + kwds = {} + cls = self.find_python_name(suffix, node.start_mark) + if not (unsafe or isinstance(cls, type)): + raise ConstructorError("while constructing a Python instance", node.start_mark, + "expected a class, but found %r" % type(cls), + node.start_mark) + if newobj and isinstance(cls, type): + return cls.__new__(cls, *args, **kwds) + else: + return cls(*args, **kwds) + + def set_python_instance_state(self, instance, state, unsafe=False): + if hasattr(instance, '__setstate__'): + instance.__setstate__(state) + else: + slotstate = {} + if isinstance(state, tuple) and len(state) == 2: + state, slotstate = state + if hasattr(instance, '__dict__'): + if not unsafe and state: + for key in state.keys(): + self.check_state_key(key) + instance.__dict__.update(state) + elif state: + slotstate.update(state) + for key, value in slotstate.items(): + if not unsafe: + self.check_state_key(key) + setattr(instance, key, value) + + def construct_python_object(self, suffix, node): + # Format: + # !!python/object:module.name { ... state ... } + instance = self.make_python_instance(suffix, node, newobj=True) + yield instance + deep = hasattr(instance, '__setstate__') + state = self.construct_mapping(node, deep=deep) + self.set_python_instance_state(instance, state) + + def construct_python_object_apply(self, suffix, node, newobj=False): + # Format: + # !!python/object/apply # (or !!python/object/new) + # args: [ ... arguments ... ] + # kwds: { ... keywords ... } + # state: ... state ... + # listitems: [ ... listitems ... ] + # dictitems: { ... dictitems ... } + # or short format: + # !!python/object/apply [ ... arguments ... ] + # The difference between !!python/object/apply and !!python/object/new + # is how an object is created, check make_python_instance for details. + if isinstance(node, SequenceNode): + args = self.construct_sequence(node, deep=True) + kwds = {} + state = {} + listitems = [] + dictitems = {} + else: + value = self.construct_mapping(node, deep=True) + args = value.get('args', []) + kwds = value.get('kwds', {}) + state = value.get('state', {}) + listitems = value.get('listitems', []) + dictitems = value.get('dictitems', {}) + instance = self.make_python_instance(suffix, node, args, kwds, newobj) + if state: + self.set_python_instance_state(instance, state) + if listitems: + instance.extend(listitems) + if dictitems: + for key in dictitems: + instance[key] = dictitems[key] + return instance + + def construct_python_object_new(self, suffix, node): + return self.construct_python_object_apply(suffix, node, newobj=True) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/none', + FullConstructor.construct_yaml_null) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/bool', + FullConstructor.construct_yaml_bool) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/str', + FullConstructor.construct_python_str) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/unicode', + FullConstructor.construct_python_unicode) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/bytes', + FullConstructor.construct_python_bytes) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/int', + FullConstructor.construct_yaml_int) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/long', + FullConstructor.construct_python_long) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/float', + FullConstructor.construct_yaml_float) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/complex', + FullConstructor.construct_python_complex) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/list', + FullConstructor.construct_yaml_seq) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/tuple', + FullConstructor.construct_python_tuple) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/dict', + FullConstructor.construct_yaml_map) + +FullConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/name:', + FullConstructor.construct_python_name) + +FullConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/module:', + FullConstructor.construct_python_module) + +FullConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/object:', + FullConstructor.construct_python_object) + +FullConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/object/new:', + FullConstructor.construct_python_object_new) + +class UnsafeConstructor(FullConstructor): + + def find_python_module(self, name, mark): + return super(UnsafeConstructor, self).find_python_module(name, mark, unsafe=True) + + def find_python_name(self, name, mark): + return super(UnsafeConstructor, self).find_python_name(name, mark, unsafe=True) + + def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False): + return super(UnsafeConstructor, self).make_python_instance( + suffix, node, args, kwds, newobj, unsafe=True) + + def set_python_instance_state(self, instance, state): + return super(UnsafeConstructor, self).set_python_instance_state( + instance, state, unsafe=True) + +UnsafeConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/object/apply:', + UnsafeConstructor.construct_python_object_apply) + +# Constructor is same as UnsafeConstructor. Need to leave this in place in case +# people have extended it directly. +class Constructor(UnsafeConstructor): + pass diff --git a/dist/ba_data/python-site-packages/yaml/cyaml.py b/dist/ba_data/python-site-packages/yaml/cyaml.py new file mode 100644 index 0000000..1e606c7 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/cyaml.py @@ -0,0 +1,101 @@ + +__all__ = [ + 'CBaseLoader', 'CSafeLoader', 'CFullLoader', 'CUnsafeLoader', 'CLoader', + 'CBaseDumper', 'CSafeDumper', 'CDumper' +] + +from _yaml import CParser, CEmitter + +from .constructor import * + +from .serializer import * +from .representer import * + +from .resolver import * + +class CBaseLoader(CParser, BaseConstructor, BaseResolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + BaseConstructor.__init__(self) + BaseResolver.__init__(self) + +class CSafeLoader(CParser, SafeConstructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + SafeConstructor.__init__(self) + Resolver.__init__(self) + +class CFullLoader(CParser, FullConstructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + FullConstructor.__init__(self) + Resolver.__init__(self) + +class CUnsafeLoader(CParser, UnsafeConstructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + UnsafeConstructor.__init__(self) + Resolver.__init__(self) + +class CLoader(CParser, Constructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + Constructor.__init__(self) + Resolver.__init__(self) + +class CBaseDumper(CEmitter, BaseRepresenter, BaseResolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + +class CSafeDumper(CEmitter, SafeRepresenter, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + SafeRepresenter.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + +class CDumper(CEmitter, Serializer, Representer, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + diff --git a/dist/ba_data/python-site-packages/yaml/dumper.py b/dist/ba_data/python-site-packages/yaml/dumper.py new file mode 100644 index 0000000..6aadba5 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/dumper.py @@ -0,0 +1,62 @@ + +__all__ = ['BaseDumper', 'SafeDumper', 'Dumper'] + +from .emitter import * +from .serializer import * +from .representer import * +from .resolver import * + +class BaseDumper(Emitter, Serializer, BaseRepresenter, BaseResolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + +class SafeDumper(Emitter, Serializer, SafeRepresenter, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + SafeRepresenter.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + +class Dumper(Emitter, Serializer, Representer, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + diff --git a/dist/ba_data/python-site-packages/yaml/emitter.py b/dist/ba_data/python-site-packages/yaml/emitter.py new file mode 100644 index 0000000..a664d01 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/emitter.py @@ -0,0 +1,1137 @@ + +# Emitter expects events obeying the following grammar: +# stream ::= STREAM-START document* STREAM-END +# document ::= DOCUMENT-START node DOCUMENT-END +# node ::= SCALAR | sequence | mapping +# sequence ::= SEQUENCE-START node* SEQUENCE-END +# mapping ::= MAPPING-START (node node)* MAPPING-END + +__all__ = ['Emitter', 'EmitterError'] + +from .error import YAMLError +from .events import * + +class EmitterError(YAMLError): + pass + +class ScalarAnalysis: + def __init__(self, scalar, empty, multiline, + allow_flow_plain, allow_block_plain, + allow_single_quoted, allow_double_quoted, + allow_block): + self.scalar = scalar + self.empty = empty + self.multiline = multiline + self.allow_flow_plain = allow_flow_plain + self.allow_block_plain = allow_block_plain + self.allow_single_quoted = allow_single_quoted + self.allow_double_quoted = allow_double_quoted + self.allow_block = allow_block + +class Emitter: + + DEFAULT_TAG_PREFIXES = { + '!' : '!', + 'tag:yaml.org,2002:' : '!!', + } + + def __init__(self, stream, canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None): + + # The stream should have the methods `write` and possibly `flush`. + self.stream = stream + + # Encoding can be overridden by STREAM-START. + self.encoding = None + + # Emitter is a state machine with a stack of states to handle nested + # structures. + self.states = [] + self.state = self.expect_stream_start + + # Current event and the event queue. + self.events = [] + self.event = None + + # The current indentation level and the stack of previous indents. + self.indents = [] + self.indent = None + + # Flow level. + self.flow_level = 0 + + # Contexts. + self.root_context = False + self.sequence_context = False + self.mapping_context = False + self.simple_key_context = False + + # Characteristics of the last emitted character: + # - current position. + # - is it a whitespace? + # - is it an indention character + # (indentation space, '-', '?', or ':')? + self.line = 0 + self.column = 0 + self.whitespace = True + self.indention = True + + # Whether the document requires an explicit document indicator + self.open_ended = False + + # Formatting details. + self.canonical = canonical + self.allow_unicode = allow_unicode + self.best_indent = 2 + if indent and 1 < indent < 10: + self.best_indent = indent + self.best_width = 80 + if width and width > self.best_indent*2: + self.best_width = width + self.best_line_break = '\n' + if line_break in ['\r', '\n', '\r\n']: + self.best_line_break = line_break + + # Tag prefixes. + self.tag_prefixes = None + + # Prepared anchor and tag. + self.prepared_anchor = None + self.prepared_tag = None + + # Scalar analysis and style. + self.analysis = None + self.style = None + + def dispose(self): + # Reset the state attributes (to clear self-references) + self.states = [] + self.state = None + + def emit(self, event): + self.events.append(event) + while not self.need_more_events(): + self.event = self.events.pop(0) + self.state() + self.event = None + + # In some cases, we wait for a few next events before emitting. + + def need_more_events(self): + if not self.events: + return True + event = self.events[0] + if isinstance(event, DocumentStartEvent): + return self.need_events(1) + elif isinstance(event, SequenceStartEvent): + return self.need_events(2) + elif isinstance(event, MappingStartEvent): + return self.need_events(3) + else: + return False + + def need_events(self, count): + level = 0 + for event in self.events[1:]: + if isinstance(event, (DocumentStartEvent, CollectionStartEvent)): + level += 1 + elif isinstance(event, (DocumentEndEvent, CollectionEndEvent)): + level -= 1 + elif isinstance(event, StreamEndEvent): + level = -1 + if level < 0: + return False + return (len(self.events) < count+1) + + def increase_indent(self, flow=False, indentless=False): + self.indents.append(self.indent) + if self.indent is None: + if flow: + self.indent = self.best_indent + else: + self.indent = 0 + elif not indentless: + self.indent += self.best_indent + + # States. + + # Stream handlers. + + def expect_stream_start(self): + if isinstance(self.event, StreamStartEvent): + if self.event.encoding and not hasattr(self.stream, 'encoding'): + self.encoding = self.event.encoding + self.write_stream_start() + self.state = self.expect_first_document_start + else: + raise EmitterError("expected StreamStartEvent, but got %s" + % self.event) + + def expect_nothing(self): + raise EmitterError("expected nothing, but got %s" % self.event) + + # Document handlers. + + def expect_first_document_start(self): + return self.expect_document_start(first=True) + + def expect_document_start(self, first=False): + if isinstance(self.event, DocumentStartEvent): + if (self.event.version or self.event.tags) and self.open_ended: + self.write_indicator('...', True) + self.write_indent() + if self.event.version: + version_text = self.prepare_version(self.event.version) + self.write_version_directive(version_text) + self.tag_prefixes = self.DEFAULT_TAG_PREFIXES.copy() + if self.event.tags: + handles = sorted(self.event.tags.keys()) + for handle in handles: + prefix = self.event.tags[handle] + self.tag_prefixes[prefix] = handle + handle_text = self.prepare_tag_handle(handle) + prefix_text = self.prepare_tag_prefix(prefix) + self.write_tag_directive(handle_text, prefix_text) + implicit = (first and not self.event.explicit and not self.canonical + and not self.event.version and not self.event.tags + and not self.check_empty_document()) + if not implicit: + self.write_indent() + self.write_indicator('---', True) + if self.canonical: + self.write_indent() + self.state = self.expect_document_root + elif isinstance(self.event, StreamEndEvent): + if self.open_ended: + self.write_indicator('...', True) + self.write_indent() + self.write_stream_end() + self.state = self.expect_nothing + else: + raise EmitterError("expected DocumentStartEvent, but got %s" + % self.event) + + def expect_document_end(self): + if isinstance(self.event, DocumentEndEvent): + self.write_indent() + if self.event.explicit: + self.write_indicator('...', True) + self.write_indent() + self.flush_stream() + self.state = self.expect_document_start + else: + raise EmitterError("expected DocumentEndEvent, but got %s" + % self.event) + + def expect_document_root(self): + self.states.append(self.expect_document_end) + self.expect_node(root=True) + + # Node handlers. + + def expect_node(self, root=False, sequence=False, mapping=False, + simple_key=False): + self.root_context = root + self.sequence_context = sequence + self.mapping_context = mapping + self.simple_key_context = simple_key + if isinstance(self.event, AliasEvent): + self.expect_alias() + elif isinstance(self.event, (ScalarEvent, CollectionStartEvent)): + self.process_anchor('&') + self.process_tag() + if isinstance(self.event, ScalarEvent): + self.expect_scalar() + elif isinstance(self.event, SequenceStartEvent): + if self.flow_level or self.canonical or self.event.flow_style \ + or self.check_empty_sequence(): + self.expect_flow_sequence() + else: + self.expect_block_sequence() + elif isinstance(self.event, MappingStartEvent): + if self.flow_level or self.canonical or self.event.flow_style \ + or self.check_empty_mapping(): + self.expect_flow_mapping() + else: + self.expect_block_mapping() + else: + raise EmitterError("expected NodeEvent, but got %s" % self.event) + + def expect_alias(self): + if self.event.anchor is None: + raise EmitterError("anchor is not specified for alias") + self.process_anchor('*') + self.state = self.states.pop() + + def expect_scalar(self): + self.increase_indent(flow=True) + self.process_scalar() + self.indent = self.indents.pop() + self.state = self.states.pop() + + # Flow sequence handlers. + + def expect_flow_sequence(self): + self.write_indicator('[', True, whitespace=True) + self.flow_level += 1 + self.increase_indent(flow=True) + self.state = self.expect_first_flow_sequence_item + + def expect_first_flow_sequence_item(self): + if isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + self.write_indicator(']', False) + self.state = self.states.pop() + else: + if self.canonical or self.column > self.best_width: + self.write_indent() + self.states.append(self.expect_flow_sequence_item) + self.expect_node(sequence=True) + + def expect_flow_sequence_item(self): + if isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + if self.canonical: + self.write_indicator(',', False) + self.write_indent() + self.write_indicator(']', False) + self.state = self.states.pop() + else: + self.write_indicator(',', False) + if self.canonical or self.column > self.best_width: + self.write_indent() + self.states.append(self.expect_flow_sequence_item) + self.expect_node(sequence=True) + + # Flow mapping handlers. + + def expect_flow_mapping(self): + self.write_indicator('{', True, whitespace=True) + self.flow_level += 1 + self.increase_indent(flow=True) + self.state = self.expect_first_flow_mapping_key + + def expect_first_flow_mapping_key(self): + if isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + self.write_indicator('}', False) + self.state = self.states.pop() + else: + if self.canonical or self.column > self.best_width: + self.write_indent() + if not self.canonical and self.check_simple_key(): + self.states.append(self.expect_flow_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator('?', True) + self.states.append(self.expect_flow_mapping_value) + self.expect_node(mapping=True) + + def expect_flow_mapping_key(self): + if isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + if self.canonical: + self.write_indicator(',', False) + self.write_indent() + self.write_indicator('}', False) + self.state = self.states.pop() + else: + self.write_indicator(',', False) + if self.canonical or self.column > self.best_width: + self.write_indent() + if not self.canonical and self.check_simple_key(): + self.states.append(self.expect_flow_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator('?', True) + self.states.append(self.expect_flow_mapping_value) + self.expect_node(mapping=True) + + def expect_flow_mapping_simple_value(self): + self.write_indicator(':', False) + self.states.append(self.expect_flow_mapping_key) + self.expect_node(mapping=True) + + def expect_flow_mapping_value(self): + if self.canonical or self.column > self.best_width: + self.write_indent() + self.write_indicator(':', True) + self.states.append(self.expect_flow_mapping_key) + self.expect_node(mapping=True) + + # Block sequence handlers. + + def expect_block_sequence(self): + indentless = (self.mapping_context and not self.indention) + self.increase_indent(flow=False, indentless=indentless) + self.state = self.expect_first_block_sequence_item + + def expect_first_block_sequence_item(self): + return self.expect_block_sequence_item(first=True) + + def expect_block_sequence_item(self, first=False): + if not first and isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.state = self.states.pop() + else: + self.write_indent() + self.write_indicator('-', True, indention=True) + self.states.append(self.expect_block_sequence_item) + self.expect_node(sequence=True) + + # Block mapping handlers. + + def expect_block_mapping(self): + self.increase_indent(flow=False) + self.state = self.expect_first_block_mapping_key + + def expect_first_block_mapping_key(self): + return self.expect_block_mapping_key(first=True) + + def expect_block_mapping_key(self, first=False): + if not first and isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.state = self.states.pop() + else: + self.write_indent() + if self.check_simple_key(): + self.states.append(self.expect_block_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator('?', True, indention=True) + self.states.append(self.expect_block_mapping_value) + self.expect_node(mapping=True) + + def expect_block_mapping_simple_value(self): + self.write_indicator(':', False) + self.states.append(self.expect_block_mapping_key) + self.expect_node(mapping=True) + + def expect_block_mapping_value(self): + self.write_indent() + self.write_indicator(':', True, indention=True) + self.states.append(self.expect_block_mapping_key) + self.expect_node(mapping=True) + + # Checkers. + + def check_empty_sequence(self): + return (isinstance(self.event, SequenceStartEvent) and self.events + and isinstance(self.events[0], SequenceEndEvent)) + + def check_empty_mapping(self): + return (isinstance(self.event, MappingStartEvent) and self.events + and isinstance(self.events[0], MappingEndEvent)) + + def check_empty_document(self): + if not isinstance(self.event, DocumentStartEvent) or not self.events: + return False + event = self.events[0] + return (isinstance(event, ScalarEvent) and event.anchor is None + and event.tag is None and event.implicit and event.value == '') + + def check_simple_key(self): + length = 0 + if isinstance(self.event, NodeEvent) and self.event.anchor is not None: + if self.prepared_anchor is None: + self.prepared_anchor = self.prepare_anchor(self.event.anchor) + length += len(self.prepared_anchor) + if isinstance(self.event, (ScalarEvent, CollectionStartEvent)) \ + and self.event.tag is not None: + if self.prepared_tag is None: + self.prepared_tag = self.prepare_tag(self.event.tag) + length += len(self.prepared_tag) + if isinstance(self.event, ScalarEvent): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + length += len(self.analysis.scalar) + return (length < 128 and (isinstance(self.event, AliasEvent) + or (isinstance(self.event, ScalarEvent) + and not self.analysis.empty and not self.analysis.multiline) + or self.check_empty_sequence() or self.check_empty_mapping())) + + # Anchor, Tag, and Scalar processors. + + def process_anchor(self, indicator): + if self.event.anchor is None: + self.prepared_anchor = None + return + if self.prepared_anchor is None: + self.prepared_anchor = self.prepare_anchor(self.event.anchor) + if self.prepared_anchor: + self.write_indicator(indicator+self.prepared_anchor, True) + self.prepared_anchor = None + + def process_tag(self): + tag = self.event.tag + if isinstance(self.event, ScalarEvent): + if self.style is None: + self.style = self.choose_scalar_style() + if ((not self.canonical or tag is None) and + ((self.style == '' and self.event.implicit[0]) + or (self.style != '' and self.event.implicit[1]))): + self.prepared_tag = None + return + if self.event.implicit[0] and tag is None: + tag = '!' + self.prepared_tag = None + else: + if (not self.canonical or tag is None) and self.event.implicit: + self.prepared_tag = None + return + if tag is None: + raise EmitterError("tag is not specified") + if self.prepared_tag is None: + self.prepared_tag = self.prepare_tag(tag) + if self.prepared_tag: + self.write_indicator(self.prepared_tag, True) + self.prepared_tag = None + + def choose_scalar_style(self): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + if self.event.style == '"' or self.canonical: + return '"' + if not self.event.style and self.event.implicit[0]: + if (not (self.simple_key_context and + (self.analysis.empty or self.analysis.multiline)) + and (self.flow_level and self.analysis.allow_flow_plain + or (not self.flow_level and self.analysis.allow_block_plain))): + return '' + if self.event.style and self.event.style in '|>': + if (not self.flow_level and not self.simple_key_context + and self.analysis.allow_block): + return self.event.style + if not self.event.style or self.event.style == '\'': + if (self.analysis.allow_single_quoted and + not (self.simple_key_context and self.analysis.multiline)): + return '\'' + return '"' + + def process_scalar(self): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + if self.style is None: + self.style = self.choose_scalar_style() + split = (not self.simple_key_context) + #if self.analysis.multiline and split \ + # and (not self.style or self.style in '\'\"'): + # self.write_indent() + if self.style == '"': + self.write_double_quoted(self.analysis.scalar, split) + elif self.style == '\'': + self.write_single_quoted(self.analysis.scalar, split) + elif self.style == '>': + self.write_folded(self.analysis.scalar) + elif self.style == '|': + self.write_literal(self.analysis.scalar) + else: + self.write_plain(self.analysis.scalar, split) + self.analysis = None + self.style = None + + # Analyzers. + + def prepare_version(self, version): + major, minor = version + if major != 1: + raise EmitterError("unsupported YAML version: %d.%d" % (major, minor)) + return '%d.%d' % (major, minor) + + def prepare_tag_handle(self, handle): + if not handle: + raise EmitterError("tag handle must not be empty") + if handle[0] != '!' or handle[-1] != '!': + raise EmitterError("tag handle must start and end with '!': %r" % handle) + for ch in handle[1:-1]: + if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_'): + raise EmitterError("invalid character %r in the tag handle: %r" + % (ch, handle)) + return handle + + def prepare_tag_prefix(self, prefix): + if not prefix: + raise EmitterError("tag prefix must not be empty") + chunks = [] + start = end = 0 + if prefix[0] == '!': + end = 1 + while end < len(prefix): + ch = prefix[end] + if '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-;/?!:@&=+$,_.~*\'()[]': + end += 1 + else: + if start < end: + chunks.append(prefix[start:end]) + start = end = end+1 + data = ch.encode('utf-8') + for ch in data: + chunks.append('%%%02X' % ord(ch)) + if start < end: + chunks.append(prefix[start:end]) + return ''.join(chunks) + + def prepare_tag(self, tag): + if not tag: + raise EmitterError("tag must not be empty") + if tag == '!': + return tag + handle = None + suffix = tag + prefixes = sorted(self.tag_prefixes.keys()) + for prefix in prefixes: + if tag.startswith(prefix) \ + and (prefix == '!' or len(prefix) < len(tag)): + handle = self.tag_prefixes[prefix] + suffix = tag[len(prefix):] + chunks = [] + start = end = 0 + while end < len(suffix): + ch = suffix[end] + if '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-;/?:@&=+$,_.~*\'()[]' \ + or (ch == '!' and handle != '!'): + end += 1 + else: + if start < end: + chunks.append(suffix[start:end]) + start = end = end+1 + data = ch.encode('utf-8') + for ch in data: + chunks.append('%%%02X' % ch) + if start < end: + chunks.append(suffix[start:end]) + suffix_text = ''.join(chunks) + if handle: + return '%s%s' % (handle, suffix_text) + else: + return '!<%s>' % suffix_text + + def prepare_anchor(self, anchor): + if not anchor: + raise EmitterError("anchor must not be empty") + for ch in anchor: + if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_'): + raise EmitterError("invalid character %r in the anchor: %r" + % (ch, anchor)) + return anchor + + def analyze_scalar(self, scalar): + + # Empty scalar is a special case. + if not scalar: + return ScalarAnalysis(scalar=scalar, empty=True, multiline=False, + allow_flow_plain=False, allow_block_plain=True, + allow_single_quoted=True, allow_double_quoted=True, + allow_block=False) + + # Indicators and special characters. + block_indicators = False + flow_indicators = False + line_breaks = False + special_characters = False + + # Important whitespace combinations. + leading_space = False + leading_break = False + trailing_space = False + trailing_break = False + break_space = False + space_break = False + + # Check document indicators. + if scalar.startswith('---') or scalar.startswith('...'): + block_indicators = True + flow_indicators = True + + # First character or preceded by a whitespace. + preceded_by_whitespace = True + + # Last character or followed by a whitespace. + followed_by_whitespace = (len(scalar) == 1 or + scalar[1] in '\0 \t\r\n\x85\u2028\u2029') + + # The previous character is a space. + previous_space = False + + # The previous character is a break. + previous_break = False + + index = 0 + while index < len(scalar): + ch = scalar[index] + + # Check for indicators. + if index == 0: + # Leading indicators are special characters. + if ch in '#,[]{}&*!|>\'\"%@`': + flow_indicators = True + block_indicators = True + if ch in '?:': + flow_indicators = True + if followed_by_whitespace: + block_indicators = True + if ch == '-' and followed_by_whitespace: + flow_indicators = True + block_indicators = True + else: + # Some indicators cannot appear within a scalar as well. + if ch in ',?[]{}': + flow_indicators = True + if ch == ':': + flow_indicators = True + if followed_by_whitespace: + block_indicators = True + if ch == '#' and preceded_by_whitespace: + flow_indicators = True + block_indicators = True + + # Check for line breaks, special, and unicode characters. + if ch in '\n\x85\u2028\u2029': + line_breaks = True + if not (ch == '\n' or '\x20' <= ch <= '\x7E'): + if (ch == '\x85' or '\xA0' <= ch <= '\uD7FF' + or '\uE000' <= ch <= '\uFFFD' + or '\U00010000' <= ch < '\U0010ffff') and ch != '\uFEFF': + unicode_characters = True + if not self.allow_unicode: + special_characters = True + else: + special_characters = True + + # Detect important whitespace combinations. + if ch == ' ': + if index == 0: + leading_space = True + if index == len(scalar)-1: + trailing_space = True + if previous_break: + break_space = True + previous_space = True + previous_break = False + elif ch in '\n\x85\u2028\u2029': + if index == 0: + leading_break = True + if index == len(scalar)-1: + trailing_break = True + if previous_space: + space_break = True + previous_space = False + previous_break = True + else: + previous_space = False + previous_break = False + + # Prepare for the next character. + index += 1 + preceded_by_whitespace = (ch in '\0 \t\r\n\x85\u2028\u2029') + followed_by_whitespace = (index+1 >= len(scalar) or + scalar[index+1] in '\0 \t\r\n\x85\u2028\u2029') + + # Let's decide what styles are allowed. + allow_flow_plain = True + allow_block_plain = True + allow_single_quoted = True + allow_double_quoted = True + allow_block = True + + # Leading and trailing whitespaces are bad for plain scalars. + if (leading_space or leading_break + or trailing_space or trailing_break): + allow_flow_plain = allow_block_plain = False + + # We do not permit trailing spaces for block scalars. + if trailing_space: + allow_block = False + + # Spaces at the beginning of a new line are only acceptable for block + # scalars. + if break_space: + allow_flow_plain = allow_block_plain = allow_single_quoted = False + + # Spaces followed by breaks, as well as special character are only + # allowed for double quoted scalars. + if space_break or special_characters: + allow_flow_plain = allow_block_plain = \ + allow_single_quoted = allow_block = False + + # Although the plain scalar writer supports breaks, we never emit + # multiline plain scalars. + if line_breaks: + allow_flow_plain = allow_block_plain = False + + # Flow indicators are forbidden for flow plain scalars. + if flow_indicators: + allow_flow_plain = False + + # Block indicators are forbidden for block plain scalars. + if block_indicators: + allow_block_plain = False + + return ScalarAnalysis(scalar=scalar, + empty=False, multiline=line_breaks, + allow_flow_plain=allow_flow_plain, + allow_block_plain=allow_block_plain, + allow_single_quoted=allow_single_quoted, + allow_double_quoted=allow_double_quoted, + allow_block=allow_block) + + # Writers. + + def flush_stream(self): + if hasattr(self.stream, 'flush'): + self.stream.flush() + + def write_stream_start(self): + # Write BOM if needed. + if self.encoding and self.encoding.startswith('utf-16'): + self.stream.write('\uFEFF'.encode(self.encoding)) + + def write_stream_end(self): + self.flush_stream() + + def write_indicator(self, indicator, need_whitespace, + whitespace=False, indention=False): + if self.whitespace or not need_whitespace: + data = indicator + else: + data = ' '+indicator + self.whitespace = whitespace + self.indention = self.indention and indention + self.column += len(data) + self.open_ended = False + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_indent(self): + indent = self.indent or 0 + if not self.indention or self.column > indent \ + or (self.column == indent and not self.whitespace): + self.write_line_break() + if self.column < indent: + self.whitespace = True + data = ' '*(indent-self.column) + self.column = indent + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_line_break(self, data=None): + if data is None: + data = self.best_line_break + self.whitespace = True + self.indention = True + self.line += 1 + self.column = 0 + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_version_directive(self, version_text): + data = '%%YAML %s' % version_text + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_line_break() + + def write_tag_directive(self, handle_text, prefix_text): + data = '%%TAG %s %s' % (handle_text, prefix_text) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_line_break() + + # Scalar streams. + + def write_single_quoted(self, text, split=True): + self.write_indicator('\'', True) + spaces = False + breaks = False + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if spaces: + if ch is None or ch != ' ': + if start+1 == end and self.column > self.best_width and split \ + and start != 0 and end != len(text): + self.write_indent() + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + elif breaks: + if ch is None or ch not in '\n\x85\u2028\u2029': + if text[start] == '\n': + self.write_line_break() + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + self.write_indent() + start = end + else: + if ch is None or ch in ' \n\x85\u2028\u2029' or ch == '\'': + if start < end: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch == '\'': + data = '\'\'' + self.column += 2 + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + 1 + if ch is not None: + spaces = (ch == ' ') + breaks = (ch in '\n\x85\u2028\u2029') + end += 1 + self.write_indicator('\'', False) + + ESCAPE_REPLACEMENTS = { + '\0': '0', + '\x07': 'a', + '\x08': 'b', + '\x09': 't', + '\x0A': 'n', + '\x0B': 'v', + '\x0C': 'f', + '\x0D': 'r', + '\x1B': 'e', + '\"': '\"', + '\\': '\\', + '\x85': 'N', + '\xA0': '_', + '\u2028': 'L', + '\u2029': 'P', + } + + def write_double_quoted(self, text, split=True): + self.write_indicator('"', True) + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if ch is None or ch in '"\\\x85\u2028\u2029\uFEFF' \ + or not ('\x20' <= ch <= '\x7E' + or (self.allow_unicode + and ('\xA0' <= ch <= '\uD7FF' + or '\uE000' <= ch <= '\uFFFD'))): + if start < end: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch is not None: + if ch in self.ESCAPE_REPLACEMENTS: + data = '\\'+self.ESCAPE_REPLACEMENTS[ch] + elif ch <= '\xFF': + data = '\\x%02X' % ord(ch) + elif ch <= '\uFFFF': + data = '\\u%04X' % ord(ch) + else: + data = '\\U%08X' % ord(ch) + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end+1 + if 0 < end < len(text)-1 and (ch == ' ' or start >= end) \ + and self.column+(end-start) > self.best_width and split: + data = text[start:end]+'\\' + if start < end: + start = end + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_indent() + self.whitespace = False + self.indention = False + if text[start] == ' ': + data = '\\' + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + end += 1 + self.write_indicator('"', False) + + def determine_block_hints(self, text): + hints = '' + if text: + if text[0] in ' \n\x85\u2028\u2029': + hints += str(self.best_indent) + if text[-1] not in '\n\x85\u2028\u2029': + hints += '-' + elif len(text) == 1 or text[-2] in '\n\x85\u2028\u2029': + hints += '+' + return hints + + def write_folded(self, text): + hints = self.determine_block_hints(text) + self.write_indicator('>'+hints, True) + if hints[-1:] == '+': + self.open_ended = True + self.write_line_break() + leading_space = True + spaces = False + breaks = True + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if breaks: + if ch is None or ch not in '\n\x85\u2028\u2029': + if not leading_space and ch is not None and ch != ' ' \ + and text[start] == '\n': + self.write_line_break() + leading_space = (ch == ' ') + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + if ch is not None: + self.write_indent() + start = end + elif spaces: + if ch != ' ': + if start+1 == end and self.column > self.best_width: + self.write_indent() + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + else: + if ch is None or ch in ' \n\x85\u2028\u2029': + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + if ch is None: + self.write_line_break() + start = end + if ch is not None: + breaks = (ch in '\n\x85\u2028\u2029') + spaces = (ch == ' ') + end += 1 + + def write_literal(self, text): + hints = self.determine_block_hints(text) + self.write_indicator('|'+hints, True) + if hints[-1:] == '+': + self.open_ended = True + self.write_line_break() + breaks = True + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if breaks: + if ch is None or ch not in '\n\x85\u2028\u2029': + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + if ch is not None: + self.write_indent() + start = end + else: + if ch is None or ch in '\n\x85\u2028\u2029': + data = text[start:end] + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + if ch is None: + self.write_line_break() + start = end + if ch is not None: + breaks = (ch in '\n\x85\u2028\u2029') + end += 1 + + def write_plain(self, text, split=True): + if self.root_context: + self.open_ended = True + if not text: + return + if not self.whitespace: + data = ' ' + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.whitespace = False + self.indention = False + spaces = False + breaks = False + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if spaces: + if ch != ' ': + if start+1 == end and self.column > self.best_width and split: + self.write_indent() + self.whitespace = False + self.indention = False + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + elif breaks: + if ch not in '\n\x85\u2028\u2029': + if text[start] == '\n': + self.write_line_break() + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + self.write_indent() + self.whitespace = False + self.indention = False + start = end + else: + if ch is None or ch in ' \n\x85\u2028\u2029': + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch is not None: + spaces = (ch == ' ') + breaks = (ch in '\n\x85\u2028\u2029') + end += 1 diff --git a/dist/ba_data/python-site-packages/yaml/error.py b/dist/ba_data/python-site-packages/yaml/error.py new file mode 100644 index 0000000..b796b4d --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/error.py @@ -0,0 +1,75 @@ + +__all__ = ['Mark', 'YAMLError', 'MarkedYAMLError'] + +class Mark: + + def __init__(self, name, index, line, column, buffer, pointer): + self.name = name + self.index = index + self.line = line + self.column = column + self.buffer = buffer + self.pointer = pointer + + def get_snippet(self, indent=4, max_length=75): + if self.buffer is None: + return None + head = '' + start = self.pointer + while start > 0 and self.buffer[start-1] not in '\0\r\n\x85\u2028\u2029': + start -= 1 + if self.pointer-start > max_length/2-1: + head = ' ... ' + start += 5 + break + tail = '' + end = self.pointer + while end < len(self.buffer) and self.buffer[end] not in '\0\r\n\x85\u2028\u2029': + end += 1 + if end-self.pointer > max_length/2-1: + tail = ' ... ' + end -= 5 + break + snippet = self.buffer[start:end] + return ' '*indent + head + snippet + tail + '\n' \ + + ' '*(indent+self.pointer-start+len(head)) + '^' + + def __str__(self): + snippet = self.get_snippet() + where = " in \"%s\", line %d, column %d" \ + % (self.name, self.line+1, self.column+1) + if snippet is not None: + where += ":\n"+snippet + return where + +class YAMLError(Exception): + pass + +class MarkedYAMLError(YAMLError): + + def __init__(self, context=None, context_mark=None, + problem=None, problem_mark=None, note=None): + self.context = context + self.context_mark = context_mark + self.problem = problem + self.problem_mark = problem_mark + self.note = note + + def __str__(self): + lines = [] + if self.context is not None: + lines.append(self.context) + if self.context_mark is not None \ + and (self.problem is None or self.problem_mark is None + or self.context_mark.name != self.problem_mark.name + or self.context_mark.line != self.problem_mark.line + or self.context_mark.column != self.problem_mark.column): + lines.append(str(self.context_mark)) + if self.problem is not None: + lines.append(self.problem) + if self.problem_mark is not None: + lines.append(str(self.problem_mark)) + if self.note is not None: + lines.append(self.note) + return '\n'.join(lines) + diff --git a/dist/ba_data/python-site-packages/yaml/events.py b/dist/ba_data/python-site-packages/yaml/events.py new file mode 100644 index 0000000..f79ad38 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/events.py @@ -0,0 +1,86 @@ + +# Abstract classes. + +class Event(object): + def __init__(self, start_mark=None, end_mark=None): + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + attributes = [key for key in ['anchor', 'tag', 'implicit', 'value'] + if hasattr(self, key)] + arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) + for key in attributes]) + return '%s(%s)' % (self.__class__.__name__, arguments) + +class NodeEvent(Event): + def __init__(self, anchor, start_mark=None, end_mark=None): + self.anchor = anchor + self.start_mark = start_mark + self.end_mark = end_mark + +class CollectionStartEvent(NodeEvent): + def __init__(self, anchor, tag, implicit, start_mark=None, end_mark=None, + flow_style=None): + self.anchor = anchor + self.tag = tag + self.implicit = implicit + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + +class CollectionEndEvent(Event): + pass + +# Implementations. + +class StreamStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + +class StreamEndEvent(Event): + pass + +class DocumentStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, + explicit=None, version=None, tags=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + self.version = version + self.tags = tags + +class DocumentEndEvent(Event): + def __init__(self, start_mark=None, end_mark=None, + explicit=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + +class AliasEvent(NodeEvent): + pass + +class ScalarEvent(NodeEvent): + def __init__(self, anchor, tag, implicit, value, + start_mark=None, end_mark=None, style=None): + self.anchor = anchor + self.tag = tag + self.implicit = implicit + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + +class SequenceStartEvent(CollectionStartEvent): + pass + +class SequenceEndEvent(CollectionEndEvent): + pass + +class MappingStartEvent(CollectionStartEvent): + pass + +class MappingEndEvent(CollectionEndEvent): + pass + diff --git a/dist/ba_data/python-site-packages/yaml/loader.py b/dist/ba_data/python-site-packages/yaml/loader.py new file mode 100644 index 0000000..e90c112 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/loader.py @@ -0,0 +1,63 @@ + +__all__ = ['BaseLoader', 'FullLoader', 'SafeLoader', 'Loader', 'UnsafeLoader'] + +from .reader import * +from .scanner import * +from .parser import * +from .composer import * +from .constructor import * +from .resolver import * + +class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, BaseResolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + BaseConstructor.__init__(self) + BaseResolver.__init__(self) + +class FullLoader(Reader, Scanner, Parser, Composer, FullConstructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + FullConstructor.__init__(self) + Resolver.__init__(self) + +class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + SafeConstructor.__init__(self) + Resolver.__init__(self) + +class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + Constructor.__init__(self) + Resolver.__init__(self) + +# UnsafeLoader is the same as Loader (which is and was always unsafe on +# untrusted input). Use of either Loader or UnsafeLoader should be rare, since +# FullLoad should be able to load almost all YAML safely. Loader is left intact +# to ensure backwards compatibility. +class UnsafeLoader(Reader, Scanner, Parser, Composer, Constructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + Constructor.__init__(self) + Resolver.__init__(self) diff --git a/dist/ba_data/python-site-packages/yaml/nodes.py b/dist/ba_data/python-site-packages/yaml/nodes.py new file mode 100644 index 0000000..c4f070c --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/nodes.py @@ -0,0 +1,49 @@ + +class Node(object): + def __init__(self, tag, value, start_mark, end_mark): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + value = self.value + #if isinstance(value, list): + # if len(value) == 0: + # value = '' + # elif len(value) == 1: + # value = '<1 item>' + # else: + # value = '<%d items>' % len(value) + #else: + # if len(value) > 75: + # value = repr(value[:70]+u' ... ') + # else: + # value = repr(value) + value = repr(value) + return '%s(tag=%r, value=%s)' % (self.__class__.__name__, self.tag, value) + +class ScalarNode(Node): + id = 'scalar' + def __init__(self, tag, value, + start_mark=None, end_mark=None, style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + +class CollectionNode(Node): + def __init__(self, tag, value, + start_mark=None, end_mark=None, flow_style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + +class SequenceNode(CollectionNode): + id = 'sequence' + +class MappingNode(CollectionNode): + id = 'mapping' + diff --git a/dist/ba_data/python-site-packages/yaml/parser.py b/dist/ba_data/python-site-packages/yaml/parser.py new file mode 100644 index 0000000..13a5995 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/parser.py @@ -0,0 +1,589 @@ + +# The following YAML grammar is LL(1) and is parsed by a recursive descent +# parser. +# +# stream ::= STREAM-START implicit_document? explicit_document* STREAM-END +# implicit_document ::= block_node DOCUMENT-END* +# explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +# block_node_or_indentless_sequence ::= +# ALIAS +# | properties (block_content | indentless_block_sequence)? +# | block_content +# | indentless_block_sequence +# block_node ::= ALIAS +# | properties block_content? +# | block_content +# flow_node ::= ALIAS +# | properties flow_content? +# | flow_content +# properties ::= TAG ANCHOR? | ANCHOR TAG? +# block_content ::= block_collection | flow_collection | SCALAR +# flow_content ::= flow_collection | SCALAR +# block_collection ::= block_sequence | block_mapping +# flow_collection ::= flow_sequence | flow_mapping +# block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +# indentless_sequence ::= (BLOCK-ENTRY block_node?)+ +# block_mapping ::= BLOCK-MAPPING_START +# ((KEY block_node_or_indentless_sequence?)? +# (VALUE block_node_or_indentless_sequence?)?)* +# BLOCK-END +# flow_sequence ::= FLOW-SEQUENCE-START +# (flow_sequence_entry FLOW-ENTRY)* +# flow_sequence_entry? +# FLOW-SEQUENCE-END +# flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +# flow_mapping ::= FLOW-MAPPING-START +# (flow_mapping_entry FLOW-ENTRY)* +# flow_mapping_entry? +# FLOW-MAPPING-END +# flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +# +# FIRST sets: +# +# stream: { STREAM-START } +# explicit_document: { DIRECTIVE DOCUMENT-START } +# implicit_document: FIRST(block_node) +# block_node: { ALIAS TAG ANCHOR SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START } +# flow_node: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START } +# block_content: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } +# flow_content: { FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } +# block_collection: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START } +# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } +# block_sequence: { BLOCK-SEQUENCE-START } +# block_mapping: { BLOCK-MAPPING-START } +# block_node_or_indentless_sequence: { ALIAS ANCHOR TAG SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START BLOCK-ENTRY } +# indentless_sequence: { ENTRY } +# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } +# flow_sequence: { FLOW-SEQUENCE-START } +# flow_mapping: { FLOW-MAPPING-START } +# flow_sequence_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } +# flow_mapping_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } + +__all__ = ['Parser', 'ParserError'] + +from .error import MarkedYAMLError +from .tokens import * +from .events import * +from .scanner import * + +class ParserError(MarkedYAMLError): + pass + +class Parser: + # Since writing a recursive-descendant parser is a straightforward task, we + # do not give many comments here. + + DEFAULT_TAGS = { + '!': '!', + '!!': 'tag:yaml.org,2002:', + } + + def __init__(self): + self.current_event = None + self.yaml_version = None + self.tag_handles = {} + self.states = [] + self.marks = [] + self.state = self.parse_stream_start + + def dispose(self): + # Reset the state attributes (to clear self-references) + self.states = [] + self.state = None + + def check_event(self, *choices): + # Check the type of the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + if self.current_event is not None: + if not choices: + return True + for choice in choices: + if isinstance(self.current_event, choice): + return True + return False + + def peek_event(self): + # Get the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + return self.current_event + + def get_event(self): + # Get the next event and proceed further. + if self.current_event is None: + if self.state: + self.current_event = self.state() + value = self.current_event + self.current_event = None + return value + + # stream ::= STREAM-START implicit_document? explicit_document* STREAM-END + # implicit_document ::= block_node DOCUMENT-END* + # explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + + def parse_stream_start(self): + + # Parse the stream start. + token = self.get_token() + event = StreamStartEvent(token.start_mark, token.end_mark, + encoding=token.encoding) + + # Prepare the next state. + self.state = self.parse_implicit_document_start + + return event + + def parse_implicit_document_start(self): + + # Parse an implicit document. + if not self.check_token(DirectiveToken, DocumentStartToken, + StreamEndToken): + self.tag_handles = self.DEFAULT_TAGS + token = self.peek_token() + start_mark = end_mark = token.start_mark + event = DocumentStartEvent(start_mark, end_mark, + explicit=False) + + # Prepare the next state. + self.states.append(self.parse_document_end) + self.state = self.parse_block_node + + return event + + else: + return self.parse_document_start() + + def parse_document_start(self): + + # Parse any extra document end indicators. + while self.check_token(DocumentEndToken): + self.get_token() + + # Parse an explicit document. + if not self.check_token(StreamEndToken): + token = self.peek_token() + start_mark = token.start_mark + version, tags = self.process_directives() + if not self.check_token(DocumentStartToken): + raise ParserError(None, None, + "expected '', but found %r" + % self.peek_token().id, + self.peek_token().start_mark) + token = self.get_token() + end_mark = token.end_mark + event = DocumentStartEvent(start_mark, end_mark, + explicit=True, version=version, tags=tags) + self.states.append(self.parse_document_end) + self.state = self.parse_document_content + else: + # Parse the end of the stream. + token = self.get_token() + event = StreamEndEvent(token.start_mark, token.end_mark) + assert not self.states + assert not self.marks + self.state = None + return event + + def parse_document_end(self): + + # Parse the document end. + token = self.peek_token() + start_mark = end_mark = token.start_mark + explicit = False + if self.check_token(DocumentEndToken): + token = self.get_token() + end_mark = token.end_mark + explicit = True + event = DocumentEndEvent(start_mark, end_mark, + explicit=explicit) + + # Prepare the next state. + self.state = self.parse_document_start + + return event + + def parse_document_content(self): + if self.check_token(DirectiveToken, + DocumentStartToken, DocumentEndToken, StreamEndToken): + event = self.process_empty_scalar(self.peek_token().start_mark) + self.state = self.states.pop() + return event + else: + return self.parse_block_node() + + def process_directives(self): + self.yaml_version = None + self.tag_handles = {} + while self.check_token(DirectiveToken): + token = self.get_token() + if token.name == 'YAML': + if self.yaml_version is not None: + raise ParserError(None, None, + "found duplicate YAML directive", token.start_mark) + major, minor = token.value + if major != 1: + raise ParserError(None, None, + "found incompatible YAML document (version 1.* is required)", + token.start_mark) + self.yaml_version = token.value + elif token.name == 'TAG': + handle, prefix = token.value + if handle in self.tag_handles: + raise ParserError(None, None, + "duplicate tag handle %r" % handle, + token.start_mark) + self.tag_handles[handle] = prefix + if self.tag_handles: + value = self.yaml_version, self.tag_handles.copy() + else: + value = self.yaml_version, None + for key in self.DEFAULT_TAGS: + if key not in self.tag_handles: + self.tag_handles[key] = self.DEFAULT_TAGS[key] + return value + + # block_node_or_indentless_sequence ::= ALIAS + # | properties (block_content | indentless_block_sequence)? + # | block_content + # | indentless_block_sequence + # block_node ::= ALIAS + # | properties block_content? + # | block_content + # flow_node ::= ALIAS + # | properties flow_content? + # | flow_content + # properties ::= TAG ANCHOR? | ANCHOR TAG? + # block_content ::= block_collection | flow_collection | SCALAR + # flow_content ::= flow_collection | SCALAR + # block_collection ::= block_sequence | block_mapping + # flow_collection ::= flow_sequence | flow_mapping + + def parse_block_node(self): + return self.parse_node(block=True) + + def parse_flow_node(self): + return self.parse_node() + + def parse_block_node_or_indentless_sequence(self): + return self.parse_node(block=True, indentless_sequence=True) + + def parse_node(self, block=False, indentless_sequence=False): + if self.check_token(AliasToken): + token = self.get_token() + event = AliasEvent(token.value, token.start_mark, token.end_mark) + self.state = self.states.pop() + else: + anchor = None + tag = None + start_mark = end_mark = tag_mark = None + if self.check_token(AnchorToken): + token = self.get_token() + start_mark = token.start_mark + end_mark = token.end_mark + anchor = token.value + if self.check_token(TagToken): + token = self.get_token() + tag_mark = token.start_mark + end_mark = token.end_mark + tag = token.value + elif self.check_token(TagToken): + token = self.get_token() + start_mark = tag_mark = token.start_mark + end_mark = token.end_mark + tag = token.value + if self.check_token(AnchorToken): + token = self.get_token() + end_mark = token.end_mark + anchor = token.value + if tag is not None: + handle, suffix = tag + if handle is not None: + if handle not in self.tag_handles: + raise ParserError("while parsing a node", start_mark, + "found undefined tag handle %r" % handle, + tag_mark) + tag = self.tag_handles[handle]+suffix + else: + tag = suffix + #if tag == '!': + # raise ParserError("while parsing a node", start_mark, + # "found non-specific tag '!'", tag_mark, + # "Please check 'http://pyyaml.org/wiki/YAMLNonSpecificTag' and share your opinion.") + if start_mark is None: + start_mark = end_mark = self.peek_token().start_mark + event = None + implicit = (tag is None or tag == '!') + if indentless_sequence and self.check_token(BlockEntryToken): + end_mark = self.peek_token().end_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark) + self.state = self.parse_indentless_sequence_entry + else: + if self.check_token(ScalarToken): + token = self.get_token() + end_mark = token.end_mark + if (token.plain and tag is None) or tag == '!': + implicit = (True, False) + elif tag is None: + implicit = (False, True) + else: + implicit = (False, False) + event = ScalarEvent(anchor, tag, implicit, token.value, + start_mark, end_mark, style=token.style) + self.state = self.states.pop() + elif self.check_token(FlowSequenceStartToken): + end_mark = self.peek_token().end_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_sequence_first_entry + elif self.check_token(FlowMappingStartToken): + end_mark = self.peek_token().end_mark + event = MappingStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_mapping_first_key + elif block and self.check_token(BlockSequenceStartToken): + end_mark = self.peek_token().start_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=False) + self.state = self.parse_block_sequence_first_entry + elif block and self.check_token(BlockMappingStartToken): + end_mark = self.peek_token().start_mark + event = MappingStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=False) + self.state = self.parse_block_mapping_first_key + elif anchor is not None or tag is not None: + # Empty scalars are allowed even if a tag or an anchor is + # specified. + event = ScalarEvent(anchor, tag, (implicit, False), '', + start_mark, end_mark) + self.state = self.states.pop() + else: + if block: + node = 'block' + else: + node = 'flow' + token = self.peek_token() + raise ParserError("while parsing a %s node" % node, start_mark, + "expected the node content, but found %r" % token.id, + token.start_mark) + return event + + # block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END + + def parse_block_sequence_first_entry(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_block_sequence_entry() + + def parse_block_sequence_entry(self): + if self.check_token(BlockEntryToken): + token = self.get_token() + if not self.check_token(BlockEntryToken, BlockEndToken): + self.states.append(self.parse_block_sequence_entry) + return self.parse_block_node() + else: + self.state = self.parse_block_sequence_entry + return self.process_empty_scalar(token.end_mark) + if not self.check_token(BlockEndToken): + token = self.peek_token() + raise ParserError("while parsing a block collection", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark) + token = self.get_token() + event = SequenceEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + # indentless_sequence ::= (BLOCK-ENTRY block_node?)+ + + def parse_indentless_sequence_entry(self): + if self.check_token(BlockEntryToken): + token = self.get_token() + if not self.check_token(BlockEntryToken, + KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_indentless_sequence_entry) + return self.parse_block_node() + else: + self.state = self.parse_indentless_sequence_entry + return self.process_empty_scalar(token.end_mark) + token = self.peek_token() + event = SequenceEndEvent(token.start_mark, token.start_mark) + self.state = self.states.pop() + return event + + # block_mapping ::= BLOCK-MAPPING_START + # ((KEY block_node_or_indentless_sequence?)? + # (VALUE block_node_or_indentless_sequence?)?)* + # BLOCK-END + + def parse_block_mapping_first_key(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_block_mapping_key() + + def parse_block_mapping_key(self): + if self.check_token(KeyToken): + token = self.get_token() + if not self.check_token(KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_block_mapping_value) + return self.parse_block_node_or_indentless_sequence() + else: + self.state = self.parse_block_mapping_value + return self.process_empty_scalar(token.end_mark) + if not self.check_token(BlockEndToken): + token = self.peek_token() + raise ParserError("while parsing a block mapping", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark) + token = self.get_token() + event = MappingEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_block_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_block_mapping_key) + return self.parse_block_node_or_indentless_sequence() + else: + self.state = self.parse_block_mapping_key + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_block_mapping_key + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + # flow_sequence ::= FLOW-SEQUENCE-START + # (flow_sequence_entry FLOW-ENTRY)* + # flow_sequence_entry? + # FLOW-SEQUENCE-END + # flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + # + # Note that while production rules for both flow_sequence_entry and + # flow_mapping_entry are equal, their interpretations are different. + # For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?` + # generate an inline mapping (set syntax). + + def parse_flow_sequence_first_entry(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_sequence_entry(first=True) + + def parse_flow_sequence_entry(self, first=False): + if not self.check_token(FlowSequenceEndToken): + if not first: + if self.check_token(FlowEntryToken): + self.get_token() + else: + token = self.peek_token() + raise ParserError("while parsing a flow sequence", self.marks[-1], + "expected ',' or ']', but got %r" % token.id, token.start_mark) + + if self.check_token(KeyToken): + token = self.peek_token() + event = MappingStartEvent(None, None, True, + token.start_mark, token.end_mark, + flow_style=True) + self.state = self.parse_flow_sequence_entry_mapping_key + return event + elif not self.check_token(FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry) + return self.parse_flow_node() + token = self.get_token() + event = SequenceEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_sequence_entry_mapping_key(self): + token = self.get_token() + if not self.check_token(ValueToken, + FlowEntryToken, FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry_mapping_value) + return self.parse_flow_node() + else: + self.state = self.parse_flow_sequence_entry_mapping_value + return self.process_empty_scalar(token.end_mark) + + def parse_flow_sequence_entry_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(FlowEntryToken, FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry_mapping_end) + return self.parse_flow_node() + else: + self.state = self.parse_flow_sequence_entry_mapping_end + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_flow_sequence_entry_mapping_end + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + def parse_flow_sequence_entry_mapping_end(self): + self.state = self.parse_flow_sequence_entry + token = self.peek_token() + return MappingEndEvent(token.start_mark, token.start_mark) + + # flow_mapping ::= FLOW-MAPPING-START + # (flow_mapping_entry FLOW-ENTRY)* + # flow_mapping_entry? + # FLOW-MAPPING-END + # flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + + def parse_flow_mapping_first_key(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_mapping_key(first=True) + + def parse_flow_mapping_key(self, first=False): + if not self.check_token(FlowMappingEndToken): + if not first: + if self.check_token(FlowEntryToken): + self.get_token() + else: + token = self.peek_token() + raise ParserError("while parsing a flow mapping", self.marks[-1], + "expected ',' or '}', but got %r" % token.id, token.start_mark) + if self.check_token(KeyToken): + token = self.get_token() + if not self.check_token(ValueToken, + FlowEntryToken, FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_value) + return self.parse_flow_node() + else: + self.state = self.parse_flow_mapping_value + return self.process_empty_scalar(token.end_mark) + elif not self.check_token(FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_empty_value) + return self.parse_flow_node() + token = self.get_token() + event = MappingEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(FlowEntryToken, FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_key) + return self.parse_flow_node() + else: + self.state = self.parse_flow_mapping_key + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_flow_mapping_key + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + def parse_flow_mapping_empty_value(self): + self.state = self.parse_flow_mapping_key + return self.process_empty_scalar(self.peek_token().start_mark) + + def process_empty_scalar(self, mark): + return ScalarEvent(None, None, (True, False), '', mark, mark) + diff --git a/dist/ba_data/python-site-packages/yaml/reader.py b/dist/ba_data/python-site-packages/yaml/reader.py new file mode 100644 index 0000000..774b021 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/reader.py @@ -0,0 +1,185 @@ +# This module contains abstractions for the input stream. You don't have to +# looks further, there are no pretty code. +# +# We define two classes here. +# +# Mark(source, line, column) +# It's just a record and its only use is producing nice error messages. +# Parser does not use it for any other purposes. +# +# Reader(source, data) +# Reader determines the encoding of `data` and converts it to unicode. +# Reader provides the following methods and attributes: +# reader.peek(length=1) - return the next `length` characters +# reader.forward(length=1) - move the current position to `length` characters. +# reader.index - the number of the current character. +# reader.line, stream.column - the line and the column of the current character. + +__all__ = ['Reader', 'ReaderError'] + +from .error import YAMLError, Mark + +import codecs, re + +class ReaderError(YAMLError): + + def __init__(self, name, position, character, encoding, reason): + self.name = name + self.character = character + self.position = position + self.encoding = encoding + self.reason = reason + + def __str__(self): + if isinstance(self.character, bytes): + return "'%s' codec can't decode byte #x%02x: %s\n" \ + " in \"%s\", position %d" \ + % (self.encoding, ord(self.character), self.reason, + self.name, self.position) + else: + return "unacceptable character #x%04x: %s\n" \ + " in \"%s\", position %d" \ + % (self.character, self.reason, + self.name, self.position) + +class Reader(object): + # Reader: + # - determines the data encoding and converts it to a unicode string, + # - checks if characters are in allowed range, + # - adds '\0' to the end. + + # Reader accepts + # - a `bytes` object, + # - a `str` object, + # - a file-like object with its `read` method returning `str`, + # - a file-like object with its `read` method returning `unicode`. + + # Yeah, it's ugly and slow. + + def __init__(self, stream): + self.name = None + self.stream = None + self.stream_pointer = 0 + self.eof = True + self.buffer = '' + self.pointer = 0 + self.raw_buffer = None + self.raw_decode = None + self.encoding = None + self.index = 0 + self.line = 0 + self.column = 0 + if isinstance(stream, str): + self.name = "" + self.check_printable(stream) + self.buffer = stream+'\0' + elif isinstance(stream, bytes): + self.name = "" + self.raw_buffer = stream + self.determine_encoding() + else: + self.stream = stream + self.name = getattr(stream, 'name', "") + self.eof = False + self.raw_buffer = None + self.determine_encoding() + + def peek(self, index=0): + try: + return self.buffer[self.pointer+index] + except IndexError: + self.update(index+1) + return self.buffer[self.pointer+index] + + def prefix(self, length=1): + if self.pointer+length >= len(self.buffer): + self.update(length) + return self.buffer[self.pointer:self.pointer+length] + + def forward(self, length=1): + if self.pointer+length+1 >= len(self.buffer): + self.update(length+1) + while length: + ch = self.buffer[self.pointer] + self.pointer += 1 + self.index += 1 + if ch in '\n\x85\u2028\u2029' \ + or (ch == '\r' and self.buffer[self.pointer] != '\n'): + self.line += 1 + self.column = 0 + elif ch != '\uFEFF': + self.column += 1 + length -= 1 + + def get_mark(self): + if self.stream is None: + return Mark(self.name, self.index, self.line, self.column, + self.buffer, self.pointer) + else: + return Mark(self.name, self.index, self.line, self.column, + None, None) + + def determine_encoding(self): + while not self.eof and (self.raw_buffer is None or len(self.raw_buffer) < 2): + self.update_raw() + if isinstance(self.raw_buffer, bytes): + if self.raw_buffer.startswith(codecs.BOM_UTF16_LE): + self.raw_decode = codecs.utf_16_le_decode + self.encoding = 'utf-16-le' + elif self.raw_buffer.startswith(codecs.BOM_UTF16_BE): + self.raw_decode = codecs.utf_16_be_decode + self.encoding = 'utf-16-be' + else: + self.raw_decode = codecs.utf_8_decode + self.encoding = 'utf-8' + self.update(1) + + NON_PRINTABLE = re.compile('[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\uD7FF\uE000-\uFFFD\U00010000-\U0010ffff]') + def check_printable(self, data): + match = self.NON_PRINTABLE.search(data) + if match: + character = match.group() + position = self.index+(len(self.buffer)-self.pointer)+match.start() + raise ReaderError(self.name, position, ord(character), + 'unicode', "special characters are not allowed") + + def update(self, length): + if self.raw_buffer is None: + return + self.buffer = self.buffer[self.pointer:] + self.pointer = 0 + while len(self.buffer) < length: + if not self.eof: + self.update_raw() + if self.raw_decode is not None: + try: + data, converted = self.raw_decode(self.raw_buffer, + 'strict', self.eof) + except UnicodeDecodeError as exc: + character = self.raw_buffer[exc.start] + if self.stream is not None: + position = self.stream_pointer-len(self.raw_buffer)+exc.start + else: + position = exc.start + raise ReaderError(self.name, position, character, + exc.encoding, exc.reason) + else: + data = self.raw_buffer + converted = len(data) + self.check_printable(data) + self.buffer += data + self.raw_buffer = self.raw_buffer[converted:] + if self.eof: + self.buffer += '\0' + self.raw_buffer = None + break + + def update_raw(self, size=4096): + data = self.stream.read(size) + if self.raw_buffer is None: + self.raw_buffer = data + else: + self.raw_buffer += data + self.stream_pointer += len(data) + if not data: + self.eof = True diff --git a/dist/ba_data/python-site-packages/yaml/representer.py b/dist/ba_data/python-site-packages/yaml/representer.py new file mode 100644 index 0000000..3b0b192 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/representer.py @@ -0,0 +1,389 @@ + +__all__ = ['BaseRepresenter', 'SafeRepresenter', 'Representer', + 'RepresenterError'] + +from .error import * +from .nodes import * + +import datetime, copyreg, types, base64, collections + +class RepresenterError(YAMLError): + pass + +class BaseRepresenter: + + yaml_representers = {} + yaml_multi_representers = {} + + def __init__(self, default_style=None, default_flow_style=False, sort_keys=True): + self.default_style = default_style + self.sort_keys = sort_keys + self.default_flow_style = default_flow_style + self.represented_objects = {} + self.object_keeper = [] + self.alias_key = None + + def represent(self, data): + node = self.represent_data(data) + self.serialize(node) + self.represented_objects = {} + self.object_keeper = [] + self.alias_key = None + + def represent_data(self, data): + if self.ignore_aliases(data): + self.alias_key = None + else: + self.alias_key = id(data) + if self.alias_key is not None: + if self.alias_key in self.represented_objects: + node = self.represented_objects[self.alias_key] + #if node is None: + # raise RepresenterError("recursive objects are not allowed: %r" % data) + return node + #self.represented_objects[alias_key] = None + self.object_keeper.append(data) + data_types = type(data).__mro__ + if data_types[0] in self.yaml_representers: + node = self.yaml_representers[data_types[0]](self, data) + else: + for data_type in data_types: + if data_type in self.yaml_multi_representers: + node = self.yaml_multi_representers[data_type](self, data) + break + else: + if None in self.yaml_multi_representers: + node = self.yaml_multi_representers[None](self, data) + elif None in self.yaml_representers: + node = self.yaml_representers[None](self, data) + else: + node = ScalarNode(None, str(data)) + #if alias_key is not None: + # self.represented_objects[alias_key] = node + return node + + @classmethod + def add_representer(cls, data_type, representer): + if not 'yaml_representers' in cls.__dict__: + cls.yaml_representers = cls.yaml_representers.copy() + cls.yaml_representers[data_type] = representer + + @classmethod + def add_multi_representer(cls, data_type, representer): + if not 'yaml_multi_representers' in cls.__dict__: + cls.yaml_multi_representers = cls.yaml_multi_representers.copy() + cls.yaml_multi_representers[data_type] = representer + + def represent_scalar(self, tag, value, style=None): + if style is None: + style = self.default_style + node = ScalarNode(tag, value, style=style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + return node + + def represent_sequence(self, tag, sequence, flow_style=None): + value = [] + node = SequenceNode(tag, value, flow_style=flow_style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + best_style = True + for item in sequence: + node_item = self.represent_data(item) + if not (isinstance(node_item, ScalarNode) and not node_item.style): + best_style = False + value.append(node_item) + if flow_style is None: + if self.default_flow_style is not None: + node.flow_style = self.default_flow_style + else: + node.flow_style = best_style + return node + + def represent_mapping(self, tag, mapping, flow_style=None): + value = [] + node = MappingNode(tag, value, flow_style=flow_style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + best_style = True + if hasattr(mapping, 'items'): + mapping = list(mapping.items()) + if self.sort_keys: + try: + mapping = sorted(mapping) + except TypeError: + pass + for item_key, item_value in mapping: + node_key = self.represent_data(item_key) + node_value = self.represent_data(item_value) + if not (isinstance(node_key, ScalarNode) and not node_key.style): + best_style = False + if not (isinstance(node_value, ScalarNode) and not node_value.style): + best_style = False + value.append((node_key, node_value)) + if flow_style is None: + if self.default_flow_style is not None: + node.flow_style = self.default_flow_style + else: + node.flow_style = best_style + return node + + def ignore_aliases(self, data): + return False + +class SafeRepresenter(BaseRepresenter): + + def ignore_aliases(self, data): + if data is None: + return True + if isinstance(data, tuple) and data == (): + return True + if isinstance(data, (str, bytes, bool, int, float)): + return True + + def represent_none(self, data): + return self.represent_scalar('tag:yaml.org,2002:null', 'null') + + def represent_str(self, data): + return self.represent_scalar('tag:yaml.org,2002:str', data) + + def represent_binary(self, data): + if hasattr(base64, 'encodebytes'): + data = base64.encodebytes(data).decode('ascii') + else: + data = base64.encodestring(data).decode('ascii') + return self.represent_scalar('tag:yaml.org,2002:binary', data, style='|') + + def represent_bool(self, data): + if data: + value = 'true' + else: + value = 'false' + return self.represent_scalar('tag:yaml.org,2002:bool', value) + + def represent_int(self, data): + return self.represent_scalar('tag:yaml.org,2002:int', str(data)) + + inf_value = 1e300 + while repr(inf_value) != repr(inf_value*inf_value): + inf_value *= inf_value + + def represent_float(self, data): + if data != data or (data == 0.0 and data == 1.0): + value = '.nan' + elif data == self.inf_value: + value = '.inf' + elif data == -self.inf_value: + value = '-.inf' + else: + value = repr(data).lower() + # Note that in some cases `repr(data)` represents a float number + # without the decimal parts. For instance: + # >>> repr(1e17) + # '1e17' + # Unfortunately, this is not a valid float representation according + # to the definition of the `!!float` tag. We fix this by adding + # '.0' before the 'e' symbol. + if '.' not in value and 'e' in value: + value = value.replace('e', '.0e', 1) + return self.represent_scalar('tag:yaml.org,2002:float', value) + + def represent_list(self, data): + #pairs = (len(data) > 0 and isinstance(data, list)) + #if pairs: + # for item in data: + # if not isinstance(item, tuple) or len(item) != 2: + # pairs = False + # break + #if not pairs: + return self.represent_sequence('tag:yaml.org,2002:seq', data) + #value = [] + #for item_key, item_value in data: + # value.append(self.represent_mapping(u'tag:yaml.org,2002:map', + # [(item_key, item_value)])) + #return SequenceNode(u'tag:yaml.org,2002:pairs', value) + + def represent_dict(self, data): + return self.represent_mapping('tag:yaml.org,2002:map', data) + + def represent_set(self, data): + value = {} + for key in data: + value[key] = None + return self.represent_mapping('tag:yaml.org,2002:set', value) + + def represent_date(self, data): + value = data.isoformat() + return self.represent_scalar('tag:yaml.org,2002:timestamp', value) + + def represent_datetime(self, data): + value = data.isoformat(' ') + return self.represent_scalar('tag:yaml.org,2002:timestamp', value) + + def represent_yaml_object(self, tag, data, cls, flow_style=None): + if hasattr(data, '__getstate__'): + state = data.__getstate__() + else: + state = data.__dict__.copy() + return self.represent_mapping(tag, state, flow_style=flow_style) + + def represent_undefined(self, data): + raise RepresenterError("cannot represent an object", data) + +SafeRepresenter.add_representer(type(None), + SafeRepresenter.represent_none) + +SafeRepresenter.add_representer(str, + SafeRepresenter.represent_str) + +SafeRepresenter.add_representer(bytes, + SafeRepresenter.represent_binary) + +SafeRepresenter.add_representer(bool, + SafeRepresenter.represent_bool) + +SafeRepresenter.add_representer(int, + SafeRepresenter.represent_int) + +SafeRepresenter.add_representer(float, + SafeRepresenter.represent_float) + +SafeRepresenter.add_representer(list, + SafeRepresenter.represent_list) + +SafeRepresenter.add_representer(tuple, + SafeRepresenter.represent_list) + +SafeRepresenter.add_representer(dict, + SafeRepresenter.represent_dict) + +SafeRepresenter.add_representer(set, + SafeRepresenter.represent_set) + +SafeRepresenter.add_representer(datetime.date, + SafeRepresenter.represent_date) + +SafeRepresenter.add_representer(datetime.datetime, + SafeRepresenter.represent_datetime) + +SafeRepresenter.add_representer(None, + SafeRepresenter.represent_undefined) + +class Representer(SafeRepresenter): + + def represent_complex(self, data): + if data.imag == 0.0: + data = '%r' % data.real + elif data.real == 0.0: + data = '%rj' % data.imag + elif data.imag > 0: + data = '%r+%rj' % (data.real, data.imag) + else: + data = '%r%rj' % (data.real, data.imag) + return self.represent_scalar('tag:yaml.org,2002:python/complex', data) + + def represent_tuple(self, data): + return self.represent_sequence('tag:yaml.org,2002:python/tuple', data) + + def represent_name(self, data): + name = '%s.%s' % (data.__module__, data.__name__) + return self.represent_scalar('tag:yaml.org,2002:python/name:'+name, '') + + def represent_module(self, data): + return self.represent_scalar( + 'tag:yaml.org,2002:python/module:'+data.__name__, '') + + def represent_object(self, data): + # We use __reduce__ API to save the data. data.__reduce__ returns + # a tuple of length 2-5: + # (function, args, state, listitems, dictitems) + + # For reconstructing, we calls function(*args), then set its state, + # listitems, and dictitems if they are not None. + + # A special case is when function.__name__ == '__newobj__'. In this + # case we create the object with args[0].__new__(*args). + + # Another special case is when __reduce__ returns a string - we don't + # support it. + + # We produce a !!python/object, !!python/object/new or + # !!python/object/apply node. + + cls = type(data) + if cls in copyreg.dispatch_table: + reduce = copyreg.dispatch_table[cls](data) + elif hasattr(data, '__reduce_ex__'): + reduce = data.__reduce_ex__(2) + elif hasattr(data, '__reduce__'): + reduce = data.__reduce__() + else: + raise RepresenterError("cannot represent an object", data) + reduce = (list(reduce)+[None]*5)[:5] + function, args, state, listitems, dictitems = reduce + args = list(args) + if state is None: + state = {} + if listitems is not None: + listitems = list(listitems) + if dictitems is not None: + dictitems = dict(dictitems) + if function.__name__ == '__newobj__': + function = args[0] + args = args[1:] + tag = 'tag:yaml.org,2002:python/object/new:' + newobj = True + else: + tag = 'tag:yaml.org,2002:python/object/apply:' + newobj = False + function_name = '%s.%s' % (function.__module__, function.__name__) + if not args and not listitems and not dictitems \ + and isinstance(state, dict) and newobj: + return self.represent_mapping( + 'tag:yaml.org,2002:python/object:'+function_name, state) + if not listitems and not dictitems \ + and isinstance(state, dict) and not state: + return self.represent_sequence(tag+function_name, args) + value = {} + if args: + value['args'] = args + if state or not isinstance(state, dict): + value['state'] = state + if listitems: + value['listitems'] = listitems + if dictitems: + value['dictitems'] = dictitems + return self.represent_mapping(tag+function_name, value) + + def represent_ordered_dict(self, data): + # Provide uniform representation across different Python versions. + data_type = type(data) + tag = 'tag:yaml.org,2002:python/object/apply:%s.%s' \ + % (data_type.__module__, data_type.__name__) + items = [[key, value] for key, value in data.items()] + return self.represent_sequence(tag, [items]) + +Representer.add_representer(complex, + Representer.represent_complex) + +Representer.add_representer(tuple, + Representer.represent_tuple) + +Representer.add_representer(type, + Representer.represent_name) + +Representer.add_representer(collections.OrderedDict, + Representer.represent_ordered_dict) + +Representer.add_representer(types.FunctionType, + Representer.represent_name) + +Representer.add_representer(types.BuiltinFunctionType, + Representer.represent_name) + +Representer.add_representer(types.ModuleType, + Representer.represent_module) + +Representer.add_multi_representer(object, + Representer.represent_object) + diff --git a/dist/ba_data/python-site-packages/yaml/resolver.py b/dist/ba_data/python-site-packages/yaml/resolver.py new file mode 100644 index 0000000..02b82e7 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/resolver.py @@ -0,0 +1,227 @@ + +__all__ = ['BaseResolver', 'Resolver'] + +from .error import * +from .nodes import * + +import re + +class ResolverError(YAMLError): + pass + +class BaseResolver: + + DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str' + DEFAULT_SEQUENCE_TAG = 'tag:yaml.org,2002:seq' + DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map' + + yaml_implicit_resolvers = {} + yaml_path_resolvers = {} + + def __init__(self): + self.resolver_exact_paths = [] + self.resolver_prefix_paths = [] + + @classmethod + def add_implicit_resolver(cls, tag, regexp, first): + if not 'yaml_implicit_resolvers' in cls.__dict__: + implicit_resolvers = {} + for key in cls.yaml_implicit_resolvers: + implicit_resolvers[key] = cls.yaml_implicit_resolvers[key][:] + cls.yaml_implicit_resolvers = implicit_resolvers + if first is None: + first = [None] + for ch in first: + cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp)) + + @classmethod + def add_path_resolver(cls, tag, path, kind=None): + # Note: `add_path_resolver` is experimental. The API could be changed. + # `new_path` is a pattern that is matched against the path from the + # root to the node that is being considered. `node_path` elements are + # tuples `(node_check, index_check)`. `node_check` is a node class: + # `ScalarNode`, `SequenceNode`, `MappingNode` or `None`. `None` + # matches any kind of a node. `index_check` could be `None`, a boolean + # value, a string value, or a number. `None` and `False` match against + # any _value_ of sequence and mapping nodes. `True` matches against + # any _key_ of a mapping node. A string `index_check` matches against + # a mapping value that corresponds to a scalar key which content is + # equal to the `index_check` value. An integer `index_check` matches + # against a sequence value with the index equal to `index_check`. + if not 'yaml_path_resolvers' in cls.__dict__: + cls.yaml_path_resolvers = cls.yaml_path_resolvers.copy() + new_path = [] + for element in path: + if isinstance(element, (list, tuple)): + if len(element) == 2: + node_check, index_check = element + elif len(element) == 1: + node_check = element[0] + index_check = True + else: + raise ResolverError("Invalid path element: %s" % element) + else: + node_check = None + index_check = element + if node_check is str: + node_check = ScalarNode + elif node_check is list: + node_check = SequenceNode + elif node_check is dict: + node_check = MappingNode + elif node_check not in [ScalarNode, SequenceNode, MappingNode] \ + and not isinstance(node_check, str) \ + and node_check is not None: + raise ResolverError("Invalid node checker: %s" % node_check) + if not isinstance(index_check, (str, int)) \ + and index_check is not None: + raise ResolverError("Invalid index checker: %s" % index_check) + new_path.append((node_check, index_check)) + if kind is str: + kind = ScalarNode + elif kind is list: + kind = SequenceNode + elif kind is dict: + kind = MappingNode + elif kind not in [ScalarNode, SequenceNode, MappingNode] \ + and kind is not None: + raise ResolverError("Invalid node kind: %s" % kind) + cls.yaml_path_resolvers[tuple(new_path), kind] = tag + + def descend_resolver(self, current_node, current_index): + if not self.yaml_path_resolvers: + return + exact_paths = {} + prefix_paths = [] + if current_node: + depth = len(self.resolver_prefix_paths) + for path, kind in self.resolver_prefix_paths[-1]: + if self.check_resolver_prefix(depth, path, kind, + current_node, current_index): + if len(path) > depth: + prefix_paths.append((path, kind)) + else: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + for path, kind in self.yaml_path_resolvers: + if not path: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + prefix_paths.append((path, kind)) + self.resolver_exact_paths.append(exact_paths) + self.resolver_prefix_paths.append(prefix_paths) + + def ascend_resolver(self): + if not self.yaml_path_resolvers: + return + self.resolver_exact_paths.pop() + self.resolver_prefix_paths.pop() + + def check_resolver_prefix(self, depth, path, kind, + current_node, current_index): + node_check, index_check = path[depth-1] + if isinstance(node_check, str): + if current_node.tag != node_check: + return + elif node_check is not None: + if not isinstance(current_node, node_check): + return + if index_check is True and current_index is not None: + return + if (index_check is False or index_check is None) \ + and current_index is None: + return + if isinstance(index_check, str): + if not (isinstance(current_index, ScalarNode) + and index_check == current_index.value): + return + elif isinstance(index_check, int) and not isinstance(index_check, bool): + if index_check != current_index: + return + return True + + def resolve(self, kind, value, implicit): + if kind is ScalarNode and implicit[0]: + if value == '': + resolvers = self.yaml_implicit_resolvers.get('', []) + else: + resolvers = self.yaml_implicit_resolvers.get(value[0], []) + resolvers += self.yaml_implicit_resolvers.get(None, []) + for tag, regexp in resolvers: + if regexp.match(value): + return tag + implicit = implicit[1] + if self.yaml_path_resolvers: + exact_paths = self.resolver_exact_paths[-1] + if kind in exact_paths: + return exact_paths[kind] + if None in exact_paths: + return exact_paths[None] + if kind is ScalarNode: + return self.DEFAULT_SCALAR_TAG + elif kind is SequenceNode: + return self.DEFAULT_SEQUENCE_TAG + elif kind is MappingNode: + return self.DEFAULT_MAPPING_TAG + +class Resolver(BaseResolver): + pass + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:bool', + re.compile(r'''^(?:yes|Yes|YES|no|No|NO + |true|True|TRUE|false|False|FALSE + |on|On|ON|off|Off|OFF)$''', re.X), + list('yYnNtTfFoO')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:float', + re.compile(r'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? + |\.[0-9_]+(?:[eE][-+][0-9]+)? + |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]* + |[-+]?\.(?:inf|Inf|INF) + |\.(?:nan|NaN|NAN))$''', re.X), + list('-+0123456789.')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:int', + re.compile(r'''^(?:[-+]?0b[0-1_]+ + |[-+]?0[0-7_]+ + |[-+]?(?:0|[1-9][0-9_]*) + |[-+]?0x[0-9a-fA-F_]+ + |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X), + list('-+0123456789')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:merge', + re.compile(r'^(?:<<)$'), + ['<']) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:null', + re.compile(r'''^(?: ~ + |null|Null|NULL + | )$''', re.X), + ['~', 'n', 'N', '']) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:timestamp', + re.compile(r'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] + |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]? + (?:[Tt]|[ \t]+)[0-9][0-9]? + :[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)? + (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X), + list('0123456789')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:value', + re.compile(r'^(?:=)$'), + ['=']) + +# The following resolver is only for documentation purposes. It cannot work +# because plain scalars cannot start with '!', '&', or '*'. +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:yaml', + re.compile(r'^(?:!|&|\*)$'), + list('!&*')) + diff --git a/dist/ba_data/python-site-packages/yaml/scanner.py b/dist/ba_data/python-site-packages/yaml/scanner.py new file mode 100644 index 0000000..7437ede --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/scanner.py @@ -0,0 +1,1435 @@ + +# Scanner produces tokens of the following types: +# STREAM-START +# STREAM-END +# DIRECTIVE(name, value) +# DOCUMENT-START +# DOCUMENT-END +# BLOCK-SEQUENCE-START +# BLOCK-MAPPING-START +# BLOCK-END +# FLOW-SEQUENCE-START +# FLOW-MAPPING-START +# FLOW-SEQUENCE-END +# FLOW-MAPPING-END +# BLOCK-ENTRY +# FLOW-ENTRY +# KEY +# VALUE +# ALIAS(value) +# ANCHOR(value) +# TAG(value) +# SCALAR(value, plain, style) +# +# Read comments in the Scanner code for more details. +# + +__all__ = ['Scanner', 'ScannerError'] + +from .error import MarkedYAMLError +from .tokens import * + +class ScannerError(MarkedYAMLError): + pass + +class SimpleKey: + # See below simple keys treatment. + + def __init__(self, token_number, required, index, line, column, mark): + self.token_number = token_number + self.required = required + self.index = index + self.line = line + self.column = column + self.mark = mark + +class Scanner: + + def __init__(self): + """Initialize the scanner.""" + # It is assumed that Scanner and Reader will have a common descendant. + # Reader do the dirty work of checking for BOM and converting the + # input data to Unicode. It also adds NUL to the end. + # + # Reader supports the following methods + # self.peek(i=0) # peek the next i-th character + # self.prefix(l=1) # peek the next l characters + # self.forward(l=1) # read the next l characters and move the pointer. + + # Had we reached the end of the stream? + self.done = False + + # The number of unclosed '{' and '['. `flow_level == 0` means block + # context. + self.flow_level = 0 + + # List of processed tokens that are not yet emitted. + self.tokens = [] + + # Add the STREAM-START token. + self.fetch_stream_start() + + # Number of tokens that were emitted through the `get_token` method. + self.tokens_taken = 0 + + # The current indentation level. + self.indent = -1 + + # Past indentation levels. + self.indents = [] + + # Variables related to simple keys treatment. + + # A simple key is a key that is not denoted by the '?' indicator. + # Example of simple keys: + # --- + # block simple key: value + # ? not a simple key: + # : { flow simple key: value } + # We emit the KEY token before all keys, so when we find a potential + # simple key, we try to locate the corresponding ':' indicator. + # Simple keys should be limited to a single line and 1024 characters. + + # Can a simple key start at the current position? A simple key may + # start: + # - at the beginning of the line, not counting indentation spaces + # (in block context), + # - after '{', '[', ',' (in the flow context), + # - after '?', ':', '-' (in the block context). + # In the block context, this flag also signifies if a block collection + # may start at the current position. + self.allow_simple_key = True + + # Keep track of possible simple keys. This is a dictionary. The key + # is `flow_level`; there can be no more that one possible simple key + # for each level. The value is a SimpleKey record: + # (token_number, required, index, line, column, mark) + # A simple key may start with ALIAS, ANCHOR, TAG, SCALAR(flow), + # '[', or '{' tokens. + self.possible_simple_keys = {} + + # Public methods. + + def check_token(self, *choices): + # Check if the next token is one of the given types. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + if not choices: + return True + for choice in choices: + if isinstance(self.tokens[0], choice): + return True + return False + + def peek_token(self): + # Return the next token, but do not delete if from the queue. + # Return None if no more tokens. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + return self.tokens[0] + else: + return None + + def get_token(self): + # Return the next token. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + self.tokens_taken += 1 + return self.tokens.pop(0) + + # Private methods. + + def need_more_tokens(self): + if self.done: + return False + if not self.tokens: + return True + # The current token may be a potential simple key, so we + # need to look further. + self.stale_possible_simple_keys() + if self.next_possible_simple_key() == self.tokens_taken: + return True + + def fetch_more_tokens(self): + + # Eat whitespaces and comments until we reach the next token. + self.scan_to_next_token() + + # Remove obsolete possible simple keys. + self.stale_possible_simple_keys() + + # Compare the current indentation and column. It may add some tokens + # and decrease the current indentation level. + self.unwind_indent(self.column) + + # Peek the next character. + ch = self.peek() + + # Is it the end of stream? + if ch == '\0': + return self.fetch_stream_end() + + # Is it a directive? + if ch == '%' and self.check_directive(): + return self.fetch_directive() + + # Is it the document start? + if ch == '-' and self.check_document_start(): + return self.fetch_document_start() + + # Is it the document end? + if ch == '.' and self.check_document_end(): + return self.fetch_document_end() + + # TODO: support for BOM within a stream. + #if ch == '\uFEFF': + # return self.fetch_bom() <-- issue BOMToken + + # Note: the order of the following checks is NOT significant. + + # Is it the flow sequence start indicator? + if ch == '[': + return self.fetch_flow_sequence_start() + + # Is it the flow mapping start indicator? + if ch == '{': + return self.fetch_flow_mapping_start() + + # Is it the flow sequence end indicator? + if ch == ']': + return self.fetch_flow_sequence_end() + + # Is it the flow mapping end indicator? + if ch == '}': + return self.fetch_flow_mapping_end() + + # Is it the flow entry indicator? + if ch == ',': + return self.fetch_flow_entry() + + # Is it the block entry indicator? + if ch == '-' and self.check_block_entry(): + return self.fetch_block_entry() + + # Is it the key indicator? + if ch == '?' and self.check_key(): + return self.fetch_key() + + # Is it the value indicator? + if ch == ':' and self.check_value(): + return self.fetch_value() + + # Is it an alias? + if ch == '*': + return self.fetch_alias() + + # Is it an anchor? + if ch == '&': + return self.fetch_anchor() + + # Is it a tag? + if ch == '!': + return self.fetch_tag() + + # Is it a literal scalar? + if ch == '|' and not self.flow_level: + return self.fetch_literal() + + # Is it a folded scalar? + if ch == '>' and not self.flow_level: + return self.fetch_folded() + + # Is it a single quoted scalar? + if ch == '\'': + return self.fetch_single() + + # Is it a double quoted scalar? + if ch == '\"': + return self.fetch_double() + + # It must be a plain scalar then. + if self.check_plain(): + return self.fetch_plain() + + # No? It's an error. Let's produce a nice error message. + raise ScannerError("while scanning for the next token", None, + "found character %r that cannot start any token" % ch, + self.get_mark()) + + # Simple keys treatment. + + def next_possible_simple_key(self): + # Return the number of the nearest possible simple key. Actually we + # don't need to loop through the whole dictionary. We may replace it + # with the following code: + # if not self.possible_simple_keys: + # return None + # return self.possible_simple_keys[ + # min(self.possible_simple_keys.keys())].token_number + min_token_number = None + for level in self.possible_simple_keys: + key = self.possible_simple_keys[level] + if min_token_number is None or key.token_number < min_token_number: + min_token_number = key.token_number + return min_token_number + + def stale_possible_simple_keys(self): + # Remove entries that are no longer possible simple keys. According to + # the YAML specification, simple keys + # - should be limited to a single line, + # - should be no longer than 1024 characters. + # Disabling this procedure will allow simple keys of any length and + # height (may cause problems if indentation is broken though). + for level in list(self.possible_simple_keys): + key = self.possible_simple_keys[level] + if key.line != self.line \ + or self.index-key.index > 1024: + if key.required: + raise ScannerError("while scanning a simple key", key.mark, + "could not find expected ':'", self.get_mark()) + del self.possible_simple_keys[level] + + def save_possible_simple_key(self): + # The next token may start a simple key. We check if it's possible + # and save its position. This function is called for + # ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'. + + # Check if a simple key is required at the current position. + required = not self.flow_level and self.indent == self.column + + # The next token might be a simple key. Let's save it's number and + # position. + if self.allow_simple_key: + self.remove_possible_simple_key() + token_number = self.tokens_taken+len(self.tokens) + key = SimpleKey(token_number, required, + self.index, self.line, self.column, self.get_mark()) + self.possible_simple_keys[self.flow_level] = key + + def remove_possible_simple_key(self): + # Remove the saved possible key position at the current flow level. + if self.flow_level in self.possible_simple_keys: + key = self.possible_simple_keys[self.flow_level] + + if key.required: + raise ScannerError("while scanning a simple key", key.mark, + "could not find expected ':'", self.get_mark()) + + del self.possible_simple_keys[self.flow_level] + + # Indentation functions. + + def unwind_indent(self, column): + + ## In flow context, tokens should respect indentation. + ## Actually the condition should be `self.indent >= column` according to + ## the spec. But this condition will prohibit intuitively correct + ## constructions such as + ## key : { + ## } + #if self.flow_level and self.indent > column: + # raise ScannerError(None, None, + # "invalid indentation or unclosed '[' or '{'", + # self.get_mark()) + + # In the flow context, indentation is ignored. We make the scanner less + # restrictive then specification requires. + if self.flow_level: + return + + # In block context, we may need to issue the BLOCK-END tokens. + while self.indent > column: + mark = self.get_mark() + self.indent = self.indents.pop() + self.tokens.append(BlockEndToken(mark, mark)) + + def add_indent(self, column): + # Check if we need to increase indentation. + if self.indent < column: + self.indents.append(self.indent) + self.indent = column + return True + return False + + # Fetchers. + + def fetch_stream_start(self): + # We always add STREAM-START as the first token and STREAM-END as the + # last token. + + # Read the token. + mark = self.get_mark() + + # Add STREAM-START. + self.tokens.append(StreamStartToken(mark, mark, + encoding=self.encoding)) + + + def fetch_stream_end(self): + + # Set the current indentation to -1. + self.unwind_indent(-1) + + # Reset simple keys. + self.remove_possible_simple_key() + self.allow_simple_key = False + self.possible_simple_keys = {} + + # Read the token. + mark = self.get_mark() + + # Add STREAM-END. + self.tokens.append(StreamEndToken(mark, mark)) + + # The steam is finished. + self.done = True + + def fetch_directive(self): + + # Set the current indentation to -1. + self.unwind_indent(-1) + + # Reset simple keys. + self.remove_possible_simple_key() + self.allow_simple_key = False + + # Scan and add DIRECTIVE. + self.tokens.append(self.scan_directive()) + + def fetch_document_start(self): + self.fetch_document_indicator(DocumentStartToken) + + def fetch_document_end(self): + self.fetch_document_indicator(DocumentEndToken) + + def fetch_document_indicator(self, TokenClass): + + # Set the current indentation to -1. + self.unwind_indent(-1) + + # Reset simple keys. Note that there could not be a block collection + # after '---'. + self.remove_possible_simple_key() + self.allow_simple_key = False + + # Add DOCUMENT-START or DOCUMENT-END. + start_mark = self.get_mark() + self.forward(3) + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_sequence_start(self): + self.fetch_flow_collection_start(FlowSequenceStartToken) + + def fetch_flow_mapping_start(self): + self.fetch_flow_collection_start(FlowMappingStartToken) + + def fetch_flow_collection_start(self, TokenClass): + + # '[' and '{' may start a simple key. + self.save_possible_simple_key() + + # Increase the flow level. + self.flow_level += 1 + + # Simple keys are allowed after '[' and '{'. + self.allow_simple_key = True + + # Add FLOW-SEQUENCE-START or FLOW-MAPPING-START. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_sequence_end(self): + self.fetch_flow_collection_end(FlowSequenceEndToken) + + def fetch_flow_mapping_end(self): + self.fetch_flow_collection_end(FlowMappingEndToken) + + def fetch_flow_collection_end(self, TokenClass): + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Decrease the flow level. + self.flow_level -= 1 + + # No simple keys after ']' or '}'. + self.allow_simple_key = False + + # Add FLOW-SEQUENCE-END or FLOW-MAPPING-END. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_entry(self): + + # Simple keys are allowed after ','. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add FLOW-ENTRY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(FlowEntryToken(start_mark, end_mark)) + + def fetch_block_entry(self): + + # Block context needs additional checks. + if not self.flow_level: + + # Are we allowed to start a new entry? + if not self.allow_simple_key: + raise ScannerError(None, None, + "sequence entries are not allowed here", + self.get_mark()) + + # We may need to add BLOCK-SEQUENCE-START. + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockSequenceStartToken(mark, mark)) + + # It's an error for the block entry to occur in the flow context, + # but we let the parser detect this. + else: + pass + + # Simple keys are allowed after '-'. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add BLOCK-ENTRY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(BlockEntryToken(start_mark, end_mark)) + + def fetch_key(self): + + # Block context needs additional checks. + if not self.flow_level: + + # Are we allowed to start a key (not necessary a simple)? + if not self.allow_simple_key: + raise ScannerError(None, None, + "mapping keys are not allowed here", + self.get_mark()) + + # We may need to add BLOCK-MAPPING-START. + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockMappingStartToken(mark, mark)) + + # Simple keys are allowed after '?' in the block context. + self.allow_simple_key = not self.flow_level + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add KEY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(KeyToken(start_mark, end_mark)) + + def fetch_value(self): + + # Do we determine a simple key? + if self.flow_level in self.possible_simple_keys: + + # Add KEY. + key = self.possible_simple_keys[self.flow_level] + del self.possible_simple_keys[self.flow_level] + self.tokens.insert(key.token_number-self.tokens_taken, + KeyToken(key.mark, key.mark)) + + # If this key starts a new block mapping, we need to add + # BLOCK-MAPPING-START. + if not self.flow_level: + if self.add_indent(key.column): + self.tokens.insert(key.token_number-self.tokens_taken, + BlockMappingStartToken(key.mark, key.mark)) + + # There cannot be two simple keys one after another. + self.allow_simple_key = False + + # It must be a part of a complex key. + else: + + # Block context needs additional checks. + # (Do we really need them? They will be caught by the parser + # anyway.) + if not self.flow_level: + + # We are allowed to start a complex value if and only if + # we can start a simple key. + if not self.allow_simple_key: + raise ScannerError(None, None, + "mapping values are not allowed here", + self.get_mark()) + + # If this value starts a new block mapping, we need to add + # BLOCK-MAPPING-START. It will be detected as an error later by + # the parser. + if not self.flow_level: + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockMappingStartToken(mark, mark)) + + # Simple keys are allowed after ':' in the block context. + self.allow_simple_key = not self.flow_level + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add VALUE. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(ValueToken(start_mark, end_mark)) + + def fetch_alias(self): + + # ALIAS could be a simple key. + self.save_possible_simple_key() + + # No simple keys after ALIAS. + self.allow_simple_key = False + + # Scan and add ALIAS. + self.tokens.append(self.scan_anchor(AliasToken)) + + def fetch_anchor(self): + + # ANCHOR could start a simple key. + self.save_possible_simple_key() + + # No simple keys after ANCHOR. + self.allow_simple_key = False + + # Scan and add ANCHOR. + self.tokens.append(self.scan_anchor(AnchorToken)) + + def fetch_tag(self): + + # TAG could start a simple key. + self.save_possible_simple_key() + + # No simple keys after TAG. + self.allow_simple_key = False + + # Scan and add TAG. + self.tokens.append(self.scan_tag()) + + def fetch_literal(self): + self.fetch_block_scalar(style='|') + + def fetch_folded(self): + self.fetch_block_scalar(style='>') + + def fetch_block_scalar(self, style): + + # A simple key may follow a block scalar. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Scan and add SCALAR. + self.tokens.append(self.scan_block_scalar(style)) + + def fetch_single(self): + self.fetch_flow_scalar(style='\'') + + def fetch_double(self): + self.fetch_flow_scalar(style='"') + + def fetch_flow_scalar(self, style): + + # A flow scalar could be a simple key. + self.save_possible_simple_key() + + # No simple keys after flow scalars. + self.allow_simple_key = False + + # Scan and add SCALAR. + self.tokens.append(self.scan_flow_scalar(style)) + + def fetch_plain(self): + + # A plain scalar could be a simple key. + self.save_possible_simple_key() + + # No simple keys after plain scalars. But note that `scan_plain` will + # change this flag if the scan is finished at the beginning of the + # line. + self.allow_simple_key = False + + # Scan and add SCALAR. May change `allow_simple_key`. + self.tokens.append(self.scan_plain()) + + # Checkers. + + def check_directive(self): + + # DIRECTIVE: ^ '%' ... + # The '%' indicator is already checked. + if self.column == 0: + return True + + def check_document_start(self): + + # DOCUMENT-START: ^ '---' (' '|'\n') + if self.column == 0: + if self.prefix(3) == '---' \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return True + + def check_document_end(self): + + # DOCUMENT-END: ^ '...' (' '|'\n') + if self.column == 0: + if self.prefix(3) == '...' \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return True + + def check_block_entry(self): + + # BLOCK-ENTRY: '-' (' '|'\n') + return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' + + def check_key(self): + + # KEY(flow context): '?' + if self.flow_level: + return True + + # KEY(block context): '?' (' '|'\n') + else: + return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' + + def check_value(self): + + # VALUE(flow context): ':' + if self.flow_level: + return True + + # VALUE(block context): ':' (' '|'\n') + else: + return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' + + def check_plain(self): + + # A plain scalar may start with any non-space character except: + # '-', '?', ':', ',', '[', ']', '{', '}', + # '#', '&', '*', '!', '|', '>', '\'', '\"', + # '%', '@', '`'. + # + # It may also start with + # '-', '?', ':' + # if it is followed by a non-space character. + # + # Note that we limit the last rule to the block context (except the + # '-' character) because we want the flow context to be space + # independent. + ch = self.peek() + return ch not in '\0 \t\r\n\x85\u2028\u2029-?:,[]{}#&*!|>\'\"%@`' \ + or (self.peek(1) not in '\0 \t\r\n\x85\u2028\u2029' + and (ch == '-' or (not self.flow_level and ch in '?:'))) + + # Scanners. + + def scan_to_next_token(self): + # We ignore spaces, line breaks and comments. + # If we find a line break in the block context, we set the flag + # `allow_simple_key` on. + # The byte order mark is stripped if it's the first character in the + # stream. We do not yet support BOM inside the stream as the + # specification requires. Any such mark will be considered as a part + # of the document. + # + # TODO: We need to make tab handling rules more sane. A good rule is + # Tabs cannot precede tokens + # BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END, + # KEY(block), VALUE(block), BLOCK-ENTRY + # So the checking code is + # if : + # self.allow_simple_keys = False + # We also need to add the check for `allow_simple_keys == True` to + # `unwind_indent` before issuing BLOCK-END. + # Scanners for block, flow, and plain scalars need to be modified. + + if self.index == 0 and self.peek() == '\uFEFF': + self.forward() + found = False + while not found: + while self.peek() == ' ': + self.forward() + if self.peek() == '#': + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + if self.scan_line_break(): + if not self.flow_level: + self.allow_simple_key = True + else: + found = True + + def scan_directive(self): + # See the specification for details. + start_mark = self.get_mark() + self.forward() + name = self.scan_directive_name(start_mark) + value = None + if name == 'YAML': + value = self.scan_yaml_directive_value(start_mark) + end_mark = self.get_mark() + elif name == 'TAG': + value = self.scan_tag_directive_value(start_mark) + end_mark = self.get_mark() + else: + end_mark = self.get_mark() + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + self.scan_directive_ignored_line(start_mark) + return DirectiveToken(name, value, start_mark, end_mark) + + def scan_directive_name(self, start_mark): + # See the specification for details. + length = 0 + ch = self.peek(length) + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_': + length += 1 + ch = self.peek(length) + if not length: + raise ScannerError("while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + value = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + return value + + def scan_yaml_directive_value(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + major = self.scan_yaml_directive_number(start_mark) + if self.peek() != '.': + raise ScannerError("while scanning a directive", start_mark, + "expected a digit or '.', but found %r" % self.peek(), + self.get_mark()) + self.forward() + minor = self.scan_yaml_directive_number(start_mark) + if self.peek() not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected a digit or ' ', but found %r" % self.peek(), + self.get_mark()) + return (major, minor) + + def scan_yaml_directive_number(self, start_mark): + # See the specification for details. + ch = self.peek() + if not ('0' <= ch <= '9'): + raise ScannerError("while scanning a directive", start_mark, + "expected a digit, but found %r" % ch, self.get_mark()) + length = 0 + while '0' <= self.peek(length) <= '9': + length += 1 + value = int(self.prefix(length)) + self.forward(length) + return value + + def scan_tag_directive_value(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + handle = self.scan_tag_directive_handle(start_mark) + while self.peek() == ' ': + self.forward() + prefix = self.scan_tag_directive_prefix(start_mark) + return (handle, prefix) + + def scan_tag_directive_handle(self, start_mark): + # See the specification for details. + value = self.scan_tag_handle('directive', start_mark) + ch = self.peek() + if ch != ' ': + raise ScannerError("while scanning a directive", start_mark, + "expected ' ', but found %r" % ch, self.get_mark()) + return value + + def scan_tag_directive_prefix(self, start_mark): + # See the specification for details. + value = self.scan_tag_uri('directive', start_mark) + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected ' ', but found %r" % ch, self.get_mark()) + return value + + def scan_directive_ignored_line(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + if self.peek() == '#': + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + ch = self.peek() + if ch not in '\0\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected a comment or a line break, but found %r" + % ch, self.get_mark()) + self.scan_line_break() + + def scan_anchor(self, TokenClass): + # The specification does not restrict characters for anchors and + # aliases. This may lead to problems, for instance, the document: + # [ *alias, value ] + # can be interpreted in two ways, as + # [ "value" ] + # and + # [ *alias , "value" ] + # Therefore we restrict aliases to numbers and ASCII letters. + start_mark = self.get_mark() + indicator = self.peek() + if indicator == '*': + name = 'alias' + else: + name = 'anchor' + self.forward() + length = 0 + ch = self.peek(length) + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_': + length += 1 + ch = self.peek(length) + if not length: + raise ScannerError("while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + value = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch not in '\0 \t\r\n\x85\u2028\u2029?:,]}%@`': + raise ScannerError("while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + end_mark = self.get_mark() + return TokenClass(value, start_mark, end_mark) + + def scan_tag(self): + # See the specification for details. + start_mark = self.get_mark() + ch = self.peek(1) + if ch == '<': + handle = None + self.forward(2) + suffix = self.scan_tag_uri('tag', start_mark) + if self.peek() != '>': + raise ScannerError("while parsing a tag", start_mark, + "expected '>', but found %r" % self.peek(), + self.get_mark()) + self.forward() + elif ch in '\0 \t\r\n\x85\u2028\u2029': + handle = None + suffix = '!' + self.forward() + else: + length = 1 + use_handle = False + while ch not in '\0 \r\n\x85\u2028\u2029': + if ch == '!': + use_handle = True + break + length += 1 + ch = self.peek(length) + handle = '!' + if use_handle: + handle = self.scan_tag_handle('tag', start_mark) + else: + handle = '!' + self.forward() + suffix = self.scan_tag_uri('tag', start_mark) + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a tag", start_mark, + "expected ' ', but found %r" % ch, self.get_mark()) + value = (handle, suffix) + end_mark = self.get_mark() + return TagToken(value, start_mark, end_mark) + + def scan_block_scalar(self, style): + # See the specification for details. + + if style == '>': + folded = True + else: + folded = False + + chunks = [] + start_mark = self.get_mark() + + # Scan the header. + self.forward() + chomping, increment = self.scan_block_scalar_indicators(start_mark) + self.scan_block_scalar_ignored_line(start_mark) + + # Determine the indentation level and go to the first non-empty line. + min_indent = self.indent+1 + if min_indent < 1: + min_indent = 1 + if increment is None: + breaks, max_indent, end_mark = self.scan_block_scalar_indentation() + indent = max(min_indent, max_indent) + else: + indent = min_indent+increment-1 + breaks, end_mark = self.scan_block_scalar_breaks(indent) + line_break = '' + + # Scan the inner part of the block scalar. + while self.column == indent and self.peek() != '\0': + chunks.extend(breaks) + leading_non_space = self.peek() not in ' \t' + length = 0 + while self.peek(length) not in '\0\r\n\x85\u2028\u2029': + length += 1 + chunks.append(self.prefix(length)) + self.forward(length) + line_break = self.scan_line_break() + breaks, end_mark = self.scan_block_scalar_breaks(indent) + if self.column == indent and self.peek() != '\0': + + # Unfortunately, folding rules are ambiguous. + # + # This is the folding according to the specification: + + if folded and line_break == '\n' \ + and leading_non_space and self.peek() not in ' \t': + if not breaks: + chunks.append(' ') + else: + chunks.append(line_break) + + # This is Clark Evans's interpretation (also in the spec + # examples): + # + #if folded and line_break == '\n': + # if not breaks: + # if self.peek() not in ' \t': + # chunks.append(' ') + # else: + # chunks.append(line_break) + #else: + # chunks.append(line_break) + else: + break + + # Chomp the tail. + if chomping is not False: + chunks.append(line_break) + if chomping is True: + chunks.extend(breaks) + + # We are done. + return ScalarToken(''.join(chunks), False, start_mark, end_mark, + style) + + def scan_block_scalar_indicators(self, start_mark): + # See the specification for details. + chomping = None + increment = None + ch = self.peek() + if ch in '+-': + if ch == '+': + chomping = True + else: + chomping = False + self.forward() + ch = self.peek() + if ch in '0123456789': + increment = int(ch) + if increment == 0: + raise ScannerError("while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark()) + self.forward() + elif ch in '0123456789': + increment = int(ch) + if increment == 0: + raise ScannerError("while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark()) + self.forward() + ch = self.peek() + if ch in '+-': + if ch == '+': + chomping = True + else: + chomping = False + self.forward() + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a block scalar", start_mark, + "expected chomping or indentation indicators, but found %r" + % ch, self.get_mark()) + return chomping, increment + + def scan_block_scalar_ignored_line(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + if self.peek() == '#': + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + ch = self.peek() + if ch not in '\0\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a block scalar", start_mark, + "expected a comment or a line break, but found %r" % ch, + self.get_mark()) + self.scan_line_break() + + def scan_block_scalar_indentation(self): + # See the specification for details. + chunks = [] + max_indent = 0 + end_mark = self.get_mark() + while self.peek() in ' \r\n\x85\u2028\u2029': + if self.peek() != ' ': + chunks.append(self.scan_line_break()) + end_mark = self.get_mark() + else: + self.forward() + if self.column > max_indent: + max_indent = self.column + return chunks, max_indent, end_mark + + def scan_block_scalar_breaks(self, indent): + # See the specification for details. + chunks = [] + end_mark = self.get_mark() + while self.column < indent and self.peek() == ' ': + self.forward() + while self.peek() in '\r\n\x85\u2028\u2029': + chunks.append(self.scan_line_break()) + end_mark = self.get_mark() + while self.column < indent and self.peek() == ' ': + self.forward() + return chunks, end_mark + + def scan_flow_scalar(self, style): + # See the specification for details. + # Note that we loose indentation rules for quoted scalars. Quoted + # scalars don't need to adhere indentation because " and ' clearly + # mark the beginning and the end of them. Therefore we are less + # restrictive then the specification requires. We only need to check + # that document separators are not included in scalars. + if style == '"': + double = True + else: + double = False + chunks = [] + start_mark = self.get_mark() + quote = self.peek() + self.forward() + chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) + while self.peek() != quote: + chunks.extend(self.scan_flow_scalar_spaces(double, start_mark)) + chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) + self.forward() + end_mark = self.get_mark() + return ScalarToken(''.join(chunks), False, start_mark, end_mark, + style) + + ESCAPE_REPLACEMENTS = { + '0': '\0', + 'a': '\x07', + 'b': '\x08', + 't': '\x09', + '\t': '\x09', + 'n': '\x0A', + 'v': '\x0B', + 'f': '\x0C', + 'r': '\x0D', + 'e': '\x1B', + ' ': '\x20', + '\"': '\"', + '\\': '\\', + '/': '/', + 'N': '\x85', + '_': '\xA0', + 'L': '\u2028', + 'P': '\u2029', + } + + ESCAPE_CODES = { + 'x': 2, + 'u': 4, + 'U': 8, + } + + def scan_flow_scalar_non_spaces(self, double, start_mark): + # See the specification for details. + chunks = [] + while True: + length = 0 + while self.peek(length) not in '\'\"\\\0 \t\r\n\x85\u2028\u2029': + length += 1 + if length: + chunks.append(self.prefix(length)) + self.forward(length) + ch = self.peek() + if not double and ch == '\'' and self.peek(1) == '\'': + chunks.append('\'') + self.forward(2) + elif (double and ch == '\'') or (not double and ch in '\"\\'): + chunks.append(ch) + self.forward() + elif double and ch == '\\': + self.forward() + ch = self.peek() + if ch in self.ESCAPE_REPLACEMENTS: + chunks.append(self.ESCAPE_REPLACEMENTS[ch]) + self.forward() + elif ch in self.ESCAPE_CODES: + length = self.ESCAPE_CODES[ch] + self.forward() + for k in range(length): + if self.peek(k) not in '0123456789ABCDEFabcdef': + raise ScannerError("while scanning a double-quoted scalar", start_mark, + "expected escape sequence of %d hexdecimal numbers, but found %r" % + (length, self.peek(k)), self.get_mark()) + code = int(self.prefix(length), 16) + chunks.append(chr(code)) + self.forward(length) + elif ch in '\r\n\x85\u2028\u2029': + self.scan_line_break() + chunks.extend(self.scan_flow_scalar_breaks(double, start_mark)) + else: + raise ScannerError("while scanning a double-quoted scalar", start_mark, + "found unknown escape character %r" % ch, self.get_mark()) + else: + return chunks + + def scan_flow_scalar_spaces(self, double, start_mark): + # See the specification for details. + chunks = [] + length = 0 + while self.peek(length) in ' \t': + length += 1 + whitespaces = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch == '\0': + raise ScannerError("while scanning a quoted scalar", start_mark, + "found unexpected end of stream", self.get_mark()) + elif ch in '\r\n\x85\u2028\u2029': + line_break = self.scan_line_break() + breaks = self.scan_flow_scalar_breaks(double, start_mark) + if line_break != '\n': + chunks.append(line_break) + elif not breaks: + chunks.append(' ') + chunks.extend(breaks) + else: + chunks.append(whitespaces) + return chunks + + def scan_flow_scalar_breaks(self, double, start_mark): + # See the specification for details. + chunks = [] + while True: + # Instead of checking indentation, we check for document + # separators. + prefix = self.prefix(3) + if (prefix == '---' or prefix == '...') \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a quoted scalar", start_mark, + "found unexpected document separator", self.get_mark()) + while self.peek() in ' \t': + self.forward() + if self.peek() in '\r\n\x85\u2028\u2029': + chunks.append(self.scan_line_break()) + else: + return chunks + + def scan_plain(self): + # See the specification for details. + # We add an additional restriction for the flow context: + # plain scalars in the flow context cannot contain ',' or '?'. + # We also keep track of the `allow_simple_key` flag here. + # Indentation rules are loosed for the flow context. + chunks = [] + start_mark = self.get_mark() + end_mark = start_mark + indent = self.indent+1 + # We allow zero indentation for scalars, but then we need to check for + # document separators at the beginning of the line. + #if indent == 0: + # indent = 1 + spaces = [] + while True: + length = 0 + if self.peek() == '#': + break + while True: + ch = self.peek(length) + if ch in '\0 \t\r\n\x85\u2028\u2029' \ + or (ch == ':' and + self.peek(length+1) in '\0 \t\r\n\x85\u2028\u2029' + + (u',[]{}' if self.flow_level else u''))\ + or (self.flow_level and ch in ',?[]{}'): + break + length += 1 + if length == 0: + break + self.allow_simple_key = False + chunks.extend(spaces) + chunks.append(self.prefix(length)) + self.forward(length) + end_mark = self.get_mark() + spaces = self.scan_plain_spaces(indent, start_mark) + if not spaces or self.peek() == '#' \ + or (not self.flow_level and self.column < indent): + break + return ScalarToken(''.join(chunks), True, start_mark, end_mark) + + def scan_plain_spaces(self, indent, start_mark): + # See the specification for details. + # The specification is really confusing about tabs in plain scalars. + # We just forbid them completely. Do not use tabs in YAML! + chunks = [] + length = 0 + while self.peek(length) in ' ': + length += 1 + whitespaces = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch in '\r\n\x85\u2028\u2029': + line_break = self.scan_line_break() + self.allow_simple_key = True + prefix = self.prefix(3) + if (prefix == '---' or prefix == '...') \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return + breaks = [] + while self.peek() in ' \r\n\x85\u2028\u2029': + if self.peek() == ' ': + self.forward() + else: + breaks.append(self.scan_line_break()) + prefix = self.prefix(3) + if (prefix == '---' or prefix == '...') \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return + if line_break != '\n': + chunks.append(line_break) + elif not breaks: + chunks.append(' ') + chunks.extend(breaks) + elif whitespaces: + chunks.append(whitespaces) + return chunks + + def scan_tag_handle(self, name, start_mark): + # See the specification for details. + # For some strange reasons, the specification does not allow '_' in + # tag handles. I have allowed it anyway. + ch = self.peek() + if ch != '!': + raise ScannerError("while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch, self.get_mark()) + length = 1 + ch = self.peek(length) + if ch != ' ': + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_': + length += 1 + ch = self.peek(length) + if ch != '!': + self.forward(length) + raise ScannerError("while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch, self.get_mark()) + length += 1 + value = self.prefix(length) + self.forward(length) + return value + + def scan_tag_uri(self, name, start_mark): + # See the specification for details. + # Note: we do not check if URI is well-formed. + chunks = [] + length = 0 + ch = self.peek(length) + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-;/?:@&=+$,_.!~*\'()[]%': + if ch == '%': + chunks.append(self.prefix(length)) + self.forward(length) + length = 0 + chunks.append(self.scan_uri_escapes(name, start_mark)) + else: + length += 1 + ch = self.peek(length) + if length: + chunks.append(self.prefix(length)) + self.forward(length) + length = 0 + if not chunks: + raise ScannerError("while parsing a %s" % name, start_mark, + "expected URI, but found %r" % ch, self.get_mark()) + return ''.join(chunks) + + def scan_uri_escapes(self, name, start_mark): + # See the specification for details. + codes = [] + mark = self.get_mark() + while self.peek() == '%': + self.forward() + for k in range(2): + if self.peek(k) not in '0123456789ABCDEFabcdef': + raise ScannerError("while scanning a %s" % name, start_mark, + "expected URI escape sequence of 2 hexdecimal numbers, but found %r" + % self.peek(k), self.get_mark()) + codes.append(int(self.prefix(2), 16)) + self.forward(2) + try: + value = bytes(codes).decode('utf-8') + except UnicodeDecodeError as exc: + raise ScannerError("while scanning a %s" % name, start_mark, str(exc), mark) + return value + + def scan_line_break(self): + # Transforms: + # '\r\n' : '\n' + # '\r' : '\n' + # '\n' : '\n' + # '\x85' : '\n' + # '\u2028' : '\u2028' + # '\u2029 : '\u2029' + # default : '' + ch = self.peek() + if ch in '\r\n\x85': + if self.prefix(2) == '\r\n': + self.forward(2) + else: + self.forward() + return '\n' + elif ch in '\u2028\u2029': + self.forward() + return ch + return '' diff --git a/dist/ba_data/python-site-packages/yaml/serializer.py b/dist/ba_data/python-site-packages/yaml/serializer.py new file mode 100644 index 0000000..fe911e6 --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/serializer.py @@ -0,0 +1,111 @@ + +__all__ = ['Serializer', 'SerializerError'] + +from .error import YAMLError +from .events import * +from .nodes import * + +class SerializerError(YAMLError): + pass + +class Serializer: + + ANCHOR_TEMPLATE = 'id%03d' + + def __init__(self, encoding=None, + explicit_start=None, explicit_end=None, version=None, tags=None): + self.use_encoding = encoding + self.use_explicit_start = explicit_start + self.use_explicit_end = explicit_end + self.use_version = version + self.use_tags = tags + self.serialized_nodes = {} + self.anchors = {} + self.last_anchor_id = 0 + self.closed = None + + def open(self): + if self.closed is None: + self.emit(StreamStartEvent(encoding=self.use_encoding)) + self.closed = False + elif self.closed: + raise SerializerError("serializer is closed") + else: + raise SerializerError("serializer is already opened") + + def close(self): + if self.closed is None: + raise SerializerError("serializer is not opened") + elif not self.closed: + self.emit(StreamEndEvent()) + self.closed = True + + #def __del__(self): + # self.close() + + def serialize(self, node): + if self.closed is None: + raise SerializerError("serializer is not opened") + elif self.closed: + raise SerializerError("serializer is closed") + self.emit(DocumentStartEvent(explicit=self.use_explicit_start, + version=self.use_version, tags=self.use_tags)) + self.anchor_node(node) + self.serialize_node(node, None, None) + self.emit(DocumentEndEvent(explicit=self.use_explicit_end)) + self.serialized_nodes = {} + self.anchors = {} + self.last_anchor_id = 0 + + def anchor_node(self, node): + if node in self.anchors: + if self.anchors[node] is None: + self.anchors[node] = self.generate_anchor(node) + else: + self.anchors[node] = None + if isinstance(node, SequenceNode): + for item in node.value: + self.anchor_node(item) + elif isinstance(node, MappingNode): + for key, value in node.value: + self.anchor_node(key) + self.anchor_node(value) + + def generate_anchor(self, node): + self.last_anchor_id += 1 + return self.ANCHOR_TEMPLATE % self.last_anchor_id + + def serialize_node(self, node, parent, index): + alias = self.anchors[node] + if node in self.serialized_nodes: + self.emit(AliasEvent(alias)) + else: + self.serialized_nodes[node] = True + self.descend_resolver(parent, index) + if isinstance(node, ScalarNode): + detected_tag = self.resolve(ScalarNode, node.value, (True, False)) + default_tag = self.resolve(ScalarNode, node.value, (False, True)) + implicit = (node.tag == detected_tag), (node.tag == default_tag) + self.emit(ScalarEvent(alias, node.tag, implicit, node.value, + style=node.style)) + elif isinstance(node, SequenceNode): + implicit = (node.tag + == self.resolve(SequenceNode, node.value, True)) + self.emit(SequenceStartEvent(alias, node.tag, implicit, + flow_style=node.flow_style)) + index = 0 + for item in node.value: + self.serialize_node(item, node, index) + index += 1 + self.emit(SequenceEndEvent()) + elif isinstance(node, MappingNode): + implicit = (node.tag + == self.resolve(MappingNode, node.value, True)) + self.emit(MappingStartEvent(alias, node.tag, implicit, + flow_style=node.flow_style)) + for key, value in node.value: + self.serialize_node(key, node, None) + self.serialize_node(value, node, key) + self.emit(MappingEndEvent()) + self.ascend_resolver() + diff --git a/dist/ba_data/python-site-packages/yaml/tokens.py b/dist/ba_data/python-site-packages/yaml/tokens.py new file mode 100644 index 0000000..4d0b48a --- /dev/null +++ b/dist/ba_data/python-site-packages/yaml/tokens.py @@ -0,0 +1,104 @@ + +class Token(object): + def __init__(self, start_mark, end_mark): + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + attributes = [key for key in self.__dict__ + if not key.endswith('_mark')] + attributes.sort() + arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) + for key in attributes]) + return '%s(%s)' % (self.__class__.__name__, arguments) + +#class BOMToken(Token): +# id = '' + +class DirectiveToken(Token): + id = '' + def __init__(self, name, value, start_mark, end_mark): + self.name = name + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class DocumentStartToken(Token): + id = '' + +class DocumentEndToken(Token): + id = '' + +class StreamStartToken(Token): + id = '' + def __init__(self, start_mark=None, end_mark=None, + encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + +class StreamEndToken(Token): + id = '' + +class BlockSequenceStartToken(Token): + id = '' + +class BlockMappingStartToken(Token): + id = '' + +class BlockEndToken(Token): + id = '' + +class FlowSequenceStartToken(Token): + id = '[' + +class FlowMappingStartToken(Token): + id = '{' + +class FlowSequenceEndToken(Token): + id = ']' + +class FlowMappingEndToken(Token): + id = '}' + +class KeyToken(Token): + id = '?' + +class ValueToken(Token): + id = ':' + +class BlockEntryToken(Token): + id = '-' + +class FlowEntryToken(Token): + id = ',' + +class AliasToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class AnchorToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class TagToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class ScalarToken(Token): + id = '' + def __init__(self, value, plain, start_mark, end_mark, style=None): + self.value = value + self.plain = plain + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + diff --git a/dist/ba_data/python/ba/__init__.py b/dist/ba_data/python/ba/__init__.py new file mode 100644 index 0000000..72792c7 --- /dev/null +++ b/dist/ba_data/python/ba/__init__.py @@ -0,0 +1,94 @@ +# Released under the MIT License. See LICENSE for details. +# +"""The public face of Ballistica. + +This top level module is a collection of most commonly used functionality. +For many modding purposes, the bits exposed here are all you'll need. +In some specific cases you may need to pull in individual submodules instead. +""" +# pylint: disable=unused-import +# pylint: disable=redefined-builtin + +from _ba import ( + CollideModel, Context, ContextCall, Data, InputDevice, Material, Model, + Node, SessionPlayer, Sound, Texture, Timer, Vec3, Widget, buttonwidget, + camerashake, checkboxwidget, columnwidget, containerwidget, do_once, + emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession, + getsound, gettexture, hscrollwidget, imagewidget, log, newactivity, + newnode, playsound, printnodes, printobjects, pushcall, quit, rowwidget, + safecolor, screenmessage, scrollwidget, set_analytics_screen, charstr, + textwidget, time, timer, open_url, widget, clipboard_is_supported, + clipboard_has_text, clipboard_get_text, clipboard_set_text) +from ba._activity import Activity +from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem +from ba._actor import Actor +from ba._player import PlayerInfo, Player, EmptyPlayer, StandLocation +from ba._nodeactor import NodeActor +from ba._app import App +from ba._coopgame import CoopGameActivity +from ba._coopsession import CoopSession +from ba._dependency import (Dependency, DependencyComponent, DependencySet, + AssetPackage) +from ba._enums import (TimeType, Permission, TimeFormat, SpecialChar, + InputType, UIScale) +from ba._error import ( + print_exception, print_error, ContextError, NotFoundError, + PlayerNotFoundError, SessionPlayerNotFoundError, NodeNotFoundError, + ActorNotFoundError, InputDeviceNotFoundError, WidgetNotFoundError, + ActivityNotFoundError, TeamNotFoundError, SessionTeamNotFoundError, + SessionNotFoundError, DelegateNotFoundError, DependencyError) +from ba._freeforallsession import FreeForAllSession +from ba._gameactivity import GameActivity +from ba._gameresults import GameResults +from ba._settings import (Setting, IntSetting, FloatSetting, ChoiceSetting, + BoolSetting, IntChoiceSetting, FloatChoiceSetting) +from ba._language import Lstr, LanguageSubsystem +from ba._map import Map, getmaps +from ba._session import Session +from ba._ui import UISubsystem +from ba._servermode import ServerController +from ba._score import ScoreType, ScoreConfig +from ba._stats import PlayerScoredMessage, PlayerRecord, Stats +from ba._team import SessionTeam, Team, EmptyTeam +from ba._teamgame import TeamGameActivity +from ba._dualteamsession import DualTeamSession +from ba._achievement import Achievement, AchievementSubsystem +from ba._appconfig import AppConfig +from ba._appdelegate import AppDelegate +from ba._apputils import is_browser_likely_available, garbage_collect +from ba._campaign import Campaign +from ba._gameutils import (GameTip, animate, animate_array, show_damage_count, + timestring, cameraflash) +from ba._general import (WeakCall, Call, existing, Existable, + verify_object_death, storagename, getclass) +from ba._keyboard import Keyboard +from ba._level import Level +from ba._lobby import Lobby, Chooser +from ba._math import normalized_color, is_point_in_box, vec3validate +from ba._meta import MetadataSubsystem +from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage, + PlayerDiedMessage, StandMessage, PickUpMessage, + DropMessage, PickedUpMessage, DroppedMessage, + ShouldShatterMessage, ImpactDamageMessage, + FreezeMessage, ThawMessage, HitMessage, + CelebrateMessage) +from ba._music import (setmusic, MusicPlayer, MusicType, MusicPlayMode, + MusicSubsystem) +from ba._powerup import PowerupMessage, PowerupAcceptMessage +from ba._multiteamsession import MultiTeamSession +from ba.ui import Window, UIController, uicleanupcheck +from ba._collision import Collision, getcollision + +app: App + + +# Change everything's listed module to simply 'ba' (instead of 'ba.foo.bar'). +def _simplify_module_names() -> None: + for attr, obj in globals().items(): + if not attr.startswith('_'): + if getattr(obj, '__module__', None) not in [None, 'ba']: + obj.__module__ = 'ba' + + +_simplify_module_names() +del _simplify_module_names diff --git a/dist/ba_data/python/ba/_account.py b/dist/ba_data/python/ba/_account.py new file mode 100644 index 0000000..4b8c997 --- /dev/null +++ b/dist/ba_data/python/ba/_account.py @@ -0,0 +1,267 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Account related functionality.""" + +from __future__ import annotations + +import copy +import time +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Any, Optional, Dict, List, Tuple + import ba + + +class AccountSubsystem: + """Subsystem for account handling in the app. + + Category: App Classes + + Access the single shared instance of this class at 'ba.app.plugins'. + """ + + def __init__(self) -> None: + self.account_tournament_list: Optional[Tuple[int, List[str]]] = None + + # FIXME: should abstract/structure these. + self.tournament_info: Dict = {} + self.league_rank_cache: Dict = {} + self.last_post_purchase_message_time: Optional[float] = None + + # If we try to run promo-codes due to launch-args/etc we might + # not be signed in yet; go ahead and queue them up in that case. + self.pending_promo_codes: List[str] = [] + + def on_app_launch(self) -> None: + """Called when the app is done bootstrapping.""" + + # Auto-sign-in to a local account in a moment if we're set to. + def do_auto_sign_in() -> None: + if _ba.app.headless_mode or _ba.app.config.get( + 'Auto Account State') == 'Local': + _ba.sign_in('Local') + + _ba.pushcall(do_auto_sign_in) + + def on_app_resume(self) -> None: + """Should be called when the app is resumed.""" + + # Mark our cached tourneys as invalid so anyone using them knows + # they might be out of date. + for entry in list(self.tournament_info.values()): + entry['valid'] = False + + def handle_account_gained_tickets(self, count: int) -> None: + """Called when the current account has been awarded tickets. + + (internal) + """ + from ba._language import Lstr + _ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText', + subs=[('${COUNT}', str(count))]), + color=(0, 1, 0)) + _ba.playsound(_ba.getsound('cashRegister')) + + def cache_league_rank_data(self, data: Any) -> None: + """(internal)""" + self.league_rank_cache['info'] = copy.deepcopy(data) + + def get_cached_league_rank_data(self) -> Any: + """(internal)""" + return self.league_rank_cache.get('info', None) + + def get_league_rank_points(self, + data: Optional[Dict[str, Any]], + subset: str = None) -> int: + """(internal)""" + if data is None: + return 0 + + # If the data contains an achievement total, use that. otherwise calc + # locally. + if data['at'] is not None: + total_ach_value = data['at'] + else: + total_ach_value = 0 + for ach in _ba.app.ach.achievements: + if ach.complete: + total_ach_value += ach.power_ranking_value + + trophies_total: int = (data['t0a'] * data['t0am'] + + data['t0b'] * data['t0bm'] + + data['t1'] * data['t1m'] + + data['t2'] * data['t2m'] + + data['t3'] * data['t3m'] + + data['t4'] * data['t4m']) + if subset == 'trophyCount': + val: int = (data['t0a'] + data['t0b'] + data['t1'] + data['t2'] + + data['t3'] + data['t4']) + assert isinstance(val, int) + return val + if subset == 'trophies': + assert isinstance(trophies_total, int) + return trophies_total + if subset is not None: + raise ValueError('invalid subset value: ' + str(subset)) + + if data['p']: + pro_mult = 1.0 + float( + _ba.get_account_misc_read_val('proPowerRankingBoost', + 0.0)) * 0.01 + else: + pro_mult = 1.0 + + # For final value, apply our pro mult and activeness-mult. + return int( + (total_ach_value + trophies_total) * + (data['act'] if data['act'] is not None else 1.0) * pro_mult) + + def cache_tournament_info(self, info: Any) -> None: + """(internal)""" + from ba._enums import TimeType, TimeFormat + for entry in info: + cache_entry = self.tournament_info[entry['tournamentID']] = ( + copy.deepcopy(entry)) + + # Also store the time we received this, so we can adjust + # time-remaining values/etc. + cache_entry['timeReceived'] = _ba.time(TimeType.REAL, + TimeFormat.MILLISECONDS) + cache_entry['valid'] = True + + def get_purchased_icons(self) -> List[str]: + """(internal)""" + # pylint: disable=cyclic-import + from ba import _store + if _ba.get_account_state() != 'signed_in': + return [] + icons = [] + store_items = _store.get_store_items() + for item_name, item in list(store_items.items()): + if item_name.startswith('icons.') and _ba.get_purchased(item_name): + icons.append(item['icon']) + return icons + + def ensure_have_account_player_profile(self) -> None: + """ + Ensure the standard account-named player profile exists; + creating if needed. + + (internal) + """ + # This only applies when we're signed in. + if _ba.get_account_state() != 'signed_in': + return + + # If the short version of our account name currently cant be + # displayed by the game, cancel. + if not _ba.have_chars(_ba.get_account_display_string(full=False)): + return + + config = _ba.app.config + if ('Player Profiles' not in config + or '__account__' not in config['Player Profiles']): + + # Create a spaz with a nice default purply color. + _ba.add_transaction({ + 'type': 'ADD_PLAYER_PROFILE', + 'name': '__account__', + 'profile': { + 'character': 'Spaz', + 'color': [0.5, 0.25, 1.0], + 'highlight': [0.5, 0.25, 1.0] + } + }) + _ba.run_transactions() + + def have_pro(self) -> bool: + """Return whether pro is currently unlocked.""" + + # Check our tickets-based pro upgrade and our two real-IAP based + # upgrades. Also unlock this stuff in ballistica-core builds. + return bool( + _ba.get_purchased('upgrades.pro') + or _ba.get_purchased('static.pro') + or _ba.get_purchased('static.pro_sale') + or 'ballistica' + 'core' == _ba.appname()) + + def have_pro_options(self) -> bool: + """Return whether pro-options are present. + + This is True for owners of Pro or old installs + before Pro was a requirement for these. + """ + + # We expose pro options if the server tells us to + # (which is generally just when we own pro), + # or also if we've been grandfathered in or are using ballistica-core + # builds. + return self.have_pro() or bool( + _ba.get_account_misc_read_val_2('proOptionsUnlocked', False) + or _ba.app.config.get('lc14292', 0) > 1) + + def show_post_purchase_message(self) -> None: + """(internal)""" + from ba._language import Lstr + from ba._enums import TimeType + cur_time = _ba.time(TimeType.REAL) + if (self.last_post_purchase_message_time is None + or cur_time - self.last_post_purchase_message_time > 3.0): + self.last_post_purchase_message_time = cur_time + with _ba.Context('ui'): + _ba.screenmessage(Lstr(resource='updatingAccountText', + fallback_resource='purchasingText'), + color=(0, 1, 0)) + _ba.playsound(_ba.getsound('click01')) + + def on_account_state_changed(self) -> None: + """(internal)""" + from ba._language import Lstr + + # Run any pending promo codes we had queued up while not signed in. + if _ba.get_account_state() == 'signed_in' and self.pending_promo_codes: + for code in self.pending_promo_codes: + _ba.screenmessage(Lstr(resource='submittingPromoCodeText'), + color=(0, 1, 0)) + _ba.add_transaction({ + 'type': 'PROMO_CODE', + 'expire_time': time.time() + 5, + 'code': code + }) + _ba.run_transactions() + self.pending_promo_codes = [] + + def add_pending_promo_code(self, code: str) -> None: + """(internal)""" + from ba._language import Lstr + from ba._enums import TimeType + + # If we're not signed in, queue up the code to run the next time we + # are and issue a warning if we haven't signed in within the next + # few seconds. + if _ba.get_account_state() != 'signed_in': + + def check_pending_codes() -> None: + """(internal)""" + + # If we're still not signed in and have pending codes, + # inform the user that they need to sign in to use them. + if self.pending_promo_codes: + _ba.screenmessage(Lstr(resource='signInForPromoCodeText'), + color=(1, 0, 0)) + _ba.playsound(_ba.getsound('error')) + + self.pending_promo_codes.append(code) + _ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL) + return + _ba.screenmessage(Lstr(resource='submittingPromoCodeText'), + color=(0, 1, 0)) + _ba.add_transaction({ + 'type': 'PROMO_CODE', + 'expire_time': time.time() + 5, + 'code': code + }) + _ba.run_transactions() diff --git a/dist/ba_data/python/ba/_achievement.py b/dist/ba_data/python/ba/_achievement.py new file mode 100644 index 0000000..8775313 --- /dev/null +++ b/dist/ba_data/python/ba/_achievement.py @@ -0,0 +1,1217 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Various functionality related to achievements.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +from ba._error import print_exception + +if TYPE_CHECKING: + from typing import Any, Sequence, List, Dict, Union, Optional, Tuple, Set + import ba + +# This could use some cleanup. +# We wear the cone of shame. +# pylint: disable=too-many-lines +# pylint: disable=too-many-statements +# pylint: disable=too-many-locals +# pylint: disable=too-many-branches + +# FIXME: We should probably point achievements +# at coop levels instead of hard-coding names. +# (so level name substitution works right and whatnot). +ACH_LEVEL_NAMES = { + 'Boom Goes the Dynamite': 'Pro Onslaught', + 'Boxer': 'Onslaught Training', + 'Flawless Victory': 'Rookie Onslaught', + 'Gold Miner': 'Uber Onslaught', + 'Got the Moves': 'Uber Football', + 'Last Stand God': 'The Last Stand', + 'Last Stand Master': 'The Last Stand', + 'Last Stand Wizard': 'The Last Stand', + 'Mine Games': 'Rookie Onslaught', + 'Off You Go Then': 'Onslaught Training', + 'Onslaught God': 'Infinite Onslaught', + 'Onslaught Master': 'Infinite Onslaught', + 'Onslaught Training Victory': 'Onslaught Training', + 'Onslaught Wizard': 'Infinite Onslaught', + 'Precision Bombing': 'Pro Runaround', + 'Pro Boxer': 'Pro Onslaught', + 'Pro Football Shutout': 'Pro Football', + 'Pro Football Victory': 'Pro Football', + 'Pro Onslaught Victory': 'Pro Onslaught', + 'Pro Runaround Victory': 'Pro Runaround', + 'Rookie Football Shutout': 'Rookie Football', + 'Rookie Football Victory': 'Rookie Football', + 'Rookie Onslaught Victory': 'Rookie Onslaught', + 'Runaround God': 'Infinite Runaround', + 'Runaround Master': 'Infinite Runaround', + 'Runaround Wizard': 'Infinite Runaround', + 'Stayin\' Alive': 'Uber Runaround', + 'Super Mega Punch': 'Pro Football', + 'Super Punch': 'Rookie Football', + 'TNT Terror': 'Uber Onslaught', + 'The Great Wall': 'Uber Runaround', + 'The Wall': 'Pro Runaround', + 'Uber Football Shutout': 'Uber Football', + 'Uber Football Victory': 'Uber Football', + 'Uber Onslaught Victory': 'Uber Onslaught', + 'Uber Runaround Victory': 'Uber Runaround' +} + + +class AchievementSubsystem: + """Subsystem for achievement handling. + + Category: App Classes + + Access the single shared instance of this class at 'ba.app.ach'. + """ + + def __init__(self) -> None: + self.achievements: List[Achievement] = [] + self.achievements_to_display: (List[Tuple[ba.Achievement, bool]]) = [] + self.achievement_display_timer: Optional[_ba.Timer] = None + self.last_achievement_display_time: float = 0.0 + self.achievement_completion_banner_slots: Set[int] = set() + self._init_achievements() + + def _init_achievements(self) -> None: + """Fill in available achievements.""" + + achs = self.achievements + + # 5 + achs.append( + Achievement('In Control', 'achievementInControl', (1, 1, 1), '', + 5)) + # 15 + achs.append( + Achievement('Sharing is Caring', 'achievementSharingIsCaring', + (1, 1, 1), '', 15)) + # 10 + achs.append( + Achievement('Dual Wielding', 'achievementDualWielding', (1, 1, 1), + '', 10)) + + # 10 + achs.append( + Achievement('Free Loader', 'achievementFreeLoader', (1, 1, 1), '', + 10)) + # 20 + achs.append( + Achievement('Team Player', 'achievementTeamPlayer', (1, 1, 1), '', + 20)) + + # 5 + achs.append( + Achievement('Onslaught Training Victory', 'achievementOnslaught', + (1, 1, 1), 'Default:Onslaught Training', 5)) + # 5 + achs.append( + Achievement('Off You Go Then', 'achievementOffYouGo', + (1, 1.1, 1.3), 'Default:Onslaught Training', 5)) + # 10 + achs.append( + Achievement('Boxer', + 'achievementBoxer', (1, 0.6, 0.6), + 'Default:Onslaught Training', + 10, + hard_mode_only=True)) + + # 10 + achs.append( + Achievement('Rookie Onslaught Victory', 'achievementOnslaught', + (0.5, 1.4, 0.6), 'Default:Rookie Onslaught', 10)) + # 10 + achs.append( + Achievement('Mine Games', 'achievementMine', (1, 1, 1.4), + 'Default:Rookie Onslaught', 10)) + # 15 + achs.append( + Achievement('Flawless Victory', + 'achievementFlawlessVictory', (1, 1, 1), + 'Default:Rookie Onslaught', + 15, + hard_mode_only=True)) + + # 10 + achs.append( + Achievement('Rookie Football Victory', + 'achievementFootballVictory', (1.0, 1, 0.6), + 'Default:Rookie Football', 10)) + # 10 + achs.append( + Achievement('Super Punch', 'achievementSuperPunch', (1, 1, 1.8), + 'Default:Rookie Football', 10)) + # 15 + achs.append( + Achievement('Rookie Football Shutout', + 'achievementFootballShutout', (1, 1, 1), + 'Default:Rookie Football', + 15, + hard_mode_only=True)) + + # 15 + achs.append( + Achievement('Pro Onslaught Victory', 'achievementOnslaught', + (0.3, 1, 2.0), 'Default:Pro Onslaught', 15)) + # 15 + achs.append( + Achievement('Boom Goes the Dynamite', 'achievementTNT', + (1.4, 1.2, 0.8), 'Default:Pro Onslaught', 15)) + # 20 + achs.append( + Achievement('Pro Boxer', + 'achievementBoxer', (2, 2, 0), + 'Default:Pro Onslaught', + 20, + hard_mode_only=True)) + + # 15 + achs.append( + Achievement('Pro Football Victory', 'achievementFootballVictory', + (1.3, 1.3, 2.0), 'Default:Pro Football', 15)) + # 15 + achs.append( + Achievement('Super Mega Punch', 'achievementSuperPunch', + (2, 1, 0.6), 'Default:Pro Football', 15)) + # 20 + achs.append( + Achievement('Pro Football Shutout', + 'achievementFootballShutout', (0.7, 0.7, 2.0), + 'Default:Pro Football', + 20, + hard_mode_only=True)) + + # 15 + achs.append( + Achievement('Pro Runaround Victory', 'achievementRunaround', + (1, 1, 1), 'Default:Pro Runaround', 15)) + # 20 + achs.append( + Achievement('Precision Bombing', + 'achievementCrossHair', (1, 1, 1.3), + 'Default:Pro Runaround', + 20, + hard_mode_only=True)) + # 25 + achs.append( + Achievement('The Wall', + 'achievementWall', (1, 0.7, 0.7), + 'Default:Pro Runaround', + 25, + hard_mode_only=True)) + + # 30 + achs.append( + Achievement('Uber Onslaught Victory', 'achievementOnslaught', + (2, 2, 1), 'Default:Uber Onslaught', 30)) + # 30 + achs.append( + Achievement('Gold Miner', + 'achievementMine', (2, 1.6, 0.2), + 'Default:Uber Onslaught', + 30, + hard_mode_only=True)) + # 30 + achs.append( + Achievement('TNT Terror', + 'achievementTNT', (2, 1.8, 0.3), + 'Default:Uber Onslaught', + 30, + hard_mode_only=True)) + + # 30 + achs.append( + Achievement('Uber Football Victory', 'achievementFootballVictory', + (1.8, 1.4, 0.3), 'Default:Uber Football', 30)) + # 30 + achs.append( + Achievement('Got the Moves', + 'achievementGotTheMoves', (2, 1, 0), + 'Default:Uber Football', + 30, + hard_mode_only=True)) + # 40 + achs.append( + Achievement('Uber Football Shutout', + 'achievementFootballShutout', (2, 2, 0), + 'Default:Uber Football', + 40, + hard_mode_only=True)) + + # 30 + achs.append( + Achievement('Uber Runaround Victory', 'achievementRunaround', + (1.5, 1.2, 0.2), 'Default:Uber Runaround', 30)) + # 40 + achs.append( + Achievement('The Great Wall', + 'achievementWall', (2, 1.7, 0.4), + 'Default:Uber Runaround', + 40, + hard_mode_only=True)) + # 40 + achs.append( + Achievement('Stayin\' Alive', + 'achievementStayinAlive', (2, 2, 1), + 'Default:Uber Runaround', + 40, + hard_mode_only=True)) + + # 20 + achs.append( + Achievement('Last Stand Master', + 'achievementMedalSmall', (2, 1.5, 0.3), + 'Default:The Last Stand', + 20, + hard_mode_only=True)) + # 40 + achs.append( + Achievement('Last Stand Wizard', + 'achievementMedalMedium', (2, 1.5, 0.3), + 'Default:The Last Stand', + 40, + hard_mode_only=True)) + # 60 + achs.append( + Achievement('Last Stand God', + 'achievementMedalLarge', (2, 1.5, 0.3), + 'Default:The Last Stand', + 60, + hard_mode_only=True)) + + # 5 + achs.append( + Achievement('Onslaught Master', 'achievementMedalSmall', + (0.7, 1, 0.7), 'Challenges:Infinite Onslaught', 5)) + # 15 + achs.append( + Achievement('Onslaught Wizard', 'achievementMedalMedium', + (0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 15)) + # 30 + achs.append( + Achievement('Onslaught God', 'achievementMedalLarge', + (0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 30)) + + # 5 + achs.append( + Achievement('Runaround Master', 'achievementMedalSmall', + (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 5)) + # 15 + achs.append( + Achievement('Runaround Wizard', 'achievementMedalMedium', + (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 15)) + # 30 + achs.append( + Achievement('Runaround God', 'achievementMedalLarge', + (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 30)) + + def award_local_achievement(self, achname: str) -> None: + """For non-game-based achievements such as controller-connection.""" + try: + ach = self.get_achievement(achname) + if not ach.complete: + + # Report new achievements to the game-service. + _ba.report_achievement(achname) + + # And to our account. + _ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname}) + + # Now attempt to show a banner. + self.display_achievement_banner(achname) + + except Exception: + print_exception() + + def display_achievement_banner(self, achname: str) -> None: + """Display a completion banner for an achievement. + + (internal) + + Used for server-driven achievements. + """ + try: + # FIXME: Need to get these using the UI context or some other + # purely local context somehow instead of trying to inject these + # into whatever activity happens to be active + # (since that won't work while in client mode). + activity = _ba.get_foreground_host_activity() + if activity is not None: + with _ba.Context(activity): + self.get_achievement(achname).announce_completion() + except Exception: + print_exception('error showing server ach') + + def set_completed_achievements(self, achs: Sequence[str]) -> None: + """Set the current state of completed achievements. + + (internal) + + All achievements not included here will be set incomplete. + """ + + # Note: This gets called whenever game-center/game-circle/etc tells + # us which achievements we currently have. We always defer to them, + # even if that means we have to un-set an achievement we think we have. + + cfg = _ba.app.config + cfg['Achievements'] = {} + for a_name in achs: + self.get_achievement(a_name).set_complete(True) + cfg.commit() + + def get_achievement(self, name: str) -> Achievement: + """Return an Achievement by name.""" + achs = [a for a in self.achievements if a.name == name] + assert len(achs) < 2 + if not achs: + raise ValueError("Invalid achievement name: '" + name + "'") + return achs[0] + + def achievements_for_coop_level(self, + level_name: str) -> List[Achievement]: + """Given a level name, return achievements available for it.""" + + # For the Easy campaign we return achievements for the Default + # campaign too. (want the user to see what achievements are part of the + # level even if they can't unlock them all on easy mode). + return [ + a for a in self.achievements + if a.level_name in (level_name, + level_name.replace('Easy', 'Default')) + ] + + def _test(self) -> None: + """For testing achievement animations.""" + from ba._enums import TimeType + + def testcall1() -> None: + self.achievements[0].announce_completion() + self.achievements[1].announce_completion() + self.achievements[2].announce_completion() + + def testcall2() -> None: + self.achievements[3].announce_completion() + self.achievements[4].announce_completion() + self.achievements[5].announce_completion() + + _ba.timer(3.0, testcall1, timetype=TimeType.BASE) + _ba.timer(7.0, testcall2, timetype=TimeType.BASE) + + +def _get_ach_mult(include_pro_bonus: bool = False) -> int: + """Return the multiplier for achievement pts. + + (just for display; changing this here won't affect actual rewards) + """ + val: int = _ba.get_account_misc_read_val('achAwardMult', 5) + assert isinstance(val, int) + if include_pro_bonus and _ba.app.accounts.have_pro(): + val *= 2 + return val + + +def _display_next_achievement() -> None: + + # Pull the first achievement off the list and display it, or kill the + # display-timer if the list is empty. + app = _ba.app + if app.ach.achievements_to_display: + try: + ach, sound = app.ach.achievements_to_display.pop(0) + ach.show_completion_banner(sound) + except Exception: + print_exception('error showing next achievement') + app.ach.achievements_to_display = [] + app.ach.achievement_display_timer = None + else: + app.ach.achievement_display_timer = None + + +class Achievement: + """Represents attributes and state for an individual achievement. + + Category: App Classes + """ + + def __init__(self, + name: str, + icon_name: str, + icon_color: Sequence[float], + level_name: str, + award: int, + hard_mode_only: bool = False): + self._name = name + self._icon_name = icon_name + self._icon_color: Sequence[float] = list(icon_color) + [1] + self._level_name = level_name + self._completion_banner_slot: Optional[int] = None + self._award = award + self._hard_mode_only = hard_mode_only + + @property + def name(self) -> str: + """The name of this achievement.""" + return self._name + + @property + def level_name(self) -> str: + """The name of the level this achievement applies to.""" + return self._level_name + + def get_icon_texture(self, complete: bool) -> ba.Texture: + """Return the icon texture to display for this achievement""" + return _ba.gettexture( + self._icon_name if complete else 'achievementEmpty') + + def get_icon_color(self, complete: bool) -> Sequence[float]: + """Return the color tint for this Achievement's icon.""" + if complete: + return self._icon_color + return 1.0, 1.0, 1.0, 0.6 + + @property + def hard_mode_only(self) -> bool: + """Whether this Achievement is only unlockable in hard-mode.""" + return self._hard_mode_only + + @property + def complete(self) -> bool: + """Whether this Achievement is currently complete.""" + val: bool = self._getconfig()['Complete'] + assert isinstance(val, bool) + return val + + def announce_completion(self, sound: bool = True) -> None: + """Kick off an announcement for this achievement's completion.""" + from ba._enums import TimeType + app = _ba.app + + # Even though there are technically achievements when we're not + # signed in, lets not show them (otherwise we tend to get + # confusing 'controller connected' achievements popping up while + # waiting to log in which can be confusing). + if _ba.get_account_state() != 'signed_in': + return + + # If we're being freshly complete, display/report it and whatnot. + if (self, sound) not in app.ach.achievements_to_display: + app.ach.achievements_to_display.append((self, sound)) + + # If there's no achievement display timer going, kick one off + # (if one's already running it will pick this up before it dies). + + # Need to check last time too; its possible our timer wasn't able to + # clear itself if an activity died and took it down with it. + if ((app.ach.achievement_display_timer is None + or _ba.time(TimeType.REAL) - app.ach.last_achievement_display_time + > 2.0) and _ba.getactivity(doraise=False) is not None): + app.ach.achievement_display_timer = _ba.Timer( + 1.0, + _display_next_achievement, + repeat=True, + timetype=TimeType.BASE) + + # Show the first immediately. + _display_next_achievement() + + def set_complete(self, complete: bool = True) -> None: + """Set an achievement's completed state. + + note this only sets local state; use a transaction to + actually award achievements. + """ + config = self._getconfig() + if complete != config['Complete']: + config['Complete'] = complete + + @property + def display_name(self) -> ba.Lstr: + """Return a ba.Lstr for this Achievement's name.""" + from ba._language import Lstr + name: Union[ba.Lstr, str] + try: + if self._level_name != '': + from ba._campaign import getcampaign + campaignname, campaign_level = self._level_name.split(':') + name = getcampaign(campaignname).getlevel( + campaign_level).displayname + else: + name = '' + except Exception: + name = '' + print_exception() + return Lstr(resource='achievements.' + self._name + '.name', + subs=[('${LEVEL}', name)]) + + @property + def description(self) -> ba.Lstr: + """Get a ba.Lstr for the Achievement's brief description.""" + from ba._language import Lstr + if 'description' in _ba.app.lang.get_resource('achievements')[ + self._name]: + return Lstr(resource='achievements.' + self._name + '.description') + return Lstr(resource='achievements.' + self._name + '.descriptionFull') + + @property + def description_complete(self) -> ba.Lstr: + """Get a ba.Lstr for the Achievement's description when completed.""" + from ba._language import Lstr + if 'descriptionComplete' in _ba.app.lang.get_resource('achievements')[ + self._name]: + return Lstr(resource='achievements.' + self._name + + '.descriptionComplete') + return Lstr(resource='achievements.' + self._name + + '.descriptionFullComplete') + + @property + def description_full(self) -> ba.Lstr: + """Get a ba.Lstr for the Achievement's full description.""" + from ba._language import Lstr + + return Lstr( + resource='achievements.' + self._name + '.descriptionFull', + subs=[('${LEVEL}', + Lstr(translate=('coopLevelNames', + ACH_LEVEL_NAMES.get(self._name, '?'))))]) + + @property + def description_full_complete(self) -> ba.Lstr: + """Get a ba.Lstr for the Achievement's full desc. when completed.""" + from ba._language import Lstr + return Lstr( + resource='achievements.' + self._name + '.descriptionFullComplete', + subs=[('${LEVEL}', + Lstr(translate=('coopLevelNames', + ACH_LEVEL_NAMES.get(self._name, '?'))))]) + + def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int: + """Get the ticket award value for this achievement.""" + val: int = (_ba.get_account_misc_read_val('achAward.' + self._name, + self._award) * + _get_ach_mult(include_pro_bonus)) + assert isinstance(val, int) + return val + + @property + def power_ranking_value(self) -> int: + """Get the power-ranking award value for this achievement.""" + val: int = _ba.get_account_misc_read_val( + 'achLeaguePoints.' + self._name, self._award) + assert isinstance(val, int) + return val + + def create_display(self, + x: float, + y: float, + delay: float, + outdelay: float = None, + color: Sequence[float] = None, + style: str = 'post_game') -> List[ba.Actor]: + """Create a display for the Achievement. + + Shows the Achievement icon, name, and description. + """ + # pylint: disable=cyclic-import + from ba._language import Lstr + from ba._enums import SpecialChar + from ba._coopsession import CoopSession + from bastd.actor.image import Image + from bastd.actor.text import Text + + # Yeah this needs cleaning up. + if style == 'post_game': + in_game_colors = False + in_main_menu = False + h_attach = Text.HAttach.CENTER + v_attach = Text.VAttach.CENTER + attach = Image.Attach.CENTER + elif style == 'in_game': + in_game_colors = True + in_main_menu = False + h_attach = Text.HAttach.LEFT + v_attach = Text.VAttach.TOP + attach = Image.Attach.TOP_LEFT + elif style == 'news': + in_game_colors = True + in_main_menu = True + h_attach = Text.HAttach.CENTER + v_attach = Text.VAttach.TOP + attach = Image.Attach.TOP_CENTER + else: + raise ValueError('invalid style "' + style + '"') + + # Attempt to determine what campaign we're in + # (so we know whether to show "hard mode only"). + if in_main_menu: + hmo = False + else: + try: + session = _ba.getsession() + if isinstance(session, CoopSession): + campaign = session.campaign + assert campaign is not None + hmo = (self._hard_mode_only and campaign.name == 'Easy') + else: + hmo = False + except Exception: + print_exception('Error determining campaign.') + hmo = False + + objs: List[ba.Actor] + + if in_game_colors: + objs = [] + out_delay_fin = (delay + + outdelay) if outdelay is not None else None + if color is not None: + cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2], + color[3]) + cl2 = color + else: + cl1 = (1.5, 1.5, 2, 1.0) + cl2 = (0.8, 0.8, 1.0, 1.0) + + if hmo: + cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6) + cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2) + + objs.append( + Image(self.get_icon_texture(False), + host_only=True, + color=cl1, + position=(x - 25, y + 5), + attach=attach, + transition=Image.Transition.FADE_IN, + transition_delay=delay, + vr_depth=4, + transition_out_delay=out_delay_fin, + scale=(40, 40)).autoretain()) + txt = self.display_name + txt_s = 0.85 + txt_max_w = 300 + objs.append( + Text(txt, + host_only=True, + maxwidth=txt_max_w, + position=(x, y + 2), + transition=Text.Transition.FADE_IN, + scale=txt_s, + flatness=0.6, + shadow=0.5, + h_attach=h_attach, + v_attach=v_attach, + color=cl2, + transition_delay=delay + 0.05, + transition_out_delay=out_delay_fin).autoretain()) + txt2_s = 0.62 + txt2_max_w = 400 + objs.append( + Text(self.description_full + if in_main_menu else self.description, + host_only=True, + maxwidth=txt2_max_w, + position=(x, y - 14), + transition=Text.Transition.FADE_IN, + vr_depth=-5, + h_attach=h_attach, + v_attach=v_attach, + scale=txt2_s, + flatness=1.0, + shadow=0.5, + color=cl2, + transition_delay=delay + 0.1, + transition_out_delay=out_delay_fin).autoretain()) + + if hmo: + txtactor = Text( + Lstr(resource='difficultyHardOnlyText'), + host_only=True, + maxwidth=txt2_max_w * 0.7, + position=(x + 60, y + 5), + transition=Text.Transition.FADE_IN, + vr_depth=-5, + h_attach=h_attach, + v_attach=v_attach, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + scale=txt_s * 0.8, + flatness=1.0, + shadow=0.5, + color=(1, 1, 0.6, 1), + transition_delay=delay + 0.1, + transition_out_delay=out_delay_fin).autoretain() + txtactor.node.rotate = 10 + objs.append(txtactor) + + # Ticket-award. + award_x = -100 + objs.append( + Text(_ba.charstr(SpecialChar.TICKET), + host_only=True, + position=(x + award_x + 33, y + 7), + transition=Text.Transition.FADE_IN, + scale=1.5, + h_attach=h_attach, + v_attach=v_attach, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + color=(1, 1, 1, 0.2 if hmo else 0.4), + transition_delay=delay + 0.05, + transition_out_delay=out_delay_fin).autoretain()) + objs.append( + Text('+' + str(self.get_award_ticket_value()), + host_only=True, + position=(x + award_x + 28, y + 16), + transition=Text.Transition.FADE_IN, + scale=0.7, + flatness=1, + h_attach=h_attach, + v_attach=v_attach, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + color=cl2, + transition_delay=delay + 0.05, + transition_out_delay=out_delay_fin).autoretain()) + + else: + complete = self.complete + objs = [] + c_icon = self.get_icon_color(complete) + if hmo and not complete: + c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3) + objs.append( + Image(self.get_icon_texture(complete), + host_only=True, + color=c_icon, + position=(x - 25, y + 5), + attach=attach, + vr_depth=4, + transition=Image.Transition.IN_RIGHT, + transition_delay=delay, + transition_out_delay=None, + scale=(40, 40)).autoretain()) + if complete: + objs.append( + Image(_ba.gettexture('achievementOutline'), + host_only=True, + model_transparent=_ba.getmodel('achievementOutline'), + color=(2, 1.4, 0.4, 1), + vr_depth=8, + position=(x - 25, y + 5), + attach=attach, + transition=Image.Transition.IN_RIGHT, + transition_delay=delay, + transition_out_delay=None, + scale=(40, 40)).autoretain()) + else: + if not complete: + award_x = -100 + objs.append( + Text(_ba.charstr(SpecialChar.TICKET), + host_only=True, + position=(x + award_x + 33, y + 7), + transition=Text.Transition.IN_RIGHT, + scale=1.5, + h_attach=h_attach, + v_attach=v_attach, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + color=(1, 1, 1, 0.4) if complete else + (1, 1, 1, (0.1 if hmo else 0.2)), + transition_delay=delay + 0.05, + transition_out_delay=None).autoretain()) + objs.append( + Text('+' + str(self.get_award_ticket_value()), + host_only=True, + position=(x + award_x + 28, y + 16), + transition=Text.Transition.IN_RIGHT, + scale=0.7, + flatness=1, + h_attach=h_attach, + v_attach=v_attach, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + color=((0.8, 0.93, 0.8, 1.0) if complete else + (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))), + transition_delay=delay + 0.05, + transition_out_delay=None).autoretain()) + + # Show 'hard-mode-only' only over incomplete achievements + # when that's the case. + if hmo: + txtactor = Text( + Lstr(resource='difficultyHardOnlyText'), + host_only=True, + maxwidth=300 * 0.7, + position=(x + 60, y + 5), + transition=Text.Transition.FADE_IN, + vr_depth=-5, + h_attach=h_attach, + v_attach=v_attach, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + scale=0.85 * 0.8, + flatness=1.0, + shadow=0.5, + color=(1, 1, 0.6, 1), + transition_delay=delay + 0.05, + transition_out_delay=None).autoretain() + assert txtactor.node + txtactor.node.rotate = 10 + objs.append(txtactor) + + objs.append( + Text(self.display_name, + host_only=True, + maxwidth=300, + position=(x, y + 2), + transition=Text.Transition.IN_RIGHT, + scale=0.85, + flatness=0.6, + h_attach=h_attach, + v_attach=v_attach, + color=((0.8, 0.93, 0.8, 1.0) if complete else + (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))), + transition_delay=delay + 0.05, + transition_out_delay=None).autoretain()) + objs.append( + Text(self.description_complete + if complete else self.description, + host_only=True, + maxwidth=400, + position=(x, y - 14), + transition=Text.Transition.IN_RIGHT, + vr_depth=-5, + h_attach=h_attach, + v_attach=v_attach, + scale=0.62, + flatness=1.0, + color=((0.6, 0.6, 0.6, 1.0) if complete else + (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))), + transition_delay=delay + 0.1, + transition_out_delay=None).autoretain()) + return objs + + def _getconfig(self) -> Dict[str, Any]: + """ + Return the sub-dict in settings where this achievement's + state is stored, creating it if need be. + """ + val: Dict[str, Any] = (_ba.app.config.setdefault( + 'Achievements', {}).setdefault(self._name, {'Complete': False})) + assert isinstance(val, dict) + return val + + def _remove_banner_slot(self) -> None: + assert self._completion_banner_slot is not None + _ba.app.ach.achievement_completion_banner_slots.remove( + self._completion_banner_slot) + self._completion_banner_slot = None + + def show_completion_banner(self, sound: bool = True) -> None: + """Create the banner/sound for an acquired achievement announcement.""" + from ba import _account + from ba import _gameutils + from bastd.actor.text import Text + from bastd.actor.image import Image + from ba._general import WeakCall + from ba._language import Lstr + from ba._messages import DieMessage + from ba._enums import TimeType, SpecialChar + app = _ba.app + app.ach.last_achievement_display_time = _ba.time(TimeType.REAL) + + # Just piggy-back onto any current activity + # (should we use the session instead?..) + activity = _ba.getactivity(doraise=False) + + # If this gets called while this achievement is occupying a slot + # already, ignore it. (probably should never happen in real + # life but whatevs). + if self._completion_banner_slot is not None: + return + + if activity is None: + print('show_completion_banner() called with no current activity!') + return + + if sound: + _ba.playsound(_ba.getsound('achievement'), host_only=True) + else: + _ba.timer( + 0.5, + lambda: _ba.playsound(_ba.getsound('ding'), host_only=True)) + + in_time = 0.300 + out_time = 3.5 + + base_vr_depth = 200 + + # Find the first free slot. + i = 0 + while True: + if i not in app.ach.achievement_completion_banner_slots: + app.ach.achievement_completion_banner_slots.add(i) + self._completion_banner_slot = i + + # Remove us from that slot when we close. + # Use a real-timer in the UI context so the removal runs even + # if our activity/session dies. + with _ba.Context('ui'): + _ba.timer(in_time + out_time, + self._remove_banner_slot, + timetype=TimeType.REAL) + break + i += 1 + assert self._completion_banner_slot is not None + y_offs = 110 * self._completion_banner_slot + objs: List[ba.Actor] = [] + obj = Image(_ba.gettexture('shadow'), + position=(-30, 30 + y_offs), + front=True, + attach=Image.Attach.BOTTOM_CENTER, + transition=Image.Transition.IN_BOTTOM, + vr_depth=base_vr_depth - 100, + transition_delay=in_time, + transition_out_delay=out_time, + color=(0.0, 0.1, 0, 1), + scale=(1000, 300)).autoretain() + objs.append(obj) + assert obj.node + obj.node.host_only = True + obj = Image(_ba.gettexture('light'), + position=(-180, 60 + y_offs), + front=True, + attach=Image.Attach.BOTTOM_CENTER, + vr_depth=base_vr_depth, + transition=Image.Transition.IN_BOTTOM, + transition_delay=in_time, + transition_out_delay=out_time, + color=(1.8, 1.8, 1.0, 0.0), + scale=(40, 300)).autoretain() + objs.append(obj) + assert obj.node + obj.node.host_only = True + obj.node.premultiplied = True + combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 2}) + _gameutils.animate( + combine, 'input0', { + in_time: 0, + in_time + 0.4: 30, + in_time + 0.5: 40, + in_time + 0.6: 30, + in_time + 2.0: 0 + }) + _gameutils.animate( + combine, 'input1', { + in_time: 0, + in_time + 0.4: 200, + in_time + 0.5: 500, + in_time + 0.6: 200, + in_time + 2.0: 0 + }) + combine.connectattr('output', obj.node, 'scale') + _gameutils.animate(obj.node, + 'rotate', { + 0: 0.0, + 0.35: 360.0 + }, + loop=True) + obj = Image(self.get_icon_texture(True), + position=(-180, 60 + y_offs), + attach=Image.Attach.BOTTOM_CENTER, + front=True, + vr_depth=base_vr_depth - 10, + transition=Image.Transition.IN_BOTTOM, + transition_delay=in_time, + transition_out_delay=out_time, + scale=(100, 100)).autoretain() + objs.append(obj) + assert obj.node + obj.node.host_only = True + + # Flash. + color = self.get_icon_color(True) + combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) + keys = { + in_time: 1.0 * color[0], + in_time + 0.4: 1.5 * color[0], + in_time + 0.5: 6.0 * color[0], + in_time + 0.6: 1.5 * color[0], + in_time + 2.0: 1.0 * color[0] + } + _gameutils.animate(combine, 'input0', keys) + keys = { + in_time: 1.0 * color[1], + in_time + 0.4: 1.5 * color[1], + in_time + 0.5: 6.0 * color[1], + in_time + 0.6: 1.5 * color[1], + in_time + 2.0: 1.0 * color[1] + } + _gameutils.animate(combine, 'input1', keys) + keys = { + in_time: 1.0 * color[2], + in_time + 0.4: 1.5 * color[2], + in_time + 0.5: 6.0 * color[2], + in_time + 0.6: 1.5 * color[2], + in_time + 2.0: 1.0 * color[2] + } + _gameutils.animate(combine, 'input2', keys) + combine.connectattr('output', obj.node, 'color') + + obj = Image(_ba.gettexture('achievementOutline'), + model_transparent=_ba.getmodel('achievementOutline'), + position=(-180, 60 + y_offs), + front=True, + attach=Image.Attach.BOTTOM_CENTER, + vr_depth=base_vr_depth, + transition=Image.Transition.IN_BOTTOM, + transition_delay=in_time, + transition_out_delay=out_time, + scale=(100, 100)).autoretain() + assert obj.node + obj.node.host_only = True + + # Flash. + color = (2, 1.4, 0.4, 1) + combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) + keys = { + in_time: 1.0 * color[0], + in_time + 0.4: 1.5 * color[0], + in_time + 0.5: 6.0 * color[0], + in_time + 0.6: 1.5 * color[0], + in_time + 2.0: 1.0 * color[0] + } + _gameutils.animate(combine, 'input0', keys) + keys = { + in_time: 1.0 * color[1], + in_time + 0.4: 1.5 * color[1], + in_time + 0.5: 6.0 * color[1], + in_time + 0.6: 1.5 * color[1], + in_time + 2.0: 1.0 * color[1] + } + _gameutils.animate(combine, 'input1', keys) + keys = { + in_time: 1.0 * color[2], + in_time + 0.4: 1.5 * color[2], + in_time + 0.5: 6.0 * color[2], + in_time + 0.6: 1.5 * color[2], + in_time + 2.0: 1.0 * color[2] + } + _gameutils.animate(combine, 'input2', keys) + combine.connectattr('output', obj.node, 'color') + objs.append(obj) + + objt = Text(Lstr(value='${A}:', + subs=[('${A}', Lstr(resource='achievementText'))]), + position=(-120, 91 + y_offs), + front=True, + v_attach=Text.VAttach.BOTTOM, + vr_depth=base_vr_depth - 10, + transition=Text.Transition.IN_BOTTOM, + flatness=0.5, + transition_delay=in_time, + transition_out_delay=out_time, + color=(1, 1, 1, 0.8), + scale=0.65).autoretain() + objs.append(objt) + assert objt.node + objt.node.host_only = True + + objt = Text(self.display_name, + position=(-120, 50 + y_offs), + front=True, + v_attach=Text.VAttach.BOTTOM, + transition=Text.Transition.IN_BOTTOM, + vr_depth=base_vr_depth, + flatness=0.5, + transition_delay=in_time, + transition_out_delay=out_time, + flash=True, + color=(1, 0.8, 0, 1.0), + scale=1.5).autoretain() + objs.append(objt) + assert objt.node + objt.node.host_only = True + + objt = Text(_ba.charstr(SpecialChar.TICKET), + position=(-120 - 170 + 5, 75 + y_offs - 20), + front=True, + v_attach=Text.VAttach.BOTTOM, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.IN_BOTTOM, + vr_depth=base_vr_depth, + transition_delay=in_time, + transition_out_delay=out_time, + flash=True, + color=(0.5, 0.5, 0.5, 1), + scale=3.0).autoretain() + objs.append(objt) + assert objt.node + objt.node.host_only = True + + objt = Text('+' + str(self.get_award_ticket_value()), + position=(-120 - 180 + 5, 80 + y_offs - 20), + v_attach=Text.VAttach.BOTTOM, + front=True, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.IN_BOTTOM, + vr_depth=base_vr_depth, + flatness=0.5, + shadow=1.0, + transition_delay=in_time, + transition_out_delay=out_time, + flash=True, + color=(0, 1, 0, 1), + scale=1.5).autoretain() + objs.append(objt) + assert objt.node + objt.node.host_only = True + + # Add the 'x 2' if we've got pro. + if app.accounts.have_pro(): + objt = Text('x 2', + position=(-120 - 180 + 45, 80 + y_offs - 50), + v_attach=Text.VAttach.BOTTOM, + front=True, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.IN_BOTTOM, + vr_depth=base_vr_depth, + flatness=0.5, + shadow=1.0, + transition_delay=in_time, + transition_out_delay=out_time, + flash=True, + color=(0.4, 0, 1, 1), + scale=0.9).autoretain() + objs.append(objt) + assert objt.node + objt.node.host_only = True + + objt = Text(self.description_complete, + position=(-120, 30 + y_offs), + front=True, + v_attach=Text.VAttach.BOTTOM, + transition=Text.Transition.IN_BOTTOM, + vr_depth=base_vr_depth - 10, + flatness=0.5, + transition_delay=in_time, + transition_out_delay=out_time, + color=(1.0, 0.7, 0.5, 1.0), + scale=0.8).autoretain() + objs.append(objt) + assert objt.node + objt.node.host_only = True + + for actor in objs: + _ba.timer(out_time + 1.000, + WeakCall(actor.handlemessage, DieMessage())) diff --git a/dist/ba_data/python/ba/_activity.py b/dist/ba_data/python/ba/_activity.py new file mode 100644 index 0000000..2304ca9 --- /dev/null +++ b/dist/ba_data/python/ba/_activity.py @@ -0,0 +1,870 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Activity class.""" +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING, Generic, TypeVar + +import _ba +from ba._team import Team +from ba._player import Player +from ba._error import (print_exception, SessionTeamNotFoundError, + SessionPlayerNotFoundError, NodeNotFoundError) +from ba._dependency import DependencyComponent +from ba._general import Call, verify_object_death +from ba._messages import UNHANDLED + +if TYPE_CHECKING: + from weakref import ReferenceType + from typing import Optional, Type, Any, Dict, List + import ba + from bastd.actor.respawnicon import RespawnIcon + +PlayerType = TypeVar('PlayerType', bound=Player) +TeamType = TypeVar('TeamType', bound=Team) + + +class Activity(DependencyComponent, Generic[PlayerType, TeamType]): + """Units of execution wrangled by a ba.Session. + + Category: Gameplay Classes + + Examples of Activities include games, score-screens, cutscenes, etc. + A ba.Session has one 'current' Activity at any time, though their existence + can overlap during transitions. + + Attributes: + + settings_raw + The settings dict passed in when the activity was made. + This attribute is deprecated and should be avoided when possible; + activities should pull all values they need from the 'settings' arg + passed to the Activity __init__ call. + + teams + The list of ba.Teams in the Activity. This gets populated just before + before on_begin() is called and is updated automatically as players + join or leave the game. (at least in free-for-all mode where every + player gets their own team; in teams mode there are always 2 teams + regardless of the player count). + + players + The list of ba.Players in the Activity. This gets populated just + before on_begin() is called and is updated automatically as players + join or leave the game. + """ + + # pylint: disable=too-many-public-methods + + # Annotating attr types at the class level lets us introspect at runtime. + settings_raw: Dict[str, Any] + teams: List[TeamType] + players: List[PlayerType] + + # Whether to print every time a player dies. This can be pertinent + # in games such as Death-Match but can be annoying in games where it + # doesn't matter. + announce_player_deaths = False + + # Joining activities are for waiting for initial player joins. + # They are treated slightly differently than regular activities, + # mainly in that all players are passed to the activity at once + # instead of as each joins. + is_joining_activity = False + + # Whether game-time should still progress when in menus/etc. + allow_pausing = False + + # Whether idle players can potentially be kicked (should not happen in + # menus/etc). + allow_kick_idle_players = True + + # In vr mode, this determines whether overlay nodes (text, images, etc) + # are created at a fixed position in space or one that moves based on + # the current map. Generally this should be on for games and off for + # transitions/score-screens/etc. that persist between maps. + use_fixed_vr_overlay = False + + # If True, runs in slow motion and turns down sound pitch. + slow_motion = False + + # Set this to True to inherit slow motion setting from previous + # activity (useful for transitions to avoid hitches). + inherits_slow_motion = False + + # Set this to True to keep playing the music from the previous activity + # (without even restarting it). + inherits_music = False + + # Set this to true to inherit VR camera offsets from the previous + # activity (useful for preventing sporadic camera movement + # during transitions). + inherits_vr_camera_offset = False + + # Set this to true to inherit (non-fixed) VR overlay positioning from + # the previous activity (useful for prevent sporadic overlay jostling + # during transitions). + inherits_vr_overlay_center = False + + # Set this to true to inherit screen tint/vignette colors from the + # previous activity (useful to prevent sudden color changes during + # transitions). + inherits_tint = False + + # If the activity fades or transitions in, it should set the length of + # time here so that previous activities will be kept alive for that + # long (avoiding 'holes' in the screen) + # This value is given in real-time seconds. + transition_time = 0.0 + + # Is it ok to show an ad after this activity ends before showing + # the next activity? + can_show_ad_on_death = False + + def __init__(self, settings: dict): + """Creates an Activity in the current ba.Session. + + The activity will not be actually run until ba.Session.setactivity() + is called. 'settings' should be a dict of key/value pairs specific + to the activity. + + Activities should preload as much of their media/etc as possible in + their constructor, but none of it should actually be used until they + are transitioned in. + """ + super().__init__() + + # Create our internal engine data. + self._activity_data = _ba.register_activity(self) + + assert isinstance(settings, dict) + assert _ba.getactivity() is self + + self._globalsnode: Optional[ba.Node] = None + + # Player/Team types should have been specified as type args; + # grab those. + self._playertype: Type[PlayerType] + self._teamtype: Type[TeamType] + self._setup_player_and_team_types() + + # FIXME: Relocate or remove the need for this stuff. + self.paused_text: Optional[ba.Actor] = None + + self._session = weakref.ref(_ba.getsession()) + + # Preloaded data for actors, maps, etc; indexed by type. + self.preloads: Dict[Type, Any] = {} + + # Hopefully can eventually kill this; activities should + # validate/store whatever settings they need at init time + # (in a more type-safe way). + self.settings_raw = settings + + self._has_transitioned_in = False + self._has_begun = False + self._has_ended = False + self._activity_death_check_timer: Optional[ba.Timer] = None + self._expired = False + self._delay_delete_players: List[PlayerType] = [] + self._delay_delete_teams: List[TeamType] = [] + self._players_that_left: List[ReferenceType[PlayerType]] = [] + self._teams_that_left: List[ReferenceType[TeamType]] = [] + self._transitioning_out = False + + # A handy place to put most actors; this list is pruned of dead + # actors regularly and these actors are insta-killed as the activity + # is dying. + self._actor_refs: List[ba.Actor] = [] + self._actor_weak_refs: List[ReferenceType[ba.Actor]] = [] + self._last_prune_dead_actors_time = _ba.time() + self._prune_dead_actors_timer: Optional[ba.Timer] = None + + self.teams = [] + self.players = [] + + self.lobby = None + self._stats: Optional[ba.Stats] = None + self._customdata: Optional[dict] = {} + + def __del__(self) -> None: + + # If the activity has been run then we should have already cleaned + # it up, but we still need to run expire calls for un-run activities. + if not self._expired: + with _ba.Context('empty'): + self._expire() + + # Inform our owner that we officially kicked the bucket. + if self._transitioning_out: + session = self._session() + if session is not None: + _ba.pushcall( + Call(session.transitioning_out_activity_was_freed, + self.can_show_ad_on_death)) + + @property + def globalsnode(self) -> ba.Node: + """The 'globals' ba.Node for the activity. This contains various + global controls and values. + """ + node = self._globalsnode + if not node: + raise NodeNotFoundError() + return node + + @property + def stats(self) -> ba.Stats: + """The stats instance accessible while the activity is running. + + If access is attempted before or after, raises a ba.NotFoundError. + """ + if self._stats is None: + from ba._error import NotFoundError + raise NotFoundError() + return self._stats + + def on_expire(self) -> None: + """Called when your activity is being expired. + + If your activity has created anything explicitly that may be retaining + a strong reference to the activity and preventing it from dying, you + should clear that out here. From this point on your activity's sole + purpose in life is to hit zero references and die so the next activity + can begin. + """ + + @property + def customdata(self) -> dict: + """Entities needing to store simple data with an activity can put it + here. This dict will be deleted when the activity expires, so contained + objects generally do not need to worry about handling expired + activities. + """ + assert not self._expired + assert isinstance(self._customdata, dict) + return self._customdata + + @property + def expired(self) -> bool: + """Whether the activity is expired. + + An activity is set as expired when shutting down. + At this point no new nodes, timers, etc should be made, + run, etc, and the activity should be considered a 'zombie'. + """ + return self._expired + + @property + def playertype(self) -> Type[PlayerType]: + """The type of ba.Player this Activity is using.""" + return self._playertype + + @property + def teamtype(self) -> Type[TeamType]: + """The type of ba.Team this Activity is using.""" + return self._teamtype + + def set_has_ended(self, val: bool) -> None: + """(internal)""" + self._has_ended = val + + def expire(self) -> None: + """Begin the process of tearing down the activity. + + (internal) + """ + from ba._enums import TimeType + + # Create a real-timer that watches a weak-ref of this activity + # and reports any lingering references keeping it alive. + # We store the timer on the activity so as soon as the activity dies + # it gets cleaned up. + with _ba.Context('ui'): + ref = weakref.ref(self) + self._activity_death_check_timer = _ba.Timer( + 5.0, + Call(self._check_activity_death, ref, [0]), + repeat=True, + timetype=TimeType.REAL) + + # Run _expire in an empty context; nothing should be happening in + # there except deleting things which requires no context. + # (plus, _expire() runs in the destructor for un-run activities + # and we can't properly provide context in that situation anyway; might + # as well be consistent). + if not self._expired: + with _ba.Context('empty'): + self._expire() + else: + raise RuntimeError(f'destroy() called when' + f' already expired for {self}') + + def retain_actor(self, actor: ba.Actor) -> None: + """Add a strong-reference to a ba.Actor to this Activity. + + The reference will be lazily released once ba.Actor.exists() + returns False for the Actor. The ba.Actor.autoretain() method + is a convenient way to access this same functionality. + """ + if __debug__: + from ba._actor import Actor + assert isinstance(actor, Actor) + self._actor_refs.append(actor) + + def add_actor_weak_ref(self, actor: ba.Actor) -> None: + """Add a weak-reference to a ba.Actor to the ba.Activity. + + (called by the ba.Actor base class) + """ + if __debug__: + from ba._actor import Actor + assert isinstance(actor, Actor) + self._actor_weak_refs.append(weakref.ref(actor)) + + @property + def session(self) -> ba.Session: + """The ba.Session this ba.Activity belongs go. + + Raises a ba.SessionNotFoundError if the Session no longer exists. + """ + session = self._session() + if session is None: + from ba._error import SessionNotFoundError + raise SessionNotFoundError() + return session + + def on_player_join(self, player: PlayerType) -> None: + """Called when a new ba.Player has joined the Activity. + + (including the initial set of Players) + """ + + def on_player_leave(self, player: PlayerType) -> None: + """Called when a ba.Player is leaving the Activity.""" + + def on_team_join(self, team: TeamType) -> None: + """Called when a new ba.Team joins the Activity. + + (including the initial set of Teams) + """ + + def on_team_leave(self, team: TeamType) -> None: + """Called when a ba.Team leaves the Activity.""" + + def on_transition_in(self) -> None: + """Called when the Activity is first becoming visible. + + Upon this call, the Activity should fade in backgrounds, + start playing music, etc. It does not yet have access to players + or teams, however. They remain owned by the previous Activity + up until ba.Activity.on_begin() is called. + """ + + def on_transition_out(self) -> None: + """Called when your activity begins transitioning out. + + Note that this may happen at any time even if end() has not been + called. + """ + + def on_begin(self) -> None: + """Called once the previous ba.Activity has finished transitioning out. + + At this point the activity's initial players and teams are filled in + and it should begin its actual game logic. + """ + + def handlemessage(self, msg: Any) -> Any: + """General message handling; can be passed any message object.""" + del msg # Unused arg. + return UNHANDLED + + def has_transitioned_in(self) -> bool: + """Return whether on_transition_in() has been called.""" + return self._has_transitioned_in + + def has_begun(self) -> bool: + """Return whether on_begin() has been called.""" + return self._has_begun + + def has_ended(self) -> bool: + """Return whether the activity has commenced ending.""" + return self._has_ended + + def is_transitioning_out(self) -> bool: + """Return whether on_transition_out() has been called.""" + return self._transitioning_out + + def transition_in(self, prev_globals: Optional[ba.Node]) -> None: + """Called by Session to kick off transition-in. + + (internal) + """ + assert not self._has_transitioned_in + self._has_transitioned_in = True + + # Set up the globals node based on our settings. + with _ba.Context(self): + glb = self._globalsnode = _ba.newnode('globals') + + # Now that it's going to be front and center, + # set some global values based on what the activity wants. + glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay + glb.allow_kick_idle_players = self.allow_kick_idle_players + if self.inherits_slow_motion and prev_globals is not None: + glb.slow_motion = prev_globals.slow_motion + else: + glb.slow_motion = self.slow_motion + if self.inherits_music and prev_globals is not None: + glb.music_continuous = True # Prevent restarting same music. + glb.music = prev_globals.music + glb.music_count += 1 + if self.inherits_vr_camera_offset and prev_globals is not None: + glb.vr_camera_offset = prev_globals.vr_camera_offset + if self.inherits_vr_overlay_center and prev_globals is not None: + glb.vr_overlay_center = prev_globals.vr_overlay_center + glb.vr_overlay_center_enabled = ( + prev_globals.vr_overlay_center_enabled) + + # If they want to inherit tint from the previous self. + if self.inherits_tint and prev_globals is not None: + glb.tint = prev_globals.tint + glb.vignette_outer = prev_globals.vignette_outer + glb.vignette_inner = prev_globals.vignette_inner + + # Start pruning our various things periodically. + self._prune_dead_actors() + self._prune_dead_actors_timer = _ba.Timer(5.17, + self._prune_dead_actors, + repeat=True) + + _ba.timer(13.3, self._prune_delay_deletes, repeat=True) + + # Also start our low-level scene running. + self._activity_data.start() + + try: + self.on_transition_in() + except Exception: + print_exception(f'Error in on_transition_in for {self}.') + + # Tell the C++ layer that this activity is the main one, so it uses + # settings from our globals, directs various events to us, etc. + self._activity_data.make_foreground() + + def transition_out(self) -> None: + """Called by the Session to start us transitioning out.""" + assert not self._transitioning_out + self._transitioning_out = True + with _ba.Context(self): + try: + self.on_transition_out() + except Exception: + print_exception(f'Error in on_transition_out for {self}.') + + def begin(self, session: ba.Session) -> None: + """Begin the activity. + + (internal) + """ + + assert not self._has_begun + + # Inherit stats from the session. + self._stats = session.stats + + # Add session's teams in. + for team in session.sessionteams: + self.add_team(team) + + # Add session's players in. + for player in session.sessionplayers: + self.add_player(player) + + self._has_begun = True + + # Let the activity do its thing. + with _ba.Context(self): + # Note: do we want to catch errors here? + # Currently I believe we wind up canceling the + # activity launch; just wanna be sure that is intentional. + self.on_begin() + + def end(self, + results: Any = None, + delay: float = 0.0, + force: bool = False) -> None: + """Commences Activity shutdown and delivers results to the ba.Session. + + 'delay' is the time delay before the Activity actually ends + (in seconds). Further calls to end() will be ignored up until + this time, unless 'force' is True, in which case the new results + will replace the old. + """ + + # Ask the session to end us. + self.session.end_activity(self, results, delay, force) + + def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType: + """Create the Player instance for this Activity. + + Subclasses can override this if the activity's player class + requires a custom constructor; otherwise it will be called with + no args. Note that the player object should not be used at this + point as it is not yet fully wired up; wait for on_player_join() + for that. + """ + del sessionplayer # Unused. + player = self._playertype() + return player + + def create_team(self, sessionteam: ba.SessionTeam) -> TeamType: + """Create the Team instance for this Activity. + + Subclasses can override this if the activity's team class + requires a custom constructor; otherwise it will be called with + no args. Note that the team object should not be used at this + point as it is not yet fully wired up; wait for on_team_join() + for that. + """ + del sessionteam # Unused. + team = self._teamtype() + return team + + def add_player(self, sessionplayer: ba.SessionPlayer) -> None: + """(internal)""" + assert sessionplayer.sessionteam is not None + sessionplayer.resetinput() + sessionteam = sessionplayer.sessionteam + assert sessionplayer in sessionteam.players + team = sessionteam.activityteam + assert team is not None + sessionplayer.setactivity(self) + with _ba.Context(self): + sessionplayer.activityplayer = player = self.create_player( + sessionplayer) + player.postinit(sessionplayer) + + assert player not in team.players + team.players.append(player) + assert player in team.players + + assert player not in self.players + self.players.append(player) + assert player in self.players + + try: + self.on_player_join(player) + except Exception: + print_exception(f'Error in on_player_join for {self}.') + + def remove_player(self, sessionplayer: ba.SessionPlayer) -> None: + """Remove a player from the Activity while it is running. + + (internal) + """ + assert not self.expired + + player: Any = sessionplayer.activityplayer + assert isinstance(player, self._playertype) + team: Any = sessionplayer.sessionteam.activityteam + assert isinstance(team, self._teamtype) + + assert player in team.players + team.players.remove(player) + assert player not in team.players + + assert player in self.players + self.players.remove(player) + assert player not in self.players + + # This should allow our ba.Player instance to die. + # Complain if that doesn't happen. + # verify_object_death(player) + + with _ba.Context(self): + try: + self.on_player_leave(player) + except Exception: + print_exception(f'Error in on_player_leave for {self}.') + try: + player.leave() + except Exception: + print_exception(f'Error on leave for {player} in {self}.') + + self._reset_session_player_for_no_activity(sessionplayer) + + # Add the player to a list to keep it around for a while. This is + # to discourage logic from firing on player object death, which + # may not happen until activity end if something is holding refs + # to it. + self._delay_delete_players.append(player) + self._players_that_left.append(weakref.ref(player)) + + def add_team(self, sessionteam: ba.SessionTeam) -> None: + """Add a team to the Activity + + (internal) + """ + assert not self.expired + + with _ba.Context(self): + sessionteam.activityteam = team = self.create_team(sessionteam) + team.postinit(sessionteam) + self.teams.append(team) + try: + self.on_team_join(team) + except Exception: + print_exception(f'Error in on_team_join for {self}.') + + def remove_team(self, sessionteam: ba.SessionTeam) -> None: + """Remove a team from a Running Activity + + (internal) + """ + assert not self.expired + assert sessionteam.activityteam is not None + + team: Any = sessionteam.activityteam + assert isinstance(team, self._teamtype) + + assert team in self.teams + self.teams.remove(team) + assert team not in self.teams + + with _ba.Context(self): + # Make a decent attempt to persevere if user code breaks. + try: + self.on_team_leave(team) + except Exception: + print_exception(f'Error in on_team_leave for {self}.') + try: + team.leave() + except Exception: + print_exception(f'Error on leave for {team} in {self}.') + + sessionteam.activityteam = None + + # Add the team to a list to keep it around for a while. This is + # to discourage logic from firing on team object death, which + # may not happen until activity end if something is holding refs + # to it. + self._delay_delete_teams.append(team) + self._teams_that_left.append(weakref.ref(team)) + + def _reset_session_player_for_no_activity( + self, sessionplayer: ba.SessionPlayer) -> None: + + # Let's be extra-defensive here: killing a node/input-call/etc + # could trigger user-code resulting in errors, but we would still + # like to complete the reset if possible. + try: + sessionplayer.setnode(None) + except Exception: + print_exception( + f'Error resetting SessionPlayer node on {sessionplayer}' + f' for {self}.') + try: + sessionplayer.resetinput() + except Exception: + print_exception( + f'Error resetting SessionPlayer input on {sessionplayer}' + f' for {self}.') + + # These should never fail I think... + sessionplayer.setactivity(None) + sessionplayer.activityplayer = None + + def _setup_player_and_team_types(self) -> None: + """Pull player and team types from our typing.Generic params.""" + + # TODO: There are proper calls for pulling these in Python 3.8; + # should update this code when we adopt that. + # NOTE: If we get Any as PlayerType or TeamType (generally due + # to no generic params being passed) we automatically use the + # base class types, but also warn the user since this will mean + # less type safety for that class. (its better to pass the base + # player/team types explicitly vs. having them be Any) + if not TYPE_CHECKING: + self._playertype = type(self).__orig_bases__[-1].__args__[0] + if not isinstance(self._playertype, type): + self._playertype = Player + print(f'ERROR: {type(self)} was not passed a Player' + f' type argument; please explicitly pass ba.Player' + f' if you do not want to override it.') + self._teamtype = type(self).__orig_bases__[-1].__args__[1] + if not isinstance(self._teamtype, type): + self._teamtype = Team + print(f'ERROR: {type(self)} was not passed a Team' + f' type argument; please explicitly pass ba.Team' + f' if you do not want to override it.') + assert issubclass(self._playertype, Player) + assert issubclass(self._teamtype, Team) + + @classmethod + def _check_activity_death(cls, activity_ref: ReferenceType[Activity], + counter: List[int]) -> None: + """Sanity check to make sure an Activity was destroyed properly. + + Receives a weakref to a ba.Activity which should have torn itself + down due to no longer being referenced anywhere. Will complain + and/or print debugging info if the Activity still exists. + """ + try: + import gc + import types + activity = activity_ref() + print('ERROR: Activity is not dying when expected:', activity, + '(warning ' + str(counter[0] + 1) + ')') + print('This means something is still strong-referencing it.') + counter[0] += 1 + + # FIXME: Running the code below shows us references but winds up + # keeping the object alive; need to figure out why. + # For now we just print refs if the count gets to 3, and then we + # kill the app at 4 so it doesn't matter anyway. + if counter[0] == 3: + print('Activity references for', activity, ':') + refs = list(gc.get_referrers(activity)) + i = 1 + for ref in refs: + if isinstance(ref, types.FrameType): + continue + print(' reference', i, ':', ref) + i += 1 + if counter[0] == 4: + print('Killing app due to stuck activity... :-(') + _ba.quit() + + except Exception: + print_exception('Error on _check_activity_death/') + + def _expire(self) -> None: + """Put the activity in a state where it can be garbage-collected. + + This involves clearing anything that might be holding a reference + to it, etc. + """ + assert not self._expired + self._expired = True + + try: + self.on_expire() + except Exception: + print_exception(f'Error in Activity on_expire() for {self}.') + + try: + self._customdata = None + except Exception: + print_exception(f'Error clearing customdata for {self}.') + + # Don't want to be holding any delay-delete refs at this point. + self._prune_delay_deletes() + + self._expire_actors() + self._expire_players() + self._expire_teams() + + # This will kill all low level stuff: Timers, Nodes, etc., which + # should clear up any remaining refs to our Activity and allow us + # to die peacefully. + try: + self._activity_data.expire() + except Exception: + print_exception(f'Error expiring _activity_data for {self}.') + + def _expire_actors(self) -> None: + # Expire all Actors. + for actor_ref in self._actor_weak_refs: + actor = actor_ref() + if actor is not None: + verify_object_death(actor) + try: + actor.on_expire() + except Exception: + print_exception(f'Error in Actor.on_expire()' + f' for {actor_ref()}.') + + def _expire_players(self) -> None: + + # Issue warnings for any players that left the game but don't + # get freed soon. + for ex_player in (p() for p in self._players_that_left): + if ex_player is not None: + verify_object_death(ex_player) + + for player in self.players: + # This should allow our ba.Player instance to be freed. + # Complain if that doesn't happen. + verify_object_death(player) + + try: + player.expire() + except Exception: + print_exception(f'Error expiring {player}') + + # Reset the SessionPlayer to a not-in-an-activity state. + try: + sessionplayer = player.sessionplayer + self._reset_session_player_for_no_activity(sessionplayer) + except SessionPlayerNotFoundError: + # Conceivably, someone could have held on to a Player object + # until now whos underlying SessionPlayer left long ago... + pass + except Exception: + print_exception(f'Error expiring {player}.') + + def _expire_teams(self) -> None: + + # Issue warnings for any teams that left the game but don't + # get freed soon. + for ex_team in (p() for p in self._teams_that_left): + if ex_team is not None: + verify_object_death(ex_team) + + for team in self.teams: + # This should allow our ba.Team instance to die. + # Complain if that doesn't happen. + verify_object_death(team) + + try: + team.expire() + except Exception: + print_exception(f'Error expiring {team}') + + try: + sessionteam = team.sessionteam + sessionteam.activityteam = None + except SessionTeamNotFoundError: + # It is expected that Team objects may last longer than + # the SessionTeam they came from (game objects may hold + # team references past the point at which the underlying + # player/team has left the game) + pass + except Exception: + print_exception(f'Error expiring Team {team}.') + + def _prune_delay_deletes(self) -> None: + self._delay_delete_players.clear() + self._delay_delete_teams.clear() + + # Clear out any dead weak-refs. + self._teams_that_left = [ + t for t in self._teams_that_left if t() is not None + ] + self._players_that_left = [ + p for p in self._players_that_left if p() is not None + ] + + def _prune_dead_actors(self) -> None: + self._last_prune_dead_actors_time = _ba.time() + + # Prune our strong refs when the Actor's exists() call gives False + self._actor_refs = [a for a in self._actor_refs if a.exists()] + + # Prune our weak refs once the Actor object has been freed. + self._actor_weak_refs = [ + a for a in self._actor_weak_refs if a() is not None + ] diff --git a/dist/ba_data/python/ba/_activitytypes.py b/dist/ba_data/python/ba/_activitytypes.py new file mode 100644 index 0000000..c51655d --- /dev/null +++ b/dist/ba_data/python/ba/_activitytypes.py @@ -0,0 +1,217 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Some handy base class and special purpose Activity types.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +from ba._activity import Activity +from ba._music import setmusic, MusicType +from ba._enums import InputType, UIScale +# False-positive from pylint due to our class-generics-filter. +from ba._player import EmptyPlayer # pylint: disable=W0611 +from ba._team import EmptyTeam # pylint: disable=W0611 + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + import ba + from ba._lobby import JoinInfo + + +class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]): + """Special ba.Activity to fade out and end the current ba.Session.""" + + def __init__(self, settings: dict): + super().__init__(settings) + + # Keeps prev activity alive while we fade out. + self.transition_time = 0.25 + self.inherits_tint = True + self.inherits_slow_motion = True + self.inherits_vr_camera_offset = True + self.inherits_vr_overlay_center = True + + def on_transition_in(self) -> None: + super().on_transition_in() + _ba.fade_screen(False) + _ba.lock_all_input() + + def on_begin(self) -> None: + # pylint: disable=cyclic-import + from bastd.mainmenu import MainMenuSession + from ba._general import Call + super().on_begin() + _ba.unlock_all_input() + _ba.app.ads.call_after_ad(Call(_ba.new_host_session, MainMenuSession)) + + +class JoinActivity(Activity[EmptyPlayer, EmptyTeam]): + """Standard activity for waiting for players to join. + + It shows tips and other info and waits for all players to check ready. + """ + + def __init__(self, settings: dict): + super().__init__(settings) + + # This activity is a special 'joiner' activity. + # It will get shut down as soon as all players have checked ready. + self.is_joining_activity = True + + # Players may be idle waiting for joiners; lets not kick them for it. + self.allow_kick_idle_players = False + + # In vr mode we don't want stuff moving around. + self.use_fixed_vr_overlay = True + + self._background: Optional[ba.Actor] = None + self._tips_text: Optional[ba.Actor] = None + self._join_info: Optional[JoinInfo] = None + + def on_transition_in(self) -> None: + # pylint: disable=cyclic-import + from bastd.actor.tipstext import TipsText + from bastd.actor.background import Background + super().on_transition_in() + self._background = Background(fade_time=0.5, + start_faded=True, + show_logo=True) + self._tips_text = TipsText() + setmusic(MusicType.CHAR_SELECT) + self._join_info = self.session.lobby.create_join_info() + _ba.set_analytics_screen('Joining Screen') + + +class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]): + """A simple overlay to fade out/in. + + Useful as a bare minimum transition between two level based activities. + """ + + # Keep prev activity alive while we fade in. + transition_time = 0.5 + inherits_slow_motion = True # Don't change. + inherits_tint = True # Don't change. + inherits_vr_camera_offset = True # Don't change. + inherits_vr_overlay_center = True + use_fixed_vr_overlay = True + + def __init__(self, settings: dict): + super().__init__(settings) + self._background: Optional[ba.Actor] = None + + def on_transition_in(self) -> None: + # pylint: disable=cyclic-import + from bastd.actor import background # FIXME: Don't use bastd from ba. + super().on_transition_in() + self._background = background.Background(fade_time=0.5, + start_faded=False, + show_logo=False) + + def on_begin(self) -> None: + super().on_begin() + + # Die almost immediately. + _ba.timer(0.1, self.end) + + +class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): + """A standard score screen that fades in and shows stuff for a while. + + After a specified delay, player input is assigned to end the activity. + """ + + transition_time = 0.5 + inherits_tint = True + inherits_vr_camera_offset = True + use_fixed_vr_overlay = True + + default_music: Optional[MusicType] = MusicType.SCORES + + def __init__(self, settings: dict): + super().__init__(settings) + self._birth_time = _ba.time() + self._min_view_time = 5.0 + self._allow_server_transition = False + self._background: Optional[ba.Actor] = None + self._tips_text: Optional[ba.Actor] = None + self._kicked_off_server_shutdown = False + self._kicked_off_server_restart = False + self._default_show_tips = True + self._custom_continue_message: Optional[ba.Lstr] = None + self._server_transitioning: Optional[bool] = None + + def on_player_join(self, player: EmptyPlayer) -> None: + from ba._general import WeakCall + super().on_player_join(player) + time_till_assign = max( + 0, self._birth_time + self._min_view_time - _ba.time()) + + # If we're still kicking at the end of our assign-delay, assign this + # guy's input to trigger us. + _ba.timer(time_till_assign, WeakCall(self._safe_assign, player)) + + def on_transition_in(self) -> None: + from bastd.actor.tipstext import TipsText + from bastd.actor.background import Background + super().on_transition_in() + self._background = Background(fade_time=0.5, + start_faded=False, + show_logo=True) + if self._default_show_tips: + self._tips_text = TipsText() + setmusic(self.default_music) + + def on_begin(self) -> None: + # pylint: disable=cyclic-import + from bastd.actor.text import Text + from ba import _language + super().on_begin() + + # Pop up a 'press any button to continue' statement after our + # min-view-time show a 'press any button to continue..' + # thing after a bit. + if _ba.app.ui.uiscale is UIScale.LARGE: + # FIXME: Need a better way to determine whether we've probably + # got a keyboard. + sval = _language.Lstr(resource='pressAnyKeyButtonText') + else: + sval = _language.Lstr(resource='pressAnyButtonText') + + Text(self._custom_continue_message + if self._custom_continue_message is not None else sval, + v_attach=Text.VAttach.BOTTOM, + h_align=Text.HAlign.CENTER, + flash=True, + vr_depth=50, + position=(0, 10), + scale=0.8, + color=(0.5, 0.7, 0.5, 0.5), + transition=Text.Transition.IN_BOTTOM_SLOW, + transition_delay=self._min_view_time).autoretain() + + def _player_press(self) -> None: + + # If this activity is a good 'end point', ask server-mode just once if + # it wants to do anything special like switch sessions or kill the app. + if (self._allow_server_transition and _ba.app.server is not None + and self._server_transitioning is None): + self._server_transitioning = _ba.app.server.handle_transition() + assert isinstance(self._server_transitioning, bool) + + # If server-mode is handling this, don't do anything ourself. + if self._server_transitioning is True: + return + + # Otherwise end the activity normally. + self.end() + + def _safe_assign(self, player: EmptyPlayer) -> None: + + # Just to be extra careful, don't assign if we're transitioning out. + # (though theoretically that should be ok). + if not self.is_transitioning_out() and player: + player.assigninput((InputType.JUMP_PRESS, InputType.PUNCH_PRESS, + InputType.BOMB_PRESS, InputType.PICK_UP_PRESS), + self._player_press) diff --git a/dist/ba_data/python/ba/_actor.py b/dist/ba_data/python/ba/_actor.py new file mode 100644 index 0000000..5c8f7aa --- /dev/null +++ b/dist/ba_data/python/ba/_actor.py @@ -0,0 +1,200 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines base Actor class.""" + +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING, TypeVar, overload + +from ba._messages import DieMessage, DeathType, OutOfBoundsMessage, UNHANDLED +from ba._error import print_exception, ActivityNotFoundError +import _ba + +if TYPE_CHECKING: + from typing import Any, Optional, Literal + import ba + +T = TypeVar('T', bound='Actor') + + +class Actor: + """High level logical entities in a ba.Activity. + + Category: Gameplay Classes + + Actors act as controllers, combining some number of ba.Nodes, + ba.Textures, ba.Sounds, etc. into a high-level cohesive unit. + + Some example actors include the Bomb, Flag, and Spaz classes that + live in the bastd.actor.* modules. + + One key feature of Actors is that they generally 'die' + (killing off or transitioning out their nodes) when the last Python + reference to them disappears, so you can use logic such as: + + # Create a flag Actor in our game activity: + from bastd.actor.flag import Flag + self.flag = Flag(position=(0, 10, 0)) + + # Later, destroy the flag. + # (provided nothing else is holding a reference to it) + # We could also just assign a new flag to this value. + # Either way, the old flag disappears. + self.flag = None + + This is in contrast to the behavior of the more low level ba.Nodes, + which are always explicitly created and destroyed and don't care + how many Python references to them exist. + + Note, however, that you can use the ba.Actor.autoretain() method + if you want an Actor to stick around until explicitly killed + regardless of references. + + Another key feature of ba.Actor is its handlemessage() method, which + takes a single arbitrary object as an argument. This provides a safe way + to communicate between ba.Actor, ba.Activity, ba.Session, and any other + class providing a handlemessage() method. The most universally handled + message type for Actors is the ba.DieMessage. + + # Another way to kill the flag from the example above: + # We can safely call this on any type with a 'handlemessage' method + # (though its not guaranteed to always have a meaningful effect). + # In this case the Actor instance will still be around, but its exists() + # and is_alive() methods will both return False. + self.flag.handlemessage(ba.DieMessage()) + """ + + def __init__(self) -> None: + """Instantiates an Actor in the current ba.Activity.""" + + if __debug__: + self._root_actor_init_called = True + activity = _ba.getactivity() + self._activity = weakref.ref(activity) + activity.add_actor_weak_ref(self) + + def __del__(self) -> None: + try: + # Unexpired Actors send themselves a DieMessage when going down. + # That way we can treat DieMessage handling as the single + # point-of-action for death. + if not self.expired: + self.handlemessage(DieMessage()) + except Exception: + print_exception('exception in ba.Actor.__del__() for', self) + + def handlemessage(self, msg: Any) -> Any: + """General message handling; can be passed any message object.""" + assert not self.expired + + # By default, actors going out-of-bounds simply kill themselves. + if isinstance(msg, OutOfBoundsMessage): + return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS)) + + return UNHANDLED + + def autoretain(self: T) -> T: + """Keep this Actor alive without needing to hold a reference to it. + + This keeps the ba.Actor in existence by storing a reference to it + with the ba.Activity it was created in. The reference is lazily + released once ba.Actor.exists() returns False for it or when the + Activity is set as expired. This can be a convenient alternative + to storing references explicitly just to keep a ba.Actor from dying. + For convenience, this method returns the ba.Actor it is called with, + enabling chained statements such as: myflag = ba.Flag().autoretain() + """ + activity = self._activity() + if activity is None: + raise ActivityNotFoundError() + activity.retain_actor(self) + return self + + def on_expire(self) -> None: + """Called for remaining ba.Actors when their ba.Activity shuts down. + + Actors can use this opportunity to clear callbacks or other + references which have the potential of keeping the ba.Activity + alive inadvertently (Activities can not exit cleanly while + any Python references to them remain.) + + Once an actor is expired (see ba.Actor.is_expired()) it should no + longer perform any game-affecting operations (creating, modifying, + or deleting nodes, media, timers, etc.) Attempts to do so will + likely result in errors. + """ + + @property + def expired(self) -> bool: + """Whether the Actor is expired. + + (see ba.Actor.on_expire()) + """ + activity = self.getactivity(doraise=False) + return True if activity is None else activity.expired + + def exists(self) -> bool: + """Returns whether the Actor is still present in a meaningful way. + + Note that a dying character should still return True here as long as + their corpse is visible; this is about presence, not being 'alive' + (see ba.Actor.is_alive() for that). + + If this returns False, it is assumed the Actor can be completely + deleted without affecting the game; this call is often used + when pruning lists of Actors, such as with ba.Actor.autoretain() + + The default implementation of this method always return True. + + Note that the boolean operator for the Actor class calls this method, + so a simple "if myactor" test will conveniently do the right thing + even if myactor is set to None. + """ + return True + + def __bool__(self) -> bool: + # Cleaner way to test existence; friendlier to None values. + return self.exists() + + def is_alive(self) -> bool: + """Returns whether the Actor is 'alive'. + + What this means is up to the Actor. + It is not a requirement for Actors to be + able to die; just that they report whether + they are Alive or not. + """ + return True + + @property + def activity(self) -> ba.Activity: + """The Activity this Actor was created in. + + Raises a ba.ActivityNotFoundError if the Activity no longer exists. + """ + activity = self._activity() + if activity is None: + raise ActivityNotFoundError() + return activity + + # Overloads to convey our exact return type depending on 'doraise' value. + + @overload + def getactivity(self, doraise: Literal[True] = True) -> ba.Activity: + ... + + @overload + def getactivity(self, doraise: Literal[False]) -> Optional[ba.Activity]: + ... + + def getactivity(self, doraise: bool = True) -> Optional[ba.Activity]: + """Return the ba.Activity this Actor is associated with. + + If the Activity no longer exists, raises a ba.ActivityNotFoundError + or returns None depending on whether 'doraise' is True. + """ + activity = self._activity() + if activity is None and doraise: + raise ActivityNotFoundError() + return activity diff --git a/dist/ba_data/python/ba/_ads.py b/dist/ba_data/python/ba/_ads.py new file mode 100644 index 0000000..7493979 --- /dev/null +++ b/dist/ba_data/python/ba/_ads.py @@ -0,0 +1,186 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to ads.""" +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Optional, Callable, Any + + +class AdsSubsystem: + """Subsystem for ads functionality in the app. + + Category: App Classes + + Access the single shared instance of this class at 'ba.app.ads'. + """ + + def __init__(self) -> None: + self.last_ad_network = 'unknown' + self.last_ad_network_set_time = time.time() + self.ad_amt: Optional[float] = None + self.last_ad_purpose = 'invalid' + self.attempted_first_ad = False + self.last_in_game_ad_remove_message_show_time: Optional[float] = None + self.last_ad_completion_time: Optional[float] = None + self.last_ad_was_short = False + + def do_remove_in_game_ads_message(self) -> None: + """(internal)""" + from ba._language import Lstr + from ba._enums import TimeType + + # Print this message once every 10 minutes at most. + tval = _ba.time(TimeType.REAL) + if (self.last_in_game_ad_remove_message_show_time is None or + (tval - self.last_in_game_ad_remove_message_show_time > 60 * 10)): + self.last_in_game_ad_remove_message_show_time = tval + with _ba.Context('ui'): + _ba.timer( + 1.0, + lambda: _ba.screenmessage(Lstr( + resource='removeInGameAdsText', + subs=[('${PRO}', + Lstr(resource='store.bombSquadProNameText')), + ('${APP_NAME}', Lstr(resource='titleText'))]), + color=(1, 1, 0)), + timetype=TimeType.REAL) + + def show_ad(self, + purpose: str, + on_completion_call: Callable[[], Any] = None) -> None: + """(internal)""" + self.last_ad_purpose = purpose + _ba.show_ad(purpose, on_completion_call) + + def show_ad_2(self, + purpose: str, + on_completion_call: Callable[[bool], Any] = None) -> None: + """(internal)""" + self.last_ad_purpose = purpose + _ba.show_ad_2(purpose, on_completion_call) + + def call_after_ad(self, call: Callable[[], Any]) -> None: + """Run a call after potentially showing an ad.""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + from ba._enums import TimeType + app = _ba.app + show = True + + # No ads without net-connections, etc. + if not _ba.can_show_ad(): + show = False + if app.accounts.have_pro(): + show = False # Pro disables interstitials. + try: + session = _ba.get_foreground_host_session() + assert session is not None + is_tournament = session.tournament_id is not None + except Exception: + is_tournament = False + if is_tournament: + show = False # Never show ads during tournaments. + + if show: + interval: Optional[float] + launch_count = app.config.get('launchCount', 0) + + # If we're seeing short ads we may want to space them differently. + interval_mult = (_ba.get_account_misc_read_val( + 'ads.shortIntervalMult', 1.0) + if self.last_ad_was_short else 1.0) + if self.ad_amt is None: + if launch_count <= 1: + self.ad_amt = _ba.get_account_misc_read_val( + 'ads.startVal1', 0.99) + else: + self.ad_amt = _ba.get_account_misc_read_val( + 'ads.startVal2', 1.0) + interval = None + else: + # So far we're cleared to show; now calc our + # ad-show-threshold and see if we should *actually* show + # (we reach our threshold faster the longer we've been + # playing). + base = 'ads' if _ba.has_video_ads() else 'ads2' + min_lc = _ba.get_account_misc_read_val(base + '.minLC', 0.0) + max_lc = _ba.get_account_misc_read_val(base + '.maxLC', 5.0) + min_lc_scale = (_ba.get_account_misc_read_val( + base + '.minLCScale', 0.25)) + max_lc_scale = (_ba.get_account_misc_read_val( + base + '.maxLCScale', 0.34)) + min_lc_interval = (_ba.get_account_misc_read_val( + base + '.minLCInterval', 360)) + max_lc_interval = (_ba.get_account_misc_read_val( + base + '.maxLCInterval', 300)) + if launch_count < min_lc: + lc_amt = 0.0 + elif launch_count > max_lc: + lc_amt = 1.0 + else: + lc_amt = ((float(launch_count) - min_lc) / + (max_lc - min_lc)) + incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale + interval = ((1.0 - lc_amt) * min_lc_interval + + lc_amt * max_lc_interval) + self.ad_amt += incr + assert self.ad_amt is not None + if self.ad_amt >= 1.0: + self.ad_amt = self.ad_amt % 1.0 + self.attempted_first_ad = True + + # After we've reached the traditional show-threshold once, + # try again whenever its been INTERVAL since our last successful + # show. + elif ( + self.attempted_first_ad and + (self.last_ad_completion_time is None or + (interval is not None + and _ba.time(TimeType.REAL) - self.last_ad_completion_time > + (interval * interval_mult)))): + # Reset our other counter too in this case. + self.ad_amt = 0.0 + else: + show = False + + # If we're *still* cleared to show, actually tell the system to show. + if show: + # As a safety-check, set up an object that will run + # the completion callback if we've returned and sat for 10 seconds + # (in case some random ad network doesn't properly deliver its + # completion callback). + class _Payload: + + def __init__(self, pcall: Callable[[], Any]): + self._call = pcall + self._ran = False + + def run(self, fallback: bool = False) -> None: + """Run fallback call (and issue a warning about it).""" + if not self._ran: + if fallback: + print( + ('ERROR: relying on fallback ad-callback! ' + 'last network: ' + app.ads.last_ad_network + + ' (set ' + str( + int(time.time() - + app.ads.last_ad_network_set_time)) + + 's ago); purpose=' + app.ads.last_ad_purpose)) + _ba.pushcall(self._call) + self._ran = True + + payload = _Payload(call) + with _ba.Context('ui'): + _ba.timer(5.0, + lambda: payload.run(fallback=True), + timetype=TimeType.REAL) + self.show_ad('between_game', on_completion_call=payload.run) + else: + _ba.pushcall(call) # Just run the callback without the ad. diff --git a/dist/ba_data/python/ba/_analytics.py b/dist/ba_data/python/ba/_analytics.py new file mode 100644 index 0000000..27c8996 --- /dev/null +++ b/dist/ba_data/python/ba/_analytics.py @@ -0,0 +1,73 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to analytics.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + pass + + +def game_begin_analytics() -> None: + """Update analytics events for the start of a game.""" + # pylint: disable=too-many-branches + # pylint: disable=cyclic-import + from ba._dualteamsession import DualTeamSession + from ba._freeforallsession import FreeForAllSession + from ba._coopsession import CoopSession + from ba._gameactivity import GameActivity + activity = _ba.getactivity(False) + session = _ba.getsession(False) + + # Fail gracefully if we didn't cleanly get a session and game activity. + if not activity or not session or not isinstance(activity, GameActivity): + return + + if isinstance(session, CoopSession): + campaign = session.campaign + assert campaign is not None + _ba.set_analytics_screen( + 'Coop Game: ' + campaign.name + ' ' + + campaign.getlevel(_ba.app.coop_session_args['level']).name) + _ba.increment_analytics_count('Co-op round start') + if len(activity.players) == 1: + _ba.increment_analytics_count('Co-op round start 1 human player') + elif len(activity.players) == 2: + _ba.increment_analytics_count('Co-op round start 2 human players') + elif len(activity.players) == 3: + _ba.increment_analytics_count('Co-op round start 3 human players') + elif len(activity.players) >= 4: + _ba.increment_analytics_count('Co-op round start 4+ human players') + + elif isinstance(session, DualTeamSession): + _ba.set_analytics_screen('Teams Game: ' + activity.getname()) + _ba.increment_analytics_count('Teams round start') + if len(activity.players) == 1: + _ba.increment_analytics_count('Teams round start 1 human player') + elif 1 < len(activity.players) < 8: + _ba.increment_analytics_count('Teams round start ' + + str(len(activity.players)) + + ' human players') + elif len(activity.players) >= 8: + _ba.increment_analytics_count('Teams round start 8+ human players') + + elif isinstance(session, FreeForAllSession): + _ba.set_analytics_screen('FreeForAll Game: ' + activity.getname()) + _ba.increment_analytics_count('Free-for-all round start') + if len(activity.players) == 1: + _ba.increment_analytics_count( + 'Free-for-all round start 1 human player') + elif 1 < len(activity.players) < 8: + _ba.increment_analytics_count('Free-for-all round start ' + + str(len(activity.players)) + + ' human players') + elif len(activity.players) >= 8: + _ba.increment_analytics_count( + 'Free-for-all round start 8+ human players') + + # For some analytics tracking on the c layer. + _ba.reset_game_activity_tracking() diff --git a/dist/ba_data/python/ba/_app.py b/dist/ba_data/python/ba/_app.py new file mode 100644 index 0000000..47b19fd --- /dev/null +++ b/dist/ba_data/python/ba/_app.py @@ -0,0 +1,569 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the high level state of the app.""" +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import _ba +from ba._music import MusicSubsystem +from ba._language import LanguageSubsystem +from ba._ui import UISubsystem +from ba._achievement import AchievementSubsystem +from ba._plugin import PluginSubsystem +from ba._account import AccountSubsystem +from ba._meta import MetadataSubsystem +from ba._ads import AdsSubsystem + +if TYPE_CHECKING: + import ba + from bastd.actor import spazappearance + from typing import Optional, Dict, Set, Any, Type, Tuple, Callable, List + + +class App: + """A class for high level app functionality and state. + + Category: App Classes + + Use ba.app to access the single shared instance of this class. + + Note that properties not documented here should be considered internal + and subject to change without warning. + """ + # pylint: disable=too-many-public-methods + + @property + def build_number(self) -> int: + """Integer build number. + + This value increases by at least 1 with each release of the game. + It is independent of the human readable ba.App.version string. + """ + assert isinstance(self._env['build_number'], int) + return self._env['build_number'] + + @property + def config_file_path(self) -> str: + """Where the game's config file is stored on disk.""" + assert isinstance(self._env['config_file_path'], str) + return self._env['config_file_path'] + + @property + def user_agent_string(self) -> str: + """String containing various bits of info about OS/device/etc.""" + assert isinstance(self._env['user_agent_string'], str) + return self._env['user_agent_string'] + + @property + def version(self) -> str: + """Human-readable version string; something like '1.3.24'. + + This should not be interpreted as a number; it may contain + string elements such as 'alpha', 'beta', 'test', etc. + If a numeric version is needed, use 'ba.App.build_number'. + """ + assert isinstance(self._env['version'], str) + return self._env['version'] + + @property + def debug_build(self) -> bool: + """Whether the game was compiled in debug mode. + + Debug builds generally run substantially slower than non-debug + builds due to compiler optimizations being disabled and extra + checks being run. + """ + assert isinstance(self._env['debug_build'], bool) + return self._env['debug_build'] + + @property + def test_build(self) -> bool: + """Whether the game was compiled in test mode. + + Test mode enables extra checks and features that are useful for + release testing but which do not slow the game down significantly. + """ + assert isinstance(self._env['test_build'], bool) + return self._env['test_build'] + + @property + def python_directory_user(self) -> str: + """Path where the app looks for custom user scripts.""" + assert isinstance(self._env['python_directory_user'], str) + return self._env['python_directory_user'] + + @property + def python_directory_app(self) -> str: + """Path where the app looks for its bundled scripts.""" + assert isinstance(self._env['python_directory_app'], str) + return self._env['python_directory_app'] + + @property + def python_directory_app_site(self) -> str: + """Path containing pip packages bundled with the app.""" + assert isinstance(self._env['python_directory_app_site'], str) + return self._env['python_directory_app_site'] + + @property + def config(self) -> ba.AppConfig: + """The ba.AppConfig instance representing the app's config state.""" + assert self._config is not None + return self._config + + @property + def platform(self) -> str: + """Name of the current platform. + + Examples are: 'mac', 'windows', android'. + """ + assert isinstance(self._env['platform'], str) + return self._env['platform'] + + @property + def subplatform(self) -> str: + """String for subplatform. + + Can be empty. For the 'android' platform, subplatform may + be 'google', 'amazon', etc. + """ + assert isinstance(self._env['subplatform'], str) + return self._env['subplatform'] + + @property + def api_version(self) -> int: + """The game's api version. + + Only Python modules and packages associated with the current API + version number will be detected by the game (see the ba_meta tag). + This value will change whenever backward-incompatible changes are + introduced to game APIs. When that happens, scripts should be updated + accordingly and set to target the new API version number. + """ + from ba._meta import CURRENT_API_VERSION + return CURRENT_API_VERSION + + @property + def on_tv(self) -> bool: + """Whether the game is currently running on a TV.""" + assert isinstance(self._env['on_tv'], bool) + return self._env['on_tv'] + + @property + def vr_mode(self) -> bool: + """Whether the game is currently running in VR.""" + assert isinstance(self._env['vr_mode'], bool) + return self._env['vr_mode'] + + @property + def ui_bounds(self) -> Tuple[float, float, float, float]: + """Bounds of the 'safe' screen area in ui space. + + This tuple contains: (x-min, x-max, y-min, y-max) + """ + return _ba.uibounds() + + def __init__(self) -> None: + """(internal) + + Do not instantiate this class; use ba.app to access + the single shared instance. + """ + # pylint: disable=too-many-statements + + # Config. + self.config_file_healthy = False + + # This is incremented any time the app is backgrounded/foregrounded; + # can be a simple way to determine if network data should be + # refreshed/etc. + self.fg_state = 0 + + self._env = _ba.env() + self.protocol_version: int = self._env['protocol_version'] + assert isinstance(self.protocol_version, int) + self.toolbar_test: bool = self._env['toolbar_test'] + assert isinstance(self.toolbar_test, bool) + self.demo_mode: bool = self._env['demo_mode'] + assert isinstance(self.demo_mode, bool) + self.arcade_mode: bool = self._env['arcade_mode'] + assert isinstance(self.arcade_mode, bool) + self.headless_mode: bool = self._env['headless_mode'] + assert isinstance(self.headless_mode, bool) + self.iircade_mode: bool = self._env['iircade_mode'] + assert isinstance(self.iircade_mode, bool) + self.allow_ticket_purchases: bool = not self.iircade_mode + + # Misc. + self.tips: List[str] = [] + self.stress_test_reset_timer: Optional[ba.Timer] = None + self.did_weak_call_warning = False + self.ran_on_app_launch = False + + self.log_have_new = False + self.log_upload_timer_started = False + self._config: Optional[ba.AppConfig] = None + self.printed_live_object_warning = False + + # We include this extra hash with shared input-mapping names so + # that we don't share mappings between differently-configured + # systems. For instance, different android devices may give different + # key values for the same controller type so we keep their mappings + # distinct. + self.input_map_hash: Optional[str] = None + + # Co-op Campaigns. + self.campaigns: Dict[str, ba.Campaign] = {} + + # Server Mode. + self.server: Optional[ba.ServerController] = None + + self.meta = MetadataSubsystem() + self.accounts = AccountSubsystem() + self.plugins = PluginSubsystem() + self.music = MusicSubsystem() + self.lang = LanguageSubsystem() + self.ach = AchievementSubsystem() + self.ui = UISubsystem() + self.ads = AdsSubsystem() + + # Lobby. + self.lobby_random_profile_index: int = 1 + self.lobby_random_char_index_offset = random.randrange(1000) + self.lobby_account_profile_device_id: Optional[int] = None + + # Main Menu. + self.main_menu_did_initial_transition = False + self.main_menu_last_news_fetch_time: Optional[float] = None + + # Spaz. + self.spaz_appearances: Dict[str, spazappearance.Appearance] = {} + self.last_spaz_turbo_warn_time: float = -99999.0 + + # Maps. + self.maps: Dict[str, Type[ba.Map]] = {} + + # Gameplay. + self.teams_series_length = 7 + self.ffa_series_length = 24 + self.coop_session_args: Dict = {} + + self.value_test_defaults: dict = {} + self.first_main_menu = True # FIXME: Move to mainmenu class. + self.did_menu_intro = False # FIXME: Move to mainmenu class. + self.main_menu_window_refresh_check_count = 0 # FIXME: Mv to mainmenu. + self.main_menu_resume_callbacks: list = [] # Can probably go away. + self.special_offer: Optional[Dict] = None + self.ping_thread_count = 0 + self.invite_confirm_windows: List[Any] = [] # FIXME: Don't use Any. + self.store_layout: Optional[Dict[str, List[Dict[str, Any]]]] = None + self.store_items: Optional[Dict[str, Dict]] = None + self.pro_sale_start_time: Optional[int] = None + self.pro_sale_start_val: Optional[int] = None + + self.delegate: Optional[ba.AppDelegate] = None + + def on_app_launch(self) -> None: + """Runs after the app finishes bootstrapping. + + (internal)""" + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from ba import _apputils + from ba import _appconfig + from ba import _achievement + from ba import _map + from ba import _campaign + from bastd import appdelegate + from bastd import maps as stdmaps + from bastd.actor import spazappearance + from ba._enums import TimeType + + cfg = self.config + + self.delegate = appdelegate.AppDelegate() + + self.ui.on_app_launch() + + spazappearance.register_appearances() + _campaign.init_campaigns() + + # FIXME: This should not be hard-coded. + for maptype in [ + stdmaps.HockeyStadium, stdmaps.FootballStadium, + stdmaps.Bridgit, stdmaps.BigG, stdmaps.Roundabout, + stdmaps.MonkeyFace, stdmaps.ZigZag, stdmaps.ThePad, + stdmaps.DoomShroom, stdmaps.LakeFrigid, stdmaps.TipTop, + stdmaps.CragCastle, stdmaps.TowerD, stdmaps.HappyThoughts, + stdmaps.StepRightUp, stdmaps.Courtyard, stdmaps.Rampage + ]: + _map.register_map(maptype) + + # Non-test, non-debug builds should generally be blessed; warn if not. + # (so I don't accidentally release a build that can't play tourneys) + if (not self.debug_build and not self.test_build + and not _ba.is_blessed()): + _ba.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) + + # If there's a leftover log file, attempt to upload it to the + # master-server and/or get rid of it. + _apputils.handle_leftover_log_file() + + # Only do this stuff if our config file is healthy so we don't + # overwrite a broken one or whatnot and wipe out data. + if not self.config_file_healthy: + if self.platform in ('mac', 'linux', 'windows'): + from bastd.ui import configerror + configerror.ConfigErrorWindow() + return + + # For now on other systems we just overwrite the bum config. + # At this point settings are already set; lets just commit them + # to disk. + _appconfig.commit_app_config(force=True) + + self.music.on_app_launch() + + launch_count = cfg.get('launchCount', 0) + launch_count += 1 + + # So we know how many times we've run the game at various + # version milestones. + for key in ('lc14173', 'lc14292'): + cfg.setdefault(key, launch_count) + + # Debugging - make note if we're using the local test server so we + # don't accidentally leave it on in a release. + # FIXME - should move this to the native layer. + server_addr = _ba.get_master_server_address() + if 'localhost' in server_addr: + _ba.timer(2.0, + lambda: _ba.screenmessage( + 'Note: using local server', + (1, 1, 0), + log=True, + ), + timetype=TimeType.REAL) + elif 'test' in server_addr: + _ba.timer(2.0, + lambda: _ba.screenmessage( + 'Note: using test server-module', + (1, 1, 0), + log=True, + ), + timetype=TimeType.REAL) + + cfg['launchCount'] = launch_count + cfg.commit() + + # Run a test in a few seconds to see if we should pop up an existing + # pending special offer. + def check_special_offer() -> None: + from bastd.ui.specialoffer import show_offer + config = self.config + if ('pendingSpecialOffer' in config and _ba.get_public_login_id() + == config['pendingSpecialOffer']['a']): + self.special_offer = config['pendingSpecialOffer']['o'] + show_offer() + + if not self.headless_mode: + _ba.timer(3.0, check_special_offer, timetype=TimeType.REAL) + + self.meta.on_app_launch() + self.accounts.on_app_launch() + self.plugins.on_app_launch() + + self.ran_on_app_launch = True + + # from ba._dependency import test_depset + # test_depset() + + def read_config(self) -> None: + """(internal)""" + from ba import _appconfig + self._config, self.config_file_healthy = _appconfig.read_config() + + def pause(self) -> None: + """Pause the game due to a user request or menu popping up. + + If there's a foreground host-activity that says it's pausable, tell it + to pause ..we now no longer pause if there are connected clients. + """ + activity: Optional[ba.Activity] = _ba.get_foreground_host_activity() + if (activity is not None and activity.allow_pausing + and not _ba.have_connected_clients()): + from ba import _gameutils + from ba._language import Lstr + from ba._nodeactor import NodeActor + + # FIXME: Shouldn't be touching scene stuff here; + # should just pass the request on to the host-session. + with _ba.Context(activity): + globs = activity.globalsnode + if not globs.paused: + _ba.playsound(_ba.getsound('refWhistle')) + globs.paused = True + + # FIXME: This should not be an attr on Actor. + activity.paused_text = NodeActor( + _ba.newnode('text', + attrs={ + 'text': Lstr(resource='pausedByHostText'), + 'client_only': True, + 'flatness': 1.0, + 'h_align': 'center' + })) + + def resume(self) -> None: + """Resume the game due to a user request or menu closing. + + If there's a foreground host-activity that's currently paused, tell it + to resume. + """ + + # FIXME: Shouldn't be touching scene stuff here; + # should just pass the request on to the host-session. + activity = _ba.get_foreground_host_activity() + if activity is not None: + with _ba.Context(activity): + globs = activity.globalsnode + if globs.paused: + _ba.playsound(_ba.getsound('refWhistle')) + globs.paused = False + + # FIXME: This should not be an actor attr. + activity.paused_text = None + + def return_to_main_menu_session_gracefully(self, + reset_ui: bool = True) -> None: + """Attempt to cleanly get back to the main menu.""" + # pylint: disable=cyclic-import + from ba import _benchmark + from ba._general import Call + from bastd.mainmenu import MainMenuSession + if reset_ui: + _ba.app.ui.clear_main_menu_window() + + if isinstance(_ba.get_foreground_host_session(), MainMenuSession): + # It may be possible we're on the main menu but the screen is faded + # so fade back in. + _ba.fade_screen(True) + return + + _benchmark.stop_stress_test() # Stop stress-test if in progress. + + # If we're in a host-session, tell them to end. + # This lets them tear themselves down gracefully. + host_session: Optional[ba.Session] = _ba.get_foreground_host_session() + if host_session is not None: + + # Kick off a little transaction so we'll hopefully have all the + # latest account state when we get back to the menu. + _ba.add_transaction({ + 'type': 'END_SESSION', + 'sType': str(type(host_session)) + }) + _ba.run_transactions() + + host_session.end() + + # Otherwise just force the issue. + else: + _ba.pushcall(Call(_ba.new_host_session, MainMenuSession)) + + def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: + """(internal)""" + + # If there's no main menu up, just call immediately. + if not self.ui.has_main_menu_window(): + with _ba.Context('ui'): + call() + else: + self.main_menu_resume_callbacks.append(call) + + def on_app_pause(self) -> None: + """Called when the app goes to a suspended state.""" + + def on_app_resume(self) -> None: + """Run when the app resumes from a suspended state.""" + + self.fg_state += 1 + self.accounts.on_app_resume() + self.music.on_app_resume() + + def launch_coop_game(self, + game: str, + force: bool = False, + args: Dict = None) -> bool: + """High level way to launch a local co-op session.""" + # pylint: disable=cyclic-import + from ba._campaign import getcampaign + from bastd.ui.coop.level import CoopLevelLockedWindow + if args is None: + args = {} + if game == '': + raise ValueError('empty game name') + campaignname, levelname = game.split(':') + campaign = getcampaign(campaignname) + + # If this campaign is sequential, make sure we've completed the + # one before this. + if campaign.sequential and not force: + for level in campaign.levels: + if level.name == levelname: + break + if not level.complete: + CoopLevelLockedWindow( + campaign.getlevel(levelname).displayname, + campaign.getlevel(level.name).displayname) + return False + + # Ok, we're good to go. + self.coop_session_args = { + 'campaign': campaignname, + 'level': levelname, + } + for arg_name, arg_val in list(args.items()): + self.coop_session_args[arg_name] = arg_val + + def _fade_end() -> None: + from ba import _coopsession + try: + _ba.new_host_session(_coopsession.CoopSession) + except Exception: + from ba import _error + _error.print_exception() + from bastd.mainmenu import MainMenuSession + _ba.new_host_session(MainMenuSession) + + _ba.fade_screen(False, endcall=_fade_end) + return True + + def on_app_shutdown(self) -> None: + """(internal)""" + self.music.on_app_shutdown() + + def handle_deep_link(self, url: str) -> None: + """Handle a deep link URL.""" + from ba._language import Lstr + appname = _ba.appname() + if url.startswith(f'{appname}://code/'): + code = url.replace(f'{appname}://code/', '') + self.accounts.add_pending_promo_code(code) + else: + _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) + _ba.playsound(_ba.getsound('error')) + + def _test_https(self) -> None: + """Testing https support. + + (would be nice to get this working on our custom Python builds; need + to wrangle certificates somehow). + """ + import urllib.request + try: + val = urllib.request.urlopen('https://example.com').read() + print('HTTPS TEST SUCCESS', len(val)) + except Exception as exc: + print('HTTPS TEST FAIL:', exc) diff --git a/dist/ba_data/python/ba/_appconfig.py b/dist/ba_data/python/ba/_appconfig.py new file mode 100644 index 0000000..ceb7c75 --- /dev/null +++ b/dist/ba_data/python/ba/_appconfig.py @@ -0,0 +1,166 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides the AppConfig class.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Any, List, Tuple + + +class AppConfig(dict): + """A special dict that holds the game's persistent configuration values. + + Category: App Classes + + It also provides methods for fetching values with app-defined fallback + defaults, applying contained values to the game, and committing the + config to storage. + + Call ba.appconfig() to get the single shared instance of this class. + + AppConfig data is stored as json on disk on so make sure to only place + json-friendly values in it (dict, list, str, float, int, bool). + Be aware that tuples will be quietly converted to lists when stored. + """ + + def resolve(self, key: str) -> Any: + """Given a string key, return a config value (type varies). + + This will substitute application defaults for values not present in + the config dict, filter some invalid values, etc. Note that these + values do not represent the state of the app; simply the state of its + config. Use ba.App to access actual live state. + + Raises an Exception for unrecognized key names. To get the list of keys + supported by this method, use ba.AppConfig.builtin_keys(). Note that it + is perfectly legal to store other data in the config; it just needs to + be accessed through standard dict methods and missing values handled + manually. + """ + return _ba.resolve_appconfig_value(key) + + def default_value(self, key: str) -> Any: + """Given a string key, return its predefined default value. + + This is the value that will be returned by ba.AppConfig.resolve() if + the key is not present in the config dict or of an incompatible type. + + Raises an Exception for unrecognized key names. To get the list of keys + supported by this method, use ba.AppConfig.builtin_keys(). Note that it + is perfectly legal to store other data in the config; it just needs to + be accessed through standard dict methods and missing values handled + manually. + """ + return _ba.get_appconfig_default_value(key) + + def builtin_keys(self) -> List[str]: + """Return the list of valid key names recognized by ba.AppConfig. + + This set of keys can be used with resolve(), default_value(), etc. + It does not vary across platforms and may include keys that are + obsolete or not relevant on the current running version. (for instance, + VR related keys on non-VR platforms). This is to minimize the amount + of platform checking necessary) + + Note that it is perfectly legal to store arbitrary named data in the + config, but in that case it is up to the user to test for the existence + of the key in the config dict, fall back to consistent defaults, etc. + """ + return _ba.get_appconfig_builtin_keys() + + def apply(self) -> None: + """Apply config values to the running app.""" + _ba.apply_config() + + def commit(self) -> None: + """Commits the config to local storage. + + Note that this call is asynchronous so the actual write to disk may not + occur immediately. + """ + commit_app_config() + + def apply_and_commit(self) -> None: + """Run apply() followed by commit(); for convenience. + + (This way the commit() will not occur if apply() hits invalid data) + """ + self.apply() + self.commit() + + +def read_config() -> Tuple[AppConfig, bool]: + """Read the game config.""" + import os + import json + from ba._enums import TimeType + + config_file_healthy = False + + # NOTE: it is assumed that this only gets called once and the + # config object will not change from here on out + config_file_path = _ba.app.config_file_path + config_contents = '' + try: + if os.path.exists(config_file_path): + with open(config_file_path) as infile: + config_contents = infile.read() + config = AppConfig(json.loads(config_contents)) + else: + config = AppConfig() + config_file_healthy = True + + except Exception as exc: + print(('error reading config file at time ' + + str(_ba.time(TimeType.REAL)) + ': \'' + config_file_path + + '\':\n'), exc) + + # Whenever this happens lets back up the broken one just in case it + # gets overwritten accidentally. + print(('backing up current config file to \'' + config_file_path + + ".broken\'")) + try: + import shutil + shutil.copyfile(config_file_path, config_file_path + '.broken') + except Exception as exc: + print('EXC copying broken config:', exc) + try: + _ba.log('broken config contents:\n' + + config_contents.replace('\000', ''), + to_stdout=False) + except Exception as exc: + print('EXC logging broken config contents:', exc) + config = AppConfig() + + # Now attempt to read one of our 'prev' backup copies. + prev_path = config_file_path + '.prev' + try: + if os.path.exists(prev_path): + with open(prev_path) as infile: + config_contents = infile.read() + config = AppConfig(json.loads(config_contents)) + else: + config = AppConfig() + config_file_healthy = True + print('successfully read backup config.') + except Exception as exc: + print('EXC reading prev backup config:', exc) + return config, config_file_healthy + + +def commit_app_config(force: bool = False) -> None: + """Commit the config to persistent storage. + + Category: General Utility Functions + + (internal) + """ + if not _ba.app.config_file_healthy and not force: + print('Current config file is broken; ' + 'skipping write to avoid losing settings.') + return + _ba.mark_config_dirty() diff --git a/dist/ba_data/python/ba/_appdelegate.py b/dist/ba_data/python/ba/_appdelegate.py new file mode 100644 index 0000000..f6282be --- /dev/null +++ b/dist/ba_data/python/ba/_appdelegate.py @@ -0,0 +1,31 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines AppDelegate class for handling high level app functionality.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Type, Optional, Any, Dict, Callable + import ba + + +class AppDelegate: + """Defines handlers for high level app functionality. + + Category: App Classes + """ + + def create_default_game_settings_ui( + self, gameclass: Type[ba.GameActivity], + sessiontype: Type[ba.Session], settings: Optional[dict], + completion_call: Callable[[Optional[dict]], None]) -> None: + """Launch a UI to configure the given game config. + + It should manipulate the contents of config and call completion_call + when done. + """ + del gameclass, sessiontype, settings, completion_call # Unused. + from ba import _error + _error.print_error( + "create_default_game_settings_ui needs to be overridden") diff --git a/dist/ba_data/python/ba/_appmode.py b/dist/ba_data/python/ba/_appmode.py new file mode 100644 index 0000000..ccec7ab --- /dev/null +++ b/dist/ba_data/python/ba/_appmode.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the high level state of the app.""" diff --git a/dist/ba_data/python/ba/_apputils.py b/dist/ba_data/python/ba/_apputils.py new file mode 100644 index 0000000..f3698a4 --- /dev/null +++ b/dist/ba_data/python/ba/_apputils.py @@ -0,0 +1,238 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Utility functionality related to the overall operation of the app.""" +from __future__ import annotations + +import gc +import os +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import List, Any, Callable, Optional + import ba + + +def is_browser_likely_available() -> bool: + """Return whether a browser likely exists on the current device. + + category: General Utility Functions + + If this returns False you may want to avoid calling ba.show_url() + with any lengthy addresses. (ba.show_url() will display an address + as a string in a window if unable to bring up a browser, but that + is only useful for simple URLs.) + """ + app = _ba.app + platform = app.platform + touchscreen = _ba.getinputdevice('TouchScreen', '#1', doraise=False) + + # If we're on a vr device or an android device with no touchscreen, + # assume no browser. + # FIXME: Might not be the case anymore; should make this definable + # at the platform level. + if app.vr_mode or (platform == 'android' and touchscreen is None): + return False + + # Anywhere else assume we've got one. + return True + + +def get_remote_app_name() -> ba.Lstr: + """(internal)""" + from ba import _language + return _language.Lstr(resource='remote_app.app_name') + + +def should_submit_debug_info() -> bool: + """(internal)""" + return _ba.app.config.get('Submit Debug Info', True) + + +def handle_log() -> None: + """Called on debug log prints. + + When this happens, we can upload our log to the server + after a short bit if desired. + """ + from ba._netutils import master_server_post + from ba._enums import TimeType + app = _ba.app + app.log_have_new = True + if not app.log_upload_timer_started: + + def _put_log() -> None: + try: + sessionname = str(_ba.get_foreground_host_session()) + except Exception: + sessionname = 'unavailable' + try: + activityname = str(_ba.get_foreground_host_activity()) + except Exception: + activityname = 'unavailable' + + info = { + 'log': _ba.getlog(), + 'version': app.version, + 'build': app.build_number, + 'userAgentString': app.user_agent_string, + 'session': sessionname, + 'activity': activityname, + 'fatal': 0, + 'userRanCommands': _ba.has_user_run_commands(), + 'time': _ba.time(TimeType.REAL), + 'userModded': _ba.has_user_mods(), + 'newsShow': _ba.get_news_show(), + } + + def response(data: Any) -> None: + # A non-None response means success; lets + # take note that we don't need to report further + # log info this run + if data is not None: + app.log_have_new = False + _ba.mark_log_sent() + + master_server_post('bsLog', info, response) + + app.log_upload_timer_started = True + + # Delay our log upload slightly in case other + # pertinent info gets printed between now and then. + with _ba.Context('ui'): + _ba.timer(3.0, _put_log, timetype=TimeType.REAL) + + # After a while, allow another log-put. + def _reset() -> None: + app.log_upload_timer_started = False + if app.log_have_new: + handle_log() + + if not _ba.is_log_full(): + with _ba.Context('ui'): + _ba.timer(600.0, + _reset, + timetype=TimeType.REAL, + suppress_format_warning=True) + + +def handle_leftover_log_file() -> None: + """Handle an un-uploaded log from a previous run.""" + try: + import json + from ba._netutils import master_server_post + + if os.path.exists(_ba.get_log_file_path()): + with open(_ba.get_log_file_path()) as infile: + info = json.loads(infile.read()) + infile.close() + do_send = should_submit_debug_info() + if do_send: + + def response(data: Any) -> None: + # Non-None response means we were successful; + # lets kill it. + if data is not None: + try: + os.remove(_ba.get_log_file_path()) + except FileNotFoundError: + # Saw this in the wild. The file just existed + # a moment ago but I suppose something could have + # killed it since. ¯\_(ツ)_/¯ + pass + + master_server_post('bsLog', info, response) + else: + # If they don't want logs uploaded just kill it. + os.remove(_ba.get_log_file_path()) + except Exception: + from ba import _error + _error.print_exception('Error handling leftover log file.') + + +def garbage_collect_session_end() -> None: + """Run explicit garbage collection with extra checks for session end.""" + gc.collect() + + # Can be handy to print this to check for leaks between games. + if bool(False): + print('PY OBJ COUNT', len(gc.get_objects())) + if gc.garbage: + print('PYTHON GC FOUND', len(gc.garbage), 'UNCOLLECTIBLE OBJECTS:') + for i, obj in enumerate(gc.garbage): + print(str(i) + ':', obj) + print_live_object_warnings('after session shutdown') + + +def garbage_collect() -> None: + """Run an explicit pass of garbage collection. + + category: General Utility Functions + + May also print warnings/etc. if collection takes too long or if + uncollectible objects are found (so use this instead of simply + gc.collect(). + """ + gc.collect() + + +def print_live_object_warnings(when: Any, + ignore_session: ba.Session = None, + ignore_activity: ba.Activity = None) -> None: + """Print warnings for remaining objects in the current context.""" + # pylint: disable=cyclic-import + from ba._session import Session + from ba._actor import Actor + from ba._activity import Activity + + sessions: List[ba.Session] = [] + activities: List[ba.Activity] = [] + actors: List[ba.Actor] = [] + + # Once we come across leaked stuff, printing again is probably + # redundant. + if _ba.app.printed_live_object_warning: + return + for obj in gc.get_objects(): + if isinstance(obj, Actor): + actors.append(obj) + elif isinstance(obj, Session): + sessions.append(obj) + elif isinstance(obj, Activity): + activities.append(obj) + + # Complain about any remaining sessions. + for session in sessions: + if session is ignore_session: + continue + _ba.app.printed_live_object_warning = True + print(f'ERROR: Session found {when}: {session}') + + # Complain about any remaining activities. + for activity in activities: + if activity is ignore_activity: + continue + _ba.app.printed_live_object_warning = True + print(f'ERROR: Activity found {when}: {activity}') + + # Complain about any remaining actors. + for actor in actors: + _ba.app.printed_live_object_warning = True + print(f'ERROR: Actor found {when}: {actor}') + + +def print_corrupt_file_error() -> None: + """Print an error if a corrupt file is found.""" + from ba._general import Call + from ba._enums import TimeType + _ba.timer(2.0, + lambda: _ba.screenmessage( + _ba.app.lang.get_resource('internal.corruptFileText'). + replace('${EMAIL}', 'support@froemling.net'), + color=(1, 0, 0), + ), + timetype=TimeType.REAL) + _ba.timer(2.0, + Call(_ba.playsound, _ba.getsound('error')), + timetype=TimeType.REAL) diff --git a/dist/ba_data/python/ba/_assetmanager.py b/dist/ba_data/python/ba/_assetmanager.py new file mode 100644 index 0000000..118f230 --- /dev/null +++ b/dist/ba_data/python/ba/_assetmanager.py @@ -0,0 +1,222 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to managing cloud based assets.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from pathlib import Path +import threading +import urllib.request +import logging +import weakref +import time +import os +import sys + +from efro import entity + +if TYPE_CHECKING: + from bacommon.assets import AssetPackageFlavor + from typing import List + + +class FileValue(entity.CompoundValue): + """State for an individual file.""" + + +class State(entity.Entity): + """Holds all persistent state for the asset-manager.""" + + files = entity.CompoundDictField('files', str, FileValue()) + + +class AssetManager: + """Wrangles all assets.""" + + _state: State + + def __init__(self, rootdir: Path) -> None: + print('AssetManager()') + assert isinstance(rootdir, Path) + self.thread_ident = threading.get_ident() + self._rootdir = rootdir + self._started = False + if not self._rootdir.is_dir(): + raise RuntimeError(f'Provided rootdir does not exist: "{rootdir}"') + + self.load_state() + + def __del__(self) -> None: + print('~AssetManager()') + if self._started: + logging.warning('AssetManager dying in a started state.') + + def launch_gather( + self, + packages: List[str], + flavor: AssetPackageFlavor, + account_token: str, + ) -> AssetGather: + """Spawn an asset-gather operation from this manager.""" + print('would gather', packages, 'and flavor', flavor, 'with token', + account_token) + return AssetGather(self) + + def update(self) -> None: + """Can be called periodically to perform upkeep.""" + + def start(self) -> None: + """Tell the manager to start working. + + This will initiate network activity and other processing. + """ + if self._started: + logging.warning('AssetManager.start() called on running manager.') + self._started = True + + def stop(self) -> None: + """Tell the manager to stop working. + + All network activity should be ceased before this function returns. + """ + if not self._started: + logging.warning('AssetManager.stop() called on stopped manager.') + self._started = False + self.save_state() + + @property + def rootdir(self) -> Path: + """The root directory for this manager.""" + return self._rootdir + + @property + def state_path(self) -> Path: + """The path of the state file.""" + return Path(self._rootdir, 'state') + + def load_state(self) -> None: + """Loads state from disk. Resets to default state if unable to.""" + print('ASSET-MANAGER LOADING STATE') + try: + state_path = self.state_path + if state_path.exists(): + with open(self.state_path) as infile: + self._state = State.from_json_str(infile.read()) + return + except Exception: + logging.exception('Error loading existing AssetManager state') + self._state = State() + + def save_state(self) -> None: + """Save state to disk (if possible).""" + + print('ASSET-MANAGER SAVING STATE') + try: + with open(self.state_path, 'w') as outfile: + outfile.write(self._state.to_json_str()) + except Exception: + logging.exception('Error writing AssetManager state') + + +class AssetGather: + """Wrangles a gathering of assets.""" + + def __init__(self, manager: AssetManager) -> None: + assert threading.get_ident() == manager.thread_ident + self._manager = weakref.ref(manager) + # self._valid = True + print('AssetGather()') + # url = 'https://files.ballistica.net/bombsquad/promo/BSGamePlay.mov' + # url = 'http://www.python.org/ftp/python/2.7.3/Python-2.7.3.tgz' + # fetch_url(url, + # filename=Path(manager.rootdir, 'testdl'), + # asset_gather=self) + # print('fetch success') + thread = threading.Thread(target=self._run) + thread.run() + + def _run(self) -> None: + """Run the gather in a background thread.""" + print('hello from gather bg') + + # First, do some sort of. + + # @property + # def valid(self) -> bool: + # """Whether this gather is still valid. + + # A gather becomes in valid if its originating AssetManager dies. + # """ + # return True + + def __del__(self) -> None: + print('~AssetGather()') + + +def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None: + """Fetch a given url to a given filename for a given AssetGather. + + """ + + import socket + + # We don't want to keep the provided AssetGather alive, but we want + # to abort if it dies. + assert isinstance(asset_gather, AssetGather) + # weak_gather = weakref.ref(asset_gather) + + # Pass a very short timeout to urllib so we have opportunities + # to cancel even with network blockage. + req = urllib.request.urlopen(url, timeout=1) + file_size = int(req.headers['Content-Length']) + print(f'\nDownloading: {filename} Bytes: {file_size:,}') + + def doit() -> None: + time.sleep(1) + print('dir', type(req.fp), dir(req.fp)) + print('WOULD DO IT', flush=True) + # req.close() + # req.fp.close() + + threading.Thread(target=doit).run() + + with open(filename, 'wb') as outfile: + file_size_dl = 0 + + block_sz = 1024 * 1024 * 1000 + time_outs = 0 + while True: + try: + data = req.read(block_sz) + except ValueError: + import traceback + traceback.print_exc() + print('VALUEERROR', flush=True) + break + except socket.timeout: + print('TIMEOUT', flush=True) + # File has not had activity in max seconds. + if time_outs > 3: + print('\n\n\nsorry -- try back later') + os.unlink(filename) + raise + print('\nHmmm... little issue... ' + 'I\'ll wait a couple of seconds') + time.sleep(3) + time_outs += 1 + continue + + # We reached the end of the download! + if not data: + sys.stdout.write('\rDone!\n\n') + sys.stdout.flush() + break + + file_size_dl += len(data) + outfile.write(data) + percent = file_size_dl * 1.0 / file_size + status = f'{file_size_dl:20,} Bytes [{percent:.2%}] received' + sys.stdout.write('\r' + status) + sys.stdout.flush() + print('done with', req.fp) diff --git a/dist/ba_data/python/ba/_benchmark.py b/dist/ba_data/python/ba/_benchmark.py new file mode 100644 index 0000000..070b022 --- /dev/null +++ b/dist/ba_data/python/ba/_benchmark.py @@ -0,0 +1,171 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Benchmark/Stress-Test related functionality.""" +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Dict, Any, Sequence + import ba + + +def run_cpu_benchmark() -> None: + """Run a cpu benchmark.""" + # pylint: disable=cyclic-import + from bastd import tutorial + from ba._session import Session + + class BenchmarkSession(Session): + """Session type for cpu benchmark.""" + + def __init__(self) -> None: + + # print('FIXME: BENCHMARK SESSION WOULD CALC DEPS.') + depsets: Sequence[ba.DependencySet] = [] + + super().__init__(depsets) + + # Store old graphics settings. + self._old_quality = _ba.app.config.resolve('Graphics Quality') + cfg = _ba.app.config + cfg['Graphics Quality'] = 'Low' + cfg.apply() + self.benchmark_type = 'cpu' + self.setactivity(_ba.newactivity(tutorial.TutorialActivity)) + + def __del__(self) -> None: + + # When we're torn down, restore old graphics settings. + cfg = _ba.app.config + cfg['Graphics Quality'] = self._old_quality + cfg.apply() + + def on_player_request(self, player: ba.SessionPlayer) -> bool: + return False + + _ba.new_host_session(BenchmarkSession, benchmark_type='cpu') + + +def run_stress_test(playlist_type: str = 'Random', + playlist_name: str = '__default__', + player_count: int = 8, + round_duration: int = 30) -> None: + """Run a stress test.""" + from ba import modutils + from ba._general import Call + from ba._enums import TimeType + _ba.screenmessage( + 'Beginning stress test.. use ' + "'End Game' to stop testing.", + color=(1, 1, 0)) + with _ba.Context('ui'): + start_stress_test({ + 'playlist_type': playlist_type, + 'playlist_name': playlist_name, + 'player_count': player_count, + 'round_duration': round_duration + }) + _ba.timer(7.0, + Call(_ba.screenmessage, + ('stats will be written to ' + + modutils.get_human_readable_user_scripts_path() + + '/stress_test_stats.csv')), + timetype=TimeType.REAL) + + +def stop_stress_test() -> None: + """End a running stress test.""" + _ba.set_stress_testing(False, 0) + try: + if _ba.app.stress_test_reset_timer is not None: + _ba.screenmessage('Ending stress test...', color=(1, 1, 0)) + except Exception: + pass + _ba.app.stress_test_reset_timer = None + + +def start_stress_test(args: Dict[str, Any]) -> None: + """(internal)""" + from ba._general import Call + from ba._dualteamsession import DualTeamSession + from ba._freeforallsession import FreeForAllSession + from ba._enums import TimeType, TimeFormat + appconfig = _ba.app.config + playlist_type = args['playlist_type'] + if playlist_type == 'Random': + if random.random() < 0.5: + playlist_type = 'Teams' + else: + playlist_type = 'Free-For-All' + _ba.screenmessage('Running Stress Test (listType="' + playlist_type + + '", listName="' + args['playlist_name'] + '")...') + if playlist_type == 'Teams': + appconfig['Team Tournament Playlist Selection'] = args['playlist_name'] + appconfig['Team Tournament Playlist Randomize'] = 1 + _ba.timer(1.0, + Call(_ba.pushcall, Call(_ba.new_host_session, + DualTeamSession)), + timetype=TimeType.REAL) + else: + appconfig['Free-for-All Playlist Selection'] = args['playlist_name'] + appconfig['Free-for-All Playlist Randomize'] = 1 + _ba.timer(1.0, + Call(_ba.pushcall, + Call(_ba.new_host_session, FreeForAllSession)), + timetype=TimeType.REAL) + _ba.set_stress_testing(True, args['player_count']) + _ba.app.stress_test_reset_timer = _ba.Timer( + args['round_duration'] * 1000, + Call(_reset_stress_test, args), + timetype=TimeType.REAL, + timeformat=TimeFormat.MILLISECONDS) + + +def _reset_stress_test(args: Dict[str, Any]) -> None: + from ba._general import Call + from ba._enums import TimeType + _ba.set_stress_testing(False, args['player_count']) + _ba.screenmessage('Resetting stress test...') + session = _ba.get_foreground_host_session() + assert session is not None + session.end() + _ba.timer(1.0, Call(start_stress_test, args), timetype=TimeType.REAL) + + +def run_gpu_benchmark() -> None: + """Kick off a benchmark to test gpu speeds.""" + _ba.screenmessage('FIXME: Not wired up yet.', color=(1, 0, 0)) + + +def run_media_reload_benchmark() -> None: + """Kick off a benchmark to test media reloading speeds.""" + from ba._general import Call + from ba._enums import TimeType + _ba.reload_media() + _ba.show_progress_bar() + + def delay_add(start_time: float) -> None: + + def doit(start_time_2: float) -> None: + _ba.screenmessage( + _ba.app.lang.get_resource( + 'debugWindow.totalReloadTimeText').replace( + '${TIME}', + str(_ba.time(TimeType.REAL) - start_time_2))) + _ba.print_load_info() + if _ba.app.config.resolve('Texture Quality') != 'High': + _ba.screenmessage(_ba.app.lang.get_resource( + 'debugWindow.reloadBenchmarkBestResultsText'), + color=(1, 1, 0)) + + _ba.add_clean_frame_callback(Call(doit, start_time)) + + # The reload starts (should add a completion callback to the + # reload func to fix this). + _ba.timer(0.05, + Call(delay_add, _ba.time(TimeType.REAL)), + timetype=TimeType.REAL) diff --git a/dist/ba_data/python/ba/_campaign.py b/dist/ba_data/python/ba/_campaign.py new file mode 100644 index 0000000..77f4ce9 --- /dev/null +++ b/dist/ba_data/python/ba/_campaign.py @@ -0,0 +1,352 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to co-op campaigns.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Any, List, Dict + import ba + + +def register_campaign(campaign: ba.Campaign) -> None: + """Register a new campaign.""" + _ba.app.campaigns[campaign.name] = campaign + + +def getcampaign(name: str) -> ba.Campaign: + """Return a campaign by name.""" + return _ba.app.campaigns[name] + + +class Campaign: + """Represents a unique set or series of ba.Levels. + + Category: App Classes + """ + + def __init__(self, name: str, sequential: bool = True): + self._name = name + self._levels: List[ba.Level] = [] + self._sequential = sequential + + @property + def name(self) -> str: + """The name of the Campaign.""" + return self._name + + @property + def sequential(self) -> bool: + """Whether this Campaign's levels must be played in sequence.""" + return self._sequential + + def addlevel(self, level: ba.Level) -> None: + """Adds a ba.Level to the Campaign.""" + if level.campaign is not None: + raise RuntimeError('Level already belongs to a campaign.') + level.set_campaign(self, len(self._levels)) + self._levels.append(level) + + @property + def levels(self) -> List[ba.Level]: + """The list of ba.Levels in the Campaign.""" + return self._levels + + def getlevel(self, name: str) -> ba.Level: + """Return a contained ba.Level by name.""" + from ba import _error + for level in self._levels: + if level.name == name: + return level + raise _error.NotFoundError("Level '" + name + + "' not found in campaign '" + self.name + + "'") + + def reset(self) -> None: + """Reset state for the Campaign.""" + _ba.app.config.setdefault('Campaigns', {})[self._name] = {} + + # FIXME should these give/take ba.Level instances instead of level names?.. + def set_selected_level(self, levelname: str) -> None: + """Set the Level currently selected in the UI (by name).""" + self.configdict['Selection'] = levelname + _ba.app.config.commit() + + def get_selected_level(self) -> str: + """Return the name of the Level currently selected in the UI.""" + return self.configdict.get('Selection', self._levels[0].name) + + @property + def configdict(self) -> Dict[str, Any]: + """Return the live config dict for this campaign.""" + val: Dict[str, Any] = (_ba.app.config.setdefault('Campaigns', + {}).setdefault( + self._name, {})) + assert isinstance(val, dict) + return val + + +def init_campaigns() -> None: + """Fill out initial default Campaigns.""" + # pylint: disable=too-many-statements + # pylint: disable=cyclic-import + from ba import _level + from bastd.game.onslaught import OnslaughtGame + from bastd.game.football import FootballCoopGame + from bastd.game.runaround import RunaroundGame + from bastd.game.thelaststand import TheLastStandGame + from bastd.game.race import RaceGame + from bastd.game.targetpractice import TargetPracticeGame + from bastd.game.meteorshower import MeteorShowerGame + from bastd.game.easteregghunt import EasterEggHuntGame + from bastd.game.ninjafight import NinjaFightGame + + # TODO: Campaigns should be load-on-demand; not all imported at launch + # like this. + + # FIXME: Once translations catch up, we can convert these to use the + # generic display-name '${GAME} Training' type stuff. + campaign = Campaign('Easy') + campaign.addlevel( + _level.Level('Onslaught Training', + gametype=OnslaughtGame, + settings={'preset': 'training_easy'}, + preview_texture_name='doomShroomPreview')) + campaign.addlevel( + _level.Level('Rookie Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'rookie_easy'}, + preview_texture_name='courtyardPreview')) + campaign.addlevel( + _level.Level('Rookie Football', + gametype=FootballCoopGame, + settings={'preset': 'rookie_easy'}, + preview_texture_name='footballStadiumPreview')) + campaign.addlevel( + _level.Level('Pro Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'pro_easy'}, + preview_texture_name='doomShroomPreview')) + campaign.addlevel( + _level.Level('Pro Football', + gametype=FootballCoopGame, + settings={'preset': 'pro_easy'}, + preview_texture_name='footballStadiumPreview')) + campaign.addlevel( + _level.Level('Pro Runaround', + gametype=RunaroundGame, + settings={'preset': 'pro_easy'}, + preview_texture_name='towerDPreview')) + campaign.addlevel( + _level.Level('Uber Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'uber_easy'}, + preview_texture_name='courtyardPreview')) + campaign.addlevel( + _level.Level('Uber Football', + gametype=FootballCoopGame, + settings={'preset': 'uber_easy'}, + preview_texture_name='footballStadiumPreview')) + campaign.addlevel( + _level.Level('Uber Runaround', + gametype=RunaroundGame, + settings={'preset': 'uber_easy'}, + preview_texture_name='towerDPreview')) + register_campaign(campaign) + + # "hard" mode + campaign = Campaign('Default') + campaign.addlevel( + _level.Level('Onslaught Training', + gametype=OnslaughtGame, + settings={'preset': 'training'}, + preview_texture_name='doomShroomPreview')) + campaign.addlevel( + _level.Level('Rookie Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'rookie'}, + preview_texture_name='courtyardPreview')) + campaign.addlevel( + _level.Level('Rookie Football', + gametype=FootballCoopGame, + settings={'preset': 'rookie'}, + preview_texture_name='footballStadiumPreview')) + campaign.addlevel( + _level.Level('Pro Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'pro'}, + preview_texture_name='doomShroomPreview')) + campaign.addlevel( + _level.Level('Pro Football', + gametype=FootballCoopGame, + settings={'preset': 'pro'}, + preview_texture_name='footballStadiumPreview')) + campaign.addlevel( + _level.Level('Pro Runaround', + gametype=RunaroundGame, + settings={'preset': 'pro'}, + preview_texture_name='towerDPreview')) + campaign.addlevel( + _level.Level('Uber Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'uber'}, + preview_texture_name='courtyardPreview')) + campaign.addlevel( + _level.Level('Uber Football', + gametype=FootballCoopGame, + settings={'preset': 'uber'}, + preview_texture_name='footballStadiumPreview')) + campaign.addlevel( + _level.Level('Uber Runaround', + gametype=RunaroundGame, + settings={'preset': 'uber'}, + preview_texture_name='towerDPreview')) + campaign.addlevel( + _level.Level('The Last Stand', + gametype=TheLastStandGame, + settings={}, + preview_texture_name='rampagePreview')) + register_campaign(campaign) + + # challenges: our 'official' random extra co-op levels + campaign = Campaign('Challenges', sequential=False) + campaign.addlevel( + _level.Level('Infinite Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'endless'}, + preview_texture_name='doomShroomPreview')) + campaign.addlevel( + _level.Level('Infinite Runaround', + gametype=RunaroundGame, + settings={'preset': 'endless'}, + preview_texture_name='towerDPreview')) + campaign.addlevel( + _level.Level('Race', + displayname='${GAME}', + gametype=RaceGame, + settings={ + 'map': 'Big G', + 'Laps': 3, + 'Bomb Spawning': 0 + }, + preview_texture_name='bigGPreview')) + campaign.addlevel( + _level.Level('Pro Race', + displayname='Pro ${GAME}', + gametype=RaceGame, + settings={ + 'map': 'Big G', + 'Laps': 3, + 'Bomb Spawning': 1000 + }, + preview_texture_name='bigGPreview')) + campaign.addlevel( + _level.Level('Lake Frigid Race', + displayname='${GAME}', + gametype=RaceGame, + settings={ + 'map': 'Lake Frigid', + 'Laps': 6, + 'Mine Spawning': 2000, + 'Bomb Spawning': 0 + }, + preview_texture_name='lakeFrigidPreview')) + campaign.addlevel( + _level.Level('Football', + displayname='${GAME}', + gametype=FootballCoopGame, + settings={'preset': 'tournament'}, + preview_texture_name='footballStadiumPreview')) + campaign.addlevel( + _level.Level('Pro Football', + displayname='Pro ${GAME}', + gametype=FootballCoopGame, + settings={'preset': 'tournament_pro'}, + preview_texture_name='footballStadiumPreview')) + campaign.addlevel( + _level.Level('Runaround', + displayname='${GAME}', + gametype=RunaroundGame, + settings={'preset': 'tournament'}, + preview_texture_name='towerDPreview')) + campaign.addlevel( + _level.Level('Uber Runaround', + displayname='Uber ${GAME}', + gametype=RunaroundGame, + settings={'preset': 'tournament_uber'}, + preview_texture_name='towerDPreview')) + campaign.addlevel( + _level.Level('The Last Stand', + displayname='${GAME}', + gametype=TheLastStandGame, + settings={'preset': 'tournament'}, + preview_texture_name='rampagePreview')) + campaign.addlevel( + _level.Level('Tournament Infinite Onslaught', + displayname='Infinite Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'endless_tournament'}, + preview_texture_name='doomShroomPreview')) + campaign.addlevel( + _level.Level('Tournament Infinite Runaround', + displayname='Infinite Runaround', + gametype=RunaroundGame, + settings={'preset': 'endless_tournament'}, + preview_texture_name='towerDPreview')) + campaign.addlevel( + _level.Level('Target Practice', + displayname='Pro ${GAME}', + gametype=TargetPracticeGame, + settings={}, + preview_texture_name='doomShroomPreview')) + campaign.addlevel( + _level.Level('Target Practice B', + displayname='${GAME}', + gametype=TargetPracticeGame, + settings={ + 'Target Count': 2, + 'Enable Impact Bombs': False, + 'Enable Triple Bombs': False + }, + preview_texture_name='doomShroomPreview')) + campaign.addlevel( + _level.Level('Meteor Shower', + displayname='${GAME}', + gametype=MeteorShowerGame, + settings={}, + preview_texture_name='rampagePreview')) + campaign.addlevel( + _level.Level('Epic Meteor Shower', + displayname='${GAME}', + gametype=MeteorShowerGame, + settings={'Epic Mode': True}, + preview_texture_name='rampagePreview')) + campaign.addlevel( + _level.Level('Easter Egg Hunt', + displayname='${GAME}', + gametype=EasterEggHuntGame, + settings={}, + preview_texture_name='towerDPreview')) + campaign.addlevel( + _level.Level('Pro Easter Egg Hunt', + displayname='Pro ${GAME}', + gametype=EasterEggHuntGame, + settings={'Pro Mode': True}, + preview_texture_name='towerDPreview')) + campaign.addlevel( + _level.Level( + name='Ninja Fight', # (unique id not seen by player) + displayname='${GAME}', # (readable name seen by player) + gametype=NinjaFightGame, + settings={'preset': 'regular'}, + preview_texture_name='courtyardPreview')) + campaign.addlevel( + _level.Level(name='Pro Ninja Fight', + displayname='Pro ${GAME}', + gametype=NinjaFightGame, + settings={'preset': 'pro'}, + preview_texture_name='courtyardPreview')) + register_campaign(campaign) diff --git a/dist/ba_data/python/ba/_collision.py b/dist/ba_data/python/ba/_collision.py new file mode 100644 index 0000000..e9db1e8 --- /dev/null +++ b/dist/ba_data/python/ba/_collision.py @@ -0,0 +1,72 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Collision related functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +from ba._error import NodeNotFoundError + +if TYPE_CHECKING: + import ba + + +class Collision: + """A class providing info about occurring collisions. + + Category: Gameplay Classes + """ + + @property + def position(self) -> ba.Vec3: + """The position of the current collision.""" + return _ba.Vec3(_ba.get_collision_info('position')) + + @property + def sourcenode(self) -> ba.Node: + """The node containing the material triggering the current callback. + + Throws a ba.NodeNotFoundError if the node does not exist, though + the node should always exist (at least at the start of the collision + callback). + """ + node = _ba.get_collision_info('sourcenode') + assert isinstance(node, (_ba.Node, type(None))) + if not node: + raise NodeNotFoundError() + return node + + @property + def opposingnode(self) -> ba.Node: + """The node the current callback material node is hitting. + + Throws a ba.NodeNotFoundError if the node does not exist. + This can be expected in some cases such as in 'disconnect' + callbacks triggered by deleting a currently-colliding node. + """ + node = _ba.get_collision_info('opposingnode') + assert isinstance(node, (_ba.Node, type(None))) + if not node: + raise NodeNotFoundError() + return node + + @property + def opposingbody(self) -> int: + """The body index on the opposing node in the current collision.""" + body = _ba.get_collision_info('opposingbody') + assert isinstance(body, int) + return body + + +# Simply recycle one instance... +_collision = Collision() + + +def getcollision() -> Collision: + """Return the in-progress collision. + + Category: Gameplay Functions + """ + return _collision diff --git a/dist/ba_data/python/ba/_coopgame.py b/dist/ba_data/python/ba/_coopgame.py new file mode 100644 index 0000000..ae7adca --- /dev/null +++ b/dist/ba_data/python/ba/_coopgame.py @@ -0,0 +1,270 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to co-op games.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +import _ba +from ba._gameactivity import GameActivity +from ba._general import WeakCall + +if TYPE_CHECKING: + from typing import Type, Dict, Any, Set, List, Sequence, Optional + from bastd.actor.playerspaz import PlayerSpaz + import ba + +PlayerType = TypeVar('PlayerType', bound='ba.Player') +TeamType = TypeVar('TeamType', bound='ba.Team') + + +class CoopGameActivity(GameActivity[PlayerType, TeamType]): + """Base class for cooperative-mode games. + + Category: Gameplay Classes + """ + + # We can assume our session is a CoopSession. + session: ba.CoopSession + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + from ba._coopsession import CoopSession + return issubclass(sessiontype, CoopSession) + + def __init__(self, settings: dict): + super().__init__(settings) + + # Cache these for efficiency. + self._achievements_awarded: Set[str] = set() + + self._life_warning_beep: Optional[ba.Actor] = None + self._life_warning_beep_timer: Optional[ba.Timer] = None + self._warn_beeps_sound = _ba.getsound('warnBeeps') + + def on_begin(self) -> None: + super().on_begin() + + # Show achievements remaining. + if not (_ba.app.demo_mode or _ba.app.arcade_mode): + _ba.timer(3.8, WeakCall(self._show_remaining_achievements)) + + # Preload achievement images in case we get some. + _ba.timer(2.0, WeakCall(self._preload_achievements)) + + # Let's ask the server for a 'time-to-beat' value. + levelname = self._get_coop_level_name() + campaign = self.session.campaign + assert campaign is not None + config_str = (str(len(self.players)) + 'p' + campaign.getlevel( + self.settings_raw['name']).get_score_version_string().replace( + ' ', '_')) + _ba.get_scores_to_beat(levelname, config_str, + WeakCall(self._on_got_scores_to_beat)) + + def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: + pass + + def _show_standard_scores_to_beat_ui(self, + scores: List[Dict[str, Any]]) -> None: + from efro.util import asserttype + from ba._gameutils import timestring, animate + from ba._nodeactor import NodeActor + from ba._enums import TimeFormat + display_type = self.get_score_type() + if scores is not None: + + # Sort by originating date so that the most recent is first. + scores.sort(reverse=True, key=lambda s: asserttype(s['time'], int)) + + # Now make a display for the most recent challenge. + for score in scores: + if score['type'] == 'score_challenge': + tval = (score['player'] + ': ' + timestring( + int(score['value']) * 10, + timeformat=TimeFormat.MILLISECONDS).evaluate() + if display_type == 'time' else str(score['value'])) + hattach = 'center' if display_type == 'time' else 'left' + halign = 'center' if display_type == 'time' else 'left' + pos = (20, -70) if display_type == 'time' else (20, -130) + txt = NodeActor( + _ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': hattach, + 'h_align': halign, + 'color': (0.7, 0.4, 1, 1), + 'shadow': 0.5, + 'flatness': 1.0, + 'position': pos, + 'scale': 0.6, + 'text': tval + })).autoretain() + assert txt.node is not None + animate(txt.node, 'scale', {1.0: 0.0, 1.1: 0.7, 1.2: 0.6}) + break + + # FIXME: this is now redundant with activityutils.getscoreconfig(); + # need to kill this. + def get_score_type(self) -> str: + """ + Return the score unit this co-op game uses ('point', 'seconds', etc.) + """ + return 'points' + + def _get_coop_level_name(self) -> str: + assert self.session.campaign is not None + return self.session.campaign.name + ':' + str( + self.settings_raw['name']) + + def celebrate(self, duration: float) -> None: + """Tells all existing player-controlled characters to celebrate. + + Can be useful in co-op games when the good guys score or complete + a wave. + duration is given in seconds. + """ + from ba._messages import CelebrateMessage + for player in self.players: + if player.actor: + player.actor.handlemessage(CelebrateMessage(duration)) + + def _preload_achievements(self) -> None: + achievements = _ba.app.ach.achievements_for_coop_level( + self._get_coop_level_name()) + for ach in achievements: + ach.get_icon_texture(True) + + def _show_remaining_achievements(self) -> None: + # pylint: disable=cyclic-import + from ba._language import Lstr + from bastd.actor.text import Text + ts_h_offs = 30 + v_offs = -200 + achievements = [ + a for a in _ba.app.ach.achievements_for_coop_level( + self._get_coop_level_name()) if not a.complete + ] + vrmode = _ba.app.vr_mode + if achievements: + Text(Lstr(resource='achievementsRemainingText'), + host_only=True, + position=(ts_h_offs - 10 + 40, v_offs - 10), + transition=Text.Transition.FADE_IN, + scale=1.1, + h_attach=Text.HAttach.LEFT, + v_attach=Text.VAttach.TOP, + color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1.0, 1.0), + flatness=1.0 if vrmode else 0.6, + shadow=1.0 if vrmode else 0.5, + transition_delay=0.0, + transition_out_delay=1.3 + if self.slow_motion else 4.0).autoretain() + hval = 70 + vval = -50 + tdelay = 0.0 + for ach in achievements: + tdelay += 0.05 + ach.create_display(hval + 40, + vval + v_offs, + 0 + tdelay, + outdelay=1.3 if self.slow_motion else 4.0, + style='in_game') + vval -= 55 + + def spawn_player_spaz(self, + player: PlayerType, + position: Sequence[float] = (0.0, 0.0, 0.0), + angle: float = None) -> PlayerSpaz: + """Spawn and wire up a standard player spaz.""" + spaz = super().spawn_player_spaz(player, position, angle) + + # Deaths are noteworthy in co-op games. + spaz.play_big_death_sound = True + return spaz + + def _award_achievement(self, + achievement_name: str, + sound: bool = True) -> None: + """Award an achievement. + + Returns True if a banner will be shown; + False otherwise + """ + + if achievement_name in self._achievements_awarded: + return + + ach = _ba.app.ach.get_achievement(achievement_name) + + # If we're in the easy campaign and this achievement is hard-mode-only, + # ignore it. + try: + campaign = self.session.campaign + assert campaign is not None + if ach.hard_mode_only and campaign.name == 'Easy': + return + except Exception: + from ba._error import print_exception + print_exception() + + # If we haven't awarded this one, check to see if we've got it. + # If not, set it through the game service *and* add a transaction + # for it. + if not ach.complete: + self._achievements_awarded.add(achievement_name) + + # Report new achievements to the game-service. + _ba.report_achievement(achievement_name) + + # ...and to our account. + _ba.add_transaction({ + 'type': 'ACHIEVEMENT', + 'name': achievement_name + }) + + # Now bring up a celebration banner. + ach.announce_completion(sound=sound) + + def fade_to_red(self) -> None: + """Fade the screen to red; (such as when the good guys have lost).""" + from ba import _gameutils + c_existing = self.globalsnode.tint + cnode = _ba.newnode('combine', + attrs={ + 'input0': c_existing[0], + 'input1': c_existing[1], + 'input2': c_existing[2], + 'size': 3 + }) + _gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0}) + _gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0}) + cnode.connectattr('output', self.globalsnode, 'tint') + + def setup_low_life_warning_sound(self) -> None: + """Set up a beeping noise to play when any players are near death.""" + self._life_warning_beep = None + self._life_warning_beep_timer = _ba.Timer( + 1.0, WeakCall(self._update_life_warning), repeat=True) + + def _update_life_warning(self) -> None: + # Beep continuously if anyone is close to death. + should_beep = False + for player in self.players: + if player.is_alive(): + # FIXME: Should abstract this instead of + # reading hitpoints directly. + if getattr(player.actor, 'hitpoints', 999) < 200: + should_beep = True + break + if should_beep and self._life_warning_beep is None: + from ba._nodeactor import NodeActor + self._life_warning_beep = NodeActor( + _ba.newnode('sound', + attrs={ + 'sound': self._warn_beeps_sound, + 'positional': False, + 'loop': True + })) + if self._life_warning_beep is not None and not should_beep: + self._life_warning_beep = None diff --git a/dist/ba_data/python/ba/_coopsession.py b/dist/ba_data/python/ba/_coopsession.py new file mode 100644 index 0000000..6273f86 --- /dev/null +++ b/dist/ba_data/python/ba/_coopsession.py @@ -0,0 +1,388 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to coop-mode sessions.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +from ba._session import Session + +if TYPE_CHECKING: + from typing import Any, List, Dict, Optional, Callable, Sequence + import ba + +TEAM_COLORS = [(0.2, 0.4, 1.6)] +TEAM_NAMES = ['Good Guys'] + + +class CoopSession(Session): + """A ba.Session which runs cooperative-mode games. + + Category: Gameplay Classes + + These generally consist of 1-4 players against + the computer and include functionality such as + high score lists. + + Attributes: + + campaign + The ba.Campaign instance this Session represents, or None if + there is no associated Campaign. + """ + use_teams = True + use_team_colors = False + allow_mid_activity_joins = False + + # Note: even though these are instance vars, we annotate them at the + # class level so that docs generation can access their types. + campaign: Optional[ba.Campaign] + + def __init__(self) -> None: + """Instantiate a co-op mode session.""" + # pylint: disable=cyclic-import + from ba._campaign import getcampaign + from bastd.activity.coopjoin import CoopJoinActivity + + _ba.increment_analytics_count('Co-op session start') + app = _ba.app + + # If they passed in explicit min/max, honor that. + # Otherwise defer to user overrides or defaults. + if 'min_players' in app.coop_session_args: + min_players = app.coop_session_args['min_players'] + else: + min_players = 1 + if 'max_players' in app.coop_session_args: + max_players = app.coop_session_args['max_players'] + else: + max_players = app.config.get('Coop Game Max Players', 4) + + # print('FIXME: COOP SESSION WOULD CALC DEPS.') + depsets: Sequence[ba.DependencySet] = [] + + super().__init__(depsets, + team_names=TEAM_NAMES, + team_colors=TEAM_COLORS, + min_players=min_players, + max_players=max_players) + + # Tournament-ID if we correspond to a co-op tournament (otherwise None) + self.tournament_id: Optional[str] = ( + app.coop_session_args.get('tournament_id')) + + self.campaign = getcampaign(app.coop_session_args['campaign']) + self.campaign_level_name: str = app.coop_session_args['level'] + + self._ran_tutorial_activity = False + self._tutorial_activity: Optional[ba.Activity] = None + self._custom_menu_ui: List[Dict[str, Any]] = [] + + # Start our joining screen. + self.setactivity(_ba.newactivity(CoopJoinActivity)) + + self._next_game_instance: Optional[ba.GameActivity] = None + self._next_game_level_name: Optional[str] = None + self._update_on_deck_game_instances() + + def get_current_game_instance(self) -> ba.GameActivity: + """Get the game instance currently being played.""" + return self._current_game_instance + + def _update_on_deck_game_instances(self) -> None: + # pylint: disable=cyclic-import + from ba._gameactivity import GameActivity + + # Instantiate levels we may be running soon to let them load in the bg. + + # Build an instance for the current level. + assert self.campaign is not None + level = self.campaign.getlevel(self.campaign_level_name) + gametype = level.gametype + settings = level.get_settings() + + # Make sure all settings the game expects are present. + neededsettings = gametype.get_available_settings(type(self)) + for setting in neededsettings: + if setting.name not in settings: + settings[setting.name] = setting.default + + newactivity = _ba.newactivity(gametype, settings) + assert isinstance(newactivity, GameActivity) + self._current_game_instance: GameActivity = newactivity + + # Find the next level and build an instance for it too. + levels = self.campaign.levels + level = self.campaign.getlevel(self.campaign_level_name) + + nextlevel: Optional[ba.Level] + if level.index < len(levels) - 1: + nextlevel = levels[level.index + 1] + else: + nextlevel = None + if nextlevel: + gametype = nextlevel.gametype + settings = nextlevel.get_settings() + + # Make sure all settings the game expects are present. + neededsettings = gametype.get_available_settings(type(self)) + for setting in neededsettings: + if setting.name not in settings: + settings[setting.name] = setting.default + + # We wanna be in the activity's context while taking it down. + newactivity = _ba.newactivity(gametype, settings) + assert isinstance(newactivity, GameActivity) + self._next_game_instance = newactivity + self._next_game_level_name = nextlevel.name + else: + self._next_game_instance = None + self._next_game_level_name = None + + # Special case: + # If our current level is 'onslaught training', instantiate + # our tutorial so its ready to go. (if we haven't run it yet). + if (self.campaign_level_name == 'Onslaught Training' + and self._tutorial_activity is None + and not self._ran_tutorial_activity): + from bastd.tutorial import TutorialActivity + self._tutorial_activity = _ba.newactivity(TutorialActivity) + + def get_custom_menu_entries(self) -> List[Dict[str, Any]]: + return self._custom_menu_ui + + def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None: + from ba._general import WeakCall + super().on_player_leave(sessionplayer) + + # If all our players leave we wanna quit out of the session. + _ba.timer(2.0, WeakCall(self._end_session_if_empty)) + + def _end_session_if_empty(self) -> None: + activity = self.getactivity() + if activity is None: + return # Hmm what should we do in this case? + + # If there's still players in the current activity, we're good. + if activity.players: + return + + # If there's *no* players left in the current activity but there *is* + # in the session, restart the activity to pull them into the game + # (or quit if they're just in the lobby). + if not activity.players and self.sessionplayers: + + # Special exception for tourney games; don't auto-restart these. + if self.tournament_id is not None: + self.end() + else: + # Don't restart joining activities; this probably means there's + # someone with a chooser up in that case. + if not activity.is_joining_activity: + self.restart() + + # Hmm; no players anywhere. lets just end the session. + else: + self.end() + + def _on_tournament_restart_menu_press( + self, resume_callback: Callable[[], Any]) -> None: + # pylint: disable=cyclic-import + from bastd.ui.tournamententry import TournamentEntryWindow + from ba._gameactivity import GameActivity + activity = self.getactivity() + if activity is not None and not activity.expired: + assert self.tournament_id is not None + assert isinstance(activity, GameActivity) + TournamentEntryWindow(tournament_id=self.tournament_id, + tournament_activity=activity, + on_close_call=resume_callback) + + def restart(self) -> None: + """Restart the current game activity.""" + + # Tell the current activity to end with a 'restart' outcome. + # We use 'force' so that we apply even if end has already been called + # (but is in its delay period). + + # Make an exception if there's no players left. Otherwise this + # can override the default session end that occurs in that case. + if not self.sessionplayers: + return + + # This method may get called from the UI context so make sure we + # explicitly run in the activity's context. + activity = self.getactivity() + if activity is not None and not activity.expired: + activity.can_show_ad_on_death = True + with _ba.Context(activity): + activity.end(results={'outcome': 'restart'}, force=True) + + def on_activity_end(self, activity: ba.Activity, results: Any) -> None: + """Method override for co-op sessions. + + Jumps between co-op games and score screens. + """ + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + # pylint: disable=cyclic-import + from ba._activitytypes import JoinActivity, TransitionActivity + from ba._language import Lstr + from ba._general import WeakCall + from ba._coopgame import CoopGameActivity + from ba._gameresults import GameResults + from ba._score import ScoreType + from ba._player import PlayerInfo + from bastd.tutorial import TutorialActivity + from bastd.activity.coopscore import CoopScoreScreen + + app = _ba.app + + # If we're running a TeamGameActivity we'll have a GameResults + # as results. Otherwise its an old CoopGameActivity so its giving + # us a dict of random stuff. + if isinstance(results, GameResults): + outcome = 'defeat' # This can't be 'beaten'. + else: + outcome = '' if results is None else results.get('outcome', '') + + # If at any point we have no in-game players, quit out of the session + # (this can happen if someone leaves in the tutorial for instance). + active_players = [p for p in self.sessionplayers if p.in_game] + if not active_players: + self.end() + return + + # If we're in a between-round activity or a restart-activity, + # hop into a round. + if (isinstance(activity, + (JoinActivity, CoopScoreScreen, TransitionActivity))): + + if outcome == 'next_level': + if self._next_game_instance is None: + raise RuntimeError() + assert self._next_game_level_name is not None + self.campaign_level_name = self._next_game_level_name + next_game = self._next_game_instance + else: + next_game = self._current_game_instance + + # Special case: if we're coming from a joining-activity + # and will be going into onslaught-training, show the + # tutorial first. + if (isinstance(activity, JoinActivity) + and self.campaign_level_name == 'Onslaught Training' + and not (app.demo_mode or app.arcade_mode)): + if self._tutorial_activity is None: + raise RuntimeError('Tutorial not preloaded properly.') + self.setactivity(self._tutorial_activity) + self._tutorial_activity = None + self._ran_tutorial_activity = True + self._custom_menu_ui = [] + + # Normal case; launch the next round. + else: + + # Reset stats for the new activity. + self.stats.reset() + for player in self.sessionplayers: + + # Skip players that are still choosing a team. + if player.in_game: + self.stats.register_sessionplayer(player) + self.stats.setactivity(next_game) + + # Now flip the current activity.. + self.setactivity(next_game) + + if not (app.demo_mode or app.arcade_mode): + if self.tournament_id is not None: + self._custom_menu_ui = [{ + 'label': + Lstr(resource='restartText'), + 'resume_on_call': + False, + 'call': + WeakCall(self._on_tournament_restart_menu_press + ) + }] + else: + self._custom_menu_ui = [{ + 'label': Lstr(resource='restartText'), + 'call': WeakCall(self.restart) + }] + + # If we were in a tutorial, just pop a transition to get to the + # actual round. + elif isinstance(activity, TutorialActivity): + self.setactivity(_ba.newactivity(TransitionActivity)) + else: + + playerinfos: List[ba.PlayerInfo] + + # Generic team games. + if isinstance(results, GameResults): + playerinfos = results.playerinfos + score = results.get_sessionteam_score(results.sessionteams[0]) + fail_message = None + score_order = ('decreasing' + if results.lower_is_better else 'increasing') + if results.scoretype in (ScoreType.SECONDS, + ScoreType.MILLISECONDS): + scoretype = 'time' + + # ScoreScreen wants hundredths of a second. + if score is not None: + if results.scoretype is ScoreType.SECONDS: + score *= 100 + elif results.scoretype is ScoreType.MILLISECONDS: + score //= 10 + else: + raise RuntimeError('FIXME') + else: + if results.scoretype is not ScoreType.POINTS: + print(f'Unknown ScoreType:' f' "{results.scoretype}"') + scoretype = 'points' + + # Old coop-game-specific results; should migrate away from these. + else: + playerinfos = results.get('playerinfos') + score = results['score'] if 'score' in results else None + fail_message = (results['fail_message'] + if 'fail_message' in results else None) + score_order = (results['score_order'] + if 'score_order' in results else 'increasing') + activity_score_type = (activity.get_score_type() if isinstance( + activity, CoopGameActivity) else None) + assert activity_score_type is not None + scoretype = activity_score_type + + # Validate types. + if playerinfos is not None: + assert isinstance(playerinfos, list) + assert (isinstance(i, PlayerInfo) for i in playerinfos) + + # Looks like we were in a round - check the outcome and + # go from there. + if outcome == 'restart': + + # This will pop up back in the same round. + self.setactivity(_ba.newactivity(TransitionActivity)) + else: + self.setactivity( + _ba.newactivity( + CoopScoreScreen, { + 'playerinfos': playerinfos, + 'score': score, + 'fail_message': fail_message, + 'score_order': score_order, + 'score_type': scoretype, + 'outcome': outcome, + 'campaign': self.campaign, + 'level': self.campaign_level_name + })) + + # No matter what, get the next 2 levels ready to go. + self._update_on_deck_game_instances() diff --git a/dist/ba_data/python/ba/_dependency.py b/dist/ba_data/python/ba/_dependency.py new file mode 100644 index 0000000..6cb43e8 --- /dev/null +++ b/dist/ba_data/python/ba/_dependency.py @@ -0,0 +1,422 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to object/asset dependencies.""" + +from __future__ import annotations + +import weakref +from typing import (Generic, TypeVar, TYPE_CHECKING) + +import _ba + +if TYPE_CHECKING: + from typing import Optional, Any, Dict, List, Set, Type + from weakref import ReferenceType + import ba + +T = TypeVar('T', bound='DependencyComponent') + + +class Dependency(Generic[T]): + """A dependency on a DependencyComponent (with an optional config). + + Category: Dependency Classes + + This class is used to request and access functionality provided + by other DependencyComponent classes from a DependencyComponent class. + The class functions as a descriptor, allowing dependencies to + be added at a class level much the same as properties or methods + and then used with class instances to access those dependencies. + For instance, if you do 'floofcls = ba.Dependency(FloofClass)' you + would then be able to instantiate a FloofClass in your class's + methods via self.floofcls(). + """ + + def __init__(self, cls: Type[T], config: Any = None): + """Instantiate a Dependency given a ba.DependencyComponent type. + + Optionally, an arbitrary object can be passed as 'config' to + influence dependency calculation for the target class. + """ + self.cls: Type[T] = cls + self.config = config + self._hash: Optional[int] = None + + def get_hash(self) -> int: + """Return the dependency's hash, calculating it if necessary.""" + from efro.util import make_hash + if self._hash is None: + self._hash = make_hash((self.cls, self.config)) + return self._hash + + def __get__(self, obj: Any, cls: Any = None) -> T: + if not isinstance(obj, DependencyComponent): + if obj is None: + raise TypeError( + 'Dependency must be accessed through an instance.') + raise TypeError( + f'Dependency cannot be added to class of type {type(obj)}' + ' (class must inherit from ba.DependencyComponent).') + + # We expect to be instantiated from an already living + # DependencyComponent with valid dep-data in place.. + assert cls is not None + + # Get the DependencyEntry this instance is associated with and from + # there get back to the DependencySet + entry = getattr(obj, '_dep_entry') + if entry is None: + raise RuntimeError('Invalid dependency access.') + entry = entry() + assert isinstance(entry, DependencyEntry) + depset = entry.depset() + assert isinstance(depset, DependencySet) + + if not depset.resolved: + raise RuntimeError( + "Can't access data on an unresolved DependencySet.") + + # Look up the data in the set based on the hash for this Dependency. + assert self._hash in depset.entries + entry = depset.entries[self._hash] + assert isinstance(entry, DependencyEntry) + retval = entry.get_component() + assert isinstance(retval, self.cls) + return retval + + +class DependencyComponent: + """Base class for all classes that can act as or use dependencies. + + category: Dependency Classes + """ + + _dep_entry: ReferenceType[DependencyEntry] + + def __init__(self) -> None: + """Instantiate a DependencyComponent.""" + + # For now lets issue a warning if these are instantiated without + # a dep-entry; we'll make this an error once we're no longer + # seeing warnings. + # entry = getattr(self, '_dep_entry', None) + # if entry is None: + # print(f'FIXME: INSTANTIATING DEP CLASS {type(self)} DIRECTLY.') + + @classmethod + def dep_is_present(cls, config: Any = None) -> bool: + """Return whether this component/config is present on this device.""" + del config # Unused here. + return True + + @classmethod + def get_dynamic_deps(cls, config: Any = None) -> List[Dependency]: + """Return any dynamically-calculated deps for this component/config. + + Deps declared statically as part of the class do not need to be + included here; this is only for additional deps that may vary based + on the dep config value. (for instance a map required by a game type) + """ + del config # Unused here. + return [] + + +class DependencyEntry: + """Data associated with a dependency/config pair in a ba.DependencySet.""" + + # def __del__(self) -> None: + # print('~DepEntry()', self.cls) + + def __init__(self, depset: DependencySet, dep: Dependency[T]): + # print("DepEntry()", dep.cls) + self.cls = dep.cls + self.config = dep.config + + # Arbitrary data for use by dependencies in the resolved set + # (the static instance for static-deps, etc). + self.component: Optional[DependencyComponent] = None + + # Weakref to the depset that includes us (to avoid ref loop). + self.depset = weakref.ref(depset) + + def get_component(self) -> DependencyComponent: + """Return the component instance, creating it if necessary.""" + if self.component is None: + # We don't simply call our type to instantiate our instance; + # instead we manually call __new__ and then __init__. + # This allows us to inject its data properly before __init__(). + print('creating', self.cls) + instance = self.cls.__new__(self.cls) + # pylint: disable=protected-access + instance._dep_entry = weakref.ref(self) + instance.__init__() + + assert self.depset + depset = self.depset() + assert depset is not None + self.component = instance + component = self.component + assert isinstance(component, self.cls) + if component is None: + raise RuntimeError(f'Accessing DependencyComponent {self.cls} ' + 'in an invalid state.') + return component + + +class DependencySet(Generic[T]): + """Set of resolved dependencies and their associated data. + + Category: Dependency Classes + + To use DependencyComponents, a set must be created, resolved, and then + loaded. The DependencyComponents are only valid while the set remains + in existence. + """ + + def __init__(self, root_dependency: Dependency[T]): + # print('DepSet()') + self._root_dependency = root_dependency + self._resolved = False + self._loaded = False + + # Dependency data indexed by hash. + self.entries: Dict[int, DependencyEntry] = {} + + # def __del__(self) -> None: + # print("~DepSet()") + + def resolve(self) -> None: + """Resolve the complete set of required dependencies for this set. + + Raises a ba.DependencyError if dependencies are missing (or other + Exception types on other errors). + """ + + if self._resolved: + raise Exception('DependencySet has already been resolved.') + + # print('RESOLVING DEP SET') + + # First, recursively expand out all dependencies. + self._resolve(self._root_dependency, 0) + + # Now, if any dependencies are not present, raise an Exception + # telling exactly which ones (so hopefully they'll be able to be + # downloaded/etc. + missing = [ + Dependency(entry.cls, entry.config) + for entry in self.entries.values() + if not entry.cls.dep_is_present(entry.config) + ] + if missing: + from ba._error import DependencyError + raise DependencyError(missing) + + self._resolved = True + # print('RESOLVE SUCCESS!') + + @property + def resolved(self) -> bool: + """Whether this set has been successfully resolved.""" + return self._resolved + + def get_asset_package_ids(self) -> Set[str]: + """Return the set of asset-package-ids required by this dep-set. + + Must be called on a resolved dep-set. + """ + ids: Set[str] = set() + if not self._resolved: + raise Exception('Must be called on a resolved dep-set.') + for entry in self.entries.values(): + if issubclass(entry.cls, AssetPackage): + assert isinstance(entry.config, str) + ids.add(entry.config) + return ids + + def load(self) -> None: + """Instantiate all DependencyComponents in the set. + + Returns a wrapper which can be used to instantiate the root dep. + """ + # NOTE: stuff below here should probably go in a separate 'instantiate' + # method or something. + if not self._resolved: + raise RuntimeError("Can't load an unresolved DependencySet") + + for entry in self.entries.values(): + # Do a get on everything which will init all payloads + # in the proper order recursively. + entry.get_component() + + self._loaded = True + + @property + def root(self) -> T: + """The instantiated root DependencyComponent instance for the set.""" + if not self._loaded: + raise RuntimeError('DependencySet is not loaded.') + + rootdata = self.entries[self._root_dependency.get_hash()].component + assert isinstance(rootdata, self._root_dependency.cls) + return rootdata + + def _resolve(self, dep: Dependency[T], recursion: int) -> None: + + # Watch for wacky infinite dep loops. + if recursion > 10: + raise RecursionError('Max recursion reached') + + hashval = dep.get_hash() + + if hashval in self.entries: + # Found an already resolved one; we're done here. + return + + # Add our entry before we recurse so we don't repeat add it if + # there's a dependency loop. + self.entries[hashval] = DependencyEntry(self, dep) + + # Grab all Dependency instances we find in the class. + subdeps = [ + cls for cls in dep.cls.__dict__.values() + if isinstance(cls, Dependency) + ] + + # ..and add in any dynamic ones it provides. + subdeps += dep.cls.get_dynamic_deps(dep.config) + for subdep in subdeps: + self._resolve(subdep, recursion + 1) + + +class AssetPackage(DependencyComponent): + """ba.DependencyComponent representing a bundled package of game assets. + + Category: Asset Classes + """ + + def __init__(self) -> None: + super().__init__() + + # This is used internally by the get_package_xxx calls. + self.context = _ba.Context('current') + + entry = self._dep_entry() + assert entry is not None + assert isinstance(entry.config, str) + self.package_id = entry.config + print(f'LOADING ASSET PACKAGE {self.package_id}') + + @classmethod + def dep_is_present(cls, config: Any = None) -> bool: + assert isinstance(config, str) + + # Temp: hard-coding for a single asset-package at the moment. + if config == 'stdassets@1': + return True + return False + + def gettexture(self, name: str) -> ba.Texture: + """Load a named ba.Texture from the AssetPackage. + + Behavior is similar to ba.gettexture() + """ + return _ba.get_package_texture(self, name) + + def getmodel(self, name: str) -> ba.Model: + """Load a named ba.Model from the AssetPackage. + + Behavior is similar to ba.getmodel() + """ + return _ba.get_package_model(self, name) + + def getcollidemodel(self, name: str) -> ba.CollideModel: + """Load a named ba.CollideModel from the AssetPackage. + + Behavior is similar to ba.getcollideModel() + """ + return _ba.get_package_collide_model(self, name) + + def getsound(self, name: str) -> ba.Sound: + """Load a named ba.Sound from the AssetPackage. + + Behavior is similar to ba.getsound() + """ + return _ba.get_package_sound(self, name) + + def getdata(self, name: str) -> ba.Data: + """Load a named ba.Data from the AssetPackage. + + Behavior is similar to ba.getdata() + """ + return _ba.get_package_data(self, name) + + +class TestClassFactory(DependencyComponent): + """Another test dep-obj.""" + + _assets = Dependency(AssetPackage, 'stdassets@1') + + def __init__(self) -> None: + super().__init__() + print('Instantiating TestClassFactory') + self.tex = self._assets.gettexture('black') + self.model = self._assets.getmodel('landMine') + self.sound = self._assets.getsound('error') + self.data = self._assets.getdata('langdata') + + +class TestClassObj(DependencyComponent): + """Another test dep-obj.""" + + +class TestClass(DependencyComponent): + """A test dep-obj.""" + + _testclass = Dependency(TestClassObj) + _factoryclass = Dependency(TestClassFactory, 123) + _factoryclass2 = Dependency(TestClassFactory, 123) + + def __del__(self) -> None: + print('~TestClass()') + + def __init__(self) -> None: + super().__init__() + print('TestClass()') + self._actor = self._testclass + print('got actor', self._actor) + print('have factory', self._factoryclass) + print('have factory2', self._factoryclass2) + + +def test_depset() -> None: + """Test call to try this stuff out...""" + if bool(False): + print('running test_depset()...') + + def doit() -> None: + from ba._error import DependencyError + depset = DependencySet(Dependency(TestClass)) + try: + depset.resolve() + except DependencyError as exc: + for dep in exc.deps: + if dep.cls is AssetPackage: + print('MISSING ASSET PACKAGE', dep.config) + else: + raise RuntimeError( + f'Unknown dependency error for {dep.cls}') from exc + except Exception as exc: + print('DependencySet resolve failed with exc type:', type(exc)) + if depset.resolved: + depset.load() + testobj = depset.root + # instance = testclass(123) + print('INSTANTIATED ROOT:', testobj) + + doit() + + # To test this, add prints on __del__ for stuff used above; + # everything should be dead at this point if we have no cycles. + print('everything should be cleaned up...') + _ba.quit() diff --git a/dist/ba_data/python/ba/_dualteamsession.py b/dist/ba_data/python/ba/_dualteamsession.py new file mode 100644 index 0000000..427e706 --- /dev/null +++ b/dist/ba_data/python/ba/_dualteamsession.py @@ -0,0 +1,57 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to teams sessions.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +from ba._multiteamsession import MultiTeamSession + +if TYPE_CHECKING: + import ba + + +class DualTeamSession(MultiTeamSession): + """ba.Session type for teams mode games. + + Category: Gameplay Classes + """ + + # Base class overrides: + use_teams = True + use_team_colors = True + + _playlist_selection_var = 'Team Tournament Playlist Selection' + _playlist_randomize_var = 'Team Tournament Playlist Randomize' + _playlists_var = 'Team Tournament Playlists' + + def __init__(self) -> None: + _ba.increment_analytics_count('Teams session start') + super().__init__() + + def _switch_to_score_screen(self, results: ba.GameResults) -> None: + # pylint: disable=cyclic-import + from bastd.activity.drawscore import DrawScoreScreenActivity + from bastd.activity.dualteamscore import ( + TeamVictoryScoreScreenActivity) + from bastd.activity.multiteamvictory import ( + TeamSeriesVictoryScoreScreenActivity) + winnergroups = results.winnergroups + + # If everyone has the same score, call it a draw. + if len(winnergroups) < 2: + self.setactivity(_ba.newactivity(DrawScoreScreenActivity)) + else: + winner = winnergroups[0].teams[0] + winner.customdata['score'] += 1 + + # If a team has won, show final victory screen. + if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1: + self.setactivity( + _ba.newactivity(TeamSeriesVictoryScoreScreenActivity, + {'winner': winner})) + else: + self.setactivity( + _ba.newactivity(TeamVictoryScoreScreenActivity, + {'winner': winner})) diff --git a/dist/ba_data/python/ba/_enums.py b/dist/ba_data/python/ba/_enums.py new file mode 100644 index 0000000..7940bcf --- /dev/null +++ b/dist/ba_data/python/ba/_enums.py @@ -0,0 +1,198 @@ +# Released under the MIT License. See LICENSE for details. +"""Enums generated by tools/update_python_enums_module in ba-internal.""" + +from enum import Enum + + +class InputType(Enum): + """Types of input a controller can send to the game. + + Category: Enums + + """ + UP_DOWN = 2 + LEFT_RIGHT = 3 + JUMP_PRESS = 4 + JUMP_RELEASE = 5 + PUNCH_PRESS = 6 + PUNCH_RELEASE = 7 + BOMB_PRESS = 8 + BOMB_RELEASE = 9 + PICK_UP_PRESS = 10 + PICK_UP_RELEASE = 11 + RUN = 12 + FLY_PRESS = 13 + FLY_RELEASE = 14 + START_PRESS = 15 + START_RELEASE = 16 + HOLD_POSITION_PRESS = 17 + HOLD_POSITION_RELEASE = 18 + LEFT_PRESS = 19 + LEFT_RELEASE = 20 + RIGHT_PRESS = 21 + RIGHT_RELEASE = 22 + UP_PRESS = 23 + UP_RELEASE = 24 + DOWN_PRESS = 25 + DOWN_RELEASE = 26 + + +class UIScale(Enum): + """The overall scale the UI is being rendered for. Note that this is + independent of pixel resolution. For example, a phone and a desktop PC + might render the game at similar pixel resolutions but the size they + display content at will vary significantly. + + Category: Enums + + 'large' is used for devices such as desktop PCs where fine details can + be clearly seen. UI elements are generally smaller on the screen + and more content can be seen at once. + + 'medium' is used for devices such as tablets, TVs, or VR headsets. + This mode strikes a balance between clean readability and amount of + content visible. + + 'small' is used primarily for phones or other small devices where + content needs to be presented as large and clear in order to remain + readable from an average distance. + """ + LARGE = 0 + MEDIUM = 1 + SMALL = 2 + + +class TimeType(Enum): + """Specifies the type of time for various operations to target/use. + + Category: Enums + + 'sim' time is the local simulation time for an activity or session. + It can proceed at different rates depending on game speed, stops + for pauses, etc. + + 'base' is the baseline time for an activity or session. It proceeds + consistently regardless of game speed or pausing, but may stop during + occurrences such as network outages. + + 'real' time is mostly based on clock time, with a few exceptions. It may + not advance while the app is backgrounded for instance. (the engine + attempts to prevent single large time jumps from occurring) + """ + SIM = 0 + BASE = 1 + REAL = 2 + + +class TimeFormat(Enum): + """Specifies the format time values are provided in. + + Category: Enums + """ + SECONDS = 0 + MILLISECONDS = 1 + + +class Permission(Enum): + """Permissions that can be requested from the OS. + + Category: Enums + """ + STORAGE = 0 + + +class SpecialChar(Enum): + """Special characters the game can print. + + Category: Enums + """ + DOWN_ARROW = 0 + UP_ARROW = 1 + LEFT_ARROW = 2 + RIGHT_ARROW = 3 + TOP_BUTTON = 4 + LEFT_BUTTON = 5 + RIGHT_BUTTON = 6 + BOTTOM_BUTTON = 7 + DELETE = 8 + SHIFT = 9 + BACK = 10 + LOGO_FLAT = 11 + REWIND_BUTTON = 12 + PLAY_PAUSE_BUTTON = 13 + FAST_FORWARD_BUTTON = 14 + DPAD_CENTER_BUTTON = 15 + OUYA_BUTTON_O = 16 + OUYA_BUTTON_U = 17 + OUYA_BUTTON_Y = 18 + OUYA_BUTTON_A = 19 + OUYA_LOGO = 20 + LOGO = 21 + TICKET = 22 + GOOGLE_PLAY_GAMES_LOGO = 23 + GAME_CENTER_LOGO = 24 + DICE_BUTTON1 = 25 + DICE_BUTTON2 = 26 + DICE_BUTTON3 = 27 + DICE_BUTTON4 = 28 + GAME_CIRCLE_LOGO = 29 + PARTY_ICON = 30 + TEST_ACCOUNT = 31 + TICKET_BACKING = 32 + TROPHY1 = 33 + TROPHY2 = 34 + TROPHY3 = 35 + TROPHY0A = 36 + TROPHY0B = 37 + TROPHY4 = 38 + LOCAL_ACCOUNT = 39 + ALIBABA_LOGO = 40 + FLAG_UNITED_STATES = 41 + FLAG_MEXICO = 42 + FLAG_GERMANY = 43 + FLAG_BRAZIL = 44 + FLAG_RUSSIA = 45 + FLAG_CHINA = 46 + FLAG_UNITED_KINGDOM = 47 + FLAG_CANADA = 48 + FLAG_INDIA = 49 + FLAG_JAPAN = 50 + FLAG_FRANCE = 51 + FLAG_INDONESIA = 52 + FLAG_ITALY = 53 + FLAG_SOUTH_KOREA = 54 + FLAG_NETHERLANDS = 55 + FEDORA = 56 + HAL = 57 + CROWN = 58 + YIN_YANG = 59 + EYE_BALL = 60 + SKULL = 61 + HEART = 62 + DRAGON = 63 + HELMET = 64 + MUSHROOM = 65 + NINJA_STAR = 66 + VIKING_HELMET = 67 + MOON = 68 + SPIDER = 69 + FIREBALL = 70 + FLAG_UNITED_ARAB_EMIRATES = 71 + FLAG_QATAR = 72 + FLAG_EGYPT = 73 + FLAG_KUWAIT = 74 + FLAG_ALGERIA = 75 + FLAG_SAUDI_ARABIA = 76 + FLAG_MALAYSIA = 77 + FLAG_CZECH_REPUBLIC = 78 + FLAG_AUSTRALIA = 79 + FLAG_SINGAPORE = 80 + OCULUS_LOGO = 81 + STEAM_LOGO = 82 + NVIDIA_LOGO = 83 + FLAG_IRAN = 84 + FLAG_POLAND = 85 + FLAG_ARGENTINA = 86 + FLAG_PHILIPPINES = 87 + FLAG_CHILE = 88 + MIKIROG = 89 diff --git a/dist/ba_data/python/ba/_error.py b/dist/ba_data/python/ba/_error.py new file mode 100644 index 0000000..ea98f3f --- /dev/null +++ b/dist/ba_data/python/ba/_error.py @@ -0,0 +1,193 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Error related functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Any, List + import ba + + +class DependencyError(Exception): + """Exception raised when one or more ba.Dependency items are missing. + + category: Exception Classes + + (this will generally be missing assets). + """ + + def __init__(self, deps: List[ba.Dependency]): + super().__init__() + self._deps = deps + + @property + def deps(self) -> List[ba.Dependency]: + """The list of missing dependencies causing this error.""" + return self._deps + + +class ContextError(Exception): + """Exception raised when a call is made in an invalid context. + + category: Exception Classes + + Examples of this include calling UI functions within an Activity context + or calling scene manipulation functions outside of a game context. + """ + + +class NotFoundError(Exception): + """Exception raised when a referenced object does not exist. + + category: Exception Classes + """ + + +class PlayerNotFoundError(NotFoundError): + """Exception raised when an expected ba.Player does not exist. + + category: Exception Classes + """ + + +class SessionPlayerNotFoundError(NotFoundError): + """Exception raised when an expected ba.SessionPlayer does not exist. + + category: Exception Classes + """ + + +class TeamNotFoundError(NotFoundError): + """Exception raised when an expected ba.Team does not exist. + + category: Exception Classes + """ + + +class DelegateNotFoundError(NotFoundError): + """Exception raised when an expected delegate object does not exist. + + category: Exception Classes + """ + + +class SessionTeamNotFoundError(NotFoundError): + """Exception raised when an expected ba.SessionTeam does not exist. + + category: Exception Classes + """ + + +class NodeNotFoundError(NotFoundError): + """Exception raised when an expected ba.Node does not exist. + + category: Exception Classes + """ + + +class ActorNotFoundError(NotFoundError): + """Exception raised when an expected ba.Actor does not exist. + + category: Exception Classes + """ + + +class ActivityNotFoundError(NotFoundError): + """Exception raised when an expected ba.Activity does not exist. + + category: Exception Classes + """ + + +class SessionNotFoundError(NotFoundError): + """Exception raised when an expected ba.Session does not exist. + + category: Exception Classes + """ + + +class InputDeviceNotFoundError(NotFoundError): + """Exception raised when an expected ba.InputDevice does not exist. + + category: Exception Classes + """ + + +class WidgetNotFoundError(NotFoundError): + """Exception raised when an expected ba.Widget does not exist. + + category: Exception Classes + """ + + +def print_exception(*args: Any, **keywds: Any) -> None: + """Print info about an exception along with pertinent context state. + + category: General Utility Functions + + Prints all arguments provided along with various info about the + current context and the outstanding exception. + Pass the keyword 'once' as True if you want the call to only happen + one time from an exact calling location. + """ + import traceback + if keywds: + allowed_keywds = ['once'] + if any(keywd not in allowed_keywds for keywd in keywds): + raise TypeError('invalid keyword(s)') + try: + # If we're only printing once and already have, bail. + if keywds.get('once', False): + if not _ba.do_once(): + return + + err_str = ' '.join([str(a) for a in args]) + print('ERROR:', err_str) + _ba.print_context() + print('PRINTED-FROM:') + + # Basically the output of traceback.print_stack() + stackstr = ''.join(traceback.format_stack()) + print(stackstr, end='') + print('EXCEPTION:') + + # Basically the output of traceback.print_exc() + excstr = traceback.format_exc() + print('\n'.join(' ' + l for l in excstr.splitlines())) + except Exception: + # I suppose using print_exception here would be a bad idea. + print('ERROR: exception in ba.print_exception():') + traceback.print_exc() + + +def print_error(err_str: str, once: bool = False) -> None: + """Print info about an error along with pertinent context state. + + category: General Utility Functions + + Prints all positional arguments provided along with various info about the + current context. + Pass the keyword 'once' as True if you want the call to only happen + one time from an exact calling location. + """ + import traceback + try: + # If we're only printing once and already have, bail. + if once: + if not _ba.do_once(): + return + + print('ERROR:', err_str) + _ba.print_context() + + # Basically the output of traceback.print_stack() + stackstr = ''.join(traceback.format_stack()) + print(stackstr, end='') + except Exception: + print('ERROR: exception in ba.print_error():') + traceback.print_exc() diff --git a/dist/ba_data/python/ba/_freeforallsession.py b/dist/ba_data/python/ba/_freeforallsession.py new file mode 100644 index 0000000..e81eb47 --- /dev/null +++ b/dist/ba_data/python/ba/_freeforallsession.py @@ -0,0 +1,97 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to free-for-all sessions.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +from ba._multiteamsession import MultiTeamSession + +if TYPE_CHECKING: + from typing import Dict + import ba + + +class FreeForAllSession(MultiTeamSession): + """ba.Session type for free-for-all mode games. + + Category: Gameplay Classes + """ + use_teams = False + use_team_colors = False + _playlist_selection_var = 'Free-for-All Playlist Selection' + _playlist_randomize_var = 'Free-for-All Playlist Randomize' + _playlists_var = 'Free-for-All Playlists' + + def get_ffa_point_awards(self) -> Dict[int, int]: + """Return the number of points awarded for different rankings. + + This is based on the current number of players. + """ + point_awards: Dict[int, int] + if len(self.sessionplayers) == 1: + point_awards = {} + elif len(self.sessionplayers) == 2: + point_awards = {0: 6} + elif len(self.sessionplayers) == 3: + point_awards = {0: 6, 1: 3} + elif len(self.sessionplayers) == 4: + point_awards = {0: 8, 1: 4, 2: 2} + elif len(self.sessionplayers) == 5: + point_awards = {0: 8, 1: 4, 2: 2} + elif len(self.sessionplayers) == 6: + point_awards = {0: 8, 1: 4, 2: 2} + else: + point_awards = {0: 8, 1: 4, 2: 2, 3: 1} + return point_awards + + def __init__(self) -> None: + _ba.increment_analytics_count('Free-for-all session start') + super().__init__() + + def _switch_to_score_screen(self, results: ba.GameResults) -> None: + # pylint: disable=cyclic-import + from efro.util import asserttype + from bastd.activity.drawscore import DrawScoreScreenActivity + from bastd.activity.multiteamvictory import ( + TeamSeriesVictoryScoreScreenActivity) + from bastd.activity.freeforallvictory import ( + FreeForAllVictoryScoreScreenActivity) + winners = results.winnergroups + + # If there's multiple players and everyone has the same score, + # call it a draw. + if len(self.sessionplayers) > 1 and len(winners) < 2: + self.setactivity( + _ba.newactivity(DrawScoreScreenActivity, {'results': results})) + else: + # Award different point amounts based on number of players. + point_awards = self.get_ffa_point_awards() + + for i, winner in enumerate(winners): + for team in winner.teams: + points = (point_awards[i] if i in point_awards else 0) + team.customdata['previous_score'] = ( + team.customdata['score']) + team.customdata['score'] += points + + series_winners = [ + team for team in self.sessionteams + if team.customdata['score'] >= self._ffa_series_length + ] + series_winners.sort( + reverse=True, + key=lambda t: asserttype(t.customdata['score'], int)) + if (len(series_winners) == 1 + or (len(series_winners) > 1 + and series_winners[0].customdata['score'] != + series_winners[1].customdata['score'])): + self.setactivity( + _ba.newactivity(TeamSeriesVictoryScoreScreenActivity, + {'winner': series_winners[0]})) + else: + self.setactivity( + _ba.newactivity(FreeForAllVictoryScoreScreenActivity, + {'results': results})) diff --git a/dist/ba_data/python/ba/_gameactivity.py b/dist/ba_data/python/ba/_gameactivity.py new file mode 100644 index 0000000..1c1e7fb --- /dev/null +++ b/dist/ba_data/python/ba/_gameactivity.py @@ -0,0 +1,1165 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides GameActivity class.""" +# pylint: disable=too-many-lines + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING, TypeVar + +import _ba +from ba._activity import Activity +from ba._score import ScoreConfig +from ba._language import Lstr +from ba._messages import PlayerDiedMessage, StandMessage +from ba._error import NotFoundError, print_error, print_exception +from ba._general import Call, WeakCall +from ba._player import PlayerInfo +from ba import _map + +if TYPE_CHECKING: + from typing import (List, Optional, Dict, Type, Any, Callable, Sequence, + Tuple, Union) + from bastd.actor.playerspaz import PlayerSpaz + from bastd.actor.bomb import TNTSpawner + import ba + +PlayerType = TypeVar('PlayerType', bound='ba.Player') +TeamType = TypeVar('TeamType', bound='ba.Team') + + +class GameActivity(Activity[PlayerType, TeamType]): + """Common base class for all game ba.Activities. + + category: Gameplay Classes + """ + # pylint: disable=too-many-public-methods + + # Tips to be presented to the user at the start of the game. + tips: List[Union[str, ba.GameTip]] = [] + + # Default getname() will return this if not None. + name: Optional[str] = None + + # Default get_description() will return this if not None. + description: Optional[str] = None + + # Default get_available_settings() will return this if not None. + available_settings: Optional[List[ba.Setting]] = None + + # Default getscoreconfig() will return this if not None. + scoreconfig: Optional[ba.ScoreConfig] = None + + # Override some defaults. + allow_pausing = True + allow_kick_idle_players = True + + # Whether to show points for kills. + show_kill_points = True + + # If not None, the music type that should play in on_transition_in() + # (unless overridden by the map). + default_music: Optional[ba.MusicType] = None + + @classmethod + def create_settings_ui( + cls, + sessiontype: Type[ba.Session], + settings: Optional[dict], + completion_call: Callable[[Optional[dict]], None], + ) -> None: + """Launch an in-game UI to configure settings for a game type. + + 'sessiontype' should be the ba.Session class the game will be used in. + + 'settings' should be an existing settings dict (implies 'edit' + ui mode) or None (implies 'add' ui mode). + + 'completion_call' will be called with a filled-out settings dict on + success or None on cancel. + + Generally subclasses don't need to override this; if they override + ba.GameActivity.get_available_settings() and + ba.GameActivity.get_supported_maps() they can just rely on + the default implementation here which calls those methods. + """ + delegate = _ba.app.delegate + assert delegate is not None + delegate.create_default_game_settings_ui(cls, sessiontype, settings, + completion_call) + + @classmethod + def getscoreconfig(cls) -> ba.ScoreConfig: + """Return info about game scoring setup; can be overridden by games.""" + return (cls.scoreconfig + if cls.scoreconfig is not None else ScoreConfig()) + + @classmethod + def getname(cls) -> str: + """Return a str name for this game type. + + This default implementation simply returns the 'name' class attr. + """ + return cls.name if cls.name is not None else 'Untitled Game' + + @classmethod + def get_display_string(cls, settings: Optional[Dict] = None) -> ba.Lstr: + """Return a descriptive name for this game/settings combo. + + Subclasses should override getname(); not this. + """ + name = Lstr(translate=('gameNames', cls.getname())) + + # A few substitutions for 'Epic', 'Solo' etc. modes. + # FIXME: Should provide a way for game types to define filters of + # their own and should not rely on hard-coded settings names. + if settings is not None: + if 'Solo Mode' in settings and settings['Solo Mode']: + name = Lstr(resource='soloNameFilterText', + subs=[('${NAME}', name)]) + if 'Epic Mode' in settings and settings['Epic Mode']: + name = Lstr(resource='epicNameFilterText', + subs=[('${NAME}', name)]) + + return name + + @classmethod + def get_team_display_string(cls, name: str) -> ba.Lstr: + """Given a team name, returns a localized version of it.""" + return Lstr(translate=('teamNames', name)) + + @classmethod + def get_description(cls, sessiontype: Type[ba.Session]) -> str: + """Get a str description of this game type. + + The default implementation simply returns the 'description' class var. + Classes which want to change their description depending on the session + can override this method. + """ + del sessiontype # Unused arg. + return cls.description if cls.description is not None else '' + + @classmethod + def get_description_display_string( + cls, sessiontype: Type[ba.Session]) -> ba.Lstr: + """Return a translated version of get_description(). + + Sub-classes should override get_description(); not this. + """ + description = cls.get_description(sessiontype) + return Lstr(translate=('gameDescriptions', description)) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + """Return a list of settings relevant to this game type when + running under the provided session type. + """ + del sessiontype # Unused arg. + return [] if cls.available_settings is None else cls.available_settings + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + """ + Called by the default ba.GameActivity.create_settings_ui() + implementation; should return a list of map names valid + for this game-type for the given ba.Session type. + """ + del sessiontype # Unused arg. + return _map.getmaps('melee') + + @classmethod + def get_settings_display_string(cls, config: Dict[str, Any]) -> ba.Lstr: + """Given a game config dict, return a short description for it. + + This is used when viewing game-lists or showing what game + is up next in a series. + """ + name = cls.get_display_string(config['settings']) + + # In newer configs, map is in settings; it used to be in the + # config root. + if 'map' in config['settings']: + sval = Lstr(value='${NAME} @ ${MAP}', + subs=[('${NAME}', name), + ('${MAP}', + _map.get_map_display_string( + _map.get_filtered_map_name( + config['settings']['map'])))]) + elif 'map' in config: + sval = Lstr(value='${NAME} @ ${MAP}', + subs=[('${NAME}', name), + ('${MAP}', + _map.get_map_display_string( + _map.get_filtered_map_name(config['map']))) + ]) + else: + print('invalid game config - expected map entry under settings') + sval = Lstr(value='???') + return sval + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + """Return whether this game supports the provided Session type.""" + from ba._multiteamsession import MultiTeamSession + + # By default, games support any versus mode + return issubclass(sessiontype, MultiTeamSession) + + def __init__(self, settings: dict): + """Instantiate the Activity.""" + super().__init__(settings) + + # Holds some flattened info about the player set at the point + # when on_begin() is called. + self.initialplayerinfos: Optional[List[ba.PlayerInfo]] = None + + # Go ahead and get our map loading. + self._map_type = _map.get_map_class(self._calc_map_name(settings)) + + self._spawn_sound = _ba.getsound('spawn') + self._map_type.preload() + self._map: Optional[ba.Map] = None + self._powerup_drop_timer: Optional[ba.Timer] = None + self._tnt_spawners: Optional[Dict[int, TNTSpawner]] = None + self._tnt_drop_timer: Optional[ba.Timer] = None + self._game_scoreboard_name_text: Optional[ba.Actor] = None + self._game_scoreboard_description_text: Optional[ba.Actor] = None + self._standard_time_limit_time: Optional[int] = None + self._standard_time_limit_timer: Optional[ba.Timer] = None + self._standard_time_limit_text: Optional[ba.NodeActor] = None + self._standard_time_limit_text_input: Optional[ba.NodeActor] = None + self._tournament_time_limit: Optional[int] = None + self._tournament_time_limit_timer: Optional[ba.Timer] = None + self._tournament_time_limit_title_text: Optional[ba.NodeActor] = None + self._tournament_time_limit_text: Optional[ba.NodeActor] = None + self._tournament_time_limit_text_input: Optional[ba.NodeActor] = None + self._zoom_message_times: Dict[int, float] = {} + self._is_waiting_for_continue = False + + self._continue_cost = _ba.get_account_misc_read_val( + 'continueStartCost', 25) + self._continue_cost_mult = _ba.get_account_misc_read_val( + 'continuesMult', 2) + self._continue_cost_offset = _ba.get_account_misc_read_val( + 'continuesOffset', 0) + + @property + def map(self) -> ba.Map: + """The map being used for this game. + + Raises a ba.NotFoundError if the map does not currently exist. + """ + if self._map is None: + raise NotFoundError + return self._map + + def get_instance_display_string(self) -> ba.Lstr: + """Return a name for this particular game instance.""" + return self.get_display_string(self.settings_raw) + + def get_instance_scoreboard_display_string(self) -> ba.Lstr: + """Return a name for this particular game instance. + + This name is used above the game scoreboard in the corner + of the screen, so it should be as concise as possible. + """ + # If we're in a co-op session, use the level name. + # FIXME: Should clean this up. + try: + from ba._coopsession import CoopSession + if isinstance(self.session, CoopSession): + campaign = self.session.campaign + assert campaign is not None + return campaign.getlevel( + self.session.campaign_level_name).displayname + except Exception: + print_error('error getting campaign level name') + return self.get_instance_display_string() + + def get_instance_description(self) -> Union[str, Sequence]: + """Return a description for this game instance, in English. + + This is shown in the center of the screen below the game name at the + start of a game. It should start with a capital letter and end with a + period, and can be a bit more verbose than the version returned by + get_instance_description_short(). + + Note that translation is applied by looking up the specific returned + value as a key, so the number of returned variations should be limited; + ideally just one or two. To include arbitrary values in the + description, you can return a sequence of values in the following + form instead of just a string: + + # This will give us something like 'Score 3 goals.' in English + # and can properly translate to 'Anota 3 goles.' in Spanish. + # If we just returned the string 'Score 3 Goals' here, there would + # have to be a translation entry for each specific number. ew. + return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']] + + This way the first string can be consistently translated, with any arg + values then substituted into the result. ${ARG1} will be replaced with + the first value, ${ARG2} with the second, etc. + """ + return self.get_description(type(self.session)) + + def get_instance_description_short(self) -> Union[str, Sequence]: + """Return a short description for this game instance in English. + + This description is used above the game scoreboard in the + corner of the screen, so it should be as concise as possible. + It should be lowercase and should not contain periods or other + punctuation. + + Note that translation is applied by looking up the specific returned + value as a key, so the number of returned variations should be limited; + ideally just one or two. To include arbitrary values in the + description, you can return a sequence of values in the following form + instead of just a string: + + # This will give us something like 'score 3 goals' in English + # and can properly translate to 'anota 3 goles' in Spanish. + # If we just returned the string 'score 3 goals' here, there would + # have to be a translation entry for each specific number. ew. + return ['score ${ARG1} goals', self.settings_raw['Score to Win']] + + This way the first string can be consistently translated, with any arg + values then substituted into the result. ${ARG1} will be replaced + with the first value, ${ARG2} with the second, etc. + + """ + return '' + + def on_transition_in(self) -> None: + super().on_transition_in() + + # Make our map. + self._map = self._map_type() + + # Give our map a chance to override the music. + # (for happy-thoughts and other such themed maps) + map_music = self._map_type.get_music_type() + music = map_music if map_music is not None else self.default_music + + if music is not None: + from ba import _music + _music.setmusic(music) + + def on_continue(self) -> None: + """ + This is called if a game supports and offers a continue and the player + accepts. In this case the player should be given an extra life or + whatever is relevant to keep the game going. + """ + + def _continue_choice(self, do_continue: bool) -> None: + self._is_waiting_for_continue = False + if self.has_ended(): + return + with _ba.Context(self): + if do_continue: + _ba.playsound(_ba.getsound('shieldUp')) + _ba.playsound(_ba.getsound('cashRegister')) + _ba.add_transaction({ + 'type': 'CONTINUE', + 'cost': self._continue_cost + }) + _ba.run_transactions() + self._continue_cost = ( + self._continue_cost * self._continue_cost_mult + + self._continue_cost_offset) + self.on_continue() + else: + self.end_game() + + def is_waiting_for_continue(self) -> bool: + """Returns whether or not this activity is currently waiting for the + player to continue (or timeout)""" + return self._is_waiting_for_continue + + def continue_or_end_game(self) -> None: + """If continues are allowed, prompts the player to purchase a continue + and calls either end_game or continue_game depending on the result""" + # pylint: disable=too-many-nested-blocks + # pylint: disable=cyclic-import + from bastd.ui.continues import ContinuesWindow + from ba._coopsession import CoopSession + from ba._enums import TimeType + + try: + if _ba.get_account_misc_read_val('enableContinues', False): + session = self.session + + # We only support continuing in non-tournament games. + tournament_id = session.tournament_id + if tournament_id is None: + + # We currently only support continuing in sequential + # co-op campaigns. + if isinstance(session, CoopSession): + assert session.campaign is not None + if session.campaign.sequential: + gnode = self.globalsnode + + # Only attempt this if we're not currently paused + # and there appears to be no UI. + if (not gnode.paused + and not _ba.app.ui.has_main_menu_window()): + self._is_waiting_for_continue = True + with _ba.Context('ui'): + _ba.timer( + 0.5, + lambda: ContinuesWindow( + self, + self._continue_cost, + continue_call=WeakCall( + self._continue_choice, True), + cancel_call=WeakCall( + self._continue_choice, False)), + timetype=TimeType.REAL) + return + + except Exception: + print_exception('Error handling continues.') + + self.end_game() + + def on_begin(self) -> None: + from ba._analytics import game_begin_analytics + super().on_begin() + + game_begin_analytics() + + # We don't do this in on_transition_in because it may depend on + # players/teams which aren't available until now. + _ba.timer(0.001, self._show_scoreboard_info) + _ba.timer(1.0, self._show_info) + _ba.timer(2.5, self._show_tip) + + # Store some basic info about players present at start time. + self.initialplayerinfos = [ + PlayerInfo(name=p.getname(full=True), character=p.character) + for p in self.players + ] + + # Sort this by name so high score lists/etc will be consistent + # regardless of player join order. + self.initialplayerinfos.sort(key=lambda x: x.name) + + # If this is a tournament, query info about it such as how much + # time is left. + tournament_id = self.session.tournament_id + if tournament_id is not None: + _ba.tournament_query( + args={ + 'tournamentIDs': [tournament_id], + 'source': 'in-game time remaining query' + }, + callback=WeakCall(self._on_tournament_query_response), + ) + + def _on_tournament_query_response(self, data: Optional[Dict[str, + Any]]) -> None: + if data is not None: + data_t = data['t'] # This used to be the whole payload. + + # Keep our cached tourney info up to date + _ba.app.accounts.cache_tournament_info(data_t) + self._setup_tournament_time_limit( + max(5, data_t[0]['timeRemaining'])) + + def on_player_join(self, player: PlayerType) -> None: + super().on_player_join(player) + + # By default, just spawn a dude. + self.spawn_player(player) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, PlayerDiedMessage): + # pylint: disable=cyclic-import + from bastd.actor.spaz import Spaz + + player = msg.getplayer(self.playertype) + killer = msg.getkillerplayer(self.playertype) + + # Inform our stats of the demise. + self.stats.player_was_killed(player, + killed=msg.killed, + killer=killer) + + # Award the killer points if he's on a different team. + # FIXME: This should not be linked to Spaz actors. + # (should move get_death_points to Actor or make it a message) + if killer and killer.team is not player.team: + assert isinstance(killer.actor, Spaz) + pts, importance = killer.actor.get_death_points(msg.how) + if not self.has_ended(): + self.stats.player_scored(killer, + pts, + kill=True, + victim_player=player, + importance=importance, + showpoints=self.show_kill_points) + else: + return super().handlemessage(msg) + return None + + def _show_scoreboard_info(self) -> None: + """Create the game info display. + + This is the thing in the top left corner showing the name + and short description of the game. + """ + # pylint: disable=too-many-locals + from ba._freeforallsession import FreeForAllSession + from ba._gameutils import animate + from ba._nodeactor import NodeActor + sb_name = self.get_instance_scoreboard_display_string() + + # The description can be either a string or a sequence with args + # to swap in post-translation. + sb_desc_in = self.get_instance_description_short() + sb_desc_l: Sequence + if isinstance(sb_desc_in, str): + sb_desc_l = [sb_desc_in] # handle simple string case + else: + sb_desc_l = sb_desc_in + if not isinstance(sb_desc_l[0], str): + raise TypeError('Invalid format for instance description.') + + is_empty = (sb_desc_l[0] == '') + subs = [] + for i in range(len(sb_desc_l) - 1): + subs.append(('${ARG' + str(i + 1) + '}', str(sb_desc_l[i + 1]))) + translation = Lstr(translate=('gameDescriptions', sb_desc_l[0]), + subs=subs) + sb_desc = translation + vrmode = _ba.app.vr_mode + yval = -34 if is_empty else -20 + yval -= 16 + sbpos = ((15, yval) if isinstance(self.session, FreeForAllSession) else + (15, yval)) + self._game_scoreboard_name_text = NodeActor( + _ba.newnode('text', + attrs={ + 'text': sb_name, + 'maxwidth': 300, + 'position': sbpos, + 'h_attach': 'left', + 'vr_depth': 10, + 'v_attach': 'top', + 'v_align': 'bottom', + 'color': (1.0, 1.0, 1.0, 1.0), + 'shadow': 1.0 if vrmode else 0.6, + 'flatness': 1.0 if vrmode else 0.5, + 'scale': 1.1 + })) + + assert self._game_scoreboard_name_text.node + animate(self._game_scoreboard_name_text.node, 'opacity', { + 0: 0.0, + 1.0: 1.0 + }) + + descpos = (((17, -44 + + 10) if isinstance(self.session, FreeForAllSession) else + (17, -44 + 10))) + self._game_scoreboard_description_text = NodeActor( + _ba.newnode( + 'text', + attrs={ + 'text': sb_desc, + 'maxwidth': 480, + 'position': descpos, + 'scale': 0.7, + 'h_attach': 'left', + 'v_attach': 'top', + 'v_align': 'top', + 'shadow': 1.0 if vrmode else 0.7, + 'flatness': 1.0 if vrmode else 0.8, + 'color': (1, 1, 1, 1) if vrmode else (0.9, 0.9, 0.9, 1.0) + })) + + assert self._game_scoreboard_description_text.node + animate(self._game_scoreboard_description_text.node, 'opacity', { + 0: 0.0, + 1.0: 1.0 + }) + + def _show_info(self) -> None: + """Show the game description.""" + from ba._gameutils import animate + from bastd.actor.zoomtext import ZoomText + name = self.get_instance_display_string() + ZoomText(name, + maxwidth=800, + lifespan=2.5, + jitter=2.0, + position=(0, 180), + flash=False, + color=(0.93 * 1.25, 0.9 * 1.25, 1.0 * 1.25), + trailcolor=(0.15, 0.05, 1.0, 0.0)).autoretain() + _ba.timer(0.2, Call(_ba.playsound, _ba.getsound('gong'))) + + # The description can be either a string or a sequence with args + # to swap in post-translation. + desc_in = self.get_instance_description() + desc_l: Sequence + if isinstance(desc_in, str): + desc_l = [desc_in] # handle simple string case + else: + desc_l = desc_in + if not isinstance(desc_l[0], str): + raise TypeError('Invalid format for instance description') + subs = [] + for i in range(len(desc_l) - 1): + subs.append(('${ARG' + str(i + 1) + '}', str(desc_l[i + 1]))) + translation = Lstr(translate=('gameDescriptions', desc_l[0]), + subs=subs) + + # Do some standard filters (epic mode, etc). + if self.settings_raw.get('Epic Mode', False): + translation = Lstr(resource='epicDescriptionFilterText', + subs=[('${DESCRIPTION}', translation)]) + vrmode = _ba.app.vr_mode + dnode = _ba.newnode('text', + attrs={ + 'v_attach': 'center', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 1, 1), + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': 1.0 if vrmode else 0.5, + 'vr_depth': -30, + 'position': (0, 80), + 'scale': 1.2, + 'maxwidth': 700, + 'text': translation + }) + cnode = _ba.newnode('combine', + owner=dnode, + attrs={ + 'input0': 1.0, + 'input1': 1.0, + 'input2': 1.0, + 'size': 4 + }) + cnode.connectattr('output', dnode, 'color') + keys = {0.5: 0, 1.0: 1.0, 2.5: 1.0, 4.0: 0.0} + animate(cnode, 'input3', keys) + _ba.timer(4.0, dnode.delete) + + def _show_tip(self) -> None: + # pylint: disable=too-many-locals + from ba._gameutils import animate, GameTip + from ba._enums import SpecialChar + + # If there's any tips left on the list, display one. + if self.tips: + tip = self.tips.pop(random.randrange(len(self.tips))) + tip_title = Lstr(value='${A}:', + subs=[('${A}', Lstr(resource='tipText'))]) + icon: Optional[ba.Texture] = None + sound: Optional[ba.Sound] = None + if isinstance(tip, GameTip): + icon = tip.icon + sound = tip.sound + tip = tip.text + assert isinstance(tip, str) + + # Do a few substitutions. + tip_lstr = Lstr(translate=('tips', tip), + subs=[('${PICKUP}', + _ba.charstr(SpecialChar.TOP_BUTTON))]) + base_position = (75, 50) + tip_scale = 0.8 + tip_title_scale = 1.2 + vrmode = _ba.app.vr_mode + + t_offs = -350.0 + tnode = _ba.newnode('text', + attrs={ + 'text': tip_lstr, + 'scale': tip_scale, + 'maxwidth': 900, + 'position': (base_position[0] + t_offs, + base_position[1]), + 'h_align': 'left', + 'vr_depth': 300, + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': 1.0 if vrmode else 0.5, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + t2pos = (base_position[0] + t_offs - (20 if icon is None else 82), + base_position[1] + 2) + t2node = _ba.newnode('text', + owner=tnode, + attrs={ + 'text': tip_title, + 'scale': tip_title_scale, + 'position': t2pos, + 'h_align': 'right', + 'vr_depth': 300, + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': 1.0 if vrmode else 0.5, + 'maxwidth': 140, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + if icon is not None: + ipos = (base_position[0] + t_offs - 40, base_position[1] + 1) + img = _ba.newnode('image', + attrs={ + 'texture': icon, + 'position': ipos, + 'scale': (50, 50), + 'opacity': 1.0, + 'vr_depth': 315, + 'color': (1, 1, 1), + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) + _ba.timer(5.0, img.delete) + if sound is not None: + _ba.playsound(sound) + + combine = _ba.newnode('combine', + owner=tnode, + attrs={ + 'input0': 1.0, + 'input1': 0.8, + 'input2': 1.0, + 'size': 4 + }) + combine.connectattr('output', tnode, 'color') + combine.connectattr('output', t2node, 'color') + animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) + _ba.timer(5.0, tnode.delete) + + def end(self, + results: Any = None, + delay: float = 0.0, + force: bool = False) -> None: + from ba._gameresults import GameResults + + # If results is a standard team-game-results, associate it with us + # so it can grab our score prefs. + if isinstance(results, GameResults): + results.set_game(self) + + # If we had a standard time-limit that had not expired, stop it so + # it doesnt tick annoyingly. + if (self._standard_time_limit_time is not None + and self._standard_time_limit_time > 0): + self._standard_time_limit_timer = None + self._standard_time_limit_text = None + + # Ditto with tournament time limits. + if (self._tournament_time_limit is not None + and self._tournament_time_limit > 0): + self._tournament_time_limit_timer = None + self._tournament_time_limit_text = None + self._tournament_time_limit_title_text = None + + super().end(results, delay, force) + + def end_game(self) -> None: + """Tell the game to wrap up and call ba.Activity.end() immediately. + + This method should be overridden by subclasses. A game should always + be prepared to end and deliver results, even if there is no 'winner' + yet; this way things like the standard time-limit + (ba.GameActivity.setup_standard_time_limit()) will work with the game. + """ + print('WARNING: default end_game() implementation called;' + ' your game should override this.') + + def respawn_player(self, + player: PlayerType, + respawn_time: Optional[float] = None) -> None: + """ + Given a ba.Player, sets up a standard respawn timer, + along with the standard counter display, etc. + At the end of the respawn period spawn_player() will + be called if the Player still exists. + An explicit 'respawn_time' can optionally be provided + (in seconds). + """ + # pylint: disable=cyclic-import + + assert player + if respawn_time is None: + teamsize = len(player.team.players) + if teamsize == 1: + respawn_time = 3.0 + elif teamsize == 2: + respawn_time = 5.0 + elif teamsize == 3: + respawn_time = 6.0 + else: + respawn_time = 7.0 + + # If this standard setting is present, factor it in. + if 'Respawn Times' in self.settings_raw: + respawn_time *= self.settings_raw['Respawn Times'] + + # We want whole seconds. + assert respawn_time is not None + respawn_time = round(max(1.0, respawn_time), 0) + + if player.actor and not self.has_ended(): + from bastd.actor.respawnicon import RespawnIcon + player.customdata['respawn_timer'] = _ba.Timer( + respawn_time, WeakCall(self.spawn_player_if_exists, player)) + player.customdata['respawn_icon'] = RespawnIcon( + player, respawn_time) + + def spawn_player_if_exists(self, player: PlayerType) -> None: + """ + A utility method which calls self.spawn_player() *only* if the + ba.Player provided still exists; handy for use in timers and whatnot. + + There is no need to override this; just override spawn_player(). + """ + if player: + self.spawn_player(player) + + def spawn_player(self, player: PlayerType) -> ba.Actor: + """Spawn *something* for the provided ba.Player. + + The default implementation simply calls spawn_player_spaz(). + """ + assert player # Dead references should never be passed as args. + + return self.spawn_player_spaz(player) + + def spawn_player_spaz(self, + player: PlayerType, + position: Sequence[float] = (0, 0, 0), + angle: float = None) -> PlayerSpaz: + """Create and wire up a ba.PlayerSpaz for the provided ba.Player.""" + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from ba import _math + from ba._gameutils import animate + from ba._coopsession import CoopSession + from bastd.actor.playerspaz import PlayerSpaz + name = player.getname() + color = player.color + highlight = player.highlight + + light_color = _math.normalized_color(color) + display_color = _ba.safecolor(color, target_intensity=0.75) + spaz = PlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + + player.actor = spaz + assert spaz.node + + # If this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls. + # FIXME: Need to generalize this. + if isinstance(self.session, CoopSession) and self.map.getname() in [ + 'Courtyard', 'Tower D' + ]: + mat = self.map.preloaddata['collide_with_wall_material'] + assert isinstance(spaz.node.materials, tuple) + assert isinstance(spaz.node.roller_materials, tuple) + spaz.node.materials += (mat, ) + spaz.node.roller_materials += (mat, ) + + spaz.node.name = name + spaz.node.name_color = display_color + spaz.connect_controls_to_player() + + # Move to the stand position and add a flash of light. + spaz.handlemessage( + StandMessage( + position, + angle if angle is not None else random.uniform(0, 360))) + _ba.playsound(self._spawn_sound, 1, position=spaz.node.position) + light = _ba.newnode('light', attrs={'color': light_color}) + spaz.node.connectattr('position', light, 'position') + animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) + _ba.timer(0.5, light.delete) + return spaz + + def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None: + """Create standard powerup drops for the current map.""" + # pylint: disable=cyclic-import + from bastd.actor.powerupbox import DEFAULT_POWERUP_INTERVAL + self._powerup_drop_timer = _ba.Timer(DEFAULT_POWERUP_INTERVAL, + WeakCall( + self._standard_drop_powerups), + repeat=True) + self._standard_drop_powerups() + if enable_tnt: + self._tnt_spawners = {} + self._setup_standard_tnt_drops() + + def _standard_drop_powerup(self, index: int, expire: bool = True) -> None: + # pylint: disable=cyclic-import + from bastd.actor.powerupbox import PowerupBox, PowerupBoxFactory + PowerupBox( + position=self.map.powerup_spawn_points[index], + poweruptype=PowerupBoxFactory.get().get_random_powerup_type(), + expire=expire).autoretain() + + def _standard_drop_powerups(self) -> None: + """Standard powerup drop.""" + + # Drop one powerup per point. + points = self.map.powerup_spawn_points + for i in range(len(points)): + _ba.timer(i * 0.4, WeakCall(self._standard_drop_powerup, i)) + + def _setup_standard_tnt_drops(self) -> None: + """Standard tnt drop.""" + # pylint: disable=cyclic-import + from bastd.actor.bomb import TNTSpawner + for i, point in enumerate(self.map.tnt_points): + assert self._tnt_spawners is not None + if self._tnt_spawners.get(i) is None: + self._tnt_spawners[i] = TNTSpawner(point) + + def setup_standard_time_limit(self, duration: float) -> None: + """ + Create a standard game time-limit given the provided + duration in seconds. + This will be displayed at the top of the screen. + If the time-limit expires, end_game() will be called. + """ + from ba._nodeactor import NodeActor + if duration <= 0.0: + return + self._standard_time_limit_time = int(duration) + self._standard_time_limit_timer = _ba.Timer( + 1.0, WeakCall(self._standard_time_limit_tick), repeat=True) + self._standard_time_limit_text = NodeActor( + _ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'left', + 'color': (1.0, 1.0, 1.0, 0.5), + 'position': (-25, -30), + 'flatness': 1.0, + 'scale': 0.9 + })) + self._standard_time_limit_text_input = NodeActor( + _ba.newnode('timedisplay', + attrs={ + 'time2': duration * 1000, + 'timemin': 0 + })) + self.globalsnode.connectattr('time', + self._standard_time_limit_text_input.node, + 'time1') + assert self._standard_time_limit_text_input.node + assert self._standard_time_limit_text.node + self._standard_time_limit_text_input.node.connectattr( + 'output', self._standard_time_limit_text.node, 'text') + + def _standard_time_limit_tick(self) -> None: + from ba._gameutils import animate + assert self._standard_time_limit_time is not None + self._standard_time_limit_time -= 1 + if self._standard_time_limit_time <= 10: + if self._standard_time_limit_time == 10: + assert self._standard_time_limit_text is not None + assert self._standard_time_limit_text.node + self._standard_time_limit_text.node.scale = 1.3 + self._standard_time_limit_text.node.position = (-30, -45) + cnode = _ba.newnode('combine', + owner=self._standard_time_limit_text.node, + attrs={'size': 4}) + cnode.connectattr('output', + self._standard_time_limit_text.node, 'color') + animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True) + animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True) + animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) + cnode.input3 = 1.0 + _ba.playsound(_ba.getsound('tick')) + if self._standard_time_limit_time <= 0: + self._standard_time_limit_timer = None + self.end_game() + node = _ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 0.7, 0, 1), + 'position': (0, -90), + 'scale': 1.2, + 'text': Lstr(resource='timeExpiredText') + }) + _ba.playsound(_ba.getsound('refWhistle')) + animate(node, 'scale', {0.0: 0.0, 0.1: 1.4, 0.15: 1.2}) + + def _setup_tournament_time_limit(self, duration: float) -> None: + """ + Create a tournament game time-limit given the provided + duration in seconds. + This will be displayed at the top of the screen. + If the time-limit expires, end_game() will be called. + """ + from ba._nodeactor import NodeActor + from ba._enums import TimeType + if duration <= 0.0: + return + self._tournament_time_limit = int(duration) + + # We want this timer to match the server's time as close as possible, + # so lets go with base-time. Theoretically we should do real-time but + # then we have to mess with contexts and whatnot since its currently + # not available in activity contexts. :-/ + self._tournament_time_limit_timer = _ba.Timer( + 1.0, + WeakCall(self._tournament_time_limit_tick), + repeat=True, + timetype=TimeType.BASE) + self._tournament_time_limit_title_text = NodeActor( + _ba.newnode('text', + attrs={ + 'v_attach': 'bottom', + 'h_attach': 'left', + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 300, + 'maxwidth': 100, + 'color': (1.0, 1.0, 1.0, 0.5), + 'position': (60, 50), + 'flatness': 1.0, + 'scale': 0.5, + 'text': Lstr(resource='tournamentText') + })) + self._tournament_time_limit_text = NodeActor( + _ba.newnode('text', + attrs={ + 'v_attach': 'bottom', + 'h_attach': 'left', + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 300, + 'maxwidth': 100, + 'color': (1.0, 1.0, 1.0, 0.5), + 'position': (60, 30), + 'flatness': 1.0, + 'scale': 0.9 + })) + self._tournament_time_limit_text_input = NodeActor( + _ba.newnode('timedisplay', + attrs={ + 'timemin': 0, + 'time2': self._tournament_time_limit * 1000 + })) + assert self._tournament_time_limit_text.node + assert self._tournament_time_limit_text_input.node + self._tournament_time_limit_text_input.node.connectattr( + 'output', self._tournament_time_limit_text.node, 'text') + + def _tournament_time_limit_tick(self) -> None: + from ba._gameutils import animate + assert self._tournament_time_limit is not None + self._tournament_time_limit -= 1 + if self._tournament_time_limit <= 10: + if self._tournament_time_limit == 10: + assert self._tournament_time_limit_title_text is not None + assert self._tournament_time_limit_title_text.node + assert self._tournament_time_limit_text is not None + assert self._tournament_time_limit_text.node + self._tournament_time_limit_title_text.node.scale = 1.0 + self._tournament_time_limit_text.node.scale = 1.3 + self._tournament_time_limit_title_text.node.position = (80, 85) + self._tournament_time_limit_text.node.position = (80, 60) + cnode = _ba.newnode( + 'combine', + owner=self._tournament_time_limit_text.node, + attrs={'size': 4}) + cnode.connectattr('output', + self._tournament_time_limit_title_text.node, + 'color') + cnode.connectattr('output', + self._tournament_time_limit_text.node, + 'color') + animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True) + animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True) + animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) + cnode.input3 = 1.0 + _ba.playsound(_ba.getsound('tick')) + if self._tournament_time_limit <= 0: + self._tournament_time_limit_timer = None + self.end_game() + tval = Lstr(resource='tournamentTimeExpiredText', + fallback_resource='timeExpiredText') + node = _ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 0.7, 0, 1), + 'position': (0, -200), + 'scale': 1.6, + 'text': tval + }) + _ba.playsound(_ba.getsound('refWhistle')) + animate(node, 'scale', {0: 0.0, 0.1: 1.4, 0.15: 1.2}) + + # Normally we just connect this to time, but since this is a bit of a + # funky setup we just update it manually once per second. + assert self._tournament_time_limit_text_input is not None + assert self._tournament_time_limit_text_input.node + self._tournament_time_limit_text_input.node.time2 = ( + self._tournament_time_limit * 1000) + + def show_zoom_message(self, + message: ba.Lstr, + color: Sequence[float] = (0.9, 0.4, 0.0), + scale: float = 0.8, + duration: float = 2.0, + trail: bool = False) -> None: + """Zooming text used to announce game names and winners.""" + # pylint: disable=cyclic-import + from bastd.actor.zoomtext import ZoomText + + # Reserve a spot on the screen (in case we get multiple of these so + # they don't overlap). + i = 0 + cur_time = _ba.time() + while True: + if (i not in self._zoom_message_times + or self._zoom_message_times[i] < cur_time): + self._zoom_message_times[i] = cur_time + duration + break + i += 1 + ZoomText(message, + lifespan=duration, + jitter=2.0, + position=(0, 200 - i * 100), + scale=scale, + maxwidth=800, + trail=trail, + color=color).autoretain() + + def _calc_map_name(self, settings: dict) -> str: + map_name: str + if 'map' in settings: + map_name = settings['map'] + else: + # If settings doesn't specify a map, pick a random one from the + # list of supported ones. + unowned_maps = _map.get_unowned_maps() + valid_maps: List[str] = [ + m for m in self.get_supported_maps(type(self.session)) + if m not in unowned_maps + ] + if not valid_maps: + _ba.screenmessage(Lstr(resource='noValidMapsErrorText')) + raise Exception('No valid maps') + map_name = valid_maps[random.randrange(len(valid_maps))] + return map_name diff --git a/dist/ba_data/python/ba/_gameresults.py b/dist/ba_data/python/ba/_gameresults.py new file mode 100644 index 0000000..9885b30 --- /dev/null +++ b/dist/ba_data/python/ba/_gameresults.py @@ -0,0 +1,212 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to game results.""" +from __future__ import annotations + +import copy +import weakref +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from efro.util import asserttype +from ba._team import Team, SessionTeam + +if TYPE_CHECKING: + from weakref import ReferenceType + from typing import Sequence, Tuple, Any, Optional, Dict, List, Union + import ba + + +@dataclass +class WinnerGroup: + """Entry for a winning team or teams calculated by game-results.""" + score: Optional[int] + teams: Sequence[ba.SessionTeam] + + +class GameResults: + """ + Results for a completed game. + + Category: Gameplay Classes + + Upon completion, a game should fill one of these out and pass it to its + ba.Activity.end() call. + """ + + def __init__(self) -> None: + self._game_set = False + self._scores: Dict[int, Tuple[ReferenceType[ba.SessionTeam], + Optional[int]]] = {} + self._sessionteams: Optional[List[ReferenceType[ + ba.SessionTeam]]] = None + self._playerinfos: Optional[List[ba.PlayerInfo]] = None + self._lower_is_better: Optional[bool] = None + self._score_label: Optional[str] = None + self._none_is_winner: Optional[bool] = None + self._scoretype: Optional[ba.ScoreType] = None + + def set_game(self, game: ba.GameActivity) -> None: + """Set the game instance these results are applying to.""" + if self._game_set: + raise RuntimeError('Game set twice for GameResults.') + self._game_set = True + self._sessionteams = [ + weakref.ref(team.sessionteam) for team in game.teams + ] + scoreconfig = game.getscoreconfig() + self._playerinfos = copy.deepcopy(game.initialplayerinfos) + self._lower_is_better = scoreconfig.lower_is_better + self._score_label = scoreconfig.label + self._none_is_winner = scoreconfig.none_is_winner + self._scoretype = scoreconfig.scoretype + + def set_team_score(self, team: ba.Team, score: Optional[int]) -> None: + """Set the score for a given team. + + This can be a number or None. + (see the none_is_winner arg in the constructor) + """ + assert isinstance(team, Team) + sessionteam = team.sessionteam + self._scores[sessionteam.id] = (weakref.ref(sessionteam), score) + + def get_sessionteam_score(self, + sessionteam: ba.SessionTeam) -> Optional[int]: + """Return the score for a given ba.SessionTeam.""" + assert isinstance(sessionteam, SessionTeam) + for score in list(self._scores.values()): + if score[0]() is sessionteam: + return score[1] + + # If we have no score value, assume None. + return None + + @property + def sessionteams(self) -> List[ba.SessionTeam]: + """Return all ba.SessionTeams in the results.""" + if not self._game_set: + raise RuntimeError("Can't get teams until game is set.") + teams = [] + assert self._sessionteams is not None + for team_ref in self._sessionteams: + team = team_ref() + if team is not None: + teams.append(team) + return teams + + def has_score_for_sessionteam(self, sessionteam: ba.SessionTeam) -> bool: + """Return whether there is a score for a given session-team.""" + return any(s[0]() is sessionteam for s in self._scores.values()) + + def get_sessionteam_score_str(self, + sessionteam: ba.SessionTeam) -> ba.Lstr: + """Return the score for the given session-team as an Lstr. + + (properly formatted for the score type.) + """ + from ba._gameutils import timestring + from ba._language import Lstr + from ba._enums import TimeFormat + from ba._score import ScoreType + if not self._game_set: + raise RuntimeError("Can't get team-score-str until game is set.") + for score in list(self._scores.values()): + if score[0]() is sessionteam: + if score[1] is None: + return Lstr(value='-') + if self._scoretype is ScoreType.SECONDS: + return timestring(score[1] * 1000, + centi=False, + timeformat=TimeFormat.MILLISECONDS) + if self._scoretype is ScoreType.MILLISECONDS: + return timestring(score[1], + centi=True, + timeformat=TimeFormat.MILLISECONDS) + return Lstr(value=str(score[1])) + return Lstr(value='-') + + @property + def playerinfos(self) -> List[ba.PlayerInfo]: + """Get info about the players represented by the results.""" + if not self._game_set: + raise RuntimeError("Can't get player-info until game is set.") + assert self._playerinfos is not None + return self._playerinfos + + @property + def scoretype(self) -> ba.ScoreType: + """The type of score.""" + if not self._game_set: + raise RuntimeError("Can't get score-type until game is set.") + assert self._scoretype is not None + return self._scoretype + + @property + def score_label(self) -> str: + """The label associated with scores ('points', etc).""" + if not self._game_set: + raise RuntimeError("Can't get score-label until game is set.") + assert self._score_label is not None + return self._score_label + + @property + def lower_is_better(self) -> bool: + """Whether lower scores are better.""" + if not self._game_set: + raise RuntimeError("Can't get lower-is-better until game is set.") + assert self._lower_is_better is not None + return self._lower_is_better + + @property + def winning_sessionteam(self) -> Optional[ba.SessionTeam]: + """The winning ba.SessionTeam if there is exactly one, or else None.""" + if not self._game_set: + raise RuntimeError("Can't get winners until game is set.") + winners = self.winnergroups + if winners and len(winners[0].teams) == 1: + return winners[0].teams[0] + return None + + @property + def winnergroups(self) -> List[WinnerGroup]: + """Get an ordered list of winner groups.""" + if not self._game_set: + raise RuntimeError("Can't get winners until game is set.") + + # Group by best scoring teams. + winners: Dict[int, List[ba.SessionTeam]] = {} + scores = [ + score for score in self._scores.values() + if score[0]() is not None and score[1] is not None + ] + for score in scores: + assert score[1] is not None + sval = winners.setdefault(score[1], []) + team = score[0]() + assert team is not None + sval.append(team) + results: List[Tuple[Optional[int], + List[ba.SessionTeam]]] = list(winners.items()) + results.sort(reverse=not self._lower_is_better, + key=lambda x: asserttype(x[0], int)) + + # Also group the 'None' scores. + none_sessionteams: List[ba.SessionTeam] = [] + for score in self._scores.values(): + scoreteam = score[0]() + if scoreteam is not None and score[1] is None: + none_sessionteams.append(scoreteam) + + # Add the Nones to the list (either as winners or losers + # depending on the rules). + if none_sessionteams: + nones: List[Tuple[Optional[int], List[ba.SessionTeam]]] = [ + (None, none_sessionteams) + ] + if self._none_is_winner: + results = nones + results + else: + results = results + nones + + return [WinnerGroup(score, team) for score, team in results] diff --git a/dist/ba_data/python/ba/_gameutils.py b/dist/ba_data/python/ba/_gameutils.py new file mode 100644 index 0000000..89e7354 --- /dev/null +++ b/dist/ba_data/python/ba/_gameutils.py @@ -0,0 +1,397 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Utility functionality pertaining to gameplay.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import _ba +from ba._enums import TimeType, TimeFormat, SpecialChar, UIScale +from ba._error import ActivityNotFoundError + +if TYPE_CHECKING: + from typing import Any, Dict, Sequence, Optional + import ba + +TROPHY_CHARS = { + '1': SpecialChar.TROPHY1, + '2': SpecialChar.TROPHY2, + '3': SpecialChar.TROPHY3, + '0a': SpecialChar.TROPHY0A, + '0b': SpecialChar.TROPHY0B, + '4': SpecialChar.TROPHY4 +} + + +@dataclass +class GameTip: + """Defines a tip presentable to the user at the start of a game. + + Category: Gameplay Classes + """ + text: str + icon: Optional[ba.Texture] = None + sound: Optional[ba.Sound] = None + + +def get_trophy_string(trophy_id: str) -> str: + """Given a trophy id, returns a string to visualize it.""" + if trophy_id in TROPHY_CHARS: + return _ba.charstr(TROPHY_CHARS[trophy_id]) + return '?' + + +def animate(node: ba.Node, + attr: str, + keys: Dict[float, float], + loop: bool = False, + offset: float = 0, + timetype: ba.TimeType = TimeType.SIM, + timeformat: ba.TimeFormat = TimeFormat.SECONDS, + suppress_format_warning: bool = False) -> ba.Node: + """Animate values on a target ba.Node. + + Category: Gameplay Functions + + Creates an 'animcurve' node with the provided values and time as an input, + connect it to the provided attribute, and set it to die with the target. + Key values are provided as time:value dictionary pairs. Time values are + relative to the current time. By default, times are specified in seconds, + but timeformat can also be set to MILLISECONDS to recreate the old behavior + (prior to ba 1.5) of taking milliseconds. Returns the animcurve node. + """ + if timetype is TimeType.SIM: + driver = 'time' + else: + raise Exception('FIXME; only SIM timetype is supported currently.') + items = list(keys.items()) + items.sort() + + # Temp sanity check while we transition from milliseconds to seconds + # based time values. + if __debug__: + if not suppress_format_warning: + for item in items: + _ba.time_format_check(timeformat, item[0]) + + curve = _ba.newnode('animcurve', + owner=node, + name='Driving ' + str(node) + ' \'' + attr + '\'') + + if timeformat is TimeFormat.SECONDS: + mult = 1000 + elif timeformat is TimeFormat.MILLISECONDS: + mult = 1 + else: + raise ValueError(f'invalid timeformat value: {timeformat}') + + curve.times = [int(mult * time) for time, val in items] + curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int( + mult * offset) + curve.values = [val for time, val in items] + curve.loop = loop + + # If we're not looping, set a timer to kill this curve + # after its done its job. + # FIXME: Even if we are looping we should have a way to die once we + # get disconnected. + if not loop: + _ba.timer(int(mult * items[-1][0]) + 1000, + curve.delete, + timeformat=TimeFormat.MILLISECONDS) + + # Do the connects last so all our attrs are in place when we push initial + # values through. + + # We operate in either activities or sessions.. + try: + globalsnode = _ba.getactivity().globalsnode + except ActivityNotFoundError: + globalsnode = _ba.getsession().sessionglobalsnode + + globalsnode.connectattr(driver, curve, 'in') + curve.connectattr('out', node, attr) + return curve + + +def animate_array(node: ba.Node, + attr: str, + size: int, + keys: Dict[float, Sequence[float]], + loop: bool = False, + offset: float = 0, + timetype: ba.TimeType = TimeType.SIM, + timeformat: ba.TimeFormat = TimeFormat.SECONDS, + suppress_format_warning: bool = False) -> None: + """Animate an array of values on a target ba.Node. + + Category: Gameplay Functions + + Like ba.animate(), but operates on array attributes. + """ + # pylint: disable=too-many-locals + combine = _ba.newnode('combine', owner=node, attrs={'size': size}) + if timetype is TimeType.SIM: + driver = 'time' + else: + raise Exception('FIXME: Only SIM timetype is supported currently.') + items = list(keys.items()) + items.sort() + + # Temp sanity check while we transition from milliseconds to seconds + # based time values. + if __debug__: + if not suppress_format_warning: + for item in items: + # (PyCharm seems to think item is a float, not a tuple) + _ba.time_format_check(timeformat, item[0]) + + if timeformat is TimeFormat.SECONDS: + mult = 1000 + elif timeformat is TimeFormat.MILLISECONDS: + mult = 1 + else: + raise ValueError('invalid timeformat value: "' + str(timeformat) + '"') + + # We operate in either activities or sessions.. + try: + globalsnode = _ba.getactivity().globalsnode + except ActivityNotFoundError: + globalsnode = _ba.getsession().sessionglobalsnode + + for i in range(size): + curve = _ba.newnode('animcurve', + owner=node, + name=('Driving ' + str(node) + ' \'' + attr + + '\' member ' + str(i))) + globalsnode.connectattr(driver, curve, 'in') + curve.times = [int(mult * time) for time, val in items] + curve.values = [val[i] for time, val in items] + curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int( + mult * offset) + curve.loop = loop + curve.connectattr('out', combine, 'input' + str(i)) + + # If we're not looping, set a timer to kill this + # curve after its done its job. + if not loop: + # (PyCharm seems to think item is a float, not a tuple) + _ba.timer(int(mult * items[-1][0]) + 1000, + curve.delete, + timeformat=TimeFormat.MILLISECONDS) + combine.connectattr('output', node, attr) + + # If we're not looping, set a timer to kill the combine once + # the job is done. + # FIXME: Even if we are looping we should have a way to die + # once we get disconnected. + if not loop: + # (PyCharm seems to think item is a float, not a tuple) + _ba.timer(int(mult * items[-1][0]) + 1000, + combine.delete, + timeformat=TimeFormat.MILLISECONDS) + + +def show_damage_count(damage: str, position: Sequence[float], + direction: Sequence[float]) -> None: + """Pop up a damage count at a position in space. + + Category: Gameplay Functions + """ + lifespan = 1.0 + app = _ba.app + + # FIXME: Should never vary game elements based on local config. + # (connected clients may have differing configs so they won't + # get the intended results). + do_big = app.ui.uiscale is UIScale.SMALL or app.vr_mode + txtnode = _ba.newnode('text', + attrs={ + 'text': damage, + 'in_world': True, + 'h_align': 'center', + 'flatness': 1.0, + 'shadow': 1.0 if do_big else 0.7, + 'color': (1, 0.25, 0.25, 1), + 'scale': 0.015 if do_big else 0.01 + }) + # Translate upward. + tcombine = _ba.newnode('combine', owner=txtnode, attrs={'size': 3}) + tcombine.connectattr('output', txtnode, 'position') + v_vals = [] + pval = 0.0 + vval = 0.07 + count = 6 + for i in range(count): + v_vals.append((float(i) / count, pval)) + pval += vval + vval *= 0.5 + p_start = position[0] + p_dir = direction[0] + animate(tcombine, 'input0', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + p_start = position[1] + p_dir = direction[1] + animate(tcombine, 'input1', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + p_start = position[2] + p_dir = direction[2] + animate(tcombine, 'input2', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0}) + _ba.timer(lifespan, txtnode.delete) + + +def timestring(timeval: float, + centi: bool = True, + timeformat: ba.TimeFormat = TimeFormat.SECONDS, + suppress_format_warning: bool = False) -> ba.Lstr: + """Generate a ba.Lstr for displaying a time value. + + Category: General Utility Functions + + Given a time value, returns a ba.Lstr with: + (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True). + + Time 'timeval' is specified in seconds by default, or 'timeformat' can + be set to ba.TimeFormat.MILLISECONDS to accept milliseconds instead. + + WARNING: the underlying Lstr value is somewhat large so don't use this + to rapidly update Node text values for an onscreen timer or you may + consume significant network bandwidth. For that purpose you should + use a 'timedisplay' Node and attribute connections. + + """ + from ba._language import Lstr + + # Temp sanity check while we transition from milliseconds to seconds + # based time values. + if __debug__: + if not suppress_format_warning: + _ba.time_format_check(timeformat, timeval) + + # We operate on milliseconds internally. + if timeformat is TimeFormat.SECONDS: + timeval = int(1000 * timeval) + elif timeformat is TimeFormat.MILLISECONDS: + pass + else: + raise ValueError(f'invalid timeformat: {timeformat}') + if not isinstance(timeval, int): + timeval = int(timeval) + bits = [] + subs = [] + hval = (timeval // 1000) // (60 * 60) + if hval != 0: + bits.append('${H}') + subs.append(('${H}', + Lstr(resource='timeSuffixHoursText', + subs=[('${COUNT}', str(hval))]))) + mval = ((timeval // 1000) // 60) % 60 + if mval != 0: + bits.append('${M}') + subs.append(('${M}', + Lstr(resource='timeSuffixMinutesText', + subs=[('${COUNT}', str(mval))]))) + + # We add seconds if its non-zero *or* we haven't added anything else. + if centi: + sval = (timeval / 1000.0 % 60.0) + if sval >= 0.005 or not bits: + bits.append('${S}') + subs.append(('${S}', + Lstr(resource='timeSuffixSecondsText', + subs=[('${COUNT}', ('%.2f' % sval))]))) + else: + sval = (timeval // 1000 % 60) + if sval != 0 or not bits: + bits.append('${S}') + subs.append(('${S}', + Lstr(resource='timeSuffixSecondsText', + subs=[('${COUNT}', str(sval))]))) + return Lstr(value=' '.join(bits), subs=subs) + + +def cameraflash(duration: float = 999.0) -> None: + """Create a strobing camera flash effect. + + Category: Gameplay Functions + + (as seen when a team wins a game) + Duration is in seconds. + """ + # pylint: disable=too-many-locals + import random + from ba._nodeactor import NodeActor + x_spread = 10 + y_spread = 5 + positions = [[-x_spread, -y_spread], [0, -y_spread], [0, y_spread], + [x_spread, -y_spread], [x_spread, y_spread], + [-x_spread, y_spread]] + times = [0, 2700, 1000, 1800, 500, 1400] + + # Store this on the current activity so we only have one at a time. + # FIXME: Need a type safe way to do this. + activity = _ba.getactivity() + activity.camera_flash_data = [] # type: ignore + for i in range(6): + light = NodeActor( + _ba.newnode('light', + attrs={ + 'position': (positions[i][0], 0, positions[i][1]), + 'radius': 1.0, + 'lights_volumes': False, + 'height_attenuated': False, + 'color': (0.2, 0.2, 0.8) + })) + sval = 1.87 + iscale = 1.3 + tcombine = _ba.newnode('combine', + owner=light.node, + attrs={ + 'size': 3, + 'input0': positions[i][0], + 'input1': 0, + 'input2': positions[i][1] + }) + assert light.node + tcombine.connectattr('output', light.node, 'position') + xval = positions[i][0] + yval = positions[i][1] + spd = 0.5 + random.random() + spd2 = 0.5 + random.random() + animate(tcombine, + 'input0', { + 0.0: xval + 0, + 0.069 * spd: xval + 10.0, + 0.143 * spd: xval - 10.0, + 0.201 * spd: xval + 0 + }, + loop=True) + animate(tcombine, + 'input2', { + 0.0: yval + 0, + 0.15 * spd2: yval + 10.0, + 0.287 * spd2: yval - 10.0, + 0.398 * spd2: yval + 0 + }, + loop=True) + animate(light.node, + 'intensity', { + 0.0: 0, + 0.02 * sval: 0, + 0.05 * sval: 0.8 * iscale, + 0.08 * sval: 0, + 0.1 * sval: 0 + }, + loop=True, + offset=times[i]) + _ba.timer((times[i] + random.randint(1, int(duration)) * 40 * sval), + light.node.delete, + timeformat=TimeFormat.MILLISECONDS) + activity.camera_flash_data.append(light) # type: ignore diff --git a/dist/ba_data/python/ba/_general.py b/dist/ba_data/python/ba/_general.py new file mode 100644 index 0000000..57ad9a8 --- /dev/null +++ b/dist/ba_data/python/ba/_general.py @@ -0,0 +1,410 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Utility snippets applying to generic Python code.""" +from __future__ import annotations + +import gc +import types +import weakref +import random +import inspect +from typing import TYPE_CHECKING, TypeVar, Protocol + +from efro.terminal import Clr +import _ba +from ba._error import print_error, print_exception +from ba._enums import TimeType + +if TYPE_CHECKING: + from types import FrameType + from typing import Any, Type, Optional + from efro.call import Call as Call # 'as Call' so we re-export. + from weakref import ReferenceType + + +class Existable(Protocol): + """A Protocol for objects supporting an exists() method. + + Category: Protocols + """ + + def exists(self) -> bool: + """Whether this object exists.""" + ... + + +ExistableType = TypeVar('ExistableType', bound=Existable) +T = TypeVar('T') + + +def existing(obj: Optional[ExistableType]) -> Optional[ExistableType]: + """Convert invalid references to None for any ba.Existable object. + + Category: Gameplay Functions + + To best support type checking, it is important that invalid references + not be passed around and instead get converted to values of None. + That way the type checker can properly flag attempts to pass dead + objects (Optional[FooType]) into functions expecting only live ones + (FooType), etc. This call can be used on any 'existable' object + (one with an exists() method) and will convert it to a None value + if it does not exist. + + For more info, see notes on 'existables' here: + https://ballistica.net/wiki/Coding-Style-Guide + """ + assert obj is None or hasattr(obj, 'exists'), f'No "exists" on {obj}' + return obj if obj is not None and obj.exists() else None + + +def getclass(name: str, subclassof: Type[T]) -> Type[T]: + """Given a full class name such as foo.bar.MyClass, return the class. + + Category: General Utility Functions + + The class will be checked to make sure it is a subclass of the provided + 'subclassof' class, and a TypeError will be raised if not. + """ + import importlib + splits = name.split('.') + modulename = '.'.join(splits[:-1]) + classname = splits[-1] + module = importlib.import_module(modulename) + cls: Type = getattr(module, classname) + + if not issubclass(cls, subclassof): + raise TypeError(f'{name} is not a subclass of {subclassof}.') + return cls + + +def json_prep(data: Any) -> Any: + """Return a json-friendly version of the provided data. + + This converts any tuples to lists and any bytes to strings + (interpreted as utf-8, ignoring errors). Logs errors (just once) + if any data is modified/discarded/unsupported. + """ + + if isinstance(data, dict): + return dict((json_prep(key), json_prep(value)) + for key, value in list(data.items())) + if isinstance(data, list): + return [json_prep(element) for element in data] + if isinstance(data, tuple): + print_error('json_prep encountered tuple', once=True) + return [json_prep(element) for element in data] + if isinstance(data, bytes): + try: + return data.decode(errors='ignore') + except Exception: + from ba import _error + print_error('json_prep encountered utf-8 decode error', once=True) + return data.decode(errors='ignore') + if not isinstance(data, (str, float, bool, type(None), int)): + print_error('got unsupported type in json_prep:' + str(type(data)), + once=True) + return data + + +def utf8_all(data: Any) -> Any: + """Convert any unicode data in provided sequence(s) to utf8 bytes.""" + if isinstance(data, dict): + return dict((utf8_all(key), utf8_all(value)) + for key, value in list(data.items())) + if isinstance(data, list): + return [utf8_all(element) for element in data] + if isinstance(data, tuple): + return tuple(utf8_all(element) for element in data) + if isinstance(data, str): + return data.encode('utf-8', errors='ignore') + return data + + +def print_refs(obj: Any) -> None: + """Print a list of known live references to an object.""" + + # Hmmm; I just noticed that calling this on an object + # seems to keep it alive. Should figure out why. + print('REFERENCES FOR', obj, ':') + refs = list(gc.get_referrers(obj)) + i = 1 + for ref in refs: + print(' ref', i, ':', ref) + i += 1 + + +def get_type_name(cls: Type) -> str: + """Return a full type name including module for a class.""" + return cls.__module__ + '.' + cls.__name__ + + +class _WeakCall: + """Wrap a callable and arguments into a single callable object. + + Category: General Utility Classes + + When passed a bound method as the callable, the instance portion + of it is weak-referenced, meaning the underlying instance is + free to die if all other references to it go away. Should this + occur, calling the WeakCall is simply a no-op. + + Think of this as a handy way to tell an object to do something + at some point in the future if it happens to still exist. + + # EXAMPLE A: this code will create a FooClass instance and call its + # bar() method 5 seconds later; it will be kept alive even though + # we overwrite its variable with None because the bound method + # we pass as a timer callback (foo.bar) strong-references it + foo = FooClass() + ba.timer(5.0, foo.bar) + foo = None + + # EXAMPLE B: this code will *not* keep our object alive; it will die + # when we overwrite it with None and the timer will be a no-op when it + # fires + foo = FooClass() + ba.timer(5.0, ba.WeakCall(foo.bar)) + foo = None + + Note: additional args and keywords you provide to the WeakCall() + constructor are stored as regular strong-references; you'll need + to wrap them in weakrefs manually if desired. + """ + + def __init__(self, *args: Any, **keywds: Any) -> None: + """Instantiate a WeakCall. + + Pass a callable as the first arg, followed by any number of + arguments or keywords. + + # Example: wrap a method call with some positional and + # keyword args: + myweakcall = ba.WeakCall(myobj.dostuff, argval1, namedarg=argval2) + + # Now we have a single callable to run that whole mess. + # The same as calling myobj.dostuff(argval1, namedarg=argval2) + # (provided my_obj still exists; this will do nothing otherwise) + myweakcall() + """ + if hasattr(args[0], '__func__'): + self._call = WeakMethod(args[0]) + else: + app = _ba.app + if not app.did_weak_call_warning: + print(('Warning: callable passed to ba.WeakCall() is not' + ' weak-referencable (' + str(args[0]) + + '); use ba.Call() instead to avoid this ' + 'warning. Stack-trace:')) + import traceback + traceback.print_stack() + app.did_weak_call_warning = True + self._call = args[0] + self._args = args[1:] + self._keywds = keywds + + def __call__(self, *args_extra: Any) -> Any: + return self._call(*self._args + args_extra, **self._keywds) + + def __str__(self) -> str: + return ('') + + +class _Call: + """Wraps a callable and arguments into a single callable object. + + Category: General Utility Classes + + The callable is strong-referenced so it won't die until this + object does. + + Note that a bound method (ex: myobj.dosomething) contains a reference + to 'self' (myobj in that case), so you will be keeping that object + alive too. Use ba.WeakCall if you want to pass a method to callback + without keeping its object alive. + """ + + def __init__(self, *args: Any, **keywds: Any): + """Instantiate a Call. + + Pass a callable as the first arg, followed by any number of + arguments or keywords. + + # Example: wrap a method call with 1 positional and 1 keyword arg: + mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2) + + # Now we have a single callable to run that whole mess. + # ..the same as calling myobj.dostuff(argval1, namedarg=argval2) + mycall() + """ + self._call = args[0] + self._args = args[1:] + self._keywds = keywds + + def __call__(self, *args_extra: Any) -> Any: + return self._call(*self._args + args_extra, **self._keywds) + + def __str__(self) -> str: + return ('') + + +if TYPE_CHECKING: + WeakCall = Call + Call = Call +else: + WeakCall = _WeakCall + WeakCall.__name__ = 'WeakCall' + Call = _Call + Call.__name__ = 'Call' + + +class WeakMethod: + """A weak-referenced bound method. + + Wraps a bound method using weak references so that the original is + free to die. If called with a dead target, is simply a no-op. + """ + + def __init__(self, call: types.MethodType): + assert isinstance(call, types.MethodType) + self._func = call.__func__ + self._obj = weakref.ref(call.__self__) + + def __call__(self, *args: Any, **keywds: Any) -> Any: + obj = self._obj() + if obj is None: + return None + return self._func(*((obj, ) + args), **keywds) + + def __str__(self) -> str: + return '' + + +def verify_object_death(obj: object) -> None: + """Warn if an object does not get freed within a short period. + + Category: General Utility Functions + + This can be handy to detect and prevent memory/resource leaks. + """ + try: + ref = weakref.ref(obj) + except Exception: + print_exception('Unable to create weak-ref in verify_object_death') + + # Use a slight range for our checks so they don't all land at once + # if we queue a lot of them. + delay = random.uniform(2.0, 5.5) + with _ba.Context('ui'): + _ba.timer(delay, + lambda: _verify_object_death(ref), + timetype=TimeType.REAL) + + +def print_active_refs(obj: Any) -> None: + """Print info about things referencing a given object. + + Category: General Utility Functions + + Useful for tracking down cyclical references and causes for zombie objects. + """ + # pylint: disable=too-many-nested-blocks + from types import FrameType, TracebackType + refs = list(gc.get_referrers(obj)) + print(f'{Clr.YLW}Active referrers to {obj}:{Clr.RST}') + for i, ref in enumerate(refs): + print(f'{Clr.YLW}#{i+1}:{Clr.BLU} {ref}{Clr.RST}') + + # For certain types of objects such as stack frames, show what is + # keeping *them* alive too. + if isinstance(ref, FrameType): + print(f'{Clr.YLW} Active referrers to #{i+1}:{Clr.RST}') + refs2 = list(gc.get_referrers(ref)) + for j, ref2 in enumerate(refs2): + print(f'{Clr.YLW} #a{j+1}:{Clr.BLU} {ref2}{Clr.RST}') + + # Can go further down the rabbit-hole if needed... + if bool(False): + if isinstance(ref2, TracebackType): + print(f'{Clr.YLW} ' + f'Active referrers to #a{j+1}:{Clr.RST}') + refs3 = list(gc.get_referrers(ref2)) + for k, ref3 in enumerate(refs3): + print(f'{Clr.YLW} ' + f'#b{k+1}:{Clr.BLU} {ref3}{Clr.RST}') + + if isinstance(ref3, BaseException): + print(f'{Clr.YLW} Active referrers to' + f' #b{k+1}:{Clr.RST}') + refs4 = list(gc.get_referrers(ref3)) + for x, ref4 in enumerate(refs4): + print(f'{Clr.YLW} #c{x+1}:{Clr.BLU}' + f' {ref4}{Clr.RST}') + + +def _verify_object_death(wref: ReferenceType) -> None: + obj = wref() + if obj is None: + return + + try: + name = type(obj).__name__ + except Exception: + print(f'Note: unable to get type name for {obj}') + name = 'object' + + print(f'{Clr.RED}Error: {name} not dying when expected to:' + f' {Clr.BLD}{obj}{Clr.RST}') + print_active_refs(obj) + + +def storagename(suffix: str = None) -> str: + """Generate a unique name for storing class data in shared places. + + Category: General Utility Functions + + This consists of a leading underscore, the module path at the + call site with dots replaced by underscores, the containing class's + qualified name, and the provided suffix. When storing data in public + places such as 'customdata' dicts, this minimizes the chance of + collisions with other similarly named classes. + + Note that this will function even if called in the class definition. + + # Example: generate a unique name for storage purposes: + class MyThingie: + + # This will give something like '_mymodule_submodule_mythingie_data'. + _STORENAME = ba.storagename('data') + + # Use that name to store some data in the Activity we were passed. + def __init__(self, activity): + activity.customdata[self._STORENAME] = {} + """ + frame = inspect.currentframe() + if frame is None: + raise RuntimeError('Cannot get current stack frame.') + fback = frame.f_back + + # Note: We need to explicitly clear frame here to avoid a ref-loop + # that keeps all function-dicts in the stack alive until the next + # full GC cycle (the stack frame refers to this function's dict, + # which refers to the stack frame). + del frame + + if fback is None: + raise RuntimeError('Cannot get parent stack frame.') + modulepath = fback.f_globals.get('__name__') + if modulepath is None: + raise RuntimeError('Cannot get parent stack module path.') + assert isinstance(modulepath, str) + qualname = fback.f_locals.get('__qualname__') + if qualname is not None: + assert isinstance(qualname, str) + fullpath = f'_{modulepath}_{qualname.lower()}' + else: + fullpath = f'_{modulepath}' + if suffix is not None: + fullpath = f'{fullpath}_{suffix}' + return fullpath.replace('.', '_') diff --git a/dist/ba_data/python/ba/_hooks.py b/dist/ba_data/python/ba/_hooks.py new file mode 100644 index 0000000..85c60f7 --- /dev/null +++ b/dist/ba_data/python/ba/_hooks.py @@ -0,0 +1,337 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Snippets of code for use by the internal C++ layer. + +History: originally I would dynamically compile/eval bits of Python text +from within C++ code, but the major downside there was that none of that was +type-checked so if names or arguments changed I would never catch code breakage +until the code was next run. By defining all snippets I use here and then +capturing references to them all at launch I can immediately verify everything +I'm looking for exists and pylint/mypy can do their magic on this file. +""" +# (most of these are self-explanatory) +# pylint: disable=missing-function-docstring +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import List, Sequence, Optional, Dict, Any + import ba + + +def reset_to_main_menu() -> None: + """Reset the game to the main menu gracefully.""" + _ba.app.return_to_main_menu_session_gracefully() + + +def set_config_fullscreen_on() -> None: + """The app has set fullscreen on its own and we should note it.""" + _ba.app.config['Fullscreen'] = True + _ba.app.config.commit() + + +def set_config_fullscreen_off() -> None: + """The app has set fullscreen on its own and we should note it.""" + _ba.app.config['Fullscreen'] = False + _ba.app.config.commit() + + +def not_signed_in_screen_message() -> None: + from ba._language import Lstr + _ba.screenmessage(Lstr(resource='notSignedInErrorText')) + + +def connecting_to_party_message() -> None: + from ba._language import Lstr + _ba.screenmessage(Lstr(resource='internal.connectingToPartyText'), + color=(1, 1, 1)) + + +def rejecting_invite_already_in_party_message() -> None: + from ba._language import Lstr + _ba.screenmessage( + Lstr(resource='internal.rejectingInviteAlreadyInPartyText'), + color=(1, 0.5, 0)) + + +def connection_failed_message() -> None: + from ba._language import Lstr + _ba.screenmessage(Lstr(resource='internal.connectionFailedText'), + color=(1, 0.5, 0)) + + +def temporarily_unavailable_message() -> None: + from ba._language import Lstr + _ba.playsound(_ba.getsound('error')) + _ba.screenmessage( + Lstr(resource='getTicketsWindow.unavailableTemporarilyText'), + color=(1, 0, 0)) + + +def in_progress_message() -> None: + from ba._language import Lstr + _ba.playsound(_ba.getsound('error')) + _ba.screenmessage(Lstr(resource='getTicketsWindow.inProgressText'), + color=(1, 0, 0)) + + +def error_message() -> None: + from ba._language import Lstr + _ba.playsound(_ba.getsound('error')) + _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) + + +def purchase_not_valid_error() -> None: + from ba._language import Lstr + _ba.playsound(_ba.getsound('error')) + _ba.screenmessage(Lstr(resource='store.purchaseNotValidError', + subs=[('${EMAIL}', 'support@froemling.net')]), + color=(1, 0, 0)) + + +def purchase_already_in_progress_error() -> None: + from ba._language import Lstr + _ba.playsound(_ba.getsound('error')) + _ba.screenmessage(Lstr(resource='store.purchaseAlreadyInProgressText'), + color=(1, 0, 0)) + + +def gear_vr_controller_warning() -> None: + from ba._language import Lstr + _ba.playsound(_ba.getsound('error')) + _ba.screenmessage(Lstr(resource='usesExternalControllerText'), + color=(1, 0, 0)) + + +def orientation_reset_cb_message() -> None: + from ba._language import Lstr + _ba.screenmessage( + Lstr(resource='internal.vrOrientationResetCardboardText'), + color=(0, 1, 0)) + + +def orientation_reset_message() -> None: + from ba._language import Lstr + _ba.screenmessage(Lstr(resource='internal.vrOrientationResetText'), + color=(0, 1, 0)) + + +def on_app_pause() -> None: + _ba.app.on_app_pause() + + +def on_app_resume() -> None: + _ba.app.on_app_resume() + + +def launch_main_menu_session() -> None: + from bastd.mainmenu import MainMenuSession + _ba.new_host_session(MainMenuSession) + + +def language_test_toggle() -> None: + _ba.app.lang.setlanguage('Gibberish' if _ba.app.lang.language == + 'English' else 'English') + + +def award_in_control_achievement() -> None: + _ba.app.ach.award_local_achievement('In Control') + + +def award_dual_wielding_achievement() -> None: + _ba.app.ach.award_local_achievement('Dual Wielding') + + +def play_gong_sound() -> None: + _ba.playsound(_ba.getsound('gong')) + + +def launch_coop_game(name: str) -> None: + _ba.app.launch_coop_game(name) + + +def purchases_restored_message() -> None: + from ba._language import Lstr + _ba.screenmessage(Lstr(resource='getTicketsWindow.purchasesRestoredText'), + color=(0, 1, 0)) + + +def dismiss_wii_remotes_window() -> None: + call = _ba.app.ui.dismiss_wii_remotes_window_call + if call is not None: + call() + + +def unavailable_message() -> None: + from ba._language import Lstr + _ba.screenmessage(Lstr(resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + + +def submit_analytics_counts(sval: str) -> None: + _ba.add_transaction({'type': 'ANALYTICS_COUNTS', 'values': sval}) + _ba.run_transactions() + + +def set_last_ad_network(sval: str) -> None: + import time + _ba.app.ads.last_ad_network = sval + _ba.app.ads.last_ad_network_set_time = time.time() + + +def no_game_circle_message() -> None: + from ba._language import Lstr + _ba.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0)) + + +def empty_call() -> None: + pass + + +def level_icon_press() -> None: + print('LEVEL ICON PRESSED') + + +def trophy_icon_press() -> None: + print('TROPHY ICON PRESSED') + + +def coin_icon_press() -> None: + print('COIN ICON PRESSED') + + +def ticket_icon_press() -> None: + from bastd.ui.resourcetypeinfo import ResourceTypeInfoWindow + ResourceTypeInfoWindow( + origin_widget=_ba.get_special_widget('tickets_info_button')) + + +def back_button_press() -> None: + _ba.back_press() + + +def friends_button_press() -> None: + print('FRIEND BUTTON PRESSED!') + + +def print_trace() -> None: + import traceback + print('Python Traceback (most recent call last):') + traceback.print_stack() + + +def toggle_fullscreen() -> None: + cfg = _ba.app.config + cfg['Fullscreen'] = not cfg.resolve('Fullscreen') + cfg.apply_and_commit() + + +def party_icon_activate(origin: Sequence[float]) -> None: + import weakref + from bastd.ui.party import PartyWindow + app = _ba.app + _ba.playsound(_ba.getsound('swish')) + + # If it exists, dismiss it; otherwise make a new one. + if app.ui.party_window is not None and app.ui.party_window() is not None: + app.ui.party_window().close() + else: + app.ui.party_window = weakref.ref(PartyWindow(origin=origin)) + + +def read_config() -> None: + _ba.app.read_config() + + +def ui_remote_press() -> None: + """Handle a press by a remote device that is only usable for nav.""" + from ba._language import Lstr + + # Can be called without a context; need a context for getsound. + with _ba.Context('ui'): + _ba.screenmessage(Lstr(resource='internal.controllerForMenusOnlyText'), + color=(1, 0, 0)) + _ba.playsound(_ba.getsound('error')) + + +def quit_window() -> None: + from bastd.ui.confirm import QuitWindow + QuitWindow() + + +def remove_in_game_ads_message() -> None: + _ba.app.ads.do_remove_in_game_ads_message() + + +def telnet_access_request() -> None: + from bastd.ui.telnet import TelnetAccessRequestWindow + TelnetAccessRequestWindow() + + +def do_quit() -> None: + _ba.quit() + + +def shutdown() -> None: + _ba.app.on_app_shutdown() + + +def gc_disable() -> None: + import gc + gc.disable() + + +def device_menu_press(device: ba.InputDevice) -> None: + from bastd.ui.mainmenu import MainMenuWindow + in_main_menu = _ba.app.ui.has_main_menu_window() + if not in_main_menu: + _ba.set_ui_input_device(device) + _ba.playsound(_ba.getsound('swish')) + _ba.app.ui.set_main_menu_window(MainMenuWindow().get_root_widget()) + + +def show_url_window(address: str) -> None: + from bastd.ui.url import ShowURLWindow + ShowURLWindow(address) + + +def party_invite_revoke(invite_id: str) -> None: + # If there's a confirm window up for joining this particular + # invite, kill it. + for winref in _ba.app.invite_confirm_windows: + win = winref() + if win is not None and win.ew_party_invite_id == invite_id: + _ba.containerwidget(edit=win.get_root_widget(), + transition='out_right') + +import privateserver as pvt +def filter_chat_message(msg: str, client_id: int) -> Optional[str]: + """Intercept/filter chat messages. + + Called for all chat messages while hosting. + Messages originating from the host will have clientID -1. + Should filter and return the string to be displayed, or return None + to ignore the message. + """ + pvt.handlechat(msg,client_id); + del client_id # Unused by default. + return msg + + +def local_chat_message(msg: str) -> None: + if (_ba.app.ui.party_window is not None + and _ba.app.ui.party_window() is not None): + _ba.app.ui.party_window().on_chat_message(msg) + + +def get_player_icon(sessionplayer: ba.SessionPlayer) -> Dict[str, Any]: + info = sessionplayer.get_icon_info() + return { + 'texture': _ba.gettexture(info['texture']), + 'tint_texture': _ba.gettexture(info['tint_texture']), + 'tint_color': info['tint_color'], + 'tint2_color': info['tint2_color'] + } diff --git a/dist/ba_data/python/ba/_input.py b/dist/ba_data/python/ba/_input.py new file mode 100644 index 0000000..533ebe8 --- /dev/null +++ b/dist/ba_data/python/ba/_input.py @@ -0,0 +1,622 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Input related functionality""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Any, Dict, Tuple + import ba + + +def get_device_value(device: ba.InputDevice, name: str) -> Any: + """Returns a mapped value for an input device. + + This checks the user config and falls back to default values + where available. + """ + # pylint: disable=too-many-statements + # pylint: disable=too-many-return-statements + # pylint: disable=too-many-branches + devicename = device.name + unique_id = device.unique_identifier + app = _ba.app + useragentstring = app.user_agent_string + platform = app.platform + subplatform = app.subplatform + appconfig = _ba.app.config + + # If there's an entry in our config for this controller, use it. + if 'Controllers' in appconfig: + ccfgs = appconfig['Controllers'] + if devicename in ccfgs: + mapping = None + if unique_id in ccfgs[devicename]: + mapping = ccfgs[devicename][unique_id] + elif 'default' in ccfgs[devicename]: + mapping = ccfgs[devicename]['default'] + if mapping is not None: + return mapping.get(name, -1) + + if platform == 'windows': + + # XInput (hopefully this mapping is consistent?...) + if devicename.startswith('XInput Controller'): + return { + 'triggerRun2': 3, + 'unassignedButtonsRun': False, + 'buttonPickUp': 4, + 'buttonBomb': 2, + 'buttonStart': 8, + 'buttonIgnored2': 7, + 'triggerRun1': 6, + 'buttonPunch': 3, + 'buttonRun2': 5, + 'buttonRun1': 6, + 'buttonJump': 1, + 'buttonIgnored': 11 + }.get(name, -1) + + # Ps4 controller. + if devicename == 'Wireless Controller': + return { + 'triggerRun2': 4, + 'unassignedButtonsRun': False, + 'buttonPickUp': 4, + 'buttonBomb': 3, + 'buttonJump': 2, + 'buttonStart': 10, + 'buttonPunch': 1, + 'buttonRun2': 5, + 'buttonRun1': 6, + 'triggerRun1': 5 + }.get(name, -1) + + # Look for some exact types. + if _ba.is_running_on_fire_tv(): + if devicename in ['Thunder', 'Amazon Fire Game Controller']: + return { + 'triggerRun2': 23, + 'unassignedButtonsRun': False, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'analogStickDeadZone': 0.0, + 'startButtonActivatesDefaultWidget': False, + 'buttonStart': 83, + 'buttonPunch': 100, + 'buttonRun2': 103, + 'buttonRun1': 104, + 'triggerRun1': 24 + }.get(name, -1) + if devicename == 'NYKO PLAYPAD PRO': + return { + 'triggerRun2': 23, + 'triggerRun1': 24, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'buttonUp': 20, + 'buttonLeft': 22, + 'buttonRight': 23, + 'buttonStart': 83, + 'buttonPunch': 100, + 'buttonDown': 21 + }.get(name, -1) + if devicename == 'Logitech Dual Action': + return { + 'triggerRun2': 23, + 'triggerRun1': 24, + 'buttonPickUp': 98, + 'buttonBomb': 101, + 'buttonJump': 100, + 'buttonStart': 109, + 'buttonPunch': 97 + }.get(name, -1) + if devicename == 'Xbox 360 Wireless Receiver': + return { + 'triggerRun2': 23, + 'triggerRun1': 24, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'buttonUp': 20, + 'buttonLeft': 22, + 'buttonRight': 23, + 'buttonStart': 83, + 'buttonPunch': 100, + 'buttonDown': 21 + }.get(name, -1) + if devicename == 'Microsoft X-Box 360 pad': + return { + 'triggerRun2': 23, + 'triggerRun1': 24, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'buttonStart': 83, + 'buttonPunch': 100 + }.get(name, -1) + if devicename in [ + 'Amazon Remote', 'Amazon Bluetooth Dev', + 'Amazon Fire TV Remote' + ]: + return { + 'triggerRun2': 23, + 'triggerRun1': 24, + 'buttonPickUp': 24, + 'buttonBomb': 91, + 'buttonJump': 86, + 'buttonUp': 20, + 'buttonLeft': 22, + 'startButtonActivatesDefaultWidget': False, + 'buttonRight': 23, + 'buttonStart': 83, + 'buttonPunch': 90, + 'buttonDown': 21 + }.get(name, -1) + + elif 'NVIDIA SHIELD;' in useragentstring: + if 'NVIDIA Controller' in devicename: + return { + 'triggerRun2': 19, + 'triggerRun1': 18, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'analogStickDeadZone': 0.0, + 'buttonStart': 109, + 'buttonPunch': 100, + 'buttonIgnored': 184, + 'buttonIgnored2': 86 + }.get(name, -1) + elif platform == 'mac': + if devicename == 'PLAYSTATION(R)3 Controller': + return { + 'buttonLeft': 8, + 'buttonUp': 5, + 'buttonRight': 6, + 'buttonDown': 7, + 'buttonJump': 15, + 'buttonPunch': 16, + 'buttonBomb': 14, + 'buttonPickUp': 13, + 'buttonStart': 4, + 'buttonIgnored': 17 + }.get(name, -1) + if devicename in ['Wireless 360 Controller', 'Controller']: + + # Xbox360 gamepads + return { + 'analogStickDeadZone': 1.2, + 'buttonBomb': 13, + 'buttonDown': 2, + 'buttonJump': 12, + 'buttonLeft': 3, + 'buttonPickUp': 15, + 'buttonPunch': 14, + 'buttonRight': 4, + 'buttonStart': 5, + 'buttonUp': 1, + 'triggerRun1': 5, + 'triggerRun2': 6, + 'buttonIgnored': 11 + }.get(name, -1) + if (devicename + in ['Logitech Dual Action', 'Logitech Cordless RumblePad 2']): + return { + 'buttonJump': 2, + 'buttonPunch': 1, + 'buttonBomb': 3, + 'buttonPickUp': 4, + 'buttonStart': 10 + }.get(name, -1) + + # Old gravis gamepad. + if devicename == 'GamePad Pro USB ': + return { + 'buttonJump': 2, + 'buttonPunch': 1, + 'buttonBomb': 3, + 'buttonPickUp': 4, + 'buttonStart': 10 + }.get(name, -1) + + if devicename == 'Microsoft SideWinder Plug & Play Game Pad': + return { + 'buttonJump': 1, + 'buttonPunch': 3, + 'buttonBomb': 2, + 'buttonPickUp': 4, + 'buttonStart': 6 + }.get(name, -1) + + # Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..) + if devicename == 'Saitek P2500 Rumble Force Pad': + return { + 'buttonJump': 3, + 'buttonPunch': 1, + 'buttonBomb': 4, + 'buttonPickUp': 2, + 'buttonStart': 11 + }.get(name, -1) + + # Some crazy 'Senze' dual gamepad. + if devicename == 'Twin USB Joystick': + return { + 'analogStickLR': 3, + 'analogStickLR_B': 7, + 'analogStickUD': 4, + 'analogStickUD_B': 8, + 'buttonBomb': 2, + 'buttonBomb_B': 14, + 'buttonJump': 3, + 'buttonJump_B': 15, + 'buttonPickUp': 1, + 'buttonPickUp_B': 13, + 'buttonPunch': 4, + 'buttonPunch_B': 16, + 'buttonRun1': 7, + 'buttonRun1_B': 19, + 'buttonRun2': 8, + 'buttonRun2_B': 20, + 'buttonStart': 10, + 'buttonStart_B': 22, + 'enableSecondary': 1, + 'unassignedButtonsRun': False + }.get(name, -1) + if devicename == 'USB Gamepad ': # some weird 'JITE' gamepad + return { + 'analogStickLR': 4, + 'analogStickUD': 5, + 'buttonJump': 3, + 'buttonPunch': 4, + 'buttonBomb': 2, + 'buttonPickUp': 1, + 'buttonStart': 10 + }.get(name, -1) + + default_android_mapping = { + 'triggerRun2': 19, + 'unassignedButtonsRun': False, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'buttonStart': 83, + 'buttonStart2': 109, + 'buttonPunch': 100, + 'buttonRun2': 104, + 'buttonRun1': 103, + 'triggerRun1': 18, + 'buttonLeft': 22, + 'buttonRight': 23, + 'buttonUp': 20, + 'buttonDown': 21, + 'buttonVRReorient': 110 + } + + # Generic android... + if platform == 'android': + + # Steelseries stratus xl. + if devicename == 'SteelSeries Stratus XL': + return { + 'triggerRun2': 23, + 'unassignedButtonsRun': False, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'buttonStart': 83, + 'buttonStart2': 109, + 'buttonPunch': 100, + 'buttonRun2': 104, + 'buttonRun1': 103, + 'triggerRun1': 24, + 'buttonLeft': 22, + 'buttonRight': 23, + 'buttonUp': 20, + 'buttonDown': 21, + 'buttonVRReorient': 108 + }.get(name, -1) + + # Adt-1 gamepad (use funky 'mode' button for start). + if devicename == 'Gamepad': + return { + 'triggerRun2': 19, + 'unassignedButtonsRun': False, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'buttonStart': 111, + 'buttonPunch': 100, + 'startButtonActivatesDefaultWidget': False, + 'buttonRun2': 104, + 'buttonRun1': 103, + 'triggerRun1': 18 + }.get(name, -1) + # Nexus player remote. + if devicename == 'Nexus Remote': + return { + 'triggerRun2': 19, + 'unassignedButtonsRun': False, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'buttonUp': 20, + 'buttonLeft': 22, + 'buttonDown': 21, + 'buttonRight': 23, + 'buttonStart': 83, + 'buttonStart2': 109, + 'buttonPunch': 24, + 'buttonRun2': 104, + 'buttonRun1': 103, + 'triggerRun1': 18 + }.get(name, -1) + + if devicename == 'virtual-remote': + return { + 'triggerRun2': 19, + 'unassignedButtonsRun': False, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonStart': 83, + 'buttonJump': 24, + 'buttonUp': 20, + 'buttonLeft': 22, + 'buttonRight': 23, + 'triggerRun1': 18, + 'buttonStart2': 109, + 'buttonPunch': 100, + 'buttonRun2': 104, + 'buttonRun1': 103, + 'buttonDown': 21, + 'startButtonActivatesDefaultWidget': False, + 'uiOnly': True + }.get(name, -1) + + # flag particular gamepads to use exact android defaults.. + # (so they don't even ask to configure themselves) + if devicename in ['Samsung Game Pad EI-GP20', 'ASUS Gamepad' + ] or devicename.startswith('Freefly VR Glide'): + return default_android_mapping.get(name, -1) + + # Nvidia controller is default, but gets some strange + # keypresses we want to ignore.. touching the touchpad, + # so lets ignore those. + if 'NVIDIA Controller' in devicename: + return { + 'triggerRun2': 19, + 'unassignedButtonsRun': False, + 'buttonPickUp': 101, + 'buttonIgnored': 126, + 'buttonIgnored2': 1, + 'buttonBomb': 98, + 'buttonJump': 97, + 'buttonStart': 83, + 'buttonStart2': 109, + 'buttonPunch': 100, + 'buttonRun2': 104, + 'buttonRun1': 103, + 'triggerRun1': 18 + }.get(name, -1) + + # Default keyboard vals across platforms.. + if devicename == 'Keyboard' and unique_id == '#2': + if platform == 'mac' and subplatform == 'appstore': + return { + 'buttonJump': 258, + 'buttonPunch': 257, + 'buttonBomb': 262, + 'buttonPickUp': 261, + 'buttonUp': 273, + 'buttonDown': 274, + 'buttonLeft': 276, + 'buttonRight': 275, + 'buttonStart': 263 + }.get(name, -1) + return { + 'buttonPickUp': 1073741917, + 'buttonBomb': 1073741918, + 'buttonJump': 1073741914, + 'buttonUp': 1073741906, + 'buttonLeft': 1073741904, + 'buttonRight': 1073741903, + 'buttonStart': 1073741919, + 'buttonPunch': 1073741913, + 'buttonDown': 1073741905 + }.get(name, -1) + if devicename == 'Keyboard' and unique_id == '#1': + return { + 'buttonJump': 107, + 'buttonPunch': 106, + 'buttonBomb': 111, + 'buttonPickUp': 105, + 'buttonUp': 119, + 'buttonDown': 115, + 'buttonLeft': 97, + 'buttonRight': 100 + }.get(name, -1) + + # Ok, this gamepad's not in our specific preset list; + # fall back to some (hopefully) reasonable defaults. + + # Leaving these in here for now but not gonna add any more now that we have + # fancy-pants config sharing across the internet. + if platform == 'mac': + if 'PLAYSTATION' in devicename: # ps3 gamepad?.. + return { + 'buttonLeft': 8, + 'buttonUp': 5, + 'buttonRight': 6, + 'buttonDown': 7, + 'buttonJump': 15, + 'buttonPunch': 16, + 'buttonBomb': 14, + 'buttonPickUp': 13, + 'buttonStart': 4 + }.get(name, -1) + + # Dual Action Config - hopefully applies to more... + if 'Logitech' in devicename: + return { + 'buttonJump': 2, + 'buttonPunch': 1, + 'buttonBomb': 3, + 'buttonPickUp': 4, + 'buttonStart': 10 + }.get(name, -1) + + # Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..) + if 'Saitek' in devicename: + return { + 'buttonJump': 3, + 'buttonPunch': 1, + 'buttonBomb': 4, + 'buttonPickUp': 2, + 'buttonStart': 11 + }.get(name, -1) + + # Gravis stuff?... + if 'GamePad' in devicename: + return { + 'buttonJump': 2, + 'buttonPunch': 1, + 'buttonBomb': 3, + 'buttonPickUp': 4, + 'buttonStart': 10 + }.get(name, -1) + + # Reasonable defaults. + if platform == 'android': + if _ba.is_running_on_fire_tv(): + + # Mostly same as default firetv controller. + return { + 'triggerRun2': 23, + 'triggerRun1': 24, + 'buttonPickUp': 101, + 'buttonBomb': 98, + 'buttonJump': 97, + 'buttonStart': 83, + 'buttonPunch': 100, + 'buttonDown': 21, + 'buttonUp': 20, + 'buttonLeft': 22, + 'buttonRight': 23, + 'startButtonActivatesDefaultWidget': False, + }.get(name, -1) + + # Mostly same as 'Gamepad' except with 'menu' for default start + # button instead of 'mode'. + return default_android_mapping.get(name, -1) + + # Is there a point to any sort of fallbacks here?.. should check. + return { + 'buttonJump': 1, + 'buttonPunch': 2, + 'buttonBomb': 3, + 'buttonPickUp': 4, + 'buttonStart': 5 + }.get(name, -1) + + +def _gen_android_input_hash() -> str: + import os + import hashlib + md5 = hashlib.md5() + + # Currently we just do a single hash of *all* inputs on android + # and that's it.. good enough. + # (grabbing mappings for a specific device looks to be non-trivial) + for dirname in [ + '/system/usr/keylayout', '/data/usr/keylayout', + '/data/system/devices/keylayout' + ]: + try: + if os.path.isdir(dirname): + for f_name in os.listdir(dirname): + # This is usually volume keys and stuff; + # assume we can skip it?.. + # (since it'll vary a lot across devices) + if f_name == 'gpio-keys.kl': + continue + try: + with open(f'{dirname}/{f_name}', 'rb') as infile: + md5.update(infile.read()) + except PermissionError: + pass + except Exception: + from ba import _error + _error.print_exception( + 'error in _gen_android_input_hash inner loop') + return md5.hexdigest() + + +def get_input_map_hash(inputdevice: ba.InputDevice) -> str: + """Given an input device, return a hash based on its raw input values. + + This lets us avoid sharing mappings across devices that may + have the same name but actually produce different input values. + (Different Android versions, for example, may return different + key codes for button presses on a given type of controller) + """ + del inputdevice # Currently unused. + app = _ba.app + try: + if app.input_map_hash is None: + if app.platform == 'android': + app.input_map_hash = _gen_android_input_hash() + else: + app.input_map_hash = '' + return app.input_map_hash + except Exception: + from ba import _error + _error.print_exception('Exception in get_input_map_hash') + return '' + + +def get_input_device_config(device: ba.InputDevice, + default: bool) -> Tuple[Dict, str]: + """Given an input device, return its config dict in the app config. + + The dict will be created if it does not exist. + """ + cfg = _ba.app.config + name = device.name + ccfgs: Dict[str, Any] = cfg.setdefault('Controllers', {}) + ccfgs.setdefault(name, {}) + unique_id = device.unique_identifier + if default: + if unique_id in ccfgs[name]: + del ccfgs[name][unique_id] + if 'default' not in ccfgs[name]: + ccfgs[name]['default'] = {} + return ccfgs[name], 'default' + if unique_id not in ccfgs[name]: + ccfgs[name][unique_id] = {} + return ccfgs[name], unique_id + + +def get_last_player_name_from_input_device(device: ba.InputDevice) -> str: + """Return a reasonable player name associated with a device. + + (generally the last one used there) + """ + appconfig = _ba.app.config + + # Look for a default player profile name for them; + # otherwise default to their current random name. + profilename = '_random' + key_name = device.name + ' ' + device.unique_identifier + if ('Default Player Profiles' in appconfig + and key_name in appconfig['Default Player Profiles']): + profilename = appconfig['Default Player Profiles'][key_name] + if profilename == '_random': + profilename = device.get_default_player_name() + if profilename == '__account__': + profilename = _ba.get_account_display_string() + return profilename diff --git a/dist/ba_data/python/ba/_keyboard.py b/dist/ba_data/python/ba/_keyboard.py new file mode 100644 index 0000000..258125f --- /dev/null +++ b/dist/ba_data/python/ba/_keyboard.py @@ -0,0 +1,35 @@ +# Released under the MIT License. See LICENSE for details. +# +"""On-screen Keyboard related functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import List, Tuple, Dict + + +class Keyboard: + """Chars definitions for on-screen keyboard. + + Category: App Classes + + Keyboards are discoverable by the meta-tag system + and the user can select which one they want to use. + On-screen keyboard uses chars from active ba.Keyboard. + Attributes: + name + Displays when user selecting this keyboard. + chars + Used for row/column lengths. + pages + Extra chars like emojis. + nums + The 'num' page. + """ + + name: str + chars: List[Tuple[str, ...]] + pages: Dict[str, Tuple[str, ...]] + nums: Tuple[str, ...] diff --git a/dist/ba_data/python/ba/_language.py b/dist/ba_data/python/ba/_language.py new file mode 100644 index 0000000..98f0da5 --- /dev/null +++ b/dist/ba_data/python/ba/_language.py @@ -0,0 +1,562 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Language related functionality.""" +from __future__ import annotations + +import json +import os +from typing import TYPE_CHECKING, overload + +import _ba + +if TYPE_CHECKING: + import ba + from typing import Any, Dict, List, Optional, Tuple, Union, Sequence + + +class LanguageSubsystem: + """Wraps up language related app functionality. + + Category: App Classes + + To use this class, access the single instance of it at 'ba.app.lang'. + """ + + def __init__(self) -> None: + self.language_target: Optional[AttrDict] = None + self.language_merged: Optional[AttrDict] = None + self.default_language = self._get_default_language() + + def _can_display_language(self, language: str) -> bool: + """Tell whether we can display a particular language. + + On some platforms we don't have unicode rendering yet + which limits the languages we can draw. + """ + + # We don't yet support full unicode display on windows or linux :-(. + if (language in { + 'Chinese', 'ChineseTraditional', 'Persian', 'Korean', 'Arabic', + 'Hindi', 'Vietnamese' + } and not _ba.can_display_full_unicode()): + return False + return True + + @property + def locale(self) -> str: + """Raw country/language code detected by the game (such as 'en_US'). + + Generally for language-specific code you should look at + ba.App.language, which is the language the game is using + (which may differ from locale if the user sets a language, etc.) + """ + env = _ba.env() + assert isinstance(env['locale'], str) + return env['locale'] + + def _get_default_language(self) -> str: + languages = { + 'de': 'German', + 'es': 'Spanish', + 'sk': 'Slovak', + 'it': 'Italian', + 'nl': 'Dutch', + 'da': 'Danish', + 'pt': 'Portuguese', + 'fr': 'French', + 'el': 'Greek', + 'ru': 'Russian', + 'pl': 'Polish', + 'sv': 'Swedish', + 'eo': 'Esperanto', + 'cs': 'Czech', + 'hr': 'Croatian', + 'hu': 'Hungarian', + 'be': 'Belarussian', + 'ro': 'Romanian', + 'ko': 'Korean', + 'fa': 'Persian', + 'ar': 'Arabic', + 'zh': 'Chinese', + 'tr': 'Turkish', + 'id': 'Indonesian', + 'sr': 'Serbian', + 'uk': 'Ukrainian', + 'vi': 'Vietnamese', + 'vec': 'Venetian', + 'hi': 'Hindi' + } + + # Special case for Chinese: map specific variations to traditional. + # (otherwise will map to 'Chinese' which is simplified) + if self.locale in ('zh_HANT', 'zh_TW'): + language = 'ChineseTraditional' + else: + language = languages.get(self.locale[:2], 'English') + if not self._can_display_language(language): + language = 'English' + return language + + @property + def language(self) -> str: + """The name of the language the game is running in. + + This can be selected explicitly by the user or may be set + automatically based on ba.App.locale or other factors. + """ + assert isinstance(_ba.app.config, dict) + return _ba.app.config.get('Lang', self.default_language) + + @property + def available_languages(self) -> List[str]: + """A list of all available languages. + + Note that languages that may be present in game assets but which + are not displayable on the running version of the game are not + included here. + """ + langs = set() + try: + names = os.listdir('ba_data/data/languages') + names = [n.replace('.json', '').capitalize() for n in names] + + # FIXME: our simple capitalization fails on multi-word names; + # should handle this in a better way... + for i, name in enumerate(names): + if name == 'Chinesetraditional': + names[i] = 'ChineseTraditional' + except Exception: + from ba import _error + _error.print_exception() + names = [] + for name in names: + if self._can_display_language(name): + langs.add(name) + return sorted(name for name in names + if self._can_display_language(name)) + + def setlanguage(self, + language: Optional[str], + print_change: bool = True, + store_to_config: bool = True) -> None: + """Set the active language used for the game. + + Pass None to use OS default language. + """ + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + cfg = _ba.app.config + cur_language = cfg.get('Lang', None) + + # Store this in the config if its changing. + if language != cur_language and store_to_config: + if language is None: + if 'Lang' in cfg: + del cfg['Lang'] # Clear it out for default. + else: + cfg['Lang'] = language + cfg.commit() + switched = True + else: + switched = False + + with open('ba_data/data/languages/english.json') as infile: + lenglishvalues = json.loads(infile.read()) + + # None implies default. + if language is None: + language = self.default_language + try: + if language == 'English': + lmodvalues = None + else: + lmodfile = 'ba_data/data/languages/' + language.lower( + ) + '.json' + with open(lmodfile) as infile: + lmodvalues = json.loads(infile.read()) + except Exception: + from ba import _error + _error.print_exception('Exception importing language:', language) + _ba.screenmessage("Error setting language to '" + language + + "'; see log for details", + color=(1, 0, 0)) + switched = False + lmodvalues = None + + # Create an attrdict of *just* our target language. + self.language_target = AttrDict() + langtarget = self.language_target + assert langtarget is not None + _add_to_attr_dict( + langtarget, + lmodvalues if lmodvalues is not None else lenglishvalues) + + # Create an attrdict of our target language overlaid + # on our base (english). + languages = [lenglishvalues] + if lmodvalues is not None: + languages.append(lmodvalues) + lfull = AttrDict() + for lmod in languages: + _add_to_attr_dict(lfull, lmod) + self.language_merged = lfull + + # Pass some keys/values in for low level code to use; + # start with everything in their 'internal' section. + internal_vals = [ + v for v in list(lfull['internal'].items()) + if isinstance(v[1], str) + ] + + # Cherry-pick various other values to include. + # (should probably get rid of the 'internal' section + # and do everything this way) + for value in [ + 'replayNameDefaultText', 'replayWriteErrorText', + 'replayVersionErrorText', 'replayReadErrorText' + ]: + internal_vals.append((value, lfull[value])) + internal_vals.append( + ('axisText', lfull['configGamepadWindow']['axisText'])) + internal_vals.append(('buttonText', lfull['buttonText'])) + lmerged = self.language_merged + assert lmerged is not None + random_names = [ + n.strip() for n in lmerged['randomPlayerNamesText'].split(',') + ] + random_names = [n for n in random_names if n != ''] + _ba.set_internal_language_keys(internal_vals, random_names) + if switched and print_change: + _ba.screenmessage(Lstr(resource='languageSetText', + subs=[('${LANGUAGE}', + Lstr(translate=('languages', + language)))]), + color=(0, 1, 0)) + + def get_resource(self, + resource: str, + fallback_resource: str = None, + fallback_value: Any = None) -> Any: + """Return a translation resource by name. + + DEPRECATED; use ba.Lstr functionality for these purposes. + """ + try: + # If we have no language set, go ahead and set it. + if self.language_merged is None: + language = self.language + try: + self.setlanguage(language, + print_change=False, + store_to_config=False) + except Exception: + from ba import _error + _error.print_exception('exception setting language to', + language) + + # Try english as a fallback. + if language != 'English': + print('Resorting to fallback language (English)') + try: + self.setlanguage('English', + print_change=False, + store_to_config=False) + except Exception: + _error.print_exception( + 'error setting language to english fallback') + + # If they provided a fallback_resource value, try the + # target-language-only dict first and then fall back to trying the + # fallback_resource value in the merged dict. + if fallback_resource is not None: + try: + values = self.language_target + splits = resource.split('.') + dicts = splits[:-1] + key = splits[-1] + for dct in dicts: + assert values is not None + values = values[dct] + assert values is not None + val = values[key] + return val + except Exception: + # FIXME: Shouldn't we try the fallback resource in the + # merged dict AFTER we try the main resource in the + # merged dict? + try: + values = self.language_merged + splits = fallback_resource.split('.') + dicts = splits[:-1] + key = splits[-1] + for dct in dicts: + assert values is not None + values = values[dct] + assert values is not None + val = values[key] + return val + + except Exception: + # If we got nothing for fallback_resource, default + # to the normal code which checks or primary + # value in the merge dict; there's a chance we can + # get an english value for it (which we weren't + # looking for the first time through). + pass + + values = self.language_merged + splits = resource.split('.') + dicts = splits[:-1] + key = splits[-1] + for dct in dicts: + assert values is not None + values = values[dct] + assert values is not None + val = values[key] + return val + + except Exception: + # Ok, looks like we couldn't find our main or fallback resource + # anywhere. Now if we've been given a fallback value, return it; + # otherwise fail. + from ba import _error + if fallback_value is not None: + return fallback_value + raise _error.NotFoundError( + f"Resource not found: '{resource}'") from None + + def translate(self, + category: str, + strval: str, + raise_exceptions: bool = False, + print_errors: bool = False) -> str: + """Translate a value (or return the value if no translation available) + + DEPRECATED; use ba.Lstr functionality for these purposes. + """ + try: + translated = self.get_resource('translations')[category][strval] + except Exception as exc: + if raise_exceptions: + raise + if print_errors: + print(('Translate error: category=\'' + category + + '\' name=\'' + strval + '\' exc=' + str(exc) + '')) + translated = None + translated_out: str + if translated is None: + translated_out = strval + else: + translated_out = translated + assert isinstance(translated_out, str) + return translated_out + + def is_custom_unicode_char(self, char: str) -> bool: + """Return whether a char is in the custom unicode range we use.""" + assert isinstance(char, str) + if len(char) != 1: + raise ValueError('Invalid Input; must be length 1') + return 0xE000 <= ord(char) <= 0xF8FF + + +class Lstr: + """Used to define strings in a language-independent way. + + category: General Utility Classes + + These should be used whenever possible in place of hard-coded strings + so that in-game or UI elements show up correctly on all clients in their + currently-active language. + + To see available resource keys, look at any of the bs_language_*.py files + in the game or the translations pages at bombsquadgame.com/translate. + + # EXAMPLE 1: specify a string from a resource path + mynode.text = ba.Lstr(resource='audioSettingsWindow.titleText') + + # EXAMPLE 2: specify a translated string via a category and english value; + # if a translated value is available, it will be used; otherwise the + # english value will be. To see available translation categories, look + # under the 'translations' resource section. + mynode.text = ba.Lstr(translate=('gameDescriptions', 'Defeat all enemies')) + + # EXAMPLE 3: specify a raw value and some substitutions. Substitutions can + # be used with resource and translate modes as well. + mynode.text = ba.Lstr(value='${A} / ${B}', + subs=[('${A}', str(score)), ('${B}', str(total))]) + + # EXAMPLE 4: Lstrs can be nested. This example would display the resource + # at res_a but replace ${NAME} with the value of the resource at res_b + mytextnode.text = ba.Lstr(resource='res_a', + subs=[('${NAME}', ba.Lstr(resource='res_b'))]) + """ + + # pylint: disable=dangerous-default-value + # noinspection PyDefaultArgument + @overload + def __init__(self, + *, + resource: str, + fallback_resource: str = '', + fallback_value: str = '', + subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None: + """Create an Lstr from a string resource.""" + ... + + # noinspection PyShadowingNames,PyDefaultArgument + @overload + def __init__(self, + *, + translate: Tuple[str, str], + subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None: + """Create an Lstr by translating a string in a category.""" + ... + + # noinspection PyDefaultArgument + @overload + def __init__(self, + *, + value: str, + subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None: + """Create an Lstr from a raw string value.""" + ... + + # pylint: enable=redefined-outer-name, dangerous-default-value + + def __init__(self, *args: Any, **keywds: Any) -> None: + """Instantiate a Lstr. + + Pass a value for either 'resource', 'translate', + or 'value'. (see Lstr help for examples). + 'subs' can be a sequence of 2-member sequences consisting of values + and replacements. + 'fallback_resource' can be a resource key that will be used if the + main one is not present for + the current language in place of falling back to the english value + ('resource' mode only). + 'fallback_value' can be a literal string that will be used if neither + the resource nor the fallback resource is found ('resource' mode only). + """ + # pylint: disable=too-many-branches + if args: + raise TypeError('Lstr accepts only keyword arguments') + + # Basically just store the exact args they passed. + # However if they passed any Lstr values for subs, + # replace them with that Lstr's dict. + self.args = keywds + our_type = type(self) + + if isinstance(self.args.get('value'), our_type): + raise TypeError("'value' must be a regular string; not an Lstr") + + if 'subs' in self.args: + subs_new = [] + for key, value in keywds['subs']: + if isinstance(value, our_type): + subs_new.append((key, value.args)) + else: + subs_new.append((key, value)) + self.args['subs'] = subs_new + + # As of protocol 31 we support compact key names + # ('t' instead of 'translate', etc). Convert as needed. + if 'translate' in keywds: + keywds['t'] = keywds['translate'] + del keywds['translate'] + if 'resource' in keywds: + keywds['r'] = keywds['resource'] + del keywds['resource'] + if 'value' in keywds: + keywds['v'] = keywds['value'] + del keywds['value'] + if 'fallback' in keywds: + from ba import _error + _error.print_error( + 'deprecated "fallback" arg passed to Lstr(); use ' + 'either "fallback_resource" or "fallback_value"', + once=True) + keywds['f'] = keywds['fallback'] + del keywds['fallback'] + if 'fallback_resource' in keywds: + keywds['f'] = keywds['fallback_resource'] + del keywds['fallback_resource'] + if 'subs' in keywds: + keywds['s'] = keywds['subs'] + del keywds['subs'] + if 'fallback_value' in keywds: + keywds['fv'] = keywds['fallback_value'] + del keywds['fallback_value'] + + def evaluate(self) -> str: + """Evaluate the Lstr and returns a flat string in the current language. + + You should avoid doing this as much as possible and instead pass + and store Lstr values. + """ + return _ba.evaluate_lstr(self._get_json()) + + def is_flat_value(self) -> bool: + """Return whether the Lstr is a 'flat' value. + + This is defined as a simple string value incorporating no translations, + resources, or substitutions. In this case it may be reasonable to + replace it with a raw string value, perform string manipulation on it, + etc. + """ + return bool('v' in self.args and not self.args.get('s', [])) + + def _get_json(self) -> str: + try: + return json.dumps(self.args, separators=(',', ':')) + except Exception: + from ba import _error + _error.print_exception('_get_json failed for', self.args) + return 'JSON_ERR' + + def __str__(self) -> str: + return '' + + def __repr__(self) -> str: + return '' + + @staticmethod + def from_json(json_string: str) -> ba.Lstr: + """Given a json string, returns a ba.Lstr. Does no data validation.""" + lstr = Lstr(value='') + lstr.args = json.loads(json_string) + return lstr + + +def _add_to_attr_dict(dst: AttrDict, src: Dict) -> None: + for key, value in list(src.items()): + if isinstance(value, dict): + try: + dst_dict = dst[key] + except Exception: + dst_dict = dst[key] = AttrDict() + if not isinstance(dst_dict, AttrDict): + raise RuntimeError("language key '" + key + + "' is defined both as a dict and value") + _add_to_attr_dict(dst_dict, value) + else: + if not isinstance(value, (float, int, bool, str, str, type(None))): + raise TypeError("invalid value type for res '" + key + "': " + + str(type(value))) + dst[key] = value + + +class AttrDict(dict): + """A dict that can be accessed with dot notation. + + (so foo.bar is equivalent to foo['bar']) + """ + + def __getattr__(self, attr: str) -> Any: + val = self[attr] + assert not isinstance(val, bytes) + return val + + def __setattr__(self, attr: str, value: Any) -> None: + raise Exception() diff --git a/dist/ba_data/python/ba/_level.py b/dist/ba_data/python/ba/_level.py new file mode 100644 index 0000000..c45f715 --- /dev/null +++ b/dist/ba_data/python/ba/_level.py @@ -0,0 +1,168 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to individual levels in a campaign.""" +from __future__ import annotations + +import copy +import weakref +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from weakref import ReferenceType + from typing import Type, Any, Dict, Optional + import ba + + +class Level: + """An entry in a ba.Campaign consisting of a name, game type, and settings. + + category: Gameplay Classes + """ + + def __init__(self, + name: str, + gametype: Type[ba.GameActivity], + settings: dict, + preview_texture_name: str, + displayname: str = None): + self._name = name + self._gametype = gametype + self._settings = settings + self._preview_texture_name = preview_texture_name + self._displayname = displayname + self._campaign: Optional[ReferenceType[ba.Campaign]] = None + self._index: Optional[int] = None + self._score_version_string: Optional[str] = None + + @property + def name(self) -> str: + """The unique name for this Level.""" + return self._name + + def get_settings(self) -> Dict[str, Any]: + """Returns the settings for this Level.""" + settings = copy.deepcopy(self._settings) + + # So the game knows what the level is called. + # Hmm; seems hacky; I think we should take this out. + settings['name'] = self._name + return settings + + @property + def preview_texture_name(self) -> str: + """The preview texture name for this Level.""" + return self._preview_texture_name + + def get_preview_texture(self) -> ba.Texture: + """Load/return the preview Texture for this Level.""" + return _ba.gettexture(self._preview_texture_name) + + @property + def displayname(self) -> ba.Lstr: + """The localized name for this Level.""" + from ba import _language + return _language.Lstr( + translate=('coopLevelNames', self._displayname + if self._displayname is not None else self._name), + subs=[('${GAME}', + self._gametype.get_display_string(self._settings))]) + + @property + def gametype(self) -> Type[ba.GameActivity]: + """The type of game used for this Level.""" + return self._gametype + + @property + def campaign(self) -> Optional[ba.Campaign]: + """The ba.Campaign this Level is associated with, or None.""" + return None if self._campaign is None else self._campaign() + + @property + def index(self) -> int: + """The zero-based index of this Level in its ba.Campaign. + + Access results in a RuntimeError if the Level is not assigned to a + Campaign. + """ + if self._index is None: + raise RuntimeError('Level is not part of a Campaign') + return self._index + + @property + def complete(self) -> bool: + """Whether this Level has been completed.""" + config = self._get_config_dict() + return config.get('Complete', False) + + def set_complete(self, val: bool) -> None: + """Set whether or not this level is complete.""" + old_val = self.complete + assert isinstance(old_val, bool) + assert isinstance(val, bool) + if val != old_val: + config = self._get_config_dict() + config['Complete'] = val + + def get_high_scores(self) -> dict: + """Return the current high scores for this Level.""" + config = self._get_config_dict() + high_scores_key = 'High Scores' + self.get_score_version_string() + if high_scores_key not in config: + return {} + return copy.deepcopy(config[high_scores_key]) + + def set_high_scores(self, high_scores: Dict) -> None: + """Set high scores for this level.""" + config = self._get_config_dict() + high_scores_key = 'High Scores' + self.get_score_version_string() + config[high_scores_key] = high_scores + + def get_score_version_string(self) -> str: + """Return the score version string for this Level. + + If a Level's gameplay changes significantly, its version string + can be changed to separate its new high score lists/etc. from the old. + """ + if self._score_version_string is None: + scorever = self._gametype.getscoreconfig().version + if scorever != '': + scorever = ' ' + scorever + self._score_version_string = scorever + assert self._score_version_string is not None + return self._score_version_string + + @property + def rating(self) -> float: + """The current rating for this Level.""" + return self._get_config_dict().get('Rating', 0.0) + + def set_rating(self, rating: float) -> None: + """Set a rating for this Level, replacing the old ONLY IF higher.""" + old_rating = self.rating + config = self._get_config_dict() + config['Rating'] = max(old_rating, rating) + + def _get_config_dict(self) -> Dict[str, Any]: + """Return/create the persistent state dict for this level. + + The referenced dict exists under the game's config dict and + can be modified in place.""" + campaign = self.campaign + if campaign is None: + raise RuntimeError('Level is not in a campaign.') + configdict = campaign.configdict + val: Dict[str, Any] = configdict.setdefault(self._name, { + 'Rating': 0.0, + 'Complete': False + }) + assert isinstance(val, dict) + return val + + def set_campaign(self, campaign: ba.Campaign, index: int) -> None: + """For use by ba.Campaign when adding levels to itself. + + (internal)""" + self._campaign = weakref.ref(campaign) + self._index = index diff --git a/dist/ba_data/python/ba/_lobby.py b/dist/ba_data/python/ba/_lobby.py new file mode 100644 index 0000000..577a0c4 --- /dev/null +++ b/dist/ba_data/python/ba/_lobby.py @@ -0,0 +1,958 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Implements lobby system for gathering before games, char select, etc.""" + +from __future__ import annotations + +import weakref +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import _ba +from ba._error import print_exception, print_error, NotFoundError +from ba._gameutils import animate, animate_array +from ba._language import Lstr +from ba._enums import SpecialChar, InputType +from ba._profile import get_player_profile_colors + +if TYPE_CHECKING: + from typing import Optional, List, Dict, Any, Sequence, Union + import ba + +MAX_QUICK_CHANGE_COUNT = 30 +QUICK_CHANGE_INTERVAL = 0.05 +QUICK_CHANGE_RESET_INTERVAL = 1.0 + + +# Hmm should we move this to actors?.. +class JoinInfo: + """Display useful info for joiners.""" + + def __init__(self, lobby: ba.Lobby): + from ba._nodeactor import NodeActor + from ba._general import WeakCall + self._state = 0 + self._press_to_punch: Union[str, ba.Lstr] = _ba.charstr( + SpecialChar.LEFT_BUTTON) + self._press_to_bomb: Union[str, ba.Lstr] = _ba.charstr( + SpecialChar.RIGHT_BUTTON) + self._joinmsg = Lstr(resource='pressAnyButtonToJoinText') + can_switch_teams = (len(lobby.sessionteams) > 1) + + # If we have a keyboard, grab keys for punch and pickup. + # FIXME: This of course is only correct on the local device; + # Should change this for net games. + keyboard = _ba.getinputdevice('Keyboard', '#1', doraise=False) + if keyboard is not None: + self._update_for_keyboard(keyboard) + + flatness = 1.0 if _ba.app.vr_mode else 0.0 + self._text = NodeActor( + _ba.newnode('text', + attrs={ + 'position': (0, -40), + 'h_attach': 'center', + 'v_attach': 'top', + 'h_align': 'center', + 'color': (0.7, 0.7, 0.95, 1.0), + 'flatness': flatness, + 'text': self._joinmsg + })) + + if _ba.app.demo_mode or _ba.app.arcade_mode: + self._messages = [self._joinmsg] + else: + msg1 = Lstr(resource='pressToSelectProfileText', + subs=[ + ('${BUTTONS}', _ba.charstr(SpecialChar.UP_ARROW) + + ' ' + _ba.charstr(SpecialChar.DOWN_ARROW)) + ]) + msg2 = Lstr(resource='pressToOverrideCharacterText', + subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))]) + msg3 = Lstr(value='${A} < ${B} >', + subs=[('${A}', msg2), ('${B}', self._press_to_bomb)]) + self._messages = (([ + Lstr( + resource='pressToSelectTeamText', + subs=[('${BUTTONS}', _ba.charstr(SpecialChar.LEFT_ARROW) + + ' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))], + ) + ] if can_switch_teams else []) + [msg1] + [msg3] + [self._joinmsg]) + + self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True) + + def _update_for_keyboard(self, keyboard: ba.InputDevice) -> None: + from ba import _input + punch_key = keyboard.get_button_name( + _input.get_device_value(keyboard, 'buttonPunch')) + self._press_to_punch = Lstr(resource='orText', + subs=[('${A}', + Lstr(value='\'${K}\'', + subs=[('${K}', punch_key)])), + ('${B}', self._press_to_punch)]) + bomb_key = keyboard.get_button_name( + _input.get_device_value(keyboard, 'buttonBomb')) + self._press_to_bomb = Lstr(resource='orText', + subs=[('${A}', + Lstr(value='\'${K}\'', + subs=[('${K}', bomb_key)])), + ('${B}', self._press_to_bomb)]) + self._joinmsg = Lstr(value='${A} < ${B} >', + subs=[('${A}', + Lstr(resource='pressPunchToJoinText')), + ('${B}', self._press_to_punch)]) + + def _update(self) -> None: + assert self._text.node + self._text.node.text = self._messages[self._state] + self._state = (self._state + 1) % len(self._messages) + + +@dataclass +class PlayerReadyMessage: + """Tells an object a player has been selected from the given chooser.""" + chooser: ba.Chooser + + +@dataclass +class ChangeMessage: + """Tells an object that a selection is being changed.""" + what: str + value: int + + +class Chooser: + """A character/team selector for a ba.Player. + + Category: Gameplay Classes + """ + + def __del__(self) -> None: + + # Just kill off our base node; the rest should go down with it. + if self._text_node: + self._text_node.delete() + + def __init__(self, vpos: float, sessionplayer: _ba.SessionPlayer, + lobby: 'Lobby') -> None: + self._deek_sound = _ba.getsound('deek') + self._click_sound = _ba.getsound('click01') + self._punchsound = _ba.getsound('punch01') + self._swish_sound = _ba.getsound('punchSwish') + self._errorsound = _ba.getsound('error') + self._mask_texture = _ba.gettexture('characterIconMask') + self._vpos = vpos + self._lobby = weakref.ref(lobby) + self._sessionplayer = sessionplayer + self._inited = False + self._dead = False + self._text_node: Optional[ba.Node] = None + self._profilename = '' + self._profilenames: List[str] = [] + self._ready: bool = False + self._character_names: List[str] = [] + self._last_change: Sequence[Union[float, int]] = (0, 0) + self._profiles: Dict[str, Dict[str, Any]] = {} + + app = _ba.app + + # Load available player profiles either from the local config or + # from the remote device. + self.reload_profiles() + + # Note: this is just our local index out of available teams; *not* + # the team-id! + self._selected_team_index: int = self.lobby.next_add_team + + # Store a persistent random character index and colors; we'll use this + # for the '_random' profile. Let's use their input_device id to seed + # it. This will give a persistent character for them between games + # and will distribute characters nicely if everyone is random. + self._random_color, self._random_highlight = ( + get_player_profile_colors(None)) + + # To calc our random character we pick a random one out of our + # unlocked list and then locate that character's index in the full + # list. + char_index_offset = app.lobby_random_char_index_offset + self._random_character_index = ( + (sessionplayer.inputdevice.id + char_index_offset) % + len(self._character_names)) + + # Attempt to set an initial profile based on what was used previously + # for this input-device, etc. + self._profileindex = self._select_initial_profile() + self._profilename = self._profilenames[self._profileindex] + + self._text_node = _ba.newnode('text', + delegate=self, + attrs={ + 'position': (-100, self._vpos), + 'maxwidth': 160, + 'shadow': 0.5, + 'vr_depth': -20, + 'h_align': 'left', + 'v_align': 'center', + 'v_attach': 'top' + }) + animate(self._text_node, 'scale', {0: 0, 0.1: 1.0}) + self.icon = _ba.newnode('image', + owner=self._text_node, + attrs={ + 'position': (-130, self._vpos + 20), + 'mask_texture': self._mask_texture, + 'vr_depth': -10, + 'attach': 'topCenter' + }) + + animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)}) + + # Set our initial name to '' in case anyone asks. + self._sessionplayer.setname( + Lstr(resource='choosingPlayerText').evaluate(), real=False) + + # Init these to our rando but they should get switched to the + # selected profile (if any) right after. + self._character_index = self._random_character_index + self._color = self._random_color + self._highlight = self._random_highlight + + self.update_from_profile() + self.update_position() + self._inited = True + + self._set_ready(False) + + def _select_initial_profile(self) -> int: + app = _ba.app + profilenames = self._profilenames + inputdevice = self._sessionplayer.inputdevice + + # If we've got a set profile name for this device, work backwards + # from that to get our index. + dprofilename = (app.config.get('Default Player Profiles', + {}).get(inputdevice.name + ' ' + + inputdevice.unique_identifier)) + if dprofilename is not None and dprofilename in profilenames: + # If we got '__account__' and its local and we haven't marked + # anyone as the 'account profile' device yet, mark this guy as + # it. (prevents the next joiner from getting the account + # profile too). + if (dprofilename == '__account__' + and not inputdevice.is_remote_client + and app.lobby_account_profile_device_id is None): + app.lobby_account_profile_device_id = inputdevice.id + return profilenames.index(dprofilename) + + # We want to mark the first local input-device in the game + # as the 'account profile' device. + if (not inputdevice.is_remote_client + and not inputdevice.is_controller_app): + if (app.lobby_account_profile_device_id is None + and '__account__' in profilenames): + app.lobby_account_profile_device_id = inputdevice.id + + # If this is the designated account-profile-device, try to default + # to the account profile. + if (inputdevice.id == app.lobby_account_profile_device_id + and '__account__' in profilenames): + return profilenames.index('__account__') + + # If this is the controller app, it defaults to using a random + # profile (since we can pull the random name from the app). + if inputdevice.is_controller_app and '_random' in profilenames: + return profilenames.index('_random') + + # If its a client connection, for now just force + # the account profile if possible.. (need to provide a + # way for clients to specify/remember their default + # profile on remote servers that do not already know them). + if inputdevice.is_remote_client and '__account__' in profilenames: + return profilenames.index('__account__') + + # Cycle through our non-random profiles once; after + # that, everyone gets random. + while (app.lobby_random_profile_index < len(profilenames) + and profilenames[app.lobby_random_profile_index] + in ('_random', '__account__', '_edit')): + app.lobby_random_profile_index += 1 + if app.lobby_random_profile_index < len(profilenames): + profileindex = app.lobby_random_profile_index + app.lobby_random_profile_index += 1 + return profileindex + assert '_random' in profilenames + return profilenames.index('_random') + + @property + def sessionplayer(self) -> ba.SessionPlayer: + """The ba.SessionPlayer associated with this chooser.""" + return self._sessionplayer + + @property + def ready(self) -> bool: + """Whether this chooser is checked in as ready.""" + return self._ready + + def set_vpos(self, vpos: float) -> None: + """(internal)""" + self._vpos = vpos + + def set_dead(self, val: bool) -> None: + """(internal)""" + self._dead = val + + @property + def sessionteam(self) -> ba.SessionTeam: + """Return this chooser's currently selected ba.SessionTeam.""" + return self.lobby.sessionteams[self._selected_team_index] + + @property + def lobby(self) -> ba.Lobby: + """The chooser's ba.Lobby.""" + lobby = self._lobby() + if lobby is None: + raise NotFoundError('Lobby does not exist.') + return lobby + + def get_lobby(self) -> Optional[ba.Lobby]: + """Return this chooser's lobby if it still exists; otherwise None.""" + return self._lobby() + + def update_from_profile(self) -> None: + """Set character/colors based on the current profile.""" + self._profilename = self._profilenames[self._profileindex] + if self._profilename == '_edit': + pass + elif self._profilename == '_random': + self._character_index = self._random_character_index + self._color = self._random_color + self._highlight = self._random_highlight + else: + character = self._profiles[self._profilename]['character'] + + # At the moment we're not properly pulling the list + # of available characters from clients, so profiles might use a + # character not in their list. For now, just go ahead and add + # a character name to their list as long as we're aware of it. + # This just means they won't always be able to override their + # character to others they own, but profile characters + # should work (and we validate profiles on the master server + # so no exploit opportunities) + if (character not in self._character_names + and character in _ba.app.spaz_appearances): + self._character_names.append(character) + self._character_index = self._character_names.index(character) + self._color, self._highlight = (get_player_profile_colors( + self._profilename, profiles=self._profiles)) + self._update_icon() + self._update_text() + + def reload_profiles(self) -> None: + """Reload all player profiles.""" + from ba._general import json_prep + app = _ba.app + + # Re-construct our profile index and other stuff since the profile + # list might have changed. + input_device = self._sessionplayer.inputdevice + is_remote = input_device.is_remote_client + is_test_input = input_device.name.startswith('TestInput') + + # Pull this player's list of unlocked characters. + if is_remote: + # TODO: Pull this from the remote player. + # (but make sure to filter it to the ones we've got). + self._character_names = ['Spaz'] + else: + self._character_names = self.lobby.character_names_local_unlocked + + # If we're a local player, pull our local profiles from the config. + # Otherwise ask the remote-input-device for its profile list. + if is_remote: + self._profiles = input_device.get_player_profiles() + else: + self._profiles = app.config.get('Player Profiles', {}) + + # These may have come over the wire from an older + # (non-unicode/non-json) version. + # Make sure they conform to our standards + # (unicode strings, no tuples, etc) + self._profiles = json_prep(self._profiles) + + # Filter out any characters we're unaware of. + for profile in list(self._profiles.items()): + if profile[1].get('character', '') not in app.spaz_appearances: + profile[1]['character'] = 'Spaz' + + # Add in a random one so we're ok even if there's no user profiles. + self._profiles['_random'] = {} + + # In kiosk mode we disable account profiles to force random. + if app.demo_mode or app.arcade_mode: + if '__account__' in self._profiles: + del self._profiles['__account__'] + + # For local devices, add it an 'edit' option which will pop up + # the profile window. + if not is_remote and not is_test_input and not (app.demo_mode + or app.arcade_mode): + self._profiles['_edit'] = {} + + # Build a sorted name list we can iterate through. + self._profilenames = list(self._profiles.keys()) + self._profilenames.sort(key=lambda x: x.lower()) + + if self._profilename in self._profilenames: + self._profileindex = self._profilenames.index(self._profilename) + else: + self._profileindex = 0 + self._profilename = self._profilenames[self._profileindex] + + def update_position(self) -> None: + """Update this chooser's position.""" + + assert self._text_node + spacing = 350 + sessionteams = self.lobby.sessionteams + offs = (spacing * -0.5 * len(sessionteams) + + spacing * self._selected_team_index + 250) + if len(sessionteams) > 1: + offs -= 35 + animate_array(self._text_node, 'position', 2, { + 0: self._text_node.position, + 0.1: (-100 + offs, self._vpos + 23) + }) + animate_array(self.icon, 'position', 2, { + 0: self.icon.position, + 0.1: (-130 + offs, self._vpos + 22) + }) + + def get_character_name(self) -> str: + """Return the selected character name.""" + return self._character_names[self._character_index] + + def _do_nothing(self) -> None: + """Does nothing! (hacky way to disable callbacks)""" + + def _getname(self, full: bool = False) -> str: + name_raw = name = self._profilenames[self._profileindex] + clamp = False + if name == '_random': + try: + name = ( + self._sessionplayer.inputdevice.get_default_player_name()) + except Exception: + print_exception('Error getting _random chooser name.') + name = 'Invalid' + clamp = not full + elif name == '__account__': + try: + name = self._sessionplayer.inputdevice.get_account_name(full) + except Exception: + print_exception('Error getting account name for chooser.') + name = 'Invalid' + clamp = not full + elif name == '_edit': + # Explicitly flattening this to a str; it's only relevant on + # the host so that's ok. + name = (Lstr( + resource='createEditPlayerText', + fallback_resource='editProfileWindow.titleNewText').evaluate()) + else: + # If we have a regular profile marked as global with an icon, + # use it (for full only). + if full: + try: + if self._profiles[name_raw].get('global', False): + icon = (self._profiles[name_raw]['icon'] + if 'icon' in self._profiles[name_raw] else + _ba.charstr(SpecialChar.LOGO)) + name = icon + name + except Exception: + print_exception('Error applying global icon.') + else: + # We now clamp non-full versions of names so there's at + # least some hope of reading them in-game. + clamp = True + + if clamp: + if len(name) > 10: + name = name[:10] + '...' + return name + + def _set_ready(self, ready: bool) -> None: + # pylint: disable=cyclic-import + from bastd.ui.profile import browser as pbrowser + from ba._general import Call + profilename = self._profilenames[self._profileindex] + + # Handle '_edit' as a special case. + if profilename == '_edit' and ready: + with _ba.Context('ui'): + pbrowser.ProfileBrowserWindow(in_main_menu=False) + + # Give their input-device UI ownership too + # (prevent someone else from snatching it in crowded games) + _ba.set_ui_input_device(self._sessionplayer.inputdevice) + return + + if not ready: + self._sessionplayer.assigninput( + InputType.LEFT_PRESS, + Call(self.handlemessage, ChangeMessage('team', -1))) + self._sessionplayer.assigninput( + InputType.RIGHT_PRESS, + Call(self.handlemessage, ChangeMessage('team', 1))) + self._sessionplayer.assigninput( + InputType.BOMB_PRESS, + Call(self.handlemessage, ChangeMessage('character', 1))) + self._sessionplayer.assigninput( + InputType.UP_PRESS, + Call(self.handlemessage, ChangeMessage('profileindex', -1))) + self._sessionplayer.assigninput( + InputType.DOWN_PRESS, + Call(self.handlemessage, ChangeMessage('profileindex', 1))) + self._sessionplayer.assigninput( + (InputType.JUMP_PRESS, InputType.PICK_UP_PRESS, + InputType.PUNCH_PRESS), + Call(self.handlemessage, ChangeMessage('ready', 1))) + self._ready = False + self._update_text() + self._sessionplayer.setname('untitled', real=False) + else: + self._sessionplayer.assigninput( + (InputType.LEFT_PRESS, InputType.RIGHT_PRESS, + InputType.UP_PRESS, InputType.DOWN_PRESS, + InputType.JUMP_PRESS, InputType.BOMB_PRESS, + InputType.PICK_UP_PRESS), self._do_nothing) + self._sessionplayer.assigninput( + (InputType.JUMP_PRESS, InputType.BOMB_PRESS, + InputType.PICK_UP_PRESS, InputType.PUNCH_PRESS), + Call(self.handlemessage, ChangeMessage('ready', 0))) + + # Store the last profile picked by this input for reuse. + input_device = self._sessionplayer.inputdevice + name = input_device.name + unique_id = input_device.unique_identifier + device_profiles = _ba.app.config.setdefault( + 'Default Player Profiles', {}) + + # Make an exception if we have no custom profiles and are set + # to random; in that case we'll want to start picking up custom + # profiles if/when one is made so keep our setting cleared. + special = ('_random', '_edit', '__account__') + have_custom_profiles = any(p not in special + for p in self._profiles) + + profilekey = name + ' ' + unique_id + if profilename == '_random' and not have_custom_profiles: + if profilekey in device_profiles: + del device_profiles[profilekey] + else: + device_profiles[profilekey] = profilename + _ba.app.config.commit() + + # Set this player's short and full name. + self._sessionplayer.setname(self._getname(), + self._getname(full=True), + real=True) + self._ready = True + self._update_text() + + # Inform the session that this player is ready. + _ba.getsession().handlemessage(PlayerReadyMessage(self)) + + def _handle_ready_msg(self, ready: bool) -> None: + force_team_switch = False + + # Team auto-balance kicks us to another team if we try to + # join the team with the most players. + if not self._ready: + if _ba.app.config.get('Auto Balance Teams', False): + lobby = self.lobby + sessionteams = lobby.sessionteams + if len(sessionteams) > 1: + + # First, calc how many players are on each team + # ..we need to count both active players and + # choosers that have been marked as ready. + team_player_counts = {} + for sessionteam in sessionteams: + team_player_counts[sessionteam.id] = len( + sessionteam.players) + for chooser in lobby.choosers: + if chooser.ready: + team_player_counts[chooser.sessionteam.id] += 1 + largest_team_size = max(team_player_counts.values()) + smallest_team_size = (min(team_player_counts.values())) + + # Force switch if we're on the biggest sessionteam + # and there's a smaller one available. + if (largest_team_size != smallest_team_size + and team_player_counts[self.sessionteam.id] >= + largest_team_size): + force_team_switch = True + + # Either force switch teams, or actually for realsies do the set-ready. + if force_team_switch: + _ba.playsound(self._errorsound) + self.handlemessage(ChangeMessage('team', 1)) + else: + _ba.playsound(self._punchsound) + self._set_ready(ready) + + # TODO: should handle this at the engine layer so this is unnecessary. + def _handle_repeat_message_attack(self) -> None: + now = _ba.time() + count = self._last_change[1] + if now - self._last_change[0] < QUICK_CHANGE_INTERVAL: + count += 1 + if count > MAX_QUICK_CHANGE_COUNT: + _ba.disconnect_client( + self._sessionplayer.inputdevice.client_id) + elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL: + count = 0 + self._last_change = (now, count) + + def handlemessage(self, msg: Any) -> Any: + """Standard generic message handler.""" + + if isinstance(msg, ChangeMessage): + self._handle_repeat_message_attack() + + # If we've been removed from the lobby, ignore this stuff. + if self._dead: + print_error('chooser got ChangeMessage after dying') + return + + if not self._text_node: + print_error('got ChangeMessage after nodes died') + return + + if msg.what == 'team': + sessionteams = self.lobby.sessionteams + if len(sessionteams) > 1: + _ba.playsound(self._swish_sound) + self._selected_team_index = ( + (self._selected_team_index + msg.value) % + len(sessionteams)) + self._update_text() + self.update_position() + self._update_icon() + + elif msg.what == 'profileindex': + if len(self._profilenames) == 1: + + # This should be pretty hard to hit now with + # automatic local accounts. + _ba.playsound(_ba.getsound('error')) + else: + + # Pick the next player profile and assign our name + # and character based on that. + _ba.playsound(self._deek_sound) + self._profileindex = ((self._profileindex + msg.value) % + len(self._profilenames)) + self.update_from_profile() + + elif msg.what == 'character': + _ba.playsound(self._click_sound) + # update our index in our local list of characters + self._character_index = ((self._character_index + msg.value) % + len(self._character_names)) + self._update_text() + self._update_icon() + + elif msg.what == 'ready': + self._handle_ready_msg(bool(msg.value)) + + def _update_text(self) -> None: + assert self._text_node is not None + if self._ready: + + # Once we're ready, we've saved the name, so lets ask the system + # for it so we get appended numbers and stuff. + text = Lstr(value=self._sessionplayer.getname(full=True)) + text = Lstr(value='${A} (${B})', + subs=[('${A}', text), + ('${B}', Lstr(resource='readyText'))]) + else: + text = Lstr(value=self._getname(full=True)) + + can_switch_teams = len(self.lobby.sessionteams) > 1 + + # Flash as we're coming in. + fin_color = _ba.safecolor(self.get_color()) + (1, ) + if not self._inited: + animate_array(self._text_node, 'color', 4, { + 0.15: fin_color, + 0.25: (2, 2, 2, 1), + 0.35: fin_color + }) + else: + + # Blend if we're in teams mode; switch instantly otherwise. + if can_switch_teams: + animate_array(self._text_node, 'color', 4, { + 0: self._text_node.color, + 0.1: fin_color + }) + else: + self._text_node.color = fin_color + + self._text_node.text = text + + def get_color(self) -> Sequence[float]: + """Return the currently selected color.""" + val: Sequence[float] + if self.lobby.use_team_colors: + val = self.lobby.sessionteams[self._selected_team_index].color + else: + val = self._color + if len(val) != 3: + print('get_color: ignoring invalid color of len', len(val)) + val = (0, 1, 0) + return val + + def get_highlight(self) -> Sequence[float]: + """Return the currently selected highlight.""" + if self._profilenames[self._profileindex] == '_edit': + return 0, 1, 0 + + # If we're using team colors we wanna make sure our highlight color + # isn't too close to any other team's color. + highlight = list(self._highlight) + if self.lobby.use_team_colors: + for i, sessionteam in enumerate(self.lobby.sessionteams): + if i != self._selected_team_index: + + # Find the dominant component of this sessionteam's color + # and adjust ours so that the component is + # not super-dominant. + max_val = 0.0 + max_index = 0 + for j in range(3): + if sessionteam.color[j] > max_val: + max_val = sessionteam.color[j] + max_index = j + that_color_for_us = highlight[max_index] + our_second_biggest = max(highlight[(max_index + 1) % 3], + highlight[(max_index + 2) % 3]) + diff = (that_color_for_us - our_second_biggest) + if diff > 0: + highlight[max_index] -= diff * 0.6 + highlight[(max_index + 1) % 3] += diff * 0.3 + highlight[(max_index + 2) % 3] += diff * 0.2 + return highlight + + def getplayer(self) -> ba.SessionPlayer: + """Return the player associated with this chooser.""" + return self._sessionplayer + + def _update_icon(self) -> None: + if self._profilenames[self._profileindex] == '_edit': + tex = _ba.gettexture('black') + tint_tex = _ba.gettexture('black') + self.icon.color = (1, 1, 1) + self.icon.texture = tex + self.icon.tint_texture = tint_tex + self.icon.tint_color = (0, 1, 0) + return + + try: + tex_name = (_ba.app.spaz_appearances[self._character_names[ + self._character_index]].icon_texture) + tint_tex_name = (_ba.app.spaz_appearances[self._character_names[ + self._character_index]].icon_mask_texture) + except Exception: + print_exception('Error updating char icon list') + tex_name = 'neoSpazIcon' + tint_tex_name = 'neoSpazIconColorMask' + + tex = _ba.gettexture(tex_name) + tint_tex = _ba.gettexture(tint_tex_name) + + self.icon.color = (1, 1, 1) + self.icon.texture = tex + self.icon.tint_texture = tint_tex + clr = self.get_color() + clr2 = self.get_highlight() + + can_switch_teams = len(self.lobby.sessionteams) > 1 + + # If we're initing, flash. + if not self._inited: + animate_array(self.icon, 'color', 3, { + 0.15: (1, 1, 1), + 0.25: (2, 2, 2), + 0.35: (1, 1, 1) + }) + + # Blend in teams mode; switch instantly in ffa-mode. + if can_switch_teams: + animate_array(self.icon, 'tint_color', 3, { + 0: self.icon.tint_color, + 0.1: clr + }) + else: + self.icon.tint_color = clr + self.icon.tint2_color = clr2 + + # Store the icon info the the player. + self._sessionplayer.set_icon_info(tex_name, tint_tex_name, clr, clr2) + + +class Lobby: + """Container for ba.Choosers. + + Category: Gameplay Classes + """ + + def __del__(self) -> None: + + # Reset any players that still have a chooser in us. + # (should allow the choosers to die). + sessionplayers = [ + c.sessionplayer for c in self.choosers if c.sessionplayer + ] + for sessionplayer in sessionplayers: + sessionplayer.resetinput() + + def __init__(self) -> None: + from ba._team import SessionTeam + from ba._coopsession import CoopSession + session = _ba.getsession() + self._use_team_colors = session.use_team_colors + if session.use_teams: + self._sessionteams = [ + weakref.ref(team) for team in session.sessionteams + ] + else: + self._dummy_teams = SessionTeam() + self._sessionteams = [weakref.ref(self._dummy_teams)] + v_offset = (-150 if isinstance(session, CoopSession) else -50) + self.choosers: List[Chooser] = [] + self.base_v_offset = v_offset + self.update_positions() + self._next_add_team = 0 + self.character_names_local_unlocked: List[str] = [] + self._vpos = 0 + + # Grab available profiles. + self.reload_profiles() + + self._join_info_text = None + + @property + def next_add_team(self) -> int: + """(internal)""" + return self._next_add_team + + @property + def use_team_colors(self) -> bool: + """A bool for whether this lobby is using team colors. + + If False, inidividual player colors are used instead. + """ + return self._use_team_colors + + @property + def sessionteams(self) -> List[ba.SessionTeam]: + """ba.SessionTeams available in this lobby.""" + allteams = [] + for tref in self._sessionteams: + team = tref() + assert team is not None + allteams.append(team) + return allteams + + def get_choosers(self) -> List[Chooser]: + """Return the lobby's current choosers.""" + return self.choosers + + def create_join_info(self) -> JoinInfo: + """Create a display of on-screen information for joiners. + + (how to switch teams, players, etc.) + Intended for use in initial joining-screens. + """ + return JoinInfo(self) + + def reload_profiles(self) -> None: + """Reload available player profiles.""" + # pylint: disable=cyclic-import + from bastd.actor.spazappearance import get_appearances + + # We may have gained or lost character names if the user + # bought something; reload these too. + self.character_names_local_unlocked = get_appearances() + self.character_names_local_unlocked.sort(key=lambda x: x.lower()) + + # Do any overall prep we need to such as creating account profile. + _ba.app.accounts.ensure_have_account_player_profile() + for chooser in self.choosers: + try: + chooser.reload_profiles() + chooser.update_from_profile() + except Exception: + print_exception('Error reloading profiles.') + + def update_positions(self) -> None: + """Update positions for all choosers.""" + self._vpos = -100 + self.base_v_offset + for chooser in self.choosers: + chooser.set_vpos(self._vpos) + chooser.update_position() + self._vpos -= 48 + + def check_all_ready(self) -> bool: + """Return whether all choosers are marked ready.""" + return all(chooser.ready for chooser in self.choosers) + + def add_chooser(self, sessionplayer: ba.SessionPlayer) -> None: + """Add a chooser to the lobby for the provided player.""" + self.choosers.append( + Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self)) + self._next_add_team = (self._next_add_team + 1) % len( + self._sessionteams) + self._vpos -= 48 + + def remove_chooser(self, player: ba.SessionPlayer) -> None: + """Remove a single player's chooser; does not kick them. + + This is used when a player enters the game and no longer + needs a chooser.""" + found = False + chooser = None + for chooser in self.choosers: + if chooser.getplayer() is player: + found = True + + # Mark it as dead since there could be more + # change-commands/etc coming in still for it; + # want to avoid duplicate player-adds/etc. + chooser.set_dead(True) + self.choosers.remove(chooser) + break + if not found: + print_error(f'remove_chooser did not find player {player}') + elif chooser in self.choosers: + print_error(f'chooser remains after removal for {player}') + self.update_positions() + + def remove_all_choosers(self) -> None: + """Remove all choosers without kicking players. + + This is called after all players check in and enter a game. + """ + self.choosers = [] + self.update_positions() + + def remove_all_choosers_and_kick_players(self) -> None: + """Remove all player choosers and kick attached players.""" + + # Copy the list; it can change under us otherwise. + for chooser in list(self.choosers): + if chooser.sessionplayer: + chooser.sessionplayer.remove_from_game() + self.remove_all_choosers() diff --git a/dist/ba_data/python/ba/_map.py b/dist/ba_data/python/ba/_map.py new file mode 100644 index 0000000..376f3a2 --- /dev/null +++ b/dist/ba_data/python/ba/_map.py @@ -0,0 +1,431 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Map related functionality.""" +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import _ba +from ba import _math +from ba._actor import Actor + +if TYPE_CHECKING: + from typing import Set, List, Type, Optional, Sequence, Any, Tuple + import ba + + +def preload_map_preview_media() -> None: + """Preload media needed for map preview UIs. + + Category: Asset Functions + """ + _ba.getmodel('level_select_button_opaque') + _ba.getmodel('level_select_button_transparent') + for maptype in list(_ba.app.maps.values()): + map_tex_name = maptype.get_preview_texture_name() + if map_tex_name is not None: + _ba.gettexture(map_tex_name) + + +def get_filtered_map_name(name: str) -> str: + """Filter a map name to account for name changes, etc. + + Category: Asset Functions + + This can be used to support old playlists, etc. + """ + # Some legacy name fallbacks... can remove these eventually. + if name in ('AlwaysLand', 'Happy Land'): + name = 'Happy Thoughts' + if name == 'Hockey Arena': + name = 'Hockey Stadium' + return name + + +def get_map_display_string(name: str) -> ba.Lstr: + """Return a ba.Lstr for displaying a given map\'s name. + + Category: Asset Functions + """ + from ba import _language + return _language.Lstr(translate=('mapsNames', name)) + + +def getmaps(playtype: str) -> List[str]: + """Return a list of ba.Map types supporting a playtype str. + + Category: Asset Functions + + Maps supporting a given playtype must provide a particular set of + features and lend themselves to a certain style of play. + + Play Types: + + 'melee' + General fighting map. + Has one or more 'spawn' locations. + + 'team_flag' + For games such as Capture The Flag where each team spawns by a flag. + Has two or more 'spawn' locations, each with a corresponding 'flag' + location (based on index). + + 'single_flag' + For games such as King of the Hill or Keep Away where multiple teams + are fighting over a single flag. + Has two or more 'spawn' locations and 1 'flag_default' location. + + 'conquest' + For games such as Conquest where flags are spread throughout the map + - has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations. + + 'king_of_the_hill' - has 2+ 'spawn' locations, 1+ 'flag_default' locations, + and 1+ 'powerup_spawn' locations + + 'hockey' + For hockey games. + Has two 'goal' locations, corresponding 'spawn' locations, and one + 'flag_default' location (for where puck spawns) + + 'football' + For football games. + Has two 'goal' locations, corresponding 'spawn' locations, and one + 'flag_default' location (for where flag/ball/etc. spawns) + + 'race' + For racing games where players much touch each region in order. + Has two or more 'race_point' locations. + """ + return sorted(key for key, val in _ba.app.maps.items() + if playtype in val.get_play_types()) + + +def get_unowned_maps() -> List[str]: + """Return the list of local maps not owned by the current account. + + Category: Asset Functions + """ + from ba import _store + unowned_maps: Set[str] = set() + if not _ba.app.headless_mode: + for map_section in _store.get_store_layout()['maps']: + for mapitem in map_section['items']: + if not _ba.get_purchased(mapitem): + m_info = _store.get_store_item(mapitem) + unowned_maps.add(m_info['map_type'].name) + return sorted(unowned_maps) + + +def get_map_class(name: str) -> Type[ba.Map]: + """Return a map type given a name. + + Category: Asset Functions + """ + name = get_filtered_map_name(name) + try: + return _ba.app.maps[name] + except KeyError: + from ba import _error + raise _error.NotFoundError(f"Map not found: '{name}'") from None + + +class Map(Actor): + """A game map. + + Category: Gameplay Classes + + Consists of a collection of terrain nodes, metadata, and other + functionality comprising a game map. + """ + defs: Any = None + name = 'Map' + _playtypes: List[str] = [] + + @classmethod + def preload(cls) -> None: + """Preload map media. + + This runs the class's on_preload() method as needed to prep it to run. + Preloading should generally be done in a ba.Activity's __init__ method. + Note that this is a classmethod since it is not operate on map + instances but rather on the class itself before instances are made + """ + activity = _ba.getactivity() + if cls not in activity.preloads: + activity.preloads[cls] = cls.on_preload() + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + @classmethod + def get_preview_texture_name(cls) -> Optional[str]: + """Return the name of the preview texture for this map.""" + return None + + @classmethod + def on_preload(cls) -> Any: + """Called when the map is being preloaded. + + It should return any media/data it requires to operate + """ + return None + + @classmethod + def getname(cls) -> str: + """Return the unique name of this map, in English.""" + return cls.name + + @classmethod + def get_music_type(cls) -> Optional[ba.MusicType]: + """Return a music-type string that should be played on this map. + + If None is returned, default music will be used. + """ + return None + + def __init__(self, + vr_overlay_offset: Optional[Sequence[float]] = None) -> None: + """Instantiate a map.""" + from ba import _gameutils + super().__init__() + + # This is expected to always be a ba.Node object (whether valid or not) + # should be set to something meaningful by child classes. + self.node: Optional[_ba.Node] = None + + # Make our class' preload-data available to us + # (and instruct the user if we weren't preloaded properly). + try: + self.preloaddata = _ba.getactivity().preloads[type(self)] + except Exception as exc: + from ba import _error + raise _error.NotFoundError( + 'Preload data not found for ' + str(type(self)) + + '; make sure to call the type\'s preload()' + ' staticmethod in the activity constructor') from exc + + # Set various globals. + gnode = _ba.getactivity().globalsnode + import ba + + + + # I DONT THINK YOU REALLY WANT TO REMOVE MY NAME , DO YOU ? + self.hg=ba.NodeActor( + _ba.newnode('text', + attrs={ + 'text': "Smoothy Build\n v1.0", + + 'flatness': 1.0, + 'h_align': 'center', + 'v_attach':'bottom', + 'h_attach':'right', + 'scale':0.7, + 'position':(-60,23), + 'color':(0.3,0.3,0.3) + })) + + + # Set area-of-interest bounds. + aoi_bounds = self.get_def_bound_box('area_of_interest_bounds') + if aoi_bounds is None: + print('WARNING: no "aoi_bounds" found for map:', self.getname()) + aoi_bounds = (-1, -1, -1, 1, 1, 1) + gnode.area_of_interest_bounds = aoi_bounds + + # Set map bounds. + map_bounds = self.get_def_bound_box('map_bounds') + if map_bounds is None: + print('WARNING: no "map_bounds" found for map:', self.getname()) + map_bounds = (-30, -10, -30, 30, 100, 30) + _ba.set_map_bounds(map_bounds) + + # Set shadow ranges. + try: + gnode.shadow_range = [ + self.defs.points[v][1] for v in [ + 'shadow_lower_bottom', 'shadow_lower_top', + 'shadow_upper_bottom', 'shadow_upper_top' + ] + ] + except Exception: + pass + + # In vr, set a fixed point in space for the overlay to show up at. + # By default we use the bounds center but allow the map to override it. + center = ((aoi_bounds[0] + aoi_bounds[3]) * 0.5, + (aoi_bounds[1] + aoi_bounds[4]) * 0.5, + (aoi_bounds[2] + aoi_bounds[5]) * 0.5) + if vr_overlay_offset is not None: + center = (center[0] + vr_overlay_offset[0], + center[1] + vr_overlay_offset[1], + center[2] + vr_overlay_offset[2]) + gnode.vr_overlay_center = center + gnode.vr_overlay_center_enabled = True + + self.spawn_points = (self.get_def_points('spawn') + or [(0, 0, 0, 0, 0, 0)]) + self.ffa_spawn_points = (self.get_def_points('ffa_spawn') + or [(0, 0, 0, 0, 0, 0)]) + self.spawn_by_flag_points = (self.get_def_points('spawn_by_flag') + or [(0, 0, 0, 0, 0, 0)]) + self.flag_points = self.get_def_points('flag') or [(0, 0, 0)] + + # We just want points. + self.flag_points = [p[:3] for p in self.flag_points] + self.flag_points_default = (self.get_def_point('flag_default') + or (0, 1, 0)) + self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [ + (0, 0, 0) + ] + + # We just want points. + self.powerup_spawn_points = ([ + p[:3] for p in self.powerup_spawn_points + ]) + self.tnt_points = self.get_def_points('tnt') or [] + + # We just want points. + self.tnt_points = [p[:3] for p in self.tnt_points] + + self.is_hockey = False + self.is_flying = False + + # FIXME: this should be part of game; not map. + self._next_ffa_start_index = 0 + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + """Return whether the provided point is near an edge of the map. + + Simple bot logic uses this call to determine if they + are approaching a cliff or wall. If this returns True they will + generally not walk/run any farther away from the origin. + If 'running' is True, the buffer should be a bit larger. + """ + del point, running # Unused. + return False + + def get_def_bound_box( + self, name: str + ) -> Optional[Tuple[float, float, float, float, float, float]]: + """Return a 6 member bounds tuple or None if it is not defined.""" + try: + box = self.defs.boxes[name] + return (box[0] - box[6] / 2.0, box[1] - box[7] / 2.0, + box[2] - box[8] / 2.0, box[0] + box[6] / 2.0, + box[1] + box[7] / 2.0, box[2] + box[8] / 2.0) + except Exception: + return None + + def get_def_point(self, name: str) -> Optional[Sequence[float]]: + """Return a single defined point or a default value in its absence.""" + val = self.defs.points.get(name) + return (None if val is None else + _math.vec3validate(val) if __debug__ else val) + + def get_def_points(self, name: str) -> List[Sequence[float]]: + """Return a list of named points. + + Return as many sequential ones are defined (flag1, flag2, flag3), etc. + If none are defined, returns an empty list. + """ + point_list = [] + if self.defs and name + '1' in self.defs.points: + i = 1 + while name + str(i) in self.defs.points: + pts = self.defs.points[name + str(i)] + if len(pts) == 6: + point_list.append(pts) + else: + if len(pts) != 3: + raise ValueError('invalid point') + point_list.append(pts + (0, 0, 0)) + i += 1 + return point_list + + def get_start_position(self, team_index: int) -> Sequence[float]: + """Return a random starting position for the given team index.""" + pnt = self.spawn_points[team_index % len(self.spawn_points)] + x_range = (-0.5, 0.5) if pnt[3] == 0.0 else (-pnt[3], pnt[3]) + z_range = (-0.5, 0.5) if pnt[5] == 0.0 else (-pnt[5], pnt[5]) + pnt = (pnt[0] + random.uniform(*x_range), pnt[1], + pnt[2] + random.uniform(*z_range)) + return pnt + + def get_ffa_start_position( + self, players: Sequence[ba.Player]) -> Sequence[float]: + """Return a random starting position in one of the FFA spawn areas. + + If a list of ba.Players is provided; the returned points will be + as far from these players as possible. + """ + + # Get positions for existing players. + player_pts = [] + for player in players: + if player.is_alive(): + player_pts.append(player.position) + + def _getpt() -> Sequence[float]: + point = self.ffa_spawn_points[self._next_ffa_start_index] + self._next_ffa_start_index = ((self._next_ffa_start_index + 1) % + len(self.ffa_spawn_points)) + x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3]) + z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5]) + point = (point[0] + random.uniform(*x_range), point[1], + point[2] + random.uniform(*z_range)) + return point + + if not player_pts: + return _getpt() + + # Let's calc several start points and then pick whichever is + # farthest from all existing players. + farthestpt_dist = -1.0 + farthestpt = None + for _i in range(10): + testpt = _ba.Vec3(_getpt()) + closest_player_dist = 9999.0 + for ppt in player_pts: + dist = (ppt - testpt).length() + if dist < closest_player_dist: + closest_player_dist = dist + if closest_player_dist > farthestpt_dist: + farthestpt_dist = closest_player_dist + farthestpt = testpt + assert farthestpt is not None + return tuple(farthestpt) + + def get_flag_position(self, team_index: int = None) -> Sequence[float]: + """Return a flag position on the map for the given team index. + + Pass None to get the default flag point. + (used for things such as king-of-the-hill) + """ + if team_index is None: + return self.flag_points_default[:3] + return self.flag_points[team_index % len(self.flag_points)][:3] + + def exists(self) -> bool: + return bool(self.node) + + def handlemessage(self, msg: Any) -> Any: + from ba import _messages + if isinstance(msg, _messages.DieMessage): + if self.node: + self.node.delete() + else: + return super().handlemessage(msg) + return None + + +def register_map(maptype: Type[Map]) -> None: + """Register a map class with the game.""" + if maptype.name in _ba.app.maps: + raise RuntimeError('map "' + maptype.name + '" already registered') + _ba.app.maps[maptype.name] = maptype diff --git a/dist/ba_data/python/ba/_math.py b/dist/ba_data/python/ba/_math.py new file mode 100644 index 0000000..98834ee --- /dev/null +++ b/dist/ba_data/python/ba/_math.py @@ -0,0 +1,55 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Math related functionality.""" + +from __future__ import annotations + +from collections import abc +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Tuple, Sequence + + +def vec3validate(value: Sequence[float]) -> Sequence[float]: + """Ensure a value is valid for use as a Vec3. + + category: General Utility Functions + + Raises a TypeError exception if not. + Valid values include any type of sequence consisting of 3 numeric values. + Returns the same value as passed in (but with a definite type + so this can be used to disambiguate 'Any' types). + Generally this should be used in 'if __debug__' or assert clauses + to keep runtime overhead minimal. + """ + from numbers import Number + if not isinstance(value, abc.Sequence): + raise TypeError(f"Expected a sequence; got {type(value)}") + if len(value) != 3: + raise TypeError(f"Expected a length-3 sequence (got {len(value)})") + if not all(isinstance(i, Number) for i in value): + raise TypeError(f"Non-numeric value passed for vec3: {value}") + return value + + +def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool: + """Return whether a given point is within a given box. + + category: General Utility Functions + + For use with standard def boxes (position|rotate|scale). + """ + return ((abs(pnt[0] - box[0]) <= box[6] * 0.5) + and (abs(pnt[1] - box[1]) <= box[7] * 0.5) + and (abs(pnt[2] - box[2]) <= box[8] * 0.5)) + + +def normalized_color(color: Sequence[float]) -> Tuple[float, ...]: + """Scale a color so its largest value is 1; useful for coloring lights. + + category: General Utility Functions + """ + color_biased = tuple(max(c, 0.01) for c in color) # account for black + mult = 1.0 / max(color_biased) + return tuple(c * mult for c in color_biased) diff --git a/dist/ba_data/python/ba/_messages.py b/dist/ba_data/python/ba/_messages.py new file mode 100644 index 0000000..c57ea55 --- /dev/null +++ b/dist/ba_data/python/ba/_messages.py @@ -0,0 +1,312 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines some standard message objects for use with handlemessage() calls.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypeVar +from enum import Enum + +import _ba + +if TYPE_CHECKING: + from typing import Sequence, Optional, Type, Any + import ba + + +class _UnhandledType: + pass + + +# A special value that should be returned from handlemessage() +# functions for unhandled message types. This may result +# in fallback message types being attempted/etc. +UNHANDLED = _UnhandledType() + + +@dataclass +class OutOfBoundsMessage: + """A message telling an object that it is out of bounds. + + Category: Message Classes + """ + + +class DeathType(Enum): + """A reason for a death. + + Category: Enums + """ + GENERIC = 'generic' + OUT_OF_BOUNDS = 'out_of_bounds' + IMPACT = 'impact' + FALL = 'fall' + REACHED_GOAL = 'reached_goal' + LEFT_GAME = 'left_game' + + +@dataclass +class DieMessage: + """A message telling an object to die. + + Category: Message Classes + + Most ba.Actors respond to this. + + Attributes: + + immediate + If this is set to True, the actor should disappear immediately. + This is for 'removing' stuff from the game more so than 'killing' + it. If False, the actor should die a 'normal' death and can take + its time with lingering corpses, sound effects, etc. + + how + The particular reason for death. + + """ + immediate: bool = False + how: DeathType = DeathType.GENERIC + + +PlayerType = TypeVar('PlayerType', bound='ba.Player') + + +class PlayerDiedMessage: + """A message saying a ba.Player has died. + + category: Message Classes + + Attributes: + + killed + If True, the player was killed; + If False, they left the game or the round ended. + + how + The particular type of death. + """ + killed: bool + how: ba.DeathType + + def __init__(self, player: ba.Player, was_killed: bool, + killerplayer: Optional[ba.Player], how: ba.DeathType): + """Instantiate a message with the given values.""" + + # Invalid refs should never be passed as args. + assert player.exists() + self._player = player + + # Invalid refs should never be passed as args. + assert killerplayer is None or killerplayer.exists() + self._killerplayer = killerplayer + self.killed = was_killed + self.how = how + + def getkillerplayer(self, + playertype: Type[PlayerType]) -> Optional[PlayerType]: + """Return the ba.Player responsible for the killing, if any. + + Pass the Player type being used by the current game. + """ + assert isinstance(self._killerplayer, (playertype, type(None))) + return self._killerplayer + + def getplayer(self, playertype: Type[PlayerType]) -> PlayerType: + """Return the ba.Player that died. + + The type of player for the current activity should be passed so that + the type-checker properly identifies the returned value as one. + """ + player: Any = self._player + assert isinstance(player, playertype) + + # We should never be delivering invalid refs. + # (could theoretically happen if someone holds on to us) + assert player.exists() + return player + + +@dataclass +class StandMessage: + """A message telling an object to move to a position in space. + + Category: Message Classes + + Used when teleporting players to home base, etc. + + Attributes: + + position + Where to move to. + + angle + The angle to face (in degrees) + """ + position: Sequence[float] = (0.0, 0.0, 0.0) + angle: float = 0.0 + + +@dataclass +class PickUpMessage: + """Tells an object that it has picked something up. + + Category: Message Classes + + Attributes: + + node + The ba.Node that is getting picked up. + """ + node: ba.Node + + +@dataclass +class DropMessage: + """Tells an object that it has dropped what it was holding. + + Category: Message Classes + """ + + +@dataclass +class PickedUpMessage: + """Tells an object that it has been picked up by something. + + Category: Message Classes + + Attributes: + + node + The ba.Node doing the picking up. + """ + node: ba.Node + + +@dataclass +class DroppedMessage: + """Tells an object that it has been dropped. + + Category: Message Classes + + Attributes: + + node + The ba.Node doing the dropping. + """ + node: ba.Node + + +@dataclass +class ShouldShatterMessage: + """Tells an object that it should shatter. + + Category: Message Classes + """ + + +@dataclass +class ImpactDamageMessage: + """Tells an object that it has been jarred violently. + + Category: Message Classes + + Attributes: + + intensity + The intensity of the impact. + """ + intensity: float + + +@dataclass +class FreezeMessage: + """Tells an object to become frozen. + + Category: Message Classes + + As seen in the effects of an ice ba.Bomb. + """ + + +@dataclass +class ThawMessage: + """Tells an object to stop being frozen. + + Category: Message Classes + """ + + +@dataclass +class CelebrateMessage: + """Tells an object to celebrate. + + Category: Message Classes + + Attributes: + + duration + Amount of time to celebrate in seconds. + """ + duration: float = 10.0 + + +class HitMessage: + """Tells an object it has been hit in some way. + + Category: Message Classes + + This is used by punches, explosions, etc to convey + their effect to a target. + """ + + def __init__(self, + srcnode: ba.Node = None, + pos: Sequence[float] = None, + velocity: Sequence[float] = None, + magnitude: float = 1.0, + velocity_magnitude: float = 0.0, + radius: float = 1.0, + source_player: ba.Player = None, + kick_back: float = 1.0, + flat_damage: float = None, + hit_type: str = 'generic', + force_direction: Sequence[float] = None, + hit_subtype: str = 'default'): + """Instantiate a message with given values.""" + + self.srcnode = srcnode + self.pos = pos if pos is not None else _ba.Vec3() + self.velocity = velocity if velocity is not None else _ba.Vec3() + self.magnitude = magnitude + self.velocity_magnitude = velocity_magnitude + self.radius = radius + + # We should not be getting passed an invalid ref. + assert source_player is None or source_player.exists() + self._source_player = source_player + self.kick_back = kick_back + self.flat_damage = flat_damage + self.hit_type = hit_type + self.hit_subtype = hit_subtype + self.force_direction = (force_direction + if force_direction is not None else velocity) + + def get_source_player( + self, playertype: Type[PlayerType]) -> Optional[PlayerType]: + """Return the source-player if one exists and is the provided type.""" + player: Any = self._source_player + + # We should not be delivering invalid refs. + # (we could translate to None here but technically we are changing + # the message delivered which seems wrong) + assert player is None or player.exists() + + # Return the player *only* if they're the type given. + return player if isinstance(player, playertype) else None + + +@dataclass +class PlayerProfilesChangedMessage: + """Signals player profiles may have changed and should be reloaded.""" diff --git a/dist/ba_data/python/ba/_meta.py b/dist/ba_data/python/ba/_meta.py new file mode 100644 index 0000000..0386552 --- /dev/null +++ b/dist/ba_data/python/ba/_meta.py @@ -0,0 +1,418 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to dynamic discoverability of classes.""" + +from __future__ import annotations + +import os +import time +import pathlib +import threading +from typing import TYPE_CHECKING +from dataclasses import dataclass, field + +import _ba + +if TYPE_CHECKING: + from typing import Dict, List, Tuple, Union, Optional, Type, Set + import ba + +# The meta api version of this build of the game. +# Only packages and modules requiring this exact api version +# will be considered when scanning directories. +# See: https://ballistica.net/wiki/Meta-Tags +CURRENT_API_VERSION = 6 + + +@dataclass +class ScanResults: + """Final results from a metadata scan.""" + games: List[str] = field(default_factory=list) + plugins: List[str] = field(default_factory=list) + keyboards: List[str] = field(default_factory=list) + errors: str = '' + warnings: str = '' + + +class MetadataSubsystem: + """Subsystem for working with script metadata in the app. + + Category: App Classes + + Access the single shared instance of this class at 'ba.app.meta'. + """ + + def __init__(self) -> None: + self.metascan: Optional[ScanResults] = None + + def on_app_launch(self) -> None: + """Should be called when the app is done bootstrapping.""" + + # Start scanning for things exposed via ba_meta. + self.start_scan() + + def start_scan(self) -> None: + """Begin scanning script directories for scripts containing metadata. + + Should be called only once at launch.""" + app = _ba.app + if self.metascan is not None: + print('WARNING: meta scan run more than once.') + pythondirs = [app.python_directory_app, app.python_directory_user] + thread = ScanThread(pythondirs) + thread.start() + + def handle_scan_results(self, results: ScanResults) -> None: + """Called in the game thread with results of a completed scan.""" + + from ba._language import Lstr + from ba._plugin import PotentialPlugin + + # Warnings generally only get printed locally for users' benefit + # (things like out-of-date scripts being ignored, etc.) + # Errors are more serious and will get included in the regular log + # warnings = results.get('warnings', '') + # errors = results.get('errors', '') + if results.warnings != '' or results.errors != '': + import textwrap + _ba.screenmessage(Lstr(resource='scanScriptsErrorText'), + color=(1, 0, 0)) + _ba.playsound(_ba.getsound('error')) + if results.warnings != '': + _ba.log(textwrap.indent(results.warnings, + 'Warning (meta-scan): '), + to_server=False) + if results.errors != '': + _ba.log(textwrap.indent(results.errors, 'Error (meta-scan): ')) + + # Handle plugins. + plugs = _ba.app.plugins + config_changed = False + found_new = False + plugstates: Dict[str, Dict] = _ba.app.config.setdefault('Plugins', {}) + assert isinstance(plugstates, dict) + + # Create a potential-plugin for each class we found in the scan. + for class_path in results.plugins: + plugs.potential_plugins.append( + PotentialPlugin(display_name=Lstr(value=class_path), + class_path=class_path, + available=True)) + if class_path not in plugstates: + if _ba.app.headless_mode: + # If we running in headless mode, enable plugin by default + # to allow server admins to get their modified build + # working 'out-of-the-box', without manually updating the + # config. + plugstates[class_path] = {'enabled': True} + else: + # If we running in normal mode, disable plugin by default + # (user can enable it later). + plugstates[class_path] = {'enabled': False} + config_changed = True + found_new = True + + # Also add a special one for any plugins set to load but *not* found + # in the scan (this way they will show up in the UI so we can disable + # them) + for class_path, plugstate in plugstates.items(): + enabled = plugstate.get('enabled', False) + assert isinstance(enabled, bool) + if enabled and class_path not in results.plugins: + plugs.potential_plugins.append( + PotentialPlugin(display_name=Lstr(value=class_path), + class_path=class_path, + available=False)) + + plugs.potential_plugins.sort(key=lambda p: p.class_path) + + if found_new: + _ba.screenmessage(Lstr(resource='pluginsDetectedText'), + color=(0, 1, 0)) + _ba.playsound(_ba.getsound('ding')) + + if config_changed: + _ba.app.config.commit() + + def get_scan_results(self) -> ScanResults: + """Return meta scan results; block if the scan is not yet complete.""" + if self.metascan is None: + print('WARNING: ba.meta.get_scan_results()' + ' called before scan completed.' + ' This can cause hitches.') + + # Now wait a bit for the scan to complete. + # Eventually error though if it doesn't. + starttime = time.time() + while self.metascan is None: + time.sleep(0.05) + if time.time() - starttime > 10.0: + raise TimeoutError( + 'timeout waiting for meta scan to complete.') + return self.metascan + + def get_game_types(self) -> List[Type[ba.GameActivity]]: + """Return available game types.""" + from ba._general import getclass + from ba._gameactivity import GameActivity + gameclassnames = self.get_scan_results().games + gameclasses = [] + for gameclassname in gameclassnames: + try: + cls = getclass(gameclassname, GameActivity) + gameclasses.append(cls) + except Exception: + from ba import _error + _error.print_exception('error importing ' + str(gameclassname)) + unowned = self.get_unowned_game_types() + return [cls for cls in gameclasses if cls not in unowned] + + def get_unowned_game_types(self) -> Set[Type[ba.GameActivity]]: + """Return present game types not owned by the current account.""" + try: + from ba import _store + unowned_games: Set[Type[ba.GameActivity]] = set() + if not _ba.app.headless_mode: + for section in _store.get_store_layout()['minigames']: + for mname in section['items']: + if not _ba.get_purchased(mname): + m_info = _store.get_store_item(mname) + unowned_games.add(m_info['gametype']) + return unowned_games + except Exception: + from ba import _error + _error.print_exception('error calcing un-owned games') + return set() + + +class ScanThread(threading.Thread): + """Thread to scan script dirs for metadata.""" + + def __init__(self, dirs: List[str]): + super().__init__() + self._dirs = dirs + + def run(self) -> None: + from ba._general import Call + try: + scan = DirectoryScan(self._dirs) + scan.scan() + results = scan.results + except Exception as exc: + results = ScanResults(errors=f'Scan exception: {exc}') + + # Push a call to the game thread to print warnings/errors + # or otherwise deal with scan results. + _ba.pushcall(Call(_ba.app.meta.handle_scan_results, results), + from_other_thread=True) + + # We also, however, immediately make results available. + # This is because the game thread may be blocked waiting + # for them so we can't push a call or we'd get deadlock. + _ba.app.meta.metascan = results + + +class DirectoryScan: + """Handles scanning directories for metadata.""" + + def __init__(self, paths: List[str]): + """Given one or more paths, parses available meta information. + + It is assumed that these paths are also in PYTHONPATH. + It is also assumed that any subdirectories are Python packages. + """ + + # Skip non-existent paths completely. + self.paths = [pathlib.Path(p) for p in paths if os.path.isdir(p)] + self.results = ScanResults() + + def _get_path_module_entries( + self, path: pathlib.Path, subpath: Union[str, pathlib.Path], + modules: List[Tuple[pathlib.Path, pathlib.Path]]) -> None: + """Scan provided path and add module entries to provided list.""" + try: + # Special case: let's save some time and skip the whole 'ba' + # package since we know it doesn't contain any meta tags. + fullpath = pathlib.Path(path, subpath) + entries = [(path, pathlib.Path(subpath, name)) + for name in os.listdir(fullpath) if name != 'ba'] + except PermissionError: + # Expected sometimes. + entries = [] + except Exception as exc: + # Unexpected; report this. + self.results.errors += f'{exc}\n' + entries = [] + + # Now identify python packages/modules out of what we found. + for entry in entries: + if entry[1].name.endswith('.py'): + modules.append(entry) + elif (pathlib.Path(entry[0], entry[1]).is_dir() and pathlib.Path( + entry[0], entry[1], '__init__.py').is_file()): + modules.append(entry) + + def scan(self) -> None: + """Scan provided paths.""" + modules: List[Tuple[pathlib.Path, pathlib.Path]] = [] + for path in self.paths: + self._get_path_module_entries(path, '', modules) + for moduledir, subpath in modules: + try: + self.scan_module(moduledir, subpath) + except Exception: + import traceback + self.results.warnings += ("Error scanning '" + str(subpath) + + "': " + traceback.format_exc() + + '\n') + # Sort our results + self.results.games.sort() + self.results.plugins.sort() + + def scan_module(self, moduledir: pathlib.Path, + subpath: pathlib.Path) -> None: + """Scan an individual module and add the findings to results.""" + if subpath.name.endswith('.py'): + fpath = pathlib.Path(moduledir, subpath) + ispackage = False + else: + fpath = pathlib.Path(moduledir, subpath, '__init__.py') + ispackage = True + with fpath.open() as infile: + flines = infile.readlines() + meta_lines = { + lnum: l[1:].split() + for lnum, l in enumerate(flines) if '# ba_meta ' in l + } + toplevel = len(subpath.parts) <= 1 + required_api = self.get_api_requirement(subpath, meta_lines, toplevel) + + # Top level modules with no discernible api version get ignored. + if toplevel and required_api is None: + return + + # If we find a module requiring a different api version, warn + # and ignore. + if required_api is not None and required_api != CURRENT_API_VERSION: + self.results.warnings += ( + f'Warning: {subpath} requires api {required_api} but' + f' we are running {CURRENT_API_VERSION}; ignoring module.\n') + return + + # Ok; can proceed with a full scan of this module. + self._process_module_meta_tags(subpath, flines, meta_lines) + + # If its a package, recurse into its subpackages. + if ispackage: + try: + submodules: List[Tuple[pathlib.Path, pathlib.Path]] = [] + self._get_path_module_entries(moduledir, subpath, submodules) + for submodule in submodules: + if submodule[1].name != '__init__.py': + self.scan_module(submodule[0], submodule[1]) + except Exception: + import traceback + self.results.warnings += ( + f"Error scanning '{subpath}': {traceback.format_exc()}\n") + + def _process_module_meta_tags(self, subpath: pathlib.Path, + flines: List[str], + meta_lines: Dict[int, List[str]]) -> None: + """Pull data from a module based on its ba_meta tags.""" + for lindex, mline in meta_lines.items(): + # meta_lines is just anything containing '# ba_meta '; make sure + # the ba_meta is in the right place. + if mline[0] != 'ba_meta': + self.results.warnings += ( + 'Warning: ' + str(subpath) + + ': malformed ba_meta statement on line ' + + str(lindex + 1) + '.\n') + elif (len(mline) == 4 and mline[1] == 'require' + and mline[2] == 'api'): + # Ignore 'require api X' lines in this pass. + pass + elif len(mline) != 3 or mline[1] != 'export': + # Currently we only support 'ba_meta export FOO'; + # complain for anything else we see. + self.results.warnings += ( + 'Warning: ' + str(subpath) + + ': unrecognized ba_meta statement on line ' + + str(lindex + 1) + '.\n') + else: + # Looks like we've got a valid export line! + modulename = '.'.join(subpath.parts) + if subpath.name.endswith('.py'): + modulename = modulename[:-3] + exporttype = mline[2] + export_class_name = self._get_export_class_name( + subpath, flines, lindex) + if export_class_name is not None: + classname = modulename + '.' + export_class_name + if exporttype == 'game': + self.results.games.append(classname) + elif exporttype == 'plugin': + self.results.plugins.append(classname) + elif exporttype == 'keyboard': + self.results.keyboards.append(classname) + else: + self.results.warnings += ( + 'Warning: ' + str(subpath) + + ': unrecognized export type "' + exporttype + + '" on line ' + str(lindex + 1) + '.\n') + + def _get_export_class_name(self, subpath: pathlib.Path, lines: List[str], + lindex: int) -> Optional[str]: + """Given line num of an export tag, returns its operand class name.""" + lindexorig = lindex + classname = None + while True: + lindex += 1 + if lindex >= len(lines): + break + lbits = lines[lindex].split() + if not lbits: + continue # Skip empty lines. + if lbits[0] != 'class': + break + if len(lbits) > 1: + cbits = lbits[1].split('(') + if len(cbits) > 1 and cbits[0].isidentifier(): + classname = cbits[0] + break # Success! + if classname is None: + self.results.warnings += ( + 'Warning: ' + str(subpath) + ': class definition not found' + ' below "ba_meta export" statement on line ' + + str(lindexorig + 1) + '.\n') + return classname + + def get_api_requirement(self, subpath: pathlib.Path, + meta_lines: Dict[int, List[str]], + toplevel: bool) -> Optional[int]: + """Return an API requirement integer or None if none present. + + Malformed api requirement strings will be logged as warnings. + """ + lines = [ + l for l in meta_lines.values() if len(l) == 4 and l[0] == 'ba_meta' + and l[1] == 'require' and l[2] == 'api' and l[3].isdigit() + ] + + # We're successful if we find exactly one properly formatted line. + if len(lines) == 1: + return int(lines[0][3]) + + # Ok; not successful. lets issue warnings for a few error cases. + if len(lines) > 1: + self.results.warnings += ( + 'Warning: ' + str(subpath) + + ': multiple "# ba_meta api require " lines found;' + ' ignoring module.\n') + elif not lines and toplevel and meta_lines: + # If we're a top-level module containing meta lines but + # no valid api require, complain. + self.results.warnings += ( + 'Warning: ' + str(subpath) + + ': no valid "# ba_meta api require " line found;' + ' ignoring module.\n') + return None diff --git a/dist/ba_data/python/ba/_multiteamsession.py b/dist/ba_data/python/ba/_multiteamsession.py new file mode 100644 index 0000000..910b72e --- /dev/null +++ b/dist/ba_data/python/ba/_multiteamsession.py @@ -0,0 +1,312 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to teams sessions.""" +from __future__ import annotations + +import copy +import random +from typing import TYPE_CHECKING + +import _ba +from ba._session import Session +from ba._error import NotFoundError, print_error + +if TYPE_CHECKING: + from typing import Optional, Any, Dict, List, Type, Sequence + import ba + +DEFAULT_TEAM_COLORS = ((0.1, 0.25, 1.0), (1.0, 0.25, 0.2)) +DEFAULT_TEAM_NAMES = ('Blue', 'Red') + + +class MultiTeamSession(Session): + """Common base class for ba.DualTeamSession and ba.FreeForAllSession. + + Category: Gameplay Classes + + Free-for-all-mode is essentially just teams-mode with each ba.Player having + their own ba.Team, so there is much overlap in functionality. + """ + + # These should be overridden. + _playlist_selection_var = 'UNSET Playlist Selection' + _playlist_randomize_var = 'UNSET Playlist Randomize' + _playlists_var = 'UNSET Playlists' + + def __init__(self) -> None: + """Set up playlists and launches a ba.Activity to accept joiners.""" + # pylint: disable=cyclic-import + from ba import _playlist + from bastd.activity.multiteamjoin import MultiTeamJoinActivity + app = _ba.app + cfg = app.config + + if self.use_teams: + team_names = cfg.get('Custom Team Names', DEFAULT_TEAM_NAMES) + team_colors = cfg.get('Custom Team Colors', DEFAULT_TEAM_COLORS) + else: + team_names = None + team_colors = None + + # print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.') + depsets: Sequence[ba.DependencySet] = [] + + super().__init__(depsets, + team_names=team_names, + team_colors=team_colors, + min_players=1, + max_players=self.get_max_players()) + + self._series_length = app.teams_series_length + self._ffa_series_length = app.ffa_series_length + + show_tutorial = cfg.get('Show Tutorial', True) + + self._tutorial_activity_instance: Optional[ba.Activity] + if show_tutorial: + from bastd.tutorial import TutorialActivity + + # Get this loading. + self._tutorial_activity_instance = _ba.newactivity( + TutorialActivity) + else: + self._tutorial_activity_instance = None + + self._playlist_name = cfg.get(self._playlist_selection_var, + '__default__') + self._playlist_randomize = cfg.get(self._playlist_randomize_var, False) + + # Which game activity we're on. + self._game_number = 0 + + playlists = cfg.get(self._playlists_var, {}) + + if (self._playlist_name != '__default__' + and self._playlist_name in playlists): + + # Make sure to copy this, as we muck with it in place once we've + # got it and we don't want that to affect our config. + playlist = copy.deepcopy(playlists[self._playlist_name]) + else: + if self.use_teams: + playlist = _playlist.get_default_teams_playlist() + else: + playlist = _playlist.get_default_free_for_all_playlist() + + # Resolve types and whatnot to get our final playlist. + playlist_resolved = _playlist.filter_playlist(playlist, + sessiontype=type(self), + add_resolved_type=True) + + if not playlist_resolved: + raise RuntimeError('Playlist contains no valid games.') + + self._playlist = ShuffleList(playlist_resolved, + shuffle=self._playlist_randomize) + + # Get a game on deck ready to go. + self._current_game_spec: Optional[Dict[str, Any]] = None + self._next_game_spec: Dict[str, Any] = self._playlist.pull_next() + self._next_game: Type[ba.GameActivity] = ( + self._next_game_spec['resolved_type']) + + # Go ahead and instantiate the next game we'll + # use so it has lots of time to load. + self._instantiate_next_game() + + # Start in our custom join screen. + self.setactivity(_ba.newactivity(MultiTeamJoinActivity)) + + def get_ffa_series_length(self) -> int: + """Return free-for-all series length.""" + return self._ffa_series_length + + def get_series_length(self) -> int: + """Return teams series length.""" + return self._series_length + + def get_next_game_description(self) -> ba.Lstr: + """Returns a description of the next game on deck.""" + # pylint: disable=cyclic-import + from ba._gameactivity import GameActivity + gametype: Type[GameActivity] = self._next_game_spec['resolved_type'] + assert issubclass(gametype, GameActivity) + return gametype.get_settings_display_string(self._next_game_spec) + + def get_game_number(self) -> int: + """Returns which game in the series is currently being played.""" + return self._game_number + + def on_team_join(self, team: ba.SessionTeam) -> None: + team.customdata['previous_score'] = team.customdata['score'] = 0 + + def get_max_players(self) -> int: + """Return max number of ba.Players allowed to join the game at once.""" + if self.use_teams: + return _ba.app.config.get('Team Game Max Players', 8) + return _ba.app.config.get('Free-for-All Max Players', 8) + + def _instantiate_next_game(self) -> None: + self._next_game_instance = _ba.newactivity( + self._next_game_spec['resolved_type'], + self._next_game_spec['settings']) + + def on_activity_end(self, activity: ba.Activity, results: Any) -> None: + # pylint: disable=cyclic-import + from bastd.tutorial import TutorialActivity + from bastd.activity.multiteamvictory import ( + TeamSeriesVictoryScoreScreenActivity) + from ba._activitytypes import (TransitionActivity, JoinActivity, + ScoreScreenActivity) + + # If we have a tutorial to show, that's the first thing we do no + # matter what. + if self._tutorial_activity_instance is not None: + self.setactivity(self._tutorial_activity_instance) + self._tutorial_activity_instance = None + + # If we're leaving the tutorial activity, pop a transition activity + # to transition us into a round gracefully (otherwise we'd snap from + # one terrain to another instantly). + elif isinstance(activity, TutorialActivity): + self.setactivity(_ba.newactivity(TransitionActivity)) + + # If we're in a between-round activity or a restart-activity, hop + # into a round. + elif isinstance( + activity, + (JoinActivity, TransitionActivity, ScoreScreenActivity)): + + # If we're coming from a series-end activity, reset scores. + if isinstance(activity, TeamSeriesVictoryScoreScreenActivity): + self.stats.reset() + self._game_number = 0 + for team in self.sessionteams: + team.customdata['score'] = 0 + + # Otherwise just set accum (per-game) scores. + else: + self.stats.reset_accum() + + next_game = self._next_game_instance + + self._current_game_spec = self._next_game_spec + self._next_game_spec = self._playlist.pull_next() + self._game_number += 1 + + # Instantiate the next now so they have plenty of time to load. + self._instantiate_next_game() + + # (Re)register all players and wire stats to our next activity. + for player in self.sessionplayers: + # ..but only ones who have been placed on a team + # (ie: no longer sitting in the lobby). + try: + has_team = (player.sessionteam is not None) + except NotFoundError: + has_team = False + if has_team: + self.stats.register_sessionplayer(player) + self.stats.setactivity(next_game) + + # Now flip the current activity. + self.setactivity(next_game) + + # If we're leaving a round, go to the score screen. + else: + self._switch_to_score_screen(results) + + def _switch_to_score_screen(self, results: Any) -> None: + """Switch to a score screen after leaving a round.""" + del results # Unused arg. + print_error('this should be overridden') + + def announce_game_results(self, + activity: ba.GameActivity, + results: ba.GameResults, + delay: float, + announce_winning_team: bool = True) -> None: + """Show basic game result at the end of a game. + + (before transitioning to a score screen). + This will include a zoom-text of 'BLUE WINS' + or whatnot, along with a possible audio + announcement of the same. + """ + # pylint: disable=cyclic-import + # pylint: disable=too-many-locals + from ba._math import normalized_color + from ba._general import Call + from ba._gameutils import cameraflash + from ba._language import Lstr + from ba._freeforallsession import FreeForAllSession + from ba._messages import CelebrateMessage + _ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell'))) + + if announce_winning_team: + winning_sessionteam = results.winning_sessionteam + if winning_sessionteam is not None: + # Have all players celebrate. + celebrate_msg = CelebrateMessage(duration=10.0) + assert winning_sessionteam.activityteam is not None + for player in winning_sessionteam.activityteam.players: + if player.actor: + player.actor.handlemessage(celebrate_msg) + cameraflash() + + # Some languages say "FOO WINS" different for teams vs players. + if isinstance(self, FreeForAllSession): + wins_resource = 'winsPlayerText' + else: + wins_resource = 'winsTeamText' + wins_text = Lstr(resource=wins_resource, + subs=[('${NAME}', winning_sessionteam.name)]) + activity.show_zoom_message( + wins_text, + scale=0.85, + color=normalized_color(winning_sessionteam.color), + ) + + +class ShuffleList: + """Smart shuffler for game playlists. + + (avoids repeats in maps or game types) + """ + + def __init__(self, items: List[Dict[str, Any]], shuffle: bool = True): + self.source_list = items + self.shuffle = shuffle + self.shuffle_list: List[Dict[str, Any]] = [] + self.last_gotten: Optional[Dict[str, Any]] = None + + def pull_next(self) -> Dict[str, Any]: + """Pull and return the next item on the shuffle-list.""" + + # Refill our list if its empty. + if not self.shuffle_list: + self.shuffle_list = list(self.source_list) + + # Ok now find an index we should pull. + index = 0 + + if self.shuffle: + for _i in range(4): + index = random.randrange(0, len(self.shuffle_list)) + test_obj = self.shuffle_list[index] + + # If the new one is the same map or game-type as the previous, + # lets try to keep looking. + if len(self.shuffle_list) > 1 and self.last_gotten is not None: + if (test_obj['settings']['map'] == + self.last_gotten['settings']['map']): + continue + if test_obj['type'] == self.last_gotten['type']: + continue + + # Sufficiently different; lets go with it. + break + + obj = self.shuffle_list.pop(index) + self.last_gotten = obj + return obj diff --git a/dist/ba_data/python/ba/_music.py b/dist/ba_data/python/ba/_music.py new file mode 100644 index 0000000..c2f92d9 --- /dev/null +++ b/dist/ba_data/python/ba/_music.py @@ -0,0 +1,503 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Music related functionality.""" +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING +from dataclasses import dataclass +from enum import Enum + +import _ba + +if TYPE_CHECKING: + from typing import Callable, Any, Optional, Dict, Union, Type + import ba + + +class MusicType(Enum): + """Types of music available to play in-game. + + Category: Enums + + These do not correspond to specific pieces of music, but rather to + 'situations'. The actual music played for each type can be overridden + by the game or by the user. + """ + MENU = 'Menu' + VICTORY = 'Victory' + CHAR_SELECT = 'CharSelect' + RUN_AWAY = 'RunAway' + ONSLAUGHT = 'Onslaught' + KEEP_AWAY = 'Keep Away' + RACE = 'Race' + EPIC_RACE = 'Epic Race' + SCORES = 'Scores' + GRAND_ROMP = 'GrandRomp' + TO_THE_DEATH = 'ToTheDeath' + CHOSEN_ONE = 'Chosen One' + FORWARD_MARCH = 'ForwardMarch' + FLAG_CATCHER = 'FlagCatcher' + SURVIVAL = 'Survival' + EPIC = 'Epic' + SPORTS = 'Sports' + HOCKEY = 'Hockey' + FOOTBALL = 'Football' + FLYING = 'Flying' + SCARY = 'Scary' + MARCHING = 'Marching' + + +class MusicPlayMode(Enum): + """Influences behavior when playing music. + + Category: Enums + """ + REGULAR = 'regular' + TEST = 'test' + + +@dataclass +class AssetSoundtrackEntry: + """A music entry using an internal asset. + + Category: App Classes + """ + assetname: str + volume: float = 1.0 + loop: bool = True + + +# What gets played by default for our different music types: +ASSET_SOUNDTRACK_ENTRIES: Dict[MusicType, AssetSoundtrackEntry] = { + MusicType.MENU: + AssetSoundtrackEntry('menuMusic'), + MusicType.VICTORY: + AssetSoundtrackEntry('victoryMusic', volume=1.2, loop=False), + MusicType.CHAR_SELECT: + AssetSoundtrackEntry('charSelectMusic', volume=0.4), + MusicType.RUN_AWAY: + AssetSoundtrackEntry('runAwayMusic', volume=1.2), + MusicType.ONSLAUGHT: + AssetSoundtrackEntry('runAwayMusic', volume=1.2), + MusicType.KEEP_AWAY: + AssetSoundtrackEntry('runAwayMusic', volume=1.2), + MusicType.RACE: + AssetSoundtrackEntry('runAwayMusic', volume=1.2), + MusicType.EPIC_RACE: + AssetSoundtrackEntry('slowEpicMusic', volume=1.2), + MusicType.SCORES: + AssetSoundtrackEntry('scoresEpicMusic', volume=0.6, loop=False), + MusicType.GRAND_ROMP: + AssetSoundtrackEntry('grandRompMusic', volume=1.2), + MusicType.TO_THE_DEATH: + AssetSoundtrackEntry('toTheDeathMusic', volume=1.2), + MusicType.CHOSEN_ONE: + AssetSoundtrackEntry('survivalMusic', volume=0.8), + MusicType.FORWARD_MARCH: + AssetSoundtrackEntry('forwardMarchMusic', volume=0.8), + MusicType.FLAG_CATCHER: + AssetSoundtrackEntry('flagCatcherMusic', volume=1.2), + MusicType.SURVIVAL: + AssetSoundtrackEntry('survivalMusic', volume=0.8), + MusicType.EPIC: + AssetSoundtrackEntry('slowEpicMusic', volume=1.2), + MusicType.SPORTS: + AssetSoundtrackEntry('sportsMusic', volume=0.8), + MusicType.HOCKEY: + AssetSoundtrackEntry('sportsMusic', volume=0.8), + MusicType.FOOTBALL: + AssetSoundtrackEntry('sportsMusic', volume=0.8), + MusicType.FLYING: + AssetSoundtrackEntry('flyingMusic', volume=0.8), + MusicType.SCARY: + AssetSoundtrackEntry('scaryMusic', volume=0.8), + MusicType.MARCHING: + AssetSoundtrackEntry('whenJohnnyComesMarchingHomeMusic', volume=0.8), +} + + +class MusicSubsystem: + """Subsystem for music playback in the app. + + Category: App Classes + + Access the single shared instance of this class at 'ba.app.music'. + """ + + def __init__(self) -> None: + # pylint: disable=cyclic-import + self._music_node: Optional[_ba.Node] = None + self._music_mode: MusicPlayMode = MusicPlayMode.REGULAR + self._music_player: Optional[MusicPlayer] = None + self._music_player_type: Optional[Type[MusicPlayer]] = None + self.music_types: Dict[MusicPlayMode, Optional[MusicType]] = { + MusicPlayMode.REGULAR: None, + MusicPlayMode.TEST: None + } + + # Set up custom music players for platforms that support them. + # FIXME: should generalize this to support arbitrary players per + # platform (which can be discovered via ba_meta). + # Our standard asset playback should probably just be one of them + # instead of a special case. + if self.supports_soundtrack_entry_type('musicFile'): + from ba.osmusic import OSMusicPlayer + self._music_player_type = OSMusicPlayer + elif self.supports_soundtrack_entry_type('iTunesPlaylist'): + from ba.macmusicapp import MacMusicAppMusicPlayer + self._music_player_type = MacMusicAppMusicPlayer + + def on_app_launch(self) -> None: + """Should be called by app on_app_launch().""" + + # If we're using a non-default playlist, lets go ahead and get our + # music-player going since it may hitch (better while we're faded + # out than later). + try: + cfg = _ba.app.config + if ('Soundtrack' in cfg and cfg['Soundtrack'] + not in ['__default__', 'Default Soundtrack']): + self.get_music_player() + except Exception: + from ba import _error + _error.print_exception('error prepping music-player') + + def on_app_shutdown(self) -> None: + """Should be called when the app is shutting down.""" + if self._music_player is not None: + self._music_player.shutdown() + + def have_music_player(self) -> bool: + """Returns whether a music player is present.""" + return self._music_player_type is not None + + def get_music_player(self) -> MusicPlayer: + """Returns the system music player, instantiating if necessary.""" + if self._music_player is None: + if self._music_player_type is None: + raise TypeError('no music player type set') + self._music_player = self._music_player_type() + return self._music_player + + def music_volume_changed(self, val: float) -> None: + """Should be called when changing the music volume.""" + if self._music_player is not None: + self._music_player.set_volume(val) + + def set_music_play_mode(self, + mode: MusicPlayMode, + force_restart: bool = False) -> None: + """Sets music play mode; used for soundtrack testing/etc.""" + old_mode = self._music_mode + self._music_mode = mode + if old_mode != self._music_mode or force_restart: + + # If we're switching into test mode we don't + # actually play anything until its requested. + # If we're switching *out* of test mode though + # we want to go back to whatever the normal song was. + if mode is MusicPlayMode.REGULAR: + mtype = self.music_types[MusicPlayMode.REGULAR] + self.do_play_music(None if mtype is None else mtype.value) + + def supports_soundtrack_entry_type(self, entry_type: str) -> bool: + """Return whether provided soundtrack entry type is supported here.""" + uas = _ba.env()['user_agent_string'] + assert isinstance(uas, str) + + # FIXME: Generalize this. + if entry_type == 'iTunesPlaylist': + return 'Mac' in uas + if entry_type in ('musicFile', 'musicFolder'): + return ('android' in uas + and _ba.android_get_external_storage_path() is not None) + if entry_type == 'default': + return True + return False + + def get_soundtrack_entry_type(self, entry: Any) -> str: + """Given a soundtrack entry, returns its type, taking into + account what is supported locally.""" + try: + if entry is None: + entry_type = 'default' + + # Simple string denotes iTunesPlaylist (legacy format). + elif isinstance(entry, str): + entry_type = 'iTunesPlaylist' + + # For other entries we expect type and name strings in a dict. + elif (isinstance(entry, dict) and 'type' in entry + and isinstance(entry['type'], str) and 'name' in entry + and isinstance(entry['name'], str)): + entry_type = entry['type'] + else: + raise TypeError('invalid soundtrack entry: ' + str(entry) + + ' (type ' + str(type(entry)) + ')') + if self.supports_soundtrack_entry_type(entry_type): + return entry_type + raise ValueError('invalid soundtrack entry:' + str(entry)) + except Exception: + from ba import _error + _error.print_exception() + return 'default' + + def get_soundtrack_entry_name(self, entry: Any) -> str: + """Given a soundtrack entry, returns its name.""" + try: + if entry is None: + raise TypeError('entry is None') + + # Simple string denotes an iTunesPlaylist name (legacy entry). + if isinstance(entry, str): + return entry + + # For other entries we expect type and name strings in a dict. + if (isinstance(entry, dict) and 'type' in entry + and isinstance(entry['type'], str) and 'name' in entry + and isinstance(entry['name'], str)): + return entry['name'] + raise ValueError('invalid soundtrack entry:' + str(entry)) + except Exception: + from ba import _error + _error.print_exception() + return 'default' + + def on_app_resume(self) -> None: + """Should be run when the app resumes from a suspended state.""" + if _ba.is_os_playing_music(): + self.do_play_music(None) + + def do_play_music(self, + musictype: Union[MusicType, str, None], + continuous: bool = False, + mode: MusicPlayMode = MusicPlayMode.REGULAR, + testsoundtrack: Dict[str, Any] = None) -> None: + """Plays the requested music type/mode. + + For most cases, setmusic() is the proper call to use, which itself + calls this. Certain cases, however, such as soundtrack testing, may + require calling this directly. + """ + + # We can be passed a MusicType or the string value corresponding + # to one. + if musictype is not None: + try: + musictype = MusicType(musictype) + except ValueError: + print(f"Invalid music type: '{musictype}'") + musictype = None + + with _ba.Context('ui'): + + # If they don't want to restart music and we're already + # playing what's requested, we're done. + if continuous and self.music_types[mode] is musictype: + return + self.music_types[mode] = musictype + + # If the OS tells us there's currently music playing, + # all our operations default to playing nothing. + if _ba.is_os_playing_music(): + musictype = None + + # If we're not in the mode this music is being set for, + # don't actually change what's playing. + if mode != self._music_mode: + return + + # Some platforms have a special music-player for things like iTunes + # soundtracks, mp3s, etc. if this is the case, attempt to grab an + # entry for this music-type, and if we have one, have the + # music-player play it. If not, we'll play game music ourself. + if musictype is not None and self._music_player_type is not None: + if testsoundtrack is not None: + soundtrack = testsoundtrack + else: + soundtrack = self._get_user_soundtrack() + entry = soundtrack.get(musictype.value) + else: + entry = None + + # Go through music-player. + if entry is not None: + self._play_music_player_music(entry) + + # Handle via internal music. + else: + self._play_internal_music(musictype) + + def _get_user_soundtrack(self) -> Dict[str, Any]: + """Return current user soundtrack or empty dict otherwise.""" + cfg = _ba.app.config + soundtrack: Dict[str, Any] = {} + soundtrackname = cfg.get('Soundtrack') + if soundtrackname is not None and soundtrackname != '__default__': + try: + soundtrack = cfg.get('Soundtracks', {})[soundtrackname] + except Exception as exc: + print(f'Error looking up user soundtrack: {exc}') + soundtrack = {} + return soundtrack + + def _play_music_player_music(self, entry: Any) -> None: + + # Stop any existing internal music. + if self._music_node is not None: + self._music_node.delete() + self._music_node = None + + # Do the thing. + self.get_music_player().play(entry) + + def _play_internal_music(self, musictype: Optional[MusicType]) -> None: + + # Stop any existing music-player playback. + if self._music_player is not None: + self._music_player.stop() + + # Stop any existing internal music. + if self._music_node: + self._music_node.delete() + self._music_node = None + + # Start up new internal music. + if musictype is not None: + + entry = ASSET_SOUNDTRACK_ENTRIES.get(musictype) + if entry is None: + print(f"Unknown music: '{musictype}'") + entry = ASSET_SOUNDTRACK_ENTRIES[MusicType.FLAG_CATCHER] + + self._music_node = _ba.newnode( + type='sound', + attrs={ + 'sound': _ba.getsound(entry.assetname), + 'positional': False, + 'music': True, + 'volume': entry.volume * 5.0, + 'loop': entry.loop + }) + + +class MusicPlayer: + """Wrangles soundtrack music playback. + + Category: App Classes + + Music can be played either through the game itself + or via a platform-specific external player. + """ + + def __init__(self) -> None: + self._have_set_initial_volume = False + self._entry_to_play: Optional[Any] = None + self._volume = 1.0 + self._actually_playing = False + + def select_entry(self, callback: Callable[[Any], None], current_entry: Any, + selection_target_name: str) -> Any: + """Summons a UI to select a new soundtrack entry.""" + return self.on_select_entry(callback, current_entry, + selection_target_name) + + def set_volume(self, volume: float) -> None: + """Set player volume (value should be between 0 and 1).""" + self._volume = volume + self.on_set_volume(volume) + self._update_play_state() + + def play(self, entry: Any) -> None: + """Play provided entry.""" + if not self._have_set_initial_volume: + self._volume = _ba.app.config.resolve('Music Volume') + self.on_set_volume(self._volume) + self._have_set_initial_volume = True + self._entry_to_play = copy.deepcopy(entry) + + # If we're currently *actually* playing something, + # switch to the new thing. + # Otherwise update state which will start us playing *only* + # if proper (volume > 0, etc). + if self._actually_playing: + self.on_play(self._entry_to_play) + else: + self._update_play_state() + + def stop(self) -> None: + """Stop any playback that is occurring.""" + self._entry_to_play = None + self._update_play_state() + + def shutdown(self) -> None: + """Shutdown music playback completely.""" + self.on_app_shutdown() + + def on_select_entry(self, callback: Callable[[Any], None], + current_entry: Any, selection_target_name: str) -> Any: + """Present a GUI to select an entry. + + The callback should be called with a valid entry or None to + signify that the default soundtrack should be used..""" + + # Subclasses should override the following: + + def on_set_volume(self, volume: float) -> None: + """Called when the volume should be changed.""" + + def on_play(self, entry: Any) -> None: + """Called when a new song/playlist/etc should be played.""" + + def on_stop(self) -> None: + """Called when the music should stop.""" + + def on_app_shutdown(self) -> None: + """Called on final app shutdown.""" + + def _update_play_state(self) -> None: + + # If we aren't playing, should be, and have positive volume, do so. + if not self._actually_playing: + if self._entry_to_play is not None and self._volume > 0.0: + self.on_play(self._entry_to_play) + self._actually_playing = True + else: + if self._actually_playing and (self._entry_to_play is None + or self._volume <= 0.0): + self.on_stop() + self._actually_playing = False + + +def setmusic(musictype: Optional[ba.MusicType], + continuous: bool = False) -> None: + """Set the app to play (or stop playing) a certain type of music. + + category: Gameplay Functions + + This function will handle loading and playing sound assets as necessary, + and also supports custom user soundtracks on specific platforms so the + user can override particular game music with their own. + + Pass None to stop music. + + if 'continuous' is True and musictype is the same as what is already + playing, the playing track will not be restarted. + """ + from ba import _gameutils + + # All we do here now is set a few music attrs on the current globals + # node. The foreground globals' current playing music then gets fed to + # the do_play_music call in our music controller. This way we can + # seamlessly support custom soundtracks in replays/etc since we're being + # driven purely by node data. + gnode = _ba.getactivity().globalsnode + gnode.music_continuous = continuous + gnode.music = '' if musictype is None else musictype.value + gnode.music_count += 1 + + +def do_play_music(*args: Any, **keywds: Any) -> None: + """A passthrough used by the C++ layer.""" + _ba.app.music.do_play_music(*args, **keywds) diff --git a/dist/ba_data/python/ba/_netutils.py b/dist/ba_data/python/ba/_netutils.py new file mode 100644 index 0000000..5f5e088 --- /dev/null +++ b/dist/ba_data/python/ba/_netutils.py @@ -0,0 +1,186 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Networking related functionality.""" +from __future__ import annotations + +import copy +import threading +import weakref +from enum import Enum +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Any, Dict, Union, Callable, Optional + import socket + import ba + ServerCallbackType = Callable[[Union[None, Dict[str, Any]]], None] + + +def get_ip_address_type(addr: str) -> socket.AddressFamily: + """Return socket.AF_INET6 or socket.AF_INET4 for the provided address.""" + import socket + socket_type = None + + # First try it as an ipv4 address. + try: + socket.inet_pton(socket.AF_INET, addr) + socket_type = socket.AF_INET + except OSError: + pass + + # Hmm apparently not ipv4; try ipv6. + if socket_type is None: + try: + socket.inet_pton(socket.AF_INET6, addr) + socket_type = socket.AF_INET6 + except OSError: + pass + if socket_type is None: + raise ValueError(f'addr seems to be neither v4 or v6: {addr}') + return socket_type + + +class ServerResponseType(Enum): + """How to interpret responses from the server.""" + JSON = 0 + + +class ServerCallThread(threading.Thread): + """Thread to communicate with the master server.""" + + def __init__(self, request: str, request_type: str, + data: Optional[Dict[str, Any]], + callback: Optional[ServerCallbackType], + response_type: ServerResponseType): + super().__init__() + self._request = request + self._request_type = request_type + if not isinstance(response_type, ServerResponseType): + raise TypeError(f'Invalid response type: {response_type}') + self._response_type = response_type + self._data = {} if data is None else copy.deepcopy(data) + self._callback: Optional[ServerCallbackType] = callback + self._context = _ba.Context('current') + + # Save and restore the context we were created from. + activity = _ba.getactivity(doraise=False) + self._activity = weakref.ref( + activity) if activity is not None else None + + def _run_callback(self, arg: Union[None, Dict[str, Any]]) -> None: + # If we were created in an activity context and that activity has + # since died, do nothing. + # FIXME: Should we just be using a ContextCall instead of doing + # this check manually? + if self._activity is not None: + activity = self._activity() + if activity is None or activity.expired: + return + + # Technically we could do the same check for session contexts, + # but not gonna worry about it for now. + assert self._context is not None + assert self._callback is not None + with self._context: + self._callback(arg) + + def run(self) -> None: + # pylint: disable=too-many-branches + import urllib.request + import urllib.error + import json + import http.client + from ba import _general + try: + self._data = _general.utf8_all(self._data) + _ba.set_thread_name('BA_ServerCallThread') + parse = urllib.parse + if self._request_type == 'get': + response = urllib.request.urlopen( + urllib.request.Request( + (_ba.get_master_server_address() + '/' + + self._request + '?' + parse.urlencode(self._data)), + None, {'User-Agent': _ba.app.user_agent_string})) + elif self._request_type == 'post': + response = urllib.request.urlopen( + urllib.request.Request( + _ba.get_master_server_address() + '/' + self._request, + parse.urlencode(self._data).encode(), + {'User-Agent': _ba.app.user_agent_string})) + else: + raise TypeError('Invalid request_type: ' + self._request_type) + + # If html request failed. + if response.getcode() != 200: + response_data = None + elif self._response_type == ServerResponseType.JSON: + raw_data = response.read() + + # Empty string here means something failed server side. + if raw_data == b'': + response_data = None + else: + # Json.loads requires str in python < 3.6. + raw_data_s = raw_data.decode() + response_data = json.loads(raw_data_s) + else: + raise TypeError(f'invalid responsetype: {self._response_type}') + + except Exception as exc: + import errno + do_print = False + response_data = None + + # Ignore common network errors; note unexpected ones. + if isinstance( + exc, + (urllib.error.URLError, ConnectionError, + http.client.IncompleteRead, http.client.BadStatusLine)): + pass + elif isinstance(exc, OSError): + if exc.errno == 10051: # Windows unreachable network error. + pass + elif exc.errno in [ + errno.ETIMEDOUT, errno.EHOSTUNREACH, errno.ENETUNREACH + ]: + pass + else: + do_print = True + elif (self._response_type == ServerResponseType.JSON + and isinstance(exc, json.decoder.JSONDecodeError)): + pass + else: + do_print = True + + if do_print: + # Any other error here is unexpected, + # so let's make a note of it, + print(f'Error in ServerCallThread' + f' (response-type={self._response_type},' + f' response-data={response_data}):') + import traceback + traceback.print_exc() + + if self._callback is not None: + _ba.pushcall(_general.Call(self._run_callback, response_data), + from_other_thread=True) + + +def master_server_get( + request: str, + data: Dict[str, Any], + callback: Optional[ServerCallbackType] = None, + response_type: ServerResponseType = ServerResponseType.JSON) -> None: + """Make a call to the master server via a http GET.""" + ServerCallThread(request, 'get', data, callback, response_type).start() + + +def master_server_post( + request: str, + data: Dict[str, Any], + callback: Optional[ServerCallbackType] = None, + response_type: ServerResponseType = ServerResponseType.JSON) -> None: + """Make a call to the master server via a http POST.""" + ServerCallThread(request, 'post', data, callback, response_type).start() diff --git a/dist/ba_data/python/ba/_nodeactor.py b/dist/ba_data/python/ba/_nodeactor.py new file mode 100644 index 0000000..756a7fc --- /dev/null +++ b/dist/ba_data/python/ba/_nodeactor.py @@ -0,0 +1,38 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines NodeActor class.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ba._messages import DieMessage +from ba._actor import Actor + +if TYPE_CHECKING: + import ba + from typing import Any + + +class NodeActor(Actor): + """A simple ba.Actor type that wraps a single ba.Node. + + Category: Gameplay Classes + + This Actor will delete its Node when told to die, and it's + exists() call will return whether the Node still exists or not. + """ + + def __init__(self, node: ba.Node): + super().__init__() + self.node = node + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, DieMessage): + if self.node: + self.node.delete() + return None + return super().handlemessage(msg) + + def exists(self) -> bool: + return bool(self.node) diff --git a/dist/ba_data/python/ba/_player.py b/dist/ba_data/python/ba/_player.py new file mode 100644 index 0000000..f0592a6 --- /dev/null +++ b/dist/ba_data/python/ba/_player.py @@ -0,0 +1,330 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Player related functionality.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypeVar, Generic, cast + +import _ba +from ba._error import (SessionPlayerNotFoundError, print_exception, + ActorNotFoundError) +from ba._messages import DeathType, DieMessage + +if TYPE_CHECKING: + from typing import (Type, Optional, Sequence, Dict, Any, Union, Tuple, + Callable) + import ba + +PlayerType = TypeVar('PlayerType', bound='ba.Player') +TeamType = TypeVar('TeamType', bound='ba.Team') + + +@dataclass +class PlayerInfo: + """Holds basic info about a player. + + Category: Gameplay Classes + """ + name: str + character: str + + +@dataclass +class StandLocation: + """Describes a point in space and an angle to face. + + Category: Gameplay Classes + """ + position: ba.Vec3 + angle: Optional[float] = None + + +class Player(Generic[TeamType]): + """A player in a specific ba.Activity. + + Category: Gameplay Classes + + These correspond to ba.SessionPlayer objects, but are associated with a + single ba.Activity instance. This allows activities to specify their + own custom ba.Player types. + + Attributes: + + actor + The ba.Actor associated with the player. + + """ + + # These are instance attrs but we define them at the type level so + # their type annotations are introspectable (for docs generation). + character: str + actor: Optional[ba.Actor] + color: Sequence[float] + highlight: Sequence[float] + + _team: TeamType + _sessionplayer: ba.SessionPlayer + _nodeactor: Optional[ba.NodeActor] + _expired: bool + _postinited: bool + _customdata: dict + + # NOTE: avoiding having any __init__() here since it seems to not + # get called by default if a dataclass inherits from us. + # This also lets us keep trivial player classes cleaner by skipping + # the super().__init__() line. + + def postinit(self, sessionplayer: ba.SessionPlayer) -> None: + """Wire up a newly created player. + + (internal) + """ + from ba._nodeactor import NodeActor + + # Sanity check; if a dataclass is created that inherits from us, + # it will define an equality operator by default which will break + # internal game logic. So complain loudly if we find one. + if type(self).__eq__ is not object.__eq__: + raise RuntimeError( + f'Player class {type(self)} defines an equality' + f' operator (__eq__) which will break internal' + f' logic. Please remove it.\n' + f'For dataclasses you can do "dataclass(eq=False)"' + f' in the class decorator.') + + self.actor = None + self.character = '' + self._nodeactor: Optional[ba.NodeActor] = None + self._sessionplayer = sessionplayer + self.character = sessionplayer.character + self.color = sessionplayer.color + self.highlight = sessionplayer.highlight + self._team = cast(TeamType, sessionplayer.sessionteam.activityteam) + assert self._team is not None + self._customdata = {} + self._expired = False + self._postinited = True + node = _ba.newnode('player', attrs={'playerID': sessionplayer.id}) + self._nodeactor = NodeActor(node) + sessionplayer.setnode(node) + + def leave(self) -> None: + """Called when the Player leaves a running game. + + (internal) + """ + assert self._postinited + assert not self._expired + try: + # If they still have an actor, kill it. + if self.actor: + self.actor.handlemessage(DieMessage(how=DeathType.LEFT_GAME)) + self.actor = None + except Exception: + print_exception(f'Error killing actor on leave for {self}') + self._nodeactor = None + del self._team + del self._customdata + + def expire(self) -> None: + """Called when the Player is expiring (when its Activity does so). + + (internal) + """ + assert self._postinited + assert not self._expired + self._expired = True + + try: + self.on_expire() + except Exception: + print_exception(f'Error in on_expire for {self}.') + + self._nodeactor = None + self.actor = None + del self._team + del self._customdata + + def on_expire(self) -> None: + """Can be overridden to handle player expiration. + + The player expires when the Activity it is a part of expires. + Expired players should no longer run any game logic (which will + likely error). They should, however, remove any references to + players/teams/games/etc. which could prevent them from being freed. + """ + + @property + def team(self) -> TeamType: + """The ba.Team for this player.""" + assert self._postinited + assert not self._expired + return self._team + + @property + def customdata(self) -> dict: + """Arbitrary values associated with the player. + Though it is encouraged that most player values be properly defined + on the ba.Player subclass, it may be useful for player-agnostic + objects to store values here. This dict is cleared when the player + leaves or expires so objects stored here will be disposed of at + the expected time, unlike the Player instance itself which may + continue to be referenced after it is no longer part of the game. + """ + assert self._postinited + assert not self._expired + return self._customdata + + @property + def sessionplayer(self) -> ba.SessionPlayer: + """Return the ba.SessionPlayer corresponding to this Player. + + Throws a ba.SessionPlayerNotFoundError if it does not exist. + """ + assert self._postinited + if bool(self._sessionplayer): + return self._sessionplayer + raise SessionPlayerNotFoundError() + + @property + def node(self) -> ba.Node: + """A ba.Node of type 'player' associated with this Player. + + This node can be used to get a generic player position/etc. + """ + assert self._postinited + assert not self._expired + assert self._nodeactor + return self._nodeactor.node + + @property + def position(self) -> ba.Vec3: + """The position of the player, as defined by its current ba.Actor. + + If the player currently has no actor, raises a ba.ActorNotFoundError. + """ + assert self._postinited + assert not self._expired + if self.actor is None: + raise ActorNotFoundError + return _ba.Vec3(self.node.position) + + def exists(self) -> bool: + """Whether the underlying player still exists. + + This will return False if the underlying ba.SessionPlayer has + left the game or if the ba.Activity this player was associated + with has ended. + Most functionality will fail on a nonexistent player. + Note that you can also use the boolean operator for this same + functionality, so a statement such as "if player" will do + the right thing both for Player objects and values of None. + """ + assert self._postinited + return self._sessionplayer.exists() and not self._expired + + def getname(self, full: bool = False, icon: bool = True) -> str: + """getname(full: bool = False, icon: bool = True) -> str + + Returns the player's name. If icon is True, the long version of the + name may include an icon. + """ + assert self._postinited + assert not self._expired + return self._sessionplayer.getname(full=full, icon=icon) + + def is_alive(self) -> bool: + """is_alive() -> bool + + Returns True if the player has a ba.Actor assigned and its + is_alive() method return True. False is returned otherwise. + """ + assert self._postinited + assert not self._expired + return self.actor is not None and self.actor.is_alive() + + def get_icon(self) -> Dict[str, Any]: + """get_icon() -> Dict[str, Any] + + Returns the character's icon (images, colors, etc contained in a dict) + """ + assert self._postinited + assert not self._expired + return self._sessionplayer.get_icon() + + def assigninput(self, inputtype: Union[ba.InputType, Tuple[ba.InputType, + ...]], + call: Callable) -> None: + """assigninput(type: Union[ba.InputType, Tuple[ba.InputType, ...]], + call: Callable) -> None + + Set the python callable to be run for one or more types of input. + """ + assert self._postinited + assert not self._expired + return self._sessionplayer.assigninput(type=inputtype, call=call) + + def resetinput(self) -> None: + """resetinput() -> None + + Clears out the player's assigned input actions. + """ + assert self._postinited + assert not self._expired + self._sessionplayer.resetinput() + + def __bool__(self) -> bool: + return self.exists() + + +class EmptyPlayer(Player['ba.EmptyTeam']): + """An empty player for use by Activities that don't need to define one. + + Category: Gameplay Classes + + ba.Player and ba.Team are 'Generic' types, and so passing those top level + classes as type arguments when defining a ba.Activity reduces type safety. + For example, activity.teams[0].player will have type 'Any' in that case. + For that reason, it is better to pass EmptyPlayer and EmptyTeam when + defining a ba.Activity that does not need custom types of its own. + + Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, + so if you want to define your own class for one of them you should do so + for both. + """ + + +# NOTE: It seems we might not need these playercast() calls; have gone +# the direction where things returning players generally take a type arg +# and do this themselves; that way the user is 'forced' to deal with types +# instead of requiring extra work by them. + + +def playercast(totype: Type[PlayerType], player: ba.Player) -> PlayerType: + """Cast a ba.Player to a specific ba.Player subclass. + + Category: Gameplay Functions + + When writing type-checked code, sometimes code will deal with raw + ba.Player objects which need to be cast back to the game's actual + player type so that access can be properly type-checked. This function + is a safe way to do so. It ensures that Optional values are not cast + into Non-Optional, etc. + """ + assert isinstance(player, totype) + return player + + +# NOTE: ideally we should have a single playercast() call and use overloads +# for the optional variety, but that currently seems to not be working. +# See: https://github.com/python/mypy/issues/8800 +def playercast_o(totype: Type[PlayerType], + player: Optional[ba.Player]) -> Optional[PlayerType]: + """A variant of ba.playercast() for use with optional ba.Player values. + + Category: Gameplay Functions + """ + assert isinstance(player, (totype, type(None))) + return player diff --git a/dist/ba_data/python/ba/_playlist.py b/dist/ba_data/python/ba/_playlist.py new file mode 100644 index 0000000..1f31ee6 --- /dev/null +++ b/dist/ba_data/python/ba/_playlist.py @@ -0,0 +1,495 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Playlist related functionality.""" + +from __future__ import annotations + +import copy +from typing import Any, TYPE_CHECKING, Dict, List + +if TYPE_CHECKING: + from typing import Type, Sequence + from ba import _session + +PlaylistType = List[Dict[str, Any]] + + +def filter_playlist(playlist: PlaylistType, + sessiontype: Type[_session.Session], + add_resolved_type: bool = False, + remove_unowned: bool = True, + mark_unowned: bool = False) -> PlaylistType: + """Return a filtered version of a playlist. + + Strips out or replaces invalid or unowned game types, makes sure all + settings are present, and adds in a 'resolved_type' which is the actual + type. + """ + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + import _ba + from ba import _map + from ba import _general + from ba import _gameactivity + goodlist: List[Dict] = [] + unowned_maps: Sequence[str] + if remove_unowned or mark_unowned: + unowned_maps = _map.get_unowned_maps() + unowned_game_types = _ba.app.meta.get_unowned_game_types() + else: + unowned_maps = [] + unowned_game_types = set() + + for entry in copy.deepcopy(playlist): + # 'map' used to be called 'level' here. + if 'level' in entry: + entry['map'] = entry['level'] + del entry['level'] + + # We now stuff map into settings instead of it being its own thing. + if 'map' in entry: + entry['settings']['map'] = entry['map'] + del entry['map'] + + # Update old map names to new ones. + entry['settings']['map'] = _map.get_filtered_map_name( + entry['settings']['map']) + if remove_unowned and entry['settings']['map'] in unowned_maps: + continue + + # Ok, for each game in our list, try to import the module and grab + # the actual game class. add successful ones to our initial list + # to present to the user. + if not isinstance(entry['type'], str): + raise TypeError('invalid entry format') + try: + # Do some type filters for backwards compat. + if entry['type'] in ('Assault.AssaultGame', + 'Happy_Thoughts.HappyThoughtsGame', + 'bsAssault.AssaultGame', + 'bs_assault.AssaultGame'): + entry['type'] = 'bastd.game.assault.AssaultGame' + if entry['type'] in ('King_of_the_Hill.KingOfTheHillGame', + 'bsKingOfTheHill.KingOfTheHillGame', + 'bs_king_of_the_hill.KingOfTheHillGame'): + entry['type'] = 'bastd.game.kingofthehill.KingOfTheHillGame' + if entry['type'] in ('Capture_the_Flag.CTFGame', + 'bsCaptureTheFlag.CTFGame', + 'bs_capture_the_flag.CTFGame'): + entry['type'] = ( + 'bastd.game.capturetheflag.CaptureTheFlagGame') + if entry['type'] in ('Death_Match.DeathMatchGame', + 'bsDeathMatch.DeathMatchGame', + 'bs_death_match.DeathMatchGame'): + entry['type'] = 'bastd.game.deathmatch.DeathMatchGame' + if entry['type'] in ('ChosenOne.ChosenOneGame', + 'bsChosenOne.ChosenOneGame', + 'bs_chosen_one.ChosenOneGame'): + entry['type'] = 'bastd.game.chosenone.ChosenOneGame' + if entry['type'] in ('Conquest.Conquest', 'Conquest.ConquestGame', + 'bsConquest.ConquestGame', + 'bs_conquest.ConquestGame'): + entry['type'] = 'bastd.game.conquest.ConquestGame' + if entry['type'] in ('Elimination.EliminationGame', + 'bsElimination.EliminationGame', + 'bs_elimination.EliminationGame'): + entry['type'] = 'bastd.game.elimination.EliminationGame' + if entry['type'] in ('Football.FootballGame', + 'bsFootball.FootballTeamGame', + 'bs_football.FootballTeamGame'): + entry['type'] = 'bastd.game.football.FootballTeamGame' + if entry['type'] in ('Hockey.HockeyGame', 'bsHockey.HockeyGame', + 'bs_hockey.HockeyGame'): + entry['type'] = 'bastd.game.hockey.HockeyGame' + if entry['type'] in ('Keep_Away.KeepAwayGame', + 'bsKeepAway.KeepAwayGame', + 'bs_keep_away.KeepAwayGame'): + entry['type'] = 'bastd.game.keepaway.KeepAwayGame' + if entry['type'] in ('Race.RaceGame', 'bsRace.RaceGame', + 'bs_race.RaceGame'): + entry['type'] = 'bastd.game.race.RaceGame' + if entry['type'] in ('bsEasterEggHunt.EasterEggHuntGame', + 'bs_easter_egg_hunt.EasterEggHuntGame'): + entry['type'] = 'bastd.game.easteregghunt.EasterEggHuntGame' + if entry['type'] in ('bsMeteorShower.MeteorShowerGame', + 'bs_meteor_shower.MeteorShowerGame'): + entry['type'] = 'bastd.game.meteorshower.MeteorShowerGame' + if entry['type'] in ('bsTargetPractice.TargetPracticeGame', + 'bs_target_practice.TargetPracticeGame'): + entry['type'] = ( + 'bastd.game.targetpractice.TargetPracticeGame') + + gameclass = _general.getclass(entry['type'], + _gameactivity.GameActivity) + + if remove_unowned and gameclass in unowned_game_types: + continue + if add_resolved_type: + entry['resolved_type'] = gameclass + if mark_unowned and entry['settings']['map'] in unowned_maps: + entry['is_unowned_map'] = True + if mark_unowned and gameclass in unowned_game_types: + entry['is_unowned_game'] = True + + # Make sure all settings the game defines are present. + neededsettings = gameclass.get_available_settings(sessiontype) + for setting in neededsettings: + if setting.name not in entry['settings']: + entry['settings'][setting.name] = setting.default + goodlist.append(entry) + except ImportError as exc: + print(f'Import failed while scanning playlist: {exc}') + except Exception: + from ba import _error + _error.print_exception() + return goodlist + + +def get_default_free_for_all_playlist() -> PlaylistType: + """Return a default playlist for free-for-all mode.""" + + # NOTE: these are currently using old type/map names, + # but filtering translates them properly to the new ones. + # (is kinda a handy way to ensure filtering is working). + # Eventually should update these though. + return [{ + 'settings': { + 'Epic Mode': False, + 'Kills to Win Per Player': 10, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Doom Shroom' + }, + 'type': 'bs_death_match.DeathMatchGame' + }, { + 'settings': { + 'Chosen One Gets Gloves': True, + 'Chosen One Gets Shield': False, + 'Chosen One Time': 30, + 'Epic Mode': 0, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Monkey Face' + }, + 'type': 'bs_chosen_one.ChosenOneGame' + }, { + 'settings': { + 'Hold Time': 30, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Zigzag' + }, + 'type': 'bs_king_of_the_hill.KingOfTheHillGame' + }, { + 'settings': { + 'Epic Mode': False, + 'map': 'Rampage' + }, + 'type': 'bs_meteor_shower.MeteorShowerGame' + }, { + 'settings': { + 'Epic Mode': 1, + 'Lives Per Player': 1, + 'Respawn Times': 1.0, + 'Time Limit': 120, + 'map': 'Tip Top' + }, + 'type': 'bs_elimination.EliminationGame' + }, { + 'settings': { + 'Hold Time': 30, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'The Pad' + }, + 'type': 'bs_keep_away.KeepAwayGame' + }, { + 'settings': { + 'Epic Mode': True, + 'Kills to Win Per Player': 10, + 'Respawn Times': 0.25, + 'Time Limit': 120, + 'map': 'Rampage' + }, + 'type': 'bs_death_match.DeathMatchGame' + }, { + 'settings': { + 'Bomb Spawning': 1000, + 'Epic Mode': False, + 'Laps': 3, + 'Mine Spawn Interval': 4000, + 'Mine Spawning': 4000, + 'Time Limit': 300, + 'map': 'Big G' + }, + 'type': 'bs_race.RaceGame' + }, { + 'settings': { + 'Hold Time': 30, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Happy Thoughts' + }, + 'type': 'bs_king_of_the_hill.KingOfTheHillGame' + }, { + 'settings': { + 'Enable Impact Bombs': 1, + 'Enable Triple Bombs': False, + 'Target Count': 2, + 'map': 'Doom Shroom' + }, + 'type': 'bs_target_practice.TargetPracticeGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Lives Per Player': 5, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Step Right Up' + }, + 'type': 'bs_elimination.EliminationGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Kills to Win Per Player': 10, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Crag Castle' + }, + 'type': 'bs_death_match.DeathMatchGame' + }, { + 'map': 'Lake Frigid', + 'settings': { + 'Bomb Spawning': 0, + 'Epic Mode': False, + 'Laps': 6, + 'Mine Spawning': 2000, + 'Time Limit': 300, + 'map': 'Lake Frigid' + }, + 'type': 'bs_race.RaceGame' + }] + + +def get_default_teams_playlist() -> PlaylistType: + """Return a default playlist for teams mode.""" + + # NOTE: these are currently using old type/map names, + # but filtering translates them properly to the new ones. + # (is kinda a handy way to ensure filtering is working). + # Eventually should update these though. + return [{ + 'settings': { + 'Epic Mode': False, + 'Flag Idle Return Time': 30, + 'Flag Touch Return Time': 0, + 'Respawn Times': 1.0, + 'Score to Win': 3, + 'Time Limit': 600, + 'map': 'Bridgit' + }, + 'type': 'bs_capture_the_flag.CTFGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Respawn Times': 1.0, + 'Score to Win': 3, + 'Time Limit': 600, + 'map': 'Step Right Up' + }, + 'type': 'bs_assault.AssaultGame' + }, { + 'settings': { + 'Balance Total Lives': False, + 'Epic Mode': False, + 'Lives Per Player': 3, + 'Respawn Times': 1.0, + 'Solo Mode': True, + 'Time Limit': 600, + 'map': 'Rampage' + }, + 'type': 'bs_elimination.EliminationGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Kills to Win Per Player': 5, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Roundabout' + }, + 'type': 'bs_death_match.DeathMatchGame' + }, { + 'settings': { + 'Respawn Times': 1.0, + 'Score to Win': 1, + 'Time Limit': 600, + 'map': 'Hockey Stadium' + }, + 'type': 'bs_hockey.HockeyGame' + }, { + 'settings': { + 'Hold Time': 30, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Monkey Face' + }, + 'type': 'bs_keep_away.KeepAwayGame' + }, { + 'settings': { + 'Balance Total Lives': False, + 'Epic Mode': True, + 'Lives Per Player': 1, + 'Respawn Times': 1.0, + 'Solo Mode': False, + 'Time Limit': 120, + 'map': 'Tip Top' + }, + 'type': 'bs_elimination.EliminationGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Respawn Times': 1.0, + 'Score to Win': 3, + 'Time Limit': 300, + 'map': 'Crag Castle' + }, + 'type': 'bs_assault.AssaultGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Kills to Win Per Player': 5, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Doom Shroom' + }, + 'type': 'bs_death_match.DeathMatchGame' + }, { + 'settings': { + 'Epic Mode': False, + 'map': 'Rampage' + }, + 'type': 'bs_meteor_shower.MeteorShowerGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Flag Idle Return Time': 30, + 'Flag Touch Return Time': 0, + 'Respawn Times': 1.0, + 'Score to Win': 2, + 'Time Limit': 600, + 'map': 'Roundabout' + }, + 'type': 'bs_capture_the_flag.CTFGame' + }, { + 'settings': { + 'Respawn Times': 1.0, + 'Score to Win': 21, + 'Time Limit': 600, + 'map': 'Football Stadium' + }, + 'type': 'bs_football.FootballTeamGame' + }, { + 'settings': { + 'Epic Mode': True, + 'Respawn Times': 0.25, + 'Score to Win': 3, + 'Time Limit': 120, + 'map': 'Bridgit' + }, + 'type': 'bs_assault.AssaultGame' + }, { + 'map': 'Doom Shroom', + 'settings': { + 'Enable Impact Bombs': 1, + 'Enable Triple Bombs': False, + 'Target Count': 2, + 'map': 'Doom Shroom' + }, + 'type': 'bs_target_practice.TargetPracticeGame' + }, { + 'settings': { + 'Hold Time': 30, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Tip Top' + }, + 'type': 'bs_king_of_the_hill.KingOfTheHillGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Respawn Times': 1.0, + 'Score to Win': 2, + 'Time Limit': 300, + 'map': 'Zigzag' + }, + 'type': 'bs_assault.AssaultGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Flag Idle Return Time': 30, + 'Flag Touch Return Time': 0, + 'Respawn Times': 1.0, + 'Score to Win': 3, + 'Time Limit': 300, + 'map': 'Happy Thoughts' + }, + 'type': 'bs_capture_the_flag.CTFGame' + }, { + 'settings': { + 'Bomb Spawning': 1000, + 'Epic Mode': True, + 'Laps': 1, + 'Mine Spawning': 2000, + 'Time Limit': 300, + 'map': 'Big G' + }, + 'type': 'bs_race.RaceGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Kills to Win Per Player': 5, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Monkey Face' + }, + 'type': 'bs_death_match.DeathMatchGame' + }, { + 'settings': { + 'Hold Time': 30, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Lake Frigid' + }, + 'type': 'bs_keep_away.KeepAwayGame' + }, { + 'settings': { + 'Epic Mode': False, + 'Flag Idle Return Time': 30, + 'Flag Touch Return Time': 3, + 'Respawn Times': 1.0, + 'Score to Win': 2, + 'Time Limit': 300, + 'map': 'Tip Top' + }, + 'type': 'bs_capture_the_flag.CTFGame' + }, { + 'settings': { + 'Balance Total Lives': False, + 'Epic Mode': False, + 'Lives Per Player': 3, + 'Respawn Times': 1.0, + 'Solo Mode': False, + 'Time Limit': 300, + 'map': 'Crag Castle' + }, + 'type': 'bs_elimination.EliminationGame' + }, { + 'settings': { + 'Epic Mode': True, + 'Respawn Times': 0.25, + 'Time Limit': 120, + 'map': 'Zigzag' + }, + 'type': 'bs_conquest.ConquestGame' + }] diff --git a/dist/ba_data/python/ba/_plugin.py b/dist/ba_data/python/ba/_plugin.py new file mode 100644 index 0000000..cf4251c --- /dev/null +++ b/dist/ba_data/python/ba/_plugin.py @@ -0,0 +1,96 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Plugin related functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from dataclasses import dataclass +import _ba + +if TYPE_CHECKING: + from typing import List, Dict + import ba + + +class PluginSubsystem: + """Subsystem for plugin handling in the app. + + Category: App Classes + + Access the single shared instance of this class at 'ba.app.plugins'. + """ + + def __init__(self) -> None: + self.potential_plugins: List[ba.PotentialPlugin] = [] + self.active_plugins: Dict[str, ba.Plugin] = {} + + def on_app_launch(self) -> None: + """Should be called at app launch time.""" + # Load up our plugins and go ahead and call their on_app_launch calls. + self.load_plugins() + for plugin in self.active_plugins.values(): + try: + plugin.on_app_launch() + except Exception: + from ba import _error + _error.print_exception('Error in plugin on_app_launch()') + + def load_plugins(self) -> None: + """(internal)""" + from ba._general import getclass + + # Note: the plugins we load is purely based on what's enabled + # in the app config. Our meta-scan gives us a list of available + # plugins, but that is only used to give the user a list of plugins + # that they can enable. (we wouldn't want to look at meta-scan here + # anyway because it may not be done yet at this point in the launch) + plugstates: Dict[str, Dict] = _ba.app.config.get('Plugins', {}) + assert isinstance(plugstates, dict) + plugkeys: List[str] = sorted(key for key, val in plugstates.items() + if val.get('enabled', False)) + for plugkey in plugkeys: + try: + cls = getclass(plugkey, Plugin) + except Exception as exc: + _ba.log(f"Error loading plugin class '{plugkey}': {exc}", + to_server=False) + continue + try: + plugin = cls() + assert plugkey not in self.active_plugins + self.active_plugins[plugkey] = plugin + except Exception: + from ba import _error + _error.print_exception(f'Error loading plugin: {plugkey}') + + +@dataclass +class PotentialPlugin: + """Represents a ba.Plugin which can potentially be loaded. + + Category: App Classes + + These generally represent plugins which were detected by the + meta-tag scan. However they may also represent plugins which + were previously set to be loaded but which were unable to be + for some reason. In that case, 'available' will be set to False. + """ + display_name: ba.Lstr + class_path: str + available: bool + + +class Plugin: + """A plugin to alter app behavior in some way. + + Category: App Classes + + Plugins are discoverable by the meta-tag system + and the user can select which ones they want to activate. + Active plugins are then called at specific times as the + app is running in order to modify its behavior in some way. + """ + + def on_app_launch(self) -> None: + """Called when the app is being launched.""" diff --git a/dist/ba_data/python/ba/_powerup.py b/dist/ba_data/python/ba/_powerup.py new file mode 100644 index 0000000..7dd895c --- /dev/null +++ b/dist/ba_data/python/ba/_powerup.py @@ -0,0 +1,54 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Powerup related functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from dataclasses import dataclass + +if TYPE_CHECKING: + from typing import Sequence, Tuple, Optional + import ba + + +@dataclass +class PowerupMessage: + """A message telling an object to accept a powerup. + + Category: Message Classes + + This message is normally received by touching a ba.PowerupBox. + + Attributes: + + poweruptype + The type of powerup to be granted (a string). + See ba.Powerup.poweruptype for available type values. + + sourcenode + The node the powerup game from, or None otherwise. + If a powerup is accepted, a ba.PowerupAcceptMessage should be sent + back to the sourcenode to inform it of the fact. This will generally + cause the powerup box to make a sound and disappear or whatnot. + """ + poweruptype: str + sourcenode: Optional[ba.Node] = None + + +@dataclass +class PowerupAcceptMessage: + """A message informing a ba.Powerup that it was accepted. + + Category: Message Classes + + This is generally sent in response to a ba.PowerupMessage + to inform the box (or whoever granted it) that it can go away. + """ + + +def get_default_powerup_distribution() -> Sequence[Tuple[str, int]]: + """Standard set of powerups.""" + return (('triple_bombs', 3), ('ice_bombs', 3), ('punch', 3), + ('impact_bombs', 3), ('land_mines', 2), ('sticky_bombs', 3), + ('shield', 2), ('health', 1), ('curse', 1)) diff --git a/dist/ba_data/python/ba/_profile.py b/dist/ba_data/python/ba/_profile.py new file mode 100644 index 0000000..a87e202 --- /dev/null +++ b/dist/ba_data/python/ba/_profile.py @@ -0,0 +1,93 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to player profiles.""" +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import List, Tuple, Any, Dict, Optional + +# NOTE: player color options are enforced server-side for non-pro accounts +# so don't change these or they won't stick... +PLAYER_COLORS = [(1, 0.15, 0.15), (0.2, 1, 0.2), (0.1, 0.1, 1), (0.2, 1, 1), + (0.5, 0.25, 1.0), (1, 1, 0), (1, 0.5, 0), (1, 0.3, 0.5), + (0.1, 0.1, 0.5), (0.4, 0.2, 0.1), (0.1, 0.35, 0.1), + (1, 0.8, 0.5), (0.4, 0.05, 0.05), (0.13, 0.13, 0.13), + (0.5, 0.5, 0.5), (1, 1, 1)] + + +def get_player_colors() -> List[Tuple[float, float, float]]: + """Return user-selectable player colors.""" + return PLAYER_COLORS + + +def get_player_profile_icon(profilename: str) -> str: + """Given a profile name, returns an icon string for it. + + (non-account profiles only) + """ + from ba._enums import SpecialChar + + appconfig = _ba.app.config + icon: str + try: + is_global = appconfig['Player Profiles'][profilename]['global'] + except KeyError: + is_global = False + if is_global: + try: + icon = appconfig['Player Profiles'][profilename]['icon'] + except KeyError: + icon = _ba.charstr(SpecialChar.LOGO) + else: + icon = '' + return icon + + +def get_player_profile_colors( + profilename: Optional[str], + profiles: Dict[str, Dict[str, Any]] = None +) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: + """Given a profile, return colors for them.""" + appconfig = _ba.app.config + if profiles is None: + profiles = appconfig['Player Profiles'] + + # Special case: when being asked for a random color in kiosk mode, + # always return default purple. + if (_ba.app.demo_mode or _ba.app.arcade_mode) and profilename is None: + color = (0.5, 0.4, 1.0) + highlight = (0.4, 0.4, 0.5) + else: + try: + assert profilename is not None + color = profiles[profilename]['color'] + except (KeyError, AssertionError): + # Key off name if possible. + if profilename is None: + # First 6 are bright-ish. + color = PLAYER_COLORS[random.randrange(6)] + else: + # First 6 are bright-ish. + color = PLAYER_COLORS[sum([ord(c) for c in profilename]) % 6] + + try: + assert profilename is not None + highlight = profiles[profilename]['highlight'] + except (KeyError, AssertionError): + # Key off name if possible. + if profilename is None: + # Last 2 are grey and white; ignore those or we + # get lots of old-looking players. + highlight = PLAYER_COLORS[random.randrange( + len(PLAYER_COLORS) - 2)] + else: + highlight = PLAYER_COLORS[sum( + [ord(c) + 1 + for c in profilename]) % (len(PLAYER_COLORS) - 2)] + + return color, highlight diff --git a/dist/ba_data/python/ba/_score.py b/dist/ba_data/python/ba/_score.py new file mode 100644 index 0000000..d1cbe52 --- /dev/null +++ b/dist/ba_data/python/ba/_score.py @@ -0,0 +1,56 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Score related functionality.""" + +from __future__ import annotations + +from enum import Enum, unique +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import ba + + +@unique +class ScoreType(Enum): + """Type of scores. + + Category: Enums + """ + SECONDS = 's' + MILLISECONDS = 'ms' + POINTS = 'p' + + +@dataclass +class ScoreConfig: + """Settings for how a game handles scores. + + Category: Gameplay Classes + + Attributes: + + label + A label show to the user for scores; 'Score', 'Time Survived', etc. + + scoretype + How the score value should be displayed. + + lower_is_better + Whether lower scores are preferable. Higher scores are by default. + + none_is_winner + Whether a value of None is considered better than other scores. + By default it is not. + + version + To change high-score lists used by a game without renaming the game, + change this. Defaults to an empty string. + + """ + label: str = 'Score' + scoretype: ba.ScoreType = ScoreType.POINTS + lower_is_better: bool = False + none_is_winner: bool = False + version: str = '' diff --git a/dist/ba_data/python/ba/_servermode.py b/dist/ba_data/python/ba/_servermode.py new file mode 100644 index 0000000..ec540d3 --- /dev/null +++ b/dist/ba_data/python/ba/_servermode.py @@ -0,0 +1,352 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to running the game in server-mode.""" +from __future__ import annotations + +import sys +import time +from typing import TYPE_CHECKING + +from efro.terminal import Clr +from bacommon.servermanager import (ServerCommand, StartServerModeCommand, + ShutdownCommand, ShutdownReason, + ChatMessageCommand, ScreenMessageCommand, + ClientListCommand, KickCommand) +import _ba +from ba._enums import TimeType +from ba._freeforallsession import FreeForAllSession +from ba._dualteamsession import DualTeamSession + +if TYPE_CHECKING: + from typing import Optional, Dict, Any, Type + import ba + from bacommon.servermanager import ServerConfig + + +def _cmd(command_data: bytes) -> None: + """Handle commands coming in from our server manager parent process.""" + import pickle + command = pickle.loads(command_data) + assert isinstance(command, ServerCommand) + + if isinstance(command, StartServerModeCommand): + assert _ba.app.server is None + _ba.app.server = ServerController(command.config) + return + + if isinstance(command, ShutdownCommand): + assert _ba.app.server is not None + _ba.app.server.shutdown(reason=command.reason, + immediate=command.immediate) + return + + if isinstance(command, ChatMessageCommand): + assert _ba.app.server is not None + _ba.chatmessage(command.message, clients=command.clients) + return + + if isinstance(command, ScreenMessageCommand): + assert _ba.app.server is not None + + # Note: we have to do transient messages if + # clients is specified, so they won't show up + # in replays. + _ba.screenmessage(command.message, + color=command.color, + clients=command.clients, + transient=command.clients is not None) + return + + if isinstance(command, ClientListCommand): + assert _ba.app.server is not None + _ba.app.server.print_client_list() + return + + if isinstance(command, KickCommand): + assert _ba.app.server is not None + _ba.app.server.kick(client_id=command.client_id, + ban_time=command.ban_time) + return + + print(f'{Clr.SRED}ERROR: server process' + f' got unknown command: {type(command)}{Clr.RST}') + + +class ServerController: + """Overall controller for the app in server mode. + + Category: App Classes + """ + + def __init__(self, config: ServerConfig) -> None: + + self._config = config + self._playlist_name = '__default__' + self._ran_access_check = False + self._prep_timer: Optional[ba.Timer] = None + self._next_stuck_login_warn_time = time.time() + 10.0 + self._first_run = True + self._shutdown_reason: Optional[ShutdownReason] = None + self._executing_shutdown = False + + # Make note if they want us to import a playlist; + # we'll need to do that first if so. + self._playlist_fetch_running = self._config.playlist_code is not None + self._playlist_fetch_sent_request = False + self._playlist_fetch_got_response = False + self._playlist_fetch_code = -1 + + # Now sit around doing any pre-launch prep such as waiting for + # account sign-in or fetching playlists; this will kick off the + # session once done. + with _ba.Context('ui'): + self._prep_timer = _ba.Timer(0.25, + self._prepare_to_serve, + timetype=TimeType.REAL, + repeat=True) + + def print_client_list(self) -> None: + """Print info about all connected clients.""" + import json + roster = _ba.get_game_roster() + title1 = 'Client ID' + title2 = 'Account Name' + title3 = 'Players' + col1 = 10 + col2 = 16 + out = (f'{Clr.BLD}' + f'{title1:<{col1}} {title2:<{col2}} {title3}' + f'{Clr.RST}') + for client in roster: + if client['client_id'] == -1: + continue + spec = json.loads(client['spec_string']) + name = spec['n'] + players = ', '.join(n['name'] for n in client['players']) + clientid = client['client_id'] + out += f'\n{clientid:<{col1}} {name:<{col2}} {players}' + print(out) + + def kick(self, client_id: int, ban_time: Optional[int]) -> None: + """Kick the provided client id. + + ban_time is provided in seconds. + If ban_time is None, ban duration will be determined automatically. + Pass 0 or a negative number for no ban time. + """ + + # FIXME: this case should be handled under the hood. + if ban_time is None: + ban_time = 300 + + _ba.disconnect_client(client_id=client_id, ban_time=ban_time) + + def shutdown(self, reason: ShutdownReason, immediate: bool) -> None: + """Set the app to quit either now or at the next clean opportunity.""" + self._shutdown_reason = reason + if immediate: + print(f'{Clr.SBLU}Immediate shutdown initiated.{Clr.RST}') + self._execute_shutdown() + else: + print(f'{Clr.SBLU}Shutdown initiated;' + f' server process will exit at the next clean opportunity.' + f'{Clr.RST}') + + def handle_transition(self) -> bool: + """Handle transitioning to a new ba.Session or quitting the app. + + Will be called once at the end of an activity that is marked as + a good 'end-point' (such as a final score screen). + Should return True if action will be handled by us; False if the + session should just continue on it's merry way. + """ + if self._shutdown_reason is not None: + self._execute_shutdown() + return True + return False + + def _execute_shutdown(self) -> None: + from ba._language import Lstr + if self._executing_shutdown: + return + self._executing_shutdown = True + timestrval = time.strftime('%c') + if self._shutdown_reason is ShutdownReason.RESTARTING: + _ba.screenmessage(Lstr(resource='internal.serverRestartingText'), + color=(1, 0.5, 0.0)) + print(f'{Clr.SBLU}Exiting for server-restart' + f' at {timestrval}.{Clr.RST}') + else: + _ba.screenmessage(Lstr(resource='internal.serverShuttingDownText'), + color=(1, 0.5, 0.0)) + print(f'{Clr.SBLU}Exiting for server-shutdown' + f' at {timestrval}.{Clr.RST}') + with _ba.Context('ui'): + _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL) + + def _run_access_check(self) -> None: + """Check with the master server to see if we're likely joinable.""" + from ba._netutils import master_server_get + master_server_get( + 'bsAccessCheck', + { + 'port': _ba.get_game_port(), + 'b': _ba.app.build_number + }, + callback=self._access_check_response, + ) + + def _access_check_response(self, data: Optional[Dict[str, Any]]) -> None: + import os + if data is None: + print('error on UDP port access check (internet down?)') + else: + addr = data['address'] + port = data['port'] + show_addr = os.environ.get('BA_ACCESS_CHECK_VERBOSE', '0') == '1' + if show_addr: + addrstr = f' {addr}' + poststr = '' + else: + addrstr = '' + poststr = ( + '\nSet environment variable BA_ACCESS_CHECK_VERBOSE=1' + ' for more info.') + if data['accessible']: + print(f'{Clr.SBLU}Master server access check of{addrstr}' + f' udp port {port} succeeded.\n' + f'Your server appears to be' + f' joinable from the internet.{poststr}{Clr.RST}') + else: + print(f'{Clr.SRED}Master server access check of{addrstr}' + f' udp port {port} failed.\n' + f'Your server does not appear to be' + f' joinable from the internet.{poststr}{Clr.RST}') + + def _prepare_to_serve(self) -> None: + """Run in a timer to do prep before beginning to serve.""" + signed_in = _ba.get_account_state() == 'signed_in' + if not signed_in: + + # Signing in to the local server account should not take long; + # complain if it does... + curtime = time.time() + if curtime > self._next_stuck_login_warn_time: + print('Still waiting for account sign-in...') + self._next_stuck_login_warn_time = curtime + 10.0 + return + + can_launch = False + + # If we're fetching a playlist, we need to do that first. + if not self._playlist_fetch_running: + can_launch = True + else: + if not self._playlist_fetch_sent_request: + print(f'{Clr.SBLU}Requesting shared-playlist' + f' {self._config.playlist_code}...{Clr.RST}') + _ba.add_transaction( + { + 'type': 'IMPORT_PLAYLIST', + 'code': str(self._config.playlist_code), + 'overwrite': True + }, + callback=self._on_playlist_fetch_response) + _ba.run_transactions() + self._playlist_fetch_sent_request = True + + if self._playlist_fetch_got_response: + self._playlist_fetch_running = False + can_launch = True + + if can_launch: + self._prep_timer = None + _ba.pushcall(self._launch_server_session) + + def _on_playlist_fetch_response( + self, + result: Optional[Dict[str, Any]], + ) -> None: + if result is None: + print('Error fetching playlist; aborting.') + sys.exit(-1) + + # Once we get here, simply modify our config to use this playlist. + typename = ( + 'teams' if result['playlistType'] == 'Team Tournament' else + 'ffa' if result['playlistType'] == 'Free-for-All' else '??') + plistname = result['playlistName'] + print(f'{Clr.SBLU}Got playlist: "{plistname}" ({typename}).{Clr.RST}') + self._playlist_fetch_got_response = True + self._config.session_type = typename + self._playlist_name = (result['playlistName']) + + def _get_session_type(self) -> Type[ba.Session]: + # Convert string session type to the class. + # Hmm should we just keep this as a string? + if self._config.session_type == 'ffa': + return FreeForAllSession + if self._config.session_type == 'teams': + return DualTeamSession + raise RuntimeError( + f'Invalid session_type: "{self._config.session_type}"') + + def _launch_server_session(self) -> None: + """Kick off a host-session based on the current server config.""" + app = _ba.app + appcfg = app.config + sessiontype = self._get_session_type() + + if _ba.get_account_state() != 'signed_in': + print('WARNING: launch_server_session() expects to run ' + 'with a signed in server account') + + if self._first_run: + curtimestr = time.strftime('%c') + _ba.log( + f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}' + f' ({app.build_number})' + f' entering server-mode {curtimestr}{Clr.RST}', + to_server=False) + + if sessiontype is FreeForAllSession: + appcfg['Free-for-All Playlist Selection'] = self._playlist_name + appcfg['Free-for-All Playlist Randomize'] = ( + self._config.playlist_shuffle) + elif sessiontype is DualTeamSession: + appcfg['Team Tournament Playlist Selection'] = self._playlist_name + appcfg['Team Tournament Playlist Randomize'] = ( + self._config.playlist_shuffle) + else: + raise RuntimeError(f'Unknown session type {sessiontype}') + + app.teams_series_length = self._config.teams_series_length + app.ffa_series_length = self._config.ffa_series_length + + _ba.set_authenticate_clients(self._config.authenticate_clients) + + _ba.set_enable_default_kick_voting( + self._config.enable_default_kick_voting) + _ba.set_admins(self._config.admins) + + # Call set-enabled last (will push state to the cloud). + _ba.set_public_party_max_size(self._config.max_party_size) + _ba.set_public_party_name(self._config.party_name) + _ba.set_public_party_stats_url(self._config.stats_url) + _ba.set_public_party_enabled(self._config.party_is_public) + + # And here.. we.. go. + if self._config.stress_test_players is not None: + # Special case: run a stress test. + from ba.internal import run_stress_test + run_stress_test(playlist_type='Random', + playlist_name='__default__', + player_count=self._config.stress_test_players, + round_duration=30) + else: + _ba.new_host_session(sessiontype) + + # Run an access check if we're trying to make a public party. + if not self._ran_access_check and self._config.party_is_public: + self._run_access_check() + self._ran_access_check = True diff --git a/dist/ba_data/python/ba/_session.py b/dist/ba_data/python/ba/_session.py new file mode 100644 index 0000000..b892832 --- /dev/null +++ b/dist/ba_data/python/ba/_session.py @@ -0,0 +1,698 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines base session class.""" +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING + +import _ba +from ba._error import print_error, print_exception, NodeNotFoundError +from ba._language import Lstr +from ba._player import Player + +if TYPE_CHECKING: + from typing import Sequence, List, Dict, Any, Optional, Set + import ba + + +class Session: + """Defines a high level series of activities with a common purpose. + + category: Gameplay Classes + + Examples of sessions are ba.FreeForAllSession, ba.DualTeamSession, and + ba.CoopSession. + + A Session is responsible for wrangling and transitioning between various + ba.Activity instances such as mini-games and score-screens, and for + maintaining state between them (players, teams, score tallies, etc). + + Attributes: + + sessionteams + All the ba.SessionTeams in the Session. Most things should use the + list of ba.Teams in ba.Activity; not this. + + sessionplayers + All ba.SessionPlayers in the Session. Most things should use the + list of ba.Players in ba.Activity; not this. Some players, such as + those who have not yet selected a character, will only be + found on this list. + + min_players + The minimum number of players who must be present for the Session + to proceed past the initial joining screen. + + max_players + The maximum number of players allowed in the Session. + + lobby + The ba.Lobby instance where new ba.Players go to select a + Profile/Team/etc. before being added to games. + Be aware this value may be None if a Session does not allow + any such selection. + + use_teams + Whether this session groups players into an explicit set of + teams. If this is off, a unique team is generated for each + player that joins. + + use_team_colors + Whether players on a team should all adopt the colors of that + team instead of their own profile colors. This only applies if + use_teams is enabled. + + allow_mid_activity_joins + Whether players should be allowed to join in the middle of + activities. + + customdata + A shared dictionary for objects to use as storage on this session. + Ensure that keys here are unique to avoid collisions. + + """ + use_teams: bool = False + use_team_colors: bool = True + allow_mid_activity_joins: bool = True + + # Note: even though these are instance vars, we annotate them at the + # class level so that docs generation can access their types. + lobby: ba.Lobby + max_players: int + min_players: int + sessionplayers: List[ba.SessionPlayer] + customdata: dict + sessionteams: List[ba.SessionTeam] + + def __init__(self, + depsets: Sequence[ba.DependencySet], + team_names: Sequence[str] = None, + team_colors: Sequence[Sequence[float]] = None, + min_players: int = 1, + max_players: int = 8): + """Instantiate a session. + + depsets should be a sequence of successfully resolved ba.DependencySet + instances; one for each ba.Activity the session may potentially run. + """ + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from ba._lobby import Lobby + from ba._stats import Stats + from ba._gameactivity import GameActivity + from ba._activity import Activity + from ba._team import SessionTeam + from ba._error import DependencyError + from ba._dependency import Dependency, AssetPackage + from efro.util import empty_weakref + + # First off, resolve all dependency-sets we were passed. + # If things are missing, we'll try to gather them into a single + # missing-deps exception if possible to give the caller a clean + # path to download missing stuff and try again. + missing_asset_packages: Set[str] = set() + for depset in depsets: + try: + depset.resolve() + except DependencyError as exc: + # Gather/report missing assets only; barf on anything else. + if all(issubclass(d.cls, AssetPackage) for d in exc.deps): + for dep in exc.deps: + assert isinstance(dep.config, str) + missing_asset_packages.add(dep.config) + else: + missing_info = [(d.cls, d.config) for d in exc.deps] + raise RuntimeError( + f'Missing non-asset dependencies: {missing_info}' + ) from exc + + # Throw a combined exception if we found anything missing. + if missing_asset_packages: + raise DependencyError([ + Dependency(AssetPackage, set_id) + for set_id in missing_asset_packages + ]) + + # Ok; looks like our dependencies check out. + # Now give the engine a list of asset-set-ids to pass along to clients. + required_asset_packages: Set[str] = set() + for depset in depsets: + required_asset_packages.update(depset.get_asset_package_ids()) + + # print('Would set host-session asset-reqs to:', + # required_asset_packages) + + # Init our C++ layer data. + self._sessiondata = _ba.register_session(self) + + # Should remove this if possible. + self.tournament_id: Optional[str] = None + + self.sessionteams = [] + self.sessionplayers = [] + self.min_players = min_players + self.max_players = max_players + + self.customdata = {} + self._in_set_activity = False + self._next_team_id = 0 + self._activity_retained: Optional[ba.Activity] = None + self._launch_end_session_activity_time: Optional[float] = None + self._activity_end_timer: Optional[ba.Timer] = None + self._activity_weak = empty_weakref(Activity) + self._next_activity: Optional[ba.Activity] = None + self._wants_to_end = False + self._ending = False + self._activity_should_end_immediately = False + self._activity_should_end_immediately_results: ( + Optional[ba.GameResults]) = None + self._activity_should_end_immediately_delay = 0.0 + + # Create static teams if we're using them. + if self.use_teams: + assert team_names is not None + assert team_colors is not None + for i, color in enumerate(team_colors): + team = SessionTeam(team_id=self._next_team_id, + name=GameActivity.get_team_display_string( + team_names[i]), + color=color) + self.sessionteams.append(team) + self._next_team_id += 1 + try: + with _ba.Context(self): + self.on_team_join(team) + except Exception: + print_exception(f'Error in on_team_join for {self}.') + + self.lobby = Lobby() + self.stats = Stats() + + # Instantiate our session globals node which will apply its settings. + self._sessionglobalsnode = _ba.newnode('sessionglobals') + + @property + def sessionglobalsnode(self) -> ba.Node: + """The sessionglobals ba.Node for the session.""" + node = self._sessionglobalsnode + if not node: + raise NodeNotFoundError() + return node + + def on_player_request(self, player: ba.SessionPlayer) -> bool: + """Called when a new ba.Player wants to join the Session. + + This should return True or False to accept/reject. + """ + import privateserver as pvt + pvt.handlerequest(player) + # Limit player counts *unless* we're in a stress test. + if _ba.app.stress_test_reset_timer is None: + + if len(self.sessionplayers) >= self.max_players: + + # Print a rejection message *only* to the client trying to + # join (prevents spamming everyone else in the game). + _ba.playsound(_ba.getsound('error')) + _ba.screenmessage(Lstr(resource='playerLimitReachedText', + subs=[('${COUNT}', + str(self.max_players))]), + color=(0.8, 0.0, 0.0), + clients=[player.inputdevice.client_id], + transient=True) + return False + + _ba.playsound(_ba.getsound('dripity')) + return True + + def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None: + """Called when a previously-accepted ba.SessionPlayer leaves.""" + + if sessionplayer not in self.sessionplayers: + print('ERROR: Session.on_player_leave called' + ' for player not in our list.') + return + + _ba.playsound(_ba.getsound('playerLeft')) + + activity = self._activity_weak() + + if not sessionplayer.in_game: + + # Ok, the player is still in the lobby; simply remove them. + with _ba.Context(self): + try: + self.lobby.remove_chooser(sessionplayer) + except Exception: + print_exception('Error in Lobby.remove_chooser().') + else: + # Ok, they've already entered the game. Remove them from + # teams/activities/etc. + sessionteam = sessionplayer.sessionteam + assert sessionteam is not None + + _ba.screenmessage( + Lstr(resource='playerLeftText', + subs=[('${PLAYER}', sessionplayer.getname(full=True))])) + + # Remove them from their SessionTeam. + if sessionplayer in sessionteam.players: + sessionteam.players.remove(sessionplayer) + else: + print('SessionPlayer not found in SessionTeam' + ' in on_player_leave.') + + # Grab their activity-specific player instance. + player = sessionplayer.activityplayer + assert isinstance(player, (Player, type(None))) + + # Remove them from any current Activity. + if activity is not None: + if player in activity.players: + activity.remove_player(sessionplayer) + else: + print('Player not found in Activity in on_player_leave.') + + # If we're a non-team session, remove their team too. + if not self.use_teams: + self._remove_player_team(sessionteam, activity) + + # Now remove them from the session list. + self.sessionplayers.remove(sessionplayer) + + def _remove_player_team(self, sessionteam: ba.SessionTeam, + activity: Optional[ba.Activity]) -> None: + """Remove the player-specific team in non-teams mode.""" + + # They should have been the only one on their team. + assert not sessionteam.players + + # Remove their Team from the Activity. + if activity is not None: + if sessionteam.activityteam in activity.teams: + activity.remove_team(sessionteam) + else: + print('Team not found in Activity in on_player_leave.') + + # And then from the Session. + with _ba.Context(self): + if sessionteam in self.sessionteams: + try: + self.sessionteams.remove(sessionteam) + self.on_team_leave(sessionteam) + except Exception: + print_exception( + f'Error in on_team_leave for Session {self}.') + else: + print('Team no in Session teams in on_player_leave.') + try: + sessionteam.leave() + except Exception: + print_exception(f'Error clearing sessiondata' + f' for team {sessionteam} in session {self}.') + + def end(self) -> None: + """Initiates an end to the session and a return to the main menu. + + Note that this happens asynchronously, allowing the + session and its activities to shut down gracefully. + """ + self._wants_to_end = True + if self._next_activity is None: + self._launch_end_session_activity() + + def _launch_end_session_activity(self) -> None: + """(internal)""" + from ba._activitytypes import EndSessionActivity + from ba._enums import TimeType + with _ba.Context(self): + curtime = _ba.time(TimeType.REAL) + if self._ending: + # Ignore repeats unless its been a while. + assert self._launch_end_session_activity_time is not None + since_last = (curtime - self._launch_end_session_activity_time) + if since_last < 30.0: + return + print_error( + '_launch_end_session_activity called twice (since_last=' + + str(since_last) + ')') + self._launch_end_session_activity_time = curtime + self.setactivity(_ba.newactivity(EndSessionActivity)) + self._wants_to_end = False + self._ending = True # Prevent further actions. + + def on_team_join(self, team: ba.SessionTeam) -> None: + """Called when a new ba.Team joins the session.""" + + def on_team_leave(self, team: ba.SessionTeam) -> None: + """Called when a ba.Team is leaving the session.""" + + def end_activity(self, activity: ba.Activity, results: Any, delay: float, + force: bool) -> None: + """Commence shutdown of a ba.Activity (if not already occurring). + + 'delay' is the time delay before the Activity actually ends + (in seconds). Further calls to end() will be ignored up until + this time, unless 'force' is True, in which case the new results + will replace the old. + """ + from ba._general import Call + from ba._enums import TimeType + + # Only pay attention if this is coming from our current activity. + if activity is not self._activity_retained: + return + + # If this activity hasn't begun yet, just set it up to end immediately + # once it does. + if not activity.has_begun(): + # activity.set_immediate_end(results, delay, force) + if not self._activity_should_end_immediately or force: + self._activity_should_end_immediately = True + self._activity_should_end_immediately_results = results + self._activity_should_end_immediately_delay = delay + + # The activity has already begun; get ready to end it. + else: + if (not activity.has_ended()) or force: + activity.set_has_ended(True) + + # Set a timer to set in motion this activity's demise. + self._activity_end_timer = _ba.Timer( + delay, + Call(self._complete_end_activity, activity, results), + timetype=TimeType.BASE) + + def handlemessage(self, msg: Any) -> Any: + """General message handling; can be passed any message object.""" + from ba._lobby import PlayerReadyMessage + from ba._messages import PlayerProfilesChangedMessage, UNHANDLED + + if isinstance(msg, PlayerReadyMessage): + self._on_player_ready(msg.chooser) + + elif isinstance(msg, PlayerProfilesChangedMessage): + # If we have a current activity with a lobby, ask it to reload + # profiles. + with _ba.Context(self): + self.lobby.reload_profiles() + return None + + else: + return UNHANDLED + return None + + class _SetActivityScopedLock: + + def __init__(self, session: ba.Session) -> None: + self._session = session + if session._in_set_activity: + raise RuntimeError('Session.setactivity() called recursively.') + self._session._in_set_activity = True + + def __del__(self) -> None: + self._session._in_set_activity = False + + def setactivity(self, activity: ba.Activity) -> None: + """Assign a new current ba.Activity for the session. + + Note that this will not change the current context to the new + Activity's. Code must be run in the new activity's methods + (on_transition_in, etc) to get it. (so you can't do + session.setactivity(foo) and then ba.newnode() to add a node to foo) + """ + from ba._enums import TimeType + + # Make sure we don't get called recursively. + _rlock = self._SetActivityScopedLock(self) + + if activity.session is not _ba.getsession(): + raise RuntimeError("Provided Activity's Session is not current.") + + # Quietly ignore this if the whole session is going down. + if self._ending: + return + + if activity is self._activity_retained: + print_error('Activity set to already-current activity.') + return + + if self._next_activity is not None: + raise RuntimeError('Activity switch already in progress (to ' + + str(self._next_activity) + ')') + + prev_activity = self._activity_retained + prev_globals = (prev_activity.globalsnode + if prev_activity is not None else None) + + # Let the activity do its thing. + activity.transition_in(prev_globals) + + self._next_activity = activity + + # If we have a current activity, tell it it's transitioning out; + # the next one will become current once this one dies. + if prev_activity is not None: + prev_activity.transition_out() + + # Setting this to None should free up the old activity to die, + # which will call begin_next_activity. + # We can still access our old activity through + # self._activity_weak() to keep it up to date on player + # joins/departures/etc until it dies. + self._activity_retained = None + + # There's no existing activity; lets just go ahead with the begin call. + else: + self.begin_next_activity() + + # We want to call destroy() for the previous activity once it should + # tear itself down, clear out any self-refs, etc. After this call + # the activity should have no refs left to it and should die (which + # will trigger the next activity to run). + if prev_activity is not None: + with _ba.Context('ui'): + _ba.timer(max(0.0, activity.transition_time), + prev_activity.expire, + timetype=TimeType.REAL) + self._in_set_activity = False + + def getactivity(self) -> Optional[ba.Activity]: + """Return the current foreground activity for this session.""" + return self._activity_weak() + + def get_custom_menu_entries(self) -> List[Dict[str, Any]]: + """Subclasses can override this to provide custom menu entries. + + The returned value should be a list of dicts, each containing + a 'label' and 'call' entry, with 'label' being the text for + the entry and 'call' being the callable to trigger if the entry + is pressed. + """ + return [] + + def _complete_end_activity(self, activity: ba.Activity, + results: Any) -> None: + # Run the subclass callback in the session context. + try: + with _ba.Context(self): + self.on_activity_end(activity, results) + except Exception: + print_exception(f'Error in on_activity_end() for session {self}' + f' activity {activity} with results {results}') + + def _request_player(self, sessionplayer: ba.SessionPlayer) -> bool: + """Called by the native layer when a player wants to join.""" + + # If we're ending, allow no new players. + if self._ending: + return False + + # Ask the ba.Session subclass to approve/deny this request. + try: + with _ba.Context(self): + result = self.on_player_request(sessionplayer) + except Exception: + print_exception(f'Error in on_player_request for {self}') + result = False + + # If they said yes, add the player to the lobby. + if result: + self.sessionplayers.append(sessionplayer) + with _ba.Context(self): + try: + self.lobby.add_chooser(sessionplayer) + except Exception: + print_exception('Error in lobby.add_chooser().') + + return result + + def on_activity_end(self, activity: ba.Activity, results: Any) -> None: + """Called when the current ba.Activity has ended. + + The ba.Session should look at the results and start + another ba.Activity. + """ + + def begin_next_activity(self) -> None: + """Called once the previous activity has been totally torn down. + + This means we're ready to begin the next one + """ + if self._next_activity is None: + # Should this ever happen? + print_error('begin_next_activity() called with no _next_activity') + return + + # We store both a weak and a strong ref to the new activity; + # the strong is to keep it alive and the weak is so we can access + # it even after we've released the strong-ref to allow it to die. + self._activity_retained = self._next_activity + self._activity_weak = weakref.ref(self._next_activity) + self._next_activity = None + self._activity_should_end_immediately = False + + # Kick out anyone loitering in the lobby. + self.lobby.remove_all_choosers_and_kick_players() + + # Kick off the activity. + self._activity_retained.begin(self) + + # If we want to completely end the session, we can now kick that off. + if self._wants_to_end: + self._launch_end_session_activity() + else: + # Otherwise, if the activity has already been told to end, + # do so now. + if self._activity_should_end_immediately: + self._activity_retained.end( + self._activity_should_end_immediately_results, + self._activity_should_end_immediately_delay) + + def _on_player_ready(self, chooser: ba.Chooser) -> None: + """Called when a ba.Player has checked themself ready.""" + lobby = chooser.lobby + activity = self._activity_weak() + + # This happens sometimes. That seems like it shouldn't be happening; + # when would we have a session and a chooser with players but no + # active activity? + if activity is None: + print('_on_player_ready called with no activity.') + return + + # In joining-activities, we wait till all choosers are ready + # and then create all players at once. + if activity.is_joining_activity: + if not lobby.check_all_ready(): + return + choosers = lobby.get_choosers() + min_players = self.min_players + if len(choosers) >= min_players: + for lch in lobby.get_choosers(): + self._add_chosen_player(lch) + lobby.remove_all_choosers() + + # Get our next activity going. + self._complete_end_activity(activity, {}) + else: + _ba.screenmessage( + Lstr(resource='notEnoughPlayersText', + subs=[('${COUNT}', str(min_players))]), + color=(1, 1, 0), + ) + _ba.playsound(_ba.getsound('error')) + + # Otherwise just add players on the fly. + else: + self._add_chosen_player(chooser) + lobby.remove_chooser(chooser.getplayer()) + + def transitioning_out_activity_was_freed( + self, can_show_ad_on_death: bool) -> None: + """(internal)""" + from ba._apputils import garbage_collect + + # Since things should be generally still right now, it's a good time + # to run garbage collection to clear out any circular dependency + # loops. We keep this disabled normally to avoid non-deterministic + # hitches. + garbage_collect() + + with _ba.Context(self): + if can_show_ad_on_death: + _ba.app.ads.call_after_ad(self.begin_next_activity) + else: + _ba.pushcall(self.begin_next_activity) + + def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer: + from ba._team import SessionTeam + sessionplayer = chooser.getplayer() + assert sessionplayer in self.sessionplayers, ( + 'SessionPlayer not found in session ' + 'player-list after chooser selection.') + + activity = self._activity_weak() + assert activity is not None + + # Reset the player's input here, as it is probably + # referencing the chooser which could inadvertently keep it alive. + sessionplayer.resetinput() + + # We can pass it to the current activity if it has already begun + # (otherwise it'll get passed once begin is called). + pass_to_activity = (activity.has_begun() + and not activity.is_joining_activity) + + # However, if we're not allowing mid-game joins, don't actually pass; + # just announce the arrival and say they'll partake next round. + if pass_to_activity: + if not self.allow_mid_activity_joins: + pass_to_activity = False + with _ba.Context(self): + _ba.screenmessage( + Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', + sessionplayer.getname(full=True))]), + color=(0, 1, 0), + ) + + # If we're a non-team session, each player gets their own team. + # (keeps mini-game coding simpler if we can always deal with teams). + if self.use_teams: + sessionteam = chooser.sessionteam + else: + our_team_id = self._next_team_id + self._next_team_id += 1 + sessionteam = SessionTeam( + team_id=our_team_id, + color=chooser.get_color(), + name=chooser.getplayer().getname(full=True, icon=False), + ) + + # Add player's team to the Session. + self.sessionteams.append(sessionteam) + + with _ba.Context(self): + try: + self.on_team_join(sessionteam) + except Exception: + print_exception(f'Error in on_team_join for {self}.') + + # Add player's team to the Activity. + if pass_to_activity: + activity.add_team(sessionteam) + + assert sessionplayer not in sessionteam.players + sessionteam.players.append(sessionplayer) + sessionplayer.setdata(team=sessionteam, + character=chooser.get_character_name(), + color=chooser.get_color(), + highlight=chooser.get_highlight()) + + self.stats.register_sessionplayer(sessionplayer) + if pass_to_activity: + activity.add_player(sessionplayer) + return sessionplayer diff --git a/dist/ba_data/python/ba/_settings.py b/dist/ba_data/python/ba/_settings.py new file mode 100644 index 0000000..1b3e404 --- /dev/null +++ b/dist/ba_data/python/ba/_settings.py @@ -0,0 +1,84 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality for user-controllable settings.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from dataclasses import dataclass + +if TYPE_CHECKING: + from typing import Any, List, Tuple + + +@dataclass +class Setting: + """Defines a user-controllable setting for a game or other entity. + + Category: Gameplay Classes + """ + + name: str + default: Any + + +@dataclass +class BoolSetting(Setting): + """A boolean game setting. + + Category: Settings Classes + """ + default: bool + + +@dataclass +class IntSetting(Setting): + """An integer game setting. + + Category: Settings Classes + """ + default: int + min_value: int = 0 + max_value: int = 9999 + increment: int = 1 + + +@dataclass +class FloatSetting(Setting): + """A floating point game setting. + + Category: Settings Classes + """ + default: float + min_value: float = 0.0 + max_value: float = 9999.0 + increment: float = 1.0 + + +@dataclass +class ChoiceSetting(Setting): + """A setting with multiple choices. + + Category: Settings Classes + """ + choices: List[Tuple[str, Any]] + + +@dataclass +class IntChoiceSetting(ChoiceSetting): + """An int setting with multiple choices. + + Category: Settings Classes + """ + default: int + choices: List[Tuple[str, int]] + + +@dataclass +class FloatChoiceSetting(ChoiceSetting): + """A float setting with multiple choices. + + Category: Settings Classes + """ + default: float + choices: List[Tuple[str, float]] diff --git a/dist/ba_data/python/ba/_stats.py b/dist/ba_data/python/ba/_stats.py new file mode 100644 index 0000000..bf3becb --- /dev/null +++ b/dist/ba_data/python/ba/_stats.py @@ -0,0 +1,470 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to scores and statistics.""" +from __future__ import annotations + +import random +import weakref +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import _ba +from ba._error import (print_exception, print_error, SessionTeamNotFoundError, + SessionPlayerNotFoundError, NotFoundError) + +if TYPE_CHECKING: + import ba + from weakref import ReferenceType + from typing import Any, Dict, Optional, Sequence, Union, Tuple + + +@dataclass +class PlayerScoredMessage: + """Informs something that a ba.Player scored. + + Category: Message Classes + + Attributes: + + score + The score value. + """ + score: int + + +class PlayerRecord: + """Stats for an individual player in a ba.Stats object. + + Category: Gameplay Classes + + This does not necessarily correspond to a ba.Player that is + still present (stats may be retained for players that leave + mid-game) + """ + character: str + + def __init__(self, name: str, name_full: str, + sessionplayer: ba.SessionPlayer, stats: ba.Stats): + self.name = name + self.name_full = name_full + self.score = 0 + self.accumscore = 0 + self.kill_count = 0 + self.accum_kill_count = 0 + self.killed_count = 0 + self.accum_killed_count = 0 + self._multi_kill_timer: Optional[ba.Timer] = None + self._multi_kill_count = 0 + self._stats = weakref.ref(stats) + self._last_sessionplayer: Optional[ba.SessionPlayer] = None + self._sessionplayer: Optional[ba.SessionPlayer] = None + self._sessionteam: Optional[ReferenceType[ba.SessionTeam]] = None + self.streak = 0 + self.associate_with_sessionplayer(sessionplayer) + + @property + def team(self) -> ba.SessionTeam: + """The ba.SessionTeam the last associated player was last on. + + This can still return a valid result even if the player is gone. + Raises a ba.SessionTeamNotFoundError if the team no longer exists. + """ + assert self._sessionteam is not None + team = self._sessionteam() + if team is None: + raise SessionTeamNotFoundError() + return team + + @property + def player(self) -> ba.SessionPlayer: + """Return the instance's associated ba.SessionPlayer. + + Raises a ba.SessionPlayerNotFoundError if the player + no longer exists. + """ + if not self._sessionplayer: + raise SessionPlayerNotFoundError() + return self._sessionplayer + + def getname(self, full: bool = False) -> str: + """Return the player entry's name.""" + return self.name_full if full else self.name + + def get_icon(self) -> Dict[str, Any]: + """Get the icon for this instance's player.""" + player = self._last_sessionplayer + assert player is not None + return player.get_icon() + + def cancel_multi_kill_timer(self) -> None: + """Cancel any multi-kill timer for this player entry.""" + self._multi_kill_timer = None + + def getactivity(self) -> Optional[ba.Activity]: + """Return the ba.Activity this instance is currently associated with. + + Returns None if the activity no longer exists.""" + stats = self._stats() + if stats is not None: + return stats.getactivity() + return None + + def associate_with_sessionplayer(self, + sessionplayer: ba.SessionPlayer) -> None: + """Associate this entry with a ba.SessionPlayer.""" + self._sessionteam = weakref.ref(sessionplayer.sessionteam) + self.character = sessionplayer.character + self._last_sessionplayer = sessionplayer + self._sessionplayer = sessionplayer + self.streak = 0 + + def _end_multi_kill(self) -> None: + self._multi_kill_timer = None + self._multi_kill_count = 0 + + def get_last_sessionplayer(self) -> ba.SessionPlayer: + """Return the last ba.Player we were associated with.""" + assert self._last_sessionplayer is not None + return self._last_sessionplayer + + def submit_kill(self, showpoints: bool = True) -> None: + """Submit a kill for this player entry.""" + # FIXME Clean this up. + # pylint: disable=too-many-statements + from ba._language import Lstr + from ba._general import Call + self._multi_kill_count += 1 + stats = self._stats() + assert stats + if self._multi_kill_count == 1: + score = 0 + name = None + delay = 0.0 + color = (0.0, 0.0, 0.0, 1.0) + scale = 1.0 + sound = None + elif self._multi_kill_count == 2: + score = 20 + name = Lstr(resource='twoKillText') + color = (0.1, 1.0, 0.0, 1) + scale = 1.0 + delay = 0.0 + sound = stats.orchestrahitsound1 + elif self._multi_kill_count == 3: + score = 40 + name = Lstr(resource='threeKillText') + color = (1.0, 0.7, 0.0, 1) + scale = 1.1 + delay = 0.3 + sound = stats.orchestrahitsound2 + elif self._multi_kill_count == 4: + score = 60 + name = Lstr(resource='fourKillText') + color = (1.0, 1.0, 0.0, 1) + scale = 1.2 + delay = 0.6 + sound = stats.orchestrahitsound3 + elif self._multi_kill_count == 5: + score = 80 + name = Lstr(resource='fiveKillText') + color = (1.0, 0.5, 0.0, 1) + scale = 1.3 + delay = 0.9 + sound = stats.orchestrahitsound4 + else: + score = 100 + name = Lstr(resource='multiKillText', + subs=[('${COUNT}', str(self._multi_kill_count))]) + color = (1.0, 0.5, 0.0, 1) + scale = 1.3 + delay = 1.0 + sound = stats.orchestrahitsound4 + + def _apply(name2: Lstr, score2: int, showpoints2: bool, + color2: Tuple[float, float, float, float], scale2: float, + sound2: Optional[ba.Sound]) -> None: + from bastd.actor.popuptext import PopupText + + # Only award this if they're still alive and we can get + # a current position for them. + our_pos: Optional[ba.Vec3] = None + if self._sessionplayer: + if self._sessionplayer.activityplayer is not None: + try: + our_pos = self._sessionplayer.activityplayer.position + except NotFoundError: + pass + if our_pos is None: + return + + # Jitter position a bit since these often come in clusters. + our_pos = _ba.Vec3(our_pos[0] + (random.random() - 0.5) * 2.0, + our_pos[1] + (random.random() - 0.5) * 2.0, + our_pos[2] + (random.random() - 0.5) * 2.0) + activity = self.getactivity() + if activity is not None: + PopupText(Lstr( + value=(('+' + str(score2) + ' ') if showpoints2 else '') + + '${N}', + subs=[('${N}', name2)]), + color=color2, + scale=scale2, + position=our_pos).autoretain() + if sound2: + _ba.playsound(sound2) + + self.score += score2 + self.accumscore += score2 + + # Inform a running game of the score. + if score2 != 0 and activity is not None: + activity.handlemessage(PlayerScoredMessage(score=score2)) + + if name is not None: + _ba.timer( + 0.3 + delay, + Call(_apply, name, score, showpoints, color, scale, sound)) + + # Keep the tally rollin'... + # set a timer for a bit in the future. + self._multi_kill_timer = _ba.Timer(1.0, self._end_multi_kill) + + +class Stats: + """Manages scores and statistics for a ba.Session. + + category: Gameplay Classes + """ + + def __init__(self) -> None: + self._activity: Optional[ReferenceType[ba.Activity]] = None + self._player_records: Dict[str, PlayerRecord] = {} + self.orchestrahitsound1: Optional[ba.Sound] = None + self.orchestrahitsound2: Optional[ba.Sound] = None + self.orchestrahitsound3: Optional[ba.Sound] = None + self.orchestrahitsound4: Optional[ba.Sound] = None + + def setactivity(self, activity: Optional[ba.Activity]) -> None: + """Set the current activity for this instance.""" + + self._activity = None if activity is None else weakref.ref(activity) + + # Load our media into this activity's context. + if activity is not None: + if activity.expired: + print_error('unexpected finalized activity') + else: + with _ba.Context(activity): + self._load_activity_media() + + def getactivity(self) -> Optional[ba.Activity]: + """Get the activity associated with this instance. + + May return None. + """ + if self._activity is None: + return None + return self._activity() + + def _load_activity_media(self) -> None: + self.orchestrahitsound1 = _ba.getsound('orchestraHit') + self.orchestrahitsound2 = _ba.getsound('orchestraHit2') + self.orchestrahitsound3 = _ba.getsound('orchestraHit3') + self.orchestrahitsound4 = _ba.getsound('orchestraHit4') + + def reset(self) -> None: + """Reset the stats instance completely.""" + + # Just to be safe, lets make sure no multi-kill timers are gonna go off + # for no-longer-on-the-list players. + for p_entry in list(self._player_records.values()): + p_entry.cancel_multi_kill_timer() + self._player_records = {} + + def reset_accum(self) -> None: + """Reset per-sound sub-scores.""" + for s_player in list(self._player_records.values()): + s_player.cancel_multi_kill_timer() + s_player.accumscore = 0 + s_player.accum_kill_count = 0 + s_player.accum_killed_count = 0 + s_player.streak = 0 + + def register_sessionplayer(self, player: ba.SessionPlayer) -> None: + """Register a ba.SessionPlayer with this score-set.""" + assert player.exists() # Invalid refs should never be passed to funcs. + name = player.getname() + if name in self._player_records: + # If the player already exists, update his character and such as + # it may have changed. + self._player_records[name].associate_with_sessionplayer(player) + else: + name_full = player.getname(full=True) + self._player_records[name] = PlayerRecord(name, name_full, player, + self) + + def get_records(self) -> Dict[str, ba.PlayerRecord]: + """Get PlayerRecord corresponding to still-existing players.""" + records = {} + + # Go through our player records and return ones whose player id still + # corresponds to a player with that name. + for record_id, record in self._player_records.items(): + lastplayer = record.get_last_sessionplayer() + if lastplayer and lastplayer.getname() == record_id: + records[record_id] = record + return records + + def player_scored(self, + player: ba.Player, + base_points: int = 1, + target: Sequence[float] = None, + kill: bool = False, + victim_player: ba.Player = None, + scale: float = 1.0, + color: Sequence[float] = None, + title: Union[str, ba.Lstr] = None, + screenmessage: bool = True, + display: bool = True, + importance: int = 1, + showpoints: bool = True, + big_message: bool = False) -> int: + """Register a score for the player. + + Return value is actual score with multipliers and such factored in. + """ + # FIXME: Tidy this up. + # pylint: disable=cyclic-import + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + from bastd.actor.popuptext import PopupText + from ba import _math + from ba._gameactivity import GameActivity + from ba._language import Lstr + del victim_player # Currently unused. + name = player.getname() + s_player = self._player_records[name] + + if kill: + s_player.submit_kill(showpoints=showpoints) + + display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0) + + if color is not None: + display_color = color + elif importance != 1: + display_color = (1.0, 1.0, 0.4, 1.0) + points = base_points + + # If they want a big announcement, throw a zoom-text up there. + if display and big_message: + try: + assert self._activity is not None + activity = self._activity() + if isinstance(activity, GameActivity): + name_full = player.getname(full=True, icon=False) + activity.show_zoom_message( + Lstr(resource='nameScoresText', + subs=[('${NAME}', name_full)]), + color=_math.normalized_color(player.team.color)) + except Exception: + print_exception('error showing big_message') + + # If we currently have a actor, pop up a score over it. + if display and showpoints: + our_pos = player.node.position if player.node else None + if our_pos is not None: + if target is None: + target = our_pos + + # If display-pos is *way* lower than us, raise it up + # (so we can still see scores from dudes that fell off cliffs). + display_pos = (target[0], max(target[1], our_pos[1] - 2.0), + min(target[2], our_pos[2] + 2.0)) + activity = self.getactivity() + if activity is not None: + if title is not None: + sval = Lstr(value='+${A} ${B}', + subs=[('${A}', str(points)), + ('${B}', title)]) + else: + sval = Lstr(value='+${A}', + subs=[('${A}', str(points))]) + PopupText(sval, + color=display_color, + scale=1.2 * scale, + position=display_pos).autoretain() + + # Tally kills. + if kill: + s_player.accum_kill_count += 1 + s_player.kill_count += 1 + + # Report non-kill scorings. + try: + if screenmessage and not kill: + _ba.screenmessage(Lstr(resource='nameScoresText', + subs=[('${NAME}', name)]), + top=True, + color=player.color, + image=player.get_icon()) + except Exception: + print_exception('error announcing score') + + s_player.score += points + s_player.accumscore += points + + # Inform a running game of the score. + if points != 0: + activity = self._activity() if self._activity is not None else None + if activity is not None: + activity.handlemessage(PlayerScoredMessage(score=points)) + + return points + + def player_was_killed(self, + player: ba.Player, + killed: bool = False, + killer: ba.Player = None) -> None: + """Should be called when a player is killed.""" + from ba._language import Lstr + name = player.getname() + prec = self._player_records[name] + prec.streak = 0 + if killed: + prec.accum_killed_count += 1 + prec.killed_count += 1 + try: + if killed and _ba.getactivity().announce_player_deaths: + if killer is player: + _ba.screenmessage(Lstr(resource='nameSuicideText', + subs=[('${NAME}', name)]), + top=True, + color=player.color, + image=player.get_icon()) + elif killer is not None: + if killer.team is player.team: + _ba.screenmessage(Lstr(resource='nameBetrayedText', + subs=[('${NAME}', + killer.getname()), + ('${VICTIM}', name)]), + top=True, + color=killer.color, + image=killer.get_icon()) + else: + _ba.screenmessage(Lstr(resource='nameKilledText', + subs=[('${NAME}', + killer.getname()), + ('${VICTIM}', name)]), + top=True, + color=killer.color, + image=killer.get_icon()) + else: + _ba.screenmessage(Lstr(resource='nameDiedText', + subs=[('${NAME}', name)]), + top=True, + color=player.color, + image=player.get_icon()) + except Exception: + print_exception('error announcing kill') diff --git a/dist/ba_data/python/ba/_store.py b/dist/ba_data/python/ba/_store.py new file mode 100644 index 0000000..fd4e5bf --- /dev/null +++ b/dist/ba_data/python/ba/_store.py @@ -0,0 +1,510 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Store related functionality for classic mode.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Type, List, Dict, Tuple, Optional, Any + import ba + + +def get_store_item(item: str) -> Dict[str, Any]: + """(internal)""" + return get_store_items()[item] + + +def get_store_item_name_translated(item_name: str) -> ba.Lstr: + """Return a ba.Lstr for a store item name.""" + # pylint: disable=cyclic-import + from ba import _language + from ba import _map + item_info = get_store_item(item_name) + if item_name.startswith('characters.'): + return _language.Lstr(translate=('characterNames', + item_info['character'])) + if item_name in ['upgrades.pro', 'pro']: + return _language.Lstr(resource='store.bombSquadProNameText', + subs=[('${APP_NAME}', + _language.Lstr(resource='titleText'))]) + if item_name.startswith('maps.'): + map_type: Type[ba.Map] = item_info['map_type'] + return _map.get_map_display_string(map_type.name) + if item_name.startswith('games.'): + gametype: Type[ba.GameActivity] = item_info['gametype'] + return gametype.get_display_string() + if item_name.startswith('icons.'): + return _language.Lstr(resource='editProfileWindow.iconText') + raise ValueError('unrecognized item: ' + item_name) + + +def get_store_item_display_size(item_name: str) -> Tuple[float, float]: + """(internal)""" + if item_name.startswith('characters.'): + return 340 * 0.6, 430 * 0.6 + if item_name in ['pro', 'upgrades.pro']: + return 650 * 0.9, 500 * 0.85 + if item_name.startswith('maps.'): + return 510 * 0.6, 450 * 0.6 + if item_name.startswith('icons.'): + return 265 * 0.6, 250 * 0.6 + return 450 * 0.6, 450 * 0.6 + + +def get_store_items() -> Dict[str, Dict]: + """Returns info about purchasable items. + + (internal) + """ + # pylint: disable=cyclic-import + from ba._enums import SpecialChar + from bastd import maps + if _ba.app.store_items is None: + from bastd.game import ninjafight + from bastd.game import meteorshower + from bastd.game import targetpractice + from bastd.game import easteregghunt + + # IMPORTANT - need to keep this synced with the master server. + # (doing so manually for now) + _ba.app.store_items = { + 'characters.kronk': { + 'character': 'Kronk' + }, + 'characters.zoe': { + 'character': 'Zoe' + }, + 'characters.jackmorgan': { + 'character': 'Jack Morgan' + }, + 'characters.mel': { + 'character': 'Mel' + }, + 'characters.snakeshadow': { + 'character': 'Snake Shadow' + }, + 'characters.bones': { + 'character': 'Bones' + }, + 'characters.bernard': { + 'character': 'Bernard', + 'highlight': (0.6, 0.5, 0.8) + }, + 'characters.pixie': { + 'character': 'Pixel' + }, + 'characters.wizard': { + 'character': 'Grumbledorf' + }, + 'characters.frosty': { + 'character': 'Frosty' + }, + 'characters.pascal': { + 'character': 'Pascal' + }, + 'characters.cyborg': { + 'character': 'B-9000' + }, + 'characters.agent': { + 'character': 'Agent Johnson' + }, + 'characters.taobaomascot': { + 'character': 'Taobao Mascot' + }, + 'characters.santa': { + 'character': 'Santa Claus' + }, + 'characters.bunny': { + 'character': 'Easter Bunny' + }, + 'pro': {}, + 'maps.lake_frigid': { + 'map_type': maps.LakeFrigid + }, + 'games.ninja_fight': { + 'gametype': ninjafight.NinjaFightGame, + 'previewTex': 'courtyardPreview' + }, + 'games.meteor_shower': { + 'gametype': meteorshower.MeteorShowerGame, + 'previewTex': 'rampagePreview' + }, + 'games.target_practice': { + 'gametype': targetpractice.TargetPracticeGame, + 'previewTex': 'doomShroomPreview' + }, + 'games.easter_egg_hunt': { + 'gametype': easteregghunt.EasterEggHuntGame, + 'previewTex': 'towerDPreview' + }, + 'icons.flag_us': { + 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_STATES) + }, + 'icons.flag_mexico': { + 'icon': _ba.charstr(SpecialChar.FLAG_MEXICO) + }, + 'icons.flag_germany': { + 'icon': _ba.charstr(SpecialChar.FLAG_GERMANY) + }, + 'icons.flag_brazil': { + 'icon': _ba.charstr(SpecialChar.FLAG_BRAZIL) + }, + 'icons.flag_russia': { + 'icon': _ba.charstr(SpecialChar.FLAG_RUSSIA) + }, + 'icons.flag_china': { + 'icon': _ba.charstr(SpecialChar.FLAG_CHINA) + }, + 'icons.flag_uk': { + 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_KINGDOM) + }, + 'icons.flag_canada': { + 'icon': _ba.charstr(SpecialChar.FLAG_CANADA) + }, + 'icons.flag_india': { + 'icon': _ba.charstr(SpecialChar.FLAG_INDIA) + }, + 'icons.flag_japan': { + 'icon': _ba.charstr(SpecialChar.FLAG_JAPAN) + }, + 'icons.flag_france': { + 'icon': _ba.charstr(SpecialChar.FLAG_FRANCE) + }, + 'icons.flag_indonesia': { + 'icon': _ba.charstr(SpecialChar.FLAG_INDONESIA) + }, + 'icons.flag_italy': { + 'icon': _ba.charstr(SpecialChar.FLAG_ITALY) + }, + 'icons.flag_south_korea': { + 'icon': _ba.charstr(SpecialChar.FLAG_SOUTH_KOREA) + }, + 'icons.flag_netherlands': { + 'icon': _ba.charstr(SpecialChar.FLAG_NETHERLANDS) + }, + 'icons.flag_uae': { + 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_ARAB_EMIRATES) + }, + 'icons.flag_qatar': { + 'icon': _ba.charstr(SpecialChar.FLAG_QATAR) + }, + 'icons.flag_egypt': { + 'icon': _ba.charstr(SpecialChar.FLAG_EGYPT) + }, + 'icons.flag_kuwait': { + 'icon': _ba.charstr(SpecialChar.FLAG_KUWAIT) + }, + 'icons.flag_algeria': { + 'icon': _ba.charstr(SpecialChar.FLAG_ALGERIA) + }, + 'icons.flag_saudi_arabia': { + 'icon': _ba.charstr(SpecialChar.FLAG_SAUDI_ARABIA) + }, + 'icons.flag_malaysia': { + 'icon': _ba.charstr(SpecialChar.FLAG_MALAYSIA) + }, + 'icons.flag_czech_republic': { + 'icon': _ba.charstr(SpecialChar.FLAG_CZECH_REPUBLIC) + }, + 'icons.flag_australia': { + 'icon': _ba.charstr(SpecialChar.FLAG_AUSTRALIA) + }, + 'icons.flag_singapore': { + 'icon': _ba.charstr(SpecialChar.FLAG_SINGAPORE) + }, + 'icons.flag_iran': { + 'icon': _ba.charstr(SpecialChar.FLAG_IRAN) + }, + 'icons.flag_poland': { + 'icon': _ba.charstr(SpecialChar.FLAG_POLAND) + }, + 'icons.flag_argentina': { + 'icon': _ba.charstr(SpecialChar.FLAG_ARGENTINA) + }, + 'icons.flag_philippines': { + 'icon': _ba.charstr(SpecialChar.FLAG_PHILIPPINES) + }, + 'icons.flag_chile': { + 'icon': _ba.charstr(SpecialChar.FLAG_CHILE) + }, + 'icons.fedora': { + 'icon': _ba.charstr(SpecialChar.FEDORA) + }, + 'icons.hal': { + 'icon': _ba.charstr(SpecialChar.HAL) + }, + 'icons.crown': { + 'icon': _ba.charstr(SpecialChar.CROWN) + }, + 'icons.yinyang': { + 'icon': _ba.charstr(SpecialChar.YIN_YANG) + }, + 'icons.eyeball': { + 'icon': _ba.charstr(SpecialChar.EYE_BALL) + }, + 'icons.skull': { + 'icon': _ba.charstr(SpecialChar.SKULL) + }, + 'icons.heart': { + 'icon': _ba.charstr(SpecialChar.HEART) + }, + 'icons.dragon': { + 'icon': _ba.charstr(SpecialChar.DRAGON) + }, + 'icons.helmet': { + 'icon': _ba.charstr(SpecialChar.HELMET) + }, + 'icons.mushroom': { + 'icon': _ba.charstr(SpecialChar.MUSHROOM) + }, + 'icons.ninja_star': { + 'icon': _ba.charstr(SpecialChar.NINJA_STAR) + }, + 'icons.viking_helmet': { + 'icon': _ba.charstr(SpecialChar.VIKING_HELMET) + }, + 'icons.moon': { + 'icon': _ba.charstr(SpecialChar.MOON) + }, + 'icons.spider': { + 'icon': _ba.charstr(SpecialChar.SPIDER) + }, + 'icons.fireball': { + 'icon': _ba.charstr(SpecialChar.FIREBALL) + }, + 'icons.mikirog': { + 'icon': _ba.charstr(SpecialChar.MIKIROG) + }, + } + store_items = _ba.app.store_items + assert store_items is not None + return store_items + + +def get_store_layout() -> Dict[str, List[Dict[str, Any]]]: + """Return what's available in the store at a given time. + + Categorized by tab and by section.""" + if _ba.app.store_layout is None: + _ba.app.store_layout = { + 'characters': [{ + 'items': [] + }], + 'extras': [{ + 'items': ['pro'] + }], + 'maps': [{ + 'items': ['maps.lake_frigid'] + }], + 'minigames': [], + 'icons': [{ + 'items': [ + 'icons.mushroom', + 'icons.heart', + 'icons.eyeball', + 'icons.yinyang', + 'icons.hal', + 'icons.flag_us', + 'icons.flag_mexico', + 'icons.flag_germany', + 'icons.flag_brazil', + 'icons.flag_russia', + 'icons.flag_china', + 'icons.flag_uk', + 'icons.flag_canada', + 'icons.flag_india', + 'icons.flag_japan', + 'icons.flag_france', + 'icons.flag_indonesia', + 'icons.flag_italy', + 'icons.flag_south_korea', + 'icons.flag_netherlands', + 'icons.flag_uae', + 'icons.flag_qatar', + 'icons.flag_egypt', + 'icons.flag_kuwait', + 'icons.flag_algeria', + 'icons.flag_saudi_arabia', + 'icons.flag_malaysia', + 'icons.flag_czech_republic', + 'icons.flag_australia', + 'icons.flag_singapore', + 'icons.flag_iran', + 'icons.flag_poland', + 'icons.flag_argentina', + 'icons.flag_philippines', + 'icons.flag_chile', + 'icons.moon', + 'icons.fedora', + 'icons.spider', + 'icons.ninja_star', + 'icons.skull', + 'icons.dragon', + 'icons.viking_helmet', + 'icons.fireball', + 'icons.helmet', + 'icons.crown', + ] + }] + } + store_layout = _ba.app.store_layout + assert store_layout is not None + store_layout['characters'] = [{ + 'items': [ + 'characters.kronk', 'characters.zoe', 'characters.jackmorgan', + 'characters.mel', 'characters.snakeshadow', 'characters.bones', + 'characters.bernard', 'characters.agent', 'characters.frosty', + 'characters.pascal', 'characters.pixie' + ] + }] + store_layout['minigames'] = [{ + 'items': [ + 'games.ninja_fight', 'games.meteor_shower', 'games.target_practice' + ] + }] + if _ba.get_account_misc_read_val('xmas', False): + store_layout['characters'][0]['items'].append('characters.santa') + store_layout['characters'][0]['items'].append('characters.wizard') + store_layout['characters'][0]['items'].append('characters.cyborg') + if _ba.get_account_misc_read_val('easter', False): + store_layout['characters'].append({ + 'title': 'store.holidaySpecialText', + 'items': ['characters.bunny'] + }) + store_layout['minigames'].append({ + 'title': 'store.holidaySpecialText', + 'items': ['games.easter_egg_hunt'] + }) + return store_layout + + +def get_clean_price(price_string: str) -> str: + """(internal)""" + + # I'm not brave enough to try and do any numerical + # manipulation on formatted price strings, but lets do a + # few swap-outs to tidy things up a bit. + psubs = { + '$2.99': '$3.00', + '$4.99': '$5.00', + '$9.99': '$10.00', + '$19.99': '$20.00', + '$49.99': '$50.00' + } + return psubs.get(price_string, price_string) + + +def get_available_purchase_count(tab: str = None) -> int: + """(internal)""" + try: + if _ba.get_account_state() != 'signed_in': + return 0 + count = 0 + our_tickets = _ba.get_account_ticket_count() + store_data = get_store_layout() + if tab is not None: + tabs = [(tab, store_data[tab])] + else: + tabs = list(store_data.items()) + for tab_name, tabval in tabs: + if tab_name == 'icons': + continue # too many of these; don't show.. + count = _calc_count_for_tab(tabval, our_tickets, count) + return count + except Exception: + from ba import _error + _error.print_exception('error calcing available purchases') + return 0 + + +def _calc_count_for_tab(tabval: List[Dict[str, Any]], our_tickets: int, + count: int) -> int: + for section in tabval: + for item in section['items']: + ticket_cost = _ba.get_account_misc_read_val('price.' + item, None) + if ticket_cost is not None: + if (our_tickets >= ticket_cost + and not _ba.get_purchased(item)): + count += 1 + return count + + +def get_available_sale_time(tab: str) -> Optional[int]: + """(internal)""" + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + # pylint: disable=too-many-locals + try: + import datetime + from ba._enums import TimeType, TimeFormat + app = _ba.app + sale_times: List[Optional[int]] = [] + + # Calc time for our pro sale (old special case). + if tab == 'extras': + config = app.config + if app.accounts.have_pro(): + return None + + # If we haven't calced/loaded start times yet. + if app.pro_sale_start_time is None: + + # If we've got a time-remaining in our config, start there. + if 'PSTR' in config: + app.pro_sale_start_time = int( + _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)) + app.pro_sale_start_val = config['PSTR'] + else: + + # We start the timer once we get the duration from + # the server. + start_duration = _ba.get_account_misc_read_val( + 'proSaleDurationMinutes', None) + if start_duration is not None: + app.pro_sale_start_time = int( + _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)) + app.pro_sale_start_val = (60000 * start_duration) + + # If we haven't heard from the server yet, no sale.. + else: + return None + + assert app.pro_sale_start_val is not None + val: Optional[int] = max( + 0, app.pro_sale_start_val - + (_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) - + app.pro_sale_start_time)) + + # Keep the value in the config up to date. I suppose we should + # write the config occasionally but it should happen often enough + # for other reasons. + config['PSTR'] = val + if val == 0: + val = None + sale_times.append(val) + + # Now look for sales in this tab. + sales_raw = _ba.get_account_misc_read_val('sales', {}) + store_layout = get_store_layout() + for section in store_layout[tab]: + for item in section['items']: + if item in sales_raw: + if not _ba.get_purchased(item): + to_end = ((datetime.datetime.utcfromtimestamp( + sales_raw[item]['e']) - + datetime.datetime.utcnow()).total_seconds()) + if to_end > 0: + sale_times.append(int(to_end * 1000)) + + # Return the smallest time I guess? + sale_times_int = [t for t in sale_times if isinstance(t, int)] + return min(sale_times_int) if sale_times_int else None + + except Exception: + from ba import _error + _error.print_exception('error calcing sale time') + return None diff --git a/dist/ba_data/python/ba/_team.py b/dist/ba_data/python/ba/_team.py new file mode 100644 index 0000000..67e254b --- /dev/null +++ b/dist/ba_data/python/ba/_team.py @@ -0,0 +1,212 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Team related functionality.""" + +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING, TypeVar, Generic + +from ba._error import print_exception + +if TYPE_CHECKING: + from weakref import ReferenceType + from typing import Dict, List, Sequence, Tuple, Union, Optional + import ba + + +class SessionTeam: + """A team of one or more ba.SessionPlayers. + + Category: Gameplay Classes + + Note that a SessionPlayer *always* has a SessionTeam; + in some cases, such as free-for-all ba.Sessions, + each SessionTeam consists of just one SessionPlayer. + + Attributes: + + name + The team's name. + + id + The unique numeric id of the team. + + color + The team's color. + + players + The list of ba.SessionPlayers on the team. + + customdata + A dict for use by the current ba.Session for + storing data associated with this team. + Unlike customdata, this persists for the duration + of the session. + """ + + # Annotate our attr types at the class level so they're introspectable. + name: Union[ba.Lstr, str] + color: Tuple[float, ...] # FIXME: can't we make this fixed len? + players: List[ba.SessionPlayer] + customdata: dict + id: int + + def __init__(self, + team_id: int = 0, + name: Union[ba.Lstr, str] = '', + color: Sequence[float] = (1.0, 1.0, 1.0)): + """Instantiate a ba.SessionTeam. + + In most cases, all teams are provided to you by the ba.Session, + ba.Session, so calling this shouldn't be necessary. + """ + + self.id = team_id + self.name = name + self.color = tuple(color) + self.players = [] + self.customdata = {} + self.activityteam: Optional[Team] = None + + def leave(self) -> None: + """(internal)""" + self.customdata = {} + + +PlayerType = TypeVar('PlayerType', bound='ba.Player') + + +class Team(Generic[PlayerType]): + """A team in a specific ba.Activity. + + Category: Gameplay Classes + + These correspond to ba.SessionTeam objects, but are created per activity + so that the activity can use its own custom team subclass. + """ + + # Defining these types at the class level instead of in __init__ so + # that types are introspectable (these are still instance attrs). + players: List[PlayerType] + id: int + name: Union[ba.Lstr, str] + color: Tuple[float, ...] # FIXME: can't we make this fixed length? + _sessionteam: ReferenceType[SessionTeam] + _expired: bool + _postinited: bool + _customdata: dict + + # NOTE: avoiding having any __init__() here since it seems to not + # get called by default if a dataclass inherits from us. + + def postinit(self, sessionteam: SessionTeam) -> None: + """Wire up a newly created SessionTeam. + + (internal) + """ + + # Sanity check; if a dataclass is created that inherits from us, + # it will define an equality operator by default which will break + # internal game logic. So complain loudly if we find one. + if type(self).__eq__ is not object.__eq__: + raise RuntimeError( + f'Team class {type(self)} defines an equality' + f' operator (__eq__) which will break internal' + f' logic. Please remove it.\n' + f'For dataclasses you can do "dataclass(eq=False)"' + f' in the class decorator.') + + self.players = [] + self._sessionteam = weakref.ref(sessionteam) + self.id = sessionteam.id + self.name = sessionteam.name + self.color = sessionteam.color + self._customdata = {} + self._expired = False + self._postinited = True + + def manual_init(self, team_id: int, name: Union[ba.Lstr, str], + color: Tuple[float, ...]) -> None: + """Manually init a team for uses such as bots.""" + self.id = team_id + self.name = name + self.color = color + self._customdata = {} + self._expired = False + self._postinited = True + + @property + def customdata(self) -> dict: + """Arbitrary values associated with the team. + Though it is encouraged that most player values be properly defined + on the ba.Team subclass, it may be useful for player-agnostic + objects to store values here. This dict is cleared when the team + leaves or expires so objects stored here will be disposed of at + the expected time, unlike the Team instance itself which may + continue to be referenced after it is no longer part of the game. + """ + assert self._postinited + assert not self._expired + return self._customdata + + def leave(self) -> None: + """Called when the Team leaves a running game. + + (internal) + """ + assert self._postinited + assert not self._expired + del self._customdata + del self.players + + def expire(self) -> None: + """Called when the Team is expiring (due to the Activity expiring). + + (internal) + """ + assert self._postinited + assert not self._expired + self._expired = True + + try: + self.on_expire() + except Exception: + print_exception(f'Error in on_expire for {self}.') + + del self._customdata + del self.players + + def on_expire(self) -> None: + """Can be overridden to handle team expiration.""" + + @property + def sessionteam(self) -> SessionTeam: + """Return the ba.SessionTeam corresponding to this Team. + + Throws a ba.SessionTeamNotFoundError if there is none. + """ + assert self._postinited + if self._sessionteam is not None: + sessionteam = self._sessionteam() + if sessionteam is not None: + return sessionteam + from ba import _error + raise _error.SessionTeamNotFoundError() + + +class EmptyTeam(Team['ba.EmptyPlayer']): + """An empty player for use by Activities that don't need to define one. + + Category: Gameplay Classes + + ba.Player and ba.Team are 'Generic' types, and so passing those top level + classes as type arguments when defining a ba.Activity reduces type safety. + For example, activity.teams[0].player will have type 'Any' in that case. + For that reason, it is better to pass EmptyPlayer and EmptyTeam when + defining a ba.Activity that does not need custom types of its own. + + Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, + so if you want to define your own class for one of them you should do so + for both. + """ diff --git a/dist/ba_data/python/ba/_teamgame.py b/dist/ba_data/python/ba/_teamgame.py new file mode 100644 index 0000000..afa8519 --- /dev/null +++ b/dist/ba_data/python/ba/_teamgame.py @@ -0,0 +1,155 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to team games.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +import _ba +from ba._freeforallsession import FreeForAllSession +from ba._gameactivity import GameActivity +from ba._gameresults import GameResults +from ba._dualteamsession import DualTeamSession + +if TYPE_CHECKING: + from typing import Any, Dict, Type, Sequence + from bastd.actor.playerspaz import PlayerSpaz + import ba + +PlayerType = TypeVar('PlayerType', bound='ba.Player') +TeamType = TypeVar('TeamType', bound='ba.Team') + + +class TeamGameActivity(GameActivity[PlayerType, TeamType]): + """Base class for teams and free-for-all mode games. + + Category: Gameplay Classes + + (Free-for-all is essentially just a special case where every + ba.Player has their own ba.Team) + """ + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + """ + Class method override; + returns True for ba.DualTeamSessions and ba.FreeForAllSessions; + False otherwise. + """ + return (issubclass(sessiontype, DualTeamSession) + or issubclass(sessiontype, FreeForAllSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + # By default we don't show kill-points in free-for-all sessions. + # (there's usually some activity-specific score and we don't + # wanna confuse things) + if isinstance(self.session, FreeForAllSession): + self.show_kill_points = False + + def on_transition_in(self) -> None: + # pylint: disable=cyclic-import + from ba._coopsession import CoopSession + from bastd.actor.controlsguide import ControlsGuide + super().on_transition_in() + + # On the first game, show the controls UI momentarily. + # (unless we're being run in co-op mode, in which case we leave + # it up to them) + if not isinstance(self.session, CoopSession): + attrname = '_have_shown_ctrl_help_overlay' + if not getattr(self.session, attrname, False): + delay = 4.0 + lifespan = 10.0 + if self.slow_motion: + lifespan *= 0.3 + ControlsGuide(delay=delay, + lifespan=lifespan, + scale=0.8, + position=(380, 200), + bright=True).autoretain() + setattr(self.session, attrname, True) + + def on_begin(self) -> None: + super().on_begin() + try: + # Award a few achievements. + if isinstance(self.session, FreeForAllSession): + if len(self.players) >= 2: + _ba.app.ach.award_local_achievement('Free Loader') + elif isinstance(self.session, DualTeamSession): + if len(self.players) >= 4: + from ba import _achievement + _ba.app.ach.award_local_achievement('Team Player') + except Exception: + from ba import _error + _error.print_exception() + + def spawn_player_spaz(self, + player: PlayerType, + position: Sequence[float] = None, + angle: float = None) -> PlayerSpaz: + """ + Method override; spawns and wires up a standard ba.PlayerSpaz for + a ba.Player. + + If position or angle is not supplied, a default will be chosen based + on the ba.Player and their ba.Team. + """ + if position is None: + # In teams-mode get our team-start-location. + if isinstance(self.session, DualTeamSession): + position = (self.map.get_start_position(player.team.id)) + else: + # Otherwise do free-for-all spawn locations. + position = self.map.get_ffa_start_position(self.players) + + return super().spawn_player_spaz(player, position, angle) + + # FIXME: need to unify these arguments with GameActivity.end() + def end( # type: ignore + self, + results: Any = None, + announce_winning_team: bool = True, + announce_delay: float = 0.1, + force: bool = False) -> None: + """ + End the game and announce the single winning team + unless 'announce_winning_team' is False. + (for results without a single most-important winner). + """ + # pylint: disable=arguments-differ + from ba._coopsession import CoopSession + from ba._multiteamsession import MultiTeamSession + from ba._general import Call + + # Announce win (but only for the first finish() call) + # (also don't announce in co-op sessions; we leave that up to them). + session = self.session + if not isinstance(session, CoopSession): + do_announce = not self.has_ended() + super().end(results, delay=2.0 + announce_delay, force=force) + + # Need to do this *after* end end call so that results is valid. + assert isinstance(results, GameResults) + if do_announce and isinstance(session, MultiTeamSession): + session.announce_game_results( + self, + results, + delay=announce_delay, + announce_winning_team=announce_winning_team) + + # For co-op we just pass this up the chain with a delay added + # (in most cases). Team games expect a delay for the announce + # portion in teams/ffa mode so this keeps it consistent. + else: + # don't want delay on restarts.. + if (isinstance(results, dict) and 'outcome' in results + and results['outcome'] == 'restart'): + delay = 0.0 + else: + delay = 2.0 + _ba.timer(0.1, Call(_ba.playsound, _ba.getsound('boxingBell'))) + super().end(results, delay=delay, force=force) diff --git a/dist/ba_data/python/ba/_tips.py b/dist/ba_data/python/ba/_tips.py new file mode 100644 index 0000000..a6c04c7 --- /dev/null +++ b/dist/ba_data/python/ba/_tips.py @@ -0,0 +1,98 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to game tips. + +These can be shown at opportune times such as between rounds.""" + +import random +from typing import List + +import _ba + + +def get_next_tip() -> str: + """Returns the next tip to be displayed.""" + app = _ba.app + if not app.tips: + for tip in get_all_tips(): + app.tips.insert(random.randint(0, len(app.tips)), tip) + tip = app.tips.pop() + return tip + + +def get_all_tips() -> List[str]: + """Return the complete list of tips.""" + tips = [ + ('If you are short on controllers, install the \'${REMOTE_APP_NAME}\' ' + 'app\non your mobile devices to use them as controllers.'), + ('Create player profiles for yourself and your friends with\nyour ' + 'preferred names and appearances instead of using random ones.'), + ('You can \'aim\' your punches by spinning left or right.\nThis is ' + 'useful for knocking bad guys off edges or scoring in hockey.'), + ('If you pick up a curse, your only hope for survival is to\nfind a ' + 'health powerup in the next few seconds.'), + ('A perfectly timed running-jumping-spin-punch can kill in a single ' + 'hit\nand earn you lifelong respect from your friends.'), + 'Always remember to floss.', + 'Don\'t run all the time. Really. You will fall off cliffs.', + ('In Capture-the-Flag, your own flag must be at your base to score, ' + 'If the other\nteam is about to score, stealing their flag can be ' + 'a good way to stop them.'), + ('If you get a sticky-bomb stuck to you, jump around and spin in ' + 'circles. You might\nshake the bomb off, or if nothing else your ' + 'last moments will be entertaining.'), + ('You take damage when you whack your head on things,\nso try to not ' + 'whack your head on things.'), + 'If you kill an enemy in one hit you get double points for it.', + ('Despite their looks, all characters\' abilities are identical,\nso ' + 'just pick whichever one you most closely resemble.'), + 'You can throw bombs higher if you jump just before throwing.', + ('Throw strength is based on the direction you are holding.\nTo toss ' + 'something gently in front of you, don\'t hold any direction.'), + ('If someone picks you up, punch them and they\'ll let go.\nThis ' + 'works in real life too.'), + ('Don\'t get too cocky with that energy shield; you can still get ' + 'yourself thrown off a cliff.'), + ('Many things can be picked up and thrown, including other players. ' + 'Tossing\nyour enemies off cliffs can be an effective and ' + 'emotionally fulfilling strategy.'), + ('Ice bombs are not very powerful, but they freeze\nwhoever they ' + 'hit, leaving them vulnerable to shattering.'), + 'Don\'t spin for too long; you\'ll become dizzy and fall.', + ('Run back and forth before throwing a bomb\nto \'whiplash\' it ' + 'and throw it farther.'), + ('Punches do more damage the faster your fists are moving,\nso ' + 'try running, jumping, and spinning like crazy.'), + 'In hockey, you\'ll maintain more speed if you turn gradually.', + ('The head is the most vulnerable area, so a sticky-bomb\nto the ' + 'noggin usually means game-over.'), + ('Hold down any button to run. You\'ll get places faster\nbut ' + 'won\'t turn very well, so watch out for cliffs.'), + ('You can judge when a bomb is going to explode based on the\n' + 'color of sparks from its fuse: yellow..orange..red..BOOM.'), + ] + tips += [ + 'If your framerate is choppy, try turning down resolution\nor ' + 'visuals in the game\'s graphics settings.' + ] + app = _ba.app + if app.platform in ('android', 'ios') and not app.on_tv: + tips += [ + ('If your device gets too warm or you\'d like to conserve ' + 'battery power,\nturn down "Visuals" or "Resolution" ' + 'in Settings->Graphics'), + ] + if app.platform in ['mac', 'android']: + tips += [ + 'Tired of the soundtrack? Replace it with your own!' + '\nSee Settings->Audio->Soundtrack' + ] + + # Hot-plugging is currently only on some platforms. + # FIXME: Should add a platform entry for this so don't forget to update it. + if app.platform in ['mac', 'android', 'windows']: + tips += [ + 'Players can join and leave in the middle of most games,\n' + 'and you can also plug and unplug controllers on the fly.', + ] + return tips diff --git a/dist/ba_data/python/ba/_tournament.py b/dist/ba_data/python/ba/_tournament.py new file mode 100644 index 0000000..bb1cc1e --- /dev/null +++ b/dist/ba_data/python/ba/_tournament.py @@ -0,0 +1,47 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to tournament play.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba + +if TYPE_CHECKING: + from typing import Dict, List, Any + + +def get_tournament_prize_strings(entry: Dict[str, Any]) -> List[str]: + """Given a tournament entry, return strings for its prize levels.""" + # pylint: disable=too-many-locals + from ba._enums import SpecialChar + from ba._gameutils import get_trophy_string + range1 = entry.get('prizeRange1') + range2 = entry.get('prizeRange2') + range3 = entry.get('prizeRange3') + prize1 = entry.get('prize1') + prize2 = entry.get('prize2') + prize3 = entry.get('prize3') + trophy_type_1 = entry.get('prizeTrophy1') + trophy_type_2 = entry.get('prizeTrophy2') + trophy_type_3 = entry.get('prizeTrophy3') + out_vals = [] + for rng, prize, trophy_type in ((range1, prize1, trophy_type_1), + (range2, prize2, trophy_type_2), + (range3, prize3, trophy_type_3)): + prval = ('' if rng is None else ('#' + str(rng[0])) if + (rng[0] == rng[1]) else + ('#' + str(rng[0]) + '-' + str(rng[1]))) + pvval = '' + if trophy_type is not None: + pvval += get_trophy_string(trophy_type) + + # If we've got trophies but not for this entry, throw some space + # in to compensate so the ticket counts line up. + if prize is not None: + pvval = _ba.charstr( + SpecialChar.TICKET_BACKING) + str(prize) + pvval + out_vals.append(prval) + out_vals.append(pvval) + return out_vals diff --git a/dist/ba_data/python/ba/_ui.py b/dist/ba_data/python/ba/_ui.py new file mode 100644 index 0000000..c5af590 --- /dev/null +++ b/dist/ba_data/python/ba/_ui.py @@ -0,0 +1,167 @@ +# Released under the MIT License. See LICENSE for details. +# +"""User interface related functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +from ba._enums import UIScale + +if TYPE_CHECKING: + from typing import Optional, Dict, Any, Callable, List, Type + from ba.ui import UICleanupCheck + import ba + + +class UISubsystem: + """Consolidated UI functionality for the app. + + Category: App Classes + + To use this class, access the single instance of it at 'ba.app.ui'. + """ + + def __init__(self) -> None: + env = _ba.env() + + self.controller: Optional[ba.UIController] = None + + self._main_menu_window: Optional[ba.Widget] = None + self._main_menu_location: Optional[str] = None + + self._uiscale: ba.UIScale + + interfacetype = env['ui_scale'] + if interfacetype == 'large': + self._uiscale = UIScale.LARGE + elif interfacetype == 'medium': + self._uiscale = UIScale.MEDIUM + elif interfacetype == 'small': + self._uiscale = UIScale.SMALL + else: + raise RuntimeError(f'Invalid UIScale value: {interfacetype}') + + self.window_states: Dict[Type, Any] = {} # FIXME: Kill this. + self.main_menu_selection: Optional[str] = None # FIXME: Kill this. + self.have_party_queue_window = False + self.quit_window: Any = None + self.dismiss_wii_remotes_window_call: (Optional[Callable[[], + Any]]) = None + self.cleanupchecks: List[UICleanupCheck] = [] + self.upkeeptimer: Optional[ba.Timer] = None + self.use_toolbars = env.get('toolbar_test', True) + self.party_window: Any = None # FIXME: Don't use Any. + self.title_color = (0.72, 0.7, 0.75) + self.heading_color = (0.72, 0.7, 0.75) + self.infotextcolor = (0.7, 0.9, 0.7) + + # Switch our overall game selection UI flow between Play and + # Private-party playlist selection modes; should do this in + # a more elegant way once we revamp high level UI stuff a bit. + self.selecting_private_party_playlist: bool = False + + @property + def uiscale(self) -> ba.UIScale: + """Current ui scale for the app.""" + return self._uiscale + + def on_app_launch(self) -> None: + """Should be run on app launch.""" + from ba.ui import UIController, ui_upkeep + from ba._enums import TimeType + + # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, + # medium, and large UI modes. (doesn't run off screen, etc). + # The overrides below can be used to test with different sizes. + # Generally small is used on phones, medium is used on tablets/tvs, + # and large is on desktop computers or perhaps large tablets. When + # possible, run in windowed mode and resize the window to assure + # this holds true at all aspect ratios. + + # UPDATE: A better way to test this is now by setting the environment + # variable BA_UI_SCALE to "small", "medium", or "large". + # This will affect system UIs not covered by the values below such + # as screen-messages. The below values remain functional, however, + # for cases such as Android where environment variables can't be set + # easily. + + if bool(False): # force-test ui scale + self._uiscale = UIScale.SMALL + with _ba.Context('ui'): + _ba.pushcall(lambda: _ba.screenmessage( + f'FORCING UISCALE {self._uiscale.name} FOR TESTING', + color=(1, 0, 1), + log=True)) + + self.controller = UIController() + + # Kick off our periodic UI upkeep. + # FIXME: Can probably kill this if we do immediate UI death checks. + self.upkeeptimer = _ba.Timer(2.6543, + ui_upkeep, + timetype=TimeType.REAL, + repeat=True) + + def set_main_menu_window(self, window: ba.Widget) -> None: + """Set the current 'main' window, replacing any existing.""" + existing = self._main_menu_window + from ba._enums import TimeType + from inspect import currentframe, getframeinfo + + # Let's grab the location where we were called from to report + # if we have to force-kill the existing window (which normally + # should not happen). + frameline = None + try: + frame = currentframe() + if frame is not None: + frame = frame.f_back + if frame is not None: + frameinfo = getframeinfo(frame) + frameline = f'{frameinfo.filename} {frameinfo.lineno}' + except Exception: + from ba._error import print_exception + print_exception('Error calcing line for set_main_menu_window') + + # With our legacy main-menu system, the caller is responsible for + # clearing out the old main menu window when assigning the new. + # However there are corner cases where that doesn't happen and we get + # old windows stuck under the new main one. So let's guard against + # that. However, we can't simply delete the existing main window when + # a new one is assigned because the user may transition the old out + # *after* the assignment. Sigh. So, as a happy medium, let's check in + # on the old after a short bit of time and kill it if its still alive. + # That will be a bit ugly on screen but at least should un-break + # things. + def _delay_kill() -> None: + import time + if existing: + print(f'Killing old main_menu_window' + f' when called at: {frameline} t={time.time():.3f}') + existing.delete() + + _ba.timer(1.0, _delay_kill, timetype=TimeType.REAL) + self._main_menu_window = window + + def clear_main_menu_window(self, transition: str = None) -> None: + """Clear any existing 'main' window with the provided transition.""" + if self._main_menu_window: + if transition is not None: + _ba.containerwidget(edit=self._main_menu_window, + transition=transition) + else: + self._main_menu_window.delete() + + def has_main_menu_window(self) -> bool: + """Return whether a main menu window is present.""" + return bool(self._main_menu_window) + + def set_main_menu_location(self, location: str) -> None: + """Set the location represented by the current main menu window.""" + self._main_menu_location = location + + def get_main_menu_location(self) -> Optional[str]: + """Return the current named main menu location, if any.""" + return self._main_menu_location diff --git a/dist/ba_data/python/ba/deprecated.py b/dist/ba_data/python/ba/deprecated.py new file mode 100644 index 0000000..de3ff2a --- /dev/null +++ b/dist/ba_data/python/ba/deprecated.py @@ -0,0 +1,8 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Deprecated functionality. + +Classes or functions can be relocated here when they are deprecated. +Any code using them should migrate to alternative methods, as +deprecated items will eventually be fully removed. +""" diff --git a/dist/ba_data/python/ba/internal.py b/dist/ba_data/python/ba/internal.py new file mode 100644 index 0000000..1d852ee --- /dev/null +++ b/dist/ba_data/python/ba/internal.py @@ -0,0 +1,41 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Exposed functionality not intended for full public use. + +Classes and functions contained here, while technically 'public', may change +or disappear without warning, so should be avoided (or used sparingly and +defensively) in mods. +""" + +# pylint: disable=unused-import + +from ba._map import (get_unowned_maps, get_map_class, register_map, + preload_map_preview_media, get_map_display_string, + get_filtered_map_name) +from ba._appconfig import commit_app_config +from ba._input import (get_device_value, get_input_map_hash, + get_input_device_config) +from ba._general import getclass, json_prep, get_type_name +from ba._activitytypes import JoinActivity, ScoreScreenActivity +from ba._apputils import (is_browser_likely_available, get_remote_app_name, + should_submit_debug_info) +from ba._benchmark import (run_gpu_benchmark, run_cpu_benchmark, + run_media_reload_benchmark, run_stress_test) +from ba._campaign import getcampaign +from ba._messages import PlayerProfilesChangedMessage +from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES +from ba._music import do_play_music +from ba._netutils import (master_server_get, master_server_post, + get_ip_address_type) +from ba._powerup import get_default_powerup_distribution +from ba._profile import (get_player_profile_colors, get_player_profile_icon, + get_player_colors) +from ba._tips import get_next_tip +from ba._playlist import (get_default_free_for_all_playlist, + get_default_teams_playlist, filter_playlist) +from ba._store import (get_available_sale_time, get_available_purchase_count, + get_store_item_name_translated, + get_store_item_display_size, get_store_layout, + get_store_item, get_clean_price) +from ba._tournament import get_tournament_prize_strings +from ba._gameutils import get_trophy_string diff --git a/dist/ba_data/python/ba/macmusicapp.py b/dist/ba_data/python/ba/macmusicapp.py new file mode 100644 index 0000000..c8a64d7 --- /dev/null +++ b/dist/ba_data/python/ba/macmusicapp.py @@ -0,0 +1,232 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Music playback functionality using the Mac Music (formerly iTunes) app.""" +from __future__ import annotations + +import threading +from typing import TYPE_CHECKING + +import _ba +from ba._music import MusicPlayer + +if TYPE_CHECKING: + from typing import List, Optional, Callable, Any + + +class MacMusicAppMusicPlayer(MusicPlayer): + """A music-player that utilizes the macOS Music.app for playback. + + Allows selecting playlists as entries. + """ + + def __init__(self) -> None: + super().__init__() + self._thread = _MacMusicAppThread() + self._thread.start() + + def on_select_entry(self, callback: Callable[[Any], None], + current_entry: Any, selection_target_name: str) -> Any: + # pylint: disable=cyclic-import + from bastd.ui.soundtrack import entrytypeselect as etsel + return etsel.SoundtrackEntryTypeSelectWindow(callback, current_entry, + selection_target_name) + + def on_set_volume(self, volume: float) -> None: + self._thread.set_volume(volume) + + def get_playlists(self, callback: Callable) -> None: + """Asynchronously fetch the list of available iTunes playlists.""" + self._thread.get_playlists(callback) + + def on_play(self, entry: Any) -> None: + music = _ba.app.music + entry_type = music.get_soundtrack_entry_type(entry) + if entry_type == 'iTunesPlaylist': + self._thread.play_playlist(music.get_soundtrack_entry_name(entry)) + else: + print('MacMusicAppMusicPlayer passed unrecognized entry type:', + entry_type) + + def on_stop(self) -> None: + self._thread.play_playlist(None) + + def on_app_shutdown(self) -> None: + self._thread.shutdown() + + +class _MacMusicAppThread(threading.Thread): + """Thread which wrangles Music.app playback""" + + def __init__(self) -> None: + super().__init__() + self._commands_available = threading.Event() + self._commands: List[List] = [] + self._volume = 1.0 + self._current_playlist: Optional[str] = None + self._orig_volume: Optional[int] = None + + def run(self) -> None: + """Run the Music.app thread.""" + from ba._general import Call + from ba._language import Lstr + from ba._enums import TimeType + _ba.set_thread_name('BA_MacMusicAppThread') + _ba.mac_music_app_init() + + # Let's mention to the user we're launching Music.app in case + # it causes any funny business (this used to background the app + # sometimes, though I think that is fixed now) + def do_print() -> None: + _ba.timer(1.0, + Call(_ba.screenmessage, Lstr(resource='usingItunesText'), + (0, 1, 0)), + timetype=TimeType.REAL) + + _ba.pushcall(do_print, from_other_thread=True) + + # Here we grab this to force the actual launch. + _ba.mac_music_app_get_volume() + _ba.mac_music_app_get_library_source() + done = False + while not done: + self._commands_available.wait() + self._commands_available.clear() + + # We're not protecting this list with a mutex but we're + # just using it as a simple queue so it should be fine. + while self._commands: + cmd = self._commands.pop(0) + if cmd[0] == 'DIE': + self._handle_die_command() + done = True + break + if cmd[0] == 'PLAY': + self._handle_play_command(target=cmd[1]) + elif cmd[0] == 'GET_PLAYLISTS': + self._handle_get_playlists_command(target=cmd[1]) + + del cmd # Allows the command data/callback/etc to be freed. + + def set_volume(self, volume: float) -> None: + """Set volume to a value between 0 and 1.""" + old_volume = self._volume + self._volume = volume + + # If we've got nothing we're supposed to be playing, + # don't touch itunes/music. + if self._current_playlist is None: + return + + # If volume is going to zero, stop actually playing + # but don't clear playlist. + if old_volume > 0.0 and volume == 0.0: + try: + assert self._orig_volume is not None + _ba.mac_music_app_stop() + _ba.mac_music_app_set_volume(self._orig_volume) + except Exception as exc: + print('Error stopping iTunes music:', exc) + elif self._volume > 0: + + # If volume was zero, store pre-playing volume and start + # playing. + if old_volume == 0.0: + self._orig_volume = _ba.mac_music_app_get_volume() + self._update_mac_music_app_volume() + if old_volume == 0.0: + self._play_current_playlist() + + def play_playlist(self, musictype: Optional[str]) -> None: + """Play the given playlist.""" + self._commands.append(['PLAY', musictype]) + self._commands_available.set() + + def shutdown(self) -> None: + """Request that the player shuts down.""" + self._commands.append(['DIE']) + self._commands_available.set() + self.join() + + def get_playlists(self, callback: Callable[[Any], None]) -> None: + """Request the list of playlists.""" + self._commands.append(['GET_PLAYLISTS', callback]) + self._commands_available.set() + + def _handle_get_playlists_command( + self, target: Callable[[List[str]], None]) -> None: + from ba._general import Call + try: + playlists = _ba.mac_music_app_get_playlists() + playlists = [ + p for p in playlists if p not in [ + 'Music', 'Movies', 'TV Shows', 'Podcasts', 'iTunes\xa0U', + 'Books', 'Genius', 'iTunes DJ', 'Music Videos', + 'Home Videos', 'Voice Memos', 'Audiobooks' + ] + ] + playlists.sort(key=lambda x: x.lower()) + except Exception as exc: + print('Error getting iTunes playlists:', exc) + playlists = [] + _ba.pushcall(Call(target, playlists), from_other_thread=True) + + def _handle_play_command(self, target: Optional[str]) -> None: + if target is None: + if self._current_playlist is not None and self._volume > 0: + try: + assert self._orig_volume is not None + _ba.mac_music_app_stop() + _ba.mac_music_app_set_volume(self._orig_volume) + except Exception as exc: + print('Error stopping iTunes music:', exc) + self._current_playlist = None + else: + # If we've got something playing with positive + # volume, stop it. + if self._current_playlist is not None and self._volume > 0: + try: + assert self._orig_volume is not None + _ba.mac_music_app_stop() + _ba.mac_music_app_set_volume(self._orig_volume) + except Exception as exc: + print('Error stopping iTunes music:', exc) + + # Set our playlist and play it if our volume is up. + self._current_playlist = target + if self._volume > 0: + self._orig_volume = (_ba.mac_music_app_get_volume()) + self._update_mac_music_app_volume() + self._play_current_playlist() + + def _handle_die_command(self) -> None: + + # Only stop if we've actually played something + # (we don't want to kill music the user has playing). + if self._current_playlist is not None and self._volume > 0: + try: + assert self._orig_volume is not None + _ba.mac_music_app_stop() + _ba.mac_music_app_set_volume(self._orig_volume) + except Exception as exc: + print('Error stopping iTunes music:', exc) + + def _play_current_playlist(self) -> None: + try: + from ba._general import Call + assert self._current_playlist is not None + if _ba.mac_music_app_play_playlist(self._current_playlist): + pass + else: + _ba.pushcall(Call( + _ba.screenmessage, + _ba.app.lang.get_resource('playlistNotFoundText') + + ': \'' + self._current_playlist + '\'', (1, 0, 0)), + from_other_thread=True) + except Exception: + from ba import _error + _error.print_exception( + f'error playing playlist {self._current_playlist}') + + def _update_mac_music_app_volume(self) -> None: + _ba.mac_music_app_set_volume( + max(0, min(100, int(100.0 * self._volume)))) diff --git a/dist/ba_data/python/ba/modutils.py b/dist/ba_data/python/ba/modutils.py new file mode 100644 index 0000000..904711f --- /dev/null +++ b/dist/ba_data/python/ba/modutils.py @@ -0,0 +1,151 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to modding.""" +from __future__ import annotations + +from typing import TYPE_CHECKING +import os + +import _ba + +if TYPE_CHECKING: + from typing import Optional, List, Sequence + + +def get_human_readable_user_scripts_path() -> str: + """Return a human readable location of user-scripts. + + This is NOT a valid filesystem path; may be something like "(SD Card)". + """ + from ba import _language + app = _ba.app + path: Optional[str] = app.python_directory_user + if path is None: + return '' + + # On newer versions of android, the user's external storage dir is probably + # only visible to the user's processes and thus not really useful printed + # in its entirety; lets print it as /myfilepath. + if app.platform == 'android': + ext_storage_path: Optional[str] = ( + _ba.android_get_external_storage_path()) + if (ext_storage_path is not None + and app.python_directory_user.startswith(ext_storage_path)): + path = ('<' + + _language.Lstr(resource='externalStorageText').evaluate() + + '>' + app.python_directory_user[len(ext_storage_path):]) + return path + + +def _request_storage_permission() -> bool: + """If needed, requests storage permission from the user (& return true).""" + from ba._language import Lstr + from ba._enums import Permission + if not _ba.have_permission(Permission.STORAGE): + _ba.playsound(_ba.getsound('error')) + _ba.screenmessage(Lstr(resource='storagePermissionAccessText'), + color=(1, 0, 0)) + _ba.timer(1.0, lambda: _ba.request_permission(Permission.STORAGE)) + return True + return False + + +def show_user_scripts() -> None: + """Open or nicely print the location of the user-scripts directory.""" + app = _ba.app + + # First off, if we need permission for this, ask for it. + if _request_storage_permission(): + return + + # Secondly, if the dir doesn't exist, attempt to make it. + if not os.path.exists(app.python_directory_user): + os.makedirs(app.python_directory_user) + + # On android, attempt to write a file in their user-scripts dir telling + # them about modding. This also has the side-effect of allowing us to + # media-scan that dir so it shows up in android-file-transfer, since it + # doesn't seem like there's a way to inform the media scanner of an empty + # directory, which means they would have to reboot their device before + # they can see it. + if app.platform == 'android': + try: + usd: Optional[str] = app.python_directory_user + if usd is not None and os.path.isdir(usd): + file_name = usd + '/about_this_folder.txt' + with open(file_name, 'w') as outfile: + outfile.write('You can drop files in here to mod the game.' + ' See settings/advanced' + ' in the game for more info.') + _ba.android_media_scan_file(file_name) + except Exception: + from ba import _error + _error.print_exception('error writing about_this_folder stuff') + + # On a few platforms we try to open the dir in the UI. + if app.platform in ['mac', 'windows']: + _ba.open_dir_externally(app.python_directory_user) + + # Otherwise we just print a pretty version of it. + else: + _ba.screenmessage(get_human_readable_user_scripts_path()) + + +def create_user_system_scripts() -> None: + """Set up a copy of Ballistica system scripts under your user scripts dir. + + (for editing and experiment with) + """ + import shutil + app = _ba.app + + # First off, if we need permission for this, ask for it. + if _request_storage_permission(): + return + + path = (app.python_directory_user + '/sys/' + app.version) + pathtmp = path + '_tmp' + if os.path.exists(path): + shutil.rmtree(path) + if os.path.exists(pathtmp): + shutil.rmtree(pathtmp) + + def _ignore_filter(src: str, names: Sequence[str]) -> Sequence[str]: + del src, names # Unused + + # We simply skip all __pycache__ directories. (the user would have + # to blow them away anyway to make changes; + # See https://github.com/efroemling/ballistica/wiki + # /Knowledge-Nuggets#python-cache-files-gotcha + return ('__pycache__', ) + + print(f'COPYING "{app.python_directory_app}" -> "{pathtmp}".') + shutil.copytree(app.python_directory_app, pathtmp, ignore=_ignore_filter) + + print(f'MOVING "{pathtmp}" -> "{path}".') + shutil.move(pathtmp, path) + print(f"Created system scripts at :'{path}" + f"'\nRestart {_ba.appname()} to use them." + f' (use ba.quit() to exit the game)') + if app.platform == 'android': + print('Note: the new files may not be visible via ' + 'android-file-transfer until you restart your device.') + + +def delete_user_system_scripts() -> None: + """Clean out the scripts created by create_user_system_scripts().""" + import shutil + app = _ba.app + path = (app.python_directory_user + '/sys/' + app.version) + if os.path.exists(path): + shutil.rmtree(path) + print(f'User system scripts deleted.\n' + f'Restart {_ba.appname()} to use internal' + f' scripts. (use ba.quit() to exit the game)') + else: + print('User system scripts not found.') + + # If the sys path is empty, kill it. + dpath = app.python_directory_user + '/sys' + if os.path.isdir(dpath) and not os.listdir(dpath): + os.rmdir(dpath) diff --git a/dist/ba_data/python/ba/osmusic.py b/dist/ba_data/python/ba/osmusic.py new file mode 100644 index 0000000..5257e0a --- /dev/null +++ b/dist/ba_data/python/ba/osmusic.py @@ -0,0 +1,135 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Music playback using OS functionality exposed through the C++ layer.""" +from __future__ import annotations + +import os +import random +import threading +from typing import TYPE_CHECKING + +import _ba +from ba._music import MusicPlayer + +if TYPE_CHECKING: + from typing import Callable, Any, Union, List, Optional + + +class OSMusicPlayer(MusicPlayer): + """Music player that talks to internal C++ layer for functionality. + + (internal)""" + + def __init__(self) -> None: + super().__init__() + self._want_to_play = False + self._actually_playing = False + + @classmethod + def get_valid_music_file_extensions(cls) -> List[str]: + """Return file extensions for types playable on this device.""" + # FIXME: should ask the C++ layer for these; just hard-coding for now. + return ['mp3', 'ogg', 'm4a', 'wav', 'flac', 'mid'] + + def on_select_entry(self, callback: Callable[[Any], None], + current_entry: Any, selection_target_name: str) -> Any: + # pylint: disable=cyclic-import + from bastd.ui.soundtrack.entrytypeselect import ( + SoundtrackEntryTypeSelectWindow) + return SoundtrackEntryTypeSelectWindow(callback, current_entry, + selection_target_name) + + def on_set_volume(self, volume: float) -> None: + _ba.music_player_set_volume(volume) + + def on_play(self, entry: Any) -> None: + music = _ba.app.music + entry_type = music.get_soundtrack_entry_type(entry) + name = music.get_soundtrack_entry_name(entry) + assert name is not None + if entry_type == 'musicFile': + self._want_to_play = self._actually_playing = True + _ba.music_player_play(name) + elif entry_type == 'musicFolder': + + # Launch a thread to scan this folder and give us a random + # valid file within it. + self._want_to_play = True + self._actually_playing = False + _PickFolderSongThread(name, self.get_valid_music_file_extensions(), + self._on_play_folder_cb).start() + + def _on_play_folder_cb(self, + result: Union[str, List[str]], + error: Optional[str] = None) -> None: + from ba import _language + if error is not None: + rstr = (_language.Lstr( + resource='internal.errorPlayingMusicText').evaluate()) + if isinstance(result, str): + err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) + + '; ' + str(error)) + else: + err_str = (rstr.replace('${MUSIC}', '') + '; ' + + str(error)) + _ba.screenmessage(err_str, color=(1, 0, 0)) + return + + # There's a chance a stop could have been issued before our thread + # returned. If that's the case, don't play. + if not self._want_to_play: + print('_on_play_folder_cb called with _want_to_play False') + else: + self._actually_playing = True + _ba.music_player_play(result) + + def on_stop(self) -> None: + self._want_to_play = False + self._actually_playing = False + _ba.music_player_stop() + + def on_app_shutdown(self) -> None: + _ba.music_player_shutdown() + + +class _PickFolderSongThread(threading.Thread): + + def __init__(self, path: str, valid_extensions: List[str], + callback: Callable[[Union[str, List[str]], Optional[str]], + None]): + super().__init__() + self._valid_extensions = valid_extensions + self._callback = callback + self._path = path + + def run(self) -> None: + from ba import _language + from ba._general import Call + do_print_error = True + try: + _ba.set_thread_name('BA_PickFolderSongThread') + all_files: List[str] = [] + valid_extensions = ['.' + x for x in self._valid_extensions] + for root, _subdirs, filenames in os.walk(self._path): + for fname in filenames: + if any(fname.lower().endswith(ext) + for ext in valid_extensions): + all_files.insert(random.randrange(len(all_files) + 1), + root + '/' + fname) + if not all_files: + do_print_error = False + raise RuntimeError( + _language.Lstr(resource='internal.noMusicFilesInFolderText' + ).evaluate()) + _ba.pushcall(Call(self._callback, all_files, None), + from_other_thread=True) + except Exception as exc: + from ba import _error + if do_print_error: + _error.print_exception() + try: + err_str = str(exc) + except Exception: + err_str = '' + _ba.pushcall(Call(self._callback, self._path, err_str), + from_other_thread=True) diff --git a/dist/ba_data/python/ba/ui/__init__.py b/dist/ba_data/python/ba/ui/__init__.py new file mode 100644 index 0000000..e65453f --- /dev/null +++ b/dist/ba_data/python/ba/ui/__init__.py @@ -0,0 +1,229 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provide top level UI related functionality.""" + +from __future__ import annotations + +import os +import weakref +from dataclasses import dataclass +from typing import TYPE_CHECKING, cast, Type + +import _ba +from ba._enums import TimeType +from ba._general import print_active_refs + +if TYPE_CHECKING: + from typing import Optional, List, Any + from weakref import ReferenceType + + import ba + +# Set environment variable BA_DEBUG_UI_CLEANUP_CHECKS to 1 +# to print detailed info about what is getting cleaned up when. +DEBUG_UI_CLEANUP_CHECKS = os.environ.get('BA_DEBUG_UI_CLEANUP_CHECKS') == '1' + + +class Window: + """A basic window. + + Category: User Interface Classes + """ + + def __init__(self, root_widget: ba.Widget, cleanupcheck: bool = True): + self._root_widget = root_widget + + # Complain if we outlive our root widget. + if cleanupcheck: + uicleanupcheck(self, root_widget) + + def get_root_widget(self) -> ba.Widget: + """Return the root widget.""" + return self._root_widget + + +@dataclass +class UICleanupCheck: + """Holds info about a uicleanupcheck target.""" + obj: ReferenceType + widget: ba.Widget + widget_death_time: Optional[float] + + +class UILocation: + """Defines a specific 'place' in the UI the user can navigate to. + + Category: User Interface Classes + """ + + def __init__(self) -> None: + pass + + def save_state(self) -> None: + """Serialize this instance's state to a dict.""" + + def restore_state(self) -> None: + """Restore this instance's state from a dict.""" + + def push_location(self, location: str) -> None: + """Push a new location to the stack and transition to it.""" + + +class UILocationWindow(UILocation): + """A UILocation consisting of a single root window widget. + + Category: User Interface Classes + """ + + def __init__(self) -> None: + super().__init__() + self._root_widget: Optional[ba.Widget] = None + + def get_root_widget(self) -> ba.Widget: + """Return the root widget for this window.""" + assert self._root_widget is not None + return self._root_widget + + +class UIEntry: + """State for a UILocation on the stack.""" + + def __init__(self, name: str, controller: UIController): + self._name = name + self._state = None + self._args = None + self._instance: Optional[UILocation] = None + self._controller = weakref.ref(controller) + + def create(self) -> None: + """Create an instance of our UI.""" + cls = self._get_class() + self._instance = cls() + + def destroy(self) -> None: + """Transition out our UI if it exists.""" + if self._instance is None: + return + print('WOULD TRANSITION OUT', self._name) + + def _get_class(self) -> Type[UILocation]: + """Returns the UI class our name points to.""" + # pylint: disable=cyclic-import + + # TEMP HARD CODED - WILL REPLACE THIS WITH BA_META LOOKUPS. + if self._name == 'mainmenu': + from bastd.ui import mainmenu + return cast(Type[UILocation], mainmenu.MainMenuWindow) + raise ValueError('unknown ui class ' + str(self._name)) + + +class UIController: + """Wrangles ba.UILocations. + + Category: User Interface Classes + """ + + def __init__(self) -> None: + + # FIXME: document why we have separate stacks for game and menu... + self._main_stack_game: List[UIEntry] = [] + self._main_stack_menu: List[UIEntry] = [] + + # This points at either the game or menu stack. + self._main_stack: Optional[List[UIEntry]] = None + + # There's only one of these since we don't need to preserve its state + # between sessions. + self._dialog_stack: List[UIEntry] = [] + + def show_main_menu(self, in_game: bool = True) -> None: + """Show the main menu, clearing other UIs from location stacks.""" + self._main_stack = [] + self._dialog_stack = [] + self._main_stack = (self._main_stack_game + if in_game else self._main_stack_menu) + self._main_stack.append(UIEntry('mainmenu', self)) + self._update_ui() + + def _update_ui(self) -> None: + """Instantiate the topmost ui in our stacks.""" + + # First tell any existing UIs to get outta here. + for stack in (self._dialog_stack, self._main_stack): + assert stack is not None + for entry in stack: + entry.destroy() + + # Now create the topmost one if there is one. + entrynew = (self._dialog_stack[-1] if self._dialog_stack else + self._main_stack[-1] if self._main_stack else None) + if entrynew is not None: + entrynew.create() + + +def uicleanupcheck(obj: Any, widget: ba.Widget) -> None: + """Add a check to ensure a widget-owning object gets cleaned up properly. + + Category: User Interface Functions + + This adds a check which will print an error message if the provided + object still exists ~5 seconds after the provided ba.Widget dies. + + This is a good sanity check for any sort of object that wraps or + controls a ba.Widget. For instance, a 'Window' class instance has + no reason to still exist once its root container ba.Widget has fully + transitioned out and been destroyed. Circular references or careless + strong referencing can lead to such objects never getting destroyed, + however, and this helps detect such cases to avoid memory leaks. + """ + if DEBUG_UI_CLEANUP_CHECKS: + print(f'adding uicleanup to {obj}') + if not isinstance(widget, _ba.Widget): + raise TypeError('widget arg is not a ba.Widget') + + if bool(False): + + def foobar() -> None: + """Just testing.""" + if DEBUG_UI_CLEANUP_CHECKS: + print('uicleanupcheck widget dying...') + + widget.add_delete_callback(foobar) + + _ba.app.ui.cleanupchecks.append( + UICleanupCheck(obj=weakref.ref(obj), + widget=widget, + widget_death_time=None)) + + +def ui_upkeep() -> None: + """Run UI cleanup checks, etc. should be called periodically.""" + ui = _ba.app.ui + remainingchecks = [] + now = _ba.time(TimeType.REAL) + for check in ui.cleanupchecks: + obj = check.obj() + + # If the object has died, ignore and don't re-add. + if obj is None: + if DEBUG_UI_CLEANUP_CHECKS: + print('uicleanupcheck object is dead; hooray!') + continue + + # If the widget hadn't died yet, note if it has. + if check.widget_death_time is None: + remainingchecks.append(check) + if not check.widget: + check.widget_death_time = now + else: + # Widget was already dead; complain if its been too long. + if now - check.widget_death_time > 5.0: + print( + 'WARNING:', obj, + 'is still alive 5 second after its widget died;' + ' you might have a memory leak.') + print_active_refs(obj) + + else: + remainingchecks.append(check) + ui.cleanupchecks = remainingchecks diff --git a/dist/ba_data/python/ba/ui/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/ba/ui/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..23ad2d3 Binary files /dev/null and b/dist/ba_data/python/ba/ui/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/ba/ui/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/ba/ui/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..8d6b09a Binary files /dev/null and b/dist/ba_data/python/ba/ui/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bacommon/__init__.py b/dist/ba_data/python/bacommon/__init__.py new file mode 100644 index 0000000..26ce706 --- /dev/null +++ b/dist/ba_data/python/bacommon/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality and data common to ballistica client and server components.""" diff --git a/dist/ba_data/python/bacommon/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bacommon/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9d9002f Binary files /dev/null and b/dist/ba_data/python/bacommon/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bacommon/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bacommon/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..8b21d41 Binary files /dev/null and b/dist/ba_data/python/bacommon/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bacommon/__pycache__/assets.cpython-38.opt-1.pyc b/dist/ba_data/python/bacommon/__pycache__/assets.cpython-38.opt-1.pyc new file mode 100644 index 0000000..12b7eaf Binary files /dev/null and b/dist/ba_data/python/bacommon/__pycache__/assets.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bacommon/__pycache__/err.cpython-38.opt-1.pyc b/dist/ba_data/python/bacommon/__pycache__/err.cpython-38.opt-1.pyc new file mode 100644 index 0000000..54b5bc0 Binary files /dev/null and b/dist/ba_data/python/bacommon/__pycache__/err.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bacommon/__pycache__/servermanager.cpython-38.opt-1.pyc b/dist/ba_data/python/bacommon/__pycache__/servermanager.cpython-38.opt-1.pyc new file mode 100644 index 0000000..fc0528c Binary files /dev/null and b/dist/ba_data/python/bacommon/__pycache__/servermanager.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bacommon/__pycache__/servermanager.cpython-38.pyc b/dist/ba_data/python/bacommon/__pycache__/servermanager.cpython-38.pyc new file mode 100644 index 0000000..4d06e37 Binary files /dev/null and b/dist/ba_data/python/bacommon/__pycache__/servermanager.cpython-38.pyc differ diff --git a/dist/ba_data/python/bacommon/assets.py b/dist/ba_data/python/bacommon/assets.py new file mode 100644 index 0000000..60d1409 --- /dev/null +++ b/dist/ba_data/python/bacommon/assets.py @@ -0,0 +1,58 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to cloud based assets.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from enum import Enum + +from efro import entity + +if TYPE_CHECKING: + pass + + +class AssetPackageFlavor(Enum): + """Flavors for asset package outputs for different platforms/etc.""" + + # DXT3/DXT5 textures + DESKTOP = 'desktop' + + # ASTC textures + MOBILE = 'mobile' + + +class AssetType(Enum): + """Types for individual assets within a package.""" + TEXTURE = 'texture' + CUBE_TEXTURE = 'cube_texture' + SOUND = 'sound' + DATA = 'data' + MESH = 'mesh' + COLLISION_MESH = 'collision_mesh' + + +class AssetPackageFlavorManifestValue(entity.CompoundValue): + """A manifest of asset info for a specific flavor of an asset package.""" + assetfiles = entity.DictField('assetfiles', str, entity.StringValue()) + + +class AssetPackageFlavorManifest(entity.EntityMixin, + AssetPackageFlavorManifestValue): + """A self contained AssetPackageFlavorManifestValue.""" + + +class AssetPackageBuildState(entity.Entity): + """Contains info about an in-progress asset cloud build.""" + + # Asset names still being built. + in_progress_builds = entity.ListField('b', entity.StringValue()) + + # The initial number of assets needing to be built. + initial_build_count = entity.Field('c', entity.IntValue()) + + # Build error string. If this is present, it should be presented + # to the user and they should required to explicitly restart the build + # in some way if desired. + error = entity.Field('e', entity.OptionalStringValue()) diff --git a/dist/ba_data/python/bacommon/err.py b/dist/ba_data/python/bacommon/err.py new file mode 100644 index 0000000..b1403cb --- /dev/null +++ b/dist/ba_data/python/bacommon/err.py @@ -0,0 +1,18 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Error related functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + pass + + +class RemoteError(Exception): + """An error occurred on the other end of some connection.""" + + def __str__(self) -> str: + s = ''.join(str(arg) for arg in self.args) + return f'Remote Exception Follows:\n{s}' diff --git a/dist/ba_data/python/bacommon/servermanager.py b/dist/ba_data/python/bacommon/servermanager.py new file mode 100644 index 0000000..ac2fd78 --- /dev/null +++ b/dist/ba_data/python/bacommon/servermanager.py @@ -0,0 +1,168 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the server manager script.""" +from __future__ import annotations + +from enum import Enum +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional, Tuple, List + + +@dataclass +class ServerConfig: + """Configuration for the server manager app (_server).""" + + # Name of our server in the public parties list. + party_name: str = 'FFA' + + # If True, your party will show up in the global public party list + # Otherwise it will still be joinable via LAN or connecting by IP address. + party_is_public: bool = True + + # If True, all connecting clients will be authenticated through the master + # server to screen for fake account info. Generally this should always + # be enabled unless you are hosting on a LAN with no internet connection. + authenticate_clients: bool = True + + # IDs of server admins. Server admins are not kickable through the default + # kick vote system and they are able to kick players without a vote. To get + # your account id, enter 'getaccountid' in settings->advanced->enter-code. + admins: List[str] = field(default_factory=list) + + # Whether the default kick-voting system is enabled. + enable_default_kick_voting: bool = True + + # UDP port to host on. Change this to work around firewalls or run multiple + # servers on one machine. + # 43210 is the default and the only port that will show up in the LAN + # browser tab. + port: int = 43210 + + # Max devices in the party. Note that this does *NOT* mean max players. + # Any device in the party can have more than one player on it if they have + # multiple controllers. Also, this number currently includes the server so + # generally make it 1 bigger than you need. Max-players is not currently + # exposed but I'll try to add that soon. + max_party_size: int = 6 + + # Options here are 'ffa' (free-for-all) and 'teams' + # This value is only used if you do not supply a playlist_code (see below). + # In that case the default teams or free-for-all playlist gets used. + session_type: str = 'ffa' + + # To host your own custom playlists, use the 'share' functionality in the + # playlist editor in the regular version of the game. + # This will give you a numeric code you can enter here to host that + # playlist. + playlist_code: Optional[int] = None + + # Whether to shuffle the playlist or play its games in designated order. + playlist_shuffle: bool = True + + # If True, keeps team sizes equal by disallowing joining the largest team + # (teams mode only). + auto_balance_teams: bool = True + + # Whether to enable telnet access. + # IMPORTANT: This option is no longer available, as it was being used + # for exploits. Live access to the running server is still possible through + # the mgr.cmd() function in the server script. Run your server through + # tools such as 'screen' or 'tmux' and you can reconnect to it remotely + # over a secure ssh connection. + enable_telnet: bool = False + + # Series length in teams mode (7 == 'best-of-7' series; a team must + # get 4 wins) + teams_series_length: int = 7 + + # Points to win in free-for-all mode (Points are awarded per game based on + # performance) + ffa_series_length: int = 24 + + # If you provide a custom stats webpage for your server, you can use + # this to provide a convenient in-game link to it in the server-browser + # beside the server name. + # if ${ACCOUNT} is present in the string, it will be replaced by the + # currently-signed-in account's id. To fetch info about an account, + # your backend server can use the following url: + # http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE + stats_url: Optional[str] = None + + # If present, the server subprocess will attempt to gracefully exit after + # this amount of time. A graceful exit can occur at the end of a series + # or other opportune time. Server-managers set to auto-restart (the + # default) will then spin up a fresh subprocess. This mechanism can be + # useful to clear out any memory leaks or other accumulated bad state + # in the server subprocess. + clean_exit_minutes: Optional[float] = None + + # If present, the server subprocess will shut down immediately after this + # amount of time. This can be useful as a fallback for clean_exit_time. + # The server manager will then spin up a fresh server subprocess if + # auto-restart is enabled (the default). + unclean_exit_minutes: Optional[float] = None + + # If present, the server subprocess will shut down immediately if this + # amount of time passes with no activity from any players. The server + # manager will then spin up a fresh server subprocess if + # auto-restart is enabled (the default). + idle_exit_minutes: Optional[float] = None + + # (internal) stress-testing mode. + stress_test_players: Optional[int] = None + + +# NOTE: as much as possible, communication from the server-manager to the +# child-process should go through these and not ad-hoc Python string commands +# since this way is type safe. +class ServerCommand: + """Base class for commands that can be sent to the server.""" + + +@dataclass +class StartServerModeCommand(ServerCommand): + """Tells the app to switch into 'server' mode.""" + config: ServerConfig + + +class ShutdownReason(Enum): + """Reason a server is shutting down.""" + NONE = 'none' + RESTARTING = 'restarting' + + +@dataclass +class ShutdownCommand(ServerCommand): + """Tells the server to shut down.""" + reason: ShutdownReason + immediate: bool + + +@dataclass +class ChatMessageCommand(ServerCommand): + """Chat message from the server.""" + message: str + clients: Optional[List[int]] + + +@dataclass +class ScreenMessageCommand(ServerCommand): + """Screen-message from the server.""" + message: str + color: Optional[Tuple[float, float, float]] + clients: Optional[List[int]] + + +@dataclass +class ClientListCommand(ServerCommand): + """Print a list of clients.""" + + +@dataclass +class KickCommand(ServerCommand): + """Kick a client.""" + client_id: int + ban_time: Optional[int] diff --git a/dist/ba_data/python/bastd/__init__.py b/dist/ba_data/python/bastd/__init__.py new file mode 100644 index 0000000..7a4ff44 --- /dev/null +++ b/dist/ba_data/python/bastd/__init__.py @@ -0,0 +1,5 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Ballistica standard library: games, UI, etc.""" + +# ba_meta require api 6 diff --git a/dist/ba_data/python/bastd/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7fa5e46 Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..29e3f6c Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/appdelegate.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/__pycache__/appdelegate.cpython-38.opt-1.pyc new file mode 100644 index 0000000..56546ac Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/appdelegate.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/appdelegate.cpython-38.pyc b/dist/ba_data/python/bastd/__pycache__/appdelegate.cpython-38.pyc new file mode 100644 index 0000000..a0d67b6 Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/appdelegate.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/gameutils.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/__pycache__/gameutils.cpython-38.opt-1.pyc new file mode 100644 index 0000000..15da1c6 Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/gameutils.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/gameutils.cpython-38.pyc b/dist/ba_data/python/bastd/__pycache__/gameutils.cpython-38.pyc new file mode 100644 index 0000000..1504bc6 Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/gameutils.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/mainmenu.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/__pycache__/mainmenu.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d5ffd5b Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/mainmenu.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/mainmenu.cpython-38.pyc b/dist/ba_data/python/bastd/__pycache__/mainmenu.cpython-38.pyc new file mode 100644 index 0000000..356b77e Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/mainmenu.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/maps.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/__pycache__/maps.cpython-38.opt-1.pyc new file mode 100644 index 0000000..bb5b27d Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/maps.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/maps.cpython-38.pyc b/dist/ba_data/python/bastd/__pycache__/maps.cpython-38.pyc new file mode 100644 index 0000000..51a339d Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/maps.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/stdmap.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/__pycache__/stdmap.cpython-38.opt-1.pyc new file mode 100644 index 0000000..cfed2ff Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/stdmap.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/tutorial.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/__pycache__/tutorial.cpython-38.opt-1.pyc new file mode 100644 index 0000000..e2af84f Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/tutorial.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/__pycache__/tutorial.cpython-38.pyc b/dist/ba_data/python/bastd/__pycache__/tutorial.cpython-38.pyc new file mode 100644 index 0000000..b01e944 Binary files /dev/null and b/dist/ba_data/python/bastd/__pycache__/tutorial.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__init__.py b/dist/ba_data/python/bastd/activity/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/activity/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/activity/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/activity/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/activity/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..84b7500 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/coopjoin.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/activity/__pycache__/coopjoin.cpython-38.opt-1.pyc new file mode 100644 index 0000000..5bee795 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/coopjoin.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/coopscore.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/activity/__pycache__/coopscore.cpython-38.opt-1.pyc new file mode 100644 index 0000000..cd464a3 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/coopscore.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-38.opt-1.pyc new file mode 100644 index 0000000..94356a6 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-38.pyc b/dist/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-38.pyc new file mode 100644 index 0000000..4f2343b Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f073d03 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-38.opt-1.pyc new file mode 100644 index 0000000..23d5137 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-38.pyc b/dist/ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-38.pyc new file mode 100644 index 0000000..eeb2b45 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-38.opt-1.pyc new file mode 100644 index 0000000..bb10971 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-38.pyc b/dist/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-38.pyc new file mode 100644 index 0000000..5206e97 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c8b019f Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-38.pyc b/dist/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-38.pyc new file mode 100644 index 0000000..180f079 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d6370f0 Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-38.pyc b/dist/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-38.pyc new file mode 100644 index 0000000..1af031d Binary files /dev/null and b/dist/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/activity/coopjoin.py b/dist/ba_data/python/bastd/activity/coopjoin.py new file mode 100644 index 0000000..1bb614a --- /dev/null +++ b/dist/ba_data/python/bastd/activity/coopjoin.py @@ -0,0 +1,195 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the co-op join screen.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba +from ba.internal import JoinActivity + +if TYPE_CHECKING: + from typing import Any, Dict, List, Optional, Sequence, Union + + +class CoopJoinActivity(JoinActivity): + """Join-screen for co-op mode.""" + + # We can assume our session is a CoopSession. + session: ba.CoopSession + + def __init__(self, settings: dict): + super().__init__(settings) + session = self.session + assert isinstance(session, ba.CoopSession) + + # Let's show a list of scores-to-beat for 1 player at least. + assert session.campaign is not None + level_name_full = (session.campaign.name + ':' + + session.campaign_level_name) + config_str = ('1p' + session.campaign.getlevel( + session.campaign_level_name).get_score_version_string().replace( + ' ', '_')) + _ba.get_scores_to_beat(level_name_full, config_str, + ba.WeakCall(self._on_got_scores_to_beat)) + + def on_transition_in(self) -> None: + from bastd.actor.controlsguide import ControlsGuide + from bastd.actor.text import Text + super().on_transition_in() + assert isinstance(self.session, ba.CoopSession) + assert self.session.campaign + Text(self.session.campaign.getlevel( + self.session.campaign_level_name).displayname, + scale=1.3, + h_attach=Text.HAttach.CENTER, + h_align=Text.HAlign.CENTER, + v_attach=Text.VAttach.TOP, + transition=Text.Transition.FADE_IN, + transition_delay=4.0, + color=(1, 1, 1, 0.6), + position=(0, -95)).autoretain() + ControlsGuide(delay=1.0).autoretain() + + def _on_got_scores_to_beat(self, + scores: Optional[List[Dict[str, Any]]]) -> None: + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + from efro.util import asserttype + from bastd.actor.text import Text + + # Sort by originating date so that the most recent is first. + if scores is not None: + scores.sort(reverse=True, + key=lambda score: asserttype(score['time'], int)) + + # We only show achievements and challenges for CoopGameActivities. + session = self.session + assert isinstance(session, ba.CoopSession) + gameinstance = session.get_current_game_instance() + if isinstance(gameinstance, ba.CoopGameActivity): + score_type = gameinstance.get_score_type() + if scores is not None: + achievement_challenges = [ + a for a in scores if a['type'] == 'achievement_challenge' + ] + score_challenges = [ + a for a in scores if a['type'] == 'score_challenge' + ] + else: + achievement_challenges = score_challenges = [] + + delay = 1.0 + vpos = -140.0 + spacing = 25 + delay_inc = 0.1 + + def _add_t( + text: Union[str, ba.Lstr], + h_offs: float = 0.0, + scale: float = 1.0, + color: Sequence[float] = (1.0, 1.0, 1.0, 0.46) + ) -> None: + Text(text, + scale=scale * 0.76, + h_align=Text.HAlign.LEFT, + h_attach=Text.HAttach.LEFT, + v_attach=Text.VAttach.TOP, + transition=Text.Transition.FADE_IN, + transition_delay=delay, + color=color, + position=(60 + h_offs, vpos)).autoretain() + + if score_challenges: + _add_t(ba.Lstr(value='${A}:', + subs=[('${A}', + ba.Lstr(resource='scoreChallengesText')) + ]), + scale=1.1) + delay += delay_inc + vpos -= spacing + for chal in score_challenges: + _add_t(str(chal['value'] if score_type == 'points' else ba. + timestring(int(chal['value']) * 10, + timeformat=ba.TimeFormat.MILLISECONDS + ).evaluate()) + ' (1 player)', + h_offs=30, + color=(0.9, 0.7, 1.0, 0.8)) + delay += delay_inc + vpos -= 0.6 * spacing + _add_t(chal['player'], + h_offs=40, + color=(0.8, 1, 0.8, 0.6), + scale=0.8) + delay += delay_inc + vpos -= 1.2 * spacing + vpos -= 0.5 * spacing + + if achievement_challenges: + _add_t(ba.Lstr( + value='${A}:', + subs=[('${A}', + ba.Lstr(resource='achievementChallengesText'))]), + scale=1.1) + delay += delay_inc + vpos -= spacing + for chal in achievement_challenges: + _add_t(str(chal['value']), + h_offs=30, + color=(0.9, 0.7, 1.0, 0.8)) + delay += delay_inc + vpos -= 0.6 * spacing + _add_t(chal['player'], + h_offs=40, + color=(0.8, 1, 0.8, 0.6), + scale=0.8) + delay += delay_inc + vpos -= 1.2 * spacing + vpos -= 0.5 * spacing + + # Now list our remaining achievements for this level. + assert self.session.campaign is not None + assert isinstance(self.session, ba.CoopSession) + levelname = (self.session.campaign.name + ':' + + self.session.campaign_level_name) + ts_h_offs = 60 + + if not (ba.app.demo_mode or ba.app.arcade_mode): + achievements = [ + a + for a in ba.app.ach.achievements_for_coop_level(levelname) + if not a.complete + ] + have_achievements = bool(achievements) + achievements = [a for a in achievements if not a.complete] + vrmode = ba.app.vr_mode + if have_achievements: + Text(ba.Lstr(resource='achievementsRemainingText'), + host_only=True, + position=(ts_h_offs - 10, vpos), + transition=Text.Transition.FADE_IN, + scale=1.1 * 0.76, + h_attach=Text.HAttach.LEFT, + v_attach=Text.VAttach.TOP, + color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1), + shadow=1.0, + flatness=1.0 if vrmode else 0.6, + transition_delay=delay).autoretain() + hval = ts_h_offs + 50 + vpos -= 35 + for ach in achievements: + delay += 0.05 + ach.create_display(hval, vpos, delay, style='in_game') + vpos -= 55 + if not achievements: + Text(ba.Lstr(resource='noAchievementsRemainingText'), + host_only=True, + position=(ts_h_offs + 15, vpos + 10), + transition=Text.Transition.FADE_IN, + scale=0.7, + h_attach=Text.HAttach.LEFT, + v_attach=Text.VAttach.TOP, + color=(1, 1, 1, 0.5), + transition_delay=delay + 0.5).autoretain() diff --git a/dist/ba_data/python/bastd/activity/coopscore.py b/dist/ba_data/python/bastd/activity/coopscore.py new file mode 100644 index 0000000..b8678cd --- /dev/null +++ b/dist/ba_data/python/bastd/activity/coopscore.py @@ -0,0 +1,1469 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a score screen for coop games.""" +# pylint: disable=too-many-lines + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.actor.text import Text +from bastd.actor.zoomtext import ZoomText + +if TYPE_CHECKING: + from typing import Optional, Tuple, List, Dict, Any, Sequence + from bastd.ui.store.button import StoreButton + from bastd.ui.league.rankbutton import LeagueRankButton + + +class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): + """Score screen showing the results of a cooperative game.""" + + def __init__(self, settings: dict): + # pylint: disable=too-many-statements + super().__init__(settings) + + # Keep prev activity alive while we fade in + self.transition_time = 0.5 + self.inherits_tint = True + self.inherits_vr_camera_offset = True + self.inherits_music = True + self.use_fixed_vr_overlay = True + + self._do_new_rating: bool = self.session.tournament_id is not None + + self._score_display_sound = ba.getsound('scoreHit01') + self._score_display_sound_small = ba.getsound('scoreHit02') + self.drum_roll_sound = ba.getsound('drumRoll') + self.cymbal_sound = ba.getsound('cymbal') + + # These get used in UI bits so need to load them in the UI context. + with ba.Context('ui'): + self._replay_icon_texture = ba.gettexture('replayIcon') + self._menu_icon_texture = ba.gettexture('menuIcon') + self._next_level_icon_texture = ba.gettexture('nextLevelIcon') + + self._campaign: ba.Campaign = settings['campaign'] + + self._have_achievements = bool( + ba.app.ach.achievements_for_coop_level(self._campaign.name + ':' + + settings['level'])) + + self._account_type = (_ba.get_account_type() if + _ba.get_account_state() == 'signed_in' else None) + + self._game_service_icon_color: Optional[Sequence[float]] + self._game_service_achievements_texture: Optional[ba.Texture] + self._game_service_leaderboards_texture: Optional[ba.Texture] + + with ba.Context('ui'): + if self._account_type == 'Game Center': + self._game_service_icon_color = (1.0, 1.0, 1.0) + icon = ba.gettexture('gameCenterIcon') + self._game_service_achievements_texture = icon + self._game_service_leaderboards_texture = icon + self._account_has_achievements = True + elif self._account_type == 'Game Circle': + icon = ba.gettexture('gameCircleIcon') + self._game_service_icon_color = (1, 1, 1) + self._game_service_achievements_texture = icon + self._game_service_leaderboards_texture = icon + self._account_has_achievements = True + elif self._account_type == 'Google Play': + self._game_service_icon_color = (0.8, 1.0, 0.6) + self._game_service_achievements_texture = ( + ba.gettexture('googlePlayAchievementsIcon')) + self._game_service_leaderboards_texture = ( + ba.gettexture('googlePlayLeaderboardsIcon')) + self._account_has_achievements = True + else: + self._game_service_icon_color = None + self._game_service_achievements_texture = None + self._game_service_leaderboards_texture = None + self._account_has_achievements = False + + self._cashregistersound = ba.getsound('cashRegister') + self._gun_cocking_sound = ba.getsound('gunCocking') + self._dingsound = ba.getsound('ding') + self._score_link: Optional[str] = None + self._root_ui: Optional[ba.Widget] = None + self._background: Optional[ba.Actor] = None + self._old_best_rank = 0.0 + self._game_name_str: Optional[str] = None + self._game_config_str: Optional[str] = None + + # Ui bits. + self._corner_button_offs: Optional[Tuple[float, float]] = None + self._league_rank_button: Optional[LeagueRankButton] = None + self._store_button_instance: Optional[StoreButton] = None + self._restart_button: Optional[ba.Widget] = None + self._update_corner_button_positions_timer: Optional[ba.Timer] = None + self._next_level_error: Optional[ba.Actor] = None + + # Score/gameplay bits. + self._was_complete: Optional[bool] = None + self._is_complete: Optional[bool] = None + self._newly_complete: Optional[bool] = None + self._is_more_levels: Optional[bool] = None + self._next_level_name: Optional[str] = None + self._show_friend_scores: Optional[bool] = None + self._show_info: Optional[Dict[str, Any]] = None + self._name_str: Optional[str] = None + self._friends_loading_status: Optional[ba.Actor] = None + self._score_loading_status: Optional[ba.Actor] = None + self._tournament_time_remaining: Optional[float] = None + self._tournament_time_remaining_text: Optional[Text] = None + self._tournament_time_remaining_text_timer: Optional[ba.Timer] = None + + self._playerinfos: List[ba.PlayerInfo] = settings['playerinfos'] + assert isinstance(self._playerinfos, list) + assert (isinstance(i, ba.PlayerInfo) for i in self._playerinfos) + + self._score: Optional[int] = settings['score'] + assert isinstance(self._score, (int, type(None))) + + self._fail_message: Optional[ba.Lstr] = settings['fail_message'] + assert isinstance(self._fail_message, (ba.Lstr, type(None))) + + self._begin_time: Optional[float] = None + + self._score_order: str + if 'score_order' in settings: + if not settings['score_order'] in ['increasing', 'decreasing']: + raise ValueError('Invalid score order: ' + + settings['score_order']) + self._score_order = settings['score_order'] + else: + self._score_order = 'increasing' + assert isinstance(self._score_order, str) + + self._score_type: str + if 'score_type' in settings: + if not settings['score_type'] in ['points', 'time']: + raise ValueError('Invalid score type: ' + + settings['score_type']) + self._score_type = settings['score_type'] + else: + self._score_type = 'points' + assert isinstance(self._score_type, str) + + self._level_name: str = settings['level'] + assert isinstance(self._level_name, str) + + self._game_name_str = self._campaign.name + ':' + self._level_name + self._game_config_str = str(len( + self._playerinfos)) + 'p' + self._campaign.getlevel( + self._level_name).get_score_version_string().replace(' ', '_') + + # If game-center/etc scores are available we show our friends' + # scores. Otherwise we show our local high scores. + self._show_friend_scores = _ba.game_service_has_leaderboard( + self._game_name_str, self._game_config_str) + + try: + self._old_best_rank = self._campaign.getlevel( + self._level_name).rating + except Exception: + self._old_best_rank = 0.0 + + self._victory: bool = settings['outcome'] == 'victory' + + def __del__(self) -> None: + super().__del__() + + # If our UI is still up, kill it. + if self._root_ui: + with ba.Context('ui'): + ba.containerwidget(edit=self._root_ui, transition='out_left') + + def on_transition_in(self) -> None: + from bastd.actor import background # FIXME NO BSSTD + ba.set_analytics_screen('Coop Score Screen') + super().on_transition_in() + self._background = background.Background(fade_time=0.45, + start_faded=False, + show_logo=True) + + def _ui_menu(self) -> None: + from bastd.ui import specialoffer + if specialoffer.show_offer(): + return + ba.containerwidget(edit=self._root_ui, transition='out_left') + with ba.Context(self): + ba.timer(0.1, ba.Call(ba.WeakCall(self.session.end))) + + def _ui_restart(self) -> None: + from bastd.ui.tournamententry import TournamentEntryWindow + from bastd.ui import specialoffer + if specialoffer.show_offer(): + return + + # If we're in a tournament and it looks like there's no time left, + # disallow. + if self.session.tournament_id is not None: + if self._tournament_time_remaining is None: + ba.screenmessage( + ba.Lstr(resource='tournamentCheckingStateText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + if self._tournament_time_remaining <= 0: + ba.screenmessage(ba.Lstr(resource='tournamentEndedText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + # If there are currently fewer players than our session min, + # don't allow. + if len(self.players) < self.session.min_players: + ba.screenmessage(ba.Lstr(resource='notEnoughPlayersRemainingText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + self._campaign.set_selected_level(self._level_name) + + # If this is a tournament, go back to the tournament-entry UI + # otherwise just hop back in. + tournament_id = self.session.tournament_id + if tournament_id is not None: + assert self._restart_button is not None + TournamentEntryWindow( + tournament_id=tournament_id, + tournament_activity=self, + position=self._restart_button.get_screen_space_center()) + else: + ba.containerwidget(edit=self._root_ui, transition='out_left') + self.can_show_ad_on_death = True + with ba.Context(self): + self.end({'outcome': 'restart'}) + + def _ui_next(self) -> None: + from bastd.ui.specialoffer import show_offer + if show_offer(): + return + + # If we didn't just complete this level but are choosing to play the + # next one, set it as current (this won't happen otherwise). + if (self._is_complete and self._is_more_levels + and not self._newly_complete): + assert self._next_level_name is not None + self._campaign.set_selected_level(self._next_level_name) + ba.containerwidget(edit=self._root_ui, transition='out_left') + with ba.Context(self): + self.end({'outcome': 'next_level'}) + + def _ui_gc(self) -> None: + _ba.show_online_score_ui('leaderboard', + game=self._game_name_str, + game_version=self._game_config_str) + + def _ui_show_achievements(self) -> None: + _ba.show_online_score_ui('achievements') + + def _ui_worlds_best(self) -> None: + if self._score_link is None: + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource='scoreListUnavailableText'), + color=(1, 0.5, 0)) + else: + ba.open_url(self._score_link) + + def _ui_error(self) -> None: + with ba.Context(self): + self._next_level_error = Text( + ba.Lstr(resource='completeThisLevelToProceedText'), + flash=True, + maxwidth=360, + scale=0.54, + h_align=Text.HAlign.CENTER, + color=(0.5, 0.7, 0.5, 1), + position=(300, -235)) + ba.playsound(ba.getsound('error')) + ba.timer( + 2.0, + ba.WeakCall(self._next_level_error.handlemessage, + ba.DieMessage())) + + def _should_show_worlds_best_button(self) -> bool: + # Link is too complicated to display with no browser. + return ba.is_browser_likely_available() + + def request_ui(self) -> None: + """Set up a callback to show our UI at the next opportune time.""" + # We don't want to just show our UI in case the user already has the + # main menu up, so instead we add a callback for when the menu + # closes; if we're still alive, we'll come up then. + # If there's no main menu this gets called immediately. + ba.app.add_main_menu_close_callback(ba.WeakCall(self.show_ui)) + + def show_ui(self) -> None: + """Show the UI for restarting, playing the next Level, etc.""" + # pylint: disable=too-many-locals + from bastd.ui.store.button import StoreButton + from bastd.ui.league.rankbutton import LeagueRankButton + + delay = 0.7 if (self._score is not None) else 0.0 + + # If there's no players left in the game, lets not show the UI + # (that would allow restarting the game with zero players, etc). + if not self.players: + return + + rootc = self._root_ui = ba.containerwidget(size=(0, 0), + transition='in_right') + + h_offs = 7.0 + v_offs = -280.0 + + # We wanna prevent controllers users from popping up browsers + # or game-center widgets in cases where they can't easily get back + # to the game (like on mac). + can_select_extra_buttons = ba.app.platform == 'android' + + _ba.set_ui_input_device(None) # Menu is up for grabs. + + if self._show_friend_scores: + ba.buttonwidget(parent=rootc, + color=(0.45, 0.4, 0.5), + position=(h_offs - 520, v_offs + 480), + size=(300, 60), + label=ba.Lstr(resource='topFriendsText'), + on_activate_call=ba.WeakCall(self._ui_gc), + transition_delay=delay + 0.5, + icon=self._game_service_leaderboards_texture, + icon_color=self._game_service_icon_color, + autoselect=True, + selectable=can_select_extra_buttons) + + if self._have_achievements and self._account_has_achievements: + ba.buttonwidget(parent=rootc, + color=(0.45, 0.4, 0.5), + position=(h_offs - 520, v_offs + 450 - 235 + 40), + size=(300, 60), + label=ba.Lstr(resource='achievementsText'), + on_activate_call=ba.WeakCall( + self._ui_show_achievements), + transition_delay=delay + 1.5, + icon=self._game_service_achievements_texture, + icon_color=self._game_service_icon_color, + autoselect=True, + selectable=can_select_extra_buttons) + + if self._should_show_worlds_best_button(): + ba.buttonwidget( + parent=rootc, + color=(0.45, 0.4, 0.5), + position=(160, v_offs + 480), + size=(350, 62), + label=ba.Lstr(resource='tournamentStandingsText') + if self.session.tournament_id is not None else ba.Lstr( + resource='worldsBestScoresText') if self._score_type + == 'points' else ba.Lstr(resource='worldsBestTimesText'), + autoselect=True, + on_activate_call=ba.WeakCall(self._ui_worlds_best), + transition_delay=delay + 1.9, + selectable=can_select_extra_buttons) + else: + pass + + show_next_button = self._is_more_levels and not (ba.app.demo_mode + or ba.app.arcade_mode) + + if not show_next_button: + h_offs += 70 + + menu_button = ba.buttonwidget(parent=rootc, + autoselect=True, + position=(h_offs - 130 - 60, v_offs), + size=(110, 85), + label='', + on_activate_call=ba.WeakCall( + self._ui_menu)) + ba.imagewidget(parent=rootc, + draw_controller=menu_button, + position=(h_offs - 130 - 60 + 22, v_offs + 14), + size=(60, 60), + texture=self._menu_icon_texture, + opacity=0.8) + self._restart_button = restart_button = ba.buttonwidget( + parent=rootc, + autoselect=True, + position=(h_offs - 60, v_offs), + size=(110, 85), + label='', + on_activate_call=ba.WeakCall(self._ui_restart)) + ba.imagewidget(parent=rootc, + draw_controller=restart_button, + position=(h_offs - 60 + 19, v_offs + 7), + size=(70, 70), + texture=self._replay_icon_texture, + opacity=0.8) + + next_button: Optional[ba.Widget] = None + + # Our 'next' button is disabled if we haven't unlocked the next + # level yet and invisible if there is none. + if show_next_button: + if self._is_complete: + call = ba.WeakCall(self._ui_next) + button_sound = True + image_opacity = 0.8 + color = None + else: + call = ba.WeakCall(self._ui_error) + button_sound = False + image_opacity = 0.2 + color = (0.3, 0.3, 0.3) + next_button = ba.buttonwidget(parent=rootc, + autoselect=True, + position=(h_offs + 130 - 60, v_offs), + size=(110, 85), + label='', + on_activate_call=call, + color=color, + enable_sound=button_sound) + ba.imagewidget(parent=rootc, + draw_controller=next_button, + position=(h_offs + 130 - 60 + 12, v_offs + 5), + size=(80, 80), + texture=self._next_level_icon_texture, + opacity=image_opacity) + + x_offs_extra = 0 if show_next_button else -100 + self._corner_button_offs = (h_offs + 300.0 + 100.0 + x_offs_extra, + v_offs + 560.0) + + if ba.app.demo_mode or ba.app.arcade_mode: + self._league_rank_button = None + self._store_button_instance = None + else: + self._league_rank_button = LeagueRankButton( + parent=rootc, + position=(h_offs + 300 + 100 + x_offs_extra, v_offs + 560), + size=(100, 60), + scale=0.9, + color=(0.4, 0.4, 0.9), + textcolor=(0.9, 0.9, 2.0), + transition_delay=0.0, + smooth_update_delay=5.0) + self._store_button_instance = StoreButton( + parent=rootc, + position=(h_offs + 400 + 100 + x_offs_extra, v_offs + 560), + show_tickets=True, + sale_scale=0.85, + size=(100, 60), + scale=0.9, + button_type='square', + color=(0.35, 0.25, 0.45), + textcolor=(0.9, 0.7, 1.0), + transition_delay=0.0) + + ba.containerwidget(edit=rootc, + selected_child=next_button if + (self._newly_complete and self._victory + and show_next_button) else restart_button, + on_cancel_call=menu_button.activate) + + self._update_corner_button_positions() + self._update_corner_button_positions_timer = ba.Timer( + 1.0, + ba.WeakCall(self._update_corner_button_positions), + repeat=True, + timetype=ba.TimeType.REAL) + + def _update_corner_button_positions(self) -> None: + offs = -55 if _ba.is_party_icon_visible() else 0 + assert self._corner_button_offs is not None + pos_x = self._corner_button_offs[0] + offs + pos_y = self._corner_button_offs[1] + if self._league_rank_button is not None: + self._league_rank_button.set_position((pos_x, pos_y)) + if self._store_button_instance is not None: + self._store_button_instance.set_position((pos_x + 100, pos_y)) + + def on_begin(self) -> None: + # FIXME: Clean this up. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + super().on_begin() + + self._begin_time = ba.time() + + # Calc whether the level is complete and other stuff. + levels = self._campaign.levels + level = self._campaign.getlevel(self._level_name) + self._was_complete = level.complete + self._is_complete = (self._was_complete or self._victory) + self._newly_complete = (self._is_complete and not self._was_complete) + self._is_more_levels = ((level.index < len(levels) - 1) + and self._campaign.sequential) + + # Any time we complete a level, set the next one as unlocked. + if self._is_complete and self._is_more_levels: + _ba.add_transaction({ + 'type': 'COMPLETE_LEVEL', + 'campaign': self._campaign.name, + 'level': self._level_name + }) + self._next_level_name = levels[level.index + 1].name + + # If this is the first time we completed it, set the next one + # as current. + if self._newly_complete: + cfg = ba.app.config + cfg['Selected Coop Game'] = (self._campaign.name + ':' + + self._next_level_name) + cfg.commit() + self._campaign.set_selected_level(self._next_level_name) + + ba.timer(1.0, ba.WeakCall(self.request_ui)) + + if (self._is_complete and self._victory and self._is_more_levels + and not (ba.app.demo_mode or ba.app.arcade_mode)): + Text(ba.Lstr(value='${A}:\n', + subs=[('${A}', ba.Lstr(resource='levelUnlockedText')) + ]) if self._newly_complete else + ba.Lstr(value='${A}:\n', + subs=[('${A}', ba.Lstr(resource='nextLevelText'))]), + transition=Text.Transition.IN_RIGHT, + transition_delay=5.2, + flash=self._newly_complete, + scale=0.54, + h_align=Text.HAlign.CENTER, + maxwidth=270, + color=(0.5, 0.7, 0.5, 1), + position=(270, -235)).autoretain() + assert self._next_level_name is not None + Text(ba.Lstr(translate=('coopLevelNames', self._next_level_name)), + transition=Text.Transition.IN_RIGHT, + transition_delay=5.2, + flash=self._newly_complete, + scale=0.7, + h_align=Text.HAlign.CENTER, + maxwidth=205, + color=(0.5, 0.7, 0.5, 1), + position=(270, -255)).autoretain() + if self._newly_complete: + ba.timer(5.2, ba.Call(ba.playsound, self._cashregistersound)) + ba.timer(5.2, ba.Call(ba.playsound, self._dingsound)) + + offs_x = -195 + if len(self._playerinfos) > 1: + pstr = ba.Lstr(value='- ${A} -', + subs=[('${A}', + ba.Lstr(resource='multiPlayerCountText', + subs=[('${COUNT}', + str(len(self._playerinfos))) + ]))]) + else: + pstr = ba.Lstr(value='- ${A} -', + subs=[('${A}', + ba.Lstr(resource='singlePlayerCountText'))]) + ZoomText(self._campaign.getlevel(self._level_name).displayname, + maxwidth=800, + flash=False, + trail=False, + color=(0.5, 1, 0.5, 1), + h_align='center', + scale=0.4, + position=(0, 292), + jitter=1.0).autoretain() + Text(pstr, + maxwidth=300, + transition=Text.Transition.FADE_IN, + scale=0.7, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + color=(0.5, 0.7, 0.5, 1), + position=(0, 230)).autoretain() + + adisp = _ba.get_account_display_string() + txt = Text(ba.Lstr(resource='waitingForHostText', + subs=[('${HOST}', adisp)]), + maxwidth=300, + transition=Text.Transition.FADE_IN, + transition_delay=8.0, + scale=0.85, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + color=(1, 1, 0, 1), + position=(0, -230)).autoretain() + assert txt.node + txt.node.client_only = True + + if self._score is not None: + ba.timer(0.35, + ba.Call(ba.playsound, self._score_display_sound_small)) + + # Vestigial remain; this stuff should just be instance vars. + self._show_info = {} + + if self._score is not None: + ba.timer(0.8, ba.WeakCall(self._show_score_val, offs_x)) + else: + ba.pushcall(ba.WeakCall(self._show_fail)) + + self._name_str = name_str = ', '.join( + [p.name for p in self._playerinfos]) + + if self._show_friend_scores: + self._friends_loading_status = Text( + ba.Lstr(value='${A}...', + subs=[('${A}', ba.Lstr(resource='loadingText'))]), + position=(-405, 150 + 30), + color=(1, 1, 1, 0.4), + transition=Text.Transition.FADE_IN, + scale=0.7, + transition_delay=2.0) + self._score_loading_status = Text(ba.Lstr( + value='${A}...', subs=[('${A}', ba.Lstr(resource='loadingText'))]), + position=(280, 150 + 30), + color=(1, 1, 1, 0.4), + transition=Text.Transition.FADE_IN, + scale=0.7, + transition_delay=2.0) + + if self._score is not None: + ba.timer(0.4, ba.WeakCall(self._play_drumroll)) + + # Add us to high scores, filter, and store. + our_high_scores_all = self._campaign.getlevel( + self._level_name).get_high_scores() + + our_high_scores = our_high_scores_all.setdefault( + str(len(self._playerinfos)) + ' Player', []) + + if self._score is not None: + our_score: Optional[list] = [ + self._score, { + 'players': [{ + 'name': p.name, + 'character': p.character + } for p in self._playerinfos] + } + ] + our_high_scores.append(our_score) + else: + our_score = None + + try: + our_high_scores.sort(reverse=self._score_order == 'increasing', + key=lambda x: x[0]) + except Exception: + ba.print_exception('Error sorting scores.') + print(f'our_high_scores: {our_high_scores}') + + del our_high_scores[10:] + + if self._score is not None: + sver = (self._campaign.getlevel( + self._level_name).get_score_version_string()) + _ba.add_transaction({ + 'type': 'SET_LEVEL_LOCAL_HIGH_SCORES', + 'campaign': self._campaign.name, + 'level': self._level_name, + 'scoreVersion': sver, + 'scores': our_high_scores_all + }) + if _ba.get_account_state() != 'signed_in': + # We expect this only in kiosk mode; complain otherwise. + if not (ba.app.demo_mode or ba.app.arcade_mode): + print('got not-signed-in at score-submit; unexpected') + if self._show_friend_scores: + ba.pushcall(ba.WeakCall(self._got_friend_score_results, None)) + ba.pushcall(ba.WeakCall(self._got_score_results, None)) + else: + assert self._game_name_str is not None + assert self._game_config_str is not None + _ba.submit_score(self._game_name_str, + self._game_config_str, + name_str, + self._score, + ba.WeakCall(self._got_score_results), + ba.WeakCall(self._got_friend_score_results) + if self._show_friend_scores else None, + order=self._score_order, + tournament_id=self.session.tournament_id, + score_type=self._score_type, + campaign=self._campaign.name, + level=self._level_name) + + # Apply the transactions we've been adding locally. + _ba.run_transactions() + + # If we're not doing the world's-best button, just show a title + # instead. + ts_height = 300 + ts_h_offs = 210 + v_offs = 40 + txt = Text(ba.Lstr(resource='tournamentStandingsText') + if self.session.tournament_id is not None else ba.Lstr( + resource='worldsBestScoresText') if self._score_type + == 'points' else ba.Lstr(resource='worldsBestTimesText'), + maxwidth=210, + position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), + transition=Text.Transition.IN_LEFT, + v_align=Text.VAlign.CENTER, + scale=1.2, + transition_delay=2.2).autoretain() + + # If we've got a button on the server, only show this on clients. + if self._should_show_worlds_best_button(): + assert txt.node + txt.node.client_only = True + + # If we have no friend scores, display local best scores. + if self._show_friend_scores: + + # Host has a button, so we need client-only text. + ts_height = 300 + ts_h_offs = -480 + v_offs = 40 + txt = Text(ba.Lstr(resource='topFriendsText'), + maxwidth=210, + position=(ts_h_offs - 10, + ts_height / 2 + 25 + v_offs + 20), + transition=Text.Transition.IN_RIGHT, + v_align=Text.VAlign.CENTER, + scale=1.2, + transition_delay=1.8).autoretain() + assert txt.node + txt.node.client_only = True + else: + + ts_height = 300 + ts_h_offs = -480 + v_offs = 40 + Text(ba.Lstr(resource='yourBestScoresText') if self._score_type + == 'points' else ba.Lstr(resource='yourBestTimesText'), + maxwidth=210, + position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), + transition=Text.Transition.IN_RIGHT, + v_align=Text.VAlign.CENTER, + scale=1.2, + transition_delay=1.8).autoretain() + + display_scores = list(our_high_scores) + display_count = 5 + + while len(display_scores) < display_count: + display_scores.append((0, None)) + + showed_ours = False + h_offs_extra = 85 if self._score_type == 'points' else 130 + v_offs_extra = 20 + v_offs_names = 0 + scale = 1.0 + p_count = len(self._playerinfos) + h_offs_extra -= 75 + if p_count > 1: + h_offs_extra -= 20 + if p_count == 2: + scale = 0.9 + elif p_count == 3: + scale = 0.65 + elif p_count == 4: + scale = 0.5 + times: List[Tuple[float, float]] = [] + for i in range(display_count): + times.insert(random.randrange(0, + len(times) + 1), + (1.9 + i * 0.05, 2.3 + i * 0.05)) + for i in range(display_count): + try: + if display_scores[i][1] is None: + name_str = '-' + else: + name_str = ', '.join([ + p['name'] for p in display_scores[i][1]['players'] + ]) + except Exception: + ba.print_exception( + f'Error calcing name_str for {display_scores}') + name_str = '-' + if display_scores[i] == our_score and not showed_ours: + flash = True + color0 = (0.6, 0.4, 0.1, 1.0) + color1 = (0.6, 0.6, 0.6, 1.0) + tdelay1 = 3.7 + tdelay2 = 3.7 + showed_ours = True + else: + flash = False + color0 = (0.6, 0.4, 0.1, 1.0) + color1 = (0.6, 0.6, 0.6, 1.0) + tdelay1 = times[i][0] + tdelay2 = times[i][1] + Text(str(display_scores[i][0]) if self._score_type == 'points' + else ba.timestring(display_scores[i][0] * 10, + timeformat=ba.TimeFormat.MILLISECONDS, + suppress_format_warning=True), + position=(ts_h_offs + 20 + h_offs_extra, + v_offs_extra + ts_height / 2 + -ts_height * + (i + 1) / 10 + v_offs + 11.0), + h_align=Text.HAlign.RIGHT, + v_align=Text.VAlign.CENTER, + color=color0, + flash=flash, + transition=Text.Transition.IN_RIGHT, + transition_delay=tdelay1).autoretain() + + Text(ba.Lstr(value=name_str), + position=(ts_h_offs + 35 + h_offs_extra, + v_offs_extra + ts_height / 2 + -ts_height * + (i + 1) / 10 + v_offs_names + v_offs + 11.0), + maxwidth=80.0 + 100.0 * len(self._playerinfos), + v_align=Text.VAlign.CENTER, + color=color1, + flash=flash, + scale=scale, + transition=Text.Transition.IN_RIGHT, + transition_delay=tdelay2).autoretain() + + # Show achievements for this level. + ts_height = -150 + ts_h_offs = -480 + v_offs = 40 + + # Only make this if we don't have the button + # (never want clients to see it so no need for client-only + # version, etc). + if self._have_achievements: + if not self._account_has_achievements: + Text(ba.Lstr(resource='achievementsText'), + position=(ts_h_offs - 10, + ts_height / 2 + 25 + v_offs + 3), + maxwidth=210, + host_only=True, + transition=Text.Transition.IN_RIGHT, + v_align=Text.VAlign.CENTER, + scale=1.2, + transition_delay=2.8).autoretain() + + assert self._game_name_str is not None + achievements = ba.app.ach.achievements_for_coop_level( + self._game_name_str) + hval = -455 + vval = -100 + tdelay = 0.0 + for ach in achievements: + ach.create_display(hval, vval + v_offs, 3.0 + tdelay) + vval -= 55 + tdelay += 0.250 + + ba.timer(5.0, ba.WeakCall(self._show_tips)) + + def _play_drumroll(self) -> None: + ba.NodeActor( + ba.newnode('sound', + attrs={ + 'sound': self.drum_roll_sound, + 'positional': False, + 'loop': False + })).autoretain() + + def _got_friend_score_results(self, results: Optional[List[Any]]) -> None: + + # FIXME: tidy this up + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + from efro.util import asserttype + # delay a bit if results come in too fast + assert self._begin_time is not None + base_delay = max(0, 1.9 - (ba.time() - self._begin_time)) + ts_height = 300 + ts_h_offs = -550 + v_offs = 30 + + # Report in case of error. + if results is None: + self._friends_loading_status = Text( + ba.Lstr(resource='friendScoresUnavailableText'), + maxwidth=330, + position=(-475, 150 + v_offs), + color=(1, 1, 1, 0.4), + transition=Text.Transition.FADE_IN, + transition_delay=base_delay + 0.8, + scale=0.7) + return + + self._friends_loading_status = None + + # Ok, it looks like we aren't able to reliably get a just-submitted + # result returned in the score list, so we need to look for our score + # in this list and replace it if ours is better or add ours otherwise. + if self._score is not None: + our_score_entry = [self._score, 'Me', True] + for score in results: + if score[2]: + if self._score_order == 'increasing': + our_score_entry[0] = max(score[0], self._score) + else: + our_score_entry[0] = min(score[0], self._score) + results.remove(score) + break + results.append(our_score_entry) + results.sort(reverse=self._score_order == 'increasing', + key=lambda x: asserttype(x[0], int)) + + # If we're not submitting our own score, we still want to change the + # name of our own score to 'Me'. + else: + for score in results: + if score[2]: + score[1] = 'Me' + break + h_offs_extra = 80 if self._score_type == 'points' else 130 + v_offs_extra = 20 + v_offs_names = 0 + scale = 1.0 + + # Make sure there's at least 5. + while len(results) < 5: + results.append([0, '-', False]) + results = results[:5] + times: List[Tuple[float, float]] = [] + for i in range(len(results)): + times.insert(random.randrange(0, + len(times) + 1), + (base_delay + i * 0.05, base_delay + 0.3 + i * 0.05)) + for i, tval in enumerate(results): + score = int(tval[0]) + name_str = tval[1] + is_me = tval[2] + if is_me and score == self._score: + flash = True + color0 = (0.6, 0.4, 0.1, 1.0) + color1 = (0.6, 0.6, 0.6, 1.0) + tdelay1 = base_delay + 1.0 + tdelay2 = base_delay + 1.0 + else: + flash = False + if is_me: + color0 = (0.6, 0.4, 0.1, 1.0) + color1 = (0.9, 1.0, 0.9, 1.0) + else: + color0 = (0.6, 0.4, 0.1, 1.0) + color1 = (0.6, 0.6, 0.6, 1.0) + tdelay1 = times[i][0] + tdelay2 = times[i][1] + if name_str != '-': + Text(str(score) if self._score_type == 'points' else + ba.timestring(score * 10, + timeformat=ba.TimeFormat.MILLISECONDS), + position=(ts_h_offs + 20 + h_offs_extra, + v_offs_extra + ts_height / 2 + -ts_height * + (i + 1) / 10 + v_offs + 11.0), + h_align=Text.HAlign.RIGHT, + v_align=Text.VAlign.CENTER, + color=color0, + flash=flash, + transition=Text.Transition.IN_RIGHT, + transition_delay=tdelay1).autoretain() + else: + if is_me: + print('Error: got empty name_str on score result:', tval) + + Text(ba.Lstr(value=name_str), + position=(ts_h_offs + 35 + h_offs_extra, + v_offs_extra + ts_height / 2 + -ts_height * + (i + 1) / 10 + v_offs_names + v_offs + 11.0), + color=color1, + maxwidth=160.0, + v_align=Text.VAlign.CENTER, + flash=flash, + scale=scale, + transition=Text.Transition.IN_RIGHT, + transition_delay=tdelay2).autoretain() + + def _got_score_results(self, results: Optional[Dict[str, Any]]) -> None: + + # FIXME: tidy this up + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + + # We need to manually run this in the context of our activity + # and only if we aren't shutting down. + # (really should make the submit_score call handle that stuff itself) + if self.expired: + return + with ba.Context(self): + # Delay a bit if results come in too fast. + assert self._begin_time is not None + base_delay = max(0, 2.7 - (ba.time() - self._begin_time)) + v_offs = 20 + if results is None: + self._score_loading_status = Text( + ba.Lstr(resource='worldScoresUnavailableText'), + position=(230, 150 + v_offs), + color=(1, 1, 1, 0.4), + transition=Text.Transition.FADE_IN, + transition_delay=base_delay + 0.3, + scale=0.7) + else: + self._score_link = results['link'] + assert self._score_link is not None + if not self._score_link.startswith('http://'): + self._score_link = (_ba.get_master_server_address() + '/' + + self._score_link) + self._score_loading_status = None + if 'tournamentSecondsRemaining' in results: + secs_remaining = results['tournamentSecondsRemaining'] + assert isinstance(secs_remaining, int) + self._tournament_time_remaining = secs_remaining + self._tournament_time_remaining_text_timer = ba.Timer( + 1.0, + ba.WeakCall( + self._update_tournament_time_remaining_text), + repeat=True, + timetype=ba.TimeType.BASE) + + assert self._show_info is not None + self._show_info['results'] = results + if results is not None: + if results['tops'] != '': + self._show_info['tops'] = results['tops'] + else: + self._show_info['tops'] = [] + offs_x = -195 + available = (self._show_info['results'] is not None) + if self._score is not None: + ba.timer((1.5 + base_delay), + ba.WeakCall(self._show_world_rank, offs_x), + timetype=ba.TimeType.BASE) + ts_h_offs = 200 + ts_height = 300 + + # Show world tops. + if available: + + # Show the number of games represented by this + # list (except for in tournaments). + if self.session.tournament_id is None: + Text(ba.Lstr(resource='lastGamesText', + subs=[ + ('${COUNT}', + str(self._show_info['results']['total'])) + ]), + position=(ts_h_offs - 35 + 95, + ts_height / 2 + 6 + v_offs), + color=(0.4, 0.4, 0.4, 1.0), + scale=0.7, + transition=Text.Transition.IN_RIGHT, + transition_delay=base_delay + 0.3).autoretain() + else: + v_offs += 20 + + h_offs_extra = 0 + v_offs_names = 0 + scale = 1.0 + p_count = len(self._playerinfos) + if p_count > 1: + h_offs_extra -= 40 + if self._score_type != 'points': + h_offs_extra += 60 + if p_count == 2: + scale = 0.9 + elif p_count == 3: + scale = 0.65 + elif p_count == 4: + scale = 0.5 + + # Make sure there's at least 10. + while len(self._show_info['tops']) < 10: + self._show_info['tops'].append([0, '-']) + + times: List[Tuple[float, float]] = [] + for i in range(len(self._show_info['tops'])): + times.insert( + random.randrange(0, + len(times) + 1), + (base_delay + i * 0.05, base_delay + 0.4 + i * 0.05)) + for i, tval in enumerate(self._show_info['tops']): + score = int(tval[0]) + name_str = tval[1] + if self._name_str == name_str and self._score == score: + flash = True + color0 = (0.6, 0.4, 0.1, 1.0) + color1 = (0.6, 0.6, 0.6, 1.0) + tdelay1 = base_delay + 1.0 + tdelay2 = base_delay + 1.0 + else: + flash = False + if self._name_str == name_str: + color0 = (0.6, 0.4, 0.1, 1.0) + color1 = (0.9, 1.0, 0.9, 1.0) + else: + color0 = (0.6, 0.4, 0.1, 1.0) + color1 = (0.6, 0.6, 0.6, 1.0) + tdelay1 = times[i][0] + tdelay2 = times[i][1] + + if name_str != '-': + Text(str(score) if self._score_type == 'points' else + ba.timestring( + score * 10, + timeformat=ba.TimeFormat.MILLISECONDS), + position=(ts_h_offs + 20 + h_offs_extra, + ts_height / 2 + -ts_height * + (i + 1) / 10 + v_offs + 11.0), + h_align=Text.HAlign.RIGHT, + v_align=Text.VAlign.CENTER, + color=color0, + flash=flash, + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay1).autoretain() + Text(ba.Lstr(value=name_str), + position=(ts_h_offs + 35 + h_offs_extra, + ts_height / 2 + -ts_height * (i + 1) / 10 + + v_offs_names + v_offs + 11.0), + maxwidth=80.0 + 100.0 * len(self._playerinfos), + v_align=Text.VAlign.CENTER, + color=color1, + flash=flash, + scale=scale, + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay2).autoretain() + + def _show_tips(self) -> None: + from bastd.actor.tipstext import TipsText + TipsText(offs_y=30).autoretain() + + def _update_tournament_time_remaining_text(self) -> None: + if self._tournament_time_remaining is None: + return + self._tournament_time_remaining = max( + 0, self._tournament_time_remaining - 1) + if self._tournament_time_remaining_text is not None: + val = ba.timestring(self._tournament_time_remaining, + suppress_format_warning=True, + centi=False) + self._tournament_time_remaining_text.node.text = val + + def _show_world_rank(self, offs_x: float) -> None: + # FIXME: Tidy this up. + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + from ba.internal import get_tournament_prize_strings + assert self._show_info is not None + available = (self._show_info['results'] is not None) + + if available: + error = (self._show_info['results']['error'] + if 'error' in self._show_info['results'] else None) + rank = self._show_info['results']['rank'] + total = self._show_info['results']['total'] + rating = (10.0 if total == 1 else 10.0 * (1.0 - (float(rank - 1) / + (total - 1)))) + player_rank = self._show_info['results']['playerRank'] + best_player_rank = self._show_info['results']['bestPlayerRank'] + else: + error = False + rating = None + player_rank = None + best_player_rank = None + + # If we've got tournament-seconds-remaining, show it. + if self._tournament_time_remaining is not None: + Text(ba.Lstr(resource='coopSelectWindow.timeRemainingText'), + position=(-360, -70 - 100), + color=(1, 1, 1, 0.7), + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=0.8, + maxwidth=300, + transition_delay=2.0).autoretain() + self._tournament_time_remaining_text = Text( + '', + position=(-360, -110 - 100), + color=(1, 1, 1, 0.7), + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=1.6, + maxwidth=150, + transition_delay=2.0) + + # If we're a tournament, show prizes. + try: + tournament_id = self.session.tournament_id + if tournament_id is not None: + if tournament_id in ba.app.accounts.tournament_info: + tourney_info = ba.app.accounts.tournament_info[ + tournament_id] + # pylint: disable=unbalanced-tuple-unpacking + pr1, pv1, pr2, pv2, pr3, pv3 = ( + get_tournament_prize_strings(tourney_info)) + # pylint: enable=unbalanced-tuple-unpacking + Text(ba.Lstr(resource='coopSelectWindow.prizesText'), + position=(-360, -70 + 77), + color=(1, 1, 1, 0.7), + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=1.0, + maxwidth=300, + transition_delay=2.0).autoretain() + vval = -107 + 70 + for rng, val in ((pr1, pv1), (pr2, pv2), (pr3, pv3)): + Text(rng, + position=(-410 + 10, vval), + color=(1, 1, 1, 0.7), + h_align=Text.HAlign.RIGHT, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=0.6, + maxwidth=300, + transition_delay=2.0).autoretain() + Text(val, + position=(-390 + 10, vval), + color=(0.7, 0.7, 0.7, 1.0), + h_align=Text.HAlign.LEFT, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=0.8, + maxwidth=300, + transition_delay=2.0).autoretain() + vval -= 35 + except Exception: + ba.print_exception('Error showing prize ranges.') + + if self._do_new_rating: + if error: + ZoomText(ba.Lstr(resource='failText'), + flash=True, + trail=True, + scale=1.0 if available else 0.333, + tilt_translate=0.11, + h_align='center', + position=(190 + offs_x, -60), + maxwidth=200, + jitter=1.0).autoretain() + Text(ba.Lstr(translate=('serverResponses', error)), + position=(0, -140), + color=(1, 1, 1, 0.7), + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=0.9, + maxwidth=400, + transition_delay=1.0).autoretain() + else: + ZoomText((('#' + str(player_rank)) if player_rank is not None + else ba.Lstr(resource='unavailableText')), + flash=True, + trail=True, + scale=1.0 if available else 0.333, + tilt_translate=0.11, + h_align='center', + position=(190 + offs_x, -60), + maxwidth=200, + jitter=1.0).autoretain() + + Text(ba.Lstr(value='${A}:', + subs=[('${A}', ba.Lstr(resource='rankText'))]), + position=(0, 36), + maxwidth=300, + transition=Text.Transition.FADE_IN, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition_delay=0).autoretain() + if best_player_rank is not None: + Text(ba.Lstr(resource='currentStandingText', + fallback_resource='bestRankText', + subs=[('${RANK}', str(best_player_rank))]), + position=(0, -155), + color=(1, 1, 1, 0.7), + h_align=Text.HAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=0.7, + transition_delay=1.0).autoretain() + else: + ZoomText((f'{rating:.1f}' if available else ba.Lstr( + resource='unavailableText')), + flash=True, + trail=True, + scale=0.6 if available else 0.333, + tilt_translate=0.11, + h_align='center', + position=(190 + offs_x, -94), + maxwidth=200, + jitter=1.0).autoretain() + + if available: + if rating >= 9.5: + stars = 3 + elif rating >= 7.5: + stars = 2 + elif rating > 0.0: + stars = 1 + else: + stars = 0 + star_tex = ba.gettexture('star') + star_x = 135 + offs_x + for _i in range(stars): + img = ba.NodeActor( + ba.newnode('image', + attrs={ + 'texture': star_tex, + 'position': (star_x, -16), + 'scale': (62, 62), + 'opacity': 1.0, + 'color': (2.2, 1.2, 0.3), + 'absolute_scale': True + })).autoretain() + + assert img.node + ba.animate(img.node, 'opacity', {0.15: 0, 0.4: 1}) + star_x += 60 + for _i in range(3 - stars): + img = ba.NodeActor( + ba.newnode('image', + attrs={ + 'texture': star_tex, + 'position': (star_x, -16), + 'scale': (62, 62), + 'opacity': 1.0, + 'color': (0.3, 0.3, 0.3), + 'absolute_scale': True + })).autoretain() + assert img.node + ba.animate(img.node, 'opacity', {0.15: 0, 0.4: 1}) + star_x += 60 + + def dostar(count: int, xval: float, offs_y: float, + score: str) -> None: + Text(score + ' =', + position=(xval, -64 + offs_y), + color=(0.6, 0.6, 0.6, 0.6), + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=0.4, + transition_delay=1.0).autoretain() + stx = xval + 20 + for _i2 in range(count): + img2 = ba.NodeActor( + ba.newnode('image', + attrs={ + 'texture': star_tex, + 'position': (stx, -64 + offs_y), + 'scale': (12, 12), + 'opacity': 0.7, + 'color': (2.2, 1.2, 0.3), + 'absolute_scale': True + })).autoretain() + assert img2.node + ba.animate(img2.node, 'opacity', {1.0: 0.0, 1.5: 0.5}) + stx += 13.0 + + dostar(1, -44 - 30, -112, '0.0') + dostar(2, 10 - 30, -112, '7.5') + dostar(3, 77 - 30, -112, '9.5') + try: + best_rank = self._campaign.getlevel(self._level_name).rating + except Exception: + best_rank = 0.0 + + if available: + Text(ba.Lstr( + resource='outOfText', + subs=[('${RANK}', + str(int(self._show_info['results']['rank']))), + ('${ALL}', str(self._show_info['results']['total'])) + ]), + position=(0, -155 if self._newly_complete else -145), + color=(1, 1, 1, 0.7), + h_align=Text.HAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=0.55, + transition_delay=1.0).autoretain() + + new_best = (best_rank > self._old_best_rank and best_rank > 0.0) + was_string = ba.Lstr(value=' ${A}', + subs=[('${A}', + ba.Lstr(resource='scoreWasText')), + ('${COUNT}', str(self._old_best_rank))]) + if not self._newly_complete: + Text(ba.Lstr(value='${A}${B}', + subs=[('${A}', + ba.Lstr(resource='newPersonalBestText')), + ('${B}', was_string)]) if new_best else + ba.Lstr(resource='bestRatingText', + subs=[('${RATING}', str(best_rank))]), + position=(0, -165), + color=(1, 1, 1, 0.7), + flash=new_best, + h_align=Text.HAlign.CENTER, + transition=(Text.Transition.IN_RIGHT + if new_best else Text.Transition.FADE_IN), + scale=0.5, + transition_delay=1.0).autoretain() + + Text(ba.Lstr(value='${A}:', + subs=[('${A}', ba.Lstr(resource='ratingText'))]), + position=(0, 36), + maxwidth=300, + transition=Text.Transition.FADE_IN, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition_delay=0).autoretain() + + ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) + if not error: + ba.timer(0.35, ba.Call(ba.playsound, self.cymbal_sound)) + + def _show_fail(self) -> None: + ZoomText(ba.Lstr(resource='failText'), + maxwidth=300, + flash=False, + trail=True, + h_align='center', + tilt_translate=0.11, + position=(0, 40), + jitter=1.0).autoretain() + if self._fail_message is not None: + Text(self._fail_message, + h_align=Text.HAlign.CENTER, + position=(0, -130), + maxwidth=300, + color=(1, 1, 1, 0.5), + transition=Text.Transition.FADE_IN, + transition_delay=1.0).autoretain() + ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) + + def _show_score_val(self, offs_x: float) -> None: + assert self._score_type is not None + assert self._score is not None + ZoomText((str(self._score) if self._score_type == 'points' else + ba.timestring(self._score * 10, + timeformat=ba.TimeFormat.MILLISECONDS)), + maxwidth=300, + flash=True, + trail=True, + scale=1.0 if self._score_type == 'points' else 0.6, + h_align='center', + tilt_translate=0.11, + position=(190 + offs_x, 115), + jitter=1.0).autoretain() + Text(ba.Lstr( + value='${A}:', subs=[('${A}', ba.Lstr( + resource='finalScoreText'))]) if self._score_type == 'points' + else ba.Lstr(value='${A}:', + subs=[('${A}', ba.Lstr(resource='finalTimeText'))]), + maxwidth=300, + position=(0, 200), + transition=Text.Transition.FADE_IN, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition_delay=0).autoretain() + ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) diff --git a/dist/ba_data/python/bastd/activity/drawscore.py b/dist/ba_data/python/bastd/activity/drawscore.py new file mode 100644 index 0000000..c12b82b --- /dev/null +++ b/dist/ba_data/python/bastd/activity/drawscore.py @@ -0,0 +1,34 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the draw screen.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity +from bastd.actor.zoomtext import ZoomText + +if TYPE_CHECKING: + from typing import Any, Dict + + +class DrawScoreScreenActivity(MultiTeamScoreScreenActivity): + """Score screen shown after a draw.""" + + default_music = None # Awkward silence... + + def on_begin(self) -> None: + ba.set_analytics_screen('Draw Score Screen') + super().on_begin() + ZoomText(ba.Lstr(resource='drawText'), + position=(0, 0), + maxwidth=400, + shiftposition=(-220, 0), + shiftdelay=2.0, + flash=False, + trail=False, + jitter=1.0).autoretain() + ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) + self.show_player_scores(results=self.settings_raw.get('results', None)) diff --git a/dist/ba_data/python/bastd/activity/dualteamscore.py b/dist/ba_data/python/bastd/activity/dualteamscore.py new file mode 100644 index 0000000..b031bb8 --- /dev/null +++ b/dist/ba_data/python/bastd/activity/dualteamscore.py @@ -0,0 +1,127 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the end screen in dual-team mode.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity +from bastd.actor.zoomtext import ZoomText + +if TYPE_CHECKING: + from typing import Any, Dict + + +class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): + """Scorescreen between rounds of a dual-team session.""" + + def __init__(self, settings: dict): + super().__init__(settings=settings) + self._winner: ba.SessionTeam = settings['winner'] + assert isinstance(self._winner, ba.SessionTeam) + + def on_begin(self) -> None: + ba.set_analytics_screen('Teams Score Screen') + super().on_begin() + + height = 130 + active_team_count = len(self.teams) + vval = (height * active_team_count) / 2 - height / 2 + i = 0 + shift_time = 2.5 + + # Usually we say 'Best of 7', but if the language prefers we can say + # 'First to 4'. + session = self.session + assert isinstance(session, ba.MultiTeamSession) + if ba.app.lang.get_resource('bestOfUseFirstToInstead'): + best_txt = ba.Lstr(resource='firstToSeriesText', + subs=[('${COUNT}', + str(session.get_series_length() / 2 + 1)) + ]) + else: + best_txt = ba.Lstr(resource='bestOfSeriesText', + subs=[('${COUNT}', + str(session.get_series_length()))]) + + ZoomText(best_txt, + position=(0, 175), + shiftposition=(-250, 175), + shiftdelay=2.5, + flash=False, + trail=False, + h_align='center', + scale=0.25, + color=(0.5, 0.5, 0.5, 1.0), + jitter=3.0).autoretain() + for team in self.session.sessionteams: + ba.timer( + i * 0.15 + 0.15, + ba.WeakCall(self._show_team_name, vval - i * height, team, + i * 0.2, shift_time - (i * 0.150 + 0.150))) + ba.timer(i * 0.150 + 0.5, + ba.Call(ba.playsound, self._score_display_sound_small)) + scored = (team is self._winner) + delay = 0.2 + if scored: + delay = 1.2 + ba.timer( + i * 0.150 + 0.2, + ba.WeakCall(self._show_team_old_score, vval - i * height, + team, shift_time - (i * 0.15 + 0.2))) + ba.timer(i * 0.15 + 1.5, + ba.Call(ba.playsound, self._score_display_sound)) + + ba.timer( + i * 0.150 + delay, + ba.WeakCall(self._show_team_score, vval - i * height, team, + scored, i * 0.2 + 0.1, + shift_time - (i * 0.15 + delay))) + i += 1 + self.show_player_scores() + + def _show_team_name(self, pos_v: float, team: ba.SessionTeam, + kill_delay: float, shiftdelay: float) -> None: + del kill_delay # Unused arg. + ZoomText(ba.Lstr(value='${A}:', subs=[('${A}', team.name)]), + position=(100, pos_v), + shiftposition=(-150, pos_v), + shiftdelay=shiftdelay, + flash=False, + trail=False, + h_align='right', + maxwidth=300, + color=team.color, + jitter=1.0).autoretain() + + def _show_team_old_score(self, pos_v: float, sessionteam: ba.SessionTeam, + shiftdelay: float) -> None: + ZoomText(str(sessionteam.customdata['score'] - 1), + position=(150, pos_v), + maxwidth=100, + color=(0.6, 0.6, 0.7), + shiftposition=(-100, pos_v), + shiftdelay=shiftdelay, + flash=False, + trail=False, + lifespan=1.0, + h_align='left', + jitter=1.0).autoretain() + + def _show_team_score(self, pos_v: float, sessionteam: ba.SessionTeam, + scored: bool, kill_delay: float, + shiftdelay: float) -> None: + del kill_delay # Unused arg. + ZoomText(str(sessionteam.customdata['score']), + position=(150, pos_v), + maxwidth=100, + color=(1.0, 0.9, 0.5) if scored else (0.6, 0.6, 0.7), + shiftposition=(-100, pos_v), + shiftdelay=shiftdelay, + flash=scored, + trail=scored, + h_align='left', + jitter=1.0, + trailcolor=(1, 0.8, 0.0, 0)).autoretain() diff --git a/dist/ba_data/python/bastd/activity/freeforallvictory.py b/dist/ba_data/python/bastd/activity/freeforallvictory.py new file mode 100644 index 0000000..1015058 --- /dev/null +++ b/dist/ba_data/python/bastd/activity/freeforallvictory.py @@ -0,0 +1,266 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the final screen in free-for-all games.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Set, Tuple + + +class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): + """Score screen shown at after free-for-all rounds.""" + + def __init__(self, settings: dict): + super().__init__(settings=settings) + + # Keep prev activity alive while we fade in. + self.transition_time = 0.5 + self._cymbal_sound = ba.getsound('cymbal') + + def on_begin(self) -> None: + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + from bastd.actor.text import Text + from bastd.actor.image import Image + ba.set_analytics_screen('FreeForAll Score Screen') + super().on_begin() + + y_base = 100.0 + ts_h_offs = -305.0 + tdelay = 1.0 + scale = 1.2 + spacing = 37.0 + + # We include name and previous score in the sort to reduce the amount + # of random jumping around the list we do in cases of ties. + player_order_prev = list(self.players) + player_order_prev.sort( + reverse=True, + key=lambda p: ( + p.team.sessionteam.customdata['previous_score'], + p.getname(full=True), + )) + player_order = list(self.players) + player_order.sort(reverse=True, + key=lambda p: ( + p.team.sessionteam.customdata['score'], + p.team.sessionteam.customdata['score'], + p.getname(full=True), + )) + + v_offs = -74.0 + spacing * len(player_order_prev) * 0.5 + delay1 = 1.3 + 0.1 + delay2 = 2.9 + 0.1 + delay3 = 2.9 + 0.1 + order_change = player_order != player_order_prev + + if order_change: + delay3 += 1.5 + + ba.timer(0.3, ba.Call(ba.playsound, self._score_display_sound)) + results = self.settings_raw['results'] + assert isinstance(results, ba.GameResults) + self.show_player_scores(delay=0.001, + results=results, + scale=1.2, + x_offset=-110.0) + + sound_times: Set[float] = set() + + def _scoretxt(text: str, + x_offs: float, + y_offs: float, + highlight: bool, + delay: float, + extrascale: float, + flash: bool = False) -> Text: + return Text(text, + position=(ts_h_offs + x_offs * scale, + y_base + (y_offs + v_offs + 2.0) * scale), + scale=scale * extrascale, + color=((1.0, 0.7, 0.3, 1.0) if highlight else + (0.7, 0.7, 0.7, 0.7)), + h_align=Text.HAlign.RIGHT, + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay + delay, + flash=flash).autoretain() + + v_offs -= spacing + slide_amt = 0.0 + transtime = 0.250 + transtime2 = 0.250 + + session = self.session + assert isinstance(session, ba.FreeForAllSession) + title = Text(ba.Lstr(resource='firstToSeriesText', + subs=[('${COUNT}', + str(session.get_ffa_series_length()))]), + scale=1.05 * scale, + position=(ts_h_offs - 0.0 * scale, + y_base + (v_offs + 50.0) * scale), + h_align=Text.HAlign.CENTER, + color=(0.5, 0.5, 0.5, 0.5), + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay).autoretain() + + v_offs -= 25 + v_offs_start = v_offs + + ba.timer( + tdelay + delay3, + ba.WeakCall( + self._safe_animate, title.position_combine, 'input0', { + 0.0: ts_h_offs - 0.0 * scale, + transtime2: ts_h_offs - (0.0 + slide_amt) * scale + })) + + for i, player in enumerate(player_order_prev): + v_offs_2 = v_offs_start - spacing * (player_order.index(player)) + ba.timer(tdelay + 0.3, + ba.Call(ba.playsound, self._score_display_sound_small)) + if order_change: + ba.timer(tdelay + delay2 + 0.1, + ba.Call(ba.playsound, self._cymbal_sound)) + img = Image(player.get_icon(), + position=(ts_h_offs - 72.0 * scale, + y_base + (v_offs + 15.0) * scale), + scale=(30.0 * scale, 30.0 * scale), + transition=Image.Transition.IN_LEFT, + transition_delay=tdelay).autoretain() + ba.timer( + tdelay + delay2, + ba.WeakCall( + self._safe_animate, img.position_combine, 'input1', { + 0: y_base + (v_offs + 15.0) * scale, + transtime: y_base + (v_offs_2 + 15.0) * scale + })) + ba.timer( + tdelay + delay3, + ba.WeakCall( + self._safe_animate, img.position_combine, 'input0', { + 0: ts_h_offs - 72.0 * scale, + transtime2: ts_h_offs - (72.0 + slide_amt) * scale + })) + txt = Text(ba.Lstr(value=player.getname(full=True)), + maxwidth=130.0, + scale=0.75 * scale, + position=(ts_h_offs - 50.0 * scale, + y_base + (v_offs + 15.0) * scale), + h_align=Text.HAlign.LEFT, + v_align=Text.VAlign.CENTER, + color=ba.safecolor(player.team.color + (1, )), + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay).autoretain() + ba.timer( + tdelay + delay2, + ba.WeakCall( + self._safe_animate, txt.position_combine, 'input1', { + 0: y_base + (v_offs + 15.0) * scale, + transtime: y_base + (v_offs_2 + 15.0) * scale + })) + ba.timer( + tdelay + delay3, + ba.WeakCall( + self._safe_animate, txt.position_combine, 'input0', { + 0: ts_h_offs - 50.0 * scale, + transtime2: ts_h_offs - (50.0 + slide_amt) * scale + })) + + txt_num = Text('#' + str(i + 1), + scale=0.55 * scale, + position=(ts_h_offs - 95.0 * scale, + y_base + (v_offs + 8.0) * scale), + h_align=Text.HAlign.RIGHT, + color=(0.6, 0.6, 0.6, 0.6), + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay).autoretain() + ba.timer( + tdelay + delay3, + ba.WeakCall( + self._safe_animate, txt_num.position_combine, 'input0', { + 0: ts_h_offs - 95.0 * scale, + transtime2: ts_h_offs - (95.0 + slide_amt) * scale + })) + + s_txt = _scoretxt( + str(player.team.sessionteam.customdata['previous_score']), 80, + 0, False, 0, 1.0) + ba.timer( + tdelay + delay2, + ba.WeakCall( + self._safe_animate, s_txt.position_combine, 'input1', { + 0: y_base + (v_offs + 2.0) * scale, + transtime: y_base + (v_offs_2 + 2.0) * scale + })) + ba.timer( + tdelay + delay3, + ba.WeakCall( + self._safe_animate, s_txt.position_combine, 'input0', { + 0: ts_h_offs + 80.0 * scale, + transtime2: ts_h_offs + (80.0 - slide_amt) * scale + })) + + score_change = ( + player.team.sessionteam.customdata['score'] - + player.team.sessionteam.customdata['previous_score']) + if score_change > 0: + xval = 113 + yval = 3.0 + s_txt_2 = _scoretxt('+' + str(score_change), + xval, + yval, + True, + 0, + 0.7, + flash=True) + ba.timer( + tdelay + delay2, + ba.WeakCall( + self._safe_animate, s_txt_2.position_combine, 'input1', + { + 0: y_base + (v_offs + yval + 2.0) * scale, + transtime: y_base + (v_offs_2 + yval + 2.0) * scale + })) + ba.timer( + tdelay + delay3, + ba.WeakCall( + self._safe_animate, s_txt_2.position_combine, 'input0', + { + 0: ts_h_offs + xval * scale, + transtime2: ts_h_offs + (xval - slide_amt) * scale + })) + + def _safesetattr(node: Optional[ba.Node], attr: str, + value: Any) -> None: + if node: + setattr(node, attr, value) + + ba.timer( + tdelay + delay1, + ba.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1))) + for j in range(score_change): + ba.timer((tdelay + delay1 + 0.15 * j), + ba.Call( + _safesetattr, s_txt.node, 'text', + str(player.team.sessionteam. + customdata['previous_score'] + j + 1))) + tfin = tdelay + delay1 + 0.15 * j + if tfin not in sound_times: + sound_times.add(tfin) + ba.timer( + tfin, + ba.Call(ba.playsound, + self._score_display_sound_small)) + v_offs -= spacing + + def _safe_animate(self, node: Optional[ba.Node], attr: str, + keys: Dict[float, float]) -> None: + """Run an animation on a node if the node still exists.""" + if node: + ba.animate(node, attr, keys) diff --git a/dist/ba_data/python/bastd/activity/multiteamjoin.py b/dist/ba_data/python/bastd/activity/multiteamjoin.py new file mode 100644 index 0000000..9c27859 --- /dev/null +++ b/dist/ba_data/python/bastd/activity/multiteamjoin.py @@ -0,0 +1,80 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the join screen for multi-team sessions.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from ba.internal import JoinActivity +from bastd.actor.text import Text + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + + +class MultiTeamJoinActivity(JoinActivity): + """Join screen for teams sessions.""" + + def __init__(self, settings: dict): + super().__init__(settings) + self._next_up_text: Optional[Text] = None + + def on_transition_in(self) -> None: + from bastd.actor.controlsguide import ControlsGuide + from ba import DualTeamSession + super().on_transition_in() + ControlsGuide(delay=1.0).autoretain() + + session = self.session + assert isinstance(session, ba.MultiTeamSession) + + # Show info about the next up game. + self._next_up_text = Text(ba.Lstr( + value='${1} ${2}', + subs=[('${1}', ba.Lstr(resource='upFirstText')), + ('${2}', session.get_next_game_description())]), + h_attach=Text.HAttach.CENTER, + scale=0.7, + v_attach=Text.VAttach.TOP, + h_align=Text.HAlign.CENTER, + position=(0, -70), + flash=False, + color=(0.5, 0.5, 0.5, 1.0), + transition=Text.Transition.FADE_IN, + transition_delay=5.0) + + # In teams mode, show our two team names. + # FIXME: Lobby should handle this. + if isinstance(ba.getsession(), DualTeamSession): + team_names = [team.name for team in ba.getsession().sessionteams] + team_colors = [ + tuple(team.color) + (0.5, ) + for team in ba.getsession().sessionteams + ] + if len(team_names) == 2: + for i in range(2): + Text(team_names[i], + scale=0.7, + h_attach=Text.HAttach.CENTER, + v_attach=Text.VAttach.TOP, + h_align=Text.HAlign.CENTER, + position=(-200 + 350 * i, -100), + color=team_colors[i], + transition=Text.Transition.FADE_IN).autoretain() + + Text(ba.Lstr(resource='mustInviteFriendsText', + subs=[('${GATHER}', + ba.Lstr(resource='gatherWindow.titleText'))]), + h_attach=Text.HAttach.CENTER, + scale=0.8, + host_only=True, + v_attach=Text.VAttach.CENTER, + h_align=Text.HAlign.CENTER, + position=(0, 0), + flash=False, + color=(0, 1, 0, 1.0), + transition=Text.Transition.FADE_IN, + transition_delay=2.0, + transition_out_delay=7.0).autoretain() diff --git a/dist/ba_data/python/bastd/activity/multiteamscore.py b/dist/ba_data/python/bastd/activity/multiteamscore.py new file mode 100644 index 0000000..ba87a61 --- /dev/null +++ b/dist/ba_data/python/bastd/activity/multiteamscore.py @@ -0,0 +1,212 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to teams mode score screen.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from ba.internal import ScoreScreenActivity +from bastd.actor.text import Text +from bastd.actor.image import Image + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + +class MultiTeamScoreScreenActivity(ScoreScreenActivity): + """Base class for score screens.""" + + def __init__(self, settings: dict): + super().__init__(settings=settings) + self._score_display_sound = ba.getsound('scoreHit01') + self._score_display_sound_small = ba.getsound('scoreHit02') + + self._show_up_next: bool = True + + def on_begin(self) -> None: + super().on_begin() + session = self.session + if self._show_up_next and isinstance(session, ba.MultiTeamSession): + txt = ba.Lstr(value='${A} ${B}', + subs=[ + ('${A}', + ba.Lstr(resource='upNextText', + subs=[ + ('${COUNT}', + str(session.get_game_number() + 1)) + ])), + ('${B}', session.get_next_game_description()) + ]) + Text(txt, + maxwidth=900, + h_attach=Text.HAttach.CENTER, + v_attach=Text.VAttach.BOTTOM, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + position=(0, 53), + flash=False, + color=(0.3, 0.3, 0.35, 1.0), + transition=Text.Transition.FADE_IN, + transition_delay=2.0).autoretain() + + def show_player_scores(self, + delay: float = 2.5, + results: Optional[ba.GameResults] = None, + scale: float = 1.0, + x_offset: float = 0.0, + y_offset: float = 0.0) -> None: + """Show scores for individual players.""" + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + + ts_v_offset = 150.0 + y_offset + ts_h_offs = 80.0 + x_offset + tdelay = delay + spacing = 40 + + is_free_for_all = isinstance(self.session, ba.FreeForAllSession) + + def _get_prec_score(p_rec: ba.PlayerRecord) -> Optional[int]: + if is_free_for_all and results is not None: + assert isinstance(results, ba.GameResults) + assert p_rec.team.activityteam is not None + val = results.get_sessionteam_score(p_rec.team) + return val + return p_rec.accumscore + + def _get_prec_score_str(p_rec: ba.PlayerRecord) -> Union[str, ba.Lstr]: + if is_free_for_all and results is not None: + assert isinstance(results, ba.GameResults) + assert p_rec.team.activityteam is not None + val = results.get_sessionteam_score_str(p_rec.team) + assert val is not None + return val + return str(p_rec.accumscore) + + # stats.get_records() can return players that are no longer in + # the game.. if we're using results we have to filter those out + # (since they're not in results and that's where we pull their + # scores from) + if results is not None: + assert isinstance(results, ba.GameResults) + player_records = [] + assert self.stats + valid_players = list(self.stats.get_records().items()) + + def _get_player_score_set_entry( + player: ba.SessionPlayer) -> Optional[ba.PlayerRecord]: + for p_rec in valid_players: + if p_rec[1].player is player: + return p_rec[1] + return None + + # Results is already sorted; just convert it into a list of + # score-set-entries. + for winnergroup in results.winnergroups: + for team in winnergroup.teams: + if len(team.players) == 1: + player_entry = _get_player_score_set_entry( + team.players[0]) + if player_entry is not None: + player_records.append(player_entry) + else: + player_records = [] + player_records_scores = [ + (_get_prec_score(p), name, p) + for name, p in list(self.stats.get_records().items()) + ] + player_records_scores.sort(reverse=True) + + # Just want living player entries. + player_records = [p[2] for p in player_records_scores if p[2]] + + voffs = -140.0 + spacing * len(player_records) * 0.5 + + def _txt(xoffs: float, + yoffs: float, + text: ba.Lstr, + h_align: Text.HAlign = Text.HAlign.RIGHT, + extrascale: float = 1.0, + maxwidth: Optional[float] = 120.0) -> None: + Text(text, + color=(0.5, 0.5, 0.6, 0.5), + position=(ts_h_offs + xoffs * scale, + ts_v_offset + (voffs + yoffs + 4.0) * scale), + h_align=h_align, + v_align=Text.VAlign.CENTER, + scale=0.8 * scale * extrascale, + maxwidth=maxwidth, + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay).autoretain() + + session = self.session + assert isinstance(session, ba.MultiTeamSession) + tval = ba.Lstr(resource='gameLeadersText', + subs=[('${COUNT}', str(session.get_game_number()))]) + _txt(180, + 43, + tval, + h_align=Text.HAlign.CENTER, + extrascale=1.4, + maxwidth=None) + _txt(-15, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT) + _txt(180, 4, ba.Lstr(resource='killsText')) + _txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100) + + score_label = 'Score' if results is None else results.score_label + translated = ba.Lstr(translate=('scoreNames', score_label)) + + _txt(390, 0, translated) + + topkillcount = 0 + topkilledcount = 99999 + top_score = 0 if not player_records else _get_prec_score( + player_records[0]) + + for prec in player_records: + topkillcount = max(topkillcount, prec.accum_kill_count) + topkilledcount = min(topkilledcount, prec.accum_killed_count) + + def _scoretxt(text: Union[str, ba.Lstr], + x_offs: float, + highlight: bool, + delay2: float, + maxwidth: float = 70.0) -> None: + Text(text, + position=(ts_h_offs + x_offs * scale, + ts_v_offset + (voffs + 15) * scale), + scale=scale, + color=(1.0, 0.9, 0.5, 1.0) if highlight else + (0.5, 0.5, 0.6, 0.5), + h_align=Text.HAlign.RIGHT, + v_align=Text.VAlign.CENTER, + maxwidth=maxwidth, + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay + delay2).autoretain() + + for playerrec in player_records: + tdelay += 0.05 + voffs -= spacing + Image(playerrec.get_icon(), + position=(ts_h_offs - 12 * scale, + ts_v_offset + (voffs + 15.0) * scale), + scale=(30.0 * scale, 30.0 * scale), + transition=Image.Transition.IN_LEFT, + transition_delay=tdelay).autoretain() + Text(ba.Lstr(value=playerrec.getname(full=True)), + maxwidth=160, + scale=0.75 * scale, + position=(ts_h_offs + 10.0 * scale, + ts_v_offset + (voffs + 15) * scale), + h_align=Text.HAlign.LEFT, + v_align=Text.VAlign.CENTER, + color=ba.safecolor(playerrec.team.color + (1, )), + transition=Text.Transition.IN_LEFT, + transition_delay=tdelay).autoretain() + _scoretxt(str(playerrec.accum_kill_count), 180, + playerrec.accum_kill_count == topkillcount, 0.1) + _scoretxt(str(playerrec.accum_killed_count), 280, + playerrec.accum_killed_count == topkilledcount, 0.1) + _scoretxt(_get_prec_score_str(playerrec), 390, + _get_prec_score(playerrec) == top_score, 0.2) diff --git a/dist/ba_data/python/bastd/activity/multiteamvictory.py b/dist/ba_data/python/bastd/activity/multiteamvictory.py new file mode 100644 index 0000000..9acc784 --- /dev/null +++ b/dist/ba_data/python/bastd/activity/multiteamvictory.py @@ -0,0 +1,376 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the final screen in multi-teams sessions.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity + +if TYPE_CHECKING: + from typing import Any, Dict, List, Tuple, Optional + + +class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): + """Final score screen for a team series.""" + + # Dont' play music by default; (we do manually after a delay). + default_music = None + + def __init__(self, settings: dict): + super().__init__(settings=settings) + self._min_view_time = 15.0 + self._is_ffa = isinstance(self.session, ba.FreeForAllSession) + self._allow_server_transition = True + self._tips_text = None + self._default_show_tips = False + + def on_begin(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + from bastd.actor.text import Text + from bastd.actor.image import Image + ba.set_analytics_screen('FreeForAll Series Victory Screen' if self. + _is_ffa else 'Teams Series Victory Screen') + if ba.app.ui.uiscale is ba.UIScale.LARGE: + sval = ba.Lstr(resource='pressAnyKeyButtonPlayAgainText') + else: + sval = ba.Lstr(resource='pressAnyButtonPlayAgainText') + self._show_up_next = False + self._custom_continue_message = sval + super().on_begin() + winning_sessionteam = self.settings_raw['winner'] + + # Pause a moment before playing victory music. + ba.timer(0.6, ba.WeakCall(self._play_victory_music)) + ba.timer(4.4, + ba.WeakCall(self._show_winner, self.settings_raw['winner'])) + ba.timer(4.6, ba.Call(ba.playsound, self._score_display_sound)) + + # Score / Name / Player-record. + player_entries: List[Tuple[int, str, ba.PlayerRecord]] = [] + + # Note: for ffa, exclude players who haven't entered the game yet. + if self._is_ffa: + for _pkey, prec in self.stats.get_records().items(): + if prec.player.in_game: + player_entries.append( + (prec.player.sessionteam.customdata['score'], + prec.getname(full=True), prec)) + player_entries.sort(reverse=True, key=lambda x: x[0]) + else: + for _pkey, prec in self.stats.get_records().items(): + player_entries.append((prec.score, prec.name_full, prec)) + player_entries.sort(reverse=True, key=lambda x: x[0]) + + ts_height = 300.0 + ts_h_offs = -390.0 + tval = 6.4 + t_incr = 0.12 + + always_use_first_to = ba.app.lang.get_resource( + 'bestOfUseFirstToInstead') + + session = self.session + if self._is_ffa: + assert isinstance(session, ba.FreeForAllSession) + txt = ba.Lstr( + value='${A}:', + subs=[('${A}', + ba.Lstr(resource='firstToFinalText', + subs=[('${COUNT}', + str(session.get_ffa_series_length()))])) + ]) + else: + assert isinstance(session, ba.MultiTeamSession) + + # Some languages may prefer to always show 'first to X' instead of + # 'best of X'. + # FIXME: This will affect all clients connected to us even if + # they're not using this language. Should try to come up + # with a wording that works everywhere. + if always_use_first_to: + txt = ba.Lstr( + value='${A}:', + subs=[ + ('${A}', + ba.Lstr(resource='firstToFinalText', + subs=[ + ('${COUNT}', + str(session.get_series_length() / 2 + 1)) + ])) + ]) + else: + txt = ba.Lstr( + value='${A}:', + subs=[('${A}', + ba.Lstr(resource='bestOfFinalText', + subs=[('${COUNT}', + str(session.get_series_length()))])) + ]) + + Text(txt, + v_align=Text.VAlign.CENTER, + maxwidth=300, + color=(0.5, 0.5, 0.5, 1.0), + position=(0, 220), + scale=1.2, + transition=Text.Transition.IN_TOP_SLOW, + h_align=Text.HAlign.CENTER, + transition_delay=t_incr * 4).autoretain() + + win_score = (session.get_series_length() - 1) // 2 + 1 + lose_score = 0 + for team in self.teams: + if team.sessionteam.customdata['score'] != win_score: + lose_score = team.sessionteam.customdata['score'] + + if not self._is_ffa: + Text(ba.Lstr(resource='gamesToText', + subs=[('${WINCOUNT}', str(win_score)), + ('${LOSECOUNT}', str(lose_score))]), + color=(0.5, 0.5, 0.5, 1.0), + maxwidth=160, + v_align=Text.VAlign.CENTER, + position=(0, -215), + scale=1.8, + transition=Text.Transition.IN_LEFT, + h_align=Text.HAlign.CENTER, + transition_delay=4.8 + t_incr * 4).autoretain() + + if self._is_ffa: + v_extra = 120 + else: + v_extra = 0 + + mvp: Optional[ba.PlayerRecord] = None + mvp_name: Optional[str] = None + + # Show game MVP. + if not self._is_ffa: + mvp, mvp_name = None, None + for entry in player_entries: + if entry[2].team == winning_sessionteam: + mvp = entry[2] + mvp_name = entry[1] + break + if mvp is not None: + Text(ba.Lstr(resource='mostValuablePlayerText'), + color=(0.5, 0.5, 0.5, 1.0), + v_align=Text.VAlign.CENTER, + maxwidth=300, + position=(180, ts_height / 2 + 15), + transition=Text.Transition.IN_LEFT, + h_align=Text.HAlign.LEFT, + transition_delay=tval).autoretain() + tval += 4 * t_incr + + Image(mvp.get_icon(), + position=(230, ts_height / 2 - 55 + 14 - 5), + scale=(70, 70), + transition=Image.Transition.IN_LEFT, + transition_delay=tval).autoretain() + assert mvp_name is not None + Text(ba.Lstr(value=mvp_name), + position=(280, ts_height / 2 - 55 + 15 - 5), + h_align=Text.HAlign.LEFT, + v_align=Text.VAlign.CENTER, + maxwidth=170, + scale=1.3, + color=ba.safecolor(mvp.team.color + (1, )), + transition=Text.Transition.IN_LEFT, + transition_delay=tval).autoretain() + tval += 4 * t_incr + + # Most violent. + most_kills = 0 + for entry in player_entries: + if entry[2].kill_count >= most_kills: + mvp = entry[2] + mvp_name = entry[1] + most_kills = entry[2].kill_count + if mvp is not None: + Text(ba.Lstr(resource='mostViolentPlayerText'), + color=(0.5, 0.5, 0.5, 1.0), + v_align=Text.VAlign.CENTER, + maxwidth=300, + position=(180, ts_height / 2 - 150 + v_extra + 15), + transition=Text.Transition.IN_LEFT, + h_align=Text.HAlign.LEFT, + transition_delay=tval).autoretain() + Text(ba.Lstr(value='(${A})', + subs=[('${A}', + ba.Lstr(resource='killsTallyText', + subs=[('${COUNT}', str(most_kills))])) + ]), + position=(260, ts_height / 2 - 150 - 15 + v_extra), + color=(0.3, 0.3, 0.3, 1.0), + scale=0.6, + h_align=Text.HAlign.LEFT, + transition=Text.Transition.IN_LEFT, + transition_delay=tval).autoretain() + tval += 4 * t_incr + + Image(mvp.get_icon(), + position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra), + scale=(50, 50), + transition=Image.Transition.IN_LEFT, + transition_delay=tval).autoretain() + assert mvp_name is not None + Text(ba.Lstr(value=mvp_name), + position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15), + h_align=Text.HAlign.LEFT, + v_align=Text.VAlign.CENTER, + maxwidth=180, + color=ba.safecolor(mvp.team.color + (1, )), + transition=Text.Transition.IN_LEFT, + transition_delay=tval).autoretain() + tval += 4 * t_incr + + # Most killed. + most_killed = 0 + mkp, mkp_name = None, None + for entry in player_entries: + if entry[2].killed_count >= most_killed: + mkp = entry[2] + mkp_name = entry[1] + most_killed = entry[2].killed_count + if mkp is not None: + Text(ba.Lstr(resource='mostViolatedPlayerText'), + color=(0.5, 0.5, 0.5, 1.0), + v_align=Text.VAlign.CENTER, + maxwidth=300, + position=(180, ts_height / 2 - 300 + v_extra + 15), + transition=Text.Transition.IN_LEFT, + h_align=Text.HAlign.LEFT, + transition_delay=tval).autoretain() + Text(ba.Lstr(value='(${A})', + subs=[('${A}', + ba.Lstr(resource='deathsTallyText', + subs=[('${COUNT}', str(most_killed))])) + ]), + position=(260, ts_height / 2 - 300 - 15 + v_extra), + h_align=Text.HAlign.LEFT, + scale=0.6, + color=(0.3, 0.3, 0.3, 1.0), + transition=Text.Transition.IN_LEFT, + transition_delay=tval).autoretain() + tval += 4 * t_incr + Image(mkp.get_icon(), + position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra), + scale=(50, 50), + transition=Image.Transition.IN_LEFT, + transition_delay=tval).autoretain() + assert mkp_name is not None + Text(ba.Lstr(value=mkp_name), + position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15), + h_align=Text.HAlign.LEFT, + v_align=Text.VAlign.CENTER, + color=ba.safecolor(mkp.team.color + (1, )), + maxwidth=180, + transition=Text.Transition.IN_LEFT, + transition_delay=tval).autoretain() + tval += 4 * t_incr + + # Now show individual scores. + tdelay = tval + Text(ba.Lstr(resource='finalScoresText'), + color=(0.5, 0.5, 0.5, 1.0), + position=(ts_h_offs, ts_height / 2), + transition=Text.Transition.IN_RIGHT, + transition_delay=tdelay).autoretain() + tdelay += 4 * t_incr + + v_offs = 0.0 + tdelay += len(player_entries) * 8 * t_incr + for _score, name, prec in player_entries: + tdelay -= 4 * t_incr + v_offs -= 40 + Text(str(prec.team.customdata['score']) + if self._is_ffa else str(prec.score), + color=(0.5, 0.5, 0.5, 1.0), + position=(ts_h_offs + 230, ts_height / 2 + v_offs), + h_align=Text.HAlign.RIGHT, + transition=Text.Transition.IN_RIGHT, + transition_delay=tdelay).autoretain() + tdelay -= 4 * t_incr + + Image(prec.get_icon(), + position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), + scale=(30, 30), + transition=Image.Transition.IN_LEFT, + transition_delay=tdelay).autoretain() + Text(ba.Lstr(value=name), + position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), + h_align=Text.HAlign.LEFT, + v_align=Text.VAlign.CENTER, + maxwidth=180, + color=ba.safecolor(prec.team.color + (1, )), + transition=Text.Transition.IN_RIGHT, + transition_delay=tdelay).autoretain() + + ba.timer(15.0, ba.WeakCall(self._show_tips)) + + def _show_tips(self) -> None: + from bastd.actor.tipstext import TipsText + self._tips_text = TipsText(offs_y=70) + + def _play_victory_music(self) -> None: + + # Make sure we don't stomp on the next activity's music choice. + if not self.is_transitioning_out(): + ba.setmusic(ba.MusicType.VICTORY) + + def _show_winner(self, team: ba.SessionTeam) -> None: + from bastd.actor.image import Image + from bastd.actor.zoomtext import ZoomText + if not self._is_ffa: + offs_v = 0.0 + ZoomText(team.name, + position=(0, 97), + color=team.color, + scale=1.15, + jitter=1.0, + maxwidth=250).autoretain() + else: + offs_v = -80.0 + if len(team.players) == 1: + i = Image(team.players[0].get_icon(), + position=(0, 143), + scale=(100, 100)).autoretain() + assert i.node + ba.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) + ZoomText(ba.Lstr( + value=team.players[0].getname(full=True, icon=False)), + position=(0, 97 + offs_v), + color=team.color, + scale=1.15, + jitter=1.0, + maxwidth=250).autoretain() + + s_extra = 1.0 if self._is_ffa else 1.0 + + # Some languages say "FOO WINS" differently for teams vs players. + if isinstance(self.session, ba.FreeForAllSession): + wins_resource = 'seriesWinLine1PlayerText' + else: + wins_resource = 'seriesWinLine1TeamText' + wins_text = ba.Lstr(resource=wins_resource) + + # Temp - if these come up as the english default, fall-back to the + # unified old form which is more likely to be translated. + ZoomText(wins_text, + position=(0, -10 + offs_v), + color=team.color, + scale=0.65 * s_extra, + jitter=1.0, + maxwidth=250).autoretain() + ZoomText(ba.Lstr(resource='seriesWinLine2Text'), + position=(0, -110 + offs_v), + scale=1.0 * s_extra, + color=team.color, + jitter=1.0, + maxwidth=250).autoretain() diff --git a/dist/ba_data/python/bastd/actor/__init__.py b/dist/ba_data/python/bastd/actor/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/actor/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..6da5c86 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/background.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/background.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6535a63 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/background.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/background.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/background.cpython-38.pyc new file mode 100644 index 0000000..56d8306 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/background.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/bomb.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/bomb.cpython-38.opt-1.pyc new file mode 100644 index 0000000..0a61b32 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/bomb.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/bomb.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/bomb.cpython-38.pyc new file mode 100644 index 0000000..f06c2c9 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/bomb.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-38.opt-1.pyc new file mode 100644 index 0000000..2995a15 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-38.pyc new file mode 100644 index 0000000..93988b0 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/flag.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/flag.cpython-38.opt-1.pyc new file mode 100644 index 0000000..cf81de8 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/flag.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/flag.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/flag.cpython-38.pyc new file mode 100644 index 0000000..9242d8e Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/flag.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/image.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/image.cpython-38.opt-1.pyc new file mode 100644 index 0000000..02cfd9c Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/image.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/image.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/image.cpython-38.pyc new file mode 100644 index 0000000..196f99a Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/image.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-38.opt-1.pyc new file mode 100644 index 0000000..3b341fa Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-38.pyc new file mode 100644 index 0000000..e0c7352 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4eb8a5b Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-38.pyc new file mode 100644 index 0000000..95a2fe4 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f0da456 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-38.pyc new file mode 100644 index 0000000..80127f3 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-38.opt-1.pyc new file mode 100644 index 0000000..138f317 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-38.pyc new file mode 100644 index 0000000..fa3aafe Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-38.opt-1.pyc new file mode 100644 index 0000000..26a50a2 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-38.pyc new file mode 100644 index 0000000..24108e4 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-38.opt-1.pyc new file mode 100644 index 0000000..5754cbe Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-38.pyc new file mode 100644 index 0000000..1b4f5aa Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-38.opt-1.pyc new file mode 100644 index 0000000..49ce4b6 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-38.pyc new file mode 100644 index 0000000..d61be52 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/spawner.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/spawner.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9a3e8b2 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/spawner.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/spaz.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/spaz.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c528ddb Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/spaz.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/spaz.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/spaz.cpython-38.pyc new file mode 100644 index 0000000..656baaa Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/spaz.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d377f0a Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-38.pyc new file mode 100644 index 0000000..c5d4654 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9768920 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-38.pyc new file mode 100644 index 0000000..a14e31e Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-38.opt-1.pyc new file mode 100644 index 0000000..5183a92 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-38.pyc new file mode 100644 index 0000000..79d19e2 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/text.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/text.cpython-38.opt-1.pyc new file mode 100644 index 0000000..8a3ab71 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/text.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/text.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/text.cpython-38.pyc new file mode 100644 index 0000000..dff61a9 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/text.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-38.opt-1.pyc new file mode 100644 index 0000000..60e30f1 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-38.pyc new file mode 100644 index 0000000..eb01009 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-38.opt-1.pyc new file mode 100644 index 0000000..3f078f6 Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-38.pyc b/dist/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-38.pyc new file mode 100644 index 0000000..7a79d5d Binary files /dev/null and b/dist/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/actor/background.py b/dist/ba_data/python/bastd/actor/background.py new file mode 100644 index 0000000..f9c486b --- /dev/null +++ b/dist/ba_data/python/bastd/actor/background.py @@ -0,0 +1,139 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Actor(s).""" + +from __future__ import annotations + +import random +import weakref +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any + + +class Background(ba.Actor): + """Simple Fading Background Actor.""" + + def __init__(self, + fade_time: float = 0.5, + start_faded: bool = False, + show_logo: bool = False): + super().__init__() + self._dying = False + self.fade_time = fade_time + # We're special in that we create our node in the session + # scene instead of the activity scene. + # This way we can overlap multiple activities for fades + # and whatnot. + session = ba.getsession() + self._session = weakref.ref(session) + with ba.Context(session): + self.node = ba.newnode('image', + delegate=self, + attrs={ + 'fill_screen': True, + 'texture': ba.gettexture('bg'), + 'tilt_translate': -0.3, + 'has_alpha_channel': False, + 'color': (1, 1, 1) + }) + if not start_faded: + ba.animate(self.node, + 'opacity', { + 0.0: 0.0, + self.fade_time: 1.0 + }, + loop=False) + if show_logo: + logo_texture = ba.gettexture('logo') + logo_model = ba.getmodel('logo') + logo_model_transparent = ba.getmodel('logoTransparent') + self.logo = ba.newnode( + 'image', + owner=self.node, + attrs={ + 'texture': logo_texture, + 'model_opaque': logo_model, + 'model_transparent': logo_model_transparent, + 'scale': (0.7, 0.7), + 'vr_depth': -250, + 'color': (0.15, 0.15, 0.15), + 'position': (0, 0), + 'tilt_translate': -0.05, + 'absolute_scale': False + }) + self.node.connectattr('opacity', self.logo, 'opacity') + # add jitter/pulse for a stop-motion-y look unless we're in VR + # in which case stillness is better + if not ba.app.vr_mode: + self.cmb = ba.newnode('combine', + owner=self.node, + attrs={'size': 2}) + for attr in ['input0', 'input1']: + ba.animate(self.cmb, + attr, { + 0.0: 0.693, + 0.05: 0.7, + 0.5: 0.693 + }, + loop=True) + self.cmb.connectattr('output', self.logo, 'scale') + cmb = ba.newnode('combine', + owner=self.node, + attrs={'size': 2}) + cmb.connectattr('output', self.logo, 'position') + # Gen some random keys for that stop-motion-y look. + keys = {} + timeval = 0.0 + for _i in range(10): + keys[timeval] = (random.random() - 0.5) * 0.0015 + timeval += random.random() * 0.1 + ba.animate(cmb, 'input0', keys, loop=True) + keys = {} + timeval = 0.0 + for _i in range(10): + keys[timeval] = (random.random() - 0.5) * 0.0015 + 0.05 + timeval += random.random() * 0.1 + ba.animate(cmb, 'input1', keys, loop=True) + + def __del__(self) -> None: + # Normal actors don't get sent DieMessages when their + # activity is shutting down, but we still need to do so + # since our node lives in the session and it wouldn't die + # otherwise. + self._die() + super().__del__() + + def _die(self, immediate: bool = False) -> None: + session = self._session() + if session is None and self.node: + # If session is gone, our node should be too, + # since it was part of the session's scene. + # Let's make sure that's the case. + # (since otherwise we have no way to kill it) + ba.print_error('got None session on Background _die' + ' (and node still exists!)') + elif session is not None: + with ba.Context(session): + if not self._dying and self.node: + self._dying = True + if immediate: + self.node.delete() + else: + ba.animate(self.node, + 'opacity', { + 0.0: 1.0, + self.fade_time: 0.0 + }, + loop=False) + ba.timer(self.fade_time + 0.1, self.node.delete) + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, ba.DieMessage): + self._die(msg.immediate) + else: + super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/actor/bomb.py b/dist/ba_data/python/bastd/actor/bomb.py new file mode 100644 index 0000000..99d7dd1 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/bomb.py @@ -0,0 +1,1093 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Various classes for bombs, mines, tnt, etc.""" + +# FIXME +# pylint: disable=too-many-lines + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING, TypeVar + +import ba +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, Callable, List, Tuple, Type + +PlayerType = TypeVar('PlayerType', bound='ba.Player') + + +class BombFactory: + """Wraps up media and other resources used by ba.Bombs. + + category: Gameplay Classes + + A single instance of this is shared between all bombs + and can be retrieved via bastd.actor.bomb.get_factory(). + + Attributes: + + bomb_model + The ba.Model of a standard or ice bomb. + + sticky_bomb_model + The ba.Model of a sticky-bomb. + + impact_bomb_model + The ba.Model of an impact-bomb. + + land_mine_model + The ba.Model of a land-mine. + + tnt_model + The ba.Model of a tnt box. + + regular_tex + The ba.Texture for regular bombs. + + ice_tex + The ba.Texture for ice bombs. + + sticky_tex + The ba.Texture for sticky bombs. + + impact_tex + The ba.Texture for impact bombs. + + impact_lit_tex + The ba.Texture for impact bombs with lights lit. + + land_mine_tex + The ba.Texture for land-mines. + + land_mine_lit_tex + The ba.Texture for land-mines with the light lit. + + tnt_tex + The ba.Texture for tnt boxes. + + hiss_sound + The ba.Sound for the hiss sound an ice bomb makes. + + debris_fall_sound + The ba.Sound for random falling debris after an explosion. + + wood_debris_fall_sound + A ba.Sound for random wood debris falling after an explosion. + + explode_sounds + A tuple of ba.Sounds for explosions. + + freeze_sound + A ba.Sound of an ice bomb freezing something. + + fuse_sound + A ba.Sound of a burning fuse. + + activate_sound + A ba.Sound for an activating impact bomb. + + warn_sound + A ba.Sound for an impact bomb about to explode due to time-out. + + bomb_material + A ba.Material applied to all bombs. + + normal_sound_material + A ba.Material that generates standard bomb noises on impacts, etc. + + sticky_material + A ba.Material that makes 'splat' sounds and makes collisions softer. + + land_mine_no_explode_material + A ba.Material that keeps land-mines from blowing up. + Applied to land-mines when they are created to allow land-mines to + touch without exploding. + + land_mine_blast_material + A ba.Material applied to activated land-mines that causes them to + explode on impact. + + impact_blast_material + A ba.Material applied to activated impact-bombs that causes them to + explode on impact. + + blast_material + A ba.Material applied to bomb blast geometry which triggers impact + events with what it touches. + + dink_sounds + A tuple of ba.Sounds for when bombs hit the ground. + + sticky_impact_sound + The ba.Sound for a squish made by a sticky bomb hitting something. + + roll_sound + ba.Sound for a rolling bomb. + """ + + _STORENAME = ba.storagename() + + @classmethod + def get(cls) -> BombFactory: + """Get/create a shared bastd.actor.bomb.BombFactory object.""" + activity = ba.getactivity() + factory = activity.customdata.get(cls._STORENAME) + if factory is None: + factory = BombFactory() + activity.customdata[cls._STORENAME] = factory + assert isinstance(factory, BombFactory) + return factory + + def random_explode_sound(self) -> ba.Sound: + """Return a random explosion ba.Sound from the factory.""" + return self.explode_sounds[random.randrange(len(self.explode_sounds))] + + def __init__(self) -> None: + """Instantiate a BombFactory. + + You shouldn't need to do this; call bastd.actor.bomb.get_factory() + to get a shared instance. + """ + shared = SharedObjects.get() + + self.bomb_model = ba.getmodel('bomb') + self.sticky_bomb_model = ba.getmodel('bombSticky') + self.impact_bomb_model = ba.getmodel('impactBomb') + self.land_mine_model = ba.getmodel('landMine') + self.tnt_model = ba.getmodel('tnt') + + self.regular_tex = ba.gettexture('bombColor') + self.ice_tex = ba.gettexture('bombColorIce') + self.sticky_tex = ba.gettexture('bombStickyColor') + self.impact_tex = ba.gettexture('impactBombColor') + self.impact_lit_tex = ba.gettexture('impactBombColorLit') + self.land_mine_tex = ba.gettexture('landMine') + self.land_mine_lit_tex = ba.gettexture('landMineLit') + self.tnt_tex = ba.gettexture('tnt') + + self.hiss_sound = ba.getsound('hiss') + self.debris_fall_sound = ba.getsound('debrisFall') + self.wood_debris_fall_sound = ba.getsound('woodDebrisFall') + + self.explode_sounds = (ba.getsound('explosion01'), + ba.getsound('explosion02'), + ba.getsound('explosion03'), + ba.getsound('explosion04'), + ba.getsound('explosion05')) + + self.freeze_sound = ba.getsound('freeze') + self.fuse_sound = ba.getsound('fuse01') + self.activate_sound = ba.getsound('activateBeep') + self.warn_sound = ba.getsound('warnBeep') + + # Set up our material so new bombs don't collide with objects + # that they are initially overlapping. + self.bomb_material = ba.Material() + self.normal_sound_material = ba.Material() + self.sticky_material = ba.Material() + + self.bomb_material.add_actions( + conditions=( + ( + ('we_are_younger_than', 100), + 'or', + ('they_are_younger_than', 100), + ), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + + # We want pickup materials to always hit us even if we're currently + # not colliding with their node. (generally due to the above rule) + self.bomb_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=('modify_part_collision', 'use_node_collide', False), + ) + + self.bomb_material.add_actions(actions=('modify_part_collision', + 'friction', 0.3)) + + self.land_mine_no_explode_material = ba.Material() + self.land_mine_blast_material = ba.Material() + self.land_mine_blast_material.add_actions( + conditions=( + ('we_are_older_than', 200), + 'and', + ('they_are_older_than', 200), + 'and', + ('eval_colliding', ), + 'and', + ( + ('they_dont_have_material', + self.land_mine_no_explode_material), + 'and', + ( + ('they_have_material', shared.object_material), + 'or', + ('they_have_material', shared.player_material), + ), + ), + ), + actions=('message', 'our_node', 'at_connect', ImpactMessage()), + ) + + self.impact_blast_material = ba.Material() + self.impact_blast_material.add_actions( + conditions=( + ('we_are_older_than', 200), + 'and', + ('they_are_older_than', 200), + 'and', + ('eval_colliding', ), + 'and', + ( + ('they_have_material', shared.footing_material), + 'or', + ('they_have_material', shared.object_material), + ), + ), + actions=('message', 'our_node', 'at_connect', ImpactMessage()), + ) + + self.blast_material = ba.Material() + self.blast_material.add_actions( + conditions=('they_have_material', shared.object_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('message', 'our_node', 'at_connect', ExplodeHitMessage()), + ), + ) + + self.dink_sounds = (ba.getsound('bombDrop01'), + ba.getsound('bombDrop02')) + self.sticky_impact_sound = ba.getsound('stickyImpact') + self.roll_sound = ba.getsound('bombRoll01') + + # Collision sounds. + self.normal_sound_material.add_actions( + conditions=('they_have_material', shared.footing_material), + actions=( + ('impact_sound', self.dink_sounds, 2, 0.8), + ('roll_sound', self.roll_sound, 3, 6), + )) + + self.sticky_material.add_actions(actions=(('modify_part_collision', + 'stiffness', 0.1), + ('modify_part_collision', + 'damping', 1.0))) + + self.sticky_material.add_actions( + conditions=( + ('they_have_material', shared.player_material), + 'or', + ('they_have_material', shared.footing_material), + ), + actions=('message', 'our_node', 'at_connect', SplatMessage()), + ) + + +class SplatMessage: + """Tells an object to make a splat noise.""" + + +class ExplodeMessage: + """Tells an object to explode.""" + + +class ImpactMessage: + """Tell an object it touched something.""" + + +class ArmMessage: + """Tell an object to become armed.""" + + +class WarnMessage: + """Tell an object to issue a warning sound.""" + + +class ExplodeHitMessage: + """Tell an object it was hit by an explosion.""" + + +class Blast(ba.Actor): + """An explosion, as generated by a bomb or some other object. + + category: Gameplay Classes + """ + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + blast_radius: float = 2.0, + blast_type: str = 'normal', + source_player: ba.Player = None, + hit_type: str = 'explosion', + hit_subtype: str = 'normal'): + """Instantiate with given values.""" + + # bah; get off my lawn! + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + + super().__init__() + + shared = SharedObjects.get() + factory = BombFactory.get() + + self.blast_type = blast_type + self._source_player = source_player + self.hit_type = hit_type + self.hit_subtype = hit_subtype + self.radius = blast_radius + + # Set our position a bit lower so we throw more things upward. + rmats = (factory.blast_material, shared.attack_material) + self.node = ba.newnode( + 'region', + delegate=self, + attrs={ + 'position': (position[0], position[1] - 0.1, position[2]), + 'scale': (self.radius, self.radius, self.radius), + 'type': 'sphere', + 'materials': rmats + }, + ) + + ba.timer(0.05, self.node.delete) + + # Throw in an explosion and flash. + evel = (velocity[0], max(-1.0, velocity[1]), velocity[2]) + explosion = ba.newnode('explosion', + attrs={ + 'position': position, + 'velocity': evel, + 'radius': self.radius, + 'big': (self.blast_type == 'tnt') + }) + if self.blast_type == 'ice': + explosion.color = (0, 0.05, 0.4) + + ba.timer(1.0, explosion.delete) + + if self.blast_type != 'ice': + ba.emitfx(position=position, + velocity=velocity, + count=int(1.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='thin_smoke') + ba.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 4), + emit_type='tendrils', + tendril_type='ice' if self.blast_type == 'ice' else 'smoke') + ba.emitfx(position=position, + emit_type='distortion', + spread=1.0 if self.blast_type == 'tnt' else 2.0) + + # And emit some shrapnel. + if self.blast_type == 'ice': + + def emit() -> None: + ba.emitfx(position=position, + velocity=velocity, + count=30, + spread=2.0, + scale=0.4, + chunk_type='ice', + emit_type='stickers') + + # It looks better if we delay a bit. + ba.timer(0.05, emit) + + elif self.blast_type == 'sticky': + + def emit() -> None: + ba.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + spread=0.7, + chunk_type='slime') + ba.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + spread=0.7, + chunk_type='slime') + ba.emitfx(position=position, + velocity=velocity, + count=15, + scale=0.6, + chunk_type='slime', + emit_type='stickers') + ba.emitfx(position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers') + ba.emitfx(position=position, + velocity=velocity, + count=int(6.0 + random.random() * 12), + scale=0.8, + spread=1.5, + chunk_type='spark') + + # It looks better if we delay a bit. + ba.timer(0.05, emit) + + elif self.blast_type == 'impact': + + def emit() -> None: + ba.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.8, + chunk_type='metal') + ba.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.4, + chunk_type='metal') + ba.emitfx(position=position, + velocity=velocity, + count=20, + scale=0.7, + chunk_type='spark', + emit_type='stickers') + ba.emitfx(position=position, + velocity=velocity, + count=int(8.0 + random.random() * 15), + scale=0.8, + spread=1.5, + chunk_type='spark') + + # It looks better if we delay a bit. + ba.timer(0.05, emit) + + else: # Regular or land mine bomb shrapnel. + + def emit() -> None: + if self.blast_type != 'tnt': + ba.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + chunk_type='rock') + ba.emitfx(position=position, + velocity=velocity, + count=int(4.0 + random.random() * 8), + scale=0.5, + chunk_type='rock') + ba.emitfx(position=position, + velocity=velocity, + count=30, + scale=1.0 if self.blast_type == 'tnt' else 0.7, + chunk_type='spark', + emit_type='stickers') + ba.emitfx(position=position, + velocity=velocity, + count=int(18.0 + random.random() * 20), + scale=1.0 if self.blast_type == 'tnt' else 0.8, + spread=1.5, + chunk_type='spark') + + # TNT throws splintery chunks. + if self.blast_type == 'tnt': + + def emit_splinters() -> None: + ba.emitfx(position=position, + velocity=velocity, + count=int(20.0 + random.random() * 25), + scale=0.8, + spread=1.0, + chunk_type='splinter') + + ba.timer(0.01, emit_splinters) + + # Every now and then do a sparky one. + if self.blast_type == 'tnt' or random.random() < 0.1: + + def emit_extra_sparks() -> None: + ba.emitfx(position=position, + velocity=velocity, + count=int(10.0 + random.random() * 20), + scale=0.8, + spread=1.5, + chunk_type='spark') + + ba.timer(0.02, emit_extra_sparks) + + # It looks better if we delay a bit. + ba.timer(0.05, emit) + + lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else + (1, 0.3, 0.1)) + light = ba.newnode('light', + attrs={ + 'position': position, + 'volume_intensity_scale': 10.0, + 'color': lcolor + }) + + scl = random.uniform(0.6, 0.9) + scorch_radius = light_radius = self.radius + if self.blast_type == 'tnt': + light_radius *= 1.4 + scorch_radius *= 1.15 + scl *= 3.0 + + iscale = 1.6 + ba.animate( + light, 'intensity', { + 0: 2.0 * iscale, + scl * 0.02: 0.1 * iscale, + scl * 0.025: 0.2 * iscale, + scl * 0.05: 17.0 * iscale, + scl * 0.06: 5.0 * iscale, + scl * 0.08: 4.0 * iscale, + scl * 0.2: 0.6 * iscale, + scl * 2.0: 0.00 * iscale, + scl * 3.0: 0.0 + }) + ba.animate( + light, 'radius', { + 0: light_radius * 0.2, + scl * 0.05: light_radius * 0.55, + scl * 0.1: light_radius * 0.3, + scl * 0.3: light_radius * 0.15, + scl * 1.0: light_radius * 0.05 + }) + ba.timer(scl * 3.0, light.delete) + + # Make a scorch that fades over time. + scorch = ba.newnode('scorch', + attrs={ + 'position': position, + 'size': scorch_radius * 0.5, + 'big': (self.blast_type == 'tnt') + }) + if self.blast_type == 'ice': + scorch.color = (1, 1, 1.5) + + ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + ba.timer(13.0, scorch.delete) + + if self.blast_type == 'ice': + ba.playsound(factory.hiss_sound, position=light.position) + + lpos = light.position + ba.playsound(factory.random_explode_sound(), position=lpos) + ba.playsound(factory.debris_fall_sound, position=lpos) + + ba.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) + + # TNT is more epic. + if self.blast_type == 'tnt': + ba.playsound(factory.random_explode_sound(), position=lpos) + + def _extra_boom() -> None: + ba.playsound(factory.random_explode_sound(), position=lpos) + + ba.timer(0.25, _extra_boom) + + def _extra_debris_sound() -> None: + ba.playsound(factory.debris_fall_sound, position=lpos) + ba.playsound(factory.wood_debris_fall_sound, position=lpos) + + ba.timer(0.4, _extra_debris_sound) + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ExplodeHitMessage): + node = ba.getcollision().opposingnode + assert self.node + nodepos = self.node.position + mag = 2000.0 + if self.blast_type == 'ice': + mag *= 0.5 + elif self.blast_type == 'land_mine': + mag *= 2.5 + elif self.blast_type == 'tnt': + mag *= 2.0 + + node.handlemessage( + ba.HitMessage(pos=nodepos, + velocity=(0, 0, 0), + magnitude=mag, + hit_type=self.hit_type, + hit_subtype=self.hit_subtype, + radius=self.radius, + source_player=ba.existing(self._source_player))) + if self.blast_type == 'ice': + ba.playsound(BombFactory.get().freeze_sound, + 10, + position=nodepos) + node.handlemessage(ba.FreezeMessage()) + + else: + return super().handlemessage(msg) + return None + + +class Bomb(ba.Actor): + """A standard bomb and its variants such as land-mines and tnt-boxes. + + category: Gameplay Classes + """ + + # Ew; should try to clean this up later. + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + bomb_type: str = 'normal', + blast_radius: float = 2.0, + bomb_scale: float = 1.0, + source_player: ba.Player = None, + owner: ba.Node = None): + """Create a new Bomb. + + bomb_type can be 'ice','impact','land_mine','normal','sticky', or + 'tnt'. Note that for impact or land_mine bombs you have to call arm() + before they will go off. + """ + super().__init__() + + shared = SharedObjects.get() + factory = BombFactory.get() + + if bomb_type not in ('ice', 'impact', 'land_mine', 'normal', 'sticky', + 'tnt'): + raise ValueError('invalid bomb type: ' + bomb_type) + self.bomb_type = bomb_type + + self._exploded = False + self.scale = bomb_scale + + self.texture_sequence: Optional[ba.Node] = None + + if self.bomb_type == 'sticky': + self._last_sticky_sound_time = 0.0 + + self.blast_radius = blast_radius + if self.bomb_type == 'ice': + self.blast_radius *= 1.2 + elif self.bomb_type == 'impact': + self.blast_radius *= 0.7 + elif self.bomb_type == 'land_mine': + self.blast_radius *= 0.7 + elif self.bomb_type == 'tnt': + self.blast_radius *= 1.45 + + self._explode_callbacks: List[Callable[[Bomb, Blast], Any]] = [] + + # The player this came from. + self._source_player = source_player + + # By default our hit type/subtype is our own, but we pick up types of + # whoever sets us off so we know what caused a chain reaction. + # UPDATE (July 2020): not inheriting hit-types anymore; this causes + # weird effects such as land-mines inheriting 'punch' hit types and + # then not being able to destroy certain things they normally could, + # etc. Inheriting owner/source-node from things that set us off + # should be all we need I think... + self.hit_type = 'explosion' + self.hit_subtype = self.bomb_type + + # The node this came from. + # FIXME: can we unify this and source_player? + self.owner = owner + + # Adding footing-materials to things can screw up jumping and flying + # since players carrying those things and thus touching footing + # objects will think they're on solid ground.. perhaps we don't + # wanna add this even in the tnt case? + materials: Tuple[ba.Material, ...] + if self.bomb_type == 'tnt': + materials = (factory.bomb_material, shared.footing_material, + shared.object_material) + else: + materials = (factory.bomb_material, shared.object_material) + + if self.bomb_type == 'impact': + materials = materials + (factory.impact_blast_material, ) + elif self.bomb_type == 'land_mine': + materials = materials + (factory.land_mine_no_explode_material, ) + + if self.bomb_type == 'sticky': + materials = materials + (factory.sticky_material, ) + else: + materials = materials + (factory.normal_sound_material, ) + + if self.bomb_type == 'land_mine': + fuse_time = None + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'model': factory.land_mine_model, + 'light_model': factory.land_mine_model, + 'body': 'landMine', + 'body_scale': self.scale, + 'shadow_size': 0.44, + 'color_texture': factory.land_mine_tex, + 'reflection': 'powerup', + 'reflection_scale': [1.0], + 'materials': materials + }) + + elif self.bomb_type == 'tnt': + fuse_time = None + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'model': factory.tnt_model, + 'light_model': factory.tnt_model, + 'body': 'crate', + 'body_scale': self.scale, + 'shadow_size': 0.5, + 'color_texture': factory.tnt_tex, + 'reflection': 'soft', + 'reflection_scale': [0.23], + 'materials': materials + }) + + elif self.bomb_type == 'impact': + fuse_time = 20.0 + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'body': 'sphere', + 'body_scale': self.scale, + 'model': factory.impact_bomb_model, + 'shadow_size': 0.3, + 'color_texture': factory.impact_tex, + 'reflection': 'powerup', + 'reflection_scale': [1.5], + 'materials': materials + }) + self.arm_timer = ba.Timer( + 0.2, ba.WeakCall(self.handlemessage, ArmMessage())) + self.warn_timer = ba.Timer( + fuse_time - 1.7, ba.WeakCall(self.handlemessage, + WarnMessage())) + + else: + fuse_time = 3.0 + if self.bomb_type == 'sticky': + sticky = True + model = factory.sticky_bomb_model + rtype = 'sharper' + rscale = 1.8 + else: + sticky = False + model = factory.bomb_model + rtype = 'sharper' + rscale = 1.8 + if self.bomb_type == 'ice': + tex = factory.ice_tex + elif self.bomb_type == 'sticky': + tex = factory.sticky_tex + else: + tex = factory.regular_tex + self.node = ba.newnode('bomb', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'model': model, + 'body_scale': self.scale, + 'shadow_size': 0.3, + 'color_texture': tex, + 'sticky': sticky, + 'owner': owner, + 'reflection': rtype, + 'reflection_scale': [rscale], + 'materials': materials + }) + + sound = ba.newnode('sound', + owner=self.node, + attrs={ + 'sound': factory.fuse_sound, + 'volume': 0.25 + }) + self.node.connectattr('position', sound, 'position') + ba.animate(self.node, 'fuse_length', {0.0: 1.0, fuse_time: 0.0}) + + # Light the fuse!!! + if self.bomb_type not in ('land_mine', 'tnt'): + assert fuse_time is not None + ba.timer(fuse_time, + ba.WeakCall(self.handlemessage, ExplodeMessage())) + + ba.animate(self.node, 'model_scale', { + 0: 0, + 0.2: 1.3 * self.scale, + 0.26: self.scale + }) + + def get_source_player( + self, playertype: Type[PlayerType]) -> Optional[PlayerType]: + """Return the source-player if one exists and is the provided type.""" + player: Any = self._source_player + return (player if isinstance(player, playertype) and player.exists() + else None) + + def on_expire(self) -> None: + super().on_expire() + + # Release callbacks/refs so we don't wind up with dependency loops. + self._explode_callbacks = [] + + def _handle_die(self) -> None: + if self.node: + self.node.delete() + + def _handle_oob(self) -> None: + self.handlemessage(ba.DieMessage()) + + def _handle_impact(self) -> None: + node = ba.getcollision().opposingnode + + # If we're an impact bomb and we came from this node, don't explode. + # (otherwise we blow up on our own head when jumping). + # Alternately if we're hitting another impact-bomb from the same + # source, don't explode. (can cause accidental explosions if rapidly + # throwing/etc.) + node_delegate = node.getdelegate(object) + if node: + if (self.bomb_type == 'impact' and + (node is self.owner or + (isinstance(node_delegate, Bomb) and node_delegate.bomb_type + == 'impact' and node_delegate.owner is self.owner))): + return + self.handlemessage(ExplodeMessage()) + + def _handle_dropped(self) -> None: + if self.bomb_type == 'land_mine': + self.arm_timer = ba.Timer( + 1.25, ba.WeakCall(self.handlemessage, ArmMessage())) + + # Once we've thrown a sticky bomb we can stick to it. + elif self.bomb_type == 'sticky': + + def _setsticky(node: ba.Node) -> None: + if node: + node.stick_to_owner = True + + ba.timer(0.25, lambda: _setsticky(self.node)) + + def _handle_splat(self) -> None: + node = ba.getcollision().opposingnode + if (node is not self.owner + and ba.time() - self._last_sticky_sound_time > 1.0): + self._last_sticky_sound_time = ba.time() + assert self.node + ba.playsound(BombFactory.get().sticky_impact_sound, + 2.0, + position=self.node.position) + + def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None: + """Add a call to be run when the bomb has exploded. + + The bomb and the new blast object are passed as arguments. + """ + self._explode_callbacks.append(call) + + def explode(self) -> None: + """Blows up the bomb if it has not yet done so.""" + if self._exploded: + return + self._exploded = True + if self.node: + blast = Blast(position=self.node.position, + velocity=self.node.velocity, + blast_radius=self.blast_radius, + blast_type=self.bomb_type, + source_player=ba.existing(self._source_player), + hit_type=self.hit_type, + hit_subtype=self.hit_subtype).autoretain() + for callback in self._explode_callbacks: + callback(self, blast) + + # We blew up so we need to go away. + # NOTE TO SELF: do we actually need this delay? + ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage())) + + def _handle_warn(self) -> None: + if self.texture_sequence and self.node: + self.texture_sequence.rate = 30 + ba.playsound(BombFactory.get().warn_sound, + 0.5, + position=self.node.position) + + def _add_material(self, material: ba.Material) -> None: + if not self.node: + return + materials = self.node.materials + if material not in materials: + assert isinstance(materials, tuple) + self.node.materials = materials + (material, ) + + def arm(self) -> None: + """Arm the bomb (for land-mines and impact-bombs). + + These types of bombs will not explode until they have been armed. + """ + if not self.node: + return + factory = BombFactory.get() + intex: Sequence[ba.Texture] + if self.bomb_type == 'land_mine': + intex = (factory.land_mine_lit_tex, factory.land_mine_tex) + self.texture_sequence = ba.newnode('texture_sequence', + owner=self.node, + attrs={ + 'rate': 30, + 'input_textures': intex + }) + ba.timer(0.5, self.texture_sequence.delete) + + # We now make it explodable. + ba.timer( + 0.25, + ba.WeakCall(self._add_material, + factory.land_mine_blast_material)) + elif self.bomb_type == 'impact': + intex = (factory.impact_lit_tex, factory.impact_tex, + factory.impact_tex) + self.texture_sequence = ba.newnode('texture_sequence', + owner=self.node, + attrs={ + 'rate': 100, + 'input_textures': intex + }) + ba.timer( + 0.25, + ba.WeakCall(self._add_material, + factory.land_mine_blast_material)) + else: + raise Exception('arm() should only be called ' + 'on land-mines or impact bombs') + self.texture_sequence.connectattr('output_texture', self.node, + 'color_texture') + ba.playsound(factory.activate_sound, 0.5, position=self.node.position) + + def _handle_hit(self, msg: ba.HitMessage) -> None: + ispunched = (msg.srcnode and msg.srcnode.getnodetype() == 'spaz') + + # Normal bombs are triggered by non-punch impacts; + # impact-bombs by all impacts. + if (not self._exploded and + (not ispunched or self.bomb_type in ['impact', 'land_mine'])): + + # Also lets change the owner of the bomb to whoever is setting + # us off. (this way points for big chain reactions go to the + # person causing them). + source_player = msg.get_source_player(ba.Player) + if source_player is not None: + self._source_player = source_player + + # Also inherit the hit type (if a landmine sets off by a bomb, + # the credit should go to the mine) + # the exception is TNT. TNT always gets credit. + # UPDATE (July 2020): not doing this anymore. Causes too much + # weird logic such as bombs acting like punches. Holler if + # anything is noticeably broken due to this. + # if self.bomb_type != 'tnt': + # self.hit_type = msg.hit_type + # self.hit_subtype = msg.hit_subtype + + ba.timer(0.1 + random.random() * 0.1, + ba.WeakCall(self.handlemessage, ExplodeMessage())) + assert self.node + self.node.handlemessage('impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], + msg.velocity[2], msg.magnitude, + msg.velocity_magnitude, msg.radius, 0, + msg.velocity[0], msg.velocity[1], + msg.velocity[2]) + + if msg.srcnode: + pass + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ExplodeMessage): + self.explode() + elif isinstance(msg, ImpactMessage): + self._handle_impact() + # Ok the logic below looks like it was backwards to me. + # Disabling for now; can bring back if need be. + # elif isinstance(msg, ba.PickedUpMessage): + # # Change our source to whoever just picked us up *only* if it + # # is None. This way we can get points for killing bots with their + # # own bombs. Hmm would there be a downside to this? + # if self._source_player is not None: + # self._source_player = msg.node.source_player + elif isinstance(msg, SplatMessage): + self._handle_splat() + elif isinstance(msg, ba.DroppedMessage): + self._handle_dropped() + elif isinstance(msg, ba.HitMessage): + self._handle_hit(msg) + elif isinstance(msg, ba.DieMessage): + self._handle_die() + elif isinstance(msg, ba.OutOfBoundsMessage): + self._handle_oob() + elif isinstance(msg, ArmMessage): + self.arm() + elif isinstance(msg, WarnMessage): + self._handle_warn() + else: + super().handlemessage(msg) + + +class TNTSpawner: + """Regenerates TNT at a given point in space every now and then. + + category: Gameplay Classes + """ + + def __init__(self, position: Sequence[float], respawn_time: float = 20.0): + """Instantiate with given position and respawn_time (in seconds).""" + self._position = position + self._tnt: Optional[Bomb] = None + self._respawn_time = random.uniform(0.8, 1.2) * respawn_time + self._wait_time = 0.0 + self._update() + + # Go with slightly more than 1 second to avoid timer stacking. + self._update_timer = ba.Timer(1.1, + ba.WeakCall(self._update), + repeat=True) + + def _update(self) -> None: + tnt_alive = self._tnt is not None and self._tnt.node + if not tnt_alive: + # Respawn if its been long enough.. otherwise just increment our + # how-long-since-we-died value. + if self._tnt is None or self._wait_time >= self._respawn_time: + self._tnt = Bomb(position=self._position, bomb_type='tnt') + self._wait_time = 0.0 + else: + self._wait_time += 1.1 diff --git a/dist/ba_data/python/bastd/actor/controlsguide.py b/dist/ba_data/python/bastd/actor/controlsguide.py new file mode 100644 index 0000000..8f284a2 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/controlsguide.py @@ -0,0 +1,475 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Actors related to controls guides.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Tuple, Optional, Sequence, Union + + +class ControlsGuide(ba.Actor): + """A screen overlay of game controls. + + category: Gameplay Classes + + Shows button mappings based on what controllers are connected. + Handy to show at the start of a series or whenever there might + be newbies watching. + """ + + def __init__(self, + position: Tuple[float, float] = (390.0, 120.0), + scale: float = 1.0, + delay: float = 0.0, + lifespan: float = None, + bright: bool = False): + """Instantiate an overlay. + + delay: is the time in seconds before the overlay fades in. + + lifespan: if not None, the overlay will fade back out and die after + that long (in milliseconds). + + bright: if True, brighter colors will be used; handy when showing + over gameplay but may be too bright for join-screens, etc. + """ + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + super().__init__() + show_title = True + scale *= 0.75 + image_size = 90.0 * scale + offs = 74.0 * scale + offs5 = 43.0 * scale + ouya = False + maxw = 50 + self._lifespan = lifespan + self._dead = False + self._bright = bright + self._cancel_timer: Optional[ba.Timer] = None + self._fade_in_timer: Optional[ba.Timer] = None + self._update_timer: Optional[ba.Timer] = None + self._title_text: Optional[ba.Node] + clr: Sequence[float] + if show_title: + self._title_text_pos_top = (position[0], + position[1] + 139.0 * scale) + self._title_text_pos_bottom = (position[0], + position[1] + 139.0 * scale) + clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) + tval = ba.Lstr(value='${A}:', + subs=[('${A}', ba.Lstr(resource='controlsText'))]) + self._title_text = ba.newnode('text', + attrs={ + 'text': tval, + 'host_only': True, + 'scale': 1.1 * scale, + 'shadow': 0.5, + 'flatness': 1.0, + 'maxwidth': 480, + 'v_align': 'center', + 'h_align': 'center', + 'color': clr + }) + else: + self._title_text = None + pos = (position[0], position[1] - offs) + clr = (0.4, 1, 0.4) + self._jump_image = ba.newnode( + 'image', + attrs={ + 'texture': ba.gettexture('buttonJump'), + 'absolute_scale': True, + 'host_only': True, + 'vr_depth': 10, + 'position': pos, + 'scale': (image_size, image_size), + 'color': clr + }) + self._jump_text = ba.newnode('text', + attrs={ + 'v_align': 'top', + 'h_align': 'center', + 'scale': 1.5 * scale, + 'flatness': 1.0, + 'host_only': True, + 'shadow': 1.0, + 'maxwidth': maxw, + 'position': (pos[0], pos[1] - offs5), + 'color': clr + }) + pos = (position[0] - offs * 1.1, position[1]) + clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3) + self._punch_image = ba.newnode( + 'image', + attrs={ + 'texture': ba.gettexture('buttonPunch'), + 'absolute_scale': True, + 'host_only': True, + 'vr_depth': 10, + 'position': pos, + 'scale': (image_size, image_size), + 'color': clr + }) + self._punch_text = ba.newnode('text', + attrs={ + 'v_align': 'top', + 'h_align': 'center', + 'scale': 1.5 * scale, + 'flatness': 1.0, + 'host_only': True, + 'shadow': 1.0, + 'maxwidth': maxw, + 'position': (pos[0], pos[1] - offs5), + 'color': clr + }) + pos = (position[0] + offs * 1.1, position[1]) + clr = (1, 0.3, 0.3) + self._bomb_image = ba.newnode( + 'image', + attrs={ + 'texture': ba.gettexture('buttonBomb'), + 'absolute_scale': True, + 'host_only': True, + 'vr_depth': 10, + 'position': pos, + 'scale': (image_size, image_size), + 'color': clr + }) + self._bomb_text = ba.newnode('text', + attrs={ + 'h_align': 'center', + 'v_align': 'top', + 'scale': 1.5 * scale, + 'flatness': 1.0, + 'host_only': True, + 'shadow': 1.0, + 'maxwidth': maxw, + 'position': (pos[0], pos[1] - offs5), + 'color': clr + }) + pos = (position[0], position[1] + offs) + clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1) + self._pickup_image = ba.newnode( + 'image', + attrs={ + 'texture': ba.gettexture('buttonPickUp'), + 'absolute_scale': True, + 'host_only': True, + 'vr_depth': 10, + 'position': pos, + 'scale': (image_size, image_size), + 'color': clr + }) + self._pick_up_text = ba.newnode('text', + attrs={ + 'v_align': 'top', + 'h_align': 'center', + 'scale': 1.5 * scale, + 'flatness': 1.0, + 'host_only': True, + 'shadow': 1.0, + 'maxwidth': maxw, + 'position': + (pos[0], pos[1] - offs5), + 'color': clr + }) + clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) + self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) + self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) + sval = (1.0 * scale if ba.app.vr_mode else 0.8 * scale) + self._run_text = ba.newnode( + 'text', + attrs={ + 'scale': sval, + 'host_only': True, + 'shadow': 1.0 if ba.app.vr_mode else 0.5, + 'flatness': 1.0, + 'maxwidth': 380, + 'v_align': 'top', + 'h_align': 'center', + 'color': clr + }) + clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) + self._extra_text = ba.newnode('text', + attrs={ + 'scale': 0.8 * scale, + 'host_only': True, + 'shadow': 0.5, + 'flatness': 1.0, + 'maxwidth': 380, + 'v_align': 'top', + 'h_align': 'center', + 'color': clr + }) + self._nodes = [ + self._bomb_image, self._bomb_text, self._punch_image, + self._punch_text, self._jump_image, self._jump_text, + self._pickup_image, self._pick_up_text, self._run_text, + self._extra_text + ] + if show_title: + assert self._title_text + self._nodes.append(self._title_text) + + # Start everything invisible. + for node in self._nodes: + node.opacity = 0.0 + + # Don't do anything until our delay has passed. + ba.timer(delay, ba.WeakCall(self._start_updating)) + + @staticmethod + def _meaningful_button_name(device: ba.InputDevice, button: int) -> str: + """Return a flattened string button name; empty for non-meaningful.""" + if not device.has_meaningful_button_names: + return '' + return device.get_button_name(button).evaluate() + + def _start_updating(self) -> None: + + # Ok, our delay has passed. Now lets periodically see if we can fade + # in (if a touch-screen is present we only want to show up if gamepads + # are connected, etc). + # Also set up a timer so if we haven't faded in by the end of our + # duration, abort. + if self._lifespan is not None: + self._cancel_timer = ba.Timer( + self._lifespan, + ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True))) + self._fade_in_timer = ba.Timer(1.0, + ba.WeakCall(self._check_fade_in), + repeat=True) + self._check_fade_in() # Do one check immediately. + + def _check_fade_in(self) -> None: + from ba.internal import get_device_value + + # If we have a touchscreen, we only fade in if we have a player with + # an input device that is *not* the touchscreen. + # (otherwise it is confusing to see the touchscreen buttons right + # next to our display buttons) + touchscreen: Optional[ba.InputDevice] = _ba.getinputdevice( + 'TouchScreen', '#1', doraise=False) + + if touchscreen is not None: + # We look at the session's players; not the activity's. + # We want to get ones who are still in the process of + # selecting a character, etc. + input_devices = [ + p.inputdevice for p in ba.getsession().sessionplayers + ] + input_devices = [ + i for i in input_devices if i and i is not touchscreen + ] + fade_in = False + if input_devices: + # Only count this one if it has non-empty button names + # (filters out wiimotes, the remote-app, etc). + for device in input_devices: + for name in ('buttonPunch', 'buttonJump', 'buttonBomb', + 'buttonPickUp'): + if self._meaningful_button_name( + device, get_device_value(device, name)) != '': + fade_in = True + break + if fade_in: + break # No need to keep looking. + else: + # No touch-screen; fade in immediately. + fade_in = True + if fade_in: + self._cancel_timer = None # Didn't need this. + self._fade_in_timer = None # Done with this. + self._fade_in() + + def _fade_in(self) -> None: + for node in self._nodes: + ba.animate(node, 'opacity', {0: 0.0, 2.0: 1.0}) + + # If we were given a lifespan, transition out after it. + if self._lifespan is not None: + ba.timer(self._lifespan, + ba.WeakCall(self.handlemessage, ba.DieMessage())) + self._update() + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + repeat=True) + + def _update(self) -> None: + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + from ba.internal import get_device_value, get_remote_app_name + if self._dead: + return + punch_button_names = set() + jump_button_names = set() + pickup_button_names = set() + bomb_button_names = set() + + # We look at the session's players; not the activity's - we want to + # get ones who are still in the process of selecting a character, etc. + input_devices = [p.inputdevice for p in ba.getsession().sessionplayers] + input_devices = [i for i in input_devices if i] + + # If there's no players with input devices yet, try to default to + # showing keyboard controls. + if not input_devices: + kbd = _ba.getinputdevice('Keyboard', '#1', doraise=False) + if kbd is not None: + input_devices.append(kbd) + + # We word things specially if we have nothing but keyboards. + all_keyboards = (input_devices + and all(i.name == 'Keyboard' for i in input_devices)) + only_remote = (len(input_devices) == 1 + and all(i.name == 'Amazon Fire TV Remote' + for i in input_devices)) + + right_button_names = set() + left_button_names = set() + up_button_names = set() + down_button_names = set() + + # For each player in the game with an input device, + # get the name of the button for each of these 4 actions. + # If any of them are uniform across all devices, display the name. + for device in input_devices: + # We only care about movement buttons in the case of keyboards. + if all_keyboards: + right_button_names.add( + device.get_button_name( + get_device_value(device, 'buttonRight'))) + left_button_names.add( + device.get_button_name( + get_device_value(device, 'buttonLeft'))) + down_button_names.add( + device.get_button_name( + get_device_value(device, 'buttonDown'))) + up_button_names.add( + device.get_button_name(get_device_value( + device, 'buttonUp'))) + + # Ignore empty values; things like the remote app or + # wiimotes can return these. + bname = self._meaningful_button_name( + device, get_device_value(device, 'buttonPunch')) + if bname != '': + punch_button_names.add(bname) + bname = self._meaningful_button_name( + device, get_device_value(device, 'buttonJump')) + if bname != '': + jump_button_names.add(bname) + bname = self._meaningful_button_name( + device, get_device_value(device, 'buttonBomb')) + if bname != '': + bomb_button_names.add(bname) + bname = self._meaningful_button_name( + device, get_device_value(device, 'buttonPickUp')) + if bname != '': + pickup_button_names.add(bname) + + # If we have no values yet, we may want to throw out some sane + # defaults. + if all(not lst for lst in (punch_button_names, jump_button_names, + bomb_button_names, pickup_button_names)): + # Otherwise on android show standard buttons. + if ba.app.platform == 'android': + punch_button_names.add('X') + jump_button_names.add('A') + bomb_button_names.add('B') + pickup_button_names.add('Y') + + run_text = ba.Lstr( + value='${R}: ${B}', + subs=[('${R}', ba.Lstr(resource='runText')), + ('${B}', + ba.Lstr(resource='holdAnyKeyText' + if all_keyboards else 'holdAnyButtonText'))]) + + # If we're all keyboards, lets show move keys too. + if (all_keyboards and len(up_button_names) == 1 + and len(down_button_names) == 1 and len(left_button_names) == 1 + and len(right_button_names) == 1): + up_text = list(up_button_names)[0] + down_text = list(down_button_names)[0] + left_text = list(left_button_names)[0] + right_text = list(right_button_names)[0] + run_text = ba.Lstr(value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}', + subs=[('${M}', ba.Lstr(resource='moveText')), + ('${U}', up_text), ('${L}', left_text), + ('${D}', down_text), ('${R}', right_text), + ('${RUN}', run_text)]) + + self._run_text.text = run_text + w_text: Union[ba.Lstr, str] + if only_remote and self._lifespan is None: + w_text = ba.Lstr(resource='fireTVRemoteWarningText', + subs=[('${REMOTE_APP_NAME}', + get_remote_app_name())]) + else: + w_text = '' + self._extra_text.text = w_text + if len(punch_button_names) == 1: + self._punch_text.text = list(punch_button_names)[0] + else: + self._punch_text.text = '' + + if len(jump_button_names) == 1: + tval = list(jump_button_names)[0] + else: + tval = '' + self._jump_text.text = tval + if tval == '': + self._run_text.position = self._run_text_pos_top + self._extra_text.position = (self._run_text_pos_top[0], + self._run_text_pos_top[1] - 50) + else: + self._run_text.position = self._run_text_pos_bottom + self._extra_text.position = (self._run_text_pos_bottom[0], + self._run_text_pos_bottom[1] - 50) + if len(bomb_button_names) == 1: + self._bomb_text.text = list(bomb_button_names)[0] + else: + self._bomb_text.text = '' + + # Also move our title up/down depending on if this is shown. + if len(pickup_button_names) == 1: + self._pick_up_text.text = list(pickup_button_names)[0] + if self._title_text is not None: + self._title_text.position = self._title_text_pos_top + else: + self._pick_up_text.text = '' + if self._title_text is not None: + self._title_text.position = self._title_text_pos_bottom + + def _die(self) -> None: + for node in self._nodes: + node.delete() + self._nodes = [] + self._update_timer = None + self._dead = True + + def exists(self) -> bool: + return not self._dead + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, ba.DieMessage): + if msg.immediate: + self._die() + else: + # If they don't need immediate, + # fade out our nodes and die later. + for node in self._nodes: + ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) + ba.timer(3.1, ba.WeakCall(self._die)) + return None + return super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/actor/flag.py b/dist/ba_data/python/bastd/actor/flag.py new file mode 100644 index 0000000..417a3d4 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/flag.py @@ -0,0 +1,366 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Implements a flag used for marking bases, capture-the-flag games, etc.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import ba +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional + + +class FlagFactory: + """Wraps up media and other resources used by ba.Flags. + + category: Gameplay Classes + + A single instance of this is shared between all flags + and can be retrieved via bastd.actor.flag.get_factory(). + + Attributes: + + flagmaterial + The ba.Material applied to all ba.Flags. + + impact_sound + The ba.Sound used when a ba.Flag hits the ground. + + skid_sound + The ba.Sound used when a ba.Flag skids along the ground. + + no_hit_material + A ba.Material that prevents contact with most objects; + applied to 'non-touchable' flags. + + flag_texture + The ba.Texture for flags. + """ + + _STORENAME = ba.storagename() + + def __init__(self) -> None: + """Instantiate a FlagFactory. + + You shouldn't need to do this; call bastd.actor.flag.get_factory() to + get a shared instance. + """ + shared = SharedObjects.get() + self.flagmaterial = ba.Material() + self.flagmaterial.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + + self.flagmaterial.add_actions( + conditions=( + 'they_have_material', + shared.footing_material, + ), + actions=( + ('message', 'our_node', 'at_connect', 'footing', 1), + ('message', 'our_node', 'at_disconnect', 'footing', -1), + ), + ) + + self.impact_sound = ba.getsound('metalHit') + self.skid_sound = ba.getsound('metalSkid') + self.flagmaterial.add_actions( + conditions=( + 'they_have_material', + shared.footing_material, + ), + actions=( + ('impact_sound', self.impact_sound, 2, 5), + ('skid_sound', self.skid_sound, 2, 5), + ), + ) + + self.no_hit_material = ba.Material() + self.no_hit_material.add_actions( + conditions=( + ('they_have_material', shared.pickup_material), + 'or', + ('they_have_material', shared.attack_material), + ), + actions=('modify_part_collision', 'collide', False), + ) + + # We also don't want anything moving it. + self.no_hit_material.add_actions( + conditions=( + ('they_have_material', shared.object_material), + 'or', + ('they_dont_have_material', shared.footing_material), + ), + actions=(('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False)), + ) + + self.flag_texture = ba.gettexture('flagColor') + + @classmethod + def get(cls) -> FlagFactory: + """Get/create a shared FlagFactory instance.""" + activity = ba.getactivity() + factory = activity.customdata.get(cls._STORENAME) + if factory is None: + factory = FlagFactory() + activity.customdata[cls._STORENAME] = factory + assert isinstance(factory, FlagFactory) + return factory + + +@dataclass +class FlagPickedUpMessage: + """A message saying a ba.Flag has been picked up. + + category: Message Classes + + Attributes: + + flag + The ba.Flag that has been picked up. + + node + The ba.Node doing the picking up. + """ + flag: Flag + node: ba.Node + + +@dataclass +class FlagDiedMessage: + """A message saying a ba.Flag has died. + + category: Message Classes + + Attributes: + + flag + The ba.Flag that died. + """ + flag: Flag + + +@dataclass +class FlagDroppedMessage: + """A message saying a ba.Flag has been dropped. + + category: Message Classes + + Attributes: + + flag + The ba.Flag that was dropped. + + node + The ba.Node that was holding it. + """ + flag: Flag + node: ba.Node + + +class Flag(ba.Actor): + """A flag; used in games such as capture-the-flag or king-of-the-hill. + + category: Gameplay Classes + + Can be stationary or carry-able by players. + """ + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + color: Sequence[float] = (1.0, 1.0, 1.0), + materials: Sequence[ba.Material] = None, + touchable: bool = True, + dropped_timeout: int = None): + """Instantiate a flag. + + If 'touchable' is False, the flag will only touch terrain; + useful for things like king-of-the-hill where players should + not be moving the flag around. + + 'materials can be a list of extra ba.Materials to apply to the flag. + + If 'dropped_timeout' is provided (in seconds), the flag will die + after remaining untouched for that long once it has been moved + from its initial position. + """ + + super().__init__() + + self._initial_position: Optional[Sequence[float]] = None + self._has_moved = False + shared = SharedObjects.get() + factory = FlagFactory.get() + + if materials is None: + materials = [] + elif not isinstance(materials, list): + # In case they passed a tuple or whatnot. + materials = list(materials) + if not touchable: + materials = [factory.no_hit_material] + materials + + finalmaterials = ([shared.object_material, factory.flagmaterial] + + materials) + self.node = ba.newnode('flag', + attrs={ + 'position': + (position[0], position[1] + 0.75, + position[2]), + 'color_texture': factory.flag_texture, + 'color': color, + 'materials': finalmaterials + }, + delegate=self) + + if dropped_timeout is not None: + dropped_timeout = int(dropped_timeout) + self._dropped_timeout = dropped_timeout + self._counter: Optional[ba.Node] + if self._dropped_timeout is not None: + self._count = self._dropped_timeout + self._tick_timer = ba.Timer(1.0, + call=ba.WeakCall(self._tick), + repeat=True) + self._counter = ba.newnode('text', + owner=self.node, + attrs={ + 'in_world': True, + 'color': (1, 1, 1, 0.7), + 'scale': 0.015, + 'shadow': 0.5, + 'flatness': 1.0, + 'h_align': 'center' + }) + else: + self._counter = None + + self._held_count = 0 + self._score_text: Optional[ba.Node] = None + self._score_text_hide_timer: Optional[ba.Timer] = None + + def _tick(self) -> None: + if self.node: + + # Grab our initial position after one tick (in case we fall). + if self._initial_position is None: + self._initial_position = self.node.position + + # Keep track of when we first move; we don't count down + # until then. + if not self._has_moved: + nodepos = self.node.position + if (max( + abs(nodepos[i] - self._initial_position[i]) + for i in list(range(3))) > 1.0): + self._has_moved = True + + if self._held_count > 0 or not self._has_moved: + assert self._dropped_timeout is not None + assert self._counter + self._count = self._dropped_timeout + self._counter.text = '' + else: + self._count -= 1 + if self._count <= 10: + nodepos = self.node.position + assert self._counter + self._counter.position = (nodepos[0], nodepos[1] + 1.3, + nodepos[2]) + self._counter.text = str(self._count) + if self._count < 1: + self.handlemessage(ba.DieMessage()) + else: + assert self._counter + self._counter.text = '' + + def _hide_score_text(self) -> None: + assert self._score_text is not None + assert isinstance(self._score_text.scale, float) + ba.animate(self._score_text, 'scale', { + 0: self._score_text.scale, + 0.2: 0 + }) + + def set_score_text(self, text: str) -> None: + """Show a message over the flag; handy for scores.""" + if not self.node: + return + if not self._score_text: + start_scale = 0.0 + math = ba.newnode('math', + owner=self.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + self.node.connectattr('position', math, 'input2') + self._score_text = ba.newnode('text', + owner=self.node, + attrs={ + 'text': text, + 'in_world': True, + 'scale': 0.02, + 'shadow': 0.5, + 'flatness': 1.0, + 'h_align': 'center' + }) + math.connectattr('output', self._score_text, 'position') + else: + assert isinstance(self._score_text.scale, float) + start_scale = self._score_text.scale + self._score_text.text = text + self._score_text.color = ba.safecolor(self.node.color) + ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) + self._score_text_hide_timer = ba.Timer( + 1.0, ba.WeakCall(self._hide_score_text)) + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + if not msg.immediate: + self.activity.handlemessage(FlagDiedMessage(self)) + elif isinstance(msg, ba.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], msg.magnitude, + msg.velocity_magnitude, msg.radius, 0, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + elif isinstance(msg, ba.PickedUpMessage): + self._held_count += 1 + if self._held_count == 1 and self._counter is not None: + self._counter.text = '' + self.activity.handlemessage(FlagPickedUpMessage(self, msg.node)) + elif isinstance(msg, ba.DroppedMessage): + self._held_count -= 1 + if self._held_count < 0: + print('Flag held count < 0.') + self._held_count = 0 + self.activity.handlemessage(FlagDroppedMessage(self, msg.node)) + else: + super().handlemessage(msg) + + @staticmethod + def project_stand(pos: Sequence[float]) -> None: + """Project a flag-stand onto the ground at the given position. + + Useful for games such as capture-the-flag to show where a + movable flag originated from. + """ + assert len(pos) == 3 + ba.emitfx(position=pos, emit_type='flag_stand') diff --git a/dist/ba_data/python/bastd/actor/image.py b/dist/ba_data/python/bastd/actor/image.py new file mode 100644 index 0000000..2db08a1 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/image.py @@ -0,0 +1,171 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Actor(s).""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, Tuple, Sequence, Union, Dict, Optional + + +class Image(ba.Actor): + """Just a wrapped up image node with a few tricks up its sleeve.""" + + class Transition(Enum): + """Transition types we support.""" + FADE_IN = 'fade_in' + IN_RIGHT = 'in_right' + IN_LEFT = 'in_left' + IN_BOTTOM = 'in_bottom' + IN_BOTTOM_SLOW = 'in_bottom_slow' + IN_TOP_SLOW = 'in_top_slow' + + class Attach(Enum): + """Attach types we support.""" + CENTER = 'center' + TOP_CENTER = 'topCenter' + TOP_LEFT = 'topLeft' + BOTTOM_CENTER = 'bottomCenter' + + def __init__(self, + texture: Union[ba.Texture, Dict[str, Any]], + position: Tuple[float, float] = (0, 0), + transition: Optional[Transition] = None, + transition_delay: float = 0.0, + attach: Attach = Attach.CENTER, + color: Sequence[float] = (1.0, 1.0, 1.0, 1.0), + scale: Tuple[float, float] = (100.0, 100.0), + transition_out_delay: float = None, + model_opaque: ba.Model = None, + model_transparent: ba.Model = None, + vr_depth: float = 0.0, + host_only: bool = False, + front: bool = False): + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + super().__init__() + + # If they provided a dict as texture, assume its an icon. + # otherwise its just a texture value itself. + mask_texture: Optional[ba.Texture] + if isinstance(texture, dict): + tint_color = texture['tint_color'] + tint2_color = texture['tint2_color'] + tint_texture = texture['tint_texture'] + texture = texture['texture'] + mask_texture = ba.gettexture('characterIconMask') + else: + tint_color = (1, 1, 1) + tint2_color = None + tint_texture = None + mask_texture = None + + self.node = ba.newnode('image', + attrs={ + 'texture': texture, + 'tint_color': tint_color, + 'tint_texture': tint_texture, + 'position': position, + 'vr_depth': vr_depth, + 'scale': scale, + 'mask_texture': mask_texture, + 'color': color, + 'absolute_scale': True, + 'host_only': host_only, + 'front': front, + 'attach': attach.value + }, + delegate=self) + + if model_opaque is not None: + self.node.model_opaque = model_opaque + if model_transparent is not None: + self.node.model_transparent = model_transparent + if tint2_color is not None: + self.node.tint2_color = tint2_color + if transition is self.Transition.FADE_IN: + keys = {transition_delay: 0, transition_delay + 0.5: color[3]} + if transition_out_delay is not None: + keys[transition_delay + transition_out_delay] = color[3] + keys[transition_delay + transition_out_delay + 0.5] = 0 + ba.animate(self.node, 'opacity', keys) + cmb = self.position_combine = ba.newnode('combine', + owner=self.node, + attrs={'size': 2}) + if transition is self.Transition.IN_RIGHT: + keys = { + transition_delay: position[0] + 1200, + transition_delay + 0.2: position[0] + } + o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0} + ba.animate(cmb, 'input0', keys) + cmb.input1 = position[1] + ba.animate(self.node, 'opacity', o_keys) + elif transition is self.Transition.IN_LEFT: + keys = { + transition_delay: position[0] - 1200, + transition_delay + 0.2: position[0] + } + o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0} + if transition_out_delay is not None: + keys[transition_delay + transition_out_delay] = position[0] + keys[transition_delay + transition_out_delay + + 200] = -position[0] - 1200 + o_keys[transition_delay + transition_out_delay + 0.15] = 1.0 + o_keys[transition_delay + transition_out_delay + 0.2] = 0.0 + ba.animate(cmb, 'input0', keys) + cmb.input1 = position[1] + ba.animate(self.node, 'opacity', o_keys) + elif transition is self.Transition.IN_BOTTOM_SLOW: + keys = { + transition_delay: -400, + transition_delay + 3.5: position[1] + } + o_keys = {transition_delay: 0.0, transition_delay + 2.0: 1.0} + cmb.input0 = position[0] + ba.animate(cmb, 'input1', keys) + ba.animate(self.node, 'opacity', o_keys) + elif transition is self.Transition.IN_BOTTOM: + keys = { + transition_delay: -400, + transition_delay + 0.2: position[1] + } + o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0} + if transition_out_delay is not None: + keys[transition_delay + transition_out_delay] = position[1] + keys[transition_delay + transition_out_delay + 0.2] = -400 + o_keys[transition_delay + transition_out_delay + 0.15] = 1.0 + o_keys[transition_delay + transition_out_delay + 0.2] = 0.0 + cmb.input0 = position[0] + ba.animate(cmb, 'input1', keys) + ba.animate(self.node, 'opacity', o_keys) + elif transition is self.Transition.IN_TOP_SLOW: + keys = {transition_delay: 400, transition_delay + 3.5: position[1]} + o_keys = {transition_delay: 0.0, transition_delay + 1.0: 1.0} + cmb.input0 = position[0] + ba.animate(cmb, 'input1', keys) + ba.animate(self.node, 'opacity', o_keys) + else: + assert transition is self.Transition.FADE_IN or transition is None + cmb.input0 = position[0] + cmb.input1 = position[1] + cmb.connectattr('output', self.node, 'position') + + # If we're transitioning out, die at the end of it. + if transition_out_delay is not None: + ba.timer(transition_delay + transition_out_delay + 1.0, + ba.WeakCall(self.handlemessage, ba.DieMessage())) + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + return None + return super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/actor/onscreencountdown.py b/dist/ba_data/python/bastd/actor/onscreencountdown.py new file mode 100644 index 0000000..a686cdf --- /dev/null +++ b/dist/ba_data/python/bastd/actor/onscreencountdown.py @@ -0,0 +1,102 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Actor Type(s).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + +class OnScreenCountdown(ba.Actor): + """A Handy On-Screen Timer. + + category: Gameplay Classes + + Useful for time-based games that count down to zero. + """ + + def __init__(self, duration: int, endcall: Callable[[], Any] = None): + """Duration is provided in seconds.""" + super().__init__() + self._timeremaining = duration + self._ended = False + self._endcall = endcall + self.node = ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -70), + 'scale': 1.4, + 'text': '' + }) + self.inputnode = ba.newnode('timedisplay', + attrs={ + 'time2': duration * 1000, + 'timemax': duration * 1000, + 'timemin': 0 + }) + self.inputnode.connectattr('output', self.node, 'text') + self._countdownsounds = { + 10: ba.getsound('announceTen'), + 9: ba.getsound('announceNine'), + 8: ba.getsound('announceEight'), + 7: ba.getsound('announceSeven'), + 6: ba.getsound('announceSix'), + 5: ba.getsound('announceFive'), + 4: ba.getsound('announceFour'), + 3: ba.getsound('announceThree'), + 2: ba.getsound('announceTwo'), + 1: ba.getsound('announceOne') + } + self._timer: Optional[ba.Timer] = None + + def start(self) -> None: + """Start the timer.""" + globalsnode = ba.getactivity().globalsnode + globalsnode.connectattr('time', self.inputnode, 'time1') + self.inputnode.time2 = (globalsnode.time + + (self._timeremaining + 1) * 1000) + self._timer = ba.Timer(1.0, self._update, repeat=True) + + def on_expire(self) -> None: + super().on_expire() + + # Release callbacks/refs. + self._endcall = None + + def _update(self, forcevalue: int = None) -> None: + if forcevalue is not None: + tval = forcevalue + else: + self._timeremaining = max(0, self._timeremaining - 1) + tval = self._timeremaining + + # if there's a countdown sound for this time that we + # haven't played yet, play it + if tval == 10: + assert self.node + assert isinstance(self.node.scale, float) + self.node.scale *= 1.2 + cmb = ba.newnode('combine', owner=self.node, attrs={'size': 4}) + cmb.connectattr('output', self.node, 'color') + ba.animate(cmb, 'input0', {0: 1.0, 0.15: 1.0}, loop=True) + ba.animate(cmb, 'input1', {0: 1.0, 0.15: 0.5}, loop=True) + ba.animate(cmb, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) + cmb.input3 = 1.0 + if tval <= 10 and not self._ended: + ba.playsound(ba.getsound('tick')) + if tval in self._countdownsounds: + ba.playsound(self._countdownsounds[tval]) + if tval <= 0 and not self._ended: + self._ended = True + if self._endcall is not None: + self._endcall() diff --git a/dist/ba_data/python/bastd/actor/onscreentimer.py b/dist/ba_data/python/bastd/actor/onscreentimer.py new file mode 100644 index 0000000..c58e87d --- /dev/null +++ b/dist/ba_data/python/bastd/actor/onscreentimer.py @@ -0,0 +1,129 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Actor(s).""" +from __future__ import annotations + +from typing import TYPE_CHECKING, overload + +import ba + +if TYPE_CHECKING: + from typing import Optional, Union, Any, Literal + + +class OnScreenTimer(ba.Actor): + """A handy on-screen timer. + + category: Gameplay Classes + + Useful for time-based games where time increases. + """ + + def __init__(self) -> None: + super().__init__() + self._starttime_ms: Optional[int] = None + self.node = ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -70), + 'scale': 1.4, + 'text': '' + }) + self.inputnode = ba.newnode('timedisplay', + attrs={ + 'timemin': 0, + 'showsubseconds': True + }) + self.inputnode.connectattr('output', self.node, 'text') + + def start(self) -> None: + """Start the timer.""" + tval = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(tval, int) + self._starttime_ms = tval + self.inputnode.time1 = self._starttime_ms + ba.getactivity().globalsnode.connectattr('time', self.inputnode, + 'time2') + + def has_started(self) -> bool: + """Return whether this timer has started yet.""" + return self._starttime_ms is not None + + def stop(self, + endtime: Union[int, float] = None, + timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS) -> None: + """End the timer. + + If 'endtime' is not None, it is used when calculating + the final display time; otherwise the current time is used. + + 'timeformat' applies to endtime and can be SECONDS or MILLISECONDS + """ + if endtime is None: + endtime = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + timeformat = ba.TimeFormat.MILLISECONDS + + if self._starttime_ms is None: + print('Warning: OnScreenTimer.stop() called without start() first') + else: + endtime_ms: int + if timeformat is ba.TimeFormat.SECONDS: + endtime_ms = int(endtime * 1000) + elif timeformat is ba.TimeFormat.MILLISECONDS: + assert isinstance(endtime, int) + endtime_ms = endtime + else: + raise ValueError(f'invalid timeformat: {timeformat}') + + self.inputnode.timemax = endtime_ms - self._starttime_ms + + # Overloads so type checker knows our exact return type based in args. + @overload + def getstarttime( + self, + timeformat: Literal[ba.TimeFormat.SECONDS] = ba.TimeFormat.SECONDS + ) -> float: + ... + + @overload + def getstarttime(self, + timeformat: Literal[ba.TimeFormat.MILLISECONDS]) -> int: + ... + + def getstarttime( + self, + timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS + ) -> Union[int, float]: + """Return the sim-time when start() was called. + + Time will be returned in seconds if timeformat is SECONDS or + milliseconds if it is MILLISECONDS. + """ + val_ms: Any + if self._starttime_ms is None: + print('WARNING: getstarttime() called on un-started timer') + val_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + else: + val_ms = self._starttime_ms + assert isinstance(val_ms, int) + if timeformat is ba.TimeFormat.SECONDS: + return 0.001 * val_ms + if timeformat is ba.TimeFormat.MILLISECONDS: + return val_ms + raise ValueError(f'invalid timeformat: {timeformat}') + + @property + def starttime(self) -> float: + """Shortcut for start time in seconds.""" + return self.getstarttime() + + def handlemessage(self, msg: Any) -> Any: + # if we're asked to die, just kill our node/timer + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() diff --git a/dist/ba_data/python/bastd/actor/playerspaz.py b/dist/ba_data/python/bastd/actor/playerspaz.py new file mode 100644 index 0000000..b77aa8a --- /dev/null +++ b/dist/ba_data/python/bastd/actor/playerspaz.py @@ -0,0 +1,289 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to player-controlled Spazzes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar, overload + +import ba +from bastd.actor.spaz import Spaz + +if TYPE_CHECKING: + from typing import Any, Sequence, Tuple, Optional, Type, Literal + +PlayerType = TypeVar('PlayerType', bound=ba.Player) +TeamType = TypeVar('TeamType', bound=ba.Team) + + +class PlayerSpazHurtMessage: + """A message saying a ba.PlayerSpaz was hurt. + + category: Message Classes + + Attributes: + + spaz + The ba.PlayerSpaz that was hurt + """ + + def __init__(self, spaz: PlayerSpaz): + """Instantiate with the given ba.Spaz value.""" + self.spaz = spaz + + +class PlayerSpaz(Spaz): + """A ba.Spaz subclass meant to be controlled by a ba.Player. + + category: Gameplay Classes + + When a PlayerSpaz dies, it delivers a ba.PlayerDiedMessage + to the current ba.Activity. (unless the death was the result of the + player leaving the game, in which case no message is sent) + + When a PlayerSpaz is hurt, it delivers a ba.PlayerSpazHurtMessage + to the current ba.Activity. + """ + + def __init__(self, + player: ba.Player, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + powerups_expire: bool = True): + """Create a spaz for the provided ba.Player. + + Note: this does not wire up any controls; + you must call connect_controls_to_player() to do so. + """ + + super().__init__(color=color, + highlight=highlight, + character=character, + source_player=player, + start_invincible=True, + powerups_expire=powerups_expire) + self.last_player_attacked_by: Optional[ba.Player] = None + self.last_attacked_time = 0.0 + self.last_attacked_type: Optional[Tuple[str, str]] = None + self.held_count = 0 + self.last_player_held_by: Optional[ba.Player] = None + self._player = player + self._drive_player_position() + + # Overloads to tell the type system our return type based on doraise val. + + @overload + def getplayer(self, + playertype: Type[PlayerType], + doraise: Literal[False] = False) -> Optional[PlayerType]: + ... + + @overload + def getplayer(self, playertype: Type[PlayerType], + doraise: Literal[True]) -> PlayerType: + ... + + def getplayer(self, + playertype: Type[PlayerType], + doraise: bool = False) -> Optional[PlayerType]: + """Get the ba.Player associated with this Spaz. + + By default this will return None if the Player no longer exists. + If you are logically certain that the Player still exists, pass + doraise=False to get a non-optional return type. + """ + player: Any = self._player + assert isinstance(player, playertype) + if not player.exists() and doraise: + raise ba.PlayerNotFoundError() + return player if player.exists() else None + + def connect_controls_to_player(self, + enable_jump: bool = True, + enable_punch: bool = True, + enable_pickup: bool = True, + enable_bomb: bool = True, + enable_run: bool = True, + enable_fly: bool = True) -> None: + """Wire this spaz up to the provided ba.Player. + + Full control of the character is given by default + but can be selectively limited by passing False + to specific arguments. + """ + player = self.getplayer(ba.Player) + assert player + + # Reset any currently connected player and/or the player we're + # wiring up. + if self._connected_to_player: + if player != self._connected_to_player: + player.resetinput() + self.disconnect_controls_from_player() + else: + player.resetinput() + + player.assigninput(ba.InputType.UP_DOWN, self.on_move_up_down) + player.assigninput(ba.InputType.LEFT_RIGHT, self.on_move_left_right) + player.assigninput(ba.InputType.HOLD_POSITION_PRESS, + self.on_hold_position_press) + player.assigninput(ba.InputType.HOLD_POSITION_RELEASE, + self.on_hold_position_release) + intp = ba.InputType + if enable_jump: + player.assigninput(intp.JUMP_PRESS, self.on_jump_press) + player.assigninput(intp.JUMP_RELEASE, self.on_jump_release) + if enable_pickup: + player.assigninput(intp.PICK_UP_PRESS, self.on_pickup_press) + player.assigninput(intp.PICK_UP_RELEASE, self.on_pickup_release) + if enable_punch: + player.assigninput(intp.PUNCH_PRESS, self.on_punch_press) + player.assigninput(intp.PUNCH_RELEASE, self.on_punch_release) + if enable_bomb: + player.assigninput(intp.BOMB_PRESS, self.on_bomb_press) + player.assigninput(intp.BOMB_RELEASE, self.on_bomb_release) + if enable_run: + player.assigninput(intp.RUN, self.on_run) + if enable_fly: + player.assigninput(intp.FLY_PRESS, self.on_fly_press) + player.assigninput(intp.FLY_RELEASE, self.on_fly_release) + + self._connected_to_player = player + + def disconnect_controls_from_player(self) -> None: + """ + Completely sever any previously connected + ba.Player from control of this spaz. + """ + if self._connected_to_player: + self._connected_to_player.resetinput() + self._connected_to_player = None + + # Send releases for anything in case its held. + self.on_move_up_down(0) + self.on_move_left_right(0) + self.on_hold_position_release() + self.on_jump_release() + self.on_pickup_release() + self.on_punch_release() + self.on_bomb_release() + self.on_run(0.0) + self.on_fly_release() + else: + print('WARNING: disconnect_controls_from_player() called for' + ' non-connected player') + + def handlemessage(self, msg: Any) -> Any: + # FIXME: Tidy this up. + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-nested-blocks + assert not self.expired + + # Keep track of if we're being held and by who most recently. + if isinstance(msg, ba.PickedUpMessage): + # Augment standard behavior. + super().handlemessage(msg) + self.held_count += 1 + picked_up_by = msg.node.source_player + if picked_up_by: + self.last_player_held_by = picked_up_by + elif isinstance(msg, ba.DroppedMessage): + # Augment standard behavior. + super().handlemessage(msg) + self.held_count -= 1 + if self.held_count < 0: + print('ERROR: spaz held_count < 0') + + # Let's count someone dropping us as an attack. + picked_up_by = msg.node.source_player + if picked_up_by: + self.last_player_attacked_by = picked_up_by + self.last_attacked_time = ba.time() + self.last_attacked_type = ('picked_up', 'default') + elif isinstance(msg, ba.StandMessage): + super().handlemessage(msg) # Augment standard behavior. + + # Our Spaz was just moved somewhere. Explicitly update + # our associated player's position in case it is being used + # for logic (otherwise it will be out of date until next step) + self._drive_player_position() + + elif isinstance(msg, ba.DieMessage): + + # Report player deaths to the game. + if not self._dead: + + # Immediate-mode or left-game deaths don't count as 'kills'. + killed = (not msg.immediate + and msg.how is not ba.DeathType.LEFT_GAME) + + activity = self._activity() + + player = self.getplayer(ba.Player, False) + if not killed: + killerplayer = None + else: + # If this player was being held at the time of death, + # the holder is the killer. + if self.held_count > 0 and self.last_player_held_by: + killerplayer = self.last_player_held_by + else: + # Otherwise, if they were attacked by someone in the + # last few seconds, that person is the killer. + # Otherwise it was a suicide. + # FIXME: Currently disabling suicides in Co-Op since + # all bot kills would register as suicides; need to + # change this from last_player_attacked_by to + # something like last_actor_attacked_by to fix that. + if (self.last_player_attacked_by + and ba.time() - self.last_attacked_time < 4.0): + killerplayer = self.last_player_attacked_by + else: + # ok, call it a suicide unless we're in co-op + if (activity is not None and not isinstance( + activity.session, ba.CoopSession)): + killerplayer = player + else: + killerplayer = None + + # We should never wind up with a dead-reference here; + # we want to use None in that case. + assert killerplayer is None or killerplayer + + # Only report if both the player and the activity still exist. + if killed and activity is not None and player: + activity.handlemessage( + ba.PlayerDiedMessage(player, killed, killerplayer, + msg.how)) + + super().handlemessage(msg) # Augment standard behavior. + + # Keep track of the player who last hit us for point rewarding. + elif isinstance(msg, ba.HitMessage): + source_player = msg.get_source_player(type(self._player)) + if source_player: + self.last_player_attacked_by = source_player + self.last_attacked_time = ba.time() + self.last_attacked_type = (msg.hit_type, msg.hit_subtype) + super().handlemessage(msg) # Augment standard behavior. + activity = self._activity() + if activity is not None and self._player.exists(): + activity.handlemessage(PlayerSpazHurtMessage(self)) + else: + return super().handlemessage(msg) + return None + + def _drive_player_position(self) -> None: + """Drive our ba.Player's official position + + If our position is changed explicitly, this should be called again + to instantly update the player position (otherwise it would be out + of date until the next sim step) + """ + player = self._player + if player: + assert self.node + assert player.node + self.node.connectattr('torso_position', player.node, 'position') diff --git a/dist/ba_data/python/bastd/actor/popuptext.py b/dist/ba_data/python/bastd/actor/popuptext.py new file mode 100644 index 0000000..5ce8af9 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/popuptext.py @@ -0,0 +1,112 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Actor(s).""" + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, Union, Sequence + + +class PopupText(ba.Actor): + """Text that pops up above a position to denote something special. + + category: Gameplay Classes + """ + + def __init__(self, + text: Union[str, ba.Lstr], + position: Sequence[float] = (0.0, 0.0, 0.0), + color: Sequence[float] = (1.0, 1.0, 1.0, 1.0), + random_offset: float = 0.5, + offset: Sequence[float] = (0.0, 0.0, 0.0), + scale: float = 1.0): + """Instantiate with given values. + + random_offset is the amount of random offset from the provided position + that will be applied. This can help multiple achievements from + overlapping too much. + """ + super().__init__() + if len(color) == 3: + color = (color[0], color[1], color[2], 1.0) + pos = (position[0] + offset[0] + random_offset * + (0.5 - random.random()), position[1] + offset[0] + + random_offset * (0.5 - random.random()), position[2] + + offset[0] + random_offset * (0.5 - random.random())) + + self.node = ba.newnode('text', + attrs={ + 'text': text, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_align': 'center' + }, + delegate=self) + + lifespan = 1.5 + + # scale up + ba.animate( + self.node, 'scale', { + 0: 0.0, + lifespan * 0.11: 0.020 * 0.7 * scale, + lifespan * 0.16: 0.013 * 0.7 * scale, + lifespan * 0.25: 0.014 * 0.7 * scale + }) + + # translate upward + self._tcombine = ba.newnode('combine', + owner=self.node, + attrs={ + 'input0': pos[0], + 'input2': pos[2], + 'size': 3 + }) + ba.animate(self._tcombine, 'input1', { + 0: pos[1] + 1.5, + lifespan: pos[1] + 2.0 + }) + self._tcombine.connectattr('output', self.node, 'position') + + # fade our opacity in/out + self._combine = ba.newnode('combine', + owner=self.node, + attrs={ + 'input0': color[0], + 'input1': color[1], + 'input2': color[2], + 'size': 4 + }) + for i in range(4): + ba.animate( + self._combine, 'input' + str(i), { + 0.13 * lifespan: color[i], + 0.18 * lifespan: 4.0 * color[i], + 0.22 * lifespan: color[i] + }) + ba.animate(self._combine, 'input3', { + 0: 0, + 0.1 * lifespan: color[3], + 0.7 * lifespan: color[3], + lifespan: 0 + }) + self._combine.connectattr('output', self.node, 'color') + + # kill ourself + self._die_timer = ba.Timer( + lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())) + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + else: + super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/actor/powerupbox.py b/dist/ba_data/python/bastd/actor/powerupbox.py new file mode 100644 index 0000000..aac89f0 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/powerupbox.py @@ -0,0 +1,312 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Actor(s).""" + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import List, Any, Optional, Sequence + +DEFAULT_POWERUP_INTERVAL = 8.0 + + +class _TouchedMessage: + pass + + +class PowerupBoxFactory: + """A collection of media and other resources used by ba.Powerups. + + category: Gameplay Classes + + A single instance of this is shared between all powerups + and can be retrieved via ba.Powerup.get_factory(). + + Attributes: + + model + The ba.Model of the powerup box. + + model_simple + A simpler ba.Model of the powerup box, for use in shadows, etc. + + tex_bomb + Triple-bomb powerup ba.Texture. + + tex_punch + Punch powerup ba.Texture. + + tex_ice_bombs + Ice bomb powerup ba.Texture. + + tex_sticky_bombs + Sticky bomb powerup ba.Texture. + + tex_shield + Shield powerup ba.Texture. + + tex_impact_bombs + Impact-bomb powerup ba.Texture. + + tex_health + Health powerup ba.Texture. + + tex_land_mines + Land-mine powerup ba.Texture. + + tex_curse + Curse powerup ba.Texture. + + health_powerup_sound + ba.Sound played when a health powerup is accepted. + + powerup_sound + ba.Sound played when a powerup is accepted. + + powerdown_sound + ba.Sound that can be used when powerups wear off. + + powerup_material + ba.Material applied to powerup boxes. + + powerup_accept_material + Powerups will send a ba.PowerupMessage to anything they touch + that has this ba.Material applied. + """ + + _STORENAME = ba.storagename() + + def __init__(self) -> None: + """Instantiate a PowerupBoxFactory. + + You shouldn't need to do this; call ba.Powerup.get_factory() + to get a shared instance. + """ + from ba.internal import get_default_powerup_distribution + shared = SharedObjects.get() + self._lastpoweruptype: Optional[str] = None + self.model = ba.getmodel('powerup') + self.model_simple = ba.getmodel('powerupSimple') + self.tex_bomb = ba.gettexture('powerupBomb') + self.tex_punch = ba.gettexture('powerupPunch') + self.tex_ice_bombs = ba.gettexture('powerupIceBombs') + self.tex_sticky_bombs = ba.gettexture('powerupStickyBombs') + self.tex_shield = ba.gettexture('powerupShield') + self.tex_impact_bombs = ba.gettexture('powerupImpactBombs') + self.tex_health = ba.gettexture('powerupHealth') + self.tex_land_mines = ba.gettexture('powerupLandMines') + self.tex_curse = ba.gettexture('powerupCurse') + self.health_powerup_sound = ba.getsound('healthPowerup') + self.powerup_sound = ba.getsound('powerup01') + self.powerdown_sound = ba.getsound('powerdown01') + self.drop_sound = ba.getsound('boxDrop') + + # Material for powerups. + self.powerup_material = ba.Material() + + # Material for anyone wanting to accept powerups. + self.powerup_accept_material = ba.Material() + + # Pass a powerup-touched message to applicable stuff. + self.powerup_material.add_actions( + conditions=('they_have_material', self.powerup_accept_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('message', 'our_node', 'at_connect', _TouchedMessage()), + )) + + # We don't wanna be picked up. + self.powerup_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=('modify_part_collision', 'collide', False), + ) + + self.powerup_material.add_actions( + conditions=('they_have_material', shared.footing_material), + actions=('impact_sound', self.drop_sound, 0.5, 0.1), + ) + + self._powerupdist: List[str] = [] + for powerup, freq in get_default_powerup_distribution(): + for _i in range(int(freq)): + self._powerupdist.append(powerup) + + def get_random_powerup_type(self, + forcetype: str = None, + excludetypes: List[str] = None) -> str: + """Returns a random powerup type (string). + + See ba.Powerup.poweruptype for available type values. + + There are certain non-random aspects to this; a 'curse' powerup, + for instance, is always followed by a 'health' powerup (to keep things + interesting). Passing 'forcetype' forces a given returned type while + still properly interacting with the non-random aspects of the system + (ie: forcing a 'curse' powerup will result + in the next powerup being health). + """ + if excludetypes is None: + excludetypes = [] + if forcetype: + ptype = forcetype + else: + # If the last one was a curse, make this one a health to + # provide some hope. + if self._lastpoweruptype == 'curse': + ptype = 'health' + else: + while True: + ptype = self._powerupdist[random.randint( + 0, + len(self._powerupdist) - 1)] + if ptype not in excludetypes: + break + self._lastpoweruptype = ptype + return ptype + + @classmethod + def get(cls) -> PowerupBoxFactory: + """Return a shared ba.PowerupBoxFactory object, creating if needed.""" + activity = ba.getactivity() + if activity is None: + raise ba.ContextError('No current activity.') + factory = activity.customdata.get(cls._STORENAME) + if factory is None: + factory = activity.customdata[cls._STORENAME] = PowerupBoxFactory() + assert isinstance(factory, PowerupBoxFactory) + return factory + + +class PowerupBox(ba.Actor): + """A box that grants a powerup. + + category: Gameplay Classes + + This will deliver a ba.PowerupMessage to anything that touches it + which has the ba.PowerupBoxFactory.powerup_accept_material applied. + + Attributes: + + poweruptype + The string powerup type. This can be 'triple_bombs', 'punch', + 'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs', 'shield', + 'health', or 'curse'. + + node + The 'prop' ba.Node representing this box. + """ + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + poweruptype: str = 'triple_bombs', + expire: bool = True): + """Create a powerup-box of the requested type at the given position. + + see ba.Powerup.poweruptype for valid type strings. + """ + + super().__init__() + shared = SharedObjects.get() + factory = PowerupBoxFactory.get() + self.poweruptype = poweruptype + self._powersgiven = False + + if poweruptype == 'triple_bombs': + tex = factory.tex_bomb + elif poweruptype == 'punch': + tex = factory.tex_punch + elif poweruptype == 'ice_bombs': + tex = factory.tex_ice_bombs + elif poweruptype == 'impact_bombs': + tex = factory.tex_impact_bombs + elif poweruptype == 'land_mines': + tex = factory.tex_land_mines + elif poweruptype == 'sticky_bombs': + tex = factory.tex_sticky_bombs + elif poweruptype == 'shield': + tex = factory.tex_shield + elif poweruptype == 'health': + tex = factory.tex_health + elif poweruptype == 'curse': + tex = factory.tex_curse + else: + raise ValueError('invalid poweruptype: ' + str(poweruptype)) + + if len(position) != 3: + raise ValueError('expected 3 floats for position') + + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'body': 'box', + 'position': position, + 'model': factory.model, + 'light_model': factory.model_simple, + 'shadow_size': 0.5, + 'color_texture': tex, + 'reflection': 'powerup', + 'reflection_scale': [1.0], + 'materials': (factory.powerup_material, + shared.object_material) + }) # yapf: disable + + # Animate in. + curve = ba.animate(self.node, 'model_scale', {0: 0, 0.14: 1.6, 0.2: 1}) + ba.timer(0.2, curve.delete) + + if expire: + ba.timer(DEFAULT_POWERUP_INTERVAL - 2.5, + ba.WeakCall(self._start_flashing)) + ba.timer(DEFAULT_POWERUP_INTERVAL - 1.0, + ba.WeakCall(self.handlemessage, ba.DieMessage())) + + def _start_flashing(self) -> None: + if self.node: + self.node.flashing = True + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, ba.PowerupAcceptMessage): + factory = PowerupBoxFactory.get() + assert self.node + if self.poweruptype == 'health': + ba.playsound(factory.health_powerup_sound, + 3, + position=self.node.position) + ba.playsound(factory.powerup_sound, 3, position=self.node.position) + self._powersgiven = True + self.handlemessage(ba.DieMessage()) + + elif isinstance(msg, _TouchedMessage): + if not self._powersgiven: + node = ba.getcollision().opposingnode + node.handlemessage( + ba.PowerupMessage(self.poweruptype, sourcenode=self.node)) + + elif isinstance(msg, ba.DieMessage): + if self.node: + if msg.immediate: + self.node.delete() + else: + ba.animate(self.node, 'model_scale', {0: 1, 0.1: 0}) + ba.timer(0.1, self.node.delete) + + elif isinstance(msg, ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage()) + + elif isinstance(msg, ba.HitMessage): + # Don't die on punches (that's annoying). + if msg.hit_type != 'punch': + self.handlemessage(ba.DieMessage()) + else: + return super().handlemessage(msg) + return None diff --git a/dist/ba_data/python/bastd/actor/respawnicon.py b/dist/ba_data/python/bastd/actor/respawnicon.py new file mode 100644 index 0000000..504a8e6 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/respawnicon.py @@ -0,0 +1,156 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Implements respawn icon actor.""" + +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Optional, Dict, Tuple + + +class RespawnIcon: + """An icon with a countdown that appears alongside the screen. + + category: Gameplay Classes + + This is used to indicate that a ba.Player is waiting to respawn. + """ + + _MASKTEXSTORENAME = ba.storagename('masktex') + _ICONSSTORENAME = ba.storagename('icons') + + def __init__(self, player: ba.Player, respawn_time: float): + """Instantiate with a ba.Player and respawn_time (in seconds).""" + self._visible = True + + on_right, offs_extra, respawn_icons = self._get_context(player) + + # Cache our mask tex on the team for easy access. + mask_tex = player.team.customdata.get(self._MASKTEXSTORENAME) + if mask_tex is None: + mask_tex = ba.gettexture('characterIconMask') + player.team.customdata[self._MASKTEXSTORENAME] = mask_tex + assert isinstance(mask_tex, ba.Texture) + + # Now find the first unused slot and use that. + index = 0 + while (index in respawn_icons and respawn_icons[index]() is not None + and respawn_icons[index]().visible): + index += 1 + respawn_icons[index] = weakref.ref(self) + + offs = offs_extra + index * -53 + icon = player.get_icon() + texture = icon['texture'] + h_offs = -10 + ipos = (-40 - h_offs if on_right else 40 + h_offs, -180 + offs) + self._image: Optional[ba.NodeActor] = ba.NodeActor( + ba.newnode('image', + attrs={ + 'texture': texture, + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'tint2_color': icon['tint2_color'], + 'mask_texture': mask_tex, + 'position': ipos, + 'scale': (32, 32), + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'topRight' if on_right else 'topLeft' + })) + + assert self._image.node + ba.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7}) + + npos = (-40 - h_offs if on_right else 40 + h_offs, -205 + 49 + offs) + self._name: Optional[ba.NodeActor] = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'right' if on_right else 'left', + 'text': ba.Lstr(value=player.getname()), + 'maxwidth': 100, + 'h_align': 'center', + 'v_align': 'center', + 'shadow': 1.0, + 'flatness': 1.0, + 'color': ba.safecolor(icon['tint_color']), + 'scale': 0.5, + 'position': npos + })) + + assert self._name.node + ba.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5}) + + tpos = (-60 - h_offs if on_right else 60 + h_offs, -192 + offs) + self._text: Optional[ba.NodeActor] = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': tpos, + 'h_attach': 'right' if on_right else 'left', + 'h_align': 'right' if on_right else 'left', + 'scale': 0.9, + 'shadow': 0.5, + 'flatness': 0.5, + 'v_attach': 'top', + 'color': ba.safecolor(icon['tint_color']), + 'text': '' + })) + + assert self._text.node + ba.animate(self._text.node, 'scale', {0: 0, 0.1: 0.9}) + + self._respawn_time = ba.time() + respawn_time + self._update() + self._timer: Optional[ba.Timer] = ba.Timer(1.0, + ba.WeakCall(self._update), + repeat=True) + + @property + def visible(self) -> bool: + """Is this icon still visible?""" + return self._visible + + def _get_context(self, player: ba.Player) -> Tuple[bool, float, Dict]: + """Return info on where we should be shown and stored.""" + activity = ba.getactivity() + + if isinstance(ba.getsession(), ba.DualTeamSession): + on_right = player.team.id % 2 == 1 + + # Store a list of icons in the team. + icons = player.team.customdata.get(self._ICONSSTORENAME) + if icons is None: + player.team.customdata[self._ICONSSTORENAME] = icons = {} + assert isinstance(icons, dict) + + offs_extra = -20 + else: + on_right = False + + # Store a list of icons in the activity. + icons = activity.customdata.get(self._ICONSSTORENAME) + if icons is None: + activity.customdata[self._ICONSSTORENAME] = icons = {} + assert isinstance(icons, dict) + + if isinstance(activity.session, ba.FreeForAllSession): + offs_extra = -150 + else: + offs_extra = -20 + return on_right, offs_extra, icons + + def _update(self) -> None: + remaining = int(round(self._respawn_time - ba.time())) + if remaining > 0: + assert self._text is not None + if self._text.node: + self._text.node.text = str(remaining) + else: + self._visible = False + self._image = self._text = self._timer = self._name = None diff --git a/dist/ba_data/python/bastd/actor/scoreboard.py b/dist/ba_data/python/bastd/actor/scoreboard.py new file mode 100644 index 0000000..54e1312 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/scoreboard.py @@ -0,0 +1,399 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines ScoreBoard Actor and related functionality.""" + +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, Optional, Sequence, Dict, Union + + +class _Entry: + + def __init__(self, scoreboard: Scoreboard, team: ba.Team, do_cover: bool, + scale: float, label: Optional[ba.Lstr], flash_length: float): + # pylint: disable=too-many-statements + self._scoreboard = weakref.ref(scoreboard) + self._do_cover = do_cover + self._scale = scale + self._flash_length = flash_length + self._width = 140.0 * self._scale + self._height = 32.0 * self._scale + self._bar_width = 2.0 * self._scale + self._bar_height = 32.0 * self._scale + self._bar_tex = self._backing_tex = ba.gettexture('bar') + self._cover_tex = ba.gettexture('uiAtlas') + self._model = ba.getmodel('meterTransparent') + self._pos: Optional[Sequence[float]] = None + self._flash_timer: Optional[ba.Timer] = None + self._flash_counter: Optional[int] = None + self._flash_colors: Optional[bool] = None + self._score: Optional[float] = None + + safe_team_color = ba.safecolor(team.color, target_intensity=1.0) + + # FIXME: Should not do things conditionally for vr-mode, as there may + # be non-vr clients connected which will also get these value. + vrmode = ba.app.vr_mode + + if self._do_cover: + if vrmode: + self._backing_color = [0.1 + c * 0.1 for c in safe_team_color] + else: + self._backing_color = [ + 0.05 + c * 0.17 for c in safe_team_color + ] + else: + self._backing_color = [0.05 + c * 0.1 for c in safe_team_color] + + opacity = (0.8 if vrmode else 0.8) if self._do_cover else 0.5 + self._backing = ba.NodeActor( + ba.newnode('image', + attrs={ + 'scale': (self._width, self._height), + 'opacity': opacity, + 'color': self._backing_color, + 'vr_depth': -3, + 'attach': 'topLeft', + 'texture': self._backing_tex + })) + + self._barcolor = safe_team_color + self._bar = ba.NodeActor( + ba.newnode('image', + attrs={ + 'opacity': 0.7, + 'color': self._barcolor, + 'attach': 'topLeft', + 'texture': self._bar_tex + })) + + self._bar_scale = ba.newnode('combine', + owner=self._bar.node, + attrs={ + 'size': 2, + 'input0': self._bar_width, + 'input1': self._bar_height + }) + assert self._bar.node + self._bar_scale.connectattr('output', self._bar.node, 'scale') + self._bar_position = ba.newnode('combine', + owner=self._bar.node, + attrs={ + 'size': 2, + 'input0': 0, + 'input1': 0 + }) + self._bar_position.connectattr('output', self._bar.node, 'position') + self._cover_color = safe_team_color + if self._do_cover: + self._cover = ba.NodeActor( + ba.newnode('image', + attrs={ + 'scale': + (self._width * 1.15, self._height * 1.6), + 'opacity': 1.0, + 'color': self._cover_color, + 'vr_depth': 2, + 'attach': 'topLeft', + 'texture': self._cover_tex, + 'model_transparent': self._model + })) + + clr = safe_team_color + maxwidth = 130.0 * (1.0 - scoreboard.score_split) + flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0) + self._score_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'h_attach': 'left', + 'v_attach': 'top', + 'h_align': 'right', + 'v_align': 'center', + 'maxwidth': maxwidth, + 'vr_depth': 2, + 'scale': self._scale * 0.9, + 'text': '', + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': flatness, + 'color': clr + })) + + clr = safe_team_color + + team_name_label: Union[str, ba.Lstr] + if label is not None: + team_name_label = label + else: + team_name_label = team.name + + # We do our own clipping here; should probably try to tap into some + # existing functionality. + if isinstance(team_name_label, ba.Lstr): + + # Hmmm; if the team-name is a non-translatable value lets go + # ahead and clip it otherwise we leave it as-is so + # translation can occur.. + if team_name_label.is_flat_value(): + val = team_name_label.evaluate() + if len(val) > 10: + team_name_label = ba.Lstr(value=val[:10] + '...') + else: + if len(team_name_label) > 10: + team_name_label = team_name_label[:10] + '...' + team_name_label = ba.Lstr(value=team_name_label) + + flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0) + self._name_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'h_attach': 'left', + 'v_attach': 'top', + 'h_align': 'left', + 'v_align': 'center', + 'vr_depth': 2, + 'scale': self._scale * 0.9, + 'shadow': 1.0 if vrmode else 0.5, + 'flatness': flatness, + 'maxwidth': 130 * scoreboard.score_split, + 'text': team_name_label, + 'color': clr + (1.0, ) + })) + + def flash(self, countdown: bool, extra_flash: bool) -> None: + """Flash momentarily.""" + self._flash_timer = ba.Timer(0.1, + ba.WeakCall(self._do_flash), + repeat=True) + if countdown: + self._flash_counter = 10 + else: + self._flash_counter = int(20.0 * self._flash_length) + if extra_flash: + self._flash_counter *= 4 + self._set_flash_colors(True) + + def set_position(self, position: Sequence[float]) -> None: + """Set the entry's position.""" + + # Abort if we've been killed + if not self._backing.node: + return + + self._pos = tuple(position) + self._backing.node.position = (position[0] + self._width / 2, + position[1] - self._height / 2) + if self._do_cover: + assert self._cover.node + self._cover.node.position = (position[0] + self._width / 2, + position[1] - self._height / 2) + self._bar_position.input0 = self._pos[0] + self._bar_width / 2 + self._bar_position.input1 = self._pos[1] - self._bar_height / 2 + assert self._score_text.node + self._score_text.node.position = (self._pos[0] + self._width - + 7.0 * self._scale, + self._pos[1] - self._bar_height + + 16.0 * self._scale) + assert self._name_text.node + self._name_text.node.position = (self._pos[0] + 7.0 * self._scale, + self._pos[1] - self._bar_height + + 16.0 * self._scale) + + def _set_flash_colors(self, flash: bool) -> None: + self._flash_colors = flash + + def _safesetcolor(node: Optional[ba.Node], val: Any) -> None: + if node: + node.color = val + + if flash: + scale = 2.0 + _safesetcolor( + self._backing.node, + (self._backing_color[0] * scale, self._backing_color[1] * + scale, self._backing_color[2] * scale)) + _safesetcolor(self._bar.node, + (self._barcolor[0] * scale, self._barcolor[1] * + scale, self._barcolor[2] * scale)) + if self._do_cover: + _safesetcolor( + self._cover.node, + (self._cover_color[0] * scale, self._cover_color[1] * + scale, self._cover_color[2] * scale)) + else: + _safesetcolor(self._backing.node, self._backing_color) + _safesetcolor(self._bar.node, self._barcolor) + if self._do_cover: + _safesetcolor(self._cover.node, self._cover_color) + + def _do_flash(self) -> None: + assert self._flash_counter is not None + if self._flash_counter <= 0: + self._set_flash_colors(False) + else: + self._flash_counter -= 1 + self._set_flash_colors(not self._flash_colors) + + def set_value(self, + score: float, + max_score: float = None, + countdown: bool = False, + flash: bool = True, + show_value: bool = True) -> None: + """Set the value for the scoreboard entry.""" + + # If we have no score yet, just set it.. otherwise compare + # and see if we should flash. + if self._score is None: + self._score = score + else: + if score > self._score or (countdown and score < self._score): + extra_flash = (max_score is not None and score >= max_score + and not countdown) or (countdown and score == 0) + if flash: + self.flash(countdown, extra_flash) + self._score = score + + if max_score is None: + self._bar_width = 0.0 + else: + if countdown: + self._bar_width = max( + 2.0 * self._scale, + self._width * (1.0 - (float(score) / max_score))) + else: + self._bar_width = max( + 2.0 * self._scale, + self._width * (min(1.0, + float(score) / max_score))) + + cur_width = self._bar_scale.input0 + ba.animate(self._bar_scale, 'input0', { + 0.0: cur_width, + 0.25: self._bar_width + }) + self._bar_scale.input1 = self._bar_height + cur_x = self._bar_position.input0 + assert self._pos is not None + ba.animate(self._bar_position, 'input0', { + 0.0: cur_x, + 0.25: self._pos[0] + self._bar_width / 2 + }) + self._bar_position.input1 = self._pos[1] - self._bar_height / 2 + assert self._score_text.node + if show_value: + self._score_text.node.text = str(score) + else: + self._score_text.node.text = '' + + +class _EntryProxy: + """Encapsulates adding/removing of a scoreboard Entry.""" + + def __init__(self, scoreboard: Scoreboard, team: ba.Team): + self._scoreboard = weakref.ref(scoreboard) + + # Have to store ID here instead of a weak-ref since the team will be + # dead when we die and need to remove it. + self._team_id = team.id + + def __del__(self) -> None: + scoreboard = self._scoreboard() + + # Remove our team from the scoreboard if its still around. + # (but deferred, in case we die in a sim step or something where + # its illegal to modify nodes) + if scoreboard is None: + return + + try: + ba.pushcall(ba.Call(scoreboard.remove_team, self._team_id)) + except ba.ContextError: + # This happens if we fire after the activity expires. + # In that case we don't need to do anything. + pass + + +class Scoreboard: + """A display for player or team scores during a game. + + category: Gameplay Classes + """ + + _ENTRYSTORENAME = ba.storagename('entry') + + def __init__(self, label: ba.Lstr = None, score_split: float = 0.7): + """Instantiate a scoreboard. + + Label can be something like 'points' and will + show up on boards if provided. + """ + self._flat_tex = ba.gettexture('null') + self._entries: Dict[int, _Entry] = {} + self._label = label + self.score_split = score_split + + # For free-for-all we go simpler since we have one per player. + self._pos: Sequence[float] + if isinstance(ba.getsession(), ba.FreeForAllSession): + self._do_cover = False + self._spacing = 35.0 + self._pos = (17.0, -65.0) + self._scale = 0.8 + self._flash_length = 0.5 + else: + self._do_cover = True + self._spacing = 50.0 + self._pos = (20.0, -70.0) + self._scale = 1.0 + self._flash_length = 1.0 + + def set_team_value(self, + team: ba.Team, + score: float, + max_score: float = None, + countdown: bool = False, + flash: bool = True, + show_value: bool = True) -> None: + """Update the score-board display for the given ba.Team.""" + if team.id not in self._entries: + self._add_team(team) + + # Create a proxy in the team which will kill + # our entry when it dies (for convenience) + assert self._ENTRYSTORENAME not in team.customdata + team.customdata[self._ENTRYSTORENAME] = _EntryProxy(self, team) + + # Now set the entry. + self._entries[team.id].set_value(score=score, + max_score=max_score, + countdown=countdown, + flash=flash, + show_value=show_value) + + def _add_team(self, team: ba.Team) -> None: + if team.id in self._entries: + raise RuntimeError('Duplicate team add') + self._entries[team.id] = _Entry(self, + team, + do_cover=self._do_cover, + scale=self._scale, + label=self._label, + flash_length=self._flash_length) + self._update_teams() + + def remove_team(self, team_id: int) -> None: + """Remove the team with the given id from the scoreboard.""" + del self._entries[team_id] + self._update_teams() + + def _update_teams(self) -> None: + pos = list(self._pos) + for entry in list(self._entries.values()): + entry.set_position(pos) + pos[1] -= self._spacing * self._scale diff --git a/dist/ba_data/python/bastd/actor/spawner.py b/dist/ba_data/python/bastd/actor/spawner.py new file mode 100644 index 0000000..e40d186 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/spawner.py @@ -0,0 +1,107 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines some lovely Actor(s).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, Sequence, Callable + + +# FIXME: Should make this an Actor. +class Spawner: + """Utility for delayed spawning of objects. + + category: Gameplay Classes + + Creates a light flash and sends a ba.Spawner.SpawnMessage + to the current activity after a delay. + """ + + class SpawnMessage: + """Spawn message sent by a ba.Spawner after its delay has passed. + + category: Message Classes + + Attributes: + + spawner + The ba.Spawner we came from. + + data + The data object passed by the user. + + pt + The spawn position. + """ + + def __init__(self, spawner: Spawner, data: Any, pt: Sequence[float]): + """Instantiate with the given values.""" + self.spawner = spawner + self.data = data + self.pt = pt # pylint: disable=invalid-name + + def __init__(self, + data: Any = None, + pt: Sequence[float] = (0, 0, 0), + spawn_time: float = 1.0, + send_spawn_message: bool = True, + spawn_callback: Callable[[], Any] = None): + """Instantiate a Spawner. + + Requires some custom data, a position, + and a spawn-time in seconds. + """ + self._spawn_callback = spawn_callback + self._send_spawn_message = send_spawn_message + self._spawner_sound = ba.getsound('swip2') + self._data = data + self._pt = pt + # create a light where the spawn will happen + self._light = ba.newnode('light', + attrs={ + 'position': tuple(pt), + 'radius': 0.1, + 'color': (1.0, 0.1, 0.1), + 'lights_volumes': False + }) + scl = float(spawn_time) / 3.75 + min_val = 0.4 + max_val = 0.7 + ba.playsound(self._spawner_sound, position=self._light.position) + ba.animate( + self._light, 'intensity', { + 0.0: 0.0, + 0.25 * scl: max_val, + 0.500 * scl: min_val, + 0.750 * scl: max_val, + 1.000 * scl: min_val, + 1.250 * scl: 1.1 * max_val, + 1.500 * scl: min_val, + 1.750 * scl: 1.2 * max_val, + 2.000 * scl: min_val, + 2.250 * scl: 1.3 * max_val, + 2.500 * scl: min_val, + 2.750 * scl: 1.4 * max_val, + 3.000 * scl: min_val, + 3.250 * scl: 1.5 * max_val, + 3.500 * scl: min_val, + 3.750 * scl: 2.0, + 4.000 * scl: 0.0 + }) + ba.timer(spawn_time, self._spawn) + + def _spawn(self) -> None: + ba.timer(1.0, self._light.delete) + if self._spawn_callback is not None: + self._spawn_callback() + if self._send_spawn_message: + # only run if our activity still exists + activity = ba.getactivity() + if activity is not None: + activity.handlemessage( + self.SpawnMessage(self, self._data, self._pt)) diff --git a/dist/ba_data/python/bastd/actor/spaz.py b/dist/ba_data/python/bastd/actor/spaz.py new file mode 100644 index 0000000..064b90b --- /dev/null +++ b/dist/ba_data/python/bastd/actor/spaz.py @@ -0,0 +1,1426 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the spaz actor.""" +# pylint: disable=too-many-lines + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.actor import bomb as stdbomb +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.actor.spazfactory import SpazFactory +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Sequence, Optional, Dict, List, Union, Callable, + Tuple, Set) + from bastd.actor.spazfactory import SpazFactory + +POWERUP_WEAR_OFF_TIME = 20000 +BASE_PUNCH_COOLDOWN = 400 + + +class PickupMessage: + """We wanna pick something up.""" + + +class PunchHitMessage: + """Message saying an object was hit.""" + + +class CurseExplodeMessage: + """We are cursed and should blow up now.""" + + +class BombDiedMessage: + """A bomb has died and thus can be recycled.""" + + +class Spaz(ba.Actor): + """ + Base class for various Spazzes. + + category: Gameplay Classes + + A Spaz is the standard little humanoid character in the game. + It can be controlled by a player or by AI, and can have + various different appearances. The name 'Spaz' is not to be + confused with the 'Spaz' character in the game, which is just + one of the skins available for instances of this class. + + Attributes: + + node + The 'spaz' ba.Node. + """ + + # pylint: disable=too-many-public-methods + # pylint: disable=too-many-locals + + points_mult = 1 + curse_time: Optional[float] = 5.0 + default_bomb_count = 1 + default_bomb_type = 'normal' + default_boxing_gloves = False + default_shields = False + + def __init__(self, + color: Sequence[float] = (1.0, 1.0, 1.0), + highlight: Sequence[float] = (0.5, 0.5, 0.5), + character: str = 'Spaz', + source_player: ba.Player = None, + start_invincible: bool = True, + can_accept_powerups: bool = True, + powerups_expire: bool = False, + demo_mode: bool = False): + """Create a spaz with the requested color, character, etc.""" + # pylint: disable=too-many-statements + + super().__init__() + shared = SharedObjects.get() + activity = self.activity + + factory = SpazFactory.get() + + # we need to behave slightly different in the tutorial + self._demo_mode = demo_mode + + self.play_big_death_sound = False + + # scales how much impacts affect us (most damage calcs) + self.impact_scale = 1.0 + + self.source_player = source_player + self._dead = False + if self._demo_mode: # preserve old behavior + self._punch_power_scale = 1.2 + else: + self._punch_power_scale = factory.punch_power_scale + self.fly = ba.getactivity().globalsnode.happy_thoughts_mode + if isinstance(activity, ba.GameActivity): + self._hockey = activity.map.is_hockey + else: + self._hockey = False + self._punched_nodes: Set[ba.Node] = set() + self._cursed = False + self._connected_to_player: Optional[ba.Player] = None + materials = [ + factory.spaz_material, shared.object_material, + shared.player_material + ] + roller_materials = [factory.roller_material, shared.player_material] + extras_material = [] + + if can_accept_powerups: + pam = PowerupBoxFactory.get().powerup_accept_material + materials.append(pam) + roller_materials.append(pam) + extras_material.append(pam) + + media = factory.get_media(character) + punchmats = (factory.punch_material, shared.attack_material) + pickupmats = (factory.pickup_material, shared.pickup_material) + self.node: ba.Node = ba.newnode( + type='spaz', + delegate=self, + attrs={ + 'color': color, + 'behavior_version': 0 if demo_mode else 1, + 'demo_mode': demo_mode, + 'highlight': highlight, + 'jump_sounds': media['jump_sounds'], + 'attack_sounds': media['attack_sounds'], + 'impact_sounds': media['impact_sounds'], + 'death_sounds': media['death_sounds'], + 'pickup_sounds': media['pickup_sounds'], + 'fall_sounds': media['fall_sounds'], + 'color_texture': media['color_texture'], + 'color_mask_texture': media['color_mask_texture'], + 'head_model': media['head_model'], + 'torso_model': media['torso_model'], + 'pelvis_model': media['pelvis_model'], + 'upper_arm_model': media['upper_arm_model'], + 'forearm_model': media['forearm_model'], + 'hand_model': media['hand_model'], + 'upper_leg_model': media['upper_leg_model'], + 'lower_leg_model': media['lower_leg_model'], + 'toes_model': media['toes_model'], + 'style': factory.get_style(character), + 'fly': self.fly, + 'hockey': self._hockey, + 'materials': materials, + 'roller_materials': roller_materials, + 'extras_material': extras_material, + 'punch_materials': punchmats, + 'pickup_materials': pickupmats, + 'invincible': start_invincible, + 'source_player': source_player + }) + self.shield: Optional[ba.Node] = None + + if start_invincible: + + def _safesetattr(node: Optional[ba.Node], attr: str, + val: Any) -> None: + if node: + setattr(node, attr, val) + + ba.timer(1.0, ba.Call(_safesetattr, self.node, 'invincible', + False)) + self.hitpoints = 1000 + self.hitpoints_max = 1000 + self.shield_hitpoints: Optional[int] = None + self.shield_hitpoints_max = 650 + self.shield_decay_rate = 0 + self.shield_decay_timer: Optional[ba.Timer] = None + self._boxing_gloves_wear_off_timer: Optional[ba.Timer] = None + self._boxing_gloves_wear_off_flash_timer: Optional[ba.Timer] = None + self._bomb_wear_off_timer: Optional[ba.Timer] = None + self._bomb_wear_off_flash_timer: Optional[ba.Timer] = None + self._multi_bomb_wear_off_timer: Optional[ba.Timer] = None + self.bomb_count = self.default_bomb_count + self._max_bomb_count = self.default_bomb_count + self.bomb_type_default = self.default_bomb_type + self.bomb_type = self.bomb_type_default + self.land_mine_count = 0 + self.blast_radius = 2.0 + self.powerups_expire = powerups_expire + if self._demo_mode: # preserve old behavior + self._punch_cooldown = BASE_PUNCH_COOLDOWN + else: + self._punch_cooldown = factory.punch_cooldown + self._jump_cooldown = 250 + self._pickup_cooldown = 0 + self._bomb_cooldown = 0 + self._has_boxing_gloves = False + if self.default_boxing_gloves: + self.equip_boxing_gloves() + self.last_punch_time_ms = -9999 + self.last_pickup_time_ms = -9999 + self.last_run_time_ms = -9999 + self._last_run_value = 0.0 + self.last_bomb_time_ms = -9999 + self._turbo_filter_times: Dict[str, int] = {} + self._turbo_filter_time_bucket = 0 + self._turbo_filter_counts: Dict[str, int] = {} + self.frozen = False + self.shattered = False + self._last_hit_time: Optional[int] = None + self._num_times_hit = 0 + self._bomb_held = False + if self.default_shields: + self.equip_shields() + self._dropped_bomb_callbacks: List[Callable[[Spaz, ba.Actor], + Any]] = [] + + self._score_text: Optional[ba.Node] = None + self._score_text_hide_timer: Optional[ba.Timer] = None + self._last_stand_pos: Optional[Sequence[float]] = None + + # Deprecated stuff.. should make these into lists. + self.punch_callback: Optional[Callable[[Spaz], Any]] = None + self.pick_up_powerup_callback: Optional[Callable[[Spaz], Any]] = None + + def exists(self) -> bool: + return bool(self.node) + + def on_expire(self) -> None: + super().on_expire() + + # Release callbacks/refs so we don't wind up with dependency loops. + self._dropped_bomb_callbacks = [] + self.punch_callback = None + self.pick_up_powerup_callback = None + + def add_dropped_bomb_callback( + self, call: Callable[[Spaz, ba.Actor], Any]) -> None: + """ + Add a call to be run whenever this Spaz drops a bomb. + The spaz and the newly-dropped bomb are passed as arguments. + """ + assert not self.expired + self._dropped_bomb_callbacks.append(call) + + def is_alive(self) -> bool: + """ + Method override; returns whether ol' spaz is still kickin'. + """ + return not self._dead + + def _hide_score_text(self) -> None: + if self._score_text: + assert isinstance(self._score_text.scale, float) + ba.animate(self._score_text, 'scale', { + 0.0: self._score_text.scale, + 0.2: 0.0 + }) + + def _turbo_filter_add_press(self, source: str) -> None: + """ + Can pass all button presses through here; if we see an obscene number + of them in a short time let's shame/pushish this guy for using turbo + """ + t_ms = ba.time(timetype=ba.TimeType.BASE, + timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + t_bucket = int(t_ms / 1000) + if t_bucket == self._turbo_filter_time_bucket: + # Add only once per timestep (filter out buttons triggering + # multiple actions). + if t_ms != self._turbo_filter_times.get(source, 0): + self._turbo_filter_counts[source] = ( + self._turbo_filter_counts.get(source, 0) + 1) + self._turbo_filter_times[source] = t_ms + # (uncomment to debug; prints what this count is at) + # ba.screenmessage( str(source) + " " + # + str(self._turbo_filter_counts[source])) + if self._turbo_filter_counts[source] == 15: + # Knock 'em out. That'll learn 'em. + assert self.node + self.node.handlemessage('knockout', 500.0) + + # Also issue periodic notices about who is turbo-ing. + now = ba.time(ba.TimeType.REAL) + if now > ba.app.last_spaz_turbo_warn_time + 30.0: + ba.app.last_spaz_turbo_warn_time = now + ba.screenmessage(ba.Lstr( + translate=('statements', + ('Warning to ${NAME}: ' + 'turbo / button-spamming knocks' + ' you out.')), + subs=[('${NAME}', self.node.name)]), + color=(1, 0.5, 0)) + ba.playsound(ba.getsound('error')) + else: + self._turbo_filter_times = {} + self._turbo_filter_time_bucket = t_bucket + self._turbo_filter_counts = {source: 1} + + def set_score_text(self, + text: Union[str, ba.Lstr], + color: Sequence[float] = (1.0, 1.0, 0.4), + flash: bool = False) -> None: + """ + Utility func to show a message momentarily over our spaz that follows + him around; Handy for score updates and things. + """ + color_fin = ba.safecolor(color)[:3] + if not self.node: + return + if not self._score_text: + start_scale = 0.0 + mnode = ba.newnode('math', + owner=self.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + self.node.connectattr('torso_position', mnode, 'input2') + self._score_text = ba.newnode('text', + owner=self.node, + attrs={ + 'text': text, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': color_fin, + 'scale': 0.02, + 'h_align': 'center' + }) + mnode.connectattr('output', self._score_text, 'position') + else: + self._score_text.color = color_fin + assert isinstance(self._score_text.scale, float) + start_scale = self._score_text.scale + self._score_text.text = text + if flash: + combine = ba.newnode('combine', + owner=self._score_text, + attrs={'size': 3}) + scl = 1.8 + offs = 0.5 + tval = 0.300 + for i in range(3): + cl1 = offs + scl * color_fin[i] + cl2 = color_fin[i] + ba.animate(combine, 'input' + str(i), { + 0.5 * tval: cl2, + 0.75 * tval: cl1, + 1.0 * tval: cl2 + }) + combine.connectattr('output', self._score_text, 'color') + + ba.animate(self._score_text, 'scale', {0.0: start_scale, 0.2: 0.02}) + self._score_text_hide_timer = ba.Timer( + 1.0, ba.WeakCall(self._hide_score_text)) + + def on_jump_press(self) -> None: + """ + Called to 'press jump' on this spaz; + used by player or AI connections. + """ + if not self.node: + return + self.node.jump_pressed = True + self._turbo_filter_add_press('jump') + + def on_jump_release(self) -> None: + """ + Called to 'release jump' on this spaz; + used by player or AI connections. + """ + if not self.node: + return + self.node.jump_pressed = False + + def on_pickup_press(self) -> None: + """ + Called to 'press pick-up' on this spaz; + used by player or AI connections. + """ + if not self.node: + return + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + if t_ms - self.last_pickup_time_ms >= self._pickup_cooldown: + self.node.pickup_pressed = True + self.last_pickup_time_ms = t_ms + self._turbo_filter_add_press('pickup') + + def on_pickup_release(self) -> None: + """ + Called to 'release pick-up' on this spaz; + used by player or AI connections. + """ + if not self.node: + return + self.node.pickup_pressed = False + + def on_hold_position_press(self) -> None: + """ + Called to 'press hold-position' on this spaz; + used for player or AI connections. + """ + if not self.node: + return + self.node.hold_position_pressed = True + self._turbo_filter_add_press('holdposition') + + def on_hold_position_release(self) -> None: + """ + Called to 'release hold-position' on this spaz; + used for player or AI connections. + """ + if not self.node: + return + self.node.hold_position_pressed = False + + def on_punch_press(self) -> None: + """ + Called to 'press punch' on this spaz; + used for player or AI connections. + """ + if not self.node or self.frozen or self.node.knockout > 0.0: + return + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + if t_ms - self.last_punch_time_ms >= self._punch_cooldown: + if self.punch_callback is not None: + self.punch_callback(self) + self._punched_nodes = set() # Reset this. + self.last_punch_time_ms = t_ms + self.node.punch_pressed = True + if not self.node.hold_node: + ba.timer( + 0.1, + ba.WeakCall(self._safe_play_sound, + SpazFactory.get().swish_sound, 0.8)) + self._turbo_filter_add_press('punch') + + def _safe_play_sound(self, sound: ba.Sound, volume: float) -> None: + """Plays a sound at our position if we exist.""" + if self.node: + ba.playsound(sound, volume, self.node.position) + + def on_punch_release(self) -> None: + """ + Called to 'release punch' on this spaz; + used for player or AI connections. + """ + if not self.node: + return + self.node.punch_pressed = False + + def on_bomb_press(self) -> None: + """ + Called to 'press bomb' on this spaz; + used for player or AI connections. + """ + if not self.node: + return + + if self._dead or self.frozen: + return + if self.node.knockout > 0.0: + return + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + if t_ms - self.last_bomb_time_ms >= self._bomb_cooldown: + self.last_bomb_time_ms = t_ms + self.node.bomb_pressed = True + if not self.node.hold_node: + self.drop_bomb() + self._turbo_filter_add_press('bomb') + + def on_bomb_release(self) -> None: + """ + Called to 'release bomb' on this spaz; + used for player or AI connections. + """ + if not self.node: + return + self.node.bomb_pressed = False + + def on_run(self, value: float) -> None: + """ + Called to 'press run' on this spaz; + used for player or AI connections. + """ + if not self.node: + return + + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.last_run_time_ms = t_ms + self.node.run = value + + # filtering these events would be tough since its an analog + # value, but lets still pass full 0-to-1 presses along to + # the turbo filter to punish players if it looks like they're turbo-ing + if self._last_run_value < 0.01 and value > 0.99: + self._turbo_filter_add_press('run') + + self._last_run_value = value + + def on_fly_press(self) -> None: + """ + Called to 'press fly' on this spaz; + used for player or AI connections. + """ + if not self.node: + return + # not adding a cooldown time here for now; slightly worried + # input events get clustered up during net-games and we'd wind up + # killing a lot and making it hard to fly.. should look into this. + self.node.fly_pressed = True + self._turbo_filter_add_press('fly') + + def on_fly_release(self) -> None: + """ + Called to 'release fly' on this spaz; + used for player or AI connections. + """ + if not self.node: + return + self.node.fly_pressed = False + + def on_move(self, x: float, y: float) -> None: + """ + Called to set the joystick amount for this spaz; + used for player or AI connections. + """ + if not self.node: + return + self.node.handlemessage('move', x, y) + + def on_move_up_down(self, value: float) -> None: + """ + Called to set the up/down joystick amount on this spaz; + used for player or AI connections. + value will be between -32768 to 32767 + WARNING: deprecated; use on_move instead. + """ + if not self.node: + return + self.node.move_up_down = value + + def on_move_left_right(self, value: float) -> None: + """ + Called to set the left/right joystick amount on this spaz; + used for player or AI connections. + value will be between -32768 to 32767 + WARNING: deprecated; use on_move instead. + """ + if not self.node: + return + self.node.move_left_right = value + + def on_punched(self, damage: int) -> None: + """Called when this spaz gets punched.""" + + def get_death_points(self, how: ba.DeathType) -> Tuple[int, int]: + """Get the points awarded for killing this spaz.""" + del how # Unused. + num_hits = float(max(1, self._num_times_hit)) + + # Base points is simply 10 for 1-hit-kills and 5 otherwise. + importance = 2 if num_hits < 2 else 1 + return (10 if num_hits < 2 else 5) * self.points_mult, importance + + def curse(self) -> None: + """ + Give this poor spaz a curse; + he will explode in 5 seconds. + """ + if not self._cursed: + factory = SpazFactory.get() + self._cursed = True + + # Add the curse material. + for attr in ['materials', 'roller_materials']: + materials = getattr(self.node, attr) + if factory.curse_material not in materials: + setattr(self.node, attr, + materials + (factory.curse_material, )) + + # None specifies no time limit + assert self.node + if self.curse_time is None: + self.node.curse_death_time = -1 + else: + # Note: curse-death-time takes milliseconds. + tval = ba.time() + assert isinstance(tval, (float, int)) + self.node.curse_death_time = int(1000.0 * + (tval + self.curse_time)) + ba.timer(5.0, ba.WeakCall(self.curse_explode)) + + def equip_boxing_gloves(self) -> None: + """ + Give this spaz some boxing gloves. + """ + assert self.node + self.node.boxing_gloves = True + if self._demo_mode: # Preserve old behavior. + self._punch_power_scale = 1.7 + self._punch_cooldown = 300 + else: + factory = SpazFactory.get() + self._punch_power_scale = factory.punch_power_scale_gloves + self._punch_cooldown = factory.punch_cooldown_gloves + + def equip_shields(self, decay: bool = False) -> None: + """ + Give this spaz a nice energy shield. + """ + + if not self.node: + ba.print_error('Can\'t equip shields; no node.') + return + + factory = SpazFactory.get() + if self.shield is None: + self.shield = ba.newnode('shield', + owner=self.node, + attrs={ + 'color': (0.3, 0.2, 2.0), + 'radius': 1.3 + }) + self.node.connectattr('position_center', self.shield, 'position') + self.shield_hitpoints = self.shield_hitpoints_max = 650 + self.shield_decay_rate = factory.shield_decay_rate if decay else 0 + self.shield.hurt = 0 + ba.playsound(factory.shield_up_sound, 1.0, position=self.node.position) + + if self.shield_decay_rate > 0: + self.shield_decay_timer = ba.Timer(0.5, + ba.WeakCall(self.shield_decay), + repeat=True) + # So user can see the decay. + self.shield.always_show_health_bar = True + + def shield_decay(self) -> None: + """Called repeatedly to decay shield HP over time.""" + if self.shield: + assert self.shield_hitpoints is not None + self.shield_hitpoints = (max( + 0, self.shield_hitpoints - self.shield_decay_rate)) + assert self.shield_hitpoints is not None + self.shield.hurt = ( + 1.0 - float(self.shield_hitpoints) / self.shield_hitpoints_max) + if self.shield_hitpoints <= 0: + self.shield.delete() + self.shield = None + self.shield_decay_timer = None + assert self.node + ba.playsound(SpazFactory.get().shield_down_sound, + 1.0, + position=self.node.position) + else: + self.shield_decay_timer = None + + def handlemessage(self, msg: Any) -> Any: + # pylint: disable=too-many-return-statements + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + assert not self.expired + + if isinstance(msg, ba.PickedUpMessage): + if self.node: + self.node.handlemessage('hurt_sound') + self.node.handlemessage('picked_up') + + # This counts as a hit. + self._num_times_hit += 1 + + elif isinstance(msg, ba.ShouldShatterMessage): + # Eww; seems we have to do this in a timer or it wont work right. + # (since we're getting called from within update() perhaps?..) + # NOTE: should test to see if that's still the case. + ba.timer(0.001, ba.WeakCall(self.shatter)) + + elif isinstance(msg, ba.ImpactDamageMessage): + # Eww; seems we have to do this in a timer or it wont work right. + # (since we're getting called from within update() perhaps?..) + ba.timer(0.001, ba.WeakCall(self._hit_self, msg.intensity)) + + elif isinstance(msg, ba.PowerupMessage): + if self._dead or not self.node: + return True + if self.pick_up_powerup_callback is not None: + self.pick_up_powerup_callback(self) + if msg.poweruptype == 'triple_bombs': + tex = PowerupBoxFactory.get().tex_bomb + self._flash_billboard(tex) + self.set_bomb_count(3) + if self.powerups_expire: + self.node.mini_billboard_1_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_1_start_time = t_ms + self.node.mini_billboard_1_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._multi_bomb_wear_off_timer = (ba.Timer( + (POWERUP_WEAR_OFF_TIME - 2000), + ba.WeakCall(self._multi_bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._multi_bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._multi_bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'land_mines': + self.set_land_mine_count(min(self.land_mine_count + 3, 3)) + elif msg.poweruptype == 'impact_bombs': + self.bomb_type = 'impact' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'sticky_bombs': + self.bomb_type = 'sticky' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'punch': + self._has_boxing_gloves = True + tex = PowerupBoxFactory.get().tex_punch + self._flash_billboard(tex) + self.equip_boxing_gloves() + if self.powerups_expire: + self.node.boxing_gloves_flashing = False + self.node.mini_billboard_3_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_3_start_time = t_ms + self.node.mini_billboard_3_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._boxing_gloves_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._gloves_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._boxing_gloves_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._gloves_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'shield': + factory = SpazFactory.get() + + # Let's allow powerup-equipped shields to lose hp over time. + self.equip_shields(decay=factory.shield_decay_rate > 0) + elif msg.poweruptype == 'curse': + self.curse() + elif msg.poweruptype == 'ice_bombs': + self.bomb_type = 'ice' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'health': + if self._cursed: + self._cursed = False + + # Remove cursed material. + factory = SpazFactory.get() + for attr in ['materials', 'roller_materials']: + materials = getattr(self.node, attr) + if factory.curse_material in materials: + setattr( + self.node, attr, + tuple(m for m in materials + if m != factory.curse_material)) + self.node.curse_death_time = 0 + self.hitpoints = self.hitpoints_max + self._flash_billboard(PowerupBoxFactory.get().tex_health) + self.node.hurt = 0 + self._last_hit_time = None + self._num_times_hit = 0 + + self.node.handlemessage('flash') + if msg.sourcenode: + msg.sourcenode.handlemessage(ba.PowerupAcceptMessage()) + return True + + elif isinstance(msg, ba.FreezeMessage): + if not self.node: + return None + if self.node.invincible: + ba.playsound(SpazFactory.get().block_sound, + 1.0, + position=self.node.position) + return None + if self.shield: + return None + if not self.frozen: + self.frozen = True + self.node.frozen = True + ba.timer(5.0, ba.WeakCall(self.handlemessage, + ba.ThawMessage())) + # Instantly shatter if we're already dead. + # (otherwise its hard to tell we're dead) + if self.hitpoints <= 0: + self.shatter() + + elif isinstance(msg, ba.ThawMessage): + if self.frozen and not self.shattered and self.node: + self.frozen = False + self.node.frozen = False + + elif isinstance(msg, ba.HitMessage): + if not self.node: + return None + if self.node.invincible: + ba.playsound(SpazFactory.get().block_sound, + 1.0, + position=self.node.position) + return True + + # If we were recently hit, don't count this as another. + # (so punch flurries and bomb pileups essentially count as 1 hit) + local_time = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(local_time, int) + if (self._last_hit_time is None + or local_time - self._last_hit_time > 1000): + self._num_times_hit += 1 + self._last_hit_time = local_time + + mag = msg.magnitude * self.impact_scale + velocity_mag = msg.velocity_magnitude * self.impact_scale + damage_scale = 0.22 + + # If they've got a shield, deliver it to that instead. + if self.shield: + if msg.flat_damage: + damage = msg.flat_damage * self.impact_scale + else: + # Hit our spaz with an impulse but tell it to only return + # theoretical damage; not apply the impulse. + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, 1, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + damage = damage_scale * self.node.damage + + assert self.shield_hitpoints is not None + self.shield_hitpoints -= int(damage) + self.shield.hurt = ( + 1.0 - + float(self.shield_hitpoints) / self.shield_hitpoints_max) + + # Its a cleaner event if a hit just kills the shield + # without damaging the player. + # However, massive damage events should still be able to + # damage the player. This hopefully gives us a happy medium. + max_spillover = SpazFactory.get().max_shield_spillover_damage + if self.shield_hitpoints <= 0: + + # FIXME: Transition out perhaps? + self.shield.delete() + self.shield = None + ba.playsound(SpazFactory.get().shield_down_sound, + 1.0, + position=self.node.position) + + # Emit some cool looking sparks when the shield dies. + npos = self.node.position + ba.emitfx(position=(npos[0], npos[1] + 0.9, npos[2]), + velocity=self.node.velocity, + count=random.randrange(20, 30), + scale=1.0, + spread=0.6, + chunk_type='spark') + + else: + ba.playsound(SpazFactory.get().shield_hit_sound, + 0.5, + position=self.node.position) + + # Emit some cool looking sparks on shield hit. + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 1.0, + msg.force_direction[1] * 1.0, + msg.force_direction[2] * 1.0), + count=min(30, 5 + int(damage * 0.005)), + scale=0.5, + spread=0.3, + chunk_type='spark') + + # If they passed our spillover threshold, + # pass damage along to spaz. + if self.shield_hitpoints <= -max_spillover: + leftover_damage = -max_spillover - self.shield_hitpoints + shield_leftover_ratio = leftover_damage / damage + + # Scale down the magnitudes applied to spaz accordingly. + mag *= shield_leftover_ratio + velocity_mag *= shield_leftover_ratio + else: + return True # Good job shield! + else: + shield_leftover_ratio = 1.0 + + if msg.flat_damage: + damage = int(msg.flat_damage * self.impact_scale * + shield_leftover_ratio) + else: + # Hit it with an impulse and get the resulting damage. + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, 0, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + + damage = int(damage_scale * self.node.damage) + self.node.handlemessage('hurt_sound') + + # Play punch impact sound based on damage if it was a punch. + if msg.hit_type == 'punch': + self.on_punched(damage) + + # If damage was significant, lets show it. + if damage > 350: + assert msg.force_direction is not None + ba.show_damage_count('-' + str(int(damage / 10)) + '%', + msg.pos, msg.force_direction) + + # Let's always add in a super-punch sound with boxing + # gloves just to differentiate them. + if msg.hit_subtype == 'super_punch': + ba.playsound(SpazFactory.get().punch_sound_stronger, + 1.0, + position=self.node.position) + if damage > 500: + sounds = SpazFactory.get().punch_sound_strong + sound = sounds[random.randrange(len(sounds))] + else: + sound = SpazFactory.get().punch_sound + ba.playsound(sound, 1.0, position=self.node.position) + + # Throw up some chunks. + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 0.5, + msg.force_direction[1] * 0.5, + msg.force_direction[2] * 0.5), + count=min(10, 1 + int(damage * 0.0025)), + scale=0.3, + spread=0.03) + + ba.emitfx(position=msg.pos, + chunk_type='sweat', + velocity=(msg.force_direction[0] * 1.3, + msg.force_direction[1] * 1.3 + 5.0, + msg.force_direction[2] * 1.3), + count=min(30, 1 + int(damage * 0.04)), + scale=0.9, + spread=0.28) + + # Momentary flash. + hurtiness = damage * 0.003 + punchpos = (msg.pos[0] + msg.force_direction[0] * 0.02, + msg.pos[1] + msg.force_direction[1] * 0.02, + msg.pos[2] + msg.force_direction[2] * 0.02) + flash_color = (1.0, 0.8, 0.4) + light = ba.newnode( + 'light', + attrs={ + 'position': punchpos, + 'radius': 0.12 + hurtiness * 0.12, + 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), + 'height_attenuated': False, + 'color': flash_color + }) + ba.timer(0.06, light.delete) + + flash = ba.newnode('flash', + attrs={ + 'position': punchpos, + 'size': 0.17 + 0.17 * hurtiness, + 'color': flash_color + }) + ba.timer(0.06, flash.delete) + + if msg.hit_type == 'impact': + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 2.0, + msg.force_direction[1] * 2.0, + msg.force_direction[2] * 2.0), + count=min(10, 1 + int(damage * 0.01)), + scale=0.4, + spread=0.1) + if self.hitpoints > 0: + + # It's kinda crappy to die from impacts, so lets reduce + # impact damage by a reasonable amount *if* it'll keep us alive + if msg.hit_type == 'impact' and damage > self.hitpoints: + # Drop damage to whatever puts us at 10 hit points, + # or 200 less than it used to be whichever is greater + # (so it *can* still kill us if its high enough) + newdamage = max(damage - 200, self.hitpoints - 10) + damage = newdamage + self.node.handlemessage('flash') + + # If we're holding something, drop it. + if damage > 0.0 and self.node.hold_node: + self.node.hold_node = None + self.hitpoints -= damage + self.node.hurt = 1.0 - float( + self.hitpoints) / self.hitpoints_max + + # If we're cursed, *any* damage blows us up. + if self._cursed and damage > 0: + ba.timer( + 0.05, + ba.WeakCall(self.curse_explode, + msg.get_source_player(ba.Player))) + + # If we're frozen, shatter.. otherwise die if we hit zero + if self.frozen and (damage > 200 or self.hitpoints <= 0): + self.shatter() + elif self.hitpoints <= 0: + self.node.handlemessage( + ba.DieMessage(how=ba.DeathType.IMPACT)) + + # If we're dead, take a look at the smoothed damage value + # (which gives us a smoothed average of recent damage) and shatter + # us if its grown high enough. + if self.hitpoints <= 0: + damage_avg = self.node.damage_smoothed * damage_scale + if damage_avg > 1000: + self.shatter() + + elif isinstance(msg, BombDiedMessage): + self.bomb_count += 1 + + elif isinstance(msg, ba.DieMessage): + wasdead = self._dead + self._dead = True + self.hitpoints = 0 + if msg.immediate: + if self.node: + self.node.delete() + elif self.node: + self.node.hurt = 1.0 + if self.play_big_death_sound and not wasdead: + ba.playsound(SpazFactory.get().single_player_death_sound) + self.node.dead = True + ba.timer(2.0, self.node.delete) + + elif isinstance(msg, ba.OutOfBoundsMessage): + # By default we just die here. + self.handlemessage(ba.DieMessage(how=ba.DeathType.FALL)) + + elif isinstance(msg, ba.StandMessage): + self._last_stand_pos = (msg.position[0], msg.position[1], + msg.position[2]) + if self.node: + self.node.handlemessage('stand', msg.position[0], + msg.position[1], msg.position[2], + msg.angle) + + elif isinstance(msg, CurseExplodeMessage): + self.curse_explode() + + elif isinstance(msg, PunchHitMessage): + if not self.node: + return None + node = ba.getcollision().opposingnode + + # Only allow one hit per node per punch. + if node and (node not in self._punched_nodes): + + punch_momentum_angular = (self.node.punch_momentum_angular * + self._punch_power_scale) + punch_power = self.node.punch_power * self._punch_power_scale + + # Ok here's the deal: we pass along our base velocity for use + # in the impulse damage calculations since that is a more + # predictable value than our fist velocity, which is rather + # erratic. However, we want to actually apply force in the + # direction our fist is moving so it looks better. So we still + # pass that along as a direction. Perhaps a time-averaged + # fist-velocity would work too?.. perhaps should try that. + + # If its something besides another spaz, just do a muffled + # punch sound. + if node.getnodetype() != 'spaz': + sounds = SpazFactory.get().impact_sounds_medium + sound = sounds[random.randrange(len(sounds))] + ba.playsound(sound, 1.0, position=self.node.position) + + ppos = self.node.punch_position + punchdir = self.node.punch_velocity + vel = self.node.punch_momentum_linear + + self._punched_nodes.add(node) + node.handlemessage( + ba.HitMessage( + pos=ppos, + velocity=vel, + magnitude=punch_power * punch_momentum_angular * 110.0, + velocity_magnitude=punch_power * 40, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=punchdir, + hit_type='punch', + hit_subtype=('super_punch' if self._has_boxing_gloves + else 'default'))) + + # Also apply opposite to ourself for the first punch only. + # This is given as a constant force so that it is more + # noticeable for slower punches where it matters. For fast + # awesome looking punches its ok if we punch 'through' + # the target. + mag = -400.0 + if self._hockey: + mag *= 0.5 + if len(self._punched_nodes) == 1: + self.node.handlemessage('kick_back', ppos[0], ppos[1], + ppos[2], punchdir[0], punchdir[1], + punchdir[2], mag) + elif isinstance(msg, PickupMessage): + if not self.node: + return None + + try: + collision = ba.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except ba.NotFoundError: + return True + + # Don't allow picking up of invincible dudes. + try: + if opposingnode.invincible: + return True + except Exception: + pass + + # If we're grabbing the pelvis of a non-shattered spaz, we wanna + # grab the torso instead. + if (opposingnode.getnodetype() == 'spaz' + and not opposingnode.shattered and opposingbody == 4): + opposingbody = 1 + + # Special case - if we're holding a flag, don't replace it + # (hmm - should make this customizable or more low level). + held = self.node.hold_node + if held and held.getnodetype() == 'flag': + return True + + # Note: hold_body needs to be set before hold_node. + self.node.hold_body = opposingbody + self.node.hold_node = opposingnode + elif isinstance(msg, ba.CelebrateMessage): + if self.node: + self.node.handlemessage('celebrate', int(msg.duration * 1000)) + + else: + return super().handlemessage(msg) + return None + + def drop_bomb(self) -> Optional[stdbomb.Bomb]: + """ + Tell the spaz to drop one of his bombs, and returns + the resulting bomb object. + If the spaz has no bombs or is otherwise unable to + drop a bomb, returns None. + """ + + if (self.land_mine_count <= 0 and self.bomb_count <= 0) or self.frozen: + return None + assert self.node + pos = self.node.position_forward + vel = self.node.velocity + + if self.land_mine_count > 0: + dropping_bomb = False + self.set_land_mine_count(self.land_mine_count - 1) + bomb_type = 'land_mine' + else: + dropping_bomb = True + bomb_type = self.bomb_type + + bomb = stdbomb.Bomb(position=(pos[0], pos[1] - 0.0, pos[2]), + velocity=(vel[0], vel[1], vel[2]), + bomb_type=bomb_type, + blast_radius=self.blast_radius, + source_player=self.source_player, + owner=self.node).autoretain() + + assert bomb.node + if dropping_bomb: + self.bomb_count -= 1 + bomb.node.add_death_action( + ba.WeakCall(self.handlemessage, BombDiedMessage())) + self._pick_up(bomb.node) + + for clb in self._dropped_bomb_callbacks: + clb(self, bomb) + + return bomb + + def _pick_up(self, node: ba.Node) -> None: + if self.node: + # Note: hold_body needs to be set before hold_node. + self.node.hold_body = 0 + self.node.hold_node = node + + def set_land_mine_count(self, count: int) -> None: + """Set the number of land-mines this spaz is carrying.""" + self.land_mine_count = count + if self.node: + if self.land_mine_count != 0: + self.node.counter_text = 'x' + str(self.land_mine_count) + self.node.counter_texture = ( + PowerupBoxFactory.get().tex_land_mines) + else: + self.node.counter_text = '' + + def curse_explode(self, source_player: ba.Player = None) -> None: + """Explode the poor spaz spectacularly.""" + if self._cursed and self.node: + self.shatter(extreme=True) + self.handlemessage(ba.DieMessage()) + activity = self._activity() + if activity: + stdbomb.Blast( + position=self.node.position, + velocity=self.node.velocity, + blast_radius=3.0, + blast_type='normal', + source_player=(source_player if source_player else + self.source_player)).autoretain() + self._cursed = False + + def shatter(self, extreme: bool = False) -> None: + """Break the poor spaz into little bits.""" + if self.shattered: + return + self.shattered = True + assert self.node + if self.frozen: + # Momentary flash of light. + light = ba.newnode('light', + attrs={ + 'position': self.node.position, + 'radius': 0.5, + 'height_attenuated': False, + 'color': (0.8, 0.8, 1.0) + }) + + ba.animate(light, 'intensity', { + 0.0: 3.0, + 0.04: 0.5, + 0.08: 0.07, + 0.3: 0 + }) + ba.timer(0.3, light.delete) + + # Emit ice chunks. + ba.emitfx(position=self.node.position, + velocity=self.node.velocity, + count=int(random.random() * 10.0 + 10.0), + scale=0.6, + spread=0.2, + chunk_type='ice') + ba.emitfx(position=self.node.position, + velocity=self.node.velocity, + count=int(random.random() * 10.0 + 10.0), + scale=0.3, + spread=0.2, + chunk_type='ice') + ba.playsound(SpazFactory.get().shatter_sound, + 1.0, + position=self.node.position) + else: + ba.playsound(SpazFactory.get().splatter_sound, + 1.0, + position=self.node.position) + self.handlemessage(ba.DieMessage()) + self.node.shattered = 2 if extreme else 1 + + def _hit_self(self, intensity: float) -> None: + if not self.node: + return + pos = self.node.position + self.handlemessage( + ba.HitMessage(flat_damage=50.0 * intensity, + pos=pos, + force_direction=self.node.velocity, + hit_type='impact')) + self.node.handlemessage('knockout', max(0.0, 50.0 * intensity)) + sounds: Sequence[ba.Sound] + if intensity > 5.0: + sounds = SpazFactory.get().impact_sounds_harder + elif intensity > 3.0: + sounds = SpazFactory.get().impact_sounds_hard + else: + sounds = SpazFactory.get().impact_sounds_medium + sound = sounds[random.randrange(len(sounds))] + ba.playsound(sound, position=pos, volume=5.0) + + def _get_bomb_type_tex(self) -> ba.Texture: + factory = PowerupBoxFactory.get() + if self.bomb_type == 'sticky': + return factory.tex_sticky_bombs + if self.bomb_type == 'ice': + return factory.tex_ice_bombs + if self.bomb_type == 'impact': + return factory.tex_impact_bombs + raise ValueError('invalid bomb type') + + def _flash_billboard(self, tex: ba.Texture) -> None: + assert self.node + self.node.billboard_texture = tex + self.node.billboard_cross_out = False + ba.animate(self.node, 'billboard_opacity', { + 0.0: 0.0, + 0.1: 1.0, + 0.4: 1.0, + 0.5: 0.0 + }) + + def set_bomb_count(self, count: int) -> None: + """Sets the number of bombs this Spaz has.""" + # We can't just set bomb_count because some bombs may be laid currently + # so we have to do a relative diff based on max. + diff = count - self._max_bomb_count + self._max_bomb_count += diff + self.bomb_count += diff + + def _gloves_wear_off_flash(self) -> None: + if self.node: + self.node.boxing_gloves_flashing = True + self.node.billboard_texture = PowerupBoxFactory.get().tex_punch + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + + def _gloves_wear_off(self) -> None: + if self._demo_mode: # Preserve old behavior. + self._punch_power_scale = 1.2 + self._punch_cooldown = BASE_PUNCH_COOLDOWN + else: + factory = SpazFactory.get() + self._punch_power_scale = factory.punch_power_scale + self._punch_cooldown = factory.punch_cooldown + self._has_boxing_gloves = False + if self.node: + ba.playsound(PowerupBoxFactory.get().powerdown_sound, + position=self.node.position) + self.node.boxing_gloves = False + self.node.billboard_opacity = 0.0 + + def _multi_bomb_wear_off_flash(self) -> None: + if self.node: + self.node.billboard_texture = PowerupBoxFactory.get().tex_bomb + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + + def _multi_bomb_wear_off(self) -> None: + self.set_bomb_count(self.default_bomb_count) + if self.node: + ba.playsound(PowerupBoxFactory.get().powerdown_sound, + position=self.node.position) + self.node.billboard_opacity = 0.0 + + def _bomb_wear_off_flash(self) -> None: + if self.node: + self.node.billboard_texture = self._get_bomb_type_tex() + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + + def _bomb_wear_off(self) -> None: + self.bomb_type = self.bomb_type_default + if self.node: + ba.playsound(PowerupBoxFactory.get().powerdown_sound, + position=self.node.position) + self.node.billboard_opacity = 0.0 diff --git a/dist/ba_data/python/bastd/actor/spazappearance.py b/dist/ba_data/python/bastd/actor/spazappearance.py new file mode 100644 index 0000000..166b1f8 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/spazappearance.py @@ -0,0 +1,949 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Appearance functionality for spazzes.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import List, Optional, Tuple + + +def get_appearances(include_locked: bool = False) -> List[str]: + """Get the list of available spaz appearances.""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + disallowed = [] + if not include_locked: + # hmm yeah this'll be tough to hack... + if not _ba.get_purchased('characters.santa'): + disallowed.append('Santa Claus') + if not _ba.get_purchased('characters.frosty'): + disallowed.append('Frosty') + if not _ba.get_purchased('characters.bones'): + disallowed.append('Bones') + if not _ba.get_purchased('characters.bernard'): + disallowed.append('Bernard') + if not _ba.get_purchased('characters.pixie'): + disallowed.append('Pixel') + if not _ba.get_purchased('characters.pascal'): + disallowed.append('Pascal') + if not _ba.get_purchased('characters.actionhero'): + disallowed.append('Todd McBurton') + if not _ba.get_purchased('characters.taobaomascot'): + disallowed.append('Taobao Mascot') + if not _ba.get_purchased('characters.agent'): + disallowed.append('Agent Johnson') + if not _ba.get_purchased('characters.jumpsuit'): + disallowed.append('Lee') + if not _ba.get_purchased('characters.assassin'): + disallowed.append('Zola') + if not _ba.get_purchased('characters.wizard'): + disallowed.append('Grumbledorf') + if not _ba.get_purchased('characters.cowboy'): + disallowed.append('Butch') + if not _ba.get_purchased('characters.witch'): + disallowed.append('Witch') + if not _ba.get_purchased('characters.warrior'): + disallowed.append('Warrior') + if not _ba.get_purchased('characters.superhero'): + disallowed.append('Middle-Man') + if not _ba.get_purchased('characters.alien'): + disallowed.append('Alien') + if not _ba.get_purchased('characters.oldlady'): + disallowed.append('OldLady') + if not _ba.get_purchased('characters.gladiator'): + disallowed.append('Gladiator') + if not _ba.get_purchased('characters.wrestler'): + disallowed.append('Wrestler') + if not _ba.get_purchased('characters.operasinger'): + disallowed.append('Gretel') + if not _ba.get_purchased('characters.robot'): + disallowed.append('Robot') + if not _ba.get_purchased('characters.cyborg'): + disallowed.append('B-9000') + if not _ba.get_purchased('characters.bunny'): + disallowed.append('Easter Bunny') + if not _ba.get_purchased('characters.kronk'): + disallowed.append('Kronk') + if not _ba.get_purchased('characters.zoe'): + disallowed.append('Zoe') + if not _ba.get_purchased('characters.jackmorgan'): + disallowed.append('Jack Morgan') + if not _ba.get_purchased('characters.mel'): + disallowed.append('Mel') + if not _ba.get_purchased('characters.snakeshadow'): + disallowed.append('Snake Shadow') + return [ + s for s in list(ba.app.spaz_appearances.keys()) if s not in disallowed + ] + + +class Appearance: + """Create and fill out one of these suckers to define a spaz appearance""" + + def __init__(self, name: str): + self.name = name + if self.name in ba.app.spaz_appearances: + raise Exception('spaz appearance name "' + self.name + + '" already exists.') + ba.app.spaz_appearances[self.name] = self + self.color_texture = '' + self.color_mask_texture = '' + self.icon_texture = '' + self.icon_mask_texture = '' + self.head_model = '' + self.torso_model = '' + self.pelvis_model = '' + self.upper_arm_model = '' + self.forearm_model = '' + self.hand_model = '' + self.upper_leg_model = '' + self.lower_leg_model = '' + self.toes_model = '' + self.jump_sounds: List[str] = [] + self.attack_sounds: List[str] = [] + self.impact_sounds: List[str] = [] + self.death_sounds: List[str] = [] + self.pickup_sounds: List[str] = [] + self.fall_sounds: List[str] = [] + self.style = 'spaz' + self.default_color: Optional[Tuple[float, float, float]] = None + self.default_highlight: Optional[Tuple[float, float, float]] = None + + +def register_appearances() -> None: + """Register our builtin spaz appearances.""" + + # this is quite ugly but will be going away so not worth cleaning up + # pylint: disable=invalid-name + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + + # Spaz ####################################### + t = Appearance('Spaz') + t.color_texture = 'neoSpazColor' + t.color_mask_texture = 'neoSpazColorMask' + t.icon_texture = 'neoSpazIcon' + t.icon_mask_texture = 'neoSpazIconColorMask' + t.head_model = 'neoSpazHead' + t.torso_model = 'neoSpazTorso' + t.pelvis_model = 'neoSpazPelvis' + t.upper_arm_model = 'neoSpazUpperArm' + t.forearm_model = 'neoSpazForeArm' + t.hand_model = 'neoSpazHand' + t.upper_leg_model = 'neoSpazUpperLeg' + t.lower_leg_model = 'neoSpazLowerLeg' + t.toes_model = 'neoSpazToes' + t.jump_sounds = ['spazJump01', 'spazJump02', 'spazJump03', 'spazJump04'] + t.attack_sounds = [ + 'spazAttack01', 'spazAttack02', 'spazAttack03', 'spazAttack04' + ] + t.impact_sounds = [ + 'spazImpact01', 'spazImpact02', 'spazImpact03', 'spazImpact04' + ] + t.death_sounds = ['spazDeath01'] + t.pickup_sounds = ['spazPickup01'] + t.fall_sounds = ['spazFall01'] + t.style = 'spaz' + + # Zoe ##################################### + t = Appearance('Zoe') + t.color_texture = 'zoeColor' + t.color_mask_texture = 'zoeColorMask' + t.default_color = (0.6, 0.6, 0.6) + t.default_highlight = (0, 1, 0) + t.icon_texture = 'zoeIcon' + t.icon_mask_texture = 'zoeIconColorMask' + t.head_model = 'zoeHead' + t.torso_model = 'zoeTorso' + t.pelvis_model = 'zoePelvis' + t.upper_arm_model = 'zoeUpperArm' + t.forearm_model = 'zoeForeArm' + t.hand_model = 'zoeHand' + t.upper_leg_model = 'zoeUpperLeg' + t.lower_leg_model = 'zoeLowerLeg' + t.toes_model = 'zoeToes' + t.jump_sounds = ['zoeJump01', 'zoeJump02', 'zoeJump03'] + t.attack_sounds = [ + 'zoeAttack01', 'zoeAttack02', 'zoeAttack03', 'zoeAttack04' + ] + t.impact_sounds = [ + 'zoeImpact01', 'zoeImpact02', 'zoeImpact03', 'zoeImpact04' + ] + t.death_sounds = ['zoeDeath01'] + t.pickup_sounds = ['zoePickup01'] + t.fall_sounds = ['zoeFall01'] + t.style = 'female' + + # Ninja ########################################## + t = Appearance('Snake Shadow') + t.color_texture = 'ninjaColor' + t.color_mask_texture = 'ninjaColorMask' + t.default_color = (1, 1, 1) + t.default_highlight = (0.55, 0.8, 0.55) + t.icon_texture = 'ninjaIcon' + t.icon_mask_texture = 'ninjaIconColorMask' + t.head_model = 'ninjaHead' + t.torso_model = 'ninjaTorso' + t.pelvis_model = 'ninjaPelvis' + t.upper_arm_model = 'ninjaUpperArm' + t.forearm_model = 'ninjaForeArm' + t.hand_model = 'ninjaHand' + t.upper_leg_model = 'ninjaUpperLeg' + t.lower_leg_model = 'ninjaLowerLeg' + t.toes_model = 'ninjaToes' + ninja_attacks = ['ninjaAttack' + str(i + 1) + '' for i in range(7)] + ninja_hits = ['ninjaHit' + str(i + 1) + '' for i in range(8)] + ninja_jumps = ['ninjaAttack' + str(i + 1) + '' for i in range(7)] + t.jump_sounds = ninja_jumps + t.attack_sounds = ninja_attacks + t.impact_sounds = ninja_hits + t.death_sounds = ['ninjaDeath1'] + t.pickup_sounds = ninja_attacks + t.fall_sounds = ['ninjaFall1'] + t.style = 'ninja' + + # Barbarian ##################################### + t = Appearance('Kronk') + t.color_texture = 'kronk' + t.color_mask_texture = 'kronkColorMask' + t.default_color = (0.4, 0.5, 0.4) + t.default_highlight = (1, 0.5, 0.3) + t.icon_texture = 'kronkIcon' + t.icon_mask_texture = 'kronkIconColorMask' + t.head_model = 'kronkHead' + t.torso_model = 'kronkTorso' + t.pelvis_model = 'kronkPelvis' + t.upper_arm_model = 'kronkUpperArm' + t.forearm_model = 'kronkForeArm' + t.hand_model = 'kronkHand' + t.upper_leg_model = 'kronkUpperLeg' + t.lower_leg_model = 'kronkLowerLeg' + t.toes_model = 'kronkToes' + kronk_sounds = [ + 'kronk1', 'kronk2', 'kronk3', 'kronk4', 'kronk5', 'kronk6', 'kronk7', + 'kronk8', 'kronk9', 'kronk10' + ] + t.jump_sounds = kronk_sounds + t.attack_sounds = kronk_sounds + t.impact_sounds = kronk_sounds + t.death_sounds = ['kronkDeath'] + t.pickup_sounds = kronk_sounds + t.fall_sounds = ['kronkFall'] + t.style = 'kronk' + + # Chef ########################################### + t = Appearance('Mel') + t.color_texture = 'melColor' + t.color_mask_texture = 'melColorMask' + t.default_color = (1, 1, 1) + t.default_highlight = (0.1, 0.6, 0.1) + t.icon_texture = 'melIcon' + t.icon_mask_texture = 'melIconColorMask' + t.head_model = 'melHead' + t.torso_model = 'melTorso' + t.pelvis_model = 'kronkPelvis' + t.upper_arm_model = 'melUpperArm' + t.forearm_model = 'melForeArm' + t.hand_model = 'melHand' + t.upper_leg_model = 'melUpperLeg' + t.lower_leg_model = 'melLowerLeg' + t.toes_model = 'melToes' + mel_sounds = [ + 'mel01', 'mel02', 'mel03', 'mel04', 'mel05', 'mel06', 'mel07', 'mel08', + 'mel09', 'mel10' + ] + t.attack_sounds = mel_sounds + t.jump_sounds = mel_sounds + t.impact_sounds = mel_sounds + t.death_sounds = ['melDeath01'] + t.pickup_sounds = mel_sounds + t.fall_sounds = ['melFall01'] + t.style = 'mel' + + # Pirate ####################################### + t = Appearance('Jack Morgan') + t.color_texture = 'jackColor' + t.color_mask_texture = 'jackColorMask' + t.default_color = (1, 0.2, 0.1) + t.default_highlight = (1, 1, 0) + t.icon_texture = 'jackIcon' + t.icon_mask_texture = 'jackIconColorMask' + t.head_model = 'jackHead' + t.torso_model = 'jackTorso' + t.pelvis_model = 'kronkPelvis' + t.upper_arm_model = 'jackUpperArm' + t.forearm_model = 'jackForeArm' + t.hand_model = 'jackHand' + t.upper_leg_model = 'jackUpperLeg' + t.lower_leg_model = 'jackLowerLeg' + t.toes_model = 'jackToes' + hit_sounds = [ + 'jackHit01', 'jackHit02', 'jackHit03', 'jackHit04', 'jackHit05', + 'jackHit06', 'jackHit07' + ] + sounds = ['jack01', 'jack02', 'jack03', 'jack04', 'jack05', 'jack06'] + t.attack_sounds = sounds + t.jump_sounds = sounds + t.impact_sounds = hit_sounds + t.death_sounds = ['jackDeath01'] + t.pickup_sounds = sounds + t.fall_sounds = ['jackFall01'] + t.style = 'pirate' + + # Santa ###################################### + t = Appearance('Santa Claus') + t.color_texture = 'santaColor' + t.color_mask_texture = 'santaColorMask' + t.default_color = (1, 0, 0) + t.default_highlight = (1, 1, 1) + t.icon_texture = 'santaIcon' + t.icon_mask_texture = 'santaIconColorMask' + t.head_model = 'santaHead' + t.torso_model = 'santaTorso' + t.pelvis_model = 'kronkPelvis' + t.upper_arm_model = 'santaUpperArm' + t.forearm_model = 'santaForeArm' + t.hand_model = 'santaHand' + t.upper_leg_model = 'santaUpperLeg' + t.lower_leg_model = 'santaLowerLeg' + t.toes_model = 'santaToes' + hit_sounds = ['santaHit01', 'santaHit02', 'santaHit03', 'santaHit04'] + sounds = ['santa01', 'santa02', 'santa03', 'santa04', 'santa05'] + t.attack_sounds = sounds + t.jump_sounds = sounds + t.impact_sounds = hit_sounds + t.death_sounds = ['santaDeath'] + t.pickup_sounds = sounds + t.fall_sounds = ['santaFall'] + t.style = 'santa' + + # Snowman ################################### + t = Appearance('Frosty') + t.color_texture = 'frostyColor' + t.color_mask_texture = 'frostyColorMask' + t.default_color = (0.5, 0.5, 1) + t.default_highlight = (1, 0.5, 0) + t.icon_texture = 'frostyIcon' + t.icon_mask_texture = 'frostyIconColorMask' + t.head_model = 'frostyHead' + t.torso_model = 'frostyTorso' + t.pelvis_model = 'frostyPelvis' + t.upper_arm_model = 'frostyUpperArm' + t.forearm_model = 'frostyForeArm' + t.hand_model = 'frostyHand' + t.upper_leg_model = 'frostyUpperLeg' + t.lower_leg_model = 'frostyLowerLeg' + t.toes_model = 'frostyToes' + frosty_sounds = [ + 'frosty01', 'frosty02', 'frosty03', 'frosty04', 'frosty05' + ] + frosty_hit_sounds = ['frostyHit01', 'frostyHit02', 'frostyHit03'] + t.attack_sounds = frosty_sounds + t.jump_sounds = frosty_sounds + t.impact_sounds = frosty_hit_sounds + t.death_sounds = ['frostyDeath'] + t.pickup_sounds = frosty_sounds + t.fall_sounds = ['frostyFall'] + t.style = 'frosty' + + # Skeleton ################################ + t = Appearance('Bones') + t.color_texture = 'bonesColor' + t.color_mask_texture = 'bonesColorMask' + t.default_color = (0.6, 0.9, 1) + t.default_highlight = (0.6, 0.9, 1) + t.icon_texture = 'bonesIcon' + t.icon_mask_texture = 'bonesIconColorMask' + t.head_model = 'bonesHead' + t.torso_model = 'bonesTorso' + t.pelvis_model = 'bonesPelvis' + t.upper_arm_model = 'bonesUpperArm' + t.forearm_model = 'bonesForeArm' + t.hand_model = 'bonesHand' + t.upper_leg_model = 'bonesUpperLeg' + t.lower_leg_model = 'bonesLowerLeg' + t.toes_model = 'bonesToes' + bones_sounds = ['bones1', 'bones2', 'bones3'] + bones_hit_sounds = ['bones1', 'bones2', 'bones3'] + t.attack_sounds = bones_sounds + t.jump_sounds = bones_sounds + t.impact_sounds = bones_hit_sounds + t.death_sounds = ['bonesDeath'] + t.pickup_sounds = bones_sounds + t.fall_sounds = ['bonesFall'] + t.style = 'bones' + + # Bear ################################### + t = Appearance('Bernard') + t.color_texture = 'bearColor' + t.color_mask_texture = 'bearColorMask' + t.default_color = (0.7, 0.5, 0.0) + t.icon_texture = 'bearIcon' + t.icon_mask_texture = 'bearIconColorMask' + t.head_model = 'bearHead' + t.torso_model = 'bearTorso' + t.pelvis_model = 'bearPelvis' + t.upper_arm_model = 'bearUpperArm' + t.forearm_model = 'bearForeArm' + t.hand_model = 'bearHand' + t.upper_leg_model = 'bearUpperLeg' + t.lower_leg_model = 'bearLowerLeg' + t.toes_model = 'bearToes' + bear_sounds = ['bear1', 'bear2', 'bear3', 'bear4'] + bear_hit_sounds = ['bearHit1', 'bearHit2'] + t.attack_sounds = bear_sounds + t.jump_sounds = bear_sounds + t.impact_sounds = bear_hit_sounds + t.death_sounds = ['bearDeath'] + t.pickup_sounds = bear_sounds + t.fall_sounds = ['bearFall'] + t.style = 'bear' + + # Penguin ################################### + t = Appearance('Pascal') + t.color_texture = 'penguinColor' + t.color_mask_texture = 'penguinColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'penguinIcon' + t.icon_mask_texture = 'penguinIconColorMask' + t.head_model = 'penguinHead' + t.torso_model = 'penguinTorso' + t.pelvis_model = 'penguinPelvis' + t.upper_arm_model = 'penguinUpperArm' + t.forearm_model = 'penguinForeArm' + t.hand_model = 'penguinHand' + t.upper_leg_model = 'penguinUpperLeg' + t.lower_leg_model = 'penguinLowerLeg' + t.toes_model = 'penguinToes' + penguin_sounds = ['penguin1', 'penguin2', 'penguin3', 'penguin4'] + penguin_hit_sounds = ['penguinHit1', 'penguinHit2'] + t.attack_sounds = penguin_sounds + t.jump_sounds = penguin_sounds + t.impact_sounds = penguin_hit_sounds + t.death_sounds = ['penguinDeath'] + t.pickup_sounds = penguin_sounds + t.fall_sounds = ['penguinFall'] + t.style = 'penguin' + + # Ali ################################### + t = Appearance('Taobao Mascot') + t.color_texture = 'aliColor' + t.color_mask_texture = 'aliColorMask' + t.default_color = (1, 0.5, 0) + t.default_highlight = (1, 1, 1) + t.icon_texture = 'aliIcon' + t.icon_mask_texture = 'aliIconColorMask' + t.head_model = 'aliHead' + t.torso_model = 'aliTorso' + t.pelvis_model = 'aliPelvis' + t.upper_arm_model = 'aliUpperArm' + t.forearm_model = 'aliForeArm' + t.hand_model = 'aliHand' + t.upper_leg_model = 'aliUpperLeg' + t.lower_leg_model = 'aliLowerLeg' + t.toes_model = 'aliToes' + ali_sounds = ['ali1', 'ali2', 'ali3', 'ali4'] + ali_hit_sounds = ['aliHit1', 'aliHit2'] + t.attack_sounds = ali_sounds + t.jump_sounds = ali_sounds + t.impact_sounds = ali_hit_sounds + t.death_sounds = ['aliDeath'] + t.pickup_sounds = ali_sounds + t.fall_sounds = ['aliFall'] + t.style = 'ali' + + # cyborg ################################### + t = Appearance('B-9000') + t.color_texture = 'cyborgColor' + t.color_mask_texture = 'cyborgColorMask' + t.default_color = (0.5, 0.5, 0.5) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'cyborgIcon' + t.icon_mask_texture = 'cyborgIconColorMask' + t.head_model = 'cyborgHead' + t.torso_model = 'cyborgTorso' + t.pelvis_model = 'cyborgPelvis' + t.upper_arm_model = 'cyborgUpperArm' + t.forearm_model = 'cyborgForeArm' + t.hand_model = 'cyborgHand' + t.upper_leg_model = 'cyborgUpperLeg' + t.lower_leg_model = 'cyborgLowerLeg' + t.toes_model = 'cyborgToes' + cyborg_sounds = ['cyborg1', 'cyborg2', 'cyborg3', 'cyborg4'] + cyborg_hit_sounds = ['cyborgHit1', 'cyborgHit2'] + t.attack_sounds = cyborg_sounds + t.jump_sounds = cyborg_sounds + t.impact_sounds = cyborg_hit_sounds + t.death_sounds = ['cyborgDeath'] + t.pickup_sounds = cyborg_sounds + t.fall_sounds = ['cyborgFall'] + t.style = 'cyborg' + + # Agent ################################### + t = Appearance('Agent Johnson') + t.color_texture = 'agentColor' + t.color_mask_texture = 'agentColorMask' + t.default_color = (0.3, 0.3, 0.33) + t.default_highlight = (1, 0.5, 0.3) + t.icon_texture = 'agentIcon' + t.icon_mask_texture = 'agentIconColorMask' + t.head_model = 'agentHead' + t.torso_model = 'agentTorso' + t.pelvis_model = 'agentPelvis' + t.upper_arm_model = 'agentUpperArm' + t.forearm_model = 'agentForeArm' + t.hand_model = 'agentHand' + t.upper_leg_model = 'agentUpperLeg' + t.lower_leg_model = 'agentLowerLeg' + t.toes_model = 'agentToes' + agent_sounds = ['agent1', 'agent2', 'agent3', 'agent4'] + agent_hit_sounds = ['agentHit1', 'agentHit2'] + t.attack_sounds = agent_sounds + t.jump_sounds = agent_sounds + t.impact_sounds = agent_hit_sounds + t.death_sounds = ['agentDeath'] + t.pickup_sounds = agent_sounds + t.fall_sounds = ['agentFall'] + t.style = 'agent' + + # Jumpsuit ################################### + t = Appearance('Lee') + t.color_texture = 'jumpsuitColor' + t.color_mask_texture = 'jumpsuitColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'jumpsuitIcon' + t.icon_mask_texture = 'jumpsuitIconColorMask' + t.head_model = 'jumpsuitHead' + t.torso_model = 'jumpsuitTorso' + t.pelvis_model = 'jumpsuitPelvis' + t.upper_arm_model = 'jumpsuitUpperArm' + t.forearm_model = 'jumpsuitForeArm' + t.hand_model = 'jumpsuitHand' + t.upper_leg_model = 'jumpsuitUpperLeg' + t.lower_leg_model = 'jumpsuitLowerLeg' + t.toes_model = 'jumpsuitToes' + jumpsuit_sounds = ['jumpsuit1', 'jumpsuit2', 'jumpsuit3', 'jumpsuit4'] + jumpsuit_hit_sounds = ['jumpsuitHit1', 'jumpsuitHit2'] + t.attack_sounds = jumpsuit_sounds + t.jump_sounds = jumpsuit_sounds + t.impact_sounds = jumpsuit_hit_sounds + t.death_sounds = ['jumpsuitDeath'] + t.pickup_sounds = jumpsuit_sounds + t.fall_sounds = ['jumpsuitFall'] + t.style = 'spaz' + + # ActionHero ################################### + t = Appearance('Todd McBurton') + t.color_texture = 'actionHeroColor' + t.color_mask_texture = 'actionHeroColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'actionHeroIcon' + t.icon_mask_texture = 'actionHeroIconColorMask' + t.head_model = 'actionHeroHead' + t.torso_model = 'actionHeroTorso' + t.pelvis_model = 'actionHeroPelvis' + t.upper_arm_model = 'actionHeroUpperArm' + t.forearm_model = 'actionHeroForeArm' + t.hand_model = 'actionHeroHand' + t.upper_leg_model = 'actionHeroUpperLeg' + t.lower_leg_model = 'actionHeroLowerLeg' + t.toes_model = 'actionHeroToes' + action_hero_sounds = [ + 'actionHero1', 'actionHero2', 'actionHero3', 'actionHero4' + ] + action_hero_hit_sounds = ['actionHeroHit1', 'actionHeroHit2'] + t.attack_sounds = action_hero_sounds + t.jump_sounds = action_hero_sounds + t.impact_sounds = action_hero_hit_sounds + t.death_sounds = ['actionHeroDeath'] + t.pickup_sounds = action_hero_sounds + t.fall_sounds = ['actionHeroFall'] + t.style = 'spaz' + + # Assassin ################################### + t = Appearance('Zola') + t.color_texture = 'assassinColor' + t.color_mask_texture = 'assassinColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'assassinIcon' + t.icon_mask_texture = 'assassinIconColorMask' + t.head_model = 'assassinHead' + t.torso_model = 'assassinTorso' + t.pelvis_model = 'assassinPelvis' + t.upper_arm_model = 'assassinUpperArm' + t.forearm_model = 'assassinForeArm' + t.hand_model = 'assassinHand' + t.upper_leg_model = 'assassinUpperLeg' + t.lower_leg_model = 'assassinLowerLeg' + t.toes_model = 'assassinToes' + assassin_sounds = ['assassin1', 'assassin2', 'assassin3', 'assassin4'] + assassin_hit_sounds = ['assassinHit1', 'assassinHit2'] + t.attack_sounds = assassin_sounds + t.jump_sounds = assassin_sounds + t.impact_sounds = assassin_hit_sounds + t.death_sounds = ['assassinDeath'] + t.pickup_sounds = assassin_sounds + t.fall_sounds = ['assassinFall'] + t.style = 'spaz' + + # Wizard ################################### + t = Appearance('Grumbledorf') + t.color_texture = 'wizardColor' + t.color_mask_texture = 'wizardColorMask' + t.default_color = (0.2, 0.4, 1.0) + t.default_highlight = (0.06, 0.15, 0.4) + t.icon_texture = 'wizardIcon' + t.icon_mask_texture = 'wizardIconColorMask' + t.head_model = 'wizardHead' + t.torso_model = 'wizardTorso' + t.pelvis_model = 'wizardPelvis' + t.upper_arm_model = 'wizardUpperArm' + t.forearm_model = 'wizardForeArm' + t.hand_model = 'wizardHand' + t.upper_leg_model = 'wizardUpperLeg' + t.lower_leg_model = 'wizardLowerLeg' + t.toes_model = 'wizardToes' + wizard_sounds = ['wizard1', 'wizard2', 'wizard3', 'wizard4'] + wizard_hit_sounds = ['wizardHit1', 'wizardHit2'] + t.attack_sounds = wizard_sounds + t.jump_sounds = wizard_sounds + t.impact_sounds = wizard_hit_sounds + t.death_sounds = ['wizardDeath'] + t.pickup_sounds = wizard_sounds + t.fall_sounds = ['wizardFall'] + t.style = 'spaz' + + # Cowboy ################################### + t = Appearance('Butch') + t.color_texture = 'cowboyColor' + t.color_mask_texture = 'cowboyColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'cowboyIcon' + t.icon_mask_texture = 'cowboyIconColorMask' + t.head_model = 'cowboyHead' + t.torso_model = 'cowboyTorso' + t.pelvis_model = 'cowboyPelvis' + t.upper_arm_model = 'cowboyUpperArm' + t.forearm_model = 'cowboyForeArm' + t.hand_model = 'cowboyHand' + t.upper_leg_model = 'cowboyUpperLeg' + t.lower_leg_model = 'cowboyLowerLeg' + t.toes_model = 'cowboyToes' + cowboy_sounds = ['cowboy1', 'cowboy2', 'cowboy3', 'cowboy4'] + cowboy_hit_sounds = ['cowboyHit1', 'cowboyHit2'] + t.attack_sounds = cowboy_sounds + t.jump_sounds = cowboy_sounds + t.impact_sounds = cowboy_hit_sounds + t.death_sounds = ['cowboyDeath'] + t.pickup_sounds = cowboy_sounds + t.fall_sounds = ['cowboyFall'] + t.style = 'spaz' + + # Witch ################################### + t = Appearance('Witch') + t.color_texture = 'witchColor' + t.color_mask_texture = 'witchColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'witchIcon' + t.icon_mask_texture = 'witchIconColorMask' + t.head_model = 'witchHead' + t.torso_model = 'witchTorso' + t.pelvis_model = 'witchPelvis' + t.upper_arm_model = 'witchUpperArm' + t.forearm_model = 'witchForeArm' + t.hand_model = 'witchHand' + t.upper_leg_model = 'witchUpperLeg' + t.lower_leg_model = 'witchLowerLeg' + t.toes_model = 'witchToes' + witch_sounds = ['witch1', 'witch2', 'witch3', 'witch4'] + witch_hit_sounds = ['witchHit1', 'witchHit2'] + t.attack_sounds = witch_sounds + t.jump_sounds = witch_sounds + t.impact_sounds = witch_hit_sounds + t.death_sounds = ['witchDeath'] + t.pickup_sounds = witch_sounds + t.fall_sounds = ['witchFall'] + t.style = 'spaz' + + # Warrior ################################### + t = Appearance('Warrior') + t.color_texture = 'warriorColor' + t.color_mask_texture = 'warriorColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'warriorIcon' + t.icon_mask_texture = 'warriorIconColorMask' + t.head_model = 'warriorHead' + t.torso_model = 'warriorTorso' + t.pelvis_model = 'warriorPelvis' + t.upper_arm_model = 'warriorUpperArm' + t.forearm_model = 'warriorForeArm' + t.hand_model = 'warriorHand' + t.upper_leg_model = 'warriorUpperLeg' + t.lower_leg_model = 'warriorLowerLeg' + t.toes_model = 'warriorToes' + warrior_sounds = ['warrior1', 'warrior2', 'warrior3', 'warrior4'] + warrior_hit_sounds = ['warriorHit1', 'warriorHit2'] + t.attack_sounds = warrior_sounds + t.jump_sounds = warrior_sounds + t.impact_sounds = warrior_hit_sounds + t.death_sounds = ['warriorDeath'] + t.pickup_sounds = warrior_sounds + t.fall_sounds = ['warriorFall'] + t.style = 'spaz' + + # Superhero ################################### + t = Appearance('Middle-Man') + t.color_texture = 'superheroColor' + t.color_mask_texture = 'superheroColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'superheroIcon' + t.icon_mask_texture = 'superheroIconColorMask' + t.head_model = 'superheroHead' + t.torso_model = 'superheroTorso' + t.pelvis_model = 'superheroPelvis' + t.upper_arm_model = 'superheroUpperArm' + t.forearm_model = 'superheroForeArm' + t.hand_model = 'superheroHand' + t.upper_leg_model = 'superheroUpperLeg' + t.lower_leg_model = 'superheroLowerLeg' + t.toes_model = 'superheroToes' + superhero_sounds = ['superhero1', 'superhero2', 'superhero3', 'superhero4'] + superhero_hit_sounds = ['superheroHit1', 'superheroHit2'] + t.attack_sounds = superhero_sounds + t.jump_sounds = superhero_sounds + t.impact_sounds = superhero_hit_sounds + t.death_sounds = ['superheroDeath'] + t.pickup_sounds = superhero_sounds + t.fall_sounds = ['superheroFall'] + t.style = 'spaz' + + # Alien ################################### + t = Appearance('Alien') + t.color_texture = 'alienColor' + t.color_mask_texture = 'alienColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'alienIcon' + t.icon_mask_texture = 'alienIconColorMask' + t.head_model = 'alienHead' + t.torso_model = 'alienTorso' + t.pelvis_model = 'alienPelvis' + t.upper_arm_model = 'alienUpperArm' + t.forearm_model = 'alienForeArm' + t.hand_model = 'alienHand' + t.upper_leg_model = 'alienUpperLeg' + t.lower_leg_model = 'alienLowerLeg' + t.toes_model = 'alienToes' + alien_sounds = ['alien1', 'alien2', 'alien3', 'alien4'] + alien_hit_sounds = ['alienHit1', 'alienHit2'] + t.attack_sounds = alien_sounds + t.jump_sounds = alien_sounds + t.impact_sounds = alien_hit_sounds + t.death_sounds = ['alienDeath'] + t.pickup_sounds = alien_sounds + t.fall_sounds = ['alienFall'] + t.style = 'spaz' + + # OldLady ################################### + t = Appearance('OldLady') + t.color_texture = 'oldLadyColor' + t.color_mask_texture = 'oldLadyColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'oldLadyIcon' + t.icon_mask_texture = 'oldLadyIconColorMask' + t.head_model = 'oldLadyHead' + t.torso_model = 'oldLadyTorso' + t.pelvis_model = 'oldLadyPelvis' + t.upper_arm_model = 'oldLadyUpperArm' + t.forearm_model = 'oldLadyForeArm' + t.hand_model = 'oldLadyHand' + t.upper_leg_model = 'oldLadyUpperLeg' + t.lower_leg_model = 'oldLadyLowerLeg' + t.toes_model = 'oldLadyToes' + old_lady_sounds = ['oldLady1', 'oldLady2', 'oldLady3', 'oldLady4'] + old_lady_hit_sounds = ['oldLadyHit1', 'oldLadyHit2'] + t.attack_sounds = old_lady_sounds + t.jump_sounds = old_lady_sounds + t.impact_sounds = old_lady_hit_sounds + t.death_sounds = ['oldLadyDeath'] + t.pickup_sounds = old_lady_sounds + t.fall_sounds = ['oldLadyFall'] + t.style = 'spaz' + + # Gladiator ################################### + t = Appearance('Gladiator') + t.color_texture = 'gladiatorColor' + t.color_mask_texture = 'gladiatorColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'gladiatorIcon' + t.icon_mask_texture = 'gladiatorIconColorMask' + t.head_model = 'gladiatorHead' + t.torso_model = 'gladiatorTorso' + t.pelvis_model = 'gladiatorPelvis' + t.upper_arm_model = 'gladiatorUpperArm' + t.forearm_model = 'gladiatorForeArm' + t.hand_model = 'gladiatorHand' + t.upper_leg_model = 'gladiatorUpperLeg' + t.lower_leg_model = 'gladiatorLowerLeg' + t.toes_model = 'gladiatorToes' + gladiator_sounds = ['gladiator1', 'gladiator2', 'gladiator3', 'gladiator4'] + gladiator_hit_sounds = ['gladiatorHit1', 'gladiatorHit2'] + t.attack_sounds = gladiator_sounds + t.jump_sounds = gladiator_sounds + t.impact_sounds = gladiator_hit_sounds + t.death_sounds = ['gladiatorDeath'] + t.pickup_sounds = gladiator_sounds + t.fall_sounds = ['gladiatorFall'] + t.style = 'spaz' + + # Wrestler ################################### + t = Appearance('Wrestler') + t.color_texture = 'wrestlerColor' + t.color_mask_texture = 'wrestlerColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'wrestlerIcon' + t.icon_mask_texture = 'wrestlerIconColorMask' + t.head_model = 'wrestlerHead' + t.torso_model = 'wrestlerTorso' + t.pelvis_model = 'wrestlerPelvis' + t.upper_arm_model = 'wrestlerUpperArm' + t.forearm_model = 'wrestlerForeArm' + t.hand_model = 'wrestlerHand' + t.upper_leg_model = 'wrestlerUpperLeg' + t.lower_leg_model = 'wrestlerLowerLeg' + t.toes_model = 'wrestlerToes' + wrestler_sounds = ['wrestler1', 'wrestler2', 'wrestler3', 'wrestler4'] + wrestler_hit_sounds = ['wrestlerHit1', 'wrestlerHit2'] + t.attack_sounds = wrestler_sounds + t.jump_sounds = wrestler_sounds + t.impact_sounds = wrestler_hit_sounds + t.death_sounds = ['wrestlerDeath'] + t.pickup_sounds = wrestler_sounds + t.fall_sounds = ['wrestlerFall'] + t.style = 'spaz' + + # OperaSinger ################################### + t = Appearance('Gretel') + t.color_texture = 'operaSingerColor' + t.color_mask_texture = 'operaSingerColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'operaSingerIcon' + t.icon_mask_texture = 'operaSingerIconColorMask' + t.head_model = 'operaSingerHead' + t.torso_model = 'operaSingerTorso' + t.pelvis_model = 'operaSingerPelvis' + t.upper_arm_model = 'operaSingerUpperArm' + t.forearm_model = 'operaSingerForeArm' + t.hand_model = 'operaSingerHand' + t.upper_leg_model = 'operaSingerUpperLeg' + t.lower_leg_model = 'operaSingerLowerLeg' + t.toes_model = 'operaSingerToes' + opera_singer_sounds = [ + 'operaSinger1', 'operaSinger2', 'operaSinger3', 'operaSinger4' + ] + opera_singer_hit_sounds = ['operaSingerHit1', 'operaSingerHit2'] + t.attack_sounds = opera_singer_sounds + t.jump_sounds = opera_singer_sounds + t.impact_sounds = opera_singer_hit_sounds + t.death_sounds = ['operaSingerDeath'] + t.pickup_sounds = opera_singer_sounds + t.fall_sounds = ['operaSingerFall'] + t.style = 'spaz' + + # Pixie ################################### + t = Appearance('Pixel') + t.color_texture = 'pixieColor' + t.color_mask_texture = 'pixieColorMask' + t.default_color = (0, 1, 0.7) + t.default_highlight = (0.65, 0.35, 0.75) + t.icon_texture = 'pixieIcon' + t.icon_mask_texture = 'pixieIconColorMask' + t.head_model = 'pixieHead' + t.torso_model = 'pixieTorso' + t.pelvis_model = 'pixiePelvis' + t.upper_arm_model = 'pixieUpperArm' + t.forearm_model = 'pixieForeArm' + t.hand_model = 'pixieHand' + t.upper_leg_model = 'pixieUpperLeg' + t.lower_leg_model = 'pixieLowerLeg' + t.toes_model = 'pixieToes' + pixie_sounds = ['pixie1', 'pixie2', 'pixie3', 'pixie4'] + pixie_hit_sounds = ['pixieHit1', 'pixieHit2'] + t.attack_sounds = pixie_sounds + t.jump_sounds = pixie_sounds + t.impact_sounds = pixie_hit_sounds + t.death_sounds = ['pixieDeath'] + t.pickup_sounds = pixie_sounds + t.fall_sounds = ['pixieFall'] + t.style = 'pixie' + + # Robot ################################### + t = Appearance('Robot') + t.color_texture = 'robotColor' + t.color_mask_texture = 'robotColorMask' + t.default_color = (0.3, 0.5, 0.8) + t.default_highlight = (1, 0, 0) + t.icon_texture = 'robotIcon' + t.icon_mask_texture = 'robotIconColorMask' + t.head_model = 'robotHead' + t.torso_model = 'robotTorso' + t.pelvis_model = 'robotPelvis' + t.upper_arm_model = 'robotUpperArm' + t.forearm_model = 'robotForeArm' + t.hand_model = 'robotHand' + t.upper_leg_model = 'robotUpperLeg' + t.lower_leg_model = 'robotLowerLeg' + t.toes_model = 'robotToes' + robot_sounds = ['robot1', 'robot2', 'robot3', 'robot4'] + robot_hit_sounds = ['robotHit1', 'robotHit2'] + t.attack_sounds = robot_sounds + t.jump_sounds = robot_sounds + t.impact_sounds = robot_hit_sounds + t.death_sounds = ['robotDeath'] + t.pickup_sounds = robot_sounds + t.fall_sounds = ['robotFall'] + t.style = 'spaz' + + # Bunny ################################### + t = Appearance('Easter Bunny') + t.color_texture = 'bunnyColor' + t.color_mask_texture = 'bunnyColorMask' + t.default_color = (1, 1, 1) + t.default_highlight = (1, 0.5, 0.5) + t.icon_texture = 'bunnyIcon' + t.icon_mask_texture = 'bunnyIconColorMask' + t.head_model = 'bunnyHead' + t.torso_model = 'bunnyTorso' + t.pelvis_model = 'bunnyPelvis' + t.upper_arm_model = 'bunnyUpperArm' + t.forearm_model = 'bunnyForeArm' + t.hand_model = 'bunnyHand' + t.upper_leg_model = 'bunnyUpperLeg' + t.lower_leg_model = 'bunnyLowerLeg' + t.toes_model = 'bunnyToes' + bunny_sounds = ['bunny1', 'bunny2', 'bunny3', 'bunny4'] + bunny_hit_sounds = ['bunnyHit1', 'bunnyHit2'] + t.attack_sounds = bunny_sounds + t.jump_sounds = ['bunnyJump'] + t.impact_sounds = bunny_hit_sounds + t.death_sounds = ['bunnyDeath'] + t.pickup_sounds = bunny_sounds + t.fall_sounds = ['bunnyFall'] + t.style = 'bunny' diff --git a/dist/ba_data/python/bastd/actor/spazbot.py b/dist/ba_data/python/bastd/actor/spazbot.py new file mode 100644 index 0000000..4e53ee6 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/spazbot.py @@ -0,0 +1,1047 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Bot versions of Spaz.""" +# pylint: disable=too-many-lines + +from __future__ import annotations + +import random +import weakref +from typing import TYPE_CHECKING + +import ba +from bastd.actor.spaz import Spaz + +if TYPE_CHECKING: + from typing import Any, Optional, List, Tuple, Sequence, Type, Callable + from bastd.actor.flag import Flag + +LITE_BOT_COLOR = (1.2, 0.9, 0.2) +LITE_BOT_HIGHLIGHT = (1.0, 0.5, 0.6) +DEFAULT_BOT_COLOR = (0.6, 0.6, 0.6) +DEFAULT_BOT_HIGHLIGHT = (0.1, 0.3, 0.1) +PRO_BOT_COLOR = (1.0, 0.2, 0.1) +PRO_BOT_HIGHLIGHT = (0.6, 0.1, 0.05) + + +class SpazBotPunchedMessage: + """A message saying a ba.SpazBot got punched. + + category: Message Classes + + Attributes: + + spazbot + The ba.SpazBot that got punched. + + damage + How much damage was done to the ba.SpazBot. + """ + + def __init__(self, spazbot: SpazBot, damage: int): + """Instantiate a message with the given values.""" + self.spazbot = spazbot + self.damage = damage + + +class SpazBotDiedMessage: + """A message saying a ba.SpazBot has died. + + category: Message Classes + + Attributes: + + spazbot + The ba.SpazBot that was killed. + + killerplayer + The ba.Player that killed it (or None). + + how + The particular type of death. + """ + + def __init__(self, spazbot: SpazBot, killerplayer: Optional[ba.Player], + how: ba.DeathType): + """Instantiate with given values.""" + self.spazbot = spazbot + self.killerplayer = killerplayer + self.how = how + + +class SpazBot(Spaz): + """A really dumb AI version of ba.Spaz. + + category: Bot Classes + + Add these to a ba.BotSet to use them. + + Note: currently the AI has no real ability to + navigate obstacles and so should only be used + on wide-open maps. + + When a SpazBot is killed, it delivers a ba.SpazBotDiedMessage + to the current activity. + + When a SpazBot is punched, it delivers a ba.SpazBotPunchedMessage + to the current activity. + """ + + character = 'Spaz' + punchiness = 0.5 + throwiness = 0.7 + static = False + bouncy = False + run = False + charge_dist_min = 0.0 # When we can start a new charge. + charge_dist_max = 2.0 # When we can start a new charge. + run_dist_min = 0.0 # How close we can be to continue running. + charge_speed_min = 0.4 + charge_speed_max = 1.0 + throw_dist_min = 5.0 + throw_dist_max = 9.0 + throw_rate = 1.0 + default_bomb_type = 'normal' + default_bomb_count = 3 + start_cursed = False + color = DEFAULT_BOT_COLOR + highlight = DEFAULT_BOT_HIGHLIGHT + + def __init__(self) -> None: + """Instantiate a spaz-bot.""" + super().__init__(color=self.color, + highlight=self.highlight, + character=self.character, + source_player=None, + start_invincible=False, + can_accept_powerups=False) + + # If you need to add custom behavior to a bot, set this to a callable + # which takes one arg (the bot) and returns False if the bot's normal + # update should be run and True if not. + self.update_callback: Optional[Callable[[SpazBot], Any]] = None + activity = self.activity + assert isinstance(activity, ba.GameActivity) + self._map = weakref.ref(activity.map) + self.last_player_attacked_by: Optional[ba.Player] = None + self.last_attacked_time = 0.0 + self.last_attacked_type: Optional[Tuple[str, str]] = None + self.target_point_default: Optional[ba.Vec3] = None + self.held_count = 0 + self.last_player_held_by: Optional[ba.Player] = None + self.target_flag: Optional[Flag] = None + self._charge_speed = 0.5 * (self.charge_speed_min + + self.charge_speed_max) + self._lead_amount = 0.5 + self._mode = 'wait' + self._charge_closing_in = False + self._last_charge_dist = 0.0 + self._running = False + self._last_jump_time = 0.0 + + self._throw_release_time: Optional[float] = None + self._have_dropped_throw_bomb: Optional[bool] = None + self._player_pts: Optional[List[Tuple[ba.Vec3, ba.Vec3]]] = None + + # These cooldowns didn't exist when these bots were calibrated, + # so take them out of the equation. + self._jump_cooldown = 0 + self._pickup_cooldown = 0 + self._fly_cooldown = 0 + self._bomb_cooldown = 0 + + if self.start_cursed: + self.curse() + + @property + def map(self) -> ba.Map: + """The map this bot was created on.""" + mval = self._map() + assert mval is not None + return mval + + def _get_target_player_pt( + self) -> Tuple[Optional[ba.Vec3], Optional[ba.Vec3]]: + """Returns the position and velocity of our target. + + Both values will be None in the case of no target. + """ + assert self.node + botpt = ba.Vec3(self.node.position) + closest_dist: Optional[float] = None + closest_vel: Optional[ba.Vec3] = None + closest: Optional[ba.Vec3] = None + assert self._player_pts is not None + for plpt, plvel in self._player_pts: + dist = (plpt - botpt).length() + + # Ignore player-points that are significantly below the bot + # (keeps bots from following players off cliffs). + if (closest_dist is None + or dist < closest_dist) and (plpt[1] > botpt[1] - 5.0): + closest_dist = dist + closest_vel = plvel + closest = plpt + if closest_dist is not None: + assert closest_vel is not None + assert closest is not None + return (ba.Vec3(closest[0], closest[1], closest[2]), + ba.Vec3(closest_vel[0], closest_vel[1], closest_vel[2])) + return None, None + + def set_player_points(self, pts: List[Tuple[ba.Vec3, ba.Vec3]]) -> None: + """Provide the spaz-bot with the locations of its enemies.""" + self._player_pts = pts + + def update_ai(self) -> None: + """Should be called periodically to update the spaz' AI.""" + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + if self.update_callback is not None: + if self.update_callback(self): + # Bot has been handled. + return + + if not self.node: + return + + pos = self.node.position + our_pos = ba.Vec3(pos[0], 0, pos[2]) + can_attack = True + + target_pt_raw: Optional[ba.Vec3] + target_vel: Optional[ba.Vec3] + + # If we're a flag-bearer, we're pretty simple-minded - just walk + # towards the flag and try to pick it up. + if self.target_flag: + if self.node.hold_node: + holding_flag = (self.node.hold_node.getnodetype() == 'flag') + else: + holding_flag = False + + # If we're holding the flag, just walk left. + if holding_flag: + # Just walk left. + self.node.move_left_right = -1.0 + self.node.move_up_down = 0.0 + + # Otherwise try to go pick it up. + elif self.target_flag.node: + target_pt_raw = ba.Vec3(*self.target_flag.node.position) + diff = (target_pt_raw - our_pos) + diff = ba.Vec3(diff[0], 0, diff[2]) # Don't care about y. + dist = diff.length() + to_target = diff.normalized() + + # If we're holding some non-flag item, drop it. + if self.node.hold_node: + self.node.pickup_pressed = True + self.node.pickup_pressed = False + return + + # If we're a runner, run only when not super-near the flag. + if self.run and dist > 3.0: + self._running = True + self.node.run = 1.0 + else: + self._running = False + self.node.run = 0.0 + + self.node.move_left_right = to_target.x + self.node.move_up_down = -to_target.z + if dist < 1.25: + self.node.pickup_pressed = True + self.node.pickup_pressed = False + return + + # Not a flag-bearer. If we're holding anything but a bomb, drop it. + if self.node.hold_node: + holding_bomb = (self.node.hold_node.getnodetype() + in ['bomb', 'prop']) + if not holding_bomb: + self.node.pickup_pressed = True + self.node.pickup_pressed = False + return + + target_pt_raw, target_vel = self._get_target_player_pt() + + if target_pt_raw is None: + # Use default target if we've got one. + if self.target_point_default is not None: + target_pt_raw = self.target_point_default + target_vel = ba.Vec3(0, 0, 0) + can_attack = False + + # With no target, we stop moving and drop whatever we're holding. + else: + self.node.move_left_right = 0 + self.node.move_up_down = 0 + if self.node.hold_node: + self.node.pickup_pressed = True + self.node.pickup_pressed = False + return + + # We don't want height to come into play. + target_pt_raw[1] = 0.0 + assert target_vel is not None + target_vel[1] = 0.0 + + dist_raw = (target_pt_raw - our_pos).length() + + # Use a point out in front of them as real target. + # (more out in front the farther from us they are) + target_pt = (target_pt_raw + + target_vel * dist_raw * 0.3 * self._lead_amount) + + diff = (target_pt - our_pos) + dist = diff.length() + to_target = diff.normalized() + + if self._mode == 'throw': + # We can only throw if alive and well. + if not self._dead and not self.node.knockout: + + assert self._throw_release_time is not None + time_till_throw = self._throw_release_time - ba.time() + + if not self.node.hold_node: + # If we haven't thrown yet, whip out the bomb. + if not self._have_dropped_throw_bomb: + self.drop_bomb() + self._have_dropped_throw_bomb = True + + # Otherwise our lack of held node means we successfully + # released our bomb; lets retreat now. + else: + self._mode = 'flee' + + # Oh crap, we're holding a bomb; better throw it. + elif time_till_throw <= 0.0: + # Jump and throw. + def _safe_pickup(node: ba.Node) -> None: + if node and self.node: + self.node.pickup_pressed = True + self.node.pickup_pressed = False + + if dist > 5.0: + self.node.jump_pressed = True + self.node.jump_pressed = False + + # Throws: + ba.timer(0.1, ba.Call(_safe_pickup, self.node)) + else: + # Throws: + ba.timer(0.1, ba.Call(_safe_pickup, self.node)) + + if self.static: + if time_till_throw < 0.3: + speed = 1.0 + elif time_till_throw < 0.7 and dist > 3.0: + speed = -1.0 # Whiplash for long throws. + else: + speed = 0.02 + else: + if time_till_throw < 0.7: + # Right before throw charge full speed towards target. + speed = 1.0 + else: + # Earlier we can hold or move backward for a whiplash. + speed = 0.0125 + self.node.move_left_right = to_target.x * speed + self.node.move_up_down = to_target.z * -1.0 * speed + + elif self._mode == 'charge': + if random.random() < 0.3: + self._charge_speed = random.uniform(self.charge_speed_min, + self.charge_speed_max) + + # If we're a runner we run during charges *except when near + # an edge (otherwise we tend to fly off easily). + if self.run and dist_raw > self.run_dist_min: + self._lead_amount = 0.3 + self._running = True + self.node.run = 1.0 + else: + self._lead_amount = 0.01 + self._running = False + self.node.run = 0.0 + + self.node.move_left_right = to_target.x * self._charge_speed + self.node.move_up_down = to_target.z * -1.0 * self._charge_speed + + elif self._mode == 'wait': + # Every now and then, aim towards our target. + # Other than that, just stand there. + if ba.time(timeformat=ba.TimeFormat.MILLISECONDS) % 1234 < 100: + self.node.move_left_right = to_target.x * (400.0 / 33000) + self.node.move_up_down = to_target.z * (-400.0 / 33000) + else: + self.node.move_left_right = 0 + self.node.move_up_down = 0 + + elif self._mode == 'flee': + # Even if we're a runner, only run till we get away from our + # target (if we keep running we tend to run off edges). + if self.run and dist < 3.0: + self._running = True + self.node.run = 1.0 + else: + self._running = False + self.node.run = 0.0 + self.node.move_left_right = to_target.x * -1.0 + self.node.move_up_down = to_target.z + + # We might wanna switch states unless we're doing a throw + # (in which case that's our sole concern). + if self._mode != 'throw': + + # If we're currently charging, keep track of how far we are + # from our target. When this value increases it means our charge + # is over (ran by them or something). + if self._mode == 'charge': + if (self._charge_closing_in + and self._last_charge_dist < dist < 3.0): + self._charge_closing_in = False + self._last_charge_dist = dist + + # If we have a clean shot, throw! + if (self.throw_dist_min <= dist < self.throw_dist_max + and random.random() < self.throwiness and can_attack): + self._mode = 'throw' + self._lead_amount = ((0.4 + random.random() * 0.6) + if dist_raw > 4.0 else + (0.1 + random.random() * 0.4)) + self._have_dropped_throw_bomb = False + self._throw_release_time = (ba.time() + + (1.0 / self.throw_rate) * + (0.8 + 1.3 * random.random())) + + # If we're static, always charge (which for us means barely move). + elif self.static: + self._mode = 'wait' + + # If we're too close to charge (and aren't in the middle of an + # existing charge) run away. + elif dist < self.charge_dist_min and not self._charge_closing_in: + # ..unless we're near an edge, in which case we've got no + # choice but to charge. + if self.map.is_point_near_edge(our_pos, self._running): + if self._mode != 'charge': + self._mode = 'charge' + self._lead_amount = 0.2 + self._charge_closing_in = True + self._last_charge_dist = dist + else: + self._mode = 'flee' + + # We're within charging distance, backed against an edge, + # or farther than our max throw distance.. chaaarge! + elif (dist < self.charge_dist_max or dist > self.throw_dist_max + or self.map.is_point_near_edge(our_pos, self._running)): + if self._mode != 'charge': + self._mode = 'charge' + self._lead_amount = 0.01 + self._charge_closing_in = True + self._last_charge_dist = dist + + # We're too close to throw but too far to charge - either run + # away or just chill if we're near an edge. + elif dist < self.throw_dist_min: + # Charge if either we're within charge range or + # cant retreat to throw. + self._mode = 'flee' + + # Do some awesome jumps if we're running. + # FIXME: pylint: disable=too-many-boolean-expressions + if ((self._running and 1.2 < dist < 2.2 + and ba.time() - self._last_jump_time > 1.0) + or (self.bouncy and ba.time() - self._last_jump_time > 0.4 + and random.random() < 0.5)): + self._last_jump_time = ba.time() + self.node.jump_pressed = True + self.node.jump_pressed = False + + # Throw punches when real close. + if dist < (1.6 if self._running else 1.2) and can_attack: + if random.random() < self.punchiness: + self.on_punch_press() + self.on_punch_release() + + def on_punched(self, damage: int) -> None: + """ + Method override; sends ba.SpazBotPunchedMessage + to the current activity. + """ + ba.getactivity().handlemessage(SpazBotPunchedMessage(self, damage)) + + def on_expire(self) -> None: + super().on_expire() + + # We're being torn down; release our callback(s) so there's + # no chance of them keeping activities or other things alive. + self.update_callback = None + + def handlemessage(self, msg: Any) -> Any: + # pylint: disable=too-many-branches + assert not self.expired + + # Keep track of if we're being held and by who most recently. + if isinstance(msg, ba.PickedUpMessage): + super().handlemessage(msg) # Augment standard behavior. + self.held_count += 1 + picked_up_by = msg.node.source_player + if picked_up_by: + self.last_player_held_by = picked_up_by + + elif isinstance(msg, ba.DroppedMessage): + super().handlemessage(msg) # Augment standard behavior. + self.held_count -= 1 + if self.held_count < 0: + print('ERROR: spaz held_count < 0') + + # Let's count someone dropping us as an attack. + try: + if msg.node: + picked_up_by = msg.node.source_player + else: + picked_up_by = None + except Exception: + ba.print_exception('Error on SpazBot DroppedMessage.') + picked_up_by = None + + if picked_up_by: + self.last_player_attacked_by = picked_up_by + self.last_attacked_time = ba.time() + self.last_attacked_type = ('picked_up', 'default') + + elif isinstance(msg, ba.DieMessage): + + # Report normal deaths for scoring purposes. + if not self._dead and not msg.immediate: + + killerplayer: Optional[ba.Player] + + # If this guy was being held at the time of death, the + # holder is the killer. + if self.held_count > 0 and self.last_player_held_by: + killerplayer = self.last_player_held_by + else: + # If they were attacked by someone in the last few + # seconds that person's the killer. + # Otherwise it was a suicide. + if (self.last_player_attacked_by + and ba.time() - self.last_attacked_time < 4.0): + killerplayer = self.last_player_attacked_by + else: + killerplayer = None + activity = self._activity() + + # (convert dead player refs to None) + if not killerplayer: + killerplayer = None + if activity is not None: + activity.handlemessage( + SpazBotDiedMessage(self, killerplayer, msg.how)) + super().handlemessage(msg) # Augment standard behavior. + + # Keep track of the player who last hit us for point rewarding. + elif isinstance(msg, ba.HitMessage): + source_player = msg.get_source_player(ba.Player) + if source_player: + self.last_player_attacked_by = source_player + self.last_attacked_time = ba.time() + self.last_attacked_type = (msg.hit_type, msg.hit_subtype) + super().handlemessage(msg) + else: + super().handlemessage(msg) + + +class BomberBot(SpazBot): + """A bot that throws regular bombs and occasionally punches. + + category: Bot Classes + """ + character = 'Spaz' + punchiness = 0.3 + + +class BomberBotLite(BomberBot): + """A less aggressive yellow version of ba.BomberBot. + + category: Bot Classes + """ + color = LITE_BOT_COLOR + highlight = LITE_BOT_HIGHLIGHT + punchiness = 0.2 + throw_rate = 0.7 + throwiness = 0.1 + charge_speed_min = 0.6 + charge_speed_max = 0.6 + + +class BomberBotStaticLite(BomberBotLite): + """A less aggressive generally immobile weak version of ba.BomberBot. + + category: Bot Classes + """ + static = True + throw_dist_min = 0.0 + + +class BomberBotStatic(BomberBot): + """A version of ba.BomberBot who generally stays in one place. + + category: Bot Classes + """ + static = True + throw_dist_min = 0.0 + + +class BomberBotPro(BomberBot): + """A more powerful version of ba.BomberBot. + + category: Bot Classes + """ + points_mult = 2 + color = PRO_BOT_COLOR + highlight = PRO_BOT_HIGHLIGHT + default_bomb_count = 3 + default_boxing_gloves = True + punchiness = 0.7 + throw_rate = 1.3 + run = True + run_dist_min = 6.0 + + +class BomberBotProShielded(BomberBotPro): + """A more powerful version of ba.BomberBot who starts with shields. + + category: Bot Classes + """ + points_mult = 3 + default_shields = True + + +class BomberBotProStatic(BomberBotPro): + """A more powerful ba.BomberBot who generally stays in one place. + + category: Bot Classes + """ + static = True + throw_dist_min = 0.0 + + +class BomberBotProStaticShielded(BomberBotProShielded): + """A powerful ba.BomberBot with shields who is generally immobile. + + category: Bot Classes + """ + static = True + throw_dist_min = 0.0 + + +class BrawlerBot(SpazBot): + """A bot who walks and punches things. + + category: Bot Classes + """ + character = 'Kronk' + punchiness = 0.9 + charge_dist_max = 9999.0 + charge_speed_min = 1.0 + charge_speed_max = 1.0 + throw_dist_min = 9999 + throw_dist_max = 9999 + + +class BrawlerBotLite(BrawlerBot): + """A weaker version of ba.BrawlerBot. + + category: Bot Classes + """ + color = LITE_BOT_COLOR + highlight = LITE_BOT_HIGHLIGHT + punchiness = 0.3 + charge_speed_min = 0.6 + charge_speed_max = 0.6 + + +class BrawlerBotPro(BrawlerBot): + """A stronger version of ba.BrawlerBot. + + category: Bot Classes + """ + color = PRO_BOT_COLOR + highlight = PRO_BOT_HIGHLIGHT + run = True + run_dist_min = 4.0 + default_boxing_gloves = True + punchiness = 0.95 + points_mult = 2 + + +class BrawlerBotProShielded(BrawlerBotPro): + """A stronger version of ba.BrawlerBot who starts with shields. + + category: Bot Classes + """ + default_shields = True + points_mult = 3 + + +class ChargerBot(SpazBot): + """A speedy melee attack bot. + + category: Bot Classes + """ + + character = 'Snake Shadow' + punchiness = 1.0 + run = True + charge_dist_min = 10.0 + charge_dist_max = 9999.0 + charge_speed_min = 1.0 + charge_speed_max = 1.0 + throw_dist_min = 9999 + throw_dist_max = 9999 + points_mult = 2 + + +class BouncyBot(SpazBot): + """A speedy attacking melee bot that jumps constantly. + + category: Bot Classes + """ + + color = (1, 1, 1) + highlight = (1.0, 0.5, 0.5) + character = 'Easter Bunny' + punchiness = 1.0 + run = True + bouncy = True + default_boxing_gloves = True + charge_dist_min = 10.0 + charge_dist_max = 9999.0 + charge_speed_min = 1.0 + charge_speed_max = 1.0 + throw_dist_min = 9999 + throw_dist_max = 9999 + points_mult = 2 + + +class ChargerBotPro(ChargerBot): + """A stronger ba.ChargerBot. + + category: Bot Classes + """ + color = PRO_BOT_COLOR + highlight = PRO_BOT_HIGHLIGHT + default_shields = True + default_boxing_gloves = True + points_mult = 3 + + +class ChargerBotProShielded(ChargerBotPro): + """A stronger ba.ChargerBot who starts with shields. + + category: Bot Classes + """ + default_shields = True + points_mult = 4 + + +class TriggerBot(SpazBot): + """A slow moving bot with trigger bombs. + + category: Bot Classes + """ + character = 'Zoe' + punchiness = 0.75 + throwiness = 0.7 + charge_dist_max = 1.0 + charge_speed_min = 0.3 + charge_speed_max = 0.5 + throw_dist_min = 3.5 + throw_dist_max = 5.5 + default_bomb_type = 'impact' + points_mult = 2 + + +class TriggerBotStatic(TriggerBot): + """A ba.TriggerBot who generally stays in one place. + + category: Bot Classes + """ + static = True + throw_dist_min = 0.0 + + +class TriggerBotPro(TriggerBot): + """A stronger version of ba.TriggerBot. + + category: Bot Classes + """ + color = PRO_BOT_COLOR + highlight = PRO_BOT_HIGHLIGHT + default_bomb_count = 3 + default_boxing_gloves = True + charge_speed_min = 1.0 + charge_speed_max = 1.0 + punchiness = 0.9 + throw_rate = 1.3 + run = True + run_dist_min = 6.0 + points_mult = 3 + + +class TriggerBotProShielded(TriggerBotPro): + """A stronger version of ba.TriggerBot who starts with shields. + + category: Bot Classes + """ + default_shields = True + points_mult = 4 + + +class StickyBot(SpazBot): + """A crazy bot who runs and throws sticky bombs. + + category: Bot Classes + """ + character = 'Mel' + punchiness = 0.9 + throwiness = 1.0 + run = True + charge_dist_min = 4.0 + charge_dist_max = 10.0 + charge_speed_min = 1.0 + charge_speed_max = 1.0 + throw_dist_min = 0.0 + throw_dist_max = 4.0 + throw_rate = 2.0 + default_bomb_type = 'sticky' + default_bomb_count = 3 + points_mult = 3 + + +class StickyBotStatic(StickyBot): + """A crazy bot who throws sticky-bombs but generally stays in one place. + + category: Bot Classes + """ + static = True + + +class ExplodeyBot(SpazBot): + """A bot who runs and explodes in 5 seconds. + + category: Bot Classes + """ + character = 'Jack Morgan' + run = True + charge_dist_min = 0.0 + charge_dist_max = 9999 + charge_speed_min = 1.0 + charge_speed_max = 1.0 + throw_dist_min = 9999 + throw_dist_max = 9999 + start_cursed = True + points_mult = 4 + + +class ExplodeyBotNoTimeLimit(ExplodeyBot): + """A bot who runs but does not explode on his own. + + category: Bot Classes + """ + curse_time = None + + +class ExplodeyBotShielded(ExplodeyBot): + """A ba.ExplodeyBot who starts with shields. + + category: Bot Classes + """ + default_shields = True + points_mult = 5 + + +class SpazBotSet: + """A container/controller for one or more ba.SpazBots. + + category: Bot Classes + """ + + def __init__(self) -> None: + """Create a bot-set.""" + + # We spread our bots out over a few lists so we can update + # them in a staggered fashion. + self._bot_list_count = 5 + self._bot_add_list = 0 + self._bot_update_list = 0 + self._bot_lists: List[List[SpazBot]] = [ + [] for _ in range(self._bot_list_count) + ] + self._spawn_sound = ba.getsound('spawn') + self._spawning_count = 0 + self._bot_update_timer: Optional[ba.Timer] = None + self.start_moving() + + def __del__(self) -> None: + self.clear() + + def spawn_bot(self, + bot_type: Type[SpazBot], + pos: Sequence[float], + spawn_time: float = 3.0, + on_spawn_call: Callable[[SpazBot], Any] = None) -> None: + """Spawn a bot from this set.""" + from bastd.actor import spawner + spawner.Spawner(pt=pos, + spawn_time=spawn_time, + send_spawn_message=False, + spawn_callback=ba.Call(self._spawn_bot, bot_type, pos, + on_spawn_call)) + self._spawning_count += 1 + + def _spawn_bot(self, bot_type: Type[SpazBot], pos: Sequence[float], + on_spawn_call: Optional[Callable[[SpazBot], Any]]) -> None: + spaz = bot_type() + ba.playsound(self._spawn_sound, position=pos) + assert spaz.node + spaz.node.handlemessage('flash') + spaz.node.is_area_of_interest = False + spaz.handlemessage(ba.StandMessage(pos, random.uniform(0, 360))) + self.add_bot(spaz) + self._spawning_count -= 1 + if on_spawn_call is not None: + on_spawn_call(spaz) + + def have_living_bots(self) -> bool: + """Return whether any bots in the set are alive or spawning.""" + return (self._spawning_count > 0 + or any(any(b.is_alive() for b in l) for l in self._bot_lists)) + + def get_living_bots(self) -> List[SpazBot]: + """Get the living bots in the set.""" + bots: List[SpazBot] = [] + for botlist in self._bot_lists: + for bot in botlist: + if bot.is_alive(): + bots.append(bot) + return bots + + def _update(self) -> None: + + # Update one of our bot lists each time through. + # First off, remove no-longer-existing bots from the list. + try: + bot_list = self._bot_lists[self._bot_update_list] = ([ + b for b in self._bot_lists[self._bot_update_list] if b + ]) + except Exception: + bot_list = [] + ba.print_exception('Error updating bot list: ' + + str(self._bot_lists[self._bot_update_list])) + self._bot_update_list = (self._bot_update_list + + 1) % self._bot_list_count + + # Update our list of player points for the bots to use. + player_pts = [] + for player in ba.getactivity().players: + assert isinstance(player, ba.Player) + try: + # TODO: could use abstracted player.position here so we + # don't have to assume their actor type, but we have no + # abstracted velocity as of yet. + if player.is_alive(): + assert isinstance(player.actor, Spaz) + assert player.actor.node + player_pts.append((ba.Vec3(player.actor.node.position), + ba.Vec3(player.actor.node.velocity))) + except Exception: + ba.print_exception('Error on bot-set _update.') + + for bot in bot_list: + bot.set_player_points(player_pts) + bot.update_ai() + + def clear(self) -> None: + """Immediately clear out any bots in the set.""" + + # Don't do this if the activity is shutting down or dead. + activity = ba.getactivity(doraise=False) + if activity is None or activity.expired: + return + + for i in range(len(self._bot_lists)): + for bot in self._bot_lists[i]: + bot.handlemessage(ba.DieMessage(immediate=True)) + self._bot_lists[i] = [] + + def start_moving(self) -> None: + """Start processing bot AI updates so they start doing their thing.""" + self._bot_update_timer = ba.Timer(0.05, + ba.WeakCall(self._update), + repeat=True) + + def stop_moving(self) -> None: + """Tell all bots to stop moving and stops updating their AI. + + Useful when players have won and you want the + enemy bots to just stand and look bewildered. + """ + self._bot_update_timer = None + for botlist in self._bot_lists: + for bot in botlist: + if bot.node: + bot.node.move_left_right = 0 + bot.node.move_up_down = 0 + + def celebrate(self, duration: float) -> None: + """Tell all living bots in the set to celebrate momentarily. + + Duration is given in seconds. + """ + msg = ba.CelebrateMessage(duration=duration) + for botlist in self._bot_lists: + for bot in botlist: + if bot: + bot.handlemessage(msg) + + def final_celebrate(self) -> None: + """Tell all bots in the set to stop what they were doing and celebrate. + + Use this when the bots have won a game. + """ + self._bot_update_timer = None + + # At this point stop doing anything but jumping and celebrating. + for botlist in self._bot_lists: + for bot in botlist: + if bot: + assert bot.node # (should exist if 'if bot' was True) + bot.node.move_left_right = 0 + bot.node.move_up_down = 0 + ba.timer(0.5 * random.random(), + ba.Call(bot.handlemessage, ba.CelebrateMessage())) + jump_duration = random.randrange(400, 500) + j = random.randrange(0, 200) + for _i in range(10): + bot.node.jump_pressed = True + bot.node.jump_pressed = False + j += jump_duration + ba.timer(random.uniform(0.0, 1.0), + ba.Call(bot.node.handlemessage, 'attack_sound')) + ba.timer(random.uniform(1.0, 2.0), + ba.Call(bot.node.handlemessage, 'attack_sound')) + ba.timer(random.uniform(2.0, 3.0), + ba.Call(bot.node.handlemessage, 'attack_sound')) + + def add_bot(self, bot: SpazBot) -> None: + """Add a ba.SpazBot instance to the set.""" + self._bot_lists[self._bot_add_list].append(bot) + self._bot_add_list = (self._bot_add_list + 1) % self._bot_list_count diff --git a/dist/ba_data/python/bastd/actor/spazfactory.py b/dist/ba_data/python/bastd/actor/spazfactory.py new file mode 100644 index 0000000..7363ec4 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/spazfactory.py @@ -0,0 +1,266 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a factory object from creating Spazzes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.gameutils import SharedObjects +import _ba + +if TYPE_CHECKING: + from typing import Any, Dict + + +class SpazFactory: + """Wraps up media and other resources used by ba.Spaz instances. + + Category: Gameplay Classes + + Generally one of these is created per ba.Activity and shared + between all spaz instances. Use ba.Spaz.get_factory() to return + the shared factory for the current activity. + + Attributes: + + impact_sounds_medium + A tuple of ba.Sounds for when a ba.Spaz hits something kinda hard. + + impact_sounds_hard + A tuple of ba.Sounds for when a ba.Spaz hits something really hard. + + impact_sounds_harder + A tuple of ba.Sounds for when a ba.Spaz hits something really + really hard. + + single_player_death_sound + The sound that plays for an 'important' spaz death such as in + co-op games. + + punch_sound + A standard punch ba.Sound. + + punch_sound_strong + A tuple of stronger sounding punch ba.Sounds. + + punch_sound_stronger + A really really strong sounding punch ba.Sound. + + swish_sound + A punch swish ba.Sound. + + block_sound + A ba.Sound for when an attack is blocked by invincibility. + + shatter_sound + A ba.Sound for when a frozen ba.Spaz shatters. + + splatter_sound + A ba.Sound for when a ba.Spaz blows up via curse. + + spaz_material + A ba.Material applied to all of parts of a ba.Spaz. + + roller_material + A ba.Material applied to the invisible roller ball body that + a ba.Spaz uses for locomotion. + + punch_material + A ba.Material applied to the 'fist' of a ba.Spaz. + + pickup_material + A ba.Material applied to the 'grabber' body of a ba.Spaz. + + curse_material + A ba.Material applied to a cursed ba.Spaz that triggers an explosion. + """ + + _STORENAME = ba.storagename() + + def _preload(self, character: str) -> None: + """Preload media needed for a given character.""" + self.get_media(character) + + def __init__(self) -> None: + """Instantiate a factory object.""" + # pylint: disable=cyclic-import + # FIXME: should probably put these somewhere common so we don't + # have to import them from a module that imports us. + from bastd.actor.spaz import (PickupMessage, PunchHitMessage, + CurseExplodeMessage) + + shared = SharedObjects.get() + self.impact_sounds_medium = (ba.getsound('impactMedium'), + ba.getsound('impactMedium2')) + self.impact_sounds_hard = (ba.getsound('impactHard'), + ba.getsound('impactHard2'), + ba.getsound('impactHard3')) + self.impact_sounds_harder = (ba.getsound('bigImpact'), + ba.getsound('bigImpact2')) + self.single_player_death_sound = ba.getsound('playerDeath') + self.punch_sound = ba.getsound('punch01') + self.punch_sound_strong = (ba.getsound('punchStrong01'), + ba.getsound('punchStrong02')) + self.punch_sound_stronger = ba.getsound('superPunch') + self.swish_sound = ba.getsound('punchSwish') + self.block_sound = ba.getsound('block') + self.shatter_sound = ba.getsound('shatter') + self.splatter_sound = ba.getsound('splatter') + self.spaz_material = ba.Material() + self.roller_material = ba.Material() + self.punch_material = ba.Material() + self.pickup_material = ba.Material() + self.curse_material = ba.Material() + + footing_material = shared.footing_material + object_material = shared.object_material + player_material = shared.player_material + region_material = shared.region_material + + # Send footing messages to spazzes so they know when they're on + # solid ground. + # Eww; this probably should just be built into the spaz node. + self.roller_material.add_actions( + conditions=('they_have_material', footing_material), + actions=(('message', 'our_node', 'at_connect', 'footing', 1), + ('message', 'our_node', 'at_disconnect', 'footing', -1))) + + self.spaz_material.add_actions( + conditions=('they_have_material', footing_material), + actions=(('message', 'our_node', 'at_connect', 'footing', 1), + ('message', 'our_node', 'at_disconnect', 'footing', -1))) + + # Punches. + self.punch_material.add_actions( + conditions=('they_are_different_node_than_us', ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('message', 'our_node', 'at_connect', PunchHitMessage()), + )) + + # Pickups. + self.pickup_material.add_actions( + conditions=(('they_are_different_node_than_us', ), 'and', + ('they_have_material', object_material)), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('message', 'our_node', 'at_connect', PickupMessage()), + )) + + # Curse. + self.curse_material.add_actions( + conditions=( + ('they_are_different_node_than_us', ), + 'and', + ('they_have_material', player_material), + ), + actions=('message', 'our_node', 'at_connect', + CurseExplodeMessage()), + ) + + self.foot_impact_sounds = (ba.getsound('footImpact01'), + ba.getsound('footImpact02'), + ba.getsound('footImpact03')) + + self.foot_skid_sound = ba.getsound('skid01') + self.foot_roll_sound = ba.getsound('scamper01') + + self.roller_material.add_actions( + conditions=('they_have_material', footing_material), + actions=( + ('impact_sound', self.foot_impact_sounds, 1, 0.2), + ('skid_sound', self.foot_skid_sound, 20, 0.3), + ('roll_sound', self.foot_roll_sound, 20, 3.0), + )) + + self.skid_sound = ba.getsound('gravelSkid') + + self.spaz_material.add_actions( + conditions=('they_have_material', footing_material), + actions=( + ('impact_sound', self.foot_impact_sounds, 20, 6), + ('skid_sound', self.skid_sound, 2.0, 1), + ('roll_sound', self.skid_sound, 2.0, 1), + )) + + self.shield_up_sound = ba.getsound('shieldUp') + self.shield_down_sound = ba.getsound('shieldDown') + self.shield_hit_sound = ba.getsound('shieldHit') + + # We don't want to collide with stuff we're initially overlapping + # (unless its marked with a special region material). + self.spaz_material.add_actions( + conditions=( + ( + ('we_are_younger_than', 51), + 'and', + ('they_are_different_node_than_us', ), + ), + 'and', + ('they_dont_have_material', region_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + + self.spaz_media: Dict[str, Any] = {} + + # Lets load some basic rules. + # (allows them to be tweaked from the master server) + self.shield_decay_rate = _ba.get_account_misc_read_val('rsdr', 10.0) + self.punch_cooldown = _ba.get_account_misc_read_val('rpc', 400) + self.punch_cooldown_gloves = (_ba.get_account_misc_read_val( + 'rpcg', 300)) + self.punch_power_scale = _ba.get_account_misc_read_val('rpp', 1.2) + self.punch_power_scale_gloves = (_ba.get_account_misc_read_val( + 'rppg', 1.4)) + self.max_shield_spillover_damage = (_ba.get_account_misc_read_val( + 'rsms', 500)) + + def get_style(self, character: str) -> str: + """Return the named style for this character. + + (this influences subtle aspects of their appearance, etc) + """ + return ba.app.spaz_appearances[character].style + + def get_media(self, character: str) -> Dict[str, Any]: + """Return the set of media used by this variant of spaz.""" + char = ba.app.spaz_appearances[character] + if character not in self.spaz_media: + media = self.spaz_media[character] = { + 'jump_sounds': [ba.getsound(s) for s in char.jump_sounds], + 'attack_sounds': [ba.getsound(s) for s in char.attack_sounds], + 'impact_sounds': [ba.getsound(s) for s in char.impact_sounds], + 'death_sounds': [ba.getsound(s) for s in char.death_sounds], + 'pickup_sounds': [ba.getsound(s) for s in char.pickup_sounds], + 'fall_sounds': [ba.getsound(s) for s in char.fall_sounds], + 'color_texture': ba.gettexture(char.color_texture), + 'color_mask_texture': ba.gettexture(char.color_mask_texture), + 'head_model': ba.getmodel(char.head_model), + 'torso_model': ba.getmodel(char.torso_model), + 'pelvis_model': ba.getmodel(char.pelvis_model), + 'upper_arm_model': ba.getmodel(char.upper_arm_model), + 'forearm_model': ba.getmodel(char.forearm_model), + 'hand_model': ba.getmodel(char.hand_model), + 'upper_leg_model': ba.getmodel(char.upper_leg_model), + 'lower_leg_model': ba.getmodel(char.lower_leg_model), + 'toes_model': ba.getmodel(char.toes_model) + } + else: + media = self.spaz_media[character] + return media + + @classmethod + def get(cls) -> SpazFactory: + """Return the shared ba.SpazFactory, creating it if necessary.""" + # pylint: disable=cyclic-import + activity = ba.getactivity() + factory = activity.customdata.get(cls._STORENAME) + if factory is None: + factory = activity.customdata[cls._STORENAME] = SpazFactory() + assert isinstance(factory, SpazFactory) + return factory diff --git a/dist/ba_data/python/bastd/actor/text.py b/dist/ba_data/python/bastd/actor/text.py new file mode 100644 index 0000000..96e3be5 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/text.py @@ -0,0 +1,219 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Actor(s).""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, Union, Tuple, Sequence, Optional + + +class Text(ba.Actor): + """Text with some tricks.""" + + class Transition(Enum): + """Transition types for text.""" + FADE_IN = 'fade_in' + IN_RIGHT = 'in_right' + IN_LEFT = 'in_left' + IN_BOTTOM = 'in_bottom' + IN_BOTTOM_SLOW = 'in_bottom_slow' + IN_TOP_SLOW = 'in_top_slow' + + class HAlign(Enum): + """Horizontal alignment type.""" + LEFT = 'left' + CENTER = 'center' + RIGHT = 'right' + + class VAlign(Enum): + """Vertical alignment type.""" + NONE = 'none' + CENTER = 'center' + + class HAttach(Enum): + """Horizontal attach type.""" + LEFT = 'left' + CENTER = 'center' + RIGHT = 'right' + + class VAttach(Enum): + """Vertical attach type.""" + BOTTOM = 'bottom' + CENTER = 'center' + TOP = 'top' + + def __init__(self, + text: Union[str, ba.Lstr], + position: Tuple[float, float] = (0.0, 0.0), + h_align: HAlign = HAlign.LEFT, + v_align: VAlign = VAlign.NONE, + color: Sequence[float] = (1.0, 1.0, 1.0, 1.0), + transition: Optional[Transition] = None, + transition_delay: float = 0.0, + flash: bool = False, + v_attach: VAttach = VAttach.CENTER, + h_attach: HAttach = HAttach.CENTER, + scale: float = 1.0, + transition_out_delay: float = None, + maxwidth: float = None, + shadow: float = 0.5, + flatness: float = 0.0, + vr_depth: float = 0.0, + host_only: bool = False, + front: bool = False): + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + super().__init__() + self.node = ba.newnode( + 'text', + delegate=self, + attrs={ + 'text': text, + 'color': color, + 'position': position, + 'h_align': h_align.value, + 'vr_depth': vr_depth, + 'v_align': v_align.value, + 'h_attach': h_attach.value, + 'v_attach': v_attach.value, + 'shadow': shadow, + 'flatness': flatness, + 'maxwidth': 0.0 if maxwidth is None else maxwidth, + 'host_only': host_only, + 'front': front, + 'scale': scale + }) + + if transition is self.Transition.FADE_IN: + if flash: + raise Exception('fixme: flash and fade-in' + ' currently cant both be on') + cmb = ba.newnode('combine', + owner=self.node, + attrs={ + 'input0': color[0], + 'input1': color[1], + 'input2': color[2], + 'size': 4 + }) + keys = {transition_delay: 0.0, transition_delay + 0.5: color[3]} + if transition_out_delay is not None: + keys[transition_delay + transition_out_delay] = color[3] + keys[transition_delay + transition_out_delay + 0.5] = 0.0 + ba.animate(cmb, 'input3', keys) + cmb.connectattr('output', self.node, 'color') + + if flash: + mult = 2.0 + tm1 = 0.15 + tm2 = 0.3 + cmb = ba.newnode('combine', owner=self.node, attrs={'size': 4}) + ba.animate(cmb, + 'input0', { + 0.0: color[0] * mult, + tm1: color[0], + tm2: color[0] * mult + }, + loop=True) + ba.animate(cmb, + 'input1', { + 0.0: color[1] * mult, + tm1: color[1], + tm2: color[1] * mult + }, + loop=True) + ba.animate(cmb, + 'input2', { + 0.0: color[2] * mult, + tm1: color[2], + tm2: color[2] * mult + }, + loop=True) + cmb.input3 = color[3] + cmb.connectattr('output', self.node, 'color') + + cmb = self.position_combine = ba.newnode('combine', + owner=self.node, + attrs={'size': 2}) + + if transition is self.Transition.IN_RIGHT: + keys = { + transition_delay: position[0] + 1300, + transition_delay + 0.2: position[0] + } + o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0} + ba.animate(cmb, 'input0', keys) + cmb.input1 = position[1] + ba.animate(self.node, 'opacity', o_keys) + elif transition is self.Transition.IN_LEFT: + keys = { + transition_delay: position[0] - 1300, + transition_delay + 0.2: position[0] + } + o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0} + if transition_out_delay is not None: + keys[transition_delay + transition_out_delay] = position[0] + keys[transition_delay + transition_out_delay + + 0.2] = position[0] - 1300.0 + o_keys[transition_delay + transition_out_delay + 0.15] = 1.0 + o_keys[transition_delay + transition_out_delay + 0.2] = 0.0 + ba.animate(cmb, 'input0', keys) + cmb.input1 = position[1] + ba.animate(self.node, 'opacity', o_keys) + elif transition is self.Transition.IN_BOTTOM_SLOW: + keys = { + transition_delay: -100.0, + transition_delay + 1.0: position[1] + } + o_keys = {transition_delay: 0.0, transition_delay + 0.2: 1.0} + cmb.input0 = position[0] + ba.animate(cmb, 'input1', keys) + ba.animate(self.node, 'opacity', o_keys) + elif transition is self.Transition.IN_BOTTOM: + keys = { + transition_delay: -100.0, + transition_delay + 0.2: position[1] + } + o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0} + if transition_out_delay is not None: + keys[transition_delay + transition_out_delay] = position[1] + keys[transition_delay + transition_out_delay + 0.2] = -100.0 + o_keys[transition_delay + transition_out_delay + 0.15] = 1.0 + o_keys[transition_delay + transition_out_delay + 0.2] = 0.0 + cmb.input0 = position[0] + ba.animate(cmb, 'input1', keys) + ba.animate(self.node, 'opacity', o_keys) + elif transition is self.Transition.IN_TOP_SLOW: + keys = { + transition_delay: 400.0, + transition_delay + 3.5: position[1] + } + o_keys = {transition_delay: 0, transition_delay + 1.0: 1.0} + cmb.input0 = position[0] + ba.animate(cmb, 'input1', keys) + ba.animate(self.node, 'opacity', o_keys) + else: + assert transition is self.Transition.FADE_IN or transition is None + cmb.input0 = position[0] + cmb.input1 = position[1] + cmb.connectattr('output', self.node, 'position') + + # If we're transitioning out, die at the end of it. + if transition_out_delay is not None: + ba.timer(transition_delay + transition_out_delay + 1.0, + ba.WeakCall(self.handlemessage, ba.DieMessage())) + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + return None + return super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/actor/tipstext.py b/dist/ba_data/python/bastd/actor/tipstext.py new file mode 100644 index 0000000..2cd5afd --- /dev/null +++ b/dist/ba_data/python/bastd/actor/tipstext.py @@ -0,0 +1,94 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides tip related Actor(s).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any + + +class TipsText(ba.Actor): + """A bit of text showing various helpful game tips.""" + + def __init__(self, offs_y: float = 100.0): + super().__init__() + self._tip_scale = 0.8 + self._tip_title_scale = 1.1 + self._offs_y = offs_y + self.node = ba.newnode('text', + delegate=self, + attrs={ + 'text': '', + 'scale': self._tip_scale, + 'h_align': 'left', + 'maxwidth': 800, + 'vr_depth': -20, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + tval = ba.Lstr(value='${A}:', + subs=[('${A}', ba.Lstr(resource='tipText'))]) + self.title_node = ba.newnode('text', + delegate=self, + attrs={ + 'text': tval, + 'scale': self._tip_title_scale, + 'maxwidth': 122, + 'h_align': 'right', + 'vr_depth': -20, + 'v_align': 'center', + 'v_attach': 'bottom' + }) + self._message_duration = 10000 + self._message_spacing = 3000 + self._change_timer = ba.Timer( + 0.001 * (self._message_duration + self._message_spacing), + ba.WeakCall(self.change_phrase), + repeat=True) + self._combine = ba.newnode('combine', + owner=self.node, + attrs={ + 'input0': 1.0, + 'input1': 0.8, + 'input2': 1.0, + 'size': 4 + }) + self._combine.connectattr('output', self.node, 'color') + self._combine.connectattr('output', self.title_node, 'color') + self.change_phrase() + + def change_phrase(self) -> None: + """Switch the visible tip phrase.""" + from ba.internal import get_remote_app_name, get_next_tip + next_tip = ba.Lstr(translate=('tips', get_next_tip()), + subs=[('${REMOTE_APP_NAME}', get_remote_app_name()) + ]) + spc = self._message_spacing + assert self.node + self.node.position = (-200, self._offs_y) + self.title_node.position = (-220, self._offs_y + 3) + keys = { + spc: 0, + spc + 1000: 1.0, + spc + self._message_duration - 1000: 1.0, + spc + self._message_duration: 0.0 + } + ba.animate(self._combine, + 'input3', {k: v * 0.5 + for k, v in list(keys.items())}, + timeformat=ba.TimeFormat.MILLISECONDS) + self.node.text = next_tip + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + self.title_node.delete() + return None + return super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/actor/zoomtext.py b/dist/ba_data/python/bastd/actor/zoomtext.py new file mode 100644 index 0000000..4c0b2c9 --- /dev/null +++ b/dist/ba_data/python/bastd/actor/zoomtext.py @@ -0,0 +1,197 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defined Actor(s).""" + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, Union, Tuple, Sequence + + +class ZoomText(ba.Actor): + """Big Zooming Text. + + Category: Gameplay Classes + + Used for things such as the 'BOB WINS' victory messages. + """ + + def __init__(self, + text: Union[str, ba.Lstr], + position: Tuple[float, float] = (0.0, 0.0), + shiftposition: Tuple[float, float] = None, + shiftdelay: float = None, + lifespan: float = None, + flash: bool = True, + trail: bool = True, + h_align: str = 'center', + color: Sequence[float] = (0.9, 0.4, 0.0), + jitter: float = 0.0, + trailcolor: Sequence[float] = (1.0, 0.35, 0.1, 0.0), + scale: float = 1.0, + project_scale: float = 1.0, + tilt_translate: float = 0.0, + maxwidth: float = None): + # pylint: disable=too-many-locals + super().__init__() + self._dying = False + positionadjusted = (position[0], position[1] - 100) + if shiftdelay is None: + shiftdelay = 2.500 + if shiftdelay < 0.0: + ba.print_error('got shiftdelay < 0') + shiftdelay = 0.0 + self._project_scale = project_scale + self.node = ba.newnode( + 'text', + delegate=self, + attrs={ + 'position': positionadjusted, + 'big': True, + 'text': text, + 'trail': trail, + 'vr_depth': 0, + 'shadow': 0.0 if trail else 0.3, + 'scale': scale, + 'maxwidth': maxwidth if maxwidth is not None else 0.0, + 'tilt_translate': tilt_translate, + 'h_align': h_align, + 'v_align': 'center' + }) + + # we never jitter in vr mode.. + if ba.app.vr_mode: + jitter = 0.0 + + # if they want jitter, animate its position slightly... + if jitter > 0.0: + self._jitter(positionadjusted, jitter * scale) + + # if they want shifting, move to the shift position and + # then resume jittering + if shiftposition is not None: + positionadjusted2 = (shiftposition[0], shiftposition[1] - 100) + ba.timer( + shiftdelay, + ba.WeakCall(self._shift, positionadjusted, positionadjusted2)) + if jitter > 0.0: + ba.timer( + shiftdelay + 0.25, + ba.WeakCall(self._jitter, positionadjusted2, + jitter * scale)) + color_combine = ba.newnode('combine', + owner=self.node, + attrs={ + 'input2': color[2], + 'input3': 1.0, + 'size': 4 + }) + if trail: + trailcolor_n = ba.newnode('combine', + owner=self.node, + attrs={ + 'size': 3, + 'input0': trailcolor[0], + 'input1': trailcolor[1], + 'input2': trailcolor[2] + }) + trailcolor_n.connectattr('output', self.node, 'trailcolor') + basemult = 0.85 + ba.animate( + self.node, 'trail_project_scale', { + 0: 0 * project_scale, + basemult * 0.201: 0.6 * project_scale, + basemult * 0.347: 0.8 * project_scale, + basemult * 0.478: 0.9 * project_scale, + basemult * 0.595: 0.93 * project_scale, + basemult * 0.748: 0.95 * project_scale, + basemult * 0.941: 0.95 * project_scale + }) + if flash: + mult = 2.0 + tm1 = 0.15 + tm2 = 0.3 + ba.animate(color_combine, + 'input0', { + 0: color[0] * mult, + tm1: color[0], + tm2: color[0] * mult + }, + loop=True) + ba.animate(color_combine, + 'input1', { + 0: color[1] * mult, + tm1: color[1], + tm2: color[1] * mult + }, + loop=True) + ba.animate(color_combine, + 'input2', { + 0: color[2] * mult, + tm1: color[2], + tm2: color[2] * mult + }, + loop=True) + else: + color_combine.input0 = color[0] + color_combine.input1 = color[1] + color_combine.connectattr('output', self.node, 'color') + ba.animate(self.node, 'project_scale', { + 0: 0, + 0.27: 1.05 * project_scale, + 0.3: 1 * project_scale + }) + + # if they give us a lifespan, kill ourself down the line + if lifespan is not None: + ba.timer(lifespan, ba.WeakCall(self.handlemessage, + ba.DieMessage())) + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + if isinstance(msg, ba.DieMessage): + if not self._dying and self.node: + self._dying = True + if msg.immediate: + self.node.delete() + else: + ba.animate( + self.node, 'project_scale', { + 0.0: 1 * self._project_scale, + 0.6: 1.2 * self._project_scale + }) + ba.animate(self.node, 'opacity', {0.0: 1, 0.3: 0}) + ba.animate(self.node, 'trail_opacity', {0.0: 1, 0.6: 0}) + ba.timer(0.7, self.node.delete) + return None + return super().handlemessage(msg) + + def _jitter(self, position: Tuple[float, float], + jitter_amount: float) -> None: + if not self.node: + return + cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2}) + for index, attr in enumerate(['input0', 'input1']): + keys = {} + timeval = 0.0 + # gen some random keys for that stop-motion-y look + for _i in range(10): + keys[timeval] = (position[index] + + (random.random() - 0.5) * jitter_amount * 1.6) + timeval += random.random() * 0.1 + ba.animate(cmb, attr, keys, loop=True) + cmb.connectattr('output', self.node, 'position') + + def _shift(self, position1: Tuple[float, float], + position2: Tuple[float, float]) -> None: + if not self.node: + return + cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2}) + ba.animate(cmb, 'input0', {0.0: position1[0], 0.25: position2[0]}) + ba.animate(cmb, 'input1', {0.0: position1[1], 0.25: position2[1]}) + cmb.connectattr('output', self.node, 'position') diff --git a/dist/ba_data/python/bastd/appdelegate.py b/dist/ba_data/python/bastd/appdelegate.py new file mode 100644 index 0000000..9ae96c6 --- /dev/null +++ b/dist/ba_data/python/bastd/appdelegate.py @@ -0,0 +1,31 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provide our delegate for high level app functionality.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Type, Any, Dict, Callable, Optional + + +class AppDelegate(ba.AppDelegate): + """Defines handlers for high level app functionality.""" + + def create_default_game_settings_ui( + self, gameclass: Type[ba.GameActivity], + sessiontype: Type[ba.Session], settings: Optional[dict], + completion_call: Callable[[Optional[dict]], Any]) -> None: + """(internal)""" + + # Replace the main window once we come up successfully. + from bastd.ui.playlist.editgame import PlaylistEditGameWindow + ba.app.ui.clear_main_menu_window(transition='out_left') + ba.app.ui.set_main_menu_window( + PlaylistEditGameWindow( + gameclass, + sessiontype, + settings, + completion_call=completion_call).get_root_widget()) diff --git a/dist/ba_data/python/bastd/game/__init__.py b/dist/ba_data/python/bastd/game/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/game/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/game/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..9d0d914 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/assault.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/assault.cpython-38.opt-1.pyc new file mode 100644 index 0000000..bd01c1d Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/assault.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d25522f Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/chosenone.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/chosenone.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6a8825f Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/chosenone.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/chosenone.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/chosenone.cpython-38.pyc new file mode 100644 index 0000000..b2ed2e9 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/chosenone.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/conquest.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/conquest.cpython-38.opt-1.pyc new file mode 100644 index 0000000..da412db Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/conquest.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4270a7e Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-38.pyc new file mode 100644 index 0000000..7049f9b Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6d26105 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-38.pyc new file mode 100644 index 0000000..ee0cdad Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/elimination.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/elimination.cpython-38.opt-1.pyc new file mode 100644 index 0000000..ea2e58c Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/elimination.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/elimination.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/elimination.cpython-38.pyc new file mode 100644 index 0000000..fb71440 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/elimination.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/football.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/football.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6dcda47 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/football.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/football.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/football.cpython-38.pyc new file mode 100644 index 0000000..4486431 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/football.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/hockey.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/hockey.cpython-38.opt-1.pyc new file mode 100644 index 0000000..276db75 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/hockey.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/keepaway.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/keepaway.cpython-38.opt-1.pyc new file mode 100644 index 0000000..28f39af Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/keepaway.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/keepaway.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/keepaway.cpython-38.pyc new file mode 100644 index 0000000..014233f Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/keepaway.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-38.opt-1.pyc new file mode 100644 index 0000000..cd36f93 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-38.pyc new file mode 100644 index 0000000..952dd85 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-38.opt-1.pyc new file mode 100644 index 0000000..353d53c Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-38.pyc new file mode 100644 index 0000000..31799fa Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a95fc1c Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-38.pyc new file mode 100644 index 0000000..f7e1624 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/onslaught.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/onslaught.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7267dea Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/onslaught.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/onslaught.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/onslaught.cpython-38.pyc new file mode 100644 index 0000000..f0de6b9 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/onslaught.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/race.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/race.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d0b3501 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/race.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/race.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/race.cpython-38.pyc new file mode 100644 index 0000000..f2e9964 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/race.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/runaround.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/runaround.cpython-38.opt-1.pyc new file mode 100644 index 0000000..adcd08d Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/runaround.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/runaround.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/runaround.cpython-38.pyc new file mode 100644 index 0000000..efbd157 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/runaround.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c4bd298 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-38.pyc new file mode 100644 index 0000000..3069a6f Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-38.opt-1.pyc new file mode 100644 index 0000000..258ac94 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-38.pyc b/dist/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-38.pyc new file mode 100644 index 0000000..5321910 Binary files /dev/null and b/dist/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/game/assault.py b/dist/ba_data/python/bastd/game/assault.py new file mode 100644 index 0000000..44dc8ca --- /dev/null +++ b/dist/ba_data/python/bastd/game/assault.py @@ -0,0 +1,254 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines assault minigame.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.flag import Flag +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Sequence, Union + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self, base_pos: Sequence[float], flag: Flag) -> None: + self.base_pos = base_pos + self.flag = flag + self.score = 0 + + +# ba_meta export game +class AssaultGame(ba.TeamGameActivity[Player, Team]): + """Game where you score by touching the other team's flag.""" + + name = 'Assault' + description = 'Reach the enemy flag to score.' + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=3, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('team_flag') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._last_score_time = 0.0 + self._score_sound = ba.getsound('score') + self._base_region_materials: Dict[int, ba.Material] = {} + self._epic_mode = bool(settings['Epic Mode']) + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + # Base class overrides + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.FORWARD_MARCH) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Touch the enemy flag.' + return 'Touch the enemy flag ${ARG1} times.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'touch 1 flag' + return 'touch ${ARG1} flags', self._score_to_win + + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + shared = SharedObjects.get() + base_pos = self.map.get_flag_position(sessionteam.id) + ba.newnode('light', + attrs={ + 'position': base_pos, + 'intensity': 0.6, + 'height_attenuated': False, + 'volume_intensity_scale': 0.1, + 'radius': 0.1, + 'color': sessionteam.color + }) + Flag.project_stand(base_pos) + flag = Flag(touchable=False, + position=base_pos, + color=sessionteam.color) + team = Team(base_pos=base_pos, flag=flag) + + mat = self._base_region_materials[sessionteam.id] = ba.Material() + mat.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', ba.Call(self._handle_base_collide, + team)), + ), + ) + + ba.newnode( + 'region', + owner=flag.node, + attrs={ + 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), + 'scale': (0.5, 0.5, 0.5), + 'type': 'sphere', + 'materials': [self._base_region_materials[sessionteam.id]] + }) + + return team + + def on_team_join(self, team: Team) -> None: + # Can't do this in create_team because the team's color/etc. have + # not been wired up yet at that point. + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard. + self.respawn_player(msg.getplayer(Player)) + else: + super().handlemessage(msg) + + def _flash_base(self, team: Team, length: float = 2.0) -> None: + light = ba.newnode('light', + attrs={ + 'position': team.base_pos, + 'height_attenuated': False, + 'radius': 0.3, + 'color': team.color + }) + ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) + ba.timer(length, light.delete) + + def _handle_base_collide(self, team: Team) -> None: + try: + player = ba.getcollision().opposingnode.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except ba.NotFoundError: + return + + if not player.is_alive(): + return + + # If its another team's player, they scored. + player_team = player.team + if player_team is not team: + + # Prevent multiple simultaneous scores. + if ba.time() != self._last_score_time: + self._last_score_time = ba.time() + self.stats.player_scored(player, 50, big_message=True) + ba.playsound(self._score_sound) + self._flash_base(team) + + # Move all players on the scoring team back to their start + # and add flashes of light so its noticeable. + for player in player_team.players: + if player.is_alive(): + pos = player.node.position + light = ba.newnode('light', + attrs={ + 'position': pos, + 'color': player_team.color, + 'height_attenuated': False, + 'radius': 0.4 + }) + ba.timer(0.5, light.delete) + ba.animate(light, 'intensity', { + 0: 0, + 0.1: 1.0, + 0.5: 0 + }) + + new_pos = (self.map.get_start_position(player_team.id)) + light = ba.newnode('light', + attrs={ + 'position': new_pos, + 'color': player_team.color, + 'radius': 0.4, + 'height_attenuated': False + }) + ba.timer(0.5, light.delete) + ba.animate(light, 'intensity', { + 0: 0, + 0.1: 1.0, + 0.5: 0 + }) + if player.actor: + player.actor.handlemessage( + ba.StandMessage(new_pos, + random.uniform(0, 360))) + + # Have teammates celebrate. + for player in player_team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + player_team.score += 1 + self._update_scoreboard() + if player_team.score >= self._score_to_win: + self.end_game() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) diff --git a/dist/ba_data/python/bastd/game/capturetheflag.py b/dist/ba_data/python/bastd/game/capturetheflag.py new file mode 100644 index 0000000..638d2d3 --- /dev/null +++ b/dist/ba_data/python/bastd/game/capturetheflag.py @@ -0,0 +1,551 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines a capture-the-flag game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.flag import (FlagFactory, Flag, FlagPickedUpMessage, + FlagDroppedMessage, FlagDiedMessage) + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Sequence, Union, Optional + + +class CTFFlag(Flag): + """Special flag type for CTF games.""" + + activity: CaptureTheFlagGame + + def __init__(self, team: Team): + assert team.flagmaterial is not None + super().__init__(materials=[team.flagmaterial], + position=team.base_pos, + color=team.color) + self._team = team + self.held_count = 0 + self.counter = ba.newnode('text', + owner=self.node, + attrs={ + 'in_world': True, + 'scale': 0.02, + 'h_align': 'center' + }) + self.reset_return_times() + self.last_player_to_hold: Optional[Player] = None + self.time_out_respawn_time: Optional[int] = None + self.touch_return_time: Optional[float] = None + + def reset_return_times(self) -> None: + """Clear flag related times in the activity.""" + self.time_out_respawn_time = int(self.activity.flag_idle_return_time) + self.touch_return_time = float(self.activity.flag_touch_return_time) + + @property + def team(self) -> Team: + """The flag's team.""" + return self._team + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.touching_own_flag = 0 + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self, base_pos: Sequence[float], + base_region_material: ba.Material, base_region: ba.Node, + spaz_material_no_flag_physical: ba.Material, + spaz_material_no_flag_collide: ba.Material, + flagmaterial: ba.Material): + self.base_pos = base_pos + self.base_region_material = base_region_material + self.base_region = base_region + self.spaz_material_no_flag_physical = spaz_material_no_flag_physical + self.spaz_material_no_flag_collide = spaz_material_no_flag_collide + self.flagmaterial = flagmaterial + self.score = 0 + self.flag_return_touches = 0 + self.home_flag_at_base = True + self.touch_return_timer: Optional[ba.Timer] = None + self.enemy_flag_at_base = False + self.flag: Optional[CTFFlag] = None + self.last_flag_leave_time: Optional[float] = None + self.touch_return_timer_ticking: Optional[ba.NodeActor] = None + + +# ba_meta export game +class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): + """Game of stealing other team's flag and returning it to your base.""" + + name = 'Capture the Flag' + description = 'Return the enemy flag to score.' + available_settings = [ + ba.IntSetting('Score to Win', min_value=1, default=3), + ba.IntSetting( + 'Flag Touch Return Time', + min_value=0, + default=0, + increment=1, + ), + ba.IntSetting( + 'Flag Idle Return Time', + min_value=5, + default=30, + increment=5, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('team_flag') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._alarmsound = ba.getsound('alarm') + self._ticking_sound = ba.getsound('ticking') + self._score_sound = ba.getsound('score') + self._swipsound = ba.getsound('swip') + self._last_score_time = 0 + self._all_bases_material = ba.Material() + self._last_home_flag_notice_print_time = 0.0 + self._score_to_win = int(settings['Score to Win']) + self._epic_mode = bool(settings['Epic Mode']) + self._time_limit = float(settings['Time Limit']) + + self.flag_touch_return_time = float(settings['Flag Touch Return Time']) + self.flag_idle_return_time = float(settings['Flag Idle Return Time']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.FLAG_CATCHER) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Steal the enemy flag.' + return 'Steal the enemy flag ${ARG1} times.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'return 1 flag' + return 'return ${ARG1} flags', self._score_to_win + + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + + # Create our team instance and its initial values. + + base_pos = self.map.get_flag_position(sessionteam.id) + Flag.project_stand(base_pos) + + ba.newnode('light', + attrs={ + 'position': base_pos, + 'intensity': 0.6, + 'height_attenuated': False, + 'volume_intensity_scale': 0.1, + 'radius': 0.1, + 'color': sessionteam.color + }) + + base_region_mat = ba.Material() + pos = base_pos + base_region = ba.newnode( + 'region', + attrs={ + 'position': (pos[0], pos[1] + 0.75, pos[2]), + 'scale': (0.5, 0.5, 0.5), + 'type': 'sphere', + 'materials': [base_region_mat, self._all_bases_material] + }) + + spaz_mat_no_flag_physical = ba.Material() + spaz_mat_no_flag_collide = ba.Material() + flagmat = ba.Material() + + team = Team(base_pos=base_pos, + base_region_material=base_region_mat, + base_region=base_region, + spaz_material_no_flag_physical=spaz_mat_no_flag_physical, + spaz_material_no_flag_collide=spaz_mat_no_flag_collide, + flagmaterial=flagmat) + + # Some parts of our spazzes don't collide physically with our + # flags but generate callbacks. + spaz_mat_no_flag_physical.add_actions( + conditions=('they_have_material', flagmat), + actions=( + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + lambda: self._handle_touching_own_flag(team, True)), + ('call', 'at_disconnect', + lambda: self._handle_touching_own_flag(team, False)), + )) + + # Other parts of our spazzes don't collide with our flags at all. + spaz_mat_no_flag_collide.add_actions( + conditions=('they_have_material', flagmat), + actions=('modify_part_collision', 'collide', False), + ) + + # We wanna know when *any* flag enters/leaves our base. + base_region_mat.add_actions( + conditions=('they_have_material', FlagFactory.get().flagmaterial), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + lambda: self._handle_flag_entered_base(team)), + ('call', 'at_disconnect', + lambda: self._handle_flag_left_base(team)), + )) + + return team + + def on_team_join(self, team: Team) -> None: + # Can't do this in create_team because the team's color/etc. have + # not been wired up yet at that point. + self._spawn_flag_for_team(team) + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + ba.timer(1.0, call=self._tick, repeat=True) + + def _spawn_flag_for_team(self, team: Team) -> None: + team.flag = CTFFlag(team) + team.flag_return_touches = 0 + self._flash_base(team, length=1.0) + assert team.flag.node + ba.playsound(self._swipsound, position=team.flag.node.position) + + def _handle_flag_entered_base(self, team: Team) -> None: + try: + flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) + except ba.NotFoundError: + # Don't think this should logically ever happen. + print('Error getting CTFFlag in entering-base callback.') + return + + if flag.team is team: + team.home_flag_at_base = True + + # If the enemy flag is already here, score! + if team.enemy_flag_at_base: + self._score(team) + else: + team.enemy_flag_at_base = True + if team.home_flag_at_base: + # Award points to whoever was carrying the enemy flag. + player = flag.last_player_to_hold + if player and player.team is team: + assert self.stats + self.stats.player_scored(player, 50, big_message=True) + + # Update score and reset flags. + self._score(team) + + # If the home-team flag isn't here, print a message to that effect. + else: + # Don't want slo-mo affecting this + curtime = ba.time(ba.TimeType.BASE) + if curtime - self._last_home_flag_notice_print_time > 5.0: + self._last_home_flag_notice_print_time = curtime + bpos = team.base_pos + tval = ba.Lstr(resource='ownFlagAtYourBaseWarning') + tnode = ba.newnode( + 'text', + attrs={ + 'text': tval, + 'in_world': True, + 'scale': 0.013, + 'color': (1, 1, 0, 1), + 'h_align': 'center', + 'position': (bpos[0], bpos[1] + 3.2, bpos[2]) + }) + ba.timer(5.1, tnode.delete) + ba.animate(tnode, 'scale', { + 0.0: 0, + 0.2: 0.013, + 4.8: 0.013, + 5.0: 0 + }) + + def _tick(self) -> None: + # If either flag is away from base and not being held, tick down its + # respawn timer. + for team in self.teams: + flag = team.flag + assert flag is not None + + if not team.home_flag_at_base and flag.held_count == 0: + time_out_counting_down = True + if flag.time_out_respawn_time is None: + flag.reset_return_times() + assert flag.time_out_respawn_time is not None + flag.time_out_respawn_time -= 1 + if flag.time_out_respawn_time <= 0: + flag.handlemessage(ba.DieMessage()) + else: + time_out_counting_down = False + + if flag.node and flag.counter: + pos = flag.node.position + flag.counter.position = (pos[0], pos[1] + 1.3, pos[2]) + + # If there's no self-touches on this flag, set its text + # to show its auto-return counter. (if there's self-touches + # its showing that time). + if team.flag_return_touches == 0: + flag.counter.text = (str(flag.time_out_respawn_time) if ( + time_out_counting_down + and flag.time_out_respawn_time is not None + and flag.time_out_respawn_time <= 10) else '') + flag.counter.color = (1, 1, 1, 0.5) + flag.counter.scale = 0.014 + + def _score(self, team: Team) -> None: + team.score += 1 + ba.playsound(self._score_sound) + self._flash_base(team) + self._update_scoreboard() + + # Have teammates celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + # Reset all flags/state. + for reset_team in self.teams: + if not reset_team.home_flag_at_base: + assert reset_team.flag is not None + reset_team.flag.handlemessage(ba.DieMessage()) + reset_team.enemy_flag_at_base = False + if team.score >= self._score_to_win: + self.end_game() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results, announce_delay=0.8) + + def _handle_flag_left_base(self, team: Team) -> None: + cur_time = ba.time() + try: + flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) + except ba.NotFoundError: + # This can happen if the flag stops touching us due to being + # deleted; that's ok. + return + + if flag.team is team: + + # Check times here to prevent too much flashing. + if (team.last_flag_leave_time is None + or cur_time - team.last_flag_leave_time > 3.0): + ba.playsound(self._alarmsound, position=team.base_pos) + self._flash_base(team) + team.last_flag_leave_time = cur_time + team.home_flag_at_base = False + else: + team.enemy_flag_at_base = False + + def _touch_return_update(self, team: Team) -> None: + # Count down only while its away from base and not being held. + assert team.flag is not None + if team.home_flag_at_base or team.flag.held_count > 0: + team.touch_return_timer_ticking = None + return # No need to return when its at home. + if team.touch_return_timer_ticking is None: + team.touch_return_timer_ticking = ba.NodeActor( + ba.newnode('sound', + attrs={ + 'sound': self._ticking_sound, + 'positional': False, + 'loop': True + })) + flag = team.flag + if flag.touch_return_time is not None: + flag.touch_return_time -= 0.1 + if flag.counter: + flag.counter.text = f'{flag.touch_return_time:.1f}' + flag.counter.color = (1, 1, 0, 1) + flag.counter.scale = 0.02 + + if flag.touch_return_time <= 0.0: + self._award_players_touching_own_flag(team) + flag.handlemessage(ba.DieMessage()) + + def _award_players_touching_own_flag(self, team: Team) -> None: + for player in team.players: + if player.touching_own_flag > 0: + return_score = 10 + 5 * int(self.flag_touch_return_time) + self.stats.player_scored(player, + return_score, + screenmessage=False) + + def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None: + """Called when a player touches or stops touching their own team flag. + + We keep track of when each player is touching their own flag so we + can award points when returned. + """ + player: Optional[Player] + try: + player = ba.getcollision().sourcenode.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except ba.NotFoundError: + # This can happen if the player leaves but his corpse touches/etc. + player = None + + if player: + player.touching_own_flag += (1 if connecting else -1) + + # If return-time is zero, just kill it immediately.. otherwise keep + # track of touches and count down. + if float(self.flag_touch_return_time) <= 0.0: + assert team.flag is not None + if (connecting and not team.home_flag_at_base + and team.flag.held_count == 0): + self._award_players_touching_own_flag(team) + ba.getcollision().opposingnode.handlemessage(ba.DieMessage()) + + # Takes a non-zero amount of time to return. + else: + if connecting: + team.flag_return_touches += 1 + if team.flag_return_touches == 1: + team.touch_return_timer = ba.Timer( + 0.1, + call=ba.Call(self._touch_return_update, team), + repeat=True) + team.touch_return_timer_ticking = None + else: + team.flag_return_touches -= 1 + if team.flag_return_touches == 0: + team.touch_return_timer = None + team.touch_return_timer_ticking = None + if team.flag_return_touches < 0: + ba.print_error('CTF flag_return_touches < 0') + + def _flash_base(self, team: Team, length: float = 2.0) -> None: + light = ba.newnode('light', + attrs={ + 'position': team.base_pos, + 'height_attenuated': False, + 'radius': 0.3, + 'color': team.color + }) + ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True) + ba.timer(length, light.delete) + + def spawn_player_spaz(self, + player: Player, + position: Sequence[float] = None, + angle: float = None) -> PlayerSpaz: + """Intercept new spazzes and add our team material for them.""" + spaz = super().spawn_player_spaz(player, position, angle) + player = spaz.getplayer(Player, True) + team: Team = player.team + player.touching_own_flag = 0 + no_physical_mats: List[ba.Material] = [ + team.spaz_material_no_flag_physical + ] + no_collide_mats: List[ba.Material] = [ + team.spaz_material_no_flag_collide + ] + + # Our normal parts should still collide; just not physically + # (so we can calc restores). + assert spaz.node + spaz.node.materials = list(spaz.node.materials) + no_physical_mats + spaz.node.roller_materials = list( + spaz.node.roller_materials) + no_physical_mats + + # Pickups and punches shouldn't hit at all though. + spaz.node.punch_materials = list( + spaz.node.punch_materials) + no_collide_mats + spaz.node.pickup_materials = list( + spaz.node.pickup_materials) + no_collide_mats + spaz.node.extras_material = list( + spaz.node.extras_material) + no_collide_mats + return spaz + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + self.respawn_player(msg.getplayer(Player)) + + elif isinstance(msg, FlagDiedMessage): + assert isinstance(msg.flag, CTFFlag) + ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team)) + + elif isinstance(msg, FlagPickedUpMessage): + + # Store the last player to hold the flag for scoring purposes. + assert isinstance(msg.flag, CTFFlag) + try: + msg.flag.last_player_to_hold = msg.node.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except ba.NotFoundError: + pass + + msg.flag.held_count += 1 + msg.flag.reset_return_times() + + elif isinstance(msg, FlagDroppedMessage): + # Store the last player to hold the flag for scoring purposes. + assert isinstance(msg.flag, CTFFlag) + msg.flag.held_count -= 1 + + else: + super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/game/chosenone.py b/dist/ba_data/python/bastd/game/chosenone.py new file mode 100644 index 0000000..465dec9 --- /dev/null +++ b/dist/ba_data/python/bastd/game/chosenone.py @@ -0,0 +1,338 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides the chosen-one mini-game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.flag import Flag +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Optional, Sequence, Union + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.chosen_light: Optional[ba.NodeActor] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self, time_remaining: int) -> None: + self.time_remaining = time_remaining + + +# ba_meta export game +class ChosenOneGame(ba.TeamGameActivity[Player, Team]): + """ + Game involving trying to remain the one 'chosen one' + for a set length of time while everyone else tries to + kill you and become the chosen one themselves. + """ + + name = 'Chosen One' + description = ('Be the chosen one for a length of time to win.\n' + 'Kill the chosen one to become it.') + available_settings = [ + ba.IntSetting( + 'Chosen One Time', + min_value=10, + default=30, + increment=10, + ), + ba.BoolSetting('Chosen One Gets Gloves', default=True), + ba.BoolSetting('Chosen One Gets Shield', default=False), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + scoreconfig = ba.ScoreConfig(label='Time Held') + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('keep_away') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._chosen_one_player: Optional[Player] = None + self._swipsound = ba.getsound('swip') + self._countdownsounds: Dict[int, ba.Sound] = { + 10: ba.getsound('announceTen'), + 9: ba.getsound('announceNine'), + 8: ba.getsound('announceEight'), + 7: ba.getsound('announceSeven'), + 6: ba.getsound('announceSix'), + 5: ba.getsound('announceFive'), + 4: ba.getsound('announceFour'), + 3: ba.getsound('announceThree'), + 2: ba.getsound('announceTwo'), + 1: ba.getsound('announceOne') + } + self._flag_spawn_pos: Optional[Sequence[float]] = None + self._reset_region_material: Optional[ba.Material] = None + self._flag: Optional[Flag] = None + self._reset_region: Optional[ba.Node] = None + self._epic_mode = bool(settings['Epic Mode']) + self._chosen_one_time = int(settings['Chosen One Time']) + self._time_limit = float(settings['Time Limit']) + self._chosen_one_gets_shield = bool(settings['Chosen One Gets Shield']) + self._chosen_one_gets_gloves = bool(settings['Chosen One Gets Gloves']) + + # Base class overrides + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.CHOSEN_ONE) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'There can be only one.' + + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + return Team(time_remaining=self._chosen_one_time) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + if self._get_chosen_one_player() is player: + self._set_chosen_one_player(None) + + def on_begin(self) -> None: + super().on_begin() + shared = SharedObjects.get() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._flag_spawn_pos = self.map.get_flag_position(None) + Flag.project_stand(self._flag_spawn_pos) + self._set_chosen_one_player(None) + + pos = self._flag_spawn_pos + ba.timer(1.0, call=self._tick, repeat=True) + + mat = self._reset_region_material = ba.Material() + mat.add_actions( + conditions=( + 'they_have_material', + shared.player_material, + ), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + ba.WeakCall(self._handle_reset_collide)), + ), + ) + + self._reset_region = ba.newnode('region', + attrs={ + 'position': (pos[0], pos[1] + 0.75, + pos[2]), + 'scale': (0.5, 0.5, 0.5), + 'type': 'sphere', + 'materials': [mat] + }) + + def _get_chosen_one_player(self) -> Optional[Player]: + # Should never return invalid references; return None in that case. + if self._chosen_one_player: + return self._chosen_one_player + return None + + def _handle_reset_collide(self) -> None: + # If we have a chosen one, ignore these. + if self._get_chosen_one_player() is not None: + return + + # Attempt to get a Player controlling a Spaz that we hit. + try: + player = ba.getcollision().opposingnode.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except ba.NotFoundError: + return + + if player.is_alive(): + self._set_chosen_one_player(player) + + def _flash_flag_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._flag_spawn_pos, + 'color': (1, 1, 1), + 'radius': 0.3, + 'height_attenuated': False + }) + ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _tick(self) -> None: + + # Give the chosen one points. + player = self._get_chosen_one_player() + if player is not None: + + # This shouldn't happen, but just in case. + if not player.is_alive(): + ba.print_error('got dead player as chosen one in _tick') + self._set_chosen_one_player(None) + else: + scoring_team = player.team + assert self.stats + self.stats.player_scored(player, + 3, + screenmessage=False, + display=False) + + scoring_team.time_remaining = max( + 0, scoring_team.time_remaining - 1) + + # Show the count over their head + if scoring_team.time_remaining > 0: + if isinstance(player.actor, PlayerSpaz) and player.actor: + player.actor.set_score_text( + str(scoring_team.time_remaining)) + + self._update_scoreboard() + + # announce numbers we have sounds for + if scoring_team.time_remaining in self._countdownsounds: + ba.playsound( + self._countdownsounds[scoring_team.time_remaining]) + + # Winner! + if scoring_team.time_remaining <= 0: + self.end_game() + + else: + # (player is None) + # This shouldn't happen, but just in case. + # (Chosen-one player ceasing to exist should + # trigger on_player_leave which resets chosen-one) + if self._chosen_one_player is not None: + ba.print_error('got nonexistent player as chosen one in _tick') + self._set_chosen_one_player(None) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, + self._chosen_one_time - team.time_remaining) + self.end(results=results, announce_delay=0) + + def _set_chosen_one_player(self, player: Optional[Player]) -> None: + existing = self._get_chosen_one_player() + if existing: + existing.chosen_light = None + ba.playsound(self._swipsound) + if not player: + assert self._flag_spawn_pos is not None + self._flag = Flag(color=(1, 0.9, 0.2), + position=self._flag_spawn_pos, + touchable=False) + self._chosen_one_player = None + + # Create a light to highlight the flag; + # this will go away when the flag dies. + ba.newnode('light', + owner=self._flag.node, + attrs={ + 'position': self._flag_spawn_pos, + 'intensity': 0.6, + 'height_attenuated': False, + 'volume_intensity_scale': 0.1, + 'radius': 0.1, + 'color': (1.2, 1.2, 0.4) + }) + + # Also an extra momentary flash. + self._flash_flag_spawn() + else: + if player.actor: + self._flag = None + self._chosen_one_player = player + + if self._chosen_one_gets_shield: + player.actor.handlemessage(ba.PowerupMessage('shield')) + if self._chosen_one_gets_gloves: + player.actor.handlemessage(ba.PowerupMessage('punch')) + + # Use a color that's partway between their team color + # and white. + color = [ + 0.3 + c * 0.7 + for c in ba.normalized_color(player.team.color) + ] + light = player.chosen_light = ba.NodeActor( + ba.newnode('light', + attrs={ + 'intensity': 0.6, + 'height_attenuated': False, + 'volume_intensity_scale': 0.1, + 'radius': 0.13, + 'color': color + })) + + assert light.node + ba.animate(light.node, + 'intensity', { + 0: 1.0, + 0.2: 0.4, + 0.4: 1.0 + }, + loop=True) + assert isinstance(player.actor, PlayerSpaz) + player.actor.node.connectattr('position', light.node, + 'position') + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if player is self._get_chosen_one_player(): + killerplayer = msg.getkillerplayer(Player) + self._set_chosen_one_player(None if ( + killerplayer is None or killerplayer is player + or not killerplayer.is_alive()) else killerplayer) + self.respawn_player(player) + else: + super().handlemessage(msg) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, + team.time_remaining, + self._chosen_one_time, + countdown=True) diff --git a/dist/ba_data/python/bastd/game/conquest.py b/dist/ba_data/python/bastd/game/conquest.py new file mode 100644 index 0000000..b2404b8 --- /dev/null +++ b/dist/ba_data/python/bastd/game/conquest.py @@ -0,0 +1,316 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides the Conquest game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.actor.flag import Flag +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.playerspaz import PlayerSpaz +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Optional, Type, List, Dict, Sequence, Union + from bastd.actor.respawnicon import RespawnIcon + + +class ConquestFlag(Flag): + """A custom flag for use with Conquest games.""" + + def __init__(self, *args: Any, **keywds: Any): + super().__init__(*args, **keywds) + self._team: Optional[Team] = None + self.light: Optional[ba.Node] = None + + @property + def team(self) -> Optional[Team]: + """The team that owns this flag.""" + return self._team + + @team.setter + def team(self, team: Team) -> None: + """Set the team that owns this flag.""" + self._team = team + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + # FIXME: We shouldn't be using customdata here + # (but need to update respawn funcs accordingly first). + @property + def respawn_timer(self) -> Optional[ba.Timer]: + """Type safe access to standard respawn timer.""" + return self.customdata.get('respawn_timer', None) + + @respawn_timer.setter + def respawn_timer(self, value: Optional[ba.Timer]) -> None: + self.customdata['respawn_timer'] = value + + @property + def respawn_icon(self) -> Optional[RespawnIcon]: + """Type safe access to standard respawn icon.""" + return self.customdata.get('respawn_icon', None) + + @respawn_icon.setter + def respawn_icon(self, value: Optional[RespawnIcon]) -> None: + self.customdata['respawn_icon'] = value + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.flags_held = 0 + + +# ba_meta export game +class ConquestGame(ba.TeamGameActivity[Player, Team]): + """A game where teams try to claim all flags on the map.""" + + name = 'Conquest' + description = 'Secure all flags on the map to win.' + available_settings = [ + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('conquest') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._score_sound = ba.getsound('score') + self._swipsound = ba.getsound('swip') + self._extraflagmat = ba.Material() + self._flags: List[ConquestFlag] = [] + self._epic_mode = bool(settings['Epic Mode']) + self._time_limit = float(settings['Time Limit']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.GRAND_ROMP) + + # We want flags to tell us they've been hit but not react physically. + self._extraflagmat.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('call', 'at_connect', self._handle_flag_player_collide), + )) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Secure all ${ARG1} flags.', len(self.map.flag_points) + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'secure all ${ARG1} flags', len(self.map.flag_points) + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scores() + + def on_player_join(self, player: Player) -> None: + player.respawn_timer = None + + # Only spawn if this player's team has a flag currently. + if player.team.flags_held > 0: + self.spawn_player(player) + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + # Set up flags with marker lights. + for i in range(len(self.map.flag_points)): + point = self.map.flag_points[i] + flag = ConquestFlag(position=point, + touchable=False, + materials=[self._extraflagmat]) + self._flags.append(flag) + Flag.project_stand(point) + flag.light = ba.newnode('light', + owner=flag.node, + attrs={ + 'position': point, + 'intensity': 0.25, + 'height_attenuated': False, + 'radius': 0.3, + 'color': (1, 1, 1) + }) + + # Give teams a flag to start with. + for i in range(len(self.teams)): + self._flags[i].team = self.teams[i] + light = self._flags[i].light + assert light + node = self._flags[i].node + assert node + light.color = self.teams[i].color + node.color = self.teams[i].color + + self._update_scores() + + # Initial joiners didn't spawn due to no flags being owned yet; + # spawn them now. + for player in self.players: + self.spawn_player(player) + + def _update_scores(self) -> None: + for team in self.teams: + team.flags_held = 0 + for flag in self._flags: + if flag.team is not None: + flag.team.flags_held += 1 + for team in self.teams: + + # If a team finds themselves with no flags, cancel all + # outstanding spawn-timers. + if team.flags_held == 0: + for player in team.players: + player.respawn_timer = None + player.respawn_icon = None + if team.flags_held == len(self._flags): + self.end_game() + self._scoreboard.set_team_value(team, team.flags_held, + len(self._flags)) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.flags_held) + self.end(results=results) + + def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None: + assert flag.node + assert flag.light + light = ba.newnode('light', + attrs={ + 'position': flag.node.position, + 'height_attenuated': False, + 'color': flag.light.color + }) + ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True) + ba.timer(length, light.delete) + + def _handle_flag_player_collide(self) -> None: + collision = ba.getcollision() + try: + flag = collision.sourcenode.getdelegate(ConquestFlag, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + assert flag.light + + if flag.team is not player.team: + flag.team = player.team + flag.light.color = player.team.color + flag.node.color = player.team.color + self.stats.player_scored(player, 10, screenmessage=False) + ba.playsound(self._swipsound) + self._flash_flag(flag) + self._update_scores() + + # Respawn any players on this team that were in limbo due to the + # lack of a flag for their team. + for otherplayer in self.players: + if (otherplayer.team is flag.team + and otherplayer.actor is not None + and not otherplayer.is_alive() + and otherplayer.respawn_timer is None): + self.spawn_player(otherplayer) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + + # Respawn only if this team has a flag. + player = msg.getplayer(Player) + if player.team.flags_held > 0: + self.respawn_player(player) + else: + player.respawn_timer = None + + else: + super().handlemessage(msg) + + def spawn_player(self, player: Player) -> ba.Actor: + # We spawn players at different places based on what flags are held. + return self.spawn_player_spaz(player, + self._get_player_spawn_position(player)) + + def _get_player_spawn_position(self, player: Player) -> Sequence[float]: + + # Iterate until we find a spawn owned by this team. + spawn_count = len(self.map.spawn_by_flag_points) + + # Get all spawns owned by this team. + spawns = [ + i for i in range(spawn_count) if self._flags[i].team is player.team + ] + + closest_spawn = 0 + closest_distance = 9999.0 + + # Now find the spawn that's closest to a spawn not owned by us; + # we'll use that one. + for spawn in spawns: + spt = self.map.spawn_by_flag_points[spawn] + our_pt = ba.Vec3(spt[0], spt[1], spt[2]) + for otherspawn in [ + i for i in range(spawn_count) + if self._flags[i].team is not player.team + ]: + spt = self.map.spawn_by_flag_points[otherspawn] + their_pt = ba.Vec3(spt[0], spt[1], spt[2]) + dist = (their_pt - our_pt).length() + if dist < closest_distance: + closest_distance = dist + closest_spawn = spawn + + pos = self.map.spawn_by_flag_points[closest_spawn] + x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3]) + z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5]) + pos = (pos[0] + random.uniform(*x_range), pos[1], + pos[2] + random.uniform(*z_range)) + return pos diff --git a/dist/ba_data/python/bastd/game/deathmatch.py b/dist/ba_data/python/bastd/game/deathmatch.py new file mode 100644 index 0000000..f91c7af --- /dev/null +++ b/dist/ba_data/python/bastd/game/deathmatch.py @@ -0,0 +1,199 @@ +# Released under the MIT License. See LICENSE for details. +# +"""DeathMatch game and support classes.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class DeathMatchGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Death Match' + description = 'Kill a set number of enemies to win.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + min_value=1, + default=5, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, ba.FreeForAllSession): + settings.append( + ba.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._kills_to_win_per_player = int( + settings['Kills to Win Per Player']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Crush ${ARG1} of your enemies.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, ba.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + ba.playsound(self._dingsound) + for team in self.teams: + if team is not killer.team: + team.score += 1 + + # Killing someone on another team nets a kill. + else: + killer.team.score += 1 + ba.playsound(self._dingsound) + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + ba.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/dist/ba_data/python/bastd/game/easteregghunt.py b/dist/ba_data/python/bastd/game/easteregghunt.py new file mode 100644 index 0000000..2c5b9b0 --- /dev/null +++ b/dist/ba_data/python/bastd/game/easteregghunt.py @@ -0,0 +1,271 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides an easter egg hunt game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.actor.bomb import Bomb +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.spazbot import SpazBotSet, BouncyBot, SpazBotDiedMessage +from bastd.actor.onscreencountdown import OnScreenCountdown +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.respawnicon import RespawnIcon +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Type, Dict, List, Tuple, Optional + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.respawn_timer: Optional[ba.Timer] = None + self.respawn_icon: Optional[RespawnIcon] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): + """A game where score is based on collecting eggs.""" + + name = 'Easter Egg Hunt' + description = 'Gather eggs!' + available_settings = [ba.BoolSetting('Pro Mode', default=False)] + scoreconfig = ba.ScoreConfig(label='Score', scoretype=ba.ScoreType.POINTS) + + # We're currently hard-coded for one map. + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Tower D'] + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.CoopSession) + or issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._last_player_death_time = None + self._scoreboard = Scoreboard() + self.egg_model = ba.getmodel('egg') + self.egg_tex_1 = ba.gettexture('eggTex1') + self.egg_tex_2 = ba.gettexture('eggTex2') + self.egg_tex_3 = ba.gettexture('eggTex3') + self._collect_sound = ba.getsound('powerup01') + self._pro_mode = settings.get('Pro Mode', False) + self._max_eggs = 1.0 + self.egg_material = ba.Material() + self.egg_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', self._on_egg_player_collide), )) + self._eggs: List[Egg] = [] + self._update_timer: Optional[ba.Timer] = None + self._countdown: Optional[OnScreenCountdown] = None + self._bots: Optional[SpazBotSet] = None + + # Base class overrides + self.default_music = ba.MusicType.FORWARD_MARCH + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + # Called when our game actually starts. + def on_begin(self) -> None: + from bastd.maps import TowerD + + # There's a player-wall on the tower-d level to prevent + # players from getting up on the stairs.. we wanna kill that. + gamemap = self.map + assert isinstance(gamemap, TowerD) + gamemap.player_wall.delete() + super().on_begin() + self._update_scoreboard() + self._update_timer = ba.Timer(0.25, self._update, repeat=True) + self._countdown = OnScreenCountdown(60, endcall=self.end_game) + ba.timer(4.0, self._countdown.start) + self._bots = SpazBotSet() + + # Spawn evil bunny in co-op only. + if isinstance(self.session, ba.CoopSession) and self._pro_mode: + self._spawn_evil_bunny() + + # Overriding the default character spawning. + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + spaz.connect_controls_to_player() + return spaz + + def _spawn_evil_bunny(self) -> None: + assert self._bots is not None + self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0) + + def _on_egg_player_collide(self) -> None: + if self.has_ended(): + return + collision = ba.getcollision() + + # Be defensive here; we could be hitting the corpse of a player + # who just left/etc. + try: + egg = collision.sourcenode.getdelegate(Egg, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + player.team.score += 1 + + # Displays a +1 (and adds to individual player score in + # teams mode). + self.stats.player_scored(player, 1, screenmessage=False) + if self._max_eggs < 5: + self._max_eggs += 1.0 + elif self._max_eggs < 10: + self._max_eggs += 0.5 + elif self._max_eggs < 30: + self._max_eggs += 0.3 + self._update_scoreboard() + ba.playsound(self._collect_sound, 0.5, position=egg.node.position) + + # Create a flash. + light = ba.newnode('light', + attrs={ + 'position': egg.node.position, + 'height_attenuated': False, + 'radius': 0.1, + 'color': (1, 1, 0) + }) + ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False) + ba.timer(0.200, light.delete) + egg.handlemessage(ba.DieMessage()) + + def _update(self) -> None: + # Misc. periodic updating. + xpos = random.uniform(-7.1, 6.0) + ypos = random.uniform(3.5, 3.5) + zpos = random.uniform(-8.2, 3.7) + + # Prune dead eggs from our list. + self._eggs = [e for e in self._eggs if e] + + # Spawn more eggs if we've got space. + if len(self._eggs) < int(self._max_eggs): + + # Occasionally spawn a land-mine in addition. + if self._pro_mode and random.random() < 0.25: + mine = Bomb(position=(xpos, ypos, zpos), + bomb_type='land_mine').autoretain() + mine.arm() + else: + self._eggs.append(Egg(position=(xpos, ypos, zpos))) + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players. + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + + # Respawn them shortly. + player = msg.getplayer(Player) + assert self.initialplayerinfos is not None + respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 + player.respawn_timer = ba.Timer( + respawn_time, ba.Call(self.spawn_player_if_exists, player)) + player.respawn_icon = RespawnIcon(player, respawn_time) + + # Whenever our evil bunny dies, respawn him and spew some eggs. + elif isinstance(msg, SpazBotDiedMessage): + self._spawn_evil_bunny() + assert msg.spazbot.node + pos = msg.spazbot.node.position + for _i in range(6): + spread = 0.4 + self._eggs.append( + Egg(position=(pos[0] + random.uniform(-spread, spread), + pos[1] + random.uniform(-spread, spread), + pos[2] + random.uniform(-spread, spread)))) + else: + # Default handler. + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results) + + +class Egg(ba.Actor): + """A lovely egg that can be picked up for points.""" + + def __init__(self, position: Tuple[float, float, float] = (0.0, 1.0, 0.0)): + super().__init__() + activity = self.activity + assert isinstance(activity, EasterEggHuntGame) + shared = SharedObjects.get() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + ctex = (activity.egg_tex_1, activity.egg_tex_2, + activity.egg_tex_3)[random.randrange(3)] + mats = [shared.object_material, activity.egg_material] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': activity.egg_model, + 'color_texture': ctex, + 'body': 'capsule', + 'reflection': 'soft', + 'model_scale': 0.5, + 'body_scale': 0.6, + 'density': 4.0, + 'reflection_scale': [0.15], + 'shadow_size': 0.6, + 'position': self._spawn_pos, + 'materials': mats + }) + + def exists(self) -> bool: + return bool(self.node) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + elif isinstance(msg, ba.HitMessage): + if self.node: + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], + 1.0 * msg.magnitude, 1.0 * msg.velocity_magnitude, + msg.radius, 0, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + else: + super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/game/elimination.py b/dist/ba_data/python/bastd/game/elimination.py new file mode 100644 index 0000000..061ff0b --- /dev/null +++ b/dist/ba_data/python/bastd/game/elimination.py @@ -0,0 +1,564 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Elimination mini-game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.spazfactory import SpazFactory +from bastd.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import (Any, Tuple, Dict, Type, List, Sequence, Optional, + Union) + + +class Icon(ba.Actor): + """Creates in in-game icon on screen.""" + + def __init__(self, + player: Player, + position: Tuple[float, float], + scale: float, + show_lives: bool = True, + show_death: bool = True, + name_scale: float = 1.0, + name_maxwidth: float = 115.0, + flatness: float = 1.0, + shadow: float = 1.0): + super().__init__() + + self._player = player + self._show_lives = show_lives + self._show_death = show_death + self._name_scale = name_scale + self._outline_tex = ba.gettexture('characterIconMask') + + icon = player.get_icon() + self.node = ba.newnode('image', + delegate=self, + attrs={ + 'texture': icon['texture'], + 'tint_texture': icon['tint_texture'], + 'tint_color': icon['tint_color'], + 'vr_depth': 400, + 'tint2_color': icon['tint2_color'], + 'mask_texture': self._outline_tex, + 'opacity': 1.0, + 'absolute_scale': True, + 'attach': 'bottomCenter' + }) + self._name_text = ba.newnode( + 'text', + owner=self.node, + attrs={ + 'text': ba.Lstr(value=player.getname()), + 'color': ba.safecolor(player.team.color), + 'h_align': 'center', + 'v_align': 'center', + 'vr_depth': 410, + 'maxwidth': name_maxwidth, + 'shadow': shadow, + 'flatness': flatness, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + if self._show_lives: + self._lives_text = ba.newnode('text', + owner=self.node, + attrs={ + 'text': 'x0', + 'color': (1, 1, 0.5), + 'h_align': 'left', + 'vr_depth': 430, + 'shadow': 1.0, + 'flatness': 1.0, + 'h_attach': 'center', + 'v_attach': 'bottom' + }) + self.set_position_and_scale(position, scale) + + def set_position_and_scale(self, position: Tuple[float, float], + scale: float) -> None: + """(Re)position the icon.""" + assert self.node + self.node.position = position + self.node.scale = [70.0 * scale] + self._name_text.position = (position[0], position[1] + scale * 52.0) + self._name_text.scale = 1.0 * scale * self._name_scale + if self._show_lives: + self._lives_text.position = (position[0] + scale * 10.0, + position[1] - scale * 43.0) + self._lives_text.scale = 1.0 * scale + + def update_for_lives(self) -> None: + """Update for the target player's current lives.""" + if self._player: + lives = self._player.lives + else: + lives = 0 + if self._show_lives: + if lives > 0: + self._lives_text.text = 'x' + str(lives - 1) + else: + self._lives_text.text = '' + if lives == 0: + self._name_text.opacity = 0.2 + assert self.node + self.node.color = (0.7, 0.3, 0.3) + self.node.opacity = 0.2 + + def handle_player_spawned(self) -> None: + """Our player spawned; hooray!""" + if not self.node: + return + self.node.opacity = 1.0 + self.update_for_lives() + + def handle_player_died(self) -> None: + """Well poo; our player died.""" + if not self.node: + return + if self._show_death: + ba.animate( + self.node, 'opacity', { + 0.00: 1.0, + 0.05: 0.0, + 0.10: 1.0, + 0.15: 0.0, + 0.20: 1.0, + 0.25: 0.0, + 0.30: 1.0, + 0.35: 0.0, + 0.40: 1.0, + 0.45: 0.0, + 0.50: 1.0, + 0.55: 0.2 + }) + lives = self._player.lives + if lives == 0: + ba.timer(0.6, self.update_for_lives) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + self.node.delete() + return None + return super().handlemessage(msg) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.lives = 0 + self.icons: List[Icon] = [] + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.survival_seconds: Optional[int] = None + self.spawn_order: List[Player] = [] + + +# ba_meta export game +class EliminationGame(ba.TeamGameActivity[Player, Team]): + """Game type where last player(s) left alive win.""" + + name = 'Elimination' + description = 'Last remaining alive wins.' + scoreconfig = ba.ScoreConfig(label='Survived', + scoretype=ba.ScoreType.SECONDS, + none_is_winner=True) + # Show messages when players die since it's meaningful here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting( + 'Lives Per Player', + default=1, + min_value=1, + max_value=10, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + if issubclass(sessiontype, ba.DualTeamSession): + settings.append(ba.BoolSetting('Solo Mode', default=False)) + settings.append( + ba.BoolSetting('Balance Total Lives', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._start_time: Optional[float] = None + self._vs_text: Optional[ba.Actor] = None + self._round_end_timer: Optional[ba.Timer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = int(settings['Lives Per Player']) + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Last team standing wins.' if isinstance( + self.session, ba.DualTeamSession) else 'Last one standing wins.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'last team standing wins' if isinstance( + self.session, ba.DualTeamSession) else 'last one standing wins' + + def on_player_join(self, player: Player) -> None: + + # No longer allowing mid-game joiners here; too easy to exploit. + if self.has_begun(): + + # Make sure their team has survival seconds set if they're all dead + # (otherwise blocked new ffa players are considered 'still alive' + # in score tallying). + if (self._get_total_team_lives(player.team) == 0 + and player.team.survival_seconds is None): + player.team.survival_seconds = 0 + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + return + + player.lives = self._lives_per_player + + if self._solo_mode: + player.team.spawn_order.append(player) + self._update_solo_mode() + else: + # Create our icon and spawn. + player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: + self.spawn_player(player) + + # Don't waste time doing this until begin. + if self.has_begun(): + self._update_icons() + + def on_begin(self) -> None: + super().on_begin() + self._start_time = ba.time() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + if self._solo_mode: + self._vs_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': ba.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, ba.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + ba.timer(1.0, self._update, repeat=True) + + def _update_solo_mode(self) -> None: + # For both teams, find the first player on the spawn order list with + # lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + break + + def _update_icons(self) -> None: + # pylint: disable=too-many-branches + + # In free-for-all mode, everyone is just lined up along the bottom. + if isinstance(self.session, ba.FreeForAllSession): + count = len(self.teams) + x_offs = 85 + xval = x_offs * (count - 1) * -0.5 + for team in self.teams: + if len(team.players) == 1: + player = team.players[0] + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + # In teams mode we split up teams. + else: + if self._solo_mode: + # First off, clear out all icons. + for player in self.players: + player.icons = [] + + # Now for each team, cycle through our available players + # adding icons. + for team in self.teams: + if team.id == 0: + xval = -60 + x_offs = -78 + else: + xval = 60 + x_offs = 78 + is_first = True + test_lives = 1 + while True: + players_with_lives = [ + p for p in team.spawn_order + if p and p.lives >= test_lives + ] + if not players_with_lives: + break + for player in players_with_lives: + player.icons.append( + Icon(player, + position=(xval, (40 if is_first else 25)), + scale=1.0 if is_first else 0.5, + name_maxwidth=130 if is_first else 75, + name_scale=0.8 if is_first else 1.0, + flatness=0.0 if is_first else 1.0, + shadow=0.5 if is_first else 1.0, + show_death=is_first, + show_lives=False)) + xval += x_offs * (0.8 if is_first else 0.56) + is_first = False + test_lives += 1 + # Non-solo mode. + else: + for team in self.teams: + if team.id == 0: + xval = -50 + x_offs = -85 + else: + xval = 50 + x_offs = 85 + for player in team.players: + for icon in player.icons: + icon.set_position_and_scale((xval, 30), 0.7) + icon.update_for_lives() + xval += x_offs + + def _get_spawn_point(self, player: Player) -> Optional[ba.Vec3]: + del player # Unused. + + # In solo-mode, if there's an existing live player on the map, spawn at + # whichever spot is farthest from them (keeps the action spread out). + if self._solo_mode: + living_player = None + living_player_pos = None + for team in self.teams: + for tplayer in team.players: + if tplayer.is_alive(): + assert tplayer.node + ppos = tplayer.node.position + living_player = tplayer + living_player_pos = ppos + break + if living_player: + assert living_player_pos is not None + player_pos = ba.Vec3(living_player_pos) + points: List[Tuple[float, ba.Vec3]] = [] + for team in self.teams: + start_pos = ba.Vec3(self.map.get_start_position(team.id)) + points.append( + ((start_pos - player_pos).length(), start_pos)) + # Hmm.. we need to sorting vectors too? + points.sort(key=lambda x: x[0]) + return points[-1][1] + return None + + def spawn_player(self, player: Player) -> ba.Actor: + actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) + if not self._solo_mode: + ba.timer(0.3, ba.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + return actor + + def _print_lives(self, player: Player) -> None: + from bastd.actor import popuptext + + # We get called in a timer so it's possible our player has left/etc. + if not player or not player.is_alive() or not player.node: + return + + popuptext.PopupText('x' + str(player.lives - 1), + color=(1, 1, 0, 1), + offset=(0, -0.8, 0), + random_offset=0.0, + scale=1.8, + position=player.node.position).autoretain() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + player.icons = [] + + # Remove us from spawn-order. + if self._solo_mode: + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) + + # Update icons in a moment since our team will be gone from the + # list then. + ba.timer(0, self._update_icons) + + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + player: Player = msg.getplayer(Player) + + player.lives -= 1 + if player.lives < 0: + ba.print_error( + "Got lives < 0 in Elim; this shouldn't happen. solo:" + + str(self._solo_mode)) + player.lives = 0 + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_died() + + # Play big death sound on our last death + # or for every one in solo mode. + if self._solo_mode or player.lives == 0: + ba.playsound(SpazFactory.get().single_player_death_sound) + + # If we hit zero lives, we're dead (and our team might be too). + if player.lives == 0: + # If the whole team is now dead, mark their survival time. + if self._get_total_team_lives(player.team) == 0: + assert self._start_time is not None + player.team.survival_seconds = int(ba.time() - + self._start_time) + else: + # Otherwise, in regular mode, respawn. + if not self._solo_mode: + self.respawn_player(player) + + # In solo, put ourself at the back of the spawn order. + if self._solo_mode: + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) + + def _update(self) -> None: + if self._solo_mode: + # For both teams, find the first player on the spawn order + # list with lives remaining and spawn them if they're not alive. + for team in self.teams: + # Prune dead players from the spawn order. + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: + if not player.is_alive(): + self.spawn_player(player) + self._update_icons() + break + + # If we're down to 1 or fewer living teams, start a timer to end + # the game (allows the dust to settle and draws to occur if deaths + # are close enough). + if len(self._get_living_teams()) < 2: + self._round_end_timer = ba.Timer(0.5, self.end_game) + + def _get_living_teams(self) -> List[Team]: + return [ + team for team in self.teams + if len(team.players) > 0 and any(player.lives > 0 + for player in team.players) + ] + + def end_game(self) -> None: + if self.has_ended(): + return + results = ba.GameResults() + self._vs_text = None # Kill our 'vs' if its there. + for team in self.teams: + results.set_team_score(team, team.survival_seconds) + self.end(results=results) diff --git a/dist/ba_data/python/bastd/game/football.py b/dist/ba_data/python/bastd/game/football.py new file mode 100644 index 0000000..3531dfc --- /dev/null +++ b/dist/ba_data/python/bastd/game/football.py @@ -0,0 +1,879 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Implements football games (both co-op and teams varieties).""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +import math + +import ba +from bastd.actor.bomb import TNTSpawner +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.respawnicon import RespawnIcon +from bastd.actor.powerupbox import PowerupBoxFactory, PowerupBox +from bastd.actor.flag import (FlagFactory, Flag, FlagPickedUpMessage, + FlagDroppedMessage, FlagDiedMessage) +from bastd.actor.spazbot import (SpazBotDiedMessage, SpazBotPunchedMessage, + SpazBotSet, BrawlerBotLite, BrawlerBot, + BomberBotLite, BomberBot, TriggerBot, + ChargerBot, TriggerBotPro, BrawlerBotPro, + StickyBot, ExplodeyBot) + +if TYPE_CHECKING: + from typing import Any, List, Type, Dict, Sequence, Optional, Union + from bastd.actor.spaz import Spaz + from bastd.actor.spazbot import SpazBot + + +class FootballFlag(Flag): + """Custom flag class for football games.""" + + def __init__(self, position: Sequence[float]): + super().__init__(position=position, + dropped_timeout=20, + color=(1.0, 1.0, 0.3)) + assert self.node + self.last_holding_player: Optional[ba.Player] = None + self.node.is_area_of_interest = True + self.respawn_timer: Optional[ba.Timer] = None + self.scored = False + self.held_count = 0 + self.light = ba.newnode('light', + owner=self.node, + attrs={ + 'intensity': 0.25, + 'height_attenuated': False, + 'radius': 0.2, + 'color': (0.9, 0.7, 0.0) + }) + self.node.connectattr('position', self.light, 'position') + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.respawn_timer: Optional[ba.Timer] = None + self.respawn_icon: Optional[RespawnIcon] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class FootballTeamGame(ba.TeamGameActivity[Player, Team]): + """Football game for teams mode.""" + + name = 'Football' + description = 'Get the flag to the enemy end zone.' + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=7, + default=21, + increment=7, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + default_music = ba.MusicType.FOOTBALL + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + # We only support two-team play. + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('football') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard: Optional[Scoreboard] = Scoreboard() + + # Load some media we need. + self._cheer_sound = ba.getsound('cheer') + self._chant_sound = ba.getsound('crowdChant') + self._score_sound = ba.getsound('score') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', FlagFactory.get().flagmaterial), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score), + )) + self._flag_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: List[ba.NodeActor] = [] + self._flag: Optional[FootballFlag] = None + self._flag_respawn_timer: Optional[ba.Timer] = None + self._flag_respawn_light: Optional[ba.NodeActor] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + touchdowns = self._score_to_win / 7 + + # NOTE: if use just touchdowns = self._score_to_win // 7 + # and we will need to score, for example, 27 points, + # we will be required to score 3 (not 4) goals .. + touchdowns = math.ceil(touchdowns) + if touchdowns > 1: + return 'Score ${ARG1} touchdowns.', touchdowns + return 'Score a touchdown.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + touchdowns = self._score_to_win / 7 + touchdowns = math.ceil(touchdowns) + if touchdowns > 1: + return 'score ${ARG1} touchdowns', touchdowns + return 'score a touchdown' + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._flag_spawn_pos = (self.map.get_flag_position(None)) + self._spawn_flag() + defs = self.map.defs + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal1'][0:3], + 'scale': defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials': (self._score_region_material, ) + }))) + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal2'][0:3], + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': (self._score_region_material, ) + }))) + self._update_scoreboard() + ba.playsound(self._chant_sound) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _kill_flag(self) -> None: + self._flag = None + + def _handle_score(self) -> None: + """A point has been scored.""" + + # Our flag might stick around for a second or two + # make sure it doesn't score again. + assert self._flag is not None + if self._flag.scored: + return + region = ba.getcollision().sourcenode + i = None + for i in range(len(self._score_regions)): + if region == self._score_regions[i].node: + break + for team in self.teams: + if team.id == i: + team.score += 7 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + # If someone on this team was last to touch it, + # give them points. + assert self._flag is not None + if (self._flag.last_holding_player + and team == self._flag.last_holding_player.team): + self.stats.player_scored(self._flag.last_holding_player, + 50, + big_message=True) + # End the game if we won. + if team.score >= self._score_to_win: + self.end_game() + ba.playsound(self._score_sound) + ba.playsound(self._cheer_sound) + assert self._flag + self._flag.scored = True + + # Kill the flag (it'll respawn shortly). + ba.timer(1.0, self._kill_flag) + light = ba.newnode('light', + attrs={ + 'position': ba.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True) + ba.timer(1.0, light.delete) + ba.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results, announce_delay=0.8) + + def _update_scoreboard(self) -> None: + assert self._scoreboard is not None + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, FlagPickedUpMessage): + assert isinstance(msg.flag, FootballFlag) + try: + msg.flag.last_holding_player = msg.node.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except ba.NotFoundError: + pass + msg.flag.held_count += 1 + + elif isinstance(msg, FlagDroppedMessage): + assert isinstance(msg.flag, FootballFlag) + msg.flag.held_count -= 1 + + # Respawn dead players if they're still in the game. + elif isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead flags. + elif isinstance(msg, FlagDiedMessage): + if not self.has_ended(): + self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag) + self._flag_respawn_light = ba.NodeActor( + ba.newnode('light', + attrs={ + 'position': self._flag_spawn_pos, + 'height_attenuated': False, + 'radius': 0.15, + 'color': (1.0, 1.0, 0.3) + })) + assert self._flag_respawn_light.node + ba.animate(self._flag_respawn_light.node, + 'intensity', { + 0.0: 0, + 0.25: 0.15, + 0.5: 0 + }, + loop=True) + ba.timer(3.0, self._flag_respawn_light.node.delete) + + else: + # Augment standard behavior. + super().handlemessage(msg) + + def _flash_flag_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._flag_spawn_pos, + 'height_attenuated': False, + 'color': (1, 1, 0) + }) + ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _spawn_flag(self) -> None: + ba.playsound(self._swipsound) + ba.playsound(self._whistle_sound) + self._flash_flag_spawn() + assert self._flag_spawn_pos is not None + self._flag = FootballFlag(position=self._flag_spawn_pos) + + +class FootballCoopGame(ba.CoopGameActivity[Player, Team]): + """Co-op variant of football.""" + + name = 'Football' + tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] + scoreconfig = ba.ScoreConfig(scoretype=ba.ScoreType.MILLISECONDS, + version='B') + default_music = ba.MusicType.FOOTBALL + + # FIXME: Need to update co-op games to use getscoreconfig. + def get_score_type(self) -> str: + return 'time' + + def get_instance_description(self) -> Union[str, Sequence]: + touchdowns = self._score_to_win / 7 + touchdowns = math.ceil(touchdowns) + if touchdowns > 1: + return 'Score ${ARG1} touchdowns.', touchdowns + return 'Score a touchdown.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + touchdowns = self._score_to_win / 7 + touchdowns = math.ceil(touchdowns) + if touchdowns > 1: + return 'score ${ARG1} touchdowns', touchdowns + return 'score a touchdown' + + def __init__(self, settings: dict): + settings['map'] = 'Football Stadium' + super().__init__(settings) + self._preset = settings.get('preset', 'rookie') + + # Load some media we need. + self._cheer_sound = ba.getsound('cheer') + self._boo_sound = ba.getsound('boo') + self._chant_sound = ba.getsound('crowdChant') + self._score_sound = ba.getsound('score') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self._score_to_win = 21 + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', FlagFactory.get().flagmaterial), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score), + )) + self._powerup_center = (0, 2, 0) + self._powerup_spread = (10, 5.5) + self._player_has_dropped_bomb = False + self._player_has_punched = False + self._scoreboard: Optional[Scoreboard] = None + self._flag_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: List[ba.NodeActor] = [] + self._exclude_powerups: List[str] = [] + self._have_tnt = False + self._bot_types_initial: Optional[List[Type[SpazBot]]] = None + self._bot_types_7: Optional[List[Type[SpazBot]]] = None + self._bot_types_14: Optional[List[Type[SpazBot]]] = None + self._bot_team: Optional[Team] = None + self._starttime_ms: Optional[int] = None + self._time_text: Optional[ba.NodeActor] = None + self._time_text_input: Optional[ba.NodeActor] = None + self._tntspawner: Optional[TNTSpawner] = None + self._bots = SpazBotSet() + self._bot_spawn_timer: Optional[ba.Timer] = None + self._powerup_drop_timer: Optional[ba.Timer] = None + self._scoring_team: Optional[Team] = None + self._final_time_ms: Optional[int] = None + self._time_text_timer: Optional[ba.Timer] = None + self._flag_respawn_light: Optional[ba.Actor] = None + self._flag: Optional[FootballFlag] = None + + def on_transition_in(self) -> None: + super().on_transition_in() + self._scoreboard = Scoreboard() + self._flag_spawn_pos = self.map.get_flag_position(None) + self._spawn_flag() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal1'][0:3], + 'scale': defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal2'][0:3], + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + ba.playsound(self._chant_sound) + + def on_begin(self) -> None: + # FIXME: Split this up a bit. + # pylint: disable=too-many-statements + from bastd.actor import controlsguide + super().on_begin() + + # Show controls help in kiosk mode. + if ba.app.demo_mode or ba.app.arcade_mode: + controlsguide.ControlsGuide(delay=3.0, lifespan=10.0, + bright=True).autoretain() + assert self.initialplayerinfos is not None + abot: Type[SpazBot] + bbot: Type[SpazBot] + cbot: Type[SpazBot] + if self._preset in ['rookie', 'rookie_easy']: + self._exclude_powerups = ['curse'] + self._have_tnt = False + abot = (BrawlerBotLite + if self._preset == 'rookie_easy' else BrawlerBot) + self._bot_types_initial = [abot] * len(self.initialplayerinfos) + bbot = (BomberBotLite + if self._preset == 'rookie_easy' else BomberBot) + self._bot_types_7 = ( + [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) + cbot = (BomberBot if self._preset == 'rookie_easy' else TriggerBot) + self._bot_types_14 = ( + [cbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) + elif self._preset == 'tournament': + self._exclude_powerups = [] + self._have_tnt = True + self._bot_types_initial = ( + [BrawlerBot] * (1 if len(self.initialplayerinfos) < 2 else 2)) + self._bot_types_7 = ( + [TriggerBot] * (1 if len(self.initialplayerinfos) < 3 else 2)) + self._bot_types_14 = ( + [ChargerBot] * (1 if len(self.initialplayerinfos) < 4 else 2)) + elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: + self._exclude_powerups = ['curse'] + self._have_tnt = True + self._bot_types_initial = [ChargerBot] * len( + self.initialplayerinfos) + abot = (BrawlerBot if self._preset == 'pro' else BrawlerBotLite) + typed_bot_list: List[Type[SpazBot]] = [] + self._bot_types_7 = ( + typed_bot_list + [abot] + [BomberBot] * + (1 if len(self.initialplayerinfos) < 3 else 2)) + bbot = (TriggerBotPro if self._preset == 'pro' else TriggerBot) + self._bot_types_14 = ( + [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) + elif self._preset in ['uber', 'uber_easy']: + self._exclude_powerups = [] + self._have_tnt = True + abot = (BrawlerBotPro if self._preset == 'uber' else BrawlerBot) + bbot = (TriggerBotPro if self._preset == 'uber' else TriggerBot) + typed_bot_list_2: List[Type[SpazBot]] = [] + self._bot_types_initial = (typed_bot_list_2 + [StickyBot] + + [abot] * len(self.initialplayerinfos)) + self._bot_types_7 = ( + [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) + self._bot_types_14 = ( + [ExplodeyBot] * (1 if len(self.initialplayerinfos) < 3 else 2)) + else: + raise Exception() + + self.setup_low_life_warning_sound() + + self._drop_powerups(standard_points=True) + ba.timer(4.0, self._start_powerup_drops) + + # Make a bogus team for our bots. + bad_team_name = self.get_team_display_string('Bad Guys') + self._bot_team = Team() + self._bot_team.manual_init(team_id=1, + name=bad_team_name, + color=(0.5, 0.4, 0.4)) + + for team in [self.teams[0], self._bot_team]: + team.score = 0 + + self.update_scores() + + # Time display. + starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(starttime_ms, int) + self._starttime_ms = starttime_ms + self._time_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -50), + 'scale': 1.3, + 'text': '' + })) + self._time_text_input = ba.NodeActor( + ba.newnode('timedisplay', attrs={'showsubseconds': True})) + self.globalsnode.connectattr('time', self._time_text_input.node, + 'time2') + assert self._time_text_input.node + assert self._time_text.node + self._time_text_input.node.connectattr('output', self._time_text.node, + 'text') + + # Our TNT spawner (if applicable). + if self._have_tnt: + self._tntspawner = TNTSpawner(position=(0, 1, -1)) + + self._bots = SpazBotSet() + self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True) + + for bottype in self._bot_types_initial: + self._spawn_bot(bottype) + + def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: + self._show_standard_scores_to_beat_ui(scores) + + def _on_bot_spawn(self, spaz: SpazBot) -> None: + # We want to move to the left by default. + spaz.target_point_default = ba.Vec3(0, 0, 0) + + def _spawn_bot(self, + spaz_type: Type[SpazBot], + immediate: bool = False) -> None: + assert self._bot_team is not None + pos = self.map.get_start_position(self._bot_team.id) + self._bots.spawn_bot(spaz_type, + pos=pos, + spawn_time=0.001 if immediate else 3.0, + on_spawn_call=self._on_bot_spawn) + + def _update_bots(self) -> None: + bots = self._bots.get_living_bots() + for bot in bots: + bot.target_flag = None + + # If we're waiting on a continue, stop here so they don't keep scoring. + if self.is_waiting_for_continue(): + self._bots.stop_moving() + return + + # If we've got a flag and no player are holding it, find the closest + # bot to it, and make them the designated flag-bearer. + assert self._flag is not None + if self._flag.node: + for player in self.players: + if player.actor: + assert isinstance(player.actor, PlayerSpaz) + if (player.actor.is_alive() and player.actor.node.hold_node + == self._flag.node): + return + + flagpos = ba.Vec3(self._flag.node.position) + closest_bot: Optional[SpazBot] = None + closest_dist = 0.0 # Always gets assigned first time through. + for bot in bots: + # If a bot is picked up, he should forget about the flag. + if bot.held_count > 0: + continue + assert bot.node + botpos = ba.Vec3(bot.node.position) + botdist = (botpos - flagpos).length() + if closest_bot is None or botdist < closest_dist: + closest_bot = bot + closest_dist = botdist + if closest_bot is not None: + closest_bot.target_flag = self._flag + + def _drop_powerup(self, index: int, poweruptype: str = None) -> None: + if poweruptype is None: + poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=self._exclude_powerups)) + PowerupBox(position=self.map.powerup_spawn_points[index], + poweruptype=poweruptype).autoretain() + + def _start_powerup_drops(self) -> None: + self._powerup_drop_timer = ba.Timer(3.0, + self._drop_powerups, + repeat=True) + + def _drop_powerups(self, + standard_points: bool = False, + poweruptype: str = None) -> None: + """Generic powerup drop.""" + if standard_points: + spawnpoints = self.map.powerup_spawn_points + for i, _point in enumerate(spawnpoints): + ba.timer(1.0 + i * 0.5, + ba.Call(self._drop_powerup, i, poweruptype)) + else: + point = (self._powerup_center[0] + random.uniform( + -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), + self._powerup_center[1], + self._powerup_center[2] + random.uniform( + -self._powerup_spread[1], self._powerup_spread[1])) + + # Drop one random one somewhere. + PowerupBox( + position=point, + poweruptype=PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=self._exclude_powerups)).autoretain() + + def _kill_flag(self) -> None: + try: + assert self._flag is not None + self._flag.handlemessage(ba.DieMessage()) + except Exception: + ba.print_exception('Error in _kill_flag.') + + def _handle_score(self) -> None: + """ a point has been scored """ + # FIXME tidy this up + # pylint: disable=too-many-branches + + # Our flag might stick around for a second or two; + # we don't want it to be able to score again. + assert self._flag is not None + if self._flag.scored: + return + + # See which score region it was. + region = ba.getcollision().sourcenode + i = None + for i in range(len(self._score_regions)): + if region == self._score_regions[i].node: + break + + for team in [self.teams[0], self._bot_team]: + assert team is not None + if team.id == i: + team.score += 7 + + # Tell all players (or bots) to celebrate. + if i == 0: + for player in team.players: + if player.actor: + player.actor.handlemessage( + ba.CelebrateMessage(2.0)) + else: + self._bots.celebrate(2.0) + + # If the good guys scored, add more enemies. + if i == 0: + if self.teams[0].score == 7: + assert self._bot_types_7 is not None + for bottype in self._bot_types_7: + self._spawn_bot(bottype) + elif self.teams[0].score == 14: + assert self._bot_types_14 is not None + for bottype in self._bot_types_14: + self._spawn_bot(bottype) + + ba.playsound(self._score_sound) + if i == 0: + ba.playsound(self._cheer_sound) + else: + ba.playsound(self._boo_sound) + + # Kill the flag (it'll respawn shortly). + self._flag.scored = True + + ba.timer(0.2, self._kill_flag) + + self.update_scores() + light = ba.newnode('light', + attrs={ + 'position': ba.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + ba.timer(1.0, light.delete) + if i == 0: + ba.cameraflash(duration=10.0) + + def end_game(self) -> None: + ba.setmusic(None) + self._bots.final_celebrate() + ba.timer(0.001, ba.Call(self.do_end, 'defeat')) + + def on_continue(self) -> None: + # Subtract one touchdown from the bots and get them moving again. + assert self._bot_team is not None + self._bot_team.score -= 7 + self._bots.start_moving() + self.update_scores() + + def update_scores(self) -> None: + """ update scoreboard and check for winners """ + # FIXME: tidy this up + # pylint: disable=too-many-nested-blocks + have_scoring_team = False + win_score = self._score_to_win + for team in [self.teams[0], self._bot_team]: + assert team is not None + assert self._scoreboard is not None + self._scoreboard.set_team_value(team, team.score, win_score) + if team.score >= win_score: + if not have_scoring_team: + self._scoring_team = team + if team is self._bot_team: + self.continue_or_end_game() + else: + ba.setmusic(ba.MusicType.VICTORY) + + # Completion achievements. + assert self._bot_team is not None + if self._preset in ['rookie', 'rookie_easy']: + self._award_achievement('Rookie Football Victory', + sound=False) + if self._bot_team.score == 0: + self._award_achievement( + 'Rookie Football Shutout', sound=False) + elif self._preset in ['pro', 'pro_easy']: + self._award_achievement('Pro Football Victory', + sound=False) + if self._bot_team.score == 0: + self._award_achievement('Pro Football Shutout', + sound=False) + elif self._preset in ['uber', 'uber_easy']: + self._award_achievement('Uber Football Victory', + sound=False) + if self._bot_team.score == 0: + self._award_achievement( + 'Uber Football Shutout', sound=False) + if (not self._player_has_dropped_bomb + and not self._player_has_punched): + self._award_achievement('Got the Moves', + sound=False) + self._bots.stop_moving() + self.show_zoom_message(ba.Lstr(resource='victoryText'), + scale=1.0, + duration=4.0) + self.celebrate(10.0) + assert self._starttime_ms is not None + self._final_time_ms = int( + ba.time(timeformat=ba.TimeFormat.MILLISECONDS) - + self._starttime_ms) + self._time_text_timer = None + assert (self._time_text_input is not None + and self._time_text_input.node) + self._time_text_input.node.timemax = ( + self._final_time_ms) + + # FIXME: Does this still need to be deferred? + ba.pushcall(ba.Call(self.do_end, 'victory')) + + def do_end(self, outcome: str) -> None: + """End the game with the specified outcome.""" + if outcome == 'defeat': + self.fade_to_red() + assert self._final_time_ms is not None + scoreval = (None if outcome == 'defeat' else int(self._final_time_ms // + 10)) + self.end(delay=3.0, + results={ + 'outcome': outcome, + 'score': scoreval, + 'score_order': 'decreasing', + 'playerinfos': self.initialplayerinfos + }) + + def handlemessage(self, msg: Any) -> Any: + """ handle high-level game messages """ + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + + # Respawn them shortly. + player = msg.getplayer(Player) + assert self.initialplayerinfos is not None + respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 + player.respawn_timer = ba.Timer( + respawn_time, ba.Call(self.spawn_player_if_exists, player)) + player.respawn_icon = RespawnIcon(player, respawn_time) + + elif isinstance(msg, SpazBotDiedMessage): + + # Every time a bad guy dies, spawn a new one. + ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot)))) + + elif isinstance(msg, SpazBotPunchedMessage): + if self._preset in ['rookie', 'rookie_easy']: + if msg.damage >= 500: + self._award_achievement('Super Punch') + elif self._preset in ['pro', 'pro_easy']: + if msg.damage >= 1000: + self._award_achievement('Super Mega Punch') + + # Respawn dead flags. + elif isinstance(msg, FlagDiedMessage): + assert isinstance(msg.flag, FootballFlag) + msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag) + self._flag_respawn_light = ba.NodeActor( + ba.newnode('light', + attrs={ + 'position': self._flag_spawn_pos, + 'height_attenuated': False, + 'radius': 0.15, + 'color': (1.0, 1.0, 0.3) + })) + assert self._flag_respawn_light.node + ba.animate(self._flag_respawn_light.node, + 'intensity', { + 0: 0, + 0.25: 0.15, + 0.5: 0 + }, + loop=True) + ba.timer(3.0, self._flag_respawn_light.node.delete) + else: + return super().handlemessage(msg) + return None + + def _handle_player_dropped_bomb(self, player: Spaz, + bomb: ba.Actor) -> None: + del player, bomb # Unused. + self._player_has_dropped_bomb = True + + def _handle_player_punched(self, player: Spaz) -> None: + del player # Unused. + self._player_has_punched = True + + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player, + position=self.map.get_start_position( + player.team.id)) + if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: + spaz.impact_scale = 0.25 + spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) + spaz.punch_callback = self._handle_player_punched + return spaz + + def _flash_flag_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._flag_spawn_pos, + 'height_attenuated': False, + 'color': (1, 1, 0) + }) + ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _spawn_flag(self) -> None: + ba.playsound(self._swipsound) + ba.playsound(self._whistle_sound) + self._flash_flag_spawn() + assert self._flag_spawn_pos is not None + self._flag = FootballFlag(position=self._flag_spawn_pos) diff --git a/dist/ba_data/python/bastd/game/hockey.py b/dist/ba_data/python/bastd/game/hockey.py new file mode 100644 index 0000000..3964ad7 --- /dev/null +++ b/dist/ba_data/python/bastd/game/hockey.py @@ -0,0 +1,368 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Hockey game and support classes.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(ba.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, HockeyGame) + pmats = [shared.object_material, activity.puck_material] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': activity.puck_model, + 'color_texture': activity.puck_tex, + 'body': 'puck', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 1.0, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + ba.animate(self.node, 'model_scale', {0: 0, 0.2: 1.3, 0.26: 1}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, ba.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, ba.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class HockeyGame(ba.TeamGameActivity[Player, Team]): + """Ice hockey game.""" + + name = 'Hockey' + description = 'Score some goals.' + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + default_music = ba.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('hockey') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = ba.getsound('cheer') + self._chant_sound = ba.getsound('crowdChant') + self._foghorn_sound = ba.getsound('foghorn') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self.puck_model = ba.getmodel('puck') + self.puck_tex = ba.gettexture('puckColor') + self._puck_sound = ba.getsound('metalHit') + self.puck_material = ba.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', False)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.puck_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', ba.DieMessage()))) + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[ba.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal1'][0:3], + 'scale': defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal2'][0:3], + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + ba.playsound(self._chant_sound) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = ba.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = ba.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 100, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + ba.playsound(self._foghorn_sound) + ba.playsound(self._cheer_sound) + + self._puck.scored = True + + # Kill the puck (it'll respawn itself shortly). + ba.timer(1.0, self._kill_puck) + + light = ba.newnode('light', + attrs={ + 'position': ba.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + ba.timer(1.0, light.delete) + + ba.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + ba.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._puck_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _spawn_puck(self) -> None: + ba.playsound(self._swipsound) + ba.playsound(self._whistle_sound) + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) diff --git a/dist/ba_data/python/bastd/game/keepaway.py b/dist/ba_data/python/bastd/game/keepaway.py new file mode 100644 index 0000000..7810b2e --- /dev/null +++ b/dist/ba_data/python/bastd/game/keepaway.py @@ -0,0 +1,268 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines a keep-away game type.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.flag import (Flag, FlagDroppedMessage, FlagDiedMessage, + FlagPickedUpMessage) + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Optional, Sequence, Union + + +class FlagState(Enum): + """States our single flag can be in.""" + NEW = 0 + UNCONTESTED = 1 + CONTESTED = 2 + HELD = 3 + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self, timeremaining: int) -> None: + self.timeremaining = timeremaining + self.holdingflag = False + + +# ba_meta export game +class KeepAwayGame(ba.TeamGameActivity[Player, Team]): + """Game where you try to keep the flag away from your enemies.""" + + name = 'Keep Away' + description = 'Carry the flag for a set length of time.' + available_settings = [ + ba.IntSetting( + 'Hold Time', + min_value=10, + default=30, + increment=10, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + scoreconfig = ba.ScoreConfig(label='Time Held') + default_music = ba.MusicType.KEEP_AWAY + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('keep_away') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._swipsound = ba.getsound('swip') + self._tick_sound = ba.getsound('tick') + self._countdownsounds = { + 10: ba.getsound('announceTen'), + 9: ba.getsound('announceNine'), + 8: ba.getsound('announceEight'), + 7: ba.getsound('announceSeven'), + 6: ba.getsound('announceSix'), + 5: ba.getsound('announceFive'), + 4: ba.getsound('announceFour'), + 3: ba.getsound('announceThree'), + 2: ba.getsound('announceTwo'), + 1: ba.getsound('announceOne') + } + self._flag_spawn_pos: Optional[Sequence[float]] = None + self._update_timer: Optional[ba.Timer] = None + self._holding_players: List[Player] = [] + self._flag_state: Optional[FlagState] = None + self._flag_light: Optional[ba.Node] = None + self._scoring_team: Optional[Team] = None + self._flag: Optional[Flag] = None + self._hold_time = int(settings['Hold Time']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Carry the flag for ${ARG1} seconds.', self._hold_time + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'carry the flag for ${ARG1} seconds', self._hold_time + + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + return Team(timeremaining=self._hold_time) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._flag_spawn_pos = self.map.get_flag_position(None) + self._spawn_flag() + self._update_timer = ba.Timer(1.0, call=self._tick, repeat=True) + self._update_flag_state() + Flag.project_stand(self._flag_spawn_pos) + + def _tick(self) -> None: + self._update_flag_state() + + # Award points to all living players holding the flag. + for player in self._holding_players: + if player: + assert self.stats + self.stats.player_scored(player, + 3, + screenmessage=False, + display=False) + + scoreteam = self._scoring_team + + if scoreteam is not None: + + if scoreteam.timeremaining > 0: + ba.playsound(self._tick_sound) + + scoreteam.timeremaining = max(0, scoreteam.timeremaining - 1) + self._update_scoreboard() + if scoreteam.timeremaining > 0: + assert self._flag is not None + self._flag.set_score_text(str(scoreteam.timeremaining)) + + # Announce numbers we have sounds for. + if scoreteam.timeremaining in self._countdownsounds: + ba.playsound(self._countdownsounds[scoreteam.timeremaining]) + + # Winner. + if scoreteam.timeremaining <= 0: + self.end_game() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, self._hold_time - team.timeremaining) + self.end(results=results, announce_delay=0) + + def _update_flag_state(self) -> None: + for team in self.teams: + team.holdingflag = False + self._holding_players = [] + for player in self.players: + holdingflag = False + try: + assert isinstance(player.actor, (PlayerSpaz, type(None))) + if (player.actor and player.actor.node + and player.actor.node.hold_node): + holdingflag = ( + player.actor.node.hold_node.getnodetype() == 'flag') + except Exception: + ba.print_exception('Error checking hold flag.') + if holdingflag: + self._holding_players.append(player) + player.team.holdingflag = True + + holdingteams = set(t for t in self.teams if t.holdingflag) + prevstate = self._flag_state + assert self._flag is not None + assert self._flag_light + assert self._flag.node + if len(holdingteams) > 1: + self._flag_state = FlagState.CONTESTED + self._scoring_team = None + self._flag_light.color = (0.6, 0.6, 0.1) + self._flag.node.color = (1.0, 1.0, 0.4) + elif len(holdingteams) == 1: + holdingteam = list(holdingteams)[0] + self._flag_state = FlagState.HELD + self._scoring_team = holdingteam + self._flag_light.color = ba.normalized_color(holdingteam.color) + self._flag.node.color = holdingteam.color + else: + self._flag_state = FlagState.UNCONTESTED + self._scoring_team = None + self._flag_light.color = (0.2, 0.2, 0.2) + self._flag.node.color = (1, 1, 1) + + if self._flag_state != prevstate: + ba.playsound(self._swipsound) + + def _spawn_flag(self) -> None: + ba.playsound(self._swipsound) + self._flash_flag_spawn() + assert self._flag_spawn_pos is not None + self._flag = Flag(dropped_timeout=20, position=self._flag_spawn_pos) + self._flag_state = FlagState.NEW + self._flag_light = ba.newnode('light', + owner=self._flag.node, + attrs={ + 'intensity': 0.2, + 'radius': 0.3, + 'color': (0.2, 0.2, 0.2) + }) + assert self._flag.node + self._flag.node.connectattr('position', self._flag_light, 'position') + self._update_flag_state() + + def _flash_flag_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._flag_spawn_pos, + 'color': (1, 1, 1), + 'radius': 0.3, + 'height_attenuated': False + }) + ba.animate(light, 'intensity', {0.0: 0, 0.25: 0.5, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, + team.timeremaining, + self._hold_time, + countdown=True) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + elif isinstance(msg, FlagDiedMessage): + self._spawn_flag() + elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)): + self._update_flag_state() + else: + super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/game/kingofthehill.py b/dist/ba_data/python/bastd/game/kingofthehill.py new file mode 100644 index 0000000..6672626 --- /dev/null +++ b/dist/ba_data/python/bastd/game/kingofthehill.py @@ -0,0 +1,273 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the King of the Hill game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import weakref +from enum import Enum +from typing import TYPE_CHECKING + +import ba +from bastd.actor.flag import Flag +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from weakref import ReferenceType + from typing import Any, Type, List, Dict, Optional, Sequence, Union + + +class FlagState(Enum): + """States our single flag can be in.""" + NEW = 0 + UNCONTESTED = 1 + CONTESTED = 2 + HELD = 3 + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.time_at_flag = 0 + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self, time_remaining: int) -> None: + self.time_remaining = time_remaining + + +# ba_meta export game +class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): + """Game where a team wins by holding a 'hill' for a set amount of time.""" + + name = 'King of the Hill' + description = 'Secure the flag for a set length of time.' + available_settings = [ + ba.IntSetting( + 'Hold Time', + min_value=10, + default=30, + increment=10, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + scoreconfig = ba.ScoreConfig(label='Time Held') + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('king_of_the_hill') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._swipsound = ba.getsound('swip') + self._tick_sound = ba.getsound('tick') + self._countdownsounds = { + 10: ba.getsound('announceTen'), + 9: ba.getsound('announceNine'), + 8: ba.getsound('announceEight'), + 7: ba.getsound('announceSeven'), + 6: ba.getsound('announceSix'), + 5: ba.getsound('announceFive'), + 4: ba.getsound('announceFour'), + 3: ba.getsound('announceThree'), + 2: ba.getsound('announceTwo'), + 1: ba.getsound('announceOne') + } + self._flag_pos: Optional[Sequence[float]] = None + self._flag_state: Optional[FlagState] = None + self._flag: Optional[Flag] = None + self._flag_light: Optional[ba.Node] = None + self._scoring_team: Optional[ReferenceType[Team]] = None + self._hold_time = int(settings['Hold Time']) + self._time_limit = float(settings['Time Limit']) + self._flag_region_material = ba.Material() + self._flag_region_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + ba.Call(self._handle_player_flag_region_collide, True)), + ('call', 'at_disconnect', + ba.Call(self._handle_player_flag_region_collide, False)), + )) + + # Base class overrides. + self.default_music = ba.MusicType.SCARY + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Secure the flag for ${ARG1} seconds.', self._hold_time + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'secure the flag for ${ARG1} seconds', self._hold_time + + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + return Team(time_remaining=self._hold_time) + + def on_begin(self) -> None: + super().on_begin() + shared = SharedObjects.get() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._flag_pos = self.map.get_flag_position(None) + ba.timer(1.0, self._tick, repeat=True) + self._flag_state = FlagState.NEW + Flag.project_stand(self._flag_pos) + self._flag = Flag(position=self._flag_pos, + touchable=False, + color=(1, 1, 1)) + self._flag_light = ba.newnode('light', + attrs={ + 'position': self._flag_pos, + 'intensity': 0.2, + 'height_attenuated': False, + 'radius': 0.4, + 'color': (0.2, 0.2, 0.2) + }) + # Flag region. + flagmats = [self._flag_region_material, shared.region_material] + ba.newnode('region', + attrs={ + 'position': self._flag_pos, + 'scale': (1.8, 1.8, 1.8), + 'type': 'sphere', + 'materials': flagmats + }) + self._update_flag_state() + + def _tick(self) -> None: + self._update_flag_state() + + # Give holding players points. + for player in self.players: + if player.time_at_flag > 0: + self.stats.player_scored(player, + 3, + screenmessage=False, + display=False) + if self._scoring_team is None: + scoring_team = None + else: + scoring_team = self._scoring_team() + if scoring_team: + + if scoring_team.time_remaining > 0: + ba.playsound(self._tick_sound) + + scoring_team.time_remaining = max(0, + scoring_team.time_remaining - 1) + self._update_scoreboard() + if scoring_team.time_remaining > 0: + assert self._flag is not None + self._flag.set_score_text(str(scoring_team.time_remaining)) + + # Announce numbers we have sounds for. + numsound = self._countdownsounds.get(scoring_team.time_remaining) + if numsound is not None: + ba.playsound(numsound) + + # winner + if scoring_team.time_remaining <= 0: + self.end_game() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, self._hold_time - team.time_remaining) + self.end(results=results, announce_delay=0) + + def _update_flag_state(self) -> None: + holding_teams = set(player.team for player in self.players + if player.time_at_flag) + prev_state = self._flag_state + assert self._flag_light + assert self._flag is not None + assert self._flag.node + if len(holding_teams) > 1: + self._flag_state = FlagState.CONTESTED + self._scoring_team = None + self._flag_light.color = (0.6, 0.6, 0.1) + self._flag.node.color = (1.0, 1.0, 0.4) + elif len(holding_teams) == 1: + holding_team = list(holding_teams)[0] + self._flag_state = FlagState.HELD + self._scoring_team = weakref.ref(holding_team) + self._flag_light.color = ba.normalized_color(holding_team.color) + self._flag.node.color = holding_team.color + else: + self._flag_state = FlagState.UNCONTESTED + self._scoring_team = None + self._flag_light.color = (0.2, 0.2, 0.2) + self._flag.node.color = (1, 1, 1) + if self._flag_state != prev_state: + ba.playsound(self._swipsound) + + def _handle_player_flag_region_collide(self, colliding: bool) -> None: + try: + player = ba.getcollision().opposingnode.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except ba.NotFoundError: + return + + # Different parts of us can collide so a single value isn't enough + # also don't count it if we're dead (flying heads shouldn't be able to + # win the game :-) + if colliding and player.is_alive(): + player.time_at_flag += 1 + else: + player.time_at_flag = max(0, player.time_at_flag - 1) + + self._update_flag_state() + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, + team.time_remaining, + self._hold_time, + countdown=True) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) # Augment default. + + # No longer can count as time_at_flag once dead. + player = msg.getplayer(Player) + player.time_at_flag = 0 + self._update_flag_state() + self.respawn_player(player) diff --git a/dist/ba_data/python/bastd/game/meteorshower.py b/dist/ba_data/python/bastd/game/meteorshower.py new file mode 100644 index 0000000..9be7990 --- /dev/null +++ b/dist/ba_data/python/bastd/game/meteorshower.py @@ -0,0 +1,267 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines a bomb-dodging mini-game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.actor.bomb import Bomb +from bastd.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Sequence, Optional, List, Dict, Type, Type + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + super().__init__() + self.death_time: Optional[float] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export game +class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): + """Minigame involving dodging falling bombs.""" + + name = 'Meteor Shower' + description = 'Dodge the falling bombs.' + available_settings = [ba.BoolSetting('Epic Mode', default=False)] + scoreconfig = ba.ScoreConfig(label='Survived', + scoretype=ba.ScoreType.MILLISECONDS, + version='B') + + # Print messages when players die (since its meaningful in this game). + announce_player_deaths = True + + # we're currently hard-coded for one map.. + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Rampage'] + + # We support teams, free-for-all, and co-op sessions. + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession) + or issubclass(sessiontype, ba.CoopSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + + self._epic_mode = settings.get('Epic Mode', False) + self._last_player_death_time: Optional[float] = None + self._meteor_time = 2.0 + self._timer: Optional[OnScreenTimer] = None + + # Some base class overrides: + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) + if self._epic_mode: + self.slow_motion = True + + def on_begin(self) -> None: + super().on_begin() + + # Drop a wave every few seconds.. and every so often drop the time + # between waves ..lets have things increase faster if we have fewer + # players. + delay = 5.0 if len(self.players) > 2 else 2.5 + if self._epic_mode: + delay *= 0.25 + ba.timer(delay, self._decrement_meteor_time, repeat=True) + + # Kick off the first wave in a few seconds. + delay = 3.0 + if self._epic_mode: + delay *= 0.25 + ba.timer(delay, self._set_meteor_timer) + + self._timer = OnScreenTimer() + self._timer.start() + + # Check for immediate end (if we've only got 1 player, etc). + ba.timer(5.0, self._check_end_game) + + def on_player_join(self, player: Player) -> None: + # Don't allow joining after we start + # (would enable leave/rejoin tomfoolery). + if self.has_begun(): + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0), + ) + # For score purposes, mark them as having died right as the + # game started. + assert self._timer is not None + player.death_time = self._timer.getstarttime() + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + # Augment default behavior. + super().on_player_leave(player) + + # A departing player may trigger game-over. + self._check_end_game() + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + + # Let's reconnect this player's controls to this + # spaz but *without* the ability to attack or pick stuff up. + spaz.connect_controls_to_player(enable_punch=False, + enable_bomb=False, + enable_pickup=False) + + # Also lets have them make some noise when they die. + spaz.play_big_death_sound = True + return spaz + + # Various high-level game events come through this method. + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + curtime = ba.time() + + # Record the player's moment of death. + # assert isinstance(msg.spaz.player + msg.getplayer(Player).death_time = curtime + + # In co-op mode, end the game the instant everyone dies + # (more accurate looking). + # In teams/ffa, allow a one-second fudge-factor so we can + # get more draws if players die basically at the same time. + if isinstance(self.session, ba.CoopSession): + # Teams will still show up if we check now.. check in + # the next cycle. + ba.pushcall(self._check_end_game) + + # Also record this for a final setting of the clock. + self._last_player_death_time = curtime + else: + ba.timer(1.0, self._check_end_game) + + else: + # Default handler: + return super().handlemessage(msg) + return None + + def _check_end_game(self) -> None: + living_team_count = 0 + for team in self.teams: + for player in team.players: + if player.is_alive(): + living_team_count += 1 + break + + # In co-op, we go till everyone is dead.. otherwise we go + # until one team remains. + if isinstance(self.session, ba.CoopSession): + if living_team_count <= 0: + self.end_game() + else: + if living_team_count <= 1: + self.end_game() + + def _set_meteor_timer(self) -> None: + ba.timer((1.0 + 0.2 * random.random()) * self._meteor_time, + self._drop_bomb_cluster) + + def _drop_bomb_cluster(self) -> None: + + # Random note: code like this is a handy way to plot out extents + # and debug things. + loc_test = False + if loc_test: + ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) + ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) + ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) + ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) + + # Drop several bombs in series. + delay = 0.0 + for _i in range(random.randrange(1, 3)): + # Drop them somewhere within our bounds with velocity pointing + # toward the opposite side. + pos = (-7.3 + 15.3 * random.random(), 11, + -5.5 + 2.1 * random.random()) + dropdir = (-1.0 if pos[0] > 0 else 1.0) + vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0) + ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) + delay += 0.1 + self._set_meteor_timer() + + def _drop_bomb(self, position: Sequence[float], + velocity: Sequence[float]) -> None: + Bomb(position=position, velocity=velocity).autoretain() + + def _decrement_meteor_time(self) -> None: + self._meteor_time = max(0.01, self._meteor_time * 0.9) + + def end_game(self) -> None: + cur_time = ba.time() + assert self._timer is not None + start_time = self._timer.getstarttime() + + # Mark death-time as now for any still-living players + # and award players points for how long they lasted. + # (these per-player scores are only meaningful in team-games) + for team in self.teams: + for player in team.players: + survived = False + + # Throw an extra fudge factor in so teams that + # didn't die come out ahead of teams that did. + if player.death_time is None: + survived = True + player.death_time = cur_time + 1 + + # Award a per-player score depending on how many seconds + # they lasted (per-player scores only affect teams mode; + # everywhere else just looks at the per-team score). + score = int(player.death_time - self._timer.getstarttime()) + if survived: + score += 50 # A bit extra for survivors. + self.stats.player_scored(player, score, screenmessage=False) + + # Stop updating our time text, and set the final time to match + # exactly when our last guy died. + self._timer.stop(endtime=self._last_player_death_time) + + # Ok now calc game results: set a score for each team and then tell + # the game to end. + results = ba.GameResults() + + # Remember that 'free-for-all' mode is simply a special form + # of 'teams' mode where each player gets their own team, so we can + # just always deal in teams and have all cases covered. + for team in self.teams: + + # Set the team score to the max time survived by any player on + # that team. + longest_life = 0.0 + for player in team.players: + assert player.death_time is not None + longest_life = max(longest_life, + player.death_time - start_time) + + # Submit the score value in milliseconds. + results.set_team_score(team, int(1000.0 * longest_life)) + + self.end(results=results) diff --git a/dist/ba_data/python/bastd/game/ninjafight.py b/dist/ba_data/python/bastd/game/ninjafight.py new file mode 100644 index 0000000..37ba021 --- /dev/null +++ b/dist/ba_data/python/bastd/game/ninjafight.py @@ -0,0 +1,169 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides Ninja Fight mini-game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.actor.spazbot import SpazBotSet, ChargerBot, SpazBotDiedMessage +from bastd.actor.onscreentimer import OnScreenTimer + +if TYPE_CHECKING: + from typing import Any, Type, Dict, List, Optional + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +# ba_meta export game +class NinjaFightGame(ba.TeamGameActivity[Player, Team]): + """ + A co-op game where you try to defeat a group + of Ninjas as fast as possible + """ + + name = 'Ninja Fight' + description = 'How fast can you defeat the ninjas?' + scoreconfig = ba.ScoreConfig(label='Time', + scoretype=ba.ScoreType.MILLISECONDS, + lower_is_better=True) + default_music = ba.MusicType.TO_THE_DEATH + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + # For now we're hard-coding spawn positions and whatnot + # so we need to be sure to specify that we only support + # a specific map. + return ['Courtyard'] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + # We currently support Co-Op only. + return issubclass(sessiontype, ba.CoopSession) + + # In the constructor we should load any media we need/etc. + # ...but not actually create anything yet. + def __init__(self, settings: dict): + super().__init__(settings) + self._winsound = ba.getsound('score') + self._won = False + self._timer: Optional[OnScreenTimer] = None + self._bots = SpazBotSet() + self._preset = str(settings['preset']) + + # Called when our game actually begins. + def on_begin(self) -> None: + super().on_begin() + is_pro = self._preset == 'pro' + + # In pro mode there's no powerups. + if not is_pro: + self.setup_standard_powerup_drops() + + # Make our on-screen timer and start it roughly when our bots appear. + self._timer = OnScreenTimer() + ba.timer(4.0, self._timer.start) + + # Spawn some baddies. + ba.timer( + 1.0, lambda: self._bots.spawn_bot( + ChargerBot, pos=(3, 3, -2), spawn_time=3.0)) + ba.timer( + 2.0, lambda: self._bots.spawn_bot( + ChargerBot, pos=(-3, 3, -2), spawn_time=3.0)) + ba.timer( + 3.0, lambda: self._bots.spawn_bot( + ChargerBot, pos=(5, 3, -2), spawn_time=3.0)) + ba.timer( + 4.0, lambda: self._bots.spawn_bot( + ChargerBot, pos=(-5, 3, -2), spawn_time=3.0)) + + # Add some extras for multiplayer or pro mode. + assert self.initialplayerinfos is not None + if len(self.initialplayerinfos) > 2 or is_pro: + ba.timer( + 5.0, lambda: self._bots.spawn_bot( + ChargerBot, pos=(0, 3, -5), spawn_time=3.0)) + if len(self.initialplayerinfos) > 3 or is_pro: + ba.timer( + 6.0, lambda: self._bots.spawn_bot( + ChargerBot, pos=(0, 3, 1), spawn_time=3.0)) + + # Called for each spawning player. + def spawn_player(self, player: Player) -> ba.Actor: + + # Let's spawn close to the center. + spawn_center = (0, 3, -2) + pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], + spawn_center[2] + random.uniform(-1.5, 1.5)) + return self.spawn_player_spaz(player, position=pos) + + def _check_if_won(self) -> None: + # Simply end the game if there's no living bots. + # FIXME: Should also make sure all bots have been spawned; + # if spawning is spread out enough that we're able to kill + # all living bots before the next spawns, it would incorrectly + # count as a win. + if not self._bots.have_living_bots(): + self._won = True + self.end_game() + + # Called for miscellaneous messages. + def handlemessage(self, msg: Any) -> Any: + + # A player has died. + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + self.respawn_player(msg.getplayer(Player)) + + # A spaz-bot has died. + elif isinstance(msg, SpazBotDiedMessage): + # Unfortunately the bot-set will always tell us there are living + # bots if we ask here (the currently-dying bot isn't officially + # marked dead yet) ..so lets push a call into the event loop to + # check once this guy has finished dying. + ba.pushcall(self._check_if_won) + + # Let the base class handle anything we don't. + else: + return super().handlemessage(msg) + return None + + # When this is called, we should fill out results and end the game + # *regardless* of whether is has been won. (this may be called due + # to a tournament ending or other external reason). + def end_game(self) -> None: + + # Stop our on-screen timer so players can see what they got. + assert self._timer is not None + self._timer.stop() + + results = ba.GameResults() + + # If we won, set our score to the elapsed time in milliseconds. + # (there should just be 1 team here since this is co-op). + # ..if we didn't win, leave scores as default (None) which means + # we lost. + if self._won: + elapsed_time_ms = int((ba.time() - self._timer.starttime) * 1000.0) + ba.cameraflash() + ba.playsound(self._winsound) + for team in self.teams: + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage()) + results.set_team_score(team, elapsed_time_ms) + + # Ends the activity. + self.end(results) diff --git a/dist/ba_data/python/bastd/game/onslaught.py b/dist/ba_data/python/bastd/game/onslaught.py new file mode 100644 index 0000000..9b8efe8 --- /dev/null +++ b/dist/ba_data/python/bastd/game/onslaught.py @@ -0,0 +1,1311 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides Onslaught Co-op game.""" + +# Yes this is a long one.. +# pylint: disable=too-many-lines + +from __future__ import annotations + +import math +import random +from enum import Enum, unique +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import ba +from bastd.actor.popuptext import PopupText +from bastd.actor.bomb import TNTSpawner +from bastd.actor.playerspaz import PlayerSpazHurtMessage +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.controlsguide import ControlsGuide +from bastd.actor.powerupbox import PowerupBox, PowerupBoxFactory +from bastd.actor.spazbot import ( + SpazBotDiedMessage, SpazBotSet, ChargerBot, StickyBot, BomberBot, + BomberBotLite, BrawlerBot, BrawlerBotLite, TriggerBot, BomberBotStaticLite, + TriggerBotStatic, BomberBotProStatic, TriggerBotPro, ExplodeyBot, + BrawlerBotProShielded, ChargerBotProShielded, BomberBotPro, + TriggerBotProShielded, BrawlerBotPro, BomberBotProShielded) + +if TYPE_CHECKING: + from typing import Any, Type, Dict, Optional, List, Tuple, Union, Sequence + from bastd.actor.spazbot import SpazBot + + +@dataclass +class Wave: + """A wave of enemies.""" + entries: List[Union[Spawn, Spacing, Delay, None]] + base_angle: float = 0.0 + + +@dataclass +class Spawn: + """A bot spawn event in a wave.""" + bottype: Union[Type[SpazBot], str] + point: Optional[Point] = None + spacing: float = 5.0 + + +@dataclass +class Spacing: + """Empty space in a wave.""" + spacing: float = 5.0 + + +@dataclass +class Delay: + """A delay between events in a wave.""" + duration: float + + +class Preset(Enum): + """Game presets we support.""" + TRAINING = 'training' + TRAINING_EASY = 'training_easy' + ROOKIE = 'rookie' + ROOKIE_EASY = 'rookie_easy' + PRO = 'pro' + PRO_EASY = 'pro_easy' + UBER = 'uber' + UBER_EASY = 'uber_easy' + ENDLESS = 'endless' + ENDLESS_TOURNAMENT = 'endless_tournament' + + +@unique +class Point(Enum): + """Points on the map we can spawn at.""" + LEFT_UPPER_MORE = 'bot_spawn_left_upper_more' + LEFT_UPPER = 'bot_spawn_left_upper' + TURRET_TOP_RIGHT = 'bot_spawn_turret_top_right' + RIGHT_UPPER = 'bot_spawn_right_upper' + TURRET_TOP_MIDDLE_LEFT = 'bot_spawn_turret_top_middle_left' + TURRET_TOP_MIDDLE_RIGHT = 'bot_spawn_turret_top_middle_right' + TURRET_TOP_LEFT = 'bot_spawn_turret_top_left' + TOP_RIGHT = 'bot_spawn_top_right' + TOP_LEFT = 'bot_spawn_top_left' + TOP = 'bot_spawn_top' + BOTTOM = 'bot_spawn_bottom' + LEFT = 'bot_spawn_left' + RIGHT = 'bot_spawn_right' + RIGHT_UPPER_MORE = 'bot_spawn_right_upper_more' + RIGHT_LOWER = 'bot_spawn_right_lower' + RIGHT_LOWER_MORE = 'bot_spawn_right_lower_more' + BOTTOM_RIGHT = 'bot_spawn_bottom_right' + BOTTOM_LEFT = 'bot_spawn_bottom_left' + TURRET_BOTTOM_RIGHT = 'bot_spawn_turret_bottom_right' + TURRET_BOTTOM_LEFT = 'bot_spawn_turret_bottom_left' + LEFT_LOWER = 'bot_spawn_left_lower' + LEFT_LOWER_MORE = 'bot_spawn_left_lower_more' + TURRET_TOP_MIDDLE = 'bot_spawn_turret_top_middle' + BOTTOM_HALF_RIGHT = 'bot_spawn_bottom_half_right' + BOTTOM_HALF_LEFT = 'bot_spawn_bottom_half_left' + TOP_HALF_RIGHT = 'bot_spawn_top_half_right' + TOP_HALF_LEFT = 'bot_spawn_top_half_left' + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.has_been_hurt = False + self.respawn_wave = 0 + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +class OnslaughtGame(ba.CoopGameActivity[Player, Team]): + """Co-op game where players try to survive attacking waves of enemies.""" + + name = 'Onslaught' + description = 'Defeat all enemies.' + + tips: List[Union[str, ba.GameTip]] = [ + 'Hold any button to run.' + ' (Trigger buttons work well if you have them)', + 'Try tricking enemies into killing eachother or running off cliffs.', + 'Try \'Cooking off\' bombs for a second or two before throwing them.', + 'It\'s easier to win with a friend or two helping.', + 'If you stay in one place, you\'re toast. Run and dodge to survive..', + 'Practice using your momentum to throw bombs more accurately.', + 'Your punches do much more damage if you are running or spinning.' + ] + + # Show messages when players die since it matters here. + announce_player_deaths = True + + def __init__(self, settings: dict): + + self._preset = Preset(settings.get('preset', 'training')) + if self._preset in { + Preset.TRAINING, Preset.TRAINING_EASY, Preset.PRO, + Preset.PRO_EASY, Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT + }: + settings['map'] = 'Doom Shroom' + else: + settings['map'] = 'Courtyard' + + super().__init__(settings) + + self._new_wave_sound = ba.getsound('scoreHit01') + self._winsound = ba.getsound('score') + self._cashregistersound = ba.getsound('cashRegister') + self._a_player_has_been_hurt = False + self._player_has_dropped_bomb = False + + # FIXME: should use standard map defs. + if settings['map'] == 'Doom Shroom': + self._spawn_center = (0, 3, -5) + self._tntspawnpos = (0.0, 3.0, -5.0) + self._powerup_center = (0, 5, -3.6) + self._powerup_spread = (6.0, 4.0) + elif settings['map'] == 'Courtyard': + self._spawn_center = (0, 3, -2) + self._tntspawnpos = (0.0, 3.0, 2.1) + self._powerup_center = (0, 5, -1.6) + self._powerup_spread = (4.6, 2.7) + else: + raise Exception('Unsupported map: ' + str(settings['map'])) + self._scoreboard: Optional[Scoreboard] = None + self._game_over = False + self._wavenum = 0 + self._can_end_wave = True + self._score = 0 + self._time_bonus = 0 + self._spawn_info_text: Optional[ba.NodeActor] = None + self._dingsound = ba.getsound('dingSmall') + self._dingsoundhigh = ba.getsound('dingSmallHigh') + self._have_tnt = False + self._excluded_powerups: Optional[List[str]] = None + self._waves: List[Wave] = [] + self._tntspawner: Optional[TNTSpawner] = None + self._bots: Optional[SpazBotSet] = None + self._powerup_drop_timer: Optional[ba.Timer] = None + self._time_bonus_timer: Optional[ba.Timer] = None + self._time_bonus_text: Optional[ba.NodeActor] = None + self._flawless_bonus: Optional[int] = None + self._wave_text: Optional[ba.NodeActor] = None + self._wave_update_timer: Optional[ba.Timer] = None + self._throw_off_kills = 0 + self._land_mine_kills = 0 + self._tnt_kills = 0 + + def on_transition_in(self) -> None: + super().on_transition_in() + customdata = ba.getsession().customdata + + # Show special landmine tip on rookie preset. + if self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: + # Show once per session only (then we revert to regular tips). + if not customdata.get('_showed_onslaught_landmine_tip', False): + customdata['_showed_onslaught_landmine_tip'] = True + self.tips = [ + ba.GameTip( + 'Land-mines are a good way to stop speedy enemies.', + icon=ba.gettexture('powerupLandMines'), + sound=ba.getsound('ding')) + ] + + # Show special tnt tip on pro preset. + if self._preset in {Preset.PRO, Preset.PRO_EASY}: + # Show once per session only (then we revert to regular tips). + if not customdata.get('_showed_onslaught_tnt_tip', False): + customdata['_showed_onslaught_tnt_tip'] = True + self.tips = [ + ba.GameTip( + 'Take out a group of enemies by\n' + 'setting off a bomb near a TNT box.', + icon=ba.gettexture('tnt'), + sound=ba.getsound('ding')) + ] + + # Show special curse tip on uber preset. + if self._preset in {Preset.UBER, Preset.UBER_EASY}: + # Show once per session only (then we revert to regular tips). + if not customdata.get('_showed_onslaught_curse_tip', False): + customdata['_showed_onslaught_curse_tip'] = True + self.tips = [ + ba.GameTip( + 'Curse boxes turn you into a ticking time bomb.\n' + 'The only cure is to quickly grab a health-pack.', + icon=ba.gettexture('powerupCurse'), + sound=ba.getsound('ding')) + ] + + self._spawn_info_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (15, -130), + 'h_attach': 'left', + 'v_attach': 'top', + 'scale': 0.55, + 'color': (0.3, 0.8, 0.3, 1.0), + 'text': '' + })) + ba.setmusic(ba.MusicType.ONSLAUGHT) + + self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), + score_split=0.5) + + def on_begin(self) -> None: + super().on_begin() + player_count = len(self.players) + hard = self._preset not in { + Preset.TRAINING_EASY, Preset.ROOKIE_EASY, Preset.PRO_EASY, + Preset.UBER_EASY + } + if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: + ControlsGuide(delay=3.0, lifespan=10.0, bright=True).autoretain() + + self._have_tnt = False + self._excluded_powerups = ['curse', 'land_mines'] + self._waves = [ + Wave(base_angle=195, + entries=[ + Spawn(BomberBotLite, spacing=5), + ] * player_count), + Wave(base_angle=130, + entries=[ + Spawn(BrawlerBotLite, spacing=5), + ] * player_count), + Wave(base_angle=195, + entries=[Spawn(BomberBotLite, spacing=10)] * + (player_count + 1)), + Wave(base_angle=130, + entries=[ + Spawn(BrawlerBotLite, spacing=10), + ] * (player_count + 1)), + Wave(base_angle=130, + entries=[ + Spawn(BrawlerBotLite, spacing=5) + if player_count > 1 else None, + Spawn(BrawlerBotLite, spacing=5), + Spacing(30), + Spawn(BomberBotLite, spacing=5) + if player_count > 3 else None, + Spawn(BomberBotLite, spacing=5), + Spacing(30), + Spawn(BrawlerBotLite, spacing=5), + Spawn(BrawlerBotLite, spacing=5) + if player_count > 2 else None, + ]), + Wave(base_angle=195, + entries=[ + Spawn(TriggerBot, spacing=90), + Spawn(TriggerBot, spacing=90) + if player_count > 1 else None, + ]), + ] + + elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: + self._have_tnt = False + self._excluded_powerups = ['curse'] + self._waves = [ + Wave(entries=[ + Spawn(ChargerBot, Point.LEFT_UPPER_MORE + ) if player_count > 2 else None, + Spawn(ChargerBot, Point.LEFT_UPPER), + ]), + Wave(entries=[ + Spawn(BomberBotStaticLite, Point.TURRET_TOP_RIGHT), + Spawn(BrawlerBotLite, Point.RIGHT_UPPER), + Spawn(BrawlerBotLite, Point.RIGHT_LOWER + ) if player_count > 1 else None, + Spawn(BomberBotStaticLite, Point.TURRET_BOTTOM_RIGHT + ) if player_count > 2 else None, + ]), + Wave(entries=[ + Spawn(BomberBotStaticLite, Point.TURRET_BOTTOM_LEFT), + Spawn(TriggerBot, Point.LEFT), + Spawn(TriggerBot, Point.LEFT_LOWER + ) if player_count > 1 else None, + Spawn(TriggerBot, Point.LEFT_UPPER + ) if player_count > 2 else None, + ]), + Wave(entries=[ + Spawn(BrawlerBotLite, Point.TOP_RIGHT), + Spawn(BrawlerBot, Point.TOP_HALF_RIGHT + ) if player_count > 1 else None, + Spawn(BrawlerBotLite, Point.TOP_LEFT), + Spawn(BrawlerBotLite, Point.TOP_HALF_LEFT + ) if player_count > 2 else None, + Spawn(BrawlerBot, Point.TOP), + Spawn(BomberBotStaticLite, Point.TURRET_TOP_MIDDLE), + ]), + Wave(entries=[ + Spawn(TriggerBotStatic, Point.TURRET_BOTTOM_LEFT), + Spawn(TriggerBotStatic, Point.TURRET_BOTTOM_RIGHT), + Spawn(TriggerBot, Point.BOTTOM), + Spawn(TriggerBot, Point.BOTTOM_HALF_RIGHT + ) if player_count > 1 else None, + Spawn(TriggerBot, Point.BOTTOM_HALF_LEFT + ) if player_count > 2 else None, + ]), + Wave(entries=[ + Spawn(BomberBotStaticLite, Point.TURRET_TOP_LEFT), + Spawn(BomberBotStaticLite, Point.TURRET_TOP_RIGHT), + Spawn(ChargerBot, Point.BOTTOM), + Spawn(ChargerBot, Point.BOTTOM_HALF_LEFT + ) if player_count > 1 else None, + Spawn(ChargerBot, Point.BOTTOM_HALF_RIGHT + ) if player_count > 2 else None, + ]), + ] + + elif self._preset in {Preset.PRO, Preset.PRO_EASY}: + self._excluded_powerups = ['curse'] + self._have_tnt = True + self._waves = [ + Wave(base_angle=-50, + entries=[ + Spawn(BrawlerBot, spacing=12) + if player_count > 3 else None, + Spawn(BrawlerBot, spacing=12), + Spawn(BomberBot, spacing=6), + Spawn(BomberBot, spacing=6) + if self._preset is Preset.PRO else None, + Spawn(BomberBot, spacing=6) + if player_count > 1 else None, + Spawn(BrawlerBot, spacing=12), + Spawn(BrawlerBot, spacing=12) + if player_count > 2 else None, + ]), + Wave(base_angle=180, + entries=[ + Spawn(BrawlerBot, spacing=6) + if player_count > 3 else None, + Spawn(BrawlerBot, spacing=6) + if self._preset is Preset.PRO else None, + Spawn(BrawlerBot, spacing=6), + Spawn(ChargerBot, spacing=45), + Spawn(ChargerBot, spacing=45) + if player_count > 1 else None, + Spawn(BrawlerBot, spacing=6), + Spawn(BrawlerBot, spacing=6) + if self._preset is Preset.PRO else None, + Spawn(BrawlerBot, spacing=6) + if player_count > 2 else None, + ]), + Wave(base_angle=0, + entries=[ + Spawn(ChargerBot, spacing=30), + Spawn(TriggerBot, spacing=30), + Spawn(TriggerBot, spacing=30), + Spawn(TriggerBot, spacing=30) + if self._preset is Preset.PRO else None, + Spawn(TriggerBot, spacing=30) + if player_count > 1 else None, + Spawn(TriggerBot, spacing=30) + if player_count > 3 else None, + Spawn(ChargerBot, spacing=30), + ]), + Wave(base_angle=90, + entries=[ + Spawn(StickyBot, spacing=50), + Spawn(StickyBot, spacing=50) + if self._preset is Preset.PRO else None, + Spawn(StickyBot, spacing=50), + Spawn(StickyBot, spacing=50) + if player_count > 1 else None, + Spawn(StickyBot, spacing=50) + if player_count > 3 else None, + ]), + Wave(base_angle=0, + entries=[ + Spawn(TriggerBot, spacing=72), + Spawn(TriggerBot, spacing=72), + Spawn(TriggerBot, spacing=72) + if self._preset is Preset.PRO else None, + Spawn(TriggerBot, spacing=72), + Spawn(TriggerBot, spacing=72), + Spawn(TriggerBot, spacing=36) + if player_count > 2 else None, + ]), + Wave(base_angle=30, + entries=[ + Spawn(ChargerBotProShielded, spacing=50), + Spawn(ChargerBotProShielded, spacing=50), + Spawn(ChargerBotProShielded, spacing=50) + if self._preset is Preset.PRO else None, + Spawn(ChargerBotProShielded, spacing=50) + if player_count > 1 else None, + Spawn(ChargerBotProShielded, spacing=50) + if player_count > 2 else None, + ]) + ] + + elif self._preset in {Preset.UBER, Preset.UBER_EASY}: + + # Show controls help in demo/arcade modes. + if ba.app.demo_mode or ba.app.arcade_mode: + ControlsGuide(delay=3.0, lifespan=10.0, + bright=True).autoretain() + + self._have_tnt = True + self._excluded_powerups = [] + self._waves = [ + Wave(entries=[ + Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_LEFT + ) if hard else None, + Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_RIGHT), + Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT + ) if player_count > 2 else None, + Spawn(ExplodeyBot, Point.TOP_RIGHT), + Delay(4.0), + Spawn(ExplodeyBot, Point.TOP_LEFT), + ]), + Wave(entries=[ + Spawn(ChargerBot, Point.LEFT), + Spawn(ChargerBot, Point.RIGHT), + Spawn(ChargerBot, Point.RIGHT_UPPER_MORE + ) if player_count > 2 else None, + Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT), + Spawn(BomberBotProStatic, Point.TURRET_TOP_RIGHT), + ]), + Wave(entries=[ + Spawn(TriggerBotPro, Point.TOP_RIGHT), + Spawn(TriggerBotPro, Point.RIGHT_UPPER_MORE + ) if player_count > 1 else None, + Spawn(TriggerBotPro, Point.RIGHT_UPPER), + Spawn(TriggerBotPro, Point.RIGHT_LOWER) if hard else None, + Spawn(TriggerBotPro, Point.RIGHT_LOWER_MORE + ) if player_count > 2 else None, + Spawn(TriggerBotPro, Point.BOTTOM_RIGHT), + ]), + Wave(entries=[ + Spawn(ChargerBotProShielded, Point.BOTTOM_RIGHT), + Spawn(ChargerBotProShielded, Point.BOTTOM + ) if player_count > 2 else None, + Spawn(ChargerBotProShielded, Point.BOTTOM_LEFT), + Spawn(ChargerBotProShielded, Point.TOP) if hard else None, + Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE), + ]), + Wave(entries=[ + Spawn(ExplodeyBot, Point.LEFT_UPPER), + Delay(1.0), + Spawn(BrawlerBotProShielded, Point.LEFT_LOWER), + Spawn(BrawlerBotProShielded, Point.LEFT_LOWER_MORE), + Delay(4.0), + Spawn(ExplodeyBot, Point.RIGHT_UPPER), + Delay(1.0), + Spawn(BrawlerBotProShielded, Point.RIGHT_LOWER), + Spawn(BrawlerBotProShielded, Point.RIGHT_UPPER_MORE), + Delay(4.0), + Spawn(ExplodeyBot, Point.LEFT), + Delay(5.0), + Spawn(ExplodeyBot, Point.RIGHT), + ]), + Wave(entries=[ + Spawn(BomberBotProStatic, Point.TURRET_TOP_LEFT), + Spawn(BomberBotProStatic, Point.TURRET_TOP_RIGHT), + Spawn(BomberBotProStatic, Point.TURRET_BOTTOM_LEFT), + Spawn(BomberBotProStatic, Point.TURRET_BOTTOM_RIGHT), + Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_LEFT + ) if hard else None, + Spawn(BomberBotProStatic, Point.TURRET_TOP_MIDDLE_RIGHT + ) if hard else None, + ]) + ] + + # We generate these on the fly in endless. + elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: + self._have_tnt = True + self._excluded_powerups = [] + self._waves = [] + + else: + raise RuntimeError(f'Invalid preset: {self._preset}') + + # FIXME: Should migrate to use setup_standard_powerup_drops(). + + # Spit out a few powerups and start dropping more shortly. + self._drop_powerups(standard_points=True, + poweruptype='curse' if self._preset + in [Preset.UBER, Preset.UBER_EASY] else + ('land_mines' if self._preset + in [Preset.ROOKIE, Preset.ROOKIE_EASY] else None)) + ba.timer(4.0, self._start_powerup_drops) + + # Our TNT spawner (if applicable). + if self._have_tnt: + self._tntspawner = TNTSpawner(position=self._tntspawnpos) + + self.setup_low_life_warning_sound() + self._update_scores() + self._bots = SpazBotSet() + ba.timer(4.0, self._start_updating_waves) + + def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: + self._show_standard_scores_to_beat_ui(scores) + + def _get_dist_grp_totals(self, grps: List[Any]) -> Tuple[int, int]: + totalpts = 0 + totaldudes = 0 + for grp in grps: + for grpentry in grp: + dudes = grpentry[1] + totalpts += grpentry[0] * dudes + totaldudes += dudes + return totalpts, totaldudes + + def _get_distribution(self, target_points: int, min_dudes: int, + max_dudes: int, group_count: int, + max_level: int) -> List[List[Tuple[int, int]]]: + """Calculate a distribution of bad guys given some params.""" + max_iterations = 10 + max_dudes * 2 + + groups: List[List[Tuple[int, int]]] = [] + for _g in range(group_count): + groups.append([]) + types = [1] + if max_level > 1: + types.append(2) + if max_level > 2: + types.append(3) + if max_level > 3: + types.append(4) + for iteration in range(max_iterations): + diff = self._add_dist_entry_if_possible(groups, max_dudes, + target_points, types) + + total_points, total_dudes = self._get_dist_grp_totals(groups) + full = (total_points >= target_points) + + if full: + # Every so often, delete a random entry just to + # shake up our distribution. + if random.random() < 0.2 and iteration != max_iterations - 1: + self._delete_random_dist_entry(groups) + + # If we don't have enough dudes, kill the group with + # the biggest point value. + elif (total_dudes < min_dudes + and iteration != max_iterations - 1): + self._delete_biggest_dist_entry(groups) + + # If we've got too many dudes, kill the group with the + # smallest point value. + elif (total_dudes > max_dudes + and iteration != max_iterations - 1): + self._delete_smallest_dist_entry(groups) + + # Close enough.. we're done. + else: + if diff == 0: + break + + return groups + + def _add_dist_entry_if_possible(self, groups: List[List[Tuple[int, int]]], + max_dudes: int, target_points: int, + types: List[int]) -> int: + # See how much we're off our target by. + total_points, total_dudes = self._get_dist_grp_totals(groups) + diff = target_points - total_points + dudes_diff = max_dudes - total_dudes + + # Add an entry if one will fit. + value = types[random.randrange(len(types))] + group = groups[random.randrange(len(groups))] + if not group: + max_count = random.randint(1, 6) + else: + max_count = 2 * random.randint(1, 3) + max_count = min(max_count, dudes_diff) + count = min(max_count, diff // value) + if count > 0: + group.append((value, count)) + total_points += value * count + total_dudes += count + diff = target_points - total_points + return diff + + def _delete_smallest_dist_entry( + self, groups: List[List[Tuple[int, int]]]) -> None: + smallest_value = 9999 + smallest_entry = None + smallest_entry_group = None + for group in groups: + for entry in group: + if entry[0] < smallest_value or smallest_entry is None: + smallest_value = entry[0] + smallest_entry = entry + smallest_entry_group = group + assert smallest_entry is not None + assert smallest_entry_group is not None + smallest_entry_group.remove(smallest_entry) + + def _delete_biggest_dist_entry( + self, groups: List[List[Tuple[int, int]]]) -> None: + biggest_value = 9999 + biggest_entry = None + biggest_entry_group = None + for group in groups: + for entry in group: + if entry[0] > biggest_value or biggest_entry is None: + biggest_value = entry[0] + biggest_entry = entry + biggest_entry_group = group + if biggest_entry is not None: + assert biggest_entry_group is not None + biggest_entry_group.remove(biggest_entry) + + def _delete_random_dist_entry(self, + groups: List[List[Tuple[int, int]]]) -> None: + entry_count = 0 + for group in groups: + for _ in group: + entry_count += 1 + if entry_count > 1: + del_entry = random.randrange(entry_count) + entry_count = 0 + for group in groups: + for entry in group: + if entry_count == del_entry: + group.remove(entry) + break + entry_count += 1 + + def spawn_player(self, player: Player) -> ba.Actor: + + # We keep track of who got hurt each wave for score purposes. + player.has_been_hurt = False + pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), + self._spawn_center[1], + self._spawn_center[2] + random.uniform(-1.5, 1.5)) + spaz = self.spawn_player_spaz(player, position=pos) + if self._preset in { + Preset.TRAINING_EASY, Preset.ROOKIE_EASY, Preset.PRO_EASY, + Preset.UBER_EASY + }: + spaz.impact_scale = 0.25 + spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) + return spaz + + def _handle_player_dropped_bomb(self, player: ba.Actor, + bomb: ba.Actor) -> None: + del player, bomb # Unused. + self._player_has_dropped_bomb = True + + def _drop_powerup(self, index: int, poweruptype: str = None) -> None: + poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( + forcetype=poweruptype, excludetypes=self._excluded_powerups)) + PowerupBox(position=self.map.powerup_spawn_points[index], + poweruptype=poweruptype).autoretain() + + def _start_powerup_drops(self) -> None: + self._powerup_drop_timer = ba.Timer(3.0, + ba.WeakCall(self._drop_powerups), + repeat=True) + + def _drop_powerups(self, + standard_points: bool = False, + poweruptype: str = None) -> None: + """Generic powerup drop.""" + if standard_points: + points = self.map.powerup_spawn_points + for i in range(len(points)): + ba.timer( + 1.0 + i * 0.5, + ba.WeakCall(self._drop_powerup, i, + poweruptype if i == 0 else None)) + else: + point = (self._powerup_center[0] + random.uniform( + -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), + self._powerup_center[1], + self._powerup_center[2] + random.uniform( + -self._powerup_spread[1], self._powerup_spread[1])) + + # Drop one random one somewhere. + PowerupBox( + position=point, + poweruptype=PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=self._excluded_powerups)).autoretain() + + def do_end(self, outcome: str, delay: float = 0.0) -> None: + """End the game with the specified outcome.""" + if outcome == 'defeat': + self.fade_to_red() + score: Optional[int] + if self._wavenum >= 2: + score = self._score + fail_message = None + else: + score = None + fail_message = ba.Lstr(resource='reachWave2Text') + self.end( + { + 'outcome': outcome, + 'score': score, + 'fail_message': fail_message, + 'playerinfos': self.initialplayerinfos + }, + delay=delay) + + def _award_completion_achievements(self) -> None: + if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: + self._award_achievement('Onslaught Training Victory', sound=False) + if not self._player_has_dropped_bomb: + self._award_achievement('Boxer', sound=False) + elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: + self._award_achievement('Rookie Onslaught Victory', sound=False) + if not self._a_player_has_been_hurt: + self._award_achievement('Flawless Victory', sound=False) + elif self._preset in {Preset.PRO, Preset.PRO_EASY}: + self._award_achievement('Pro Onslaught Victory', sound=False) + if not self._player_has_dropped_bomb: + self._award_achievement('Pro Boxer', sound=False) + elif self._preset in {Preset.UBER, Preset.UBER_EASY}: + self._award_achievement('Uber Onslaught Victory', sound=False) + + def _update_waves(self) -> None: + + # If we have no living bots, go to the next wave. + assert self._bots is not None + if (self._can_end_wave and not self._bots.have_living_bots() + and not self._game_over): + self._can_end_wave = False + self._time_bonus_timer = None + self._time_bonus_text = None + if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: + won = False + else: + won = (self._wavenum == len(self._waves)) + + base_delay = 4.0 if won else 0.0 + + # Reward time bonus. + if self._time_bonus > 0: + ba.timer(0, lambda: ba.playsound(self._cashregistersound)) + ba.timer(base_delay, + ba.WeakCall(self._award_time_bonus, self._time_bonus)) + base_delay += 1.0 + + # Reward flawless bonus. + if self._wavenum > 0: + have_flawless = False + for player in self.players: + if player.is_alive() and not player.has_been_hurt: + have_flawless = True + ba.timer( + base_delay, + ba.WeakCall(self._award_flawless_bonus, player)) + player.has_been_hurt = False # reset + if have_flawless: + base_delay += 1.0 + + if won: + self.show_zoom_message(ba.Lstr(resource='victoryText'), + scale=1.0, + duration=4.0) + self.celebrate(20.0) + self._award_completion_achievements() + ba.timer(base_delay, ba.WeakCall(self._award_completion_bonus)) + base_delay += 0.85 + ba.playsound(self._winsound) + ba.cameraflash() + ba.setmusic(ba.MusicType.VICTORY) + self._game_over = True + + # Can't just pass delay to do_end because our extra bonuses + # haven't been added yet (once we call do_end the score + # gets locked in). + ba.timer(base_delay, ba.WeakCall(self.do_end, 'victory')) + return + + self._wavenum += 1 + + # Short celebration after waves. + if self._wavenum > 1: + self.celebrate(0.5) + ba.timer(base_delay, ba.WeakCall(self._start_next_wave)) + + def _award_completion_bonus(self) -> None: + ba.playsound(self._cashregistersound) + for player in self.players: + try: + if player.is_alive(): + assert self.initialplayerinfos is not None + self.stats.player_scored( + player, + int(100 / len(self.initialplayerinfos)), + scale=1.4, + color=(0.6, 0.6, 1.0, 1.0), + title=ba.Lstr(resource='completionBonusText'), + screenmessage=False) + except Exception: + ba.print_exception() + + def _award_time_bonus(self, bonus: int) -> None: + ba.playsound(self._cashregistersound) + PopupText(ba.Lstr(value='+${A} ${B}', + subs=[('${A}', str(bonus)), + ('${B}', ba.Lstr(resource='timeBonusText'))]), + color=(1, 1, 0.5, 1), + scale=1.0, + position=(0, 3, -1)).autoretain() + self._score += self._time_bonus + self._update_scores() + + def _award_flawless_bonus(self, player: Player) -> None: + ba.playsound(self._cashregistersound) + try: + if player.is_alive(): + assert self._flawless_bonus is not None + self.stats.player_scored( + player, + self._flawless_bonus, + scale=1.2, + color=(0.6, 1.0, 0.6, 1.0), + title=ba.Lstr(resource='flawlessWaveText'), + screenmessage=False) + except Exception: + ba.print_exception() + + def _start_time_bonus_timer(self) -> None: + self._time_bonus_timer = ba.Timer(1.0, + ba.WeakCall(self._update_time_bonus), + repeat=True) + + def _update_player_spawn_info(self) -> None: + + # If we have no living players lets just blank this. + assert self._spawn_info_text is not None + assert self._spawn_info_text.node + if not any(player.is_alive() for player in self.teams[0].players): + self._spawn_info_text.node.text = '' + else: + text: Union[str, ba.Lstr] = '' + for player in self.players: + if (not player.is_alive() + and (self._preset + in [Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT] or + (player.respawn_wave <= len(self._waves)))): + rtxt = ba.Lstr(resource='onslaughtRespawnText', + subs=[('${PLAYER}', player.getname()), + ('${WAVE}', str(player.respawn_wave)) + ]) + text = ba.Lstr(value='${A}${B}\n', + subs=[ + ('${A}', text), + ('${B}', rtxt), + ]) + self._spawn_info_text.node.text = text + + def _respawn_players_for_wave(self) -> None: + # Respawn applicable players. + if self._wavenum > 1 and not self.is_waiting_for_continue(): + for player in self.players: + if (not player.is_alive() + and player.respawn_wave == self._wavenum): + self.spawn_player(player) + self._update_player_spawn_info() + + def _setup_wave_spawns(self, wave: Wave) -> None: + tval = 0.0 + dtime = 0.2 + if self._wavenum == 1: + spawn_time = 3.973 + tval += 0.5 + else: + spawn_time = 2.648 + + bot_angle = wave.base_angle + self._time_bonus = 0 + self._flawless_bonus = 0 + for info in wave.entries: + if info is None: + continue + if isinstance(info, Delay): + spawn_time += info.duration + continue + if isinstance(info, Spacing): + bot_angle += info.spacing + continue + bot_type_2 = info.bottype + if bot_type_2 is not None: + assert not isinstance(bot_type_2, str) + self._time_bonus += bot_type_2.points_mult * 20 + self._flawless_bonus += bot_type_2.points_mult * 5 + + # If its got a position, use that. + point = info.point + if point is not None: + assert bot_type_2 is not None + spcall = ba.WeakCall(self.add_bot_at_point, point, bot_type_2, + spawn_time) + ba.timer(tval, spcall) + tval += dtime + else: + spacing = info.spacing + bot_angle += spacing * 0.5 + if bot_type_2 is not None: + tcall = ba.WeakCall(self.add_bot_at_angle, bot_angle, + bot_type_2, spawn_time) + ba.timer(tval, tcall) + tval += dtime + bot_angle += spacing * 0.5 + + # We can end the wave after all the spawning happens. + ba.timer(tval + spawn_time - dtime + 0.01, + ba.WeakCall(self._set_can_end_wave)) + + def _start_next_wave(self) -> None: + + # This can happen if we beat a wave as we die. + # We don't wanna respawn players and whatnot if this happens. + if self._game_over: + return + + self._respawn_players_for_wave() + if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: + wave = self._generate_random_wave() + else: + wave = self._waves[self._wavenum - 1] + self._setup_wave_spawns(wave) + self._update_wave_ui_and_bonuses() + ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound)) + + def _update_wave_ui_and_bonuses(self) -> None: + + self.show_zoom_message(ba.Lstr(value='${A} ${B}', + subs=[('${A}', + ba.Lstr(resource='waveText')), + ('${B}', str(self._wavenum))]), + scale=1.0, + duration=1.0, + trail=True) + + # Reset our time bonus. + tbtcolor = (1, 1, 0, 1) + tbttxt = ba.Lstr(value='${A}: ${B}', + subs=[ + ('${A}', ba.Lstr(resource='timeBonusText')), + ('${B}', str(self._time_bonus)), + ]) + self._time_bonus_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'vr_depth': -30, + 'color': tbtcolor, + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0, -60), + 'scale': 0.8, + 'text': tbttxt + })) + + ba.timer(5.0, ba.WeakCall(self._start_time_bonus_timer)) + wtcolor = (1, 1, 1, 1) + wttxt = ba.Lstr( + value='${A} ${B}', + subs=[('${A}', ba.Lstr(resource='waveText')), + ('${B}', str(self._wavenum) + + ('' if self._preset + in [Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT] else + ('/' + str(len(self._waves)))))]) + self._wave_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'vr_depth': -10, + 'color': wtcolor, + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0, -40), + 'scale': 1.3, + 'text': wttxt + })) + + def _bot_levels_for_wave(self) -> List[List[Type[SpazBot]]]: + level = self._wavenum + bot_types = [ + BomberBot, BrawlerBot, TriggerBot, ChargerBot, BomberBotPro, + BrawlerBotPro, TriggerBotPro, BomberBotProShielded, ExplodeyBot, + ChargerBotProShielded, StickyBot, BrawlerBotProShielded, + TriggerBotProShielded + ] + if level > 5: + bot_types += [ + ExplodeyBot, + TriggerBotProShielded, + BrawlerBotProShielded, + ChargerBotProShielded, + ] + if level > 7: + bot_types += [ + ExplodeyBot, + TriggerBotProShielded, + BrawlerBotProShielded, + ChargerBotProShielded, + ] + if level > 10: + bot_types += [ + TriggerBotProShielded, TriggerBotProShielded, + TriggerBotProShielded, TriggerBotProShielded + ] + if level > 13: + bot_types += [ + TriggerBotProShielded, TriggerBotProShielded, + TriggerBotProShielded, TriggerBotProShielded + ] + bot_levels = [[b for b in bot_types if b.points_mult == 1], + [b for b in bot_types if b.points_mult == 2], + [b for b in bot_types if b.points_mult == 3], + [b for b in bot_types if b.points_mult == 4]] + + # Make sure all lists have something in them + if not all(bot_levels): + raise RuntimeError('Got empty bot level') + return bot_levels + + def _add_entries_for_distribution_group( + self, group: List[Tuple[int, int]], + bot_levels: List[List[Type[SpazBot]]], + all_entries: List[Union[Spawn, Spacing, Delay, None]]) -> None: + entries: List[Union[Spawn, Spacing, Delay, None]] = [] + for entry in group: + bot_level = bot_levels[entry[0] - 1] + bot_type = bot_level[random.randrange(len(bot_level))] + rval = random.random() + if rval < 0.5: + spacing = 10.0 + elif rval < 0.9: + spacing = 20.0 + else: + spacing = 40.0 + split = random.random() > 0.3 + for i in range(entry[1]): + if split and i % 2 == 0: + entries.insert(0, Spawn(bot_type, spacing=spacing)) + else: + entries.append(Spawn(bot_type, spacing=spacing)) + if entries: + all_entries += entries + all_entries.append( + Spacing(40.0 if random.random() < 0.5 else 80.0)) + + def _generate_random_wave(self) -> Wave: + level = self._wavenum + bot_levels = self._bot_levels_for_wave() + + target_points = level * 3 - 2 + min_dudes = min(1 + level // 3, 10) + max_dudes = min(10, level + 1) + max_level = 4 if level > 6 else (3 if level > 3 else + (2 if level > 2 else 1)) + group_count = 3 + distribution = self._get_distribution(target_points, min_dudes, + max_dudes, group_count, + max_level) + all_entries: List[Union[Spawn, Spacing, Delay, None]] = [] + for group in distribution: + self._add_entries_for_distribution_group(group, bot_levels, + all_entries) + angle_rand = random.random() + if angle_rand > 0.75: + base_angle = 130.0 + elif angle_rand > 0.5: + base_angle = 210.0 + elif angle_rand > 0.25: + base_angle = 20.0 + else: + base_angle = -30.0 + base_angle += (0.5 - random.random()) * 20.0 + wave = Wave(base_angle=base_angle, entries=all_entries) + return wave + + def add_bot_at_point(self, + point: Point, + spaz_type: Type[SpazBot], + spawn_time: float = 1.0) -> None: + """Add a new bot at a specified named point.""" + if self._game_over: + return + assert isinstance(point.value, str) + pointpos = self.map.defs.points[point.value] + assert self._bots is not None + self._bots.spawn_bot(spaz_type, pos=pointpos, spawn_time=spawn_time) + + def add_bot_at_angle(self, + angle: float, + spaz_type: Type[SpazBot], + spawn_time: float = 1.0) -> None: + """Add a new bot at a specified angle (for circular maps).""" + if self._game_over: + return + angle_radians = angle / 57.2957795 + xval = math.sin(angle_radians) * 1.06 + zval = math.cos(angle_radians) * 1.06 + point = (xval / 0.125, 2.3, (zval / 0.2) - 3.7) + assert self._bots is not None + self._bots.spawn_bot(spaz_type, pos=point, spawn_time=spawn_time) + + def _update_time_bonus(self) -> None: + self._time_bonus = int(self._time_bonus * 0.93) + if self._time_bonus > 0 and self._time_bonus_text is not None: + assert self._time_bonus_text.node + self._time_bonus_text.node.text = ba.Lstr( + value='${A}: ${B}', + subs=[('${A}', ba.Lstr(resource='timeBonusText')), + ('${B}', str(self._time_bonus))]) + else: + self._time_bonus_text = None + + def _start_updating_waves(self) -> None: + self._wave_update_timer = ba.Timer(2.0, + ba.WeakCall(self._update_waves), + repeat=True) + + def _update_scores(self) -> None: + score = self._score + if self._preset is Preset.ENDLESS: + if score >= 500: + self._award_achievement('Onslaught Master') + if score >= 1000: + self._award_achievement('Onslaught Wizard') + if score >= 5000: + self._award_achievement('Onslaught God') + assert self._scoreboard is not None + self._scoreboard.set_team_value(self.teams[0], score, max_score=None) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, PlayerSpazHurtMessage): + msg.spaz.getplayer(Player, True).has_been_hurt = True + self._a_player_has_been_hurt = True + + elif isinstance(msg, ba.PlayerScoredMessage): + self._score += msg.score + self._update_scores() + + elif isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) # Augment standard behavior. + player = msg.getplayer(Player) + self._a_player_has_been_hurt = True + + # Make note with the player when they can respawn: + if self._wavenum < 10: + player.respawn_wave = max(2, self._wavenum + 1) + elif self._wavenum < 15: + player.respawn_wave = max(2, self._wavenum + 2) + else: + player.respawn_wave = max(2, self._wavenum + 3) + ba.timer(0.1, self._update_player_spawn_info) + ba.timer(0.1, self._checkroundover) + + elif isinstance(msg, SpazBotDiedMessage): + pts, importance = msg.spazbot.get_death_points(msg.how) + if msg.killerplayer is not None: + self._handle_kill_achievements(msg) + target: Optional[Sequence[float]] + if msg.spazbot.node: + target = msg.spazbot.node.position + else: + target = None + + killerplayer = msg.killerplayer + self.stats.player_scored(killerplayer, + pts, + target=target, + kill=True, + screenmessage=False, + importance=importance) + ba.playsound(self._dingsound + if importance == 1 else self._dingsoundhigh, + volume=0.6) + + # Normally we pull scores from the score-set, but if there's + # no player lets be explicit. + else: + self._score += pts + self._update_scores() + else: + super().handlemessage(msg) + + def _handle_kill_achievements(self, msg: SpazBotDiedMessage) -> None: + if self._preset in {Preset.TRAINING, Preset.TRAINING_EASY}: + self._handle_training_kill_achievements(msg) + elif self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: + self._handle_rookie_kill_achievements(msg) + elif self._preset in {Preset.PRO, Preset.PRO_EASY}: + self._handle_pro_kill_achievements(msg) + elif self._preset in {Preset.UBER, Preset.UBER_EASY}: + self._handle_uber_kill_achievements(msg) + + def _handle_uber_kill_achievements(self, msg: SpazBotDiedMessage) -> None: + + # Uber mine achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): + self._land_mine_kills += 1 + if self._land_mine_kills >= 6: + self._award_achievement('Gold Miner') + + # Uber tnt achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): + self._tnt_kills += 1 + if self._tnt_kills >= 6: + ba.timer(0.5, ba.WeakCall(self._award_achievement, + 'TNT Terror')) + + def _handle_pro_kill_achievements(self, msg: SpazBotDiedMessage) -> None: + + # TNT achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): + self._tnt_kills += 1 + if self._tnt_kills >= 3: + ba.timer( + 0.5, + ba.WeakCall(self._award_achievement, + 'Boom Goes the Dynamite')) + + def _handle_rookie_kill_achievements(self, + msg: SpazBotDiedMessage) -> None: + # Land-mine achievement: + if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): + self._land_mine_kills += 1 + if self._land_mine_kills >= 3: + self._award_achievement('Mine Games') + + def _handle_training_kill_achievements(self, + msg: SpazBotDiedMessage) -> None: + # Toss-off-map achievement: + if msg.spazbot.last_attacked_type == ('picked_up', 'default'): + self._throw_off_kills += 1 + if self._throw_off_kills >= 3: + self._award_achievement('Off You Go Then') + + def _set_can_end_wave(self) -> None: + self._can_end_wave = True + + def end_game(self) -> None: + # Tell our bots to celebrate just to rub it in. + assert self._bots is not None + self._bots.final_celebrate() + self._game_over = True + self.do_end('defeat', delay=2.0) + ba.setmusic(None) + + def on_continue(self) -> None: + for player in self.players: + if not player.is_alive(): + self.spawn_player(player) + + def _checkroundover(self) -> None: + """Potentially end the round based on the state of the game.""" + if self.has_ended(): + return + if not any(player.is_alive() for player in self.teams[0].players): + # Allow continuing after wave 1. + if self._wavenum > 1: + self.continue_or_end_game() + else: + self.end_game() diff --git a/dist/ba_data/python/bastd/game/race.py b/dist/ba_data/python/bastd/game/race.py new file mode 100644 index 0000000..0a058ac --- /dev/null +++ b/dist/ba_data/python/bastd/game/race.py @@ -0,0 +1,715 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines Race mini-game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import ba +from bastd.actor.bomb import Bomb +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bastd.actor.onscreentimer import OnScreenTimer + + +@dataclass +class RaceMine: + """Holds info about a mine on the track.""" + point: Sequence[float] + mine: Optional[Bomb] + + +class RaceRegion(ba.Actor): + """Region used to track progress during a race.""" + + def __init__(self, pt: Sequence[float], index: int): + super().__init__() + activity = self.activity + assert isinstance(activity, RaceGame) + self.pos = pt + self.index = index + self.node = ba.newnode( + 'region', + delegate=self, + attrs={ + 'position': pt[:3], + 'scale': (pt[3] * 2.0, pt[4] * 2.0, pt[5] * 2.0), + 'type': 'box', + 'materials': [activity.race_region_material] + }) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[ba.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.time: Optional[float] = None + self.lap = 0 + self.finished = False + + +# ba_meta export game +class RaceGame(ba.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Race' + description = 'Run real fast!' + scoreconfig = ba.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=ba.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting('Laps', min_value=1, default=3, increment=1), + ba.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + ba.IntChoiceSetting( + 'Mine Spawning', + default=4000, + choices=[ + ('No Mines', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ], + ), + ba.IntChoiceSetting( + 'Bomb Spawning', + choices=[ + ('None', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ('1 Second', 1000), + ], + default=2000, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, ba.DualTeamSession): + settings.append( + ba.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('race') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_sound = ba.getsound('score') + self._swipsound = ba.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = ba.gettexture('nub') + self._beep_1_sound = ba.getsound('raceBeep1') + self._beep_2_sound = ba.getsound('raceBeep2') + self.race_region_material: Optional[ba.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[ba.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._race_mines: Optional[List[RaceMine]] = None + self._race_mine_timer: Optional[ba.Timer] = None + self._scoreboard_timer: Optional[ba.Timer] = None + self._player_order_update_timer: Optional[ba.Timer] = None + self._start_lights: Optional[List[ba.Node]] = None + self._bomb_spawn_timer: Optional[ba.Timer] = None + self._laps = int(settings['Laps']) + self._entire_team_must_finish = bool( + settings.get('Entire Team Must Finish', False)) + self._time_limit = float(settings['Time Limit']) + self._mine_spawning = int(settings['Mine Spawning']) + self._bomb_spawning = int(settings['Bomb Spawning']) + self._epic_mode = bool(settings['Epic Mode']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC_RACE + if self._epic_mode else ba.MusicType.RACE) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + t_str = ' Your entire team has to finish.' + else: + t_str = '' + + if self._laps > 1: + return 'Run ${ARG1} laps.' + t_str, self._laps + return 'Run 1 lap.' + t_str + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._laps > 1: + return 'run ${ARG1} laps', self._laps + return 'run 1 lap' + + def on_transition_in(self) -> None: + super().on_transition_in() + shared = SharedObjects.get() + pts = self.map.get_def_points('race_point') + mat = self.race_region_material = ba.Material() + mat.add_actions(conditions=('they_have_material', + shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + self._handle_race_point_collide), + )) + for rpt in pts: + self._regions.append(RaceRegion(rpt, len(self._regions))) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = ba.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + ba.timer(0.5, light.delete) + ba.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + + def _handle_race_point_collide(self) -> None: + # FIXME: Tidy this up. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + collision = ba.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + last_region = player.last_region + this_region = region.index + + if last_region != this_region: + + # If a player tries to skip regions, smite them. + # Allow a one region leeway though (its plausible players can get + # blown over a region, etc). + if this_region > last_region + 2: + if player.is_alive(): + assert player.actor + player.actor.handlemessage(ba.DieMessage()) + ba.screenmessage(ba.Lstr( + translate=('statements', 'Killing ${NAME} for' + ' skipping part of the track!'), + subs=[('${NAME}', player.getname(full=True))]), + color=(1, 0, 0)) + else: + # If this player is in first, note that this is the + # front-most race-point. + if player.rank == 0: + self._front_race_region = this_region + + player.last_region = this_region + if last_region >= len(self._regions) - 2 and this_region == 0: + team = player.team + player.lap = min(self._laps, player.lap + 1) + + # In teams mode with all-must-finish on, the team lap + # value is the min of all team players. + # Otherwise its the max. + if isinstance(self.session, ba.DualTeamSession + ) and self._entire_team_must_finish: + team.lap = min([p.lap for p in team.players]) + else: + team.lap = max([p.lap for p in team.players]) + + # A player is finishing. + if player.lap == self._laps: + + # In teams mode, hand out points based on the order + # players come in. + if isinstance(self.session, ba.DualTeamSession): + assert self._team_finish_pts is not None + if self._team_finish_pts > 0: + self.stats.player_scored(player, + self._team_finish_pts, + screenmessage=False) + self._team_finish_pts -= 25 + + # Flash where the player is. + self._flash_player(player, 1.0) + player.finished = True + assert player.actor + player.actor.handlemessage( + ba.DieMessage(immediate=True)) + + # Makes sure noone behind them passes them in rank + # while finishing. + player.distance = 9999.0 + + # If the whole team has finished the race. + if team.lap == self._laps: + ba.playsound(self._score_sound) + player.team.finished = True + assert self._timer is not None + elapsed = ba.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + self._check_end_game() + + # Team has yet to finish. + else: + ba.playsound(self._swipsound) + + # They've just finished a lap but not the race. + else: + ba.playsound(self._swipsound) + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = ba.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = ba.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = ba.newnode('text', + owner=mathnode, + attrs={ + 'text': tstr, + 'in_world': True, + 'color': (1, 1, 0, 1), + 'scale': 0.015, + 'h_align': 'center' + }) + mathnode.connectattr('output', txtnode, 'position') + ba.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + ba.timer(2.3, mathnode.delete) + except Exception: + ba.print_exception('Error printing lap.') + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + + # A player leaving disqualifies the team if 'Entire Team Must Finish' + # is on (otherwise in teams mode everyone could just leave except the + # leading player to win). + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + ba.screenmessage(ba.Lstr( + translate=('statements', + '${TEAM} is disqualified because ${PLAYER} left'), + subs=[('${TEAM}', player.team.name), + ('${PLAYER}', player.getname(full=True))]), + color=(1, 1, 0)) + player.team.finished = True + player.team.time = None + player.team.lap = 0 + ba.playsound(ba.getsound('boo')) + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(ba.DieMessage()) + except Exception: + ba.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + ba.pushcall(self._check_end_game) + + def _update_scoreboard(self) -> None: + for team in self.teams: + distances = [player.distance for player in team.players] + if not distances: + teams_dist = 0.0 + else: + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + teams_dist = min(distances) + else: + teams_dist = max(distances) + self._scoreboard.set_team_value( + team, + teams_dist, + self._laps, + flash=(teams_dist >= float(self._laps)), + show_value=False) + + def on_begin(self) -> None: + from bastd.actor.onscreentimer import OnScreenTimer + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._team_finish_pts = 100 + + # Throw a timer up on-screen. + self._time_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -50), + 'scale': 1.4, + 'text': '' + })) + self._timer = OnScreenTimer() + + if self._mine_spawning != 0: + self._race_mines = [ + RaceMine(point=p, mine=None) + for p in self.map.get_def_points('race_mine') + ] + if self._race_mines: + self._race_mine_timer = ba.Timer(0.001 * self._mine_spawning, + self._update_race_mine, + repeat=True) + + self._scoreboard_timer = ba.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = ba.Timer(0.25, + self._update_player_order, + repeat=True) + + if self.slow_motion: + t_scale = 0.4 + light_y = 50 + else: + t_scale = 1.0 + light_y = 150 + lstart = 7.1 * t_scale + inc = 1.25 * t_scale + + ba.timer(lstart, self._do_light_1) + ba.timer(lstart + inc, self._do_light_2) + ba.timer(lstart + 2 * inc, self._do_light_3) + ba.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = ba.newnode('image', + attrs={ + 'texture': ba.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + ba.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0, + 12.0 * t_scale: 1.0, + 12.5 * t_scale: 0.0 + }) + ba.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._start_lights[0].color = (0.2, 0, 0) + self._start_lights[1].color = (0.2, 0, 0) + self._start_lights[2].color = (0.2, 0.05, 0) + self._start_lights[3].color = (0.0, 0.3, 0) + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + ba.playsound(self._beep_1_sound) + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + ba.playsound(self._beep_2_sound) + for player in self.players: + if player.actor is not None: + try: + assert isinstance(player.actor, PlayerSpaz) + player.actor.connect_controls_to_player() + except Exception: + ba.print_exception('Error in race player connects.') + assert self._timer is not None + self._timer.start() + + if self._bomb_spawning != 0: + self._bomb_spawn_timer = ba.Timer(0.001 * self._bomb_spawning, + self._spawn_bomb, + repeat=True) + + self._race_started = True + + def _update_player_order(self) -> None: + + # Calc all player distances. + for player in self.players: + pos: Optional[ba.Vec3] + try: + pos = player.position + except ba.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = ba.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = ba.Vec3(rg2.pos[:3]) + r2dist = (pos - r2pt).length() + amt = 1.0 - (r2dist / (r2pt - r1pt).length()) + amt = player.lap + (r_index + amt) * (1.0 / len(self._regions)) + player.distance = amt + + # Sort players by distance and update their ranks. + p_list = [(player.distance, player) for player in self.players] + + p_list.sort(reverse=True, key=lambda x: x[0]) + for i, plr in enumerate(p_list): + plr[1].rank = i + if plr[1].actor: + node = plr[1].distance_txt + if node: + node.text = str(i + 1) if plr[1].is_alive() else '' + + def _spawn_bomb(self) -> None: + if self._front_race_region is None: + return + region = (self._front_race_region + 3) % len(self._regions) + pos = self._regions[region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1] + 1.0, + pos[2] + random.uniform(*z_range)) + ba.timer(random.uniform(0.0, 2.0), + ba.WeakCall(self._spawn_bomb_at_pos, pos)) + + def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: + if self.has_ended(): + return + Bomb(position=pos, bomb_type='normal').autoretain() + + def _make_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + rmine.mine = Bomb(position=rmine.point[:3], bomb_type='land_mine') + rmine.mine.arm() + + def _flash_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + light = ba.newnode('light', + attrs={ + 'position': rmine.point[:3], + 'color': (1, 0.2, 0.2), + 'radius': 0.1, + 'height_attenuated': False + }) + ba.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _update_race_mine(self) -> None: + assert self._race_mines is not None + m_index = -1 + rmine = None + for _i in range(3): + m_index = random.randrange(len(self._race_mines)) + rmine = self._race_mines[m_index] + if not rmine.mine: + break + assert rmine is not None + if not rmine.mine: + self._flash_mine(m_index) + ba.timer(0.95, ba.Call(self._make_mine, m_index)) + + def spawn_player(self, player: Player) -> ba.Actor: + if player.team.finished: + # FIXME: This is not type-safe! + # This call is expected to always return an Actor! + # Perhaps we need something like can_spawn_player()... + # noinspection PyTypeChecker + return None # type: ignore + pos = self._regions[player.last_region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1], + pos[2] + random.uniform(*z_range)) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90 if not self._race_started else None) + assert spaz.node + + # Prevent controlling of characters before the start of the race. + if not self._race_started: + spaz.disconnect_controls_from_player() + + mathnode = ba.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = ba.newnode('text', + owner=spaz.node, + attrs={ + 'text': '', + 'in_world': True, + 'color': (1, 1, 0.4), + 'scale': 0.02, + 'h_align': 'center' + }) + player.distance_txt = distance_txt + mathnode.connectattr('output', distance_txt, 'position') + return spaz + + def _check_end_game(self) -> None: + + # If there's no teams left racing, finish. + teams_still_in = len([t for t in self.teams if not t.finished]) + if teams_still_in == 0: + self.end_game() + return + + # Count the number of teams that have completed the race. + teams_completed = len( + [t for t in self.teams if t.finished and t.time is not None]) + + if teams_completed > 0: + session = self.session + + # In teams mode its over as soon as any team finishes the race + + # FIXME: The get_ffa_point_awards code looks dangerous. + if isinstance(session, ba.DualTeamSession): + self.end_game() + else: + # In ffa we keep the race going while there's still any points + # to be handed out. Find out how many points we have to award + # and how many teams have finished, and once that matches + # we're done. + assert isinstance(session, ba.FreeForAllSession) + points_to_award = len(session.get_ffa_point_awards()) + if teams_completed >= points_to_award - teams_completed: + self.end_game() + return + + def end_game(self) -> None: + + # Stop updating our time text, and set it to show the exact last + # finish time if we have one. (so users don't get upset if their + # final time differs from what they see onscreen by a tiny amount) + assert self._timer is not None + if self._timer.has_started(): + self._timer.stop( + endtime=None if self._last_team_time is None else ( + self._timer.getstarttime() + self._last_team_time)) + + results = ba.GameResults() + + for team in self.teams: + if team.time is not None: + # We store time in seconds, but pass a score in milliseconds. + results.set_team_score(team, int(team.time * 1000.0)) + else: + results.set_team_score(team, None) + + # We don't announce a winner in ffa mode since its probably been a + # while since the first place guy crossed the finish line so it seems + # odd to be announcing that now. + self.end(results=results, + announce_winning_team=isinstance(self.session, + ba.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment default behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if not player.finished: + self.respawn_player(player, respawn_time=1) + else: + super().handlemessage(msg) diff --git a/dist/ba_data/python/bastd/game/runaround.py b/dist/ba_data/python/bastd/game/runaround.py new file mode 100644 index 0000000..711ee46 --- /dev/null +++ b/dist/ba_data/python/bastd/game/runaround.py @@ -0,0 +1,1163 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the runaround co-op game.""" + +# We wear the cone of shame. +# pylint: disable=too-many-lines + +from __future__ import annotations + +import random +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING + +import ba +from bastd.actor.popuptext import PopupText +from bastd.actor.bomb import TNTSpawner +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.respawnicon import RespawnIcon +from bastd.actor.powerupbox import PowerupBox, PowerupBoxFactory +from bastd.gameutils import SharedObjects +from bastd.actor.spazbot import ( + SpazBotSet, SpazBot, SpazBotDiedMessage, BomberBot, BrawlerBot, TriggerBot, + TriggerBotPro, BomberBotProShielded, TriggerBotProShielded, ChargerBot, + ChargerBotProShielded, StickyBot, ExplodeyBot, BrawlerBotProShielded, + BomberBotPro, BrawlerBotPro) + +if TYPE_CHECKING: + from typing import Type, Any, List, Dict, Tuple, Sequence, Optional, Union + + +class Preset(Enum): + """Play presets.""" + ENDLESS = 'endless' + ENDLESS_TOURNAMENT = 'endless_tournament' + PRO = 'pro' + PRO_EASY = 'pro_easy' + UBER = 'uber' + UBER_EASY = 'uber_easy' + TOURNAMENT = 'tournament' + TOURNAMENT_UBER = 'tournament_uber' + + +class Point(Enum): + """Where we can spawn stuff and the corresponding map attr name.""" + BOTTOM_LEFT = 'bot_spawn_bottom_left' + BOTTOM_RIGHT = 'bot_spawn_bottom_right' + START = 'bot_spawn_start' + + +@dataclass +class Spawn: + """Defines a bot spawn event.""" + type: Type[SpazBot] + path: int = 0 + point: Optional[Point] = None + + +@dataclass +class Spacing: + """Defines spacing between spawns.""" + duration: float + + +@dataclass +class Wave: + """Defines a wave of enemies.""" + entries: List[Union[Spawn, Spacing, None]] + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.respawn_timer: Optional[ba.Timer] = None + self.respawn_icon: Optional[RespawnIcon] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +class RunaroundGame(ba.CoopGameActivity[Player, Team]): + """Game involving trying to bomb bots as they walk through the map.""" + + name = 'Runaround' + description = 'Prevent enemies from reaching the exit.' + tips = [ + 'Jump just as you\'re throwing to get bombs up to the highest levels.', + 'No, you can\'t get up on the ledge. You have to throw bombs.', + 'Whip back and forth to get more distance on your throws..' + ] + default_music = ba.MusicType.MARCHING + + # How fast our various bot types walk. + _bot_speed_map: Dict[Type[SpazBot], float] = { + BomberBot: 0.48, + BomberBotPro: 0.48, + BomberBotProShielded: 0.48, + BrawlerBot: 0.57, + BrawlerBotPro: 0.57, + BrawlerBotProShielded: 0.57, + TriggerBot: 0.73, + TriggerBotPro: 0.78, + TriggerBotProShielded: 0.78, + ChargerBot: 1.0, + ChargerBotProShielded: 1.0, + ExplodeyBot: 1.0, + StickyBot: 0.5 + } + + def __init__(self, settings: dict): + settings['map'] = 'Tower D' + super().__init__(settings) + shared = SharedObjects.get() + self._preset = Preset(settings.get('preset', 'pro')) + + self._player_death_sound = ba.getsound('playerDeath') + self._new_wave_sound = ba.getsound('scoreHit01') + self._winsound = ba.getsound('score') + self._cashregistersound = ba.getsound('cashRegister') + self._bad_guy_score_sound = ba.getsound('shieldDown') + self._heart_tex = ba.gettexture('heart') + self._heart_model_opaque = ba.getmodel('heartOpaque') + self._heart_model_transparent = ba.getmodel('heartTransparent') + + self._a_player_has_been_killed = False + self._spawn_center = self._map_type.defs.points['spawn1'][0:3] + self._tntspawnpos = self._map_type.defs.points['tnt_loc'][0:3] + self._powerup_center = self._map_type.defs.boxes['powerup_region'][0:3] + self._powerup_spread = ( + self._map_type.defs.boxes['powerup_region'][6] * 0.5, + self._map_type.defs.boxes['powerup_region'][8] * 0.5) + + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_reached_end), + )) + + self._last_wave_end_time = ba.time() + self._player_has_picked_up_powerup = False + self._scoreboard: Optional[Scoreboard] = None + self._game_over = False + self._wavenum = 0 + self._can_end_wave = True + self._score = 0 + self._time_bonus = 0 + self._score_region: Optional[ba.Actor] = None + self._dingsound = ba.getsound('dingSmall') + self._dingsoundhigh = ba.getsound('dingSmallHigh') + self._exclude_powerups: Optional[List[str]] = None + self._have_tnt: Optional[bool] = None + self._waves: Optional[List[Wave]] = None + self._bots = SpazBotSet() + self._tntspawner: Optional[TNTSpawner] = None + self._lives_bg: Optional[ba.NodeActor] = None + self._start_lives = 10 + self._lives = self._start_lives + self._lives_text: Optional[ba.NodeActor] = None + self._flawless = True + self._time_bonus_timer: Optional[ba.Timer] = None + self._time_bonus_text: Optional[ba.NodeActor] = None + self._time_bonus_mult: Optional[float] = None + self._wave_text: Optional[ba.NodeActor] = None + self._flawless_bonus: Optional[int] = None + self._wave_update_timer: Optional[ba.Timer] = None + + def on_transition_in(self) -> None: + super().on_transition_in() + self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), + score_split=0.5) + self._score_region = ba.NodeActor( + ba.newnode( + 'region', + attrs={ + 'position': self.map.defs.boxes['score_region'][0:3], + 'scale': self.map.defs.boxes['score_region'][6:9], + 'type': 'box', + 'materials': [self._score_region_material] + })) + + def on_begin(self) -> None: + super().on_begin() + player_count = len(self.players) + hard = self._preset not in {Preset.PRO_EASY, Preset.UBER_EASY} + + if self._preset in {Preset.PRO, Preset.PRO_EASY, Preset.TOURNAMENT}: + self._exclude_powerups = ['curse'] + self._have_tnt = True + self._waves = [ + Wave(entries=[ + Spawn(BomberBot, path=3 if hard else 2), + Spawn(BomberBot, path=2), + Spawn(BomberBot, path=2) if hard else None, + Spawn(BomberBot, path=2) if player_count > 1 else None, + Spawn(BomberBot, path=1) if hard else None, + Spawn(BomberBot, path=1) if player_count > 2 else None, + Spawn(BomberBot, path=1) if player_count > 3 else None, + ]), + Wave(entries=[ + Spawn(BomberBot, path=1) if hard else None, + Spawn(BomberBot, path=2) if hard else None, + Spawn(BomberBot, path=2), + Spawn(BomberBot, path=2), + Spawn(BomberBot, path=2) if player_count > 3 else None, + Spawn(BrawlerBot, path=3), + Spawn(BrawlerBot, path=3), + Spawn(BrawlerBot, path=3) if hard else None, + Spawn(BrawlerBot, path=3) if player_count > 1 else None, + Spawn(BrawlerBot, path=3) if player_count > 2 else None, + ]), + Wave(entries=[ + Spawn(ChargerBot, path=2) if hard else None, + Spawn(ChargerBot, path=2) if player_count > 2 else None, + Spawn(TriggerBot, path=2), + Spawn(TriggerBot, path=2) if player_count > 1 else None, + Spacing(duration=3.0), + Spawn(BomberBot, path=2) if hard else None, + Spawn(BomberBot, path=2) if hard else None, + Spawn(BomberBot, path=2), + Spawn(BomberBot, path=3) if hard else None, + Spawn(BomberBot, path=3), + Spawn(BomberBot, path=3), + Spawn(BomberBot, path=3) if player_count > 3 else None, + ]), + Wave(entries=[ + Spawn(TriggerBot, path=1) if hard else None, + Spacing(duration=1.0) if hard else None, + Spawn(TriggerBot, path=2), + Spacing(duration=1.0), + Spawn(TriggerBot, path=3), + Spacing(duration=1.0), + Spawn(TriggerBot, path=1) if hard else None, + Spacing(duration=1.0) if hard else None, + Spawn(TriggerBot, path=2), + Spacing(duration=1.0), + Spawn(TriggerBot, path=3), + Spacing(duration=1.0), + Spawn(TriggerBot, path=1) if ( + player_count > 1 and hard) else None, + Spacing(duration=1.0), + Spawn(TriggerBot, path=2) if player_count > 2 else None, + Spacing(duration=1.0), + Spawn(TriggerBot, path=3) if player_count > 3 else None, + Spacing(duration=1.0), + ]), + Wave(entries=[ + Spawn(ChargerBotProShielded if hard else ChargerBot, + path=1), + Spawn(BrawlerBot, path=2) if hard else None, + Spawn(BrawlerBot, path=2), + Spawn(BrawlerBot, path=2), + Spawn(BrawlerBot, path=3) if hard else None, + Spawn(BrawlerBot, path=3), + Spawn(BrawlerBot, path=3), + Spawn(BrawlerBot, path=3) if player_count > 1 else None, + Spawn(BrawlerBot, path=3) if player_count > 2 else None, + Spawn(BrawlerBot, path=3) if player_count > 3 else None, + ]), + Wave(entries=[ + Spawn(BomberBotProShielded, path=3), + Spacing(duration=1.5), + Spawn(BomberBotProShielded, path=2), + Spacing(duration=1.5), + Spawn(BomberBotProShielded, path=1) if hard else None, + Spacing(duration=1.0) if hard else None, + Spawn(BomberBotProShielded, path=3), + Spacing(duration=1.5), + Spawn(BomberBotProShielded, path=2), + Spacing(duration=1.5), + Spawn(BomberBotProShielded, path=1) if hard else None, + Spacing(duration=1.5) if hard else None, + Spawn(BomberBotProShielded, path=3 + ) if player_count > 1 else None, + Spacing(duration=1.5), + Spawn(BomberBotProShielded, path=2 + ) if player_count > 2 else None, + Spacing(duration=1.5), + Spawn(BomberBotProShielded, path=1 + ) if player_count > 3 else None, + ]), + ] + elif self._preset in { + Preset.UBER_EASY, Preset.UBER, Preset.TOURNAMENT_UBER + }: + self._exclude_powerups = [] + self._have_tnt = True + self._waves = [ + Wave(entries=[ + Spawn(TriggerBot, path=1) if hard else None, + Spawn(TriggerBot, path=2), + Spawn(TriggerBot, path=2), + Spawn(TriggerBot, path=3), + Spawn(BrawlerBotPro if hard else BrawlerBot, + point=Point.BOTTOM_LEFT), + Spawn(BrawlerBotPro, point=Point.BOTTOM_RIGHT + ) if player_count > 2 else None, + ]), + Wave(entries=[ + Spawn(ChargerBot, path=2), + Spawn(ChargerBot, path=3), + Spawn(ChargerBot, path=1) if hard else None, + Spawn(ChargerBot, path=2), + Spawn(ChargerBot, path=3), + Spawn(ChargerBot, path=1) if player_count > 2 else None, + ]), + Wave(entries=[ + Spawn(BomberBotProShielded, path=1) if hard else None, + Spawn(BomberBotProShielded, path=2), + Spawn(BomberBotProShielded, path=2), + Spawn(BomberBotProShielded, path=3), + Spawn(BomberBotProShielded, path=3), + Spawn(ChargerBot, point=Point.BOTTOM_RIGHT), + Spawn(ChargerBot, point=Point.BOTTOM_LEFT + ) if player_count > 2 else None, + ]), + Wave(entries=[ + Spawn(TriggerBotPro, path=1) if hard else None, + Spawn(TriggerBotPro, path=1 if hard else 2), + Spawn(TriggerBotPro, path=1 if hard else 2), + Spawn(TriggerBotPro, path=1 if hard else 2), + Spawn(TriggerBotPro, path=1 if hard else 2), + Spawn(TriggerBotPro, path=1 if hard else 2), + Spawn(TriggerBotPro, path=1 if hard else 2 + ) if player_count > 1 else None, + Spawn(TriggerBotPro, path=1 if hard else 2 + ) if player_count > 3 else None, + ]), + Wave(entries=[ + Spawn(TriggerBotProShielded if hard else TriggerBotPro, + point=Point.BOTTOM_LEFT), + Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT + ) if hard else None, + Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT + ) if player_count > 2 else None, + Spawn(BomberBot, path=3), + Spawn(BomberBot, path=3), + Spacing(duration=5.0), + Spawn(BrawlerBot, path=2), + Spawn(BrawlerBot, path=2), + Spacing(duration=5.0), + Spawn(TriggerBot, path=1) if hard else None, + Spawn(TriggerBot, path=1) if hard else None, + ]), + Wave(entries=[ + Spawn(BomberBotProShielded, path=2), + Spawn(BomberBotProShielded, path=2) if hard else None, + Spawn(StickyBot, point=Point.BOTTOM_RIGHT), + Spawn(BomberBotProShielded, path=2), + Spawn(BomberBotProShielded, path=2), + Spawn(StickyBot, point=Point.BOTTOM_RIGHT + ) if player_count > 2 else None, + Spawn(BomberBotProShielded, path=2), + Spawn(ExplodeyBot, point=Point.BOTTOM_LEFT), + Spawn(BomberBotProShielded, path=2), + Spawn(BomberBotProShielded, path=2 + ) if player_count > 1 else None, + Spacing(duration=5.0), + Spawn(StickyBot, point=Point.BOTTOM_LEFT), + Spacing(duration=2.0), + Spawn(ExplodeyBot, point=Point.BOTTOM_RIGHT), + ]), + ] + elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: + self._exclude_powerups = [] + self._have_tnt = True + + # Spit out a few powerups and start dropping more shortly. + self._drop_powerups(standard_points=True) + ba.timer(4.0, self._start_powerup_drops) + self.setup_low_life_warning_sound() + self._update_scores() + + # Our TNT spawner (if applicable). + if self._have_tnt: + self._tntspawner = TNTSpawner(position=self._tntspawnpos) + + # Make sure to stay out of the way of menu/party buttons in the corner. + uiscale = ba.app.ui.uiscale + l_offs = (-80 if uiscale is ba.UIScale.SMALL else + -40 if uiscale is ba.UIScale.MEDIUM else 0) + + self._lives_bg = ba.NodeActor( + ba.newnode('image', + attrs={ + 'texture': self._heart_tex, + 'model_opaque': self._heart_model_opaque, + 'model_transparent': self._heart_model_transparent, + 'attach': 'topRight', + 'scale': (90, 90), + 'position': (-110 + l_offs, -50), + 'color': (1, 0.2, 0.2) + })) + # FIXME; should not set things based on vr mode. + # (won't look right to non-vr connected clients, etc) + vrmode = ba.app.vr_mode + self._lives_text = ba.NodeActor( + ba.newnode( + 'text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'right', + 'h_align': 'center', + 'color': (1, 1, 1, 1) if vrmode else (0.8, 0.8, 0.8, 1.0), + 'flatness': 1.0 if vrmode else 0.5, + 'shadow': 1.0 if vrmode else 0.5, + 'vr_depth': 10, + 'position': (-113 + l_offs, -69), + 'scale': 1.3, + 'text': str(self._lives) + })) + + ba.timer(2.0, self._start_updating_waves) + + def _handle_reached_end(self) -> None: + spaz = ba.getcollision().opposingnode.getdelegate(SpazBot, True) + if not spaz.is_alive(): + return # Ignore bodies flying in. + + self._flawless = False + pos = spaz.node.position + ba.playsound(self._bad_guy_score_sound, position=pos) + light = ba.newnode('light', + attrs={ + 'position': pos, + 'radius': 0.5, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False) + ba.timer(1.0, light.delete) + spaz.handlemessage( + ba.DieMessage(immediate=True, how=ba.DeathType.REACHED_GOAL)) + + if self._lives > 0: + self._lives -= 1 + if self._lives == 0: + self._bots.stop_moving() + self.continue_or_end_game() + assert self._lives_text is not None + assert self._lives_text.node + self._lives_text.node.text = str(self._lives) + delay = 0.0 + + def _safesetattr(node: ba.Node, attr: str, value: Any) -> None: + if node: + setattr(node, attr, value) + + for _i in range(4): + ba.timer( + delay, + ba.Call(_safesetattr, self._lives_text.node, 'color', + (1, 0, 0, 1.0))) + assert self._lives_bg is not None + assert self._lives_bg.node + ba.timer( + delay, + ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5)) + delay += 0.125 + ba.timer( + delay, + ba.Call(_safesetattr, self._lives_text.node, 'color', + (1.0, 1.0, 0.0, 1.0))) + ba.timer( + delay, + ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0)) + delay += 0.125 + ba.timer( + delay, + ba.Call(_safesetattr, self._lives_text.node, 'color', + (0.8, 0.8, 0.8, 1.0))) + + def on_continue(self) -> None: + self._lives = 3 + assert self._lives_text is not None + assert self._lives_text.node + self._lives_text.node.text = str(self._lives) + self._bots.start_moving() + + def spawn_player(self, player: Player) -> ba.Actor: + pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), + self._spawn_center[1], + self._spawn_center[2] + random.uniform(-1.5, 1.5)) + spaz = self.spawn_player_spaz(player, position=pos) + if self._preset in {Preset.PRO_EASY, Preset.UBER_EASY}: + spaz.impact_scale = 0.25 + + # Add the material that causes us to hit the player-wall. + spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup + return spaz + + def _on_player_picked_up_powerup(self, player: ba.Actor) -> None: + del player # Unused. + self._player_has_picked_up_powerup = True + + def _drop_powerup(self, index: int, poweruptype: str = None) -> None: + if poweruptype is None: + poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=self._exclude_powerups)) + PowerupBox(position=self.map.powerup_spawn_points[index], + poweruptype=poweruptype).autoretain() + + def _start_powerup_drops(self) -> None: + ba.timer(3.0, self._drop_powerups, repeat=True) + + def _drop_powerups(self, + standard_points: bool = False, + force_first: str = None) -> None: + """Generic powerup drop.""" + + # If its been a minute since our last wave finished emerging, stop + # giving out land-mine powerups. (prevents players from waiting + # around for them on purpose and filling the map up) + if ba.time() - self._last_wave_end_time > 60.0: + extra_excludes = ['land_mines'] + else: + extra_excludes = [] + + if standard_points: + points = self.map.powerup_spawn_points + for i in range(len(points)): + ba.timer( + 1.0 + i * 0.5, + ba.Call(self._drop_powerup, i, + force_first if i == 0 else None)) + else: + pos = (self._powerup_center[0] + random.uniform( + -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), + self._powerup_center[1], + self._powerup_center[2] + random.uniform( + -self._powerup_spread[1], self._powerup_spread[1])) + + # drop one random one somewhere.. + assert self._exclude_powerups is not None + PowerupBox( + position=pos, + poweruptype=PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=self._exclude_powerups + + extra_excludes)).autoretain() + + def end_game(self) -> None: + ba.pushcall(ba.Call(self.do_end, 'defeat')) + ba.setmusic(None) + ba.playsound(self._player_death_sound) + + def do_end(self, outcome: str) -> None: + """End the game now with the provided outcome.""" + + if outcome == 'defeat': + delay = 2.0 + self.fade_to_red() + else: + delay = 0 + + score: Optional[int] + if self._wavenum >= 2: + score = self._score + fail_message = None + else: + score = None + fail_message = 'Reach wave 2 to rank.' + + self.end(delay=delay, + results={ + 'outcome': outcome, + 'score': score, + 'fail_message': fail_message, + 'playerinfos': self.initialplayerinfos + }) + + def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: + self._show_standard_scores_to_beat_ui(scores) + + def _update_waves(self) -> None: + # pylint: disable=too-many-branches + + # If we have no living bots, go to the next wave. + if (self._can_end_wave and not self._bots.have_living_bots() + and not self._game_over and self._lives > 0): + + self._can_end_wave = False + self._time_bonus_timer = None + self._time_bonus_text = None + + if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: + won = False + else: + assert self._waves is not None + won = (self._wavenum == len(self._waves)) + + # Reward time bonus. + base_delay = 4.0 if won else 0 + if self._time_bonus > 0: + ba.timer(0, ba.Call(ba.playsound, self._cashregistersound)) + ba.timer(base_delay, + ba.Call(self._award_time_bonus, self._time_bonus)) + base_delay += 1.0 + + # Reward flawless bonus. + if self._wavenum > 0 and self._flawless: + ba.timer(base_delay, self._award_flawless_bonus) + base_delay += 1.0 + + self._flawless = True # reset + + if won: + + # Completion achievements: + if self._preset in {Preset.PRO, Preset.PRO_EASY}: + self._award_achievement('Pro Runaround Victory', + sound=False) + if self._lives == self._start_lives: + self._award_achievement('The Wall', sound=False) + if not self._player_has_picked_up_powerup: + self._award_achievement('Precision Bombing', + sound=False) + elif self._preset in {Preset.UBER, Preset.UBER_EASY}: + self._award_achievement('Uber Runaround Victory', + sound=False) + if self._lives == self._start_lives: + self._award_achievement('The Great Wall', sound=False) + if not self._a_player_has_been_killed: + self._award_achievement('Stayin\' Alive', sound=False) + + # Give remaining players some points and have them celebrate. + self.show_zoom_message(ba.Lstr(resource='victoryText'), + scale=1.0, + duration=4.0) + + self.celebrate(10.0) + ba.timer(base_delay, self._award_lives_bonus) + base_delay += 1.0 + ba.timer(base_delay, self._award_completion_bonus) + base_delay += 0.85 + ba.playsound(self._winsound) + ba.cameraflash() + ba.setmusic(ba.MusicType.VICTORY) + self._game_over = True + ba.timer(base_delay, ba.Call(self.do_end, 'victory')) + return + + self._wavenum += 1 + + # Short celebration after waves. + if self._wavenum > 1: + self.celebrate(0.5) + + ba.timer(base_delay, self._start_next_wave) + + def _award_completion_bonus(self) -> None: + bonus = 200 + ba.playsound(self._cashregistersound) + PopupText(ba.Lstr(value='+${A} ${B}', + subs=[('${A}', str(bonus)), + ('${B}', + ba.Lstr(resource='completionBonusText'))]), + color=(0.7, 0.7, 1.0, 1), + scale=1.6, + position=(0, 1.5, -1)).autoretain() + self._score += bonus + self._update_scores() + + def _award_lives_bonus(self) -> None: + bonus = self._lives * 30 + ba.playsound(self._cashregistersound) + PopupText(ba.Lstr(value='+${A} ${B}', + subs=[('${A}', str(bonus)), + ('${B}', ba.Lstr(resource='livesBonusText'))]), + color=(0.7, 1.0, 0.3, 1), + scale=1.3, + position=(0, 1, -1)).autoretain() + self._score += bonus + self._update_scores() + + def _award_time_bonus(self, bonus: int) -> None: + ba.playsound(self._cashregistersound) + PopupText(ba.Lstr(value='+${A} ${B}', + subs=[('${A}', str(bonus)), + ('${B}', ba.Lstr(resource='timeBonusText'))]), + color=(1, 1, 0.5, 1), + scale=1.0, + position=(0, 3, -1)).autoretain() + + self._score += self._time_bonus + self._update_scores() + + def _award_flawless_bonus(self) -> None: + ba.playsound(self._cashregistersound) + PopupText(ba.Lstr(value='+${A} ${B}', + subs=[('${A}', str(self._flawless_bonus)), + ('${B}', ba.Lstr(resource='perfectWaveText')) + ]), + color=(1, 1, 0.2, 1), + scale=1.2, + position=(0, 2, -1)).autoretain() + + assert self._flawless_bonus is not None + self._score += self._flawless_bonus + self._update_scores() + + def _start_time_bonus_timer(self) -> None: + self._time_bonus_timer = ba.Timer(1.0, + self._update_time_bonus, + repeat=True) + + def _start_next_wave(self) -> None: + # FIXME: Need to split this up. + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + self.show_zoom_message(ba.Lstr(value='${A} ${B}', + subs=[('${A}', + ba.Lstr(resource='waveText')), + ('${B}', str(self._wavenum))]), + scale=1.0, + duration=1.0, + trail=True) + ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound)) + t_sec = 0.0 + base_delay = 0.5 + delay = 0.0 + bot_types: List[Union[Spawn, Spacing, None]] = [] + + if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: + level = self._wavenum + target_points = (level + 1) * 8.0 + group_count = random.randint(1, 3) + entries: List[Union[Spawn, Spacing, None]] = [] + spaz_types: List[Tuple[Type[SpazBot], float]] = [] + if level < 6: + spaz_types += [(BomberBot, 5.0)] + if level < 10: + spaz_types += [(BrawlerBot, 5.0)] + if level < 15: + spaz_types += [(TriggerBot, 6.0)] + if level > 5: + spaz_types += [(TriggerBotPro, 7.5)] * (1 + (level - 5) // 7) + if level > 2: + spaz_types += [(BomberBotProShielded, 8.0) + ] * (1 + (level - 2) // 6) + if level > 6: + spaz_types += [(TriggerBotProShielded, 12.0) + ] * (1 + (level - 6) // 5) + if level > 1: + spaz_types += ([(ChargerBot, 10.0)] * (1 + (level - 1) // 4)) + if level > 7: + spaz_types += [(ChargerBotProShielded, 15.0) + ] * (1 + (level - 7) // 3) + + # Bot type, their effect on target points. + defender_types: List[Tuple[Type[SpazBot], float]] = [ + (BomberBot, 0.9), + (BrawlerBot, 0.9), + (TriggerBot, 0.85), + ] + if level > 2: + defender_types += [(ChargerBot, 0.75)] + if level > 4: + defender_types += ([(StickyBot, 0.7)] * (1 + (level - 5) // 6)) + if level > 6: + defender_types += ([(ExplodeyBot, 0.7)] * (1 + + (level - 5) // 5)) + if level > 8: + defender_types += ([(BrawlerBotProShielded, 0.65)] * + (1 + (level - 5) // 4)) + if level > 10: + defender_types += ([(TriggerBotProShielded, 0.6)] * + (1 + (level - 6) // 3)) + + for group in range(group_count): + this_target_point_s = target_points / group_count + + # Adding spacing makes things slightly harder. + rval = random.random() + if rval < 0.07: + spacing = 1.5 + this_target_point_s *= 0.85 + elif rval < 0.15: + spacing = 1.0 + this_target_point_s *= 0.9 + else: + spacing = 0.0 + + path = random.randint(1, 3) + + # Don't allow hard paths on early levels. + if level < 3: + if path == 1: + path = 3 + + # Easy path. + if path == 3: + pass + + # Harder path. + elif path == 2: + this_target_point_s *= 0.8 + + # Even harder path. + elif path == 1: + this_target_point_s *= 0.7 + + # Looping forward. + elif path == 4: + this_target_point_s *= 0.7 + + # Looping backward. + elif path == 5: + this_target_point_s *= 0.7 + + # Random. + elif path == 6: + this_target_point_s *= 0.7 + + def _add_defender(defender_type: Tuple[Type[SpazBot], float], + pnt: Point) -> Tuple[float, Spawn]: + # This is ok because we call it immediately. + # pylint: disable=cell-var-from-loop + return this_target_point_s * defender_type[1], Spawn( + defender_type[0], point=pnt) + + # Add defenders. + defender_type1 = defender_types[random.randrange( + len(defender_types))] + defender_type2 = defender_types[random.randrange( + len(defender_types))] + defender1 = defender2 = None + if ((group == 0) or (group == 1 and level > 3) + or (group == 2 and level > 5)): + if random.random() < min(0.75, (level - 1) * 0.11): + this_target_point_s, defender1 = _add_defender( + defender_type1, Point.BOTTOM_LEFT) + if random.random() < min(0.75, (level - 1) * 0.04): + this_target_point_s, defender2 = _add_defender( + defender_type2, Point.BOTTOM_RIGHT) + + spaz_type = spaz_types[random.randrange(len(spaz_types))] + member_count = max( + 1, int(round(this_target_point_s / spaz_type[1]))) + for i, _member in enumerate(range(member_count)): + if path == 4: + this_path = i % 3 # Looping forward. + elif path == 5: + this_path = 3 - (i % 3) # Looping backward. + elif path == 6: + this_path = random.randint(1, 3) # Random. + else: + this_path = path + entries.append(Spawn(spaz_type[0], path=this_path)) + if spacing != 0.0: + entries.append(Spacing(duration=spacing)) + + if defender1 is not None: + entries.append(defender1) + if defender2 is not None: + entries.append(defender2) + + # Some spacing between groups. + rval = random.random() + if rval < 0.1: + spacing = 5.0 + elif rval < 0.5: + spacing = 1.0 + else: + spacing = 1.0 + entries.append(Spacing(duration=spacing)) + + wave = Wave(entries=entries) + + else: + assert self._waves is not None + wave = self._waves[self._wavenum - 1] + + bot_types += wave.entries + self._time_bonus_mult = 1.0 + this_flawless_bonus = 0 + non_runner_spawn_time = 1.0 + + for info in bot_types: + if info is None: + continue + if isinstance(info, Spacing): + t_sec += info.duration + continue + bot_type = info.type + path = info.path + self._time_bonus_mult += bot_type.points_mult * 0.02 + this_flawless_bonus += bot_type.points_mult * 5 + + # If its got a position, use that. + if info.point is not None: + point = info.point + else: + point = Point.START + + # Space our our slower bots. + delay = base_delay + delay /= self._get_bot_speed(bot_type) + t_sec += delay * 0.5 + tcall = ba.Call( + self.add_bot_at_point, point, bot_type, path, + 0.1 if point is Point.START else non_runner_spawn_time) + ba.timer(t_sec, tcall) + t_sec += delay * 0.5 + + # We can end the wave after all the spawning happens. + ba.timer(t_sec - delay * 0.5 + non_runner_spawn_time + 0.01, + self._set_can_end_wave) + + # Reset our time bonus. + # In this game we use a constant time bonus so it erodes away in + # roughly the same time (since the time limit a wave can take is + # relatively constant) ..we then post-multiply a modifier to adjust + # points. + self._time_bonus = 150 + self._flawless_bonus = this_flawless_bonus + assert self._time_bonus_mult is not None + txtval = ba.Lstr( + value='${A}: ${B}', + subs=[('${A}', ba.Lstr(resource='timeBonusText')), + ('${B}', str(int(self._time_bonus * self._time_bonus_mult))) + ]) + self._time_bonus_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.0, 1), + 'shadow': 1.0, + 'vr_depth': -30, + 'flatness': 1.0, + 'position': (0, -60), + 'scale': 0.8, + 'text': txtval + })) + + ba.timer(t_sec, self._start_time_bonus_timer) + + # Keep track of when this wave finishes emerging. We wanna stop + # dropping land-mines powerups at some point (otherwise a crafty + # player could fill the whole map with them) + self._last_wave_end_time = ba.time() + t_sec + totalwaves = str(len(self._waves)) if self._waves is not None else '??' + txtval = ba.Lstr(value='${A} ${B}', + subs=[('${A}', ba.Lstr(resource='waveText')), + ('${B}', + str(self._wavenum) + ('' if self._preset in { + Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT + } else f'/{totalwaves}'))]) + self._wave_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'vr_depth': -10, + 'color': (1, 1, 1, 1), + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0, -40), + 'scale': 1.3, + 'text': txtval + })) + + def _on_bot_spawn(self, path: int, spaz: SpazBot) -> None: + + # Add our custom update callback and set some info for this bot. + spaz_type = type(spaz) + assert spaz is not None + spaz.update_callback = self._update_bot + + # Tack some custom attrs onto the spaz. + setattr(spaz, 'r_walk_row', path) + setattr(spaz, 'r_walk_speed', self._get_bot_speed(spaz_type)) + + def add_bot_at_point(self, + point: Point, + spaztype: Type[SpazBot], + path: int, + spawn_time: float = 0.1) -> None: + """Add the given type bot with the given delay (in seconds).""" + + # Don't add if the game has ended. + if self._game_over: + return + pos = self.map.defs.points[point.value][:3] + self._bots.spawn_bot(spaztype, + pos=pos, + spawn_time=spawn_time, + on_spawn_call=ba.Call(self._on_bot_spawn, path)) + + def _update_time_bonus(self) -> None: + self._time_bonus = int(self._time_bonus * 0.91) + if self._time_bonus > 0 and self._time_bonus_text is not None: + assert self._time_bonus_text.node + assert self._time_bonus_mult + self._time_bonus_text.node.text = ba.Lstr( + value='${A}: ${B}', + subs=[('${A}', ba.Lstr(resource='timeBonusText')), + ('${B}', + str(int(self._time_bonus * self._time_bonus_mult)))]) + else: + self._time_bonus_text = None + + def _start_updating_waves(self) -> None: + self._wave_update_timer = ba.Timer(2.0, + self._update_waves, + repeat=True) + + def _update_scores(self) -> None: + score = self._score + if self._preset is Preset.ENDLESS: + if score >= 500: + self._award_achievement('Runaround Master') + if score >= 1000: + self._award_achievement('Runaround Wizard') + if score >= 2000: + self._award_achievement('Runaround God') + + assert self._scoreboard is not None + self._scoreboard.set_team_value(self.teams[0], score, max_score=None) + + def _update_bot(self, bot: SpazBot) -> bool: + # Yup; that's a lot of return statements right there. + # pylint: disable=too-many-return-statements + + if not bool(bot): + return True + + assert bot.node + + # FIXME: Do this in a type safe way. + r_walk_speed: float = getattr(bot, 'r_walk_speed') + r_walk_row: int = getattr(bot, 'r_walk_row') + + speed = r_walk_speed + pos = bot.node.position + boxes = self.map.defs.boxes + + # Bots in row 1 attempt the high road.. + if r_walk_row == 1: + if ba.is_point_in_box(pos, boxes['b4']): + bot.node.move_up_down = speed + bot.node.move_left_right = 0 + bot.node.run = 0.0 + return True + + # Row 1 and 2 bots attempt the middle road.. + if r_walk_row in [1, 2]: + if ba.is_point_in_box(pos, boxes['b1']): + bot.node.move_up_down = speed + bot.node.move_left_right = 0 + bot.node.run = 0.0 + return True + + # All bots settle for the third row. + if ba.is_point_in_box(pos, boxes['b7']): + bot.node.move_up_down = speed + bot.node.move_left_right = 0 + bot.node.run = 0.0 + return True + if ba.is_point_in_box(pos, boxes['b2']): + bot.node.move_up_down = -speed + bot.node.move_left_right = 0 + bot.node.run = 0.0 + return True + if ba.is_point_in_box(pos, boxes['b3']): + bot.node.move_up_down = -speed + bot.node.move_left_right = 0 + bot.node.run = 0.0 + return True + if ba.is_point_in_box(pos, boxes['b5']): + bot.node.move_up_down = -speed + bot.node.move_left_right = 0 + bot.node.run = 0.0 + return True + if ba.is_point_in_box(pos, boxes['b6']): + bot.node.move_up_down = speed + bot.node.move_left_right = 0 + bot.node.run = 0.0 + return True + if ((ba.is_point_in_box(pos, boxes['b8']) + and not ba.is_point_in_box(pos, boxes['b9'])) + or pos == (0.0, 0.0, 0.0)): + + # Default to walking right if we're still in the walking area. + bot.node.move_left_right = speed + bot.node.move_up_down = 0 + bot.node.run = 0.0 + return True + + # Revert to normal bot behavior otherwise.. + return False + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerScoredMessage): + self._score += msg.score + self._update_scores() + + elif isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + + self._a_player_has_been_killed = True + + # Respawn them shortly. + player = msg.getplayer(Player) + assert self.initialplayerinfos is not None + respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 + player.respawn_timer = ba.Timer( + respawn_time, ba.Call(self.spawn_player_if_exists, player)) + player.respawn_icon = RespawnIcon(player, respawn_time) + + elif isinstance(msg, SpazBotDiedMessage): + if msg.how is ba.DeathType.REACHED_GOAL: + return None + pts, importance = msg.spazbot.get_death_points(msg.how) + if msg.killerplayer is not None: + target: Optional[Sequence[float]] + try: + assert msg.spazbot is not None + assert msg.spazbot.node + target = msg.spazbot.node.position + except Exception: + ba.print_exception() + target = None + try: + if msg.killerplayer: + self.stats.player_scored(msg.killerplayer, + pts, + target=target, + kill=True, + screenmessage=False, + importance=importance) + ba.playsound(self._dingsound if importance == 1 else + self._dingsoundhigh, + volume=0.6) + except Exception: + ba.print_exception('Error on SpazBotDiedMessage.') + + # Normally we pull scores from the score-set, but if there's no + # player lets be explicit. + else: + self._score += pts + self._update_scores() + + else: + return super().handlemessage(msg) + return None + + def _get_bot_speed(self, bot_type: Type[SpazBot]) -> float: + speed = self._bot_speed_map.get(bot_type) + if speed is None: + raise TypeError('Invalid bot type to _get_bot_speed(): ' + + str(bot_type)) + return speed + + def _set_can_end_wave(self) -> None: + self._can_end_wave = True diff --git a/dist/ba_data/python/bastd/game/targetpractice.py b/dist/ba_data/python/bastd/game/targetpractice.py new file mode 100644 index 0000000..91d0e13 --- /dev/null +++ b/dist/ba_data/python/bastd/game/targetpractice.py @@ -0,0 +1,365 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Implements Target Practice game.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import ba +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.onscreencountdown import OnScreenCountdown +from bastd.actor.bomb import Bomb +from bastd.actor.popuptext import PopupText + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Optional, Sequence + from bastd.actor.bomb import Blast + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.streak = 0 + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): + """Game where players try to hit targets with bombs.""" + + name = 'Target Practice' + description = 'Bomb as many targets as you can.' + available_settings = [ + ba.IntSetting('Target Count', min_value=1, default=3), + ba.BoolSetting('Enable Impact Bombs', default=True), + ba.BoolSetting('Enable Triple Bombs', default=True) + ] + default_music = ba.MusicType.FORWARD_MARCH + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Doom Shroom'] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + # We support any teams or versus sessions. + return (issubclass(sessiontype, ba.CoopSession) + or issubclass(sessiontype, ba.MultiTeamSession)) + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._targets: List[Target] = [] + self._update_timer: Optional[ba.Timer] = None + self._countdown: Optional[OnScreenCountdown] = None + self._target_count = int(settings['Target Count']) + self._enable_impact_bombs = bool(settings['Enable Impact Bombs']) + self._enable_triple_bombs = bool(settings['Enable Triple Bombs']) + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self.update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.update_scoreboard() + + # Number of targets is based on player count. + for i in range(self._target_count): + ba.timer(5.0 + i * 1.0, self._spawn_target) + + self._update_timer = ba.Timer(1.0, self._update, repeat=True) + self._countdown = OnScreenCountdown(60, endcall=self.end_game) + ba.timer(4.0, self._countdown.start) + + def spawn_player(self, player: Player) -> ba.Actor: + spawn_center = (0, 3, -5) + pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], + spawn_center[2] + random.uniform(-1.5, 1.5)) + + # Reset their streak. + player.streak = 0 + spaz = self.spawn_player_spaz(player, position=pos) + + # Give players permanent triple impact bombs and wire them up + # to tell us when they drop a bomb. + if self._enable_impact_bombs: + spaz.bomb_type = 'impact' + if self._enable_triple_bombs: + spaz.set_bomb_count(3) + spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb) + return spaz + + def _spawn_target(self) -> None: + + # Generate a few random points; we'll use whichever one is farthest + # from our existing targets (don't want overlapping targets). + points = [] + + for _i in range(4): + # Calc a random point within a circle. + while True: + xpos = random.uniform(-1.0, 1.0) + ypos = random.uniform(-1.0, 1.0) + if xpos * xpos + ypos * ypos < 1.0: + break + points.append(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) + + def get_min_dist_from_target(pnt: ba.Vec3) -> float: + return min((t.get_dist_from_point(pnt) for t in self._targets)) + + # If we have existing targets, use the point with the highest + # min-distance-from-targets. + if self._targets: + point = max(points, key=get_min_dist_from_target) + else: + point = points[0] + + self._targets.append(Target(position=point)) + + def _on_spaz_dropped_bomb(self, spaz: ba.Actor, bomb: ba.Actor) -> None: + del spaz # Unused. + + # Wire up this bomb to inform us when it blows up. + assert isinstance(bomb, Bomb) + bomb.add_explode_callback(self._on_bomb_exploded) + + def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None: + assert blast.node + pos = blast.node.position + + # Debugging: throw a locator down where we landed. + # ba.newnode('locator', attrs={'position':blast.node.position}) + + # Feed the explosion point to all our targets and get points in return. + # Note: we operate on a copy of self._targets since the list may change + # under us if we hit stuff (don't wanna get points for new targets). + player = bomb.get_source_player(Player) + if not player: + # It's possible the player left after throwing the bomb. + return + + bullseye = any( + target.do_hit_at_position(pos, player) + for target in list(self._targets)) + if bullseye: + player.streak += 1 + else: + player.streak = 0 + + def _update(self) -> None: + """Misc. periodic updating.""" + # Clear out targets that have died. + self._targets = [t for t in self._targets if t] + + def handlemessage(self, msg: Any) -> Any: + # When players die, respawn them. + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) # Do standard stuff. + player = msg.getplayer(Player) + assert player is not None + self.respawn_player(player) # Kick off a respawn. + elif isinstance(msg, Target.TargetHitMessage): + # A target is telling us it was hit and will die soon.. + # ..so make another one. + self._spawn_target() + else: + super().handlemessage(msg) + + def update_scoreboard(self) -> None: + """Update the game scoreboard with current team values.""" + for team in self.teams: + self._scoreboard.set_team_value(team, team.score) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results) + + +class Target(ba.Actor): + """A target practice target.""" + + class TargetHitMessage: + """Inform an object a target was hit.""" + + def __init__(self, position: Sequence[float]): + self._r1 = 0.45 + self._r2 = 1.1 + self._r3 = 2.0 + self._rfudge = 0.15 + super().__init__() + self._position = ba.Vec3(position) + self._hit = False + + # It can be handy to test with this on to make sure the projection + # isn't too far off from the actual object. + show_in_space = False + loc1 = ba.newnode('locator', + attrs={ + 'shape': 'circle', + 'position': position, + 'color': (0, 1, 0), + 'opacity': 0.5, + 'draw_beauty': show_in_space, + 'additive': True + }) + loc2 = ba.newnode('locator', + attrs={ + 'shape': 'circleOutline', + 'position': position, + 'color': (0, 1, 0), + 'opacity': 0.3, + 'draw_beauty': False, + 'additive': True + }) + loc3 = ba.newnode('locator', + attrs={ + 'shape': 'circleOutline', + 'position': position, + 'color': (0, 1, 0), + 'opacity': 0.1, + 'draw_beauty': False, + 'additive': True + }) + self._nodes = [loc1, loc2, loc3] + ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) + ba.animate_array(loc2, 'size', 1, { + 0.05: [0.0], + 0.25: [self._r2 * 2.0] + }) + ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) + ba.playsound(ba.getsound('laserReverse')) + + def exists(self) -> bool: + return bool(self._nodes) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + for node in self._nodes: + node.delete() + self._nodes = [] + else: + super().handlemessage(msg) + + def get_dist_from_point(self, pos: ba.Vec3) -> float: + """Given a point, returns distance squared from it.""" + return (pos - self._position).length() + + def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool: + """Handle a bomb hit at the given position.""" + # pylint: disable=too-many-statements + activity = self.activity + + # Ignore hits if the game is over or if we've already been hit + if activity.has_ended() or self._hit or not self._nodes: + return False + + diff = (ba.Vec3(pos) - self._position) + + # Disregard Y difference. Our target point probably isn't exactly + # on the ground anyway. + diff[1] = 0.0 + dist = diff.length() + + bullseye = False + if dist <= self._r3 + self._rfudge: + # Inform our activity that we were hit + self._hit = True + activity.handlemessage(self.TargetHitMessage()) + keys: Dict[float, Sequence[float]] = { + 0.0: (1.0, 0.0, 0.0), + 0.049: (1.0, 0.0, 0.0), + 0.05: (1.0, 1.0, 1.0), + 0.1: (0.0, 1.0, 0.0) + } + cdull = (0.3, 0.3, 0.3) + popupcolor: Sequence[float] + if dist <= self._r1 + self._rfudge: + bullseye = True + self._nodes[1].color = cdull + self._nodes[2].color = cdull + ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True) + popupscale = 1.8 + popupcolor = (1, 1, 0, 1) + streak = player.streak + points = 10 + min(20, streak * 2) + ba.playsound(ba.getsound('bellHigh')) + if streak > 0: + ba.playsound( + ba.getsound( + 'orchestraHit4' if streak > 3 else + 'orchestraHit3' if streak > 2 else + 'orchestraHit2' if streak > 1 else 'orchestraHit')) + elif dist <= self._r2 + self._rfudge: + self._nodes[0].color = cdull + self._nodes[2].color = cdull + ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True) + popupscale = 1.25 + popupcolor = (1, 0.5, 0.2, 1) + points = 4 + ba.playsound(ba.getsound('bellMed')) + else: + self._nodes[0].color = cdull + self._nodes[1].color = cdull + ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True) + popupscale = 1.0 + popupcolor = (0.8, 0.3, 0.3, 1) + points = 2 + ba.playsound(ba.getsound('bellLow')) + + # Award points/etc.. (technically should probably leave this up + # to the activity). + popupstr = '+' + str(points) + + # If there's more than 1 player in the game, include their + # names and colors so they know who got the hit. + if len(activity.players) > 1: + popupcolor = ba.safecolor(player.color, target_intensity=0.75) + popupstr += ' ' + player.getname() + PopupText(popupstr, + position=self._position, + color=popupcolor, + scale=popupscale).autoretain() + + # Give this player's team points and update the score-board. + player.team.score += points + assert isinstance(activity, TargetPracticeGame) + activity.update_scoreboard() + + # Also give this individual player points + # (only applies in teams mode). + assert activity.stats is not None + activity.stats.player_scored(player, + points, + showpoints=False, + screenmessage=False) + + ba.animate_array(self._nodes[0], 'size', 1, { + 0.8: self._nodes[0].size, + 1.0: [0.0] + }) + ba.animate_array(self._nodes[1], 'size', 1, { + 0.85: self._nodes[1].size, + 1.05: [0.0] + }) + ba.animate_array(self._nodes[2], 'size', 1, { + 0.9: self._nodes[2].size, + 1.1: [0.0] + }) + ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage())) + + return bullseye diff --git a/dist/ba_data/python/bastd/game/thelaststand.py b/dist/ba_data/python/bastd/game/thelaststand.py new file mode 100644 index 0000000..5a24cc4 --- /dev/null +++ b/dist/ba_data/python/bastd/game/thelaststand.py @@ -0,0 +1,299 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the last stand minigame.""" + +from __future__ import annotations + +import random +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.bomb import TNTSpawner +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBoxFactory, PowerupBox +from bastd.actor.spazbot import (SpazBotSet, SpazBotDiedMessage, BomberBot, + BomberBotPro, BomberBotProShielded, + BrawlerBot, BrawlerBotPro, + BrawlerBotProShielded, TriggerBot, + TriggerBotPro, TriggerBotProShielded, + ChargerBot, StickyBot, ExplodeyBot) + +if TYPE_CHECKING: + from typing import Any, Dict, Type, List, Optional, Sequence + from bastd.actor.spazbot import SpazBot + + +@dataclass +class SpawnInfo: + """Spawning info for a particular bot type.""" + spawnrate: float + increase: float + dincrease: float + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +class TheLastStandGame(ba.CoopGameActivity[Player, Team]): + """Slow motion how-long-can-you-last game.""" + + name = 'The Last Stand' + description = 'Final glorious epic slow motion battle to the death.' + tips = [ + 'This level never ends, but a high score here\n' + 'will earn you eternal respect throughout the world.' + ] + + # Show messages when players die since it matters here. + announce_player_deaths = True + + # And of course the most important part. + slow_motion = True + + default_music = ba.MusicType.EPIC + + def __init__(self, settings: dict): + settings['map'] = 'Rampage' + super().__init__(settings) + self._new_wave_sound = ba.getsound('scoreHit01') + self._winsound = ba.getsound('score') + self._cashregistersound = ba.getsound('cashRegister') + self._spawn_center = (0, 5.5, -4.14) + self._tntspawnpos = (0, 5.5, -6) + self._powerup_center = (0, 7, -4.14) + self._powerup_spread = (7, 2) + self._preset = str(settings.get('preset', 'default')) + self._excludepowerups: List[str] = [] + self._scoreboard: Optional[Scoreboard] = None + self._score = 0 + self._bots = SpazBotSet() + self._dingsound = ba.getsound('dingSmall') + self._dingsoundhigh = ba.getsound('dingSmallHigh') + self._tntspawner: Optional[TNTSpawner] = None + self._bot_update_interval: Optional[float] = None + self._bot_update_timer: Optional[ba.Timer] = None + self._powerup_drop_timer = None + + # For each bot type: [spawnrate, increase, d_increase] + self._bot_spawn_types = { + BomberBot: SpawnInfo(1.00, 0.00, 0.000), + BomberBotPro: SpawnInfo(0.00, 0.05, 0.001), + BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002), + BrawlerBot: SpawnInfo(1.00, 0.00, 0.000), + BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001), + BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), + TriggerBot: SpawnInfo(0.30, 0.00, 0.000), + TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001), + TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), + ChargerBot: SpawnInfo(0.30, 0.05, 0.000), + StickyBot: SpawnInfo(0.10, 0.03, 0.001), + ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002) + } # yapf: disable + + def on_transition_in(self) -> None: + super().on_transition_in() + ba.timer(1.3, ba.Call(ba.playsound, self._new_wave_sound)) + self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), + score_split=0.5) + + def on_begin(self) -> None: + super().on_begin() + + # Spit out a few powerups and start dropping more shortly. + self._drop_powerups(standard_points=True) + ba.timer(2.0, ba.WeakCall(self._start_powerup_drops)) + ba.timer(0.001, ba.WeakCall(self._start_bot_updates)) + self.setup_low_life_warning_sound() + self._update_scores() + self._tntspawner = TNTSpawner(position=self._tntspawnpos, + respawn_time=10.0) + + def spawn_player(self, player: Player) -> ba.Actor: + pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), + self._spawn_center[1], + self._spawn_center[2] + random.uniform(-1.5, 1.5)) + return self.spawn_player_spaz(player, position=pos) + + def _start_bot_updates(self) -> None: + self._bot_update_interval = 3.3 - 0.3 * (len(self.players)) + self._update_bots() + self._update_bots() + if len(self.players) > 2: + self._update_bots() + if len(self.players) > 3: + self._update_bots() + self._bot_update_timer = ba.Timer(self._bot_update_interval, + ba.WeakCall(self._update_bots)) + + def _drop_powerup(self, index: int, poweruptype: str = None) -> None: + if poweruptype is None: + poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=self._excludepowerups)) + PowerupBox(position=self.map.powerup_spawn_points[index], + poweruptype=poweruptype).autoretain() + + def _start_powerup_drops(self) -> None: + self._powerup_drop_timer = ba.Timer(3.0, + ba.WeakCall(self._drop_powerups), + repeat=True) + + def _drop_powerups(self, + standard_points: bool = False, + force_first: str = None) -> None: + """Generic powerup drop.""" + from bastd.actor import powerupbox + if standard_points: + pts = self.map.powerup_spawn_points + for i in range(len(pts)): + ba.timer( + 1.0 + i * 0.5, + ba.WeakCall(self._drop_powerup, i, + force_first if i == 0 else None)) + else: + drop_pt = (self._powerup_center[0] + random.uniform( + -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), + self._powerup_center[1], + self._powerup_center[2] + random.uniform( + -self._powerup_spread[1], self._powerup_spread[1])) + + # Drop one random one somewhere. + powerupbox.PowerupBox( + position=drop_pt, + poweruptype=PowerupBoxFactory.get().get_random_powerup_type( + excludetypes=self._excludepowerups)).autoretain() + + def do_end(self, outcome: str) -> None: + """End the game.""" + if outcome == 'defeat': + self.fade_to_red() + self.end(delay=2.0, + results={ + 'outcome': outcome, + 'score': self._score, + 'playerinfos': self.initialplayerinfos + }) + + def _update_bots(self) -> None: + assert self._bot_update_interval is not None + self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98) + self._bot_update_timer = ba.Timer(self._bot_update_interval, + ba.WeakCall(self._update_bots)) + botspawnpts: List[Sequence[float]] = [[-5.0, 5.5, -4.14], + [0.0, 5.5, -4.14], + [5.0, 5.5, -4.14]] + dists = [0.0, 0.0, 0.0] + playerpts: List[Sequence[float]] = [] + for player in self.players: + try: + if player.is_alive(): + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + playerpts.append(player.actor.node.position) + except Exception: + ba.print_exception('Error updating bots.') + for i in range(3): + for playerpt in playerpts: + dists[i] += abs(playerpt[0] - botspawnpts[i][0]) + dists[i] += random.random() * 5.0 # Minor random variation. + if dists[0] > dists[1] and dists[0] > dists[2]: + spawnpt = botspawnpts[0] + elif dists[1] > dists[2]: + spawnpt = botspawnpts[1] + else: + spawnpt = botspawnpts[2] + + spawnpt = (spawnpt[0] + 3.0 * (random.random() - 0.5), spawnpt[1], + 2.0 * (random.random() - 0.5) + spawnpt[2]) + + # Normalize our bot type total and find a random number within that. + total = 0.0 + for spawninfo in self._bot_spawn_types.values(): + total += spawninfo.spawnrate + randval = random.random() * total + + # Now go back through and see where this value falls. + total = 0 + bottype: Optional[Type[SpazBot]] = None + for spawntype, spawninfo in self._bot_spawn_types.items(): + total += spawninfo.spawnrate + if randval <= total: + bottype = spawntype + break + spawn_time = 1.0 + assert bottype is not None + self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time) + + # After every spawn we adjust our ratios slightly to get more + # difficult. + for spawninfo in self._bot_spawn_types.values(): + spawninfo.spawnrate += spawninfo.increase + spawninfo.increase += spawninfo.dincrease + + def _update_scores(self) -> None: + score = self._score + + # Achievements apply to the default preset only. + if self._preset == 'default': + if score >= 250: + self._award_achievement('Last Stand Master') + if score >= 500: + self._award_achievement('Last Stand Wizard') + if score >= 1000: + self._award_achievement('Last Stand God') + assert self._scoreboard is not None + self._scoreboard.set_team_value(self.teams[0], score, max_score=None) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + player = msg.getplayer(Player) + self.stats.player_was_killed(player) + ba.timer(0.1, self._checkroundover) + + elif isinstance(msg, ba.PlayerScoredMessage): + self._score += msg.score + self._update_scores() + + elif isinstance(msg, SpazBotDiedMessage): + pts, importance = msg.spazbot.get_death_points(msg.how) + target: Optional[Sequence[float]] + if msg.killerplayer: + assert msg.spazbot.node + target = msg.spazbot.node.position + self.stats.player_scored(msg.killerplayer, + pts, + target=target, + kill=True, + screenmessage=False, + importance=importance) + ba.playsound(self._dingsound + if importance == 1 else self._dingsoundhigh, + volume=0.6) + + # Normally we pull scores from the score-set, but if there's no + # player lets be explicit. + else: + self._score += pts + self._update_scores() + else: + super().handlemessage(msg) + + def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: + self._show_standard_scores_to_beat_ui(scores) + + def end_game(self) -> None: + # Tell our bots to celebrate just to rub it in. + self._bots.final_celebrate() + ba.setmusic(None) + ba.pushcall(ba.WeakCall(self.do_end, 'defeat')) + + def _checkroundover(self) -> None: + """End the round if conditions are met.""" + if not any(player.is_alive() for player in self.teams[0].players): + self.end_game() diff --git a/dist/ba_data/python/bastd/gameutils.py b/dist/ba_data/python/bastd/gameutils.py new file mode 100644 index 0000000..280e574 --- /dev/null +++ b/dist/ba_data/python/bastd/gameutils.py @@ -0,0 +1,145 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Various utilities useful for gameplay.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Sequence, Optional + + +class SharedObjects: + """Various common components for use in games. + + Category: Gameplay Classes + + Objects contained here are created on-demand as accessed and shared + by everything in the current activity. This includes things such as + standard materials. + """ + + _STORENAME = ba.storagename() + + def __init__(self) -> None: + activity = ba.getactivity() + if hasattr(activity, self._STORENAME): + raise RuntimeError('Use SharedObjects.get() to fetch the' + ' shared instance for this activity.') + self._object_material: Optional[ba.Material] = None + self._player_material: Optional[ba.Material] = None + self._pickup_material: Optional[ba.Material] = None + self._footing_material: Optional[ba.Material] = None + self._attack_material: Optional[ba.Material] = None + self._death_material: Optional[ba.Material] = None + self._region_material: Optional[ba.Material] = None + self._railing_material: Optional[ba.Material] = None + + @classmethod + def get(cls) -> SharedObjects: + """Fetch/create the instance of this class for the current activity.""" + activity = ba.getactivity() + shobs = activity.customdata.get(cls._STORENAME) + if shobs is None: + shobs = SharedObjects() + activity.customdata[cls._STORENAME] = shobs + assert isinstance(shobs, SharedObjects) + return shobs + + @property + def player_material(self) -> ba.Material: + """a ba.Material to be applied to player parts. Generally, + materials related to the process of scoring when reaching a goal, etc + will look for the presence of this material on things that hit them. + """ + if self._player_material is None: + self._player_material = ba.Material() + return self._player_material + + @property + def object_material(self) -> ba.Material: + """A ba.Material that should be applied to any small, + normal, physical objects such as bombs, boxes, players, etc. Other + materials often check for the presence of this material as a + prerequisite for performing certain actions (such as disabling + collisions between initially-overlapping objects) + """ + if self._object_material is None: + self._object_material = ba.Material() + return self._object_material + + @property + def pickup_material(self) -> ba.Material: + """A ba.Material; collision shapes used for picking things + up will have this material applied. To prevent an object from being + picked up, you can add a material that disables collisions against + things containing this material. + """ + if self._pickup_material is None: + self._pickup_material = ba.Material() + return self._pickup_material + + @property + def footing_material(self) -> ba.Material: + """Anything that can be 'walked on' should have this + ba.Material applied; generally just terrain and whatnot. A character + will snap upright whenever touching something with this material so it + should not be applied to props, etc. + """ + if self._footing_material is None: + self._footing_material = ba.Material() + return self._footing_material + + @property + def attack_material(self) -> ba.Material: + """A ba.Material applied to explosion shapes, punch + shapes, etc. An object not wanting to receive impulse/etc messages can + disable collisions against this material. + """ + if self._attack_material is None: + self._attack_material = ba.Material() + return self._attack_material + + @property + def death_material(self) -> ba.Material: + """A ba.Material that sends a ba.DieMessage() to anything + that touches it; handy for terrain below a cliff, etc. + """ + if self._death_material is None: + mat = self._death_material = ba.Material() + mat.add_actions( + ('message', 'their_node', 'at_connect', ba.DieMessage())) + return self._death_material + + @property + def region_material(self) -> ba.Material: + """A ba.Material used for non-physical collision shapes + (regions); collisions can generally be allowed with this material even + when initially overlapping since it is not physical. + """ + if self._region_material is None: + self._region_material = ba.Material() + return self._region_material + + @property + def railing_material(self) -> ba.Material: + """A ba.Material with a very low friction/stiffness/etc + that can be applied to invisible 'railings' useful for gently keeping + characters from falling off of cliffs. + """ + if self._railing_material is None: + mat = self._railing_material = ba.Material() + mat.add_actions(('modify_part_collision', 'collide', False)) + mat.add_actions(('modify_part_collision', 'stiffness', 0.003)) + mat.add_actions(('modify_part_collision', 'damping', 0.00001)) + mat.add_actions( + conditions=('they_have_material', self.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'friction', 0.0), + ), + ) + return self._railing_material diff --git a/dist/ba_data/python/bastd/keyboard/__init__.py b/dist/ba_data/python/bastd/keyboard/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/keyboard/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/keyboard/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/keyboard/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/keyboard/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/keyboard/__pycache__/englishkeyboard.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/keyboard/__pycache__/englishkeyboard.cpython-38.opt-1.pyc new file mode 100644 index 0000000..81da29f Binary files /dev/null and b/dist/ba_data/python/bastd/keyboard/__pycache__/englishkeyboard.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/keyboard/englishkeyboard.py b/dist/ba_data/python/bastd/keyboard/englishkeyboard.py new file mode 100644 index 0000000..a7e197d --- /dev/null +++ b/dist/ba_data/python/bastd/keyboard/englishkeyboard.py @@ -0,0 +1,55 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines a default keyboards.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Iterable, List, Tuple, Dict + + +def split(chars: Iterable[str], maxlen: int) -> List[List[str]]: + """Returns char groups with a fixed number of elements""" + result = [] + shatter: List[str] = [] + for i in chars: + if len(shatter) < maxlen: + shatter.append(i) + else: + result.append(shatter) + shatter = [i] + if shatter: + while len(shatter) < maxlen: + shatter.append('') + result.append(shatter) + return result + + +def generate_emojis(maxlen: int) -> List[List[str]]: + """Generates a lot of UTF8 emojis prepared for ba.Keyboard pages""" + all_emojis = split([chr(i) for i in range(0x1F601, 0x1F650)], maxlen) + all_emojis += split([chr(i) for i in range(0x2702, 0x27B1)], maxlen) + all_emojis += split([chr(i) for i in range(0x1F680, 0x1F6C1)], maxlen) + return all_emojis + + +# ba_meta export keyboard +class EnglishKeyboard(ba.Keyboard): + """Default English keyboard.""" + name = 'English' + chars = [('q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'), + ('a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'), + ('z', 'x', 'c', 'v', 'b', 'n', 'm')] + nums = ('1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '/', ':', + ';', '(', ')', '$', '&', '@', '"', '.', ',', '?', '!', '\'', '_') + pages: Dict[str, Tuple[str, ...]] = { + f'emoji{i}': tuple(page) + for i, page in enumerate(generate_emojis(len(nums))) + } diff --git a/dist/ba_data/python/bastd/mainmenu.py b/dist/ba_data/python/bastd/mainmenu.py new file mode 100644 index 0000000..c767015 --- /dev/null +++ b/dist/ba_data/python/bastd/mainmenu.py @@ -0,0 +1,940 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Session and Activity for displaying the main menu bg.""" + +from __future__ import annotations + +import random +import time +import weakref +from typing import TYPE_CHECKING + +import ba +import _ba + +if TYPE_CHECKING: + from typing import Any, List, Optional + +# FIXME: Clean this up if I ever revisit it. +# pylint: disable=attribute-defined-outside-init +# pylint: disable=too-many-branches +# pylint: disable=too-many-statements +# pylint: disable=too-many-locals +# noinspection PyUnreachableCode +# noinspection PyAttributeOutsideInit + + +class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): + """Activity showing the rotating main menu bg stuff.""" + + _stdassets = ba.Dependency(ba.AssetPackage, 'stdassets@1') + + def on_transition_in(self) -> None: + super().on_transition_in() + random.seed(123) + self._logo_node: Optional[ba.Node] = None + self._custom_logo_tex_name: Optional[str] = None + self._word_actors: List[ba.Actor] = [] + app = ba.app + + # FIXME: We shouldn't be doing things conditionally based on whether + # the host is VR mode or not (clients may differ in that regard). + # Any differences need to happen at the engine level so everyone + # sees things in their own optimal way. + vr_mode = ba.app.vr_mode + + if not ba.app.toolbar_test: + color = ((1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6)) + + # FIXME: Need a node attr for vr-specific-scale. + scale = (0.9 if + (app.ui.uiscale is ba.UIScale.SMALL or vr_mode) else 0.7) + self.my_name = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'bottom', + 'h_align': 'center', + 'color': color, + 'flatness': 1.0, + 'shadow': 1.0 if vr_mode else 0.5, + 'scale': scale, + 'position': (0, 10), + 'vr_depth': -10, + 'text': '\xa9 2011-2020 Eric Froemling' + })) + + # Throw up some text that only clients can see so they know that the + # host is navigating menus while they're just staring at an + # empty-ish screen. + tval = ba.Lstr(resource='hostIsNavigatingMenusText', + subs=[('${HOST}', _ba.get_account_display_string())]) + self._host_is_navigating_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'text': tval, + 'client_only': True, + 'position': (0, -200), + 'flatness': 1.0, + 'h_align': 'center' + })) + if not ba.app.main_menu_did_initial_transition and hasattr( + self, 'my_name'): + assert self.my_name.node + ba.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) + + # FIXME: We shouldn't be doing things conditionally based on whether + # the host is vr mode or not (clients may not be or vice versa). + # Any differences need to happen at the engine level so everyone sees + # things in their own optimal way. + vr_mode = app.vr_mode + uiscale = app.ui.uiscale + + # In cases where we're doing lots of dev work lets always show the + # build number. + force_show_build_number = False + + if not ba.app.toolbar_test: + if app.debug_build or app.test_build or force_show_build_number: + if app.debug_build: + text = ba.Lstr(value='${V} (${B}) (${D})', + subs=[ + ('${V}', app.version), + ('${B}', str(app.build_number)), + ('${D}', ba.Lstr(resource='debugText')), + ]) + else: + text = ba.Lstr(value='${V} (${B})', + subs=[ + ('${V}', app.version), + ('${B}', str(app.build_number)), + ]) + else: + text = ba.Lstr(value='${V}', subs=[('${V}', app.version)]) + scale = 0.9 if (uiscale is ba.UIScale.SMALL or vr_mode) else 0.7 + color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7) + self.version = ba.NodeActor( + ba.newnode( + 'text', + attrs={ + 'v_attach': 'bottom', + 'h_attach': 'right', + 'h_align': 'right', + 'flatness': 1.0, + 'vr_depth': -10, + 'shadow': 1.0 if vr_mode else 0.5, + 'color': color, + 'scale': scale, + 'position': (-260, 10) if vr_mode else (-10, 10), + 'text': text + })) + if not ba.app.main_menu_did_initial_transition: + assert self.version.node + ba.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0}) + + # Show the iircade logo on our iircade build. + if app.iircade_mode: + img = ba.NodeActor( + ba.newnode('image', + attrs={ + 'texture': ba.gettexture('iircadeLogo'), + 'attach': 'center', + 'scale': (250, 250), + 'position': (0, 0), + 'tilt_translate': 0.21, + 'absolute_scale': True + })).autoretain() + imgdelay = 0.0 if app.main_menu_did_initial_transition else 1.0 + ba.animate(img.node, 'opacity', { + imgdelay + 1.5: 0.0, + imgdelay + 2.5: 1.0 + }) + + # Throw in test build info. + self.beta_info = self.beta_info_2 = None + if app.test_build and not (app.demo_mode or app.arcade_mode): + pos = ((230, 125) if (app.demo_mode or app.arcade_mode) else + (230, 35)) + self.beta_info = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 1, 1), + 'shadow': 0.5, + 'flatness': 0.5, + 'scale': 1, + 'vr_depth': -60, + 'position': pos, + 'text': ba.Lstr(resource='testBuildText') + })) + if not ba.app.main_menu_did_initial_transition: + assert self.beta_info.node + ba.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) + + model = ba.getmodel('thePadLevel') + trees_model = ba.getmodel('trees') + bottom_model = ba.getmodel('thePadLevelBottom') + color_texture = ba.gettexture('thePadLevelColor') + trees_texture = ba.gettexture('treesColor') + bgtex = ba.gettexture('menuBG') + bgmodel = ba.getmodel('thePadBG') + + # Load these last since most platforms don't use them. + vr_bottom_fill_model = ba.getmodel('thePadVRFillBottom') + vr_top_fill_model = ba.getmodel('thePadVRFillTop') + + gnode = self.globalsnode + gnode.camera_mode = 'rotate' + + tint = (1.14, 1.1, 1.0) + gnode.tint = tint + gnode.ambient_color = (1.06, 1.04, 1.03) + gnode.vignette_outer = (0.45, 0.55, 0.54) + gnode.vignette_inner = (0.99, 0.98, 0.98) + + self.bottom = ba.NodeActor( + ba.newnode('terrain', + attrs={ + 'model': bottom_model, + 'lighting': False, + 'reflection': 'soft', + 'reflection_scale': [0.45], + 'color_texture': color_texture + })) + self.vr_bottom_fill = ba.NodeActor( + ba.newnode('terrain', + attrs={ + 'model': vr_bottom_fill_model, + 'lighting': False, + 'vr_only': True, + 'color_texture': color_texture + })) + self.vr_top_fill = ba.NodeActor( + ba.newnode('terrain', + attrs={ + 'model': vr_top_fill_model, + 'vr_only': True, + 'lighting': False, + 'color_texture': bgtex + })) + self.terrain = ba.NodeActor( + ba.newnode('terrain', + attrs={ + 'model': model, + 'color_texture': color_texture, + 'reflection': 'soft', + 'reflection_scale': [0.3] + })) + self.trees = ba.NodeActor( + ba.newnode('terrain', + attrs={ + 'model': trees_model, + 'lighting': False, + 'reflection': 'char', + 'reflection_scale': [0.1], + 'color_texture': trees_texture + })) + self.bgterrain = ba.NodeActor( + ba.newnode('terrain', + attrs={ + 'model': bgmodel, + 'color': (0.92, 0.91, 0.9), + 'lighting': False, + 'background': True, + 'color_texture': bgtex + })) + + self._ts = 0.86 + + self._language: Optional[str] = None + self._update_timer = ba.Timer(1.0, self._update, repeat=True) + self._update() + + # Hopefully this won't hitch but lets space these out anyway. + _ba.add_clean_frame_callback(ba.WeakCall(self._start_preloads)) + + random.seed() + + # On the main menu, also show our news. + class News: + """Wrangles news display.""" + + def __init__(self, activity: ba.Activity): + self._valid = True + self._message_duration = 10.0 + self._message_spacing = 2.0 + self._text: Optional[ba.NodeActor] = None + self._activity = weakref.ref(activity) + + # If we're signed in, fetch news immediately. + # Otherwise wait until we are signed in. + self._fetch_timer: Optional[ba.Timer] = ba.Timer( + 1.0, ba.WeakCall(self._try_fetching_news), repeat=True) + self._try_fetching_news() + + # We now want to wait until we're signed in before fetching news. + def _try_fetching_news(self) -> None: + if _ba.get_account_state() == 'signed_in': + self._fetch_news() + self._fetch_timer = None + + def _fetch_news(self) -> None: + ba.app.main_menu_last_news_fetch_time = time.time() + + # UPDATE - We now just pull news from MRVs. + news = _ba.get_account_misc_read_val('n', None) + if news is not None: + self._got_news(news) + + def _change_phrase(self) -> None: + from bastd.actor.text import Text + + # If our news is way out of date, lets re-request it; + # otherwise, rotate our phrase. + assert ba.app.main_menu_last_news_fetch_time is not None + if time.time() - ba.app.main_menu_last_news_fetch_time > 600.0: + self._fetch_news() + self._text = None + else: + if self._text is not None: + if not self._phrases: + for phr in self._used_phrases: + self._phrases.insert(0, phr) + val = self._phrases.pop() + if val == '__ACH__': + vrmode = app.vr_mode + Text(ba.Lstr(resource='nextAchievementsText'), + color=((1, 1, 1, 1) if vrmode else + (0.95, 0.9, 1, 0.4)), + host_only=True, + maxwidth=200, + position=(-300, -35), + h_align=Text.HAlign.RIGHT, + transition=Text.Transition.FADE_IN, + scale=0.9 if vrmode else 0.7, + flatness=1.0 if vrmode else 0.6, + shadow=1.0 if vrmode else 0.5, + h_attach=Text.HAttach.CENTER, + v_attach=Text.VAttach.TOP, + transition_delay=1.0, + transition_out_delay=self._message_duration + ).autoretain() + achs = [ + a for a in app.ach.achievements + if not a.complete + ] + if achs: + ach = achs.pop( + random.randrange(min(4, len(achs)))) + ach.create_display( + -180, + -35, + 1.0, + outdelay=self._message_duration, + style='news') + if achs: + ach = achs.pop( + random.randrange(min(8, len(achs)))) + ach.create_display( + 180, + -35, + 1.25, + outdelay=self._message_duration, + style='news') + else: + spc = self._message_spacing + keys = { + spc: 0.0, + spc + 1.0: 1.0, + spc + self._message_duration - 1.0: 1.0, + spc + self._message_duration: 0.0 + } + assert self._text.node + ba.animate(self._text.node, 'opacity', keys) + # {k: v + # for k, v in list(keys.items())}) + self._text.node.text = val + + def _got_news(self, news: str) -> None: + # Run this stuff in the context of our activity since we + # need to make nodes and stuff.. should fix the serverget + # call so it. + activity = self._activity() + if activity is None or activity.expired: + return + with ba.Context(activity): + + self._phrases: List[str] = [] + + # Show upcoming achievements in non-vr versions + # (currently too hard to read in vr). + self._used_phrases = ( + ['__ACH__'] if not ba.app.vr_mode else + []) + [s for s in news.split('
\n') if s != ''] + self._phrase_change_timer = ba.Timer( + (self._message_duration + self._message_spacing), + ba.WeakCall(self._change_phrase), + repeat=True) + + scl = 1.2 if (ba.app.ui.uiscale is ba.UIScale.SMALL + or ba.app.vr_mode) else 0.8 + + color2 = ((1, 1, 1, 1) if ba.app.vr_mode else + (0.7, 0.65, 0.75, 1.0)) + shadow = (1.0 if ba.app.vr_mode else 0.4) + self._text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'vr_depth': -20, + 'shadow': shadow, + 'flatness': 0.8, + 'v_align': 'top', + 'color': color2, + 'scale': scl, + 'maxwidth': 900.0 / scl, + 'position': (0, -10) + })) + self._change_phrase() + + if not (app.demo_mode or app.arcade_mode) and not app.toolbar_test: + self._news = News(self) + + # Bring up the last place we were, or start at the main menu otherwise. + with ba.Context('ui'): + from bastd.ui import specialoffer + if bool(False): + uicontroller = ba.app.ui.controller + assert uicontroller is not None + uicontroller.show_main_menu() + else: + main_menu_location = ba.app.ui.get_main_menu_location() + + # When coming back from a kiosk-mode game, jump to + # the kiosk start screen. + if ba.app.demo_mode or ba.app.arcade_mode: + # pylint: disable=cyclic-import + from bastd.ui.kiosk import KioskWindow + ba.app.ui.set_main_menu_window( + KioskWindow().get_root_widget()) + # ..or in normal cases go back to the main menu + else: + if main_menu_location == 'Gather': + # pylint: disable=cyclic-import + from bastd.ui.gather import GatherWindow + ba.app.ui.set_main_menu_window( + GatherWindow(transition=None).get_root_widget()) + elif main_menu_location == 'Watch': + # pylint: disable=cyclic-import + from bastd.ui.watch import WatchWindow + ba.app.ui.set_main_menu_window( + WatchWindow(transition=None).get_root_widget()) + elif main_menu_location == 'Team Game Select': + # pylint: disable=cyclic-import + from bastd.ui.playlist.browser import ( + PlaylistBrowserWindow) + ba.app.ui.set_main_menu_window( + PlaylistBrowserWindow( + sessiontype=ba.DualTeamSession, + transition=None).get_root_widget()) + elif main_menu_location == 'Free-for-All Game Select': + # pylint: disable=cyclic-import + from bastd.ui.playlist.browser import ( + PlaylistBrowserWindow) + ba.app.ui.set_main_menu_window( + PlaylistBrowserWindow( + sessiontype=ba.FreeForAllSession, + transition=None).get_root_widget()) + elif main_menu_location == 'Coop Select': + # pylint: disable=cyclic-import + from bastd.ui.coop.browser import CoopBrowserWindow + ba.app.ui.set_main_menu_window( + CoopBrowserWindow( + transition=None).get_root_widget()) + else: + # pylint: disable=cyclic-import + from bastd.ui.mainmenu import MainMenuWindow + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition=None).get_root_widget()) + + # attempt to show any pending offers immediately. + # If that doesn't work, try again in a few seconds + # (we may not have heard back from the server) + # ..if that doesn't work they'll just have to wait + # until the next opportunity. + if not specialoffer.show_offer(): + + def try_again() -> None: + if not specialoffer.show_offer(): + # Try one last time.. + ba.timer(2.0, + specialoffer.show_offer, + timetype=ba.TimeType.REAL) + + ba.timer(2.0, try_again, timetype=ba.TimeType.REAL) + ba.app.main_menu_did_initial_transition = True + + def _update(self) -> None: + app = ba.app + + # Update logo in case it changes. + if self._logo_node: + custom_texture = self._get_custom_logo_tex_name() + if custom_texture != self._custom_logo_tex_name: + self._custom_logo_tex_name = custom_texture + self._logo_node.texture = ba.gettexture( + custom_texture if custom_texture is not None else 'logo') + self._logo_node.model_opaque = (None + if custom_texture is not None + else ba.getmodel('logo')) + self._logo_node.model_transparent = ( + None if custom_texture is not None else + ba.getmodel('logoTransparent')) + + # If language has changed, recreate our logo text/graphics. + lang = app.lang.language + if lang != self._language: + self._language = lang + y = 20 + base_scale = 1.1 + self._word_actors = [] + base_delay = 1.0 + delay = base_delay + delay_inc = 0.02 + + # Come on faster after the first time. + if app.main_menu_did_initial_transition: + base_delay = 0.0 + delay = base_delay + delay_inc = 0.02 + + # We draw higher in kiosk mode (make sure to test this + # when making adjustments) for now we're hard-coded for + # a few languages.. should maybe look into generalizing this?.. + if app.lang.language == 'Chinese': + base_x = -270.0 + x = base_x - 20.0 + spacing = 85.0 * base_scale + y_extra = 0.0 if (app.demo_mode or app.arcade_mode) else 0.0 + self._make_logo(x - 110 + 50, + 113 + y + 1.2 * y_extra, + 0.34 * base_scale, + delay=base_delay + 0.1, + custom_texture='chTitleChar1', + jitter_scale=2.0, + vr_depth_offset=-30) + x += spacing + delay += delay_inc + self._make_logo(x - 10 + 50, + 110 + y + 1.2 * y_extra, + 0.31 * base_scale, + delay=base_delay + 0.15, + custom_texture='chTitleChar2', + jitter_scale=2.0, + vr_depth_offset=-30) + x += 2.0 * spacing + delay += delay_inc + self._make_logo(x + 180 - 140, + 110 + y + 1.2 * y_extra, + 0.3 * base_scale, + delay=base_delay + 0.25, + custom_texture='chTitleChar3', + jitter_scale=2.0, + vr_depth_offset=-30) + x += spacing + delay += delay_inc + self._make_logo(x + 241 - 120, + 110 + y + 1.2 * y_extra, + 0.31 * base_scale, + delay=base_delay + 0.3, + custom_texture='chTitleChar4', + jitter_scale=2.0, + vr_depth_offset=-30) + x += spacing + delay += delay_inc + self._make_logo(x + 300 - 90, + 105 + y + 1.2 * y_extra, + 0.34 * base_scale, + delay=base_delay + 0.35, + custom_texture='chTitleChar5', + jitter_scale=2.0, + vr_depth_offset=-30) + self._make_logo(base_x + 155, + 146 + y + 1.2 * y_extra, + 0.28 * base_scale, + delay=base_delay + 0.2, + rotate=-7) + else: + base_x = -170 + x = base_x - 20 + spacing = 55 * base_scale + y_extra = 0 if (app.demo_mode or app.arcade_mode) else 0 + xv1 = x + delay1 = delay + for shadow in (True, False): + x = xv1 + delay = delay1 + self._make_word('B', + x - 50, + y - 23 + 0.8 * y_extra, + scale=1.3 * base_scale, + delay=delay, + vr_depth_offset=3, + shadow=shadow) + x += spacing + delay += delay_inc + self._make_word('m', + x, + y + y_extra, + delay=delay, + scale=base_scale, + shadow=shadow) + x += spacing * 1.25 + delay += delay_inc + self._make_word('b', + x, + y + y_extra - 10, + delay=delay, + scale=1.1 * base_scale, + vr_depth_offset=5, + shadow=shadow) + x += spacing * 0.85 + delay += delay_inc + self._make_word('S', + x, + y - 25 + 0.8 * y_extra, + scale=1.35 * base_scale, + delay=delay, + vr_depth_offset=14, + shadow=shadow) + x += spacing + delay += delay_inc + self._make_word('q', + x, + y + y_extra, + delay=delay, + scale=base_scale, + shadow=shadow) + x += spacing * 0.9 + delay += delay_inc + self._make_word('u', + x, + y + y_extra, + delay=delay, + scale=base_scale, + vr_depth_offset=7, + shadow=shadow) + x += spacing * 0.9 + delay += delay_inc + self._make_word('a', + x, + y + y_extra, + delay=delay, + scale=base_scale, + shadow=shadow) + x += spacing * 0.64 + delay += delay_inc + self._make_word('d', + x, + y + y_extra - 10, + delay=delay, + scale=1.1 * base_scale, + vr_depth_offset=6, + shadow=shadow) + self._make_logo(base_x - 28, + 125 + y + 1.2 * y_extra, + 0.32 * base_scale, + delay=base_delay) + + def _make_word(self, + word: str, + x: float, + y: float, + scale: float = 1.0, + delay: float = 0.0, + vr_depth_offset: float = 0.0, + shadow: bool = False) -> None: + if shadow: + word_obj = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (x, y), + 'big': True, + 'color': (0.0, 0.0, 0.2, 0.08), + 'tilt_translate': 0.09, + 'opacity_scales_shadow': False, + 'shadow': 0.2, + 'vr_depth': -130, + 'v_align': 'center', + 'project_scale': 0.97 * scale, + 'scale': 1.0, + 'text': word + })) + self._word_actors.append(word_obj) + else: + word_obj = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (x, y), + 'big': True, + 'color': (1.2, 1.15, 1.15, 1.0), + 'tilt_translate': 0.11, + 'shadow': 0.2, + 'vr_depth': -40 + vr_depth_offset, + 'v_align': 'center', + 'project_scale': scale, + 'scale': 1.0, + 'text': word + })) + self._word_actors.append(word_obj) + + # Add a bit of stop-motion-y jitter to the logo + # (unless we're in VR mode in which case its best to + # leave things still). + if not ba.app.vr_mode: + cmb: Optional[ba.Node] + cmb2: Optional[ba.Node] + if not shadow: + cmb = ba.newnode('combine', + owner=word_obj.node, + attrs={'size': 2}) + else: + cmb = None + if shadow: + cmb2 = ba.newnode('combine', + owner=word_obj.node, + attrs={'size': 2}) + else: + cmb2 = None + if not shadow: + assert cmb and word_obj.node + cmb.connectattr('output', word_obj.node, 'position') + if shadow: + assert cmb2 and word_obj.node + cmb2.connectattr('output', word_obj.node, 'position') + keys = {} + keys2 = {} + time_v = 0.0 + for _i in range(10): + val = x + (random.random() - 0.5) * 0.8 + val2 = x + (random.random() - 0.5) * 0.8 + keys[time_v * self._ts] = val + keys2[time_v * self._ts] = val2 + 5 + time_v += random.random() * 0.1 + if cmb is not None: + ba.animate(cmb, 'input0', keys, loop=True) + if cmb2 is not None: + ba.animate(cmb2, 'input0', keys2, loop=True) + keys = {} + keys2 = {} + time_v = 0 + for _i in range(10): + val = y + (random.random() - 0.5) * 0.8 + val2 = y + (random.random() - 0.5) * 0.8 + keys[time_v * self._ts] = val + keys2[time_v * self._ts] = val2 - 9 + time_v += random.random() * 0.1 + if cmb is not None: + ba.animate(cmb, 'input1', keys, loop=True) + if cmb2 is not None: + ba.animate(cmb2, 'input1', keys2, loop=True) + + if not shadow: + assert word_obj.node + ba.animate(word_obj.node, 'project_scale', { + delay: 0.0, + delay + 0.1: scale * 1.1, + delay + 0.2: scale + }) + else: + assert word_obj.node + ba.animate(word_obj.node, 'project_scale', { + delay: 0.0, + delay + 0.1: scale * 1.1, + delay + 0.2: scale + }) + + def _get_custom_logo_tex_name(self) -> Optional[str]: + if _ba.get_account_misc_read_val('easter', False): + return 'logoEaster' + return None + + # Pop the logo and menu in. + def _make_logo(self, + x: float, + y: float, + scale: float, + delay: float, + custom_texture: str = None, + jitter_scale: float = 1.0, + rotate: float = 0.0, + vr_depth_offset: float = 0.0) -> None: + + # Temp easter goodness. + if custom_texture is None: + custom_texture = self._get_custom_logo_tex_name() + self._custom_logo_tex_name = custom_texture + ltex = ba.gettexture( + custom_texture if custom_texture is not None else 'logo') + mopaque = (None if custom_texture is not None else ba.getmodel('logo')) + mtrans = (None if custom_texture is not None else + ba.getmodel('logoTransparent')) + logo = ba.NodeActor( + ba.newnode('image', + attrs={ + 'texture': ltex, + 'model_opaque': mopaque, + 'model_transparent': mtrans, + 'vr_depth': -10 + vr_depth_offset, + 'rotate': rotate, + 'attach': 'center', + 'tilt_translate': 0.21, + 'absolute_scale': True + })) + self._logo_node = logo.node + self._word_actors.append(logo) + + # Add a bit of stop-motion-y jitter to the logo + # (unless we're in VR mode in which case its best to + # leave things still). + assert logo.node + if not ba.app.vr_mode: + cmb = ba.newnode('combine', owner=logo.node, attrs={'size': 2}) + cmb.connectattr('output', logo.node, 'position') + keys = {} + time_v = 0.0 + + # Gen some random keys for that stop-motion-y look + for _i in range(10): + keys[time_v] = x + (random.random() - 0.5) * 0.7 * jitter_scale + time_v += random.random() * 0.1 + ba.animate(cmb, 'input0', keys, loop=True) + keys = {} + time_v = 0.0 + for _i in range(10): + keys[time_v * self._ts] = y + (random.random() - + 0.5) * 0.7 * jitter_scale + time_v += random.random() * 0.1 + ba.animate(cmb, 'input1', keys, loop=True) + else: + logo.node.position = (x, y) + + cmb = ba.newnode('combine', owner=logo.node, attrs={'size': 2}) + + keys = { + delay: 0.0, + delay + 0.1: 700.0 * scale, + delay + 0.2: 600.0 * scale + } + ba.animate(cmb, 'input0', keys) + ba.animate(cmb, 'input1', keys) + cmb.connectattr('output', logo.node, 'scale') + + def _start_preloads(self) -> None: + # FIXME: The func that calls us back doesn't save/restore state + # or check for a dead activity so we have to do that ourself. + if self.expired: + return + with ba.Context(self): + _preload1() + + ba.timer(0.5, lambda: ba.setmusic(ba.MusicType.MENU)) + + +def _preload1() -> None: + """Pre-load some assets a second or two into the main menu. + + Helps avoid hitches later on. + """ + for mname in [ + 'plasticEyesTransparent', 'playerLineup1Transparent', + 'playerLineup2Transparent', 'playerLineup3Transparent', + 'playerLineup4Transparent', 'angryComputerTransparent', + 'scrollWidgetShort', 'windowBGBlotch' + ]: + ba.getmodel(mname) + for tname in ['playerLineup', 'lock']: + ba.gettexture(tname) + for tex in [ + 'iconRunaround', 'iconOnslaught', 'medalComplete', 'medalBronze', + 'medalSilver', 'medalGold', 'characterIconMask' + ]: + ba.gettexture(tex) + ba.gettexture('bg') + from bastd.actor.powerupbox import PowerupBoxFactory + PowerupBoxFactory.get() + ba.timer(0.1, _preload2) + + +def _preload2() -> None: + # FIXME: Could integrate these loads with the classes that use them + # so they don't have to redundantly call the load + # (even if the actual result is cached). + for mname in ['powerup', 'powerupSimple']: + ba.getmodel(mname) + for tname in [ + 'powerupBomb', 'powerupSpeed', 'powerupPunch', 'powerupIceBombs', + 'powerupStickyBombs', 'powerupShield', 'powerupImpactBombs', + 'powerupHealth' + ]: + ba.gettexture(tname) + for sname in [ + 'powerup01', 'boxDrop', 'boxingBell', 'scoreHit01', 'scoreHit02', + 'dripity', 'spawn', 'gong' + ]: + ba.getsound(sname) + from bastd.actor.bomb import BombFactory + BombFactory.get() + ba.timer(0.1, _preload3) + + +def _preload3() -> None: + from bastd.actor.spazfactory import SpazFactory + for mname in ['bomb', 'bombSticky', 'impactBomb']: + ba.getmodel(mname) + for tname in [ + 'bombColor', 'bombColorIce', 'bombStickyColor', 'impactBombColor', + 'impactBombColorLit' + ]: + ba.gettexture(tname) + for sname in ['freeze', 'fuse01', 'activateBeep', 'warnBeep']: + ba.getsound(sname) + SpazFactory.get() + ba.timer(0.2, _preload4) + + +def _preload4() -> None: + for tname in ['bar', 'meter', 'null', 'flagColor', 'achievementOutline']: + ba.gettexture(tname) + for mname in ['frameInset', 'meterTransparent', 'achievementOutline']: + ba.getmodel(mname) + for sname in ['metalHit', 'metalSkid', 'refWhistle', 'achievement']: + ba.getsound(sname) + from bastd.actor.flag import FlagFactory + FlagFactory.get() + + +class MainMenuSession(ba.Session): + """Session that runs the main menu environment.""" + + def __init__(self) -> None: + + # Gather dependencies we'll need (just our activity). + self._activity_deps = ba.DependencySet(ba.Dependency(MainMenuActivity)) + + super().__init__([self._activity_deps]) + self._locked = False + self.setactivity(ba.newactivity(MainMenuActivity)) + + def on_activity_end(self, activity: ba.Activity, results: Any) -> None: + if self._locked: + _ba.unlock_all_input() + + # Any ending activity leads us into the main menu one. + self.setactivity(ba.newactivity(MainMenuActivity)) + + def on_player_request(self, player: ba.SessionPlayer) -> bool: + # Reject all player requests. + return False diff --git a/dist/ba_data/python/bastd/mapdata/__init__.py b/dist/ba_data/python/bastd/mapdata/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..d2d57e1 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-38.opt-1.pyc new file mode 100644 index 0000000..47e58b1 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-38.pyc new file mode 100644 index 0000000..2a086a7 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-38.opt-1.pyc new file mode 100644 index 0000000..62e9b7e Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-38.pyc new file mode 100644 index 0000000..2347411 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-38.opt-1.pyc new file mode 100644 index 0000000..2398123 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-38.pyc new file mode 100644 index 0000000..c8e8ab6 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c0c454f Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-38.pyc new file mode 100644 index 0000000..ba2fea0 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-38.opt-1.pyc new file mode 100644 index 0000000..be6dd55 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-38.pyc new file mode 100644 index 0000000..b37563e Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-38.opt-1.pyc new file mode 100644 index 0000000..85cdcef Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-38.pyc new file mode 100644 index 0000000..c043763 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-38.opt-1.pyc new file mode 100644 index 0000000..cef5840 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-38.pyc new file mode 100644 index 0000000..4e290f5 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d9b5dc8 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-38.pyc new file mode 100644 index 0000000..c1f8632 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4aa3732 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-38.pyc new file mode 100644 index 0000000..10acaec Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-38.opt-1.pyc new file mode 100644 index 0000000..1193053 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-38.pyc new file mode 100644 index 0000000..5460bef Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-38.opt-1.pyc new file mode 100644 index 0000000..67f852a Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-38.pyc new file mode 100644 index 0000000..2bc4535 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-38.opt-1.pyc new file mode 100644 index 0000000..ead4b25 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-38.pyc new file mode 100644 index 0000000..ccc2c30 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-38.opt-1.pyc new file mode 100644 index 0000000..ba468ec Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-38.pyc new file mode 100644 index 0000000..9feadea Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-38.opt-1.pyc new file mode 100644 index 0000000..34ebc19 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-38.pyc new file mode 100644 index 0000000..82b174d Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-38.opt-1.pyc new file mode 100644 index 0000000..ed28e81 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-38.pyc new file mode 100644 index 0000000..ae6125f Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-38.opt-1.pyc new file mode 100644 index 0000000..ec51382 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-38.pyc new file mode 100644 index 0000000..29a3ea1 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-38.opt-1.pyc new file mode 100644 index 0000000..1c31c69 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-38.pyc b/dist/ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-38.pyc new file mode 100644 index 0000000..b5be610 Binary files /dev/null and b/dist/ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/mapdata/big_g.py b/dist/ba_data/python/bastd/mapdata/big_g.py new file mode 100644 index 0000000..207e93e --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/big_g.py @@ -0,0 +1,84 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "big_g.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (-0.4011866709, 2.331310176, + -0.5426286416) + (0.0, 0.0, 0.0) + ( + 19.11746262, 10.19675564, 23.50119277) +points['ffa_spawn1'] = (3.140826121, 1.16512015, + 6.172121491) + (4.739204545, 1.0, 1.028864849) +points['ffa_spawn2'] = (5.416289073, 1.180022599, -0.1696495695) + ( + 2.945888237, 0.621599724, 0.4969830881) +points['ffa_spawn3'] = (-0.3692088357, 2.88984723, -6.909741615) + ( + 7.575371952, 0.621599724, 0.4969830881) +points['ffa_spawn4'] = (-2.391932409, 1.123690253, -3.417262271) + ( + 2.933065031, 0.621599724, 0.9796558695) +points['ffa_spawn5'] = (-7.46052038, 2.863807079, + 4.936420902) + (0.8707600789, 0.621599724, 2.233577195) +points['flag1'] = (7.557928387, 2.889342613, -7.208799596) +points['flag2'] = (7.696183956, 1.095466627, 6.103380446) +points['flag3'] = (-8.122819332, 2.844893069, 6.103380446) +points['flag4'] = (-8.018537918, 2.844893069, -6.202403896) +points['flag_default'] = (-7.563673017, 2.850652319, 0.08844978098) +boxes['map_bounds'] = (-0.1916036665, 8.764115729, 0.1971423239) + ( + 0.0, 0.0, 0.0) + (27.41996888, 18.47258973, 22.17335735) +points['powerup_spawn1'] = (7.830495287, 2.115087683, -0.05452287857) +points['powerup_spawn2'] = (-5.190293739, 1.476317443, -3.80237889) +points['powerup_spawn3'] = (-8.540957726, 3.762979519, -7.27710542) +points['powerup_spawn4'] = (7.374052727, 3.762979519, -3.091707631) +points['powerup_spawn5'] = (-8.691423338, 3.692026034, 6.627877455) +points['race_mine1'] = (-0.06161453294, 1.123140909, 4.966104324) +points['race_mine10'] = (-6.870248758, 2.851484105, 2.718992803) +points['race_mine2'] = (-0.06161453294, 1.123140909, 6.99632996) +points['race_mine3'] = (-0.7319278377, 1.123140909, -2.828583367) +points['race_mine4'] = (-3.286508423, 1.123140909, 0.8453899305) +points['race_mine5'] = (5.077545429, 2.850225463, -5.253575631) +points['race_mine6'] = (6.286453838, 2.850225463, -5.253575631) +points['race_mine7'] = (0.969120762, 2.851484105, -7.892038145) +points['race_mine8'] = (-2.976299166, 2.851484105, -6.241064664) +points['race_mine9'] = (-6.962812986, 2.851484105, -2.120262964) +points['race_point1'] = (2.280447713, 1.16512015, 6.015278429) + ( + 0.7066894139, 4.672784871, 1.322422256) +points['race_point10'] = (-4.196540687, 2.877461266, -7.106874334) + ( + 0.1057202515, 5.496127671, 1.028552836) +points['race_point11'] = (-7.634488499, 2.877461266, -3.61728743) + ( + 1.438144134, 5.157457566, 0.06318119808) +points['race_point12'] = (-7.541251512, 2.877461266, 3.290439202) + ( + 1.668578284, 5.52484043, 0.06318119808) +points['race_point2'] = (4.853459878, 1.16512015, + 6.035867283) + (0.3920628436, 4.577066678, 1.34568243) +points['race_point3'] = (6.905234402, 1.16512015, 1.143337503) + ( + 1.611663691, 3.515259775, 0.1135135003) +points['race_point4'] = (2.681673258, 1.16512015, 0.771967064) + ( + 0.6475414982, 3.602143342, 0.1135135003) +points['race_point5'] = (-0.3776550727, 1.225615225, 1.920343787) + ( + 0.1057202515, 4.245024435, 0.5914887576) +points['race_point6'] = (-4.365081958, 1.16512015, -0.3565529313) + ( + 1.627090525, 4.549428479, 0.1135135003) +points['race_point7'] = (0.4149308672, 1.16512015, -3.394316313) + ( + 0.1057202515, 4.945367833, 1.310190117) +points['race_point8'] = (4.27031635, 2.19747021, -3.335165617) + ( + 0.1057202515, 4.389664492, 1.20413595) +points['race_point9'] = (2.552998384, 2.877461266, -7.117366939) + ( + 0.1057202515, 5.512312989, 0.9986814472) +points['shadow_lower_bottom'] = (-0.2227795102, 0.2903873918, 2.680075641) +points['shadow_lower_top'] = (-0.2227795102, 0.8824975157, 2.680075641) +points['shadow_upper_bottom'] = (-0.2227795102, 6.305086402, 2.680075641) +points['shadow_upper_top'] = (-0.2227795102, 9.470923628, 2.680075641) +points['spawn1'] = (7.180043217, 2.85596295, -4.407134234) + (0.7629937742, + 1.0, 1.818908238) +points['spawn2'] = (5.880548999, 1.142163379, 6.171168951) + (1.817516622, 1.0, + 0.7724344394) +points['spawn_by_flag1'] = (7.180043217, 2.85596295, + -4.407134234) + (0.7629937742, 1.0, 1.818908238) +points['spawn_by_flag2'] = (5.880548999, 1.142163379, + 6.171168951) + (1.817516622, 1.0, 0.7724344394) +points['spawn_by_flag3'] = (-6.66642559, 3.554416948, + 5.820238985) + (1.097315815, 1.0, 1.285161684) +points['spawn_by_flag4'] = (-6.842951255, 3.554416948, + -6.17429905) + (0.8208434737, 1.0, 1.285161684) +points['tnt1'] = (-3.398312776, 2.067056737, -1.90142919) diff --git a/dist/ba_data/python/bastd/mapdata/bridgit.py b/dist/ba_data/python/bastd/mapdata/bridgit.py new file mode 100644 index 0000000..70cb3c7 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/bridgit.py @@ -0,0 +1,34 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "bridgit.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (-0.2457963347, 3.828181068, + -1.528362695) + (0.0, 0.0, 0.0) + ( + 19.14849937, 7.312788846, 8.436232726) +points['ffa_spawn1'] = (-5.869295124, 3.715437928, + -1.617274877) + (0.9410329222, 1.0, 1.818908238) +points['ffa_spawn2'] = (5.160809653, 3.761793434, + -1.443012115) + (0.7729807005, 1.0, 1.818908238) +points['ffa_spawn3'] = (-0.4266381164, 3.761793434, + -1.555562653) + (4.034151421, 1.0, 0.2731725824) +points['flag1'] = (-7.354603923, 3.770769731, -1.617274877) +points['flag2'] = (6.885846926, 3.770685211, -1.443012115) +points['flag_default'] = (-0.2227795102, 3.802429326, -1.562586233) +boxes['map_bounds'] = (-0.1916036665, 7.481446847, -1.311948055) + ( + 0.0, 0.0, 0.0) + (27.41996888, 18.47258973, 19.52220249) +points['powerup_spawn1'] = (6.82849491, 4.658454461, 0.1938139802) +points['powerup_spawn2'] = (-7.253381358, 4.728692078, 0.252121017) +points['powerup_spawn3'] = (6.82849491, 4.658454461, -3.461765427) +points['powerup_spawn4'] = (-7.253381358, 4.728692078, -3.40345839) +points['shadow_lower_bottom'] = (-0.2227795102, 2.83188898, 2.680075641) +points['shadow_lower_top'] = (-0.2227795102, 3.498267184, 2.680075641) +points['shadow_upper_bottom'] = (-0.2227795102, 6.305086402, 2.680075641) +points['shadow_upper_top'] = (-0.2227795102, 9.470923628, 2.680075641) +points['spawn1'] = (-5.869295124, 3.715437928, + -1.617274877) + (0.9410329222, 1.0, 1.818908238) +points['spawn2'] = (5.160809653, 3.761793434, + -1.443012115) + (0.7729807005, 1.0, 1.818908238) diff --git a/dist/ba_data/python/bastd/mapdata/courtyard.py b/dist/ba_data/python/bastd/mapdata/courtyard.py new file mode 100644 index 0000000..634cdb2 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/courtyard.py @@ -0,0 +1,70 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "courtyard.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.3544110667, 3.958431362, + -2.175025358) + (0.0, 0.0, 0.0) + ( + 16.37702017, 7.755670126, 13.38680645) +points['bot_spawn_bottom'] = (-0.06281376545, 2.814769232, 1.95079953) +points['bot_spawn_bottom_half_left'] = (-2.05017213, 2.814769232, 1.95079953) +points['bot_spawn_bottom_half_right'] = (1.85515704, 2.814769232, 1.95079953) +points['bot_spawn_bottom_left'] = (-3.680966394, 2.814769232, 1.95079953) +points['bot_spawn_bottom_right'] = (3.586455826, 2.814769232, 1.95079953) +points['bot_spawn_left'] = (-6.447075231, 2.814769232, -2.317996277) +points['bot_spawn_left_lower'] = (-6.447075231, 2.814769232, -1.509957962) +points['bot_spawn_left_lower_more'] = (-6.447075231, 2.814769232, + -0.4832205112) +points['bot_spawn_left_upper'] = (-6.447075231, 2.814769232, -3.183562653) +points['bot_spawn_left_upper_more'] = (-6.447075231, 2.814769232, -4.010007449) +points['bot_spawn_right'] = (6.539735433, 2.814769232, -2.317996277) +points['bot_spawn_right_lower'] = (6.539735433, 2.814769232, -1.396042829) +points['bot_spawn_right_lower_more'] = (6.539735433, 2.814769232, + -0.3623501424) +points['bot_spawn_right_upper'] = (6.539735433, 2.814769232, -3.130071083) +points['bot_spawn_right_upper_more'] = (6.539735433, 2.814769232, -3.977427131) +points['bot_spawn_top'] = (-0.06281376545, 2.814769232, -5.833265855) +points['bot_spawn_top_half_left'] = (-1.494224867, 2.814769232, -5.833265855) +points['bot_spawn_top_half_right'] = (1.600833867, 2.814769232, -5.833265855) +points['bot_spawn_top_left'] = (-3.12023359, 2.814769232, -5.950680835) +points['bot_spawn_top_right'] = (3.396752931, 2.814769232, -5.950680835) +points['bot_spawn_turret_bottom_left'] = (-6.127144702, 3.3275475, 1.911189749) +points['bot_spawn_turret_bottom_right'] = (6.372913618, 3.3275475, 1.79864574) +points['bot_spawn_turret_top_left'] = (-6.127144702, 3.3275475, -6.572879116) +points['bot_spawn_turret_top_middle'] = (0.08149184008, 4.270281808, + -8.522292633) +points['bot_spawn_turret_top_middle_left'] = (-1.271380584, 4.270281808, + -8.522292633) +points['bot_spawn_turret_top_middle_right'] = (1.128462393, 4.270281808, + -8.522292633) +points['bot_spawn_turret_top_right'] = (6.372913618, 3.3275475, -6.603689486) +boxes['edge_box'] = (0.0, 1.036729365, -2.142494752) + (0.0, 0.0, 0.0) + ( + 12.01667356, 11.40580437, 7.808185564) +points['ffa_spawn1'] = (-6.228613999, 3.765660284, + -5.15969075) + (1.480100328, 1.0, 0.07121651432) +points['ffa_spawn2'] = (6.286481065, 3.765660284, + -4.923207718) + (1.419728931, 1.0, 0.07121651432) +points['ffa_spawn3'] = (-0.01917923364, 4.39873514, + -6.964732605) + (1.505953039, 1.0, 0.2494784408) +points['ffa_spawn4'] = (-0.01917923364, 3.792688047, + 3.453884398) + (4.987737689, 1.0, 0.1505089956) +points['flag1'] = (-5.965661853, 2.820013813, -2.428844806) +points['flag2'] = (5.905546426, 2.800475393, -2.218272564) +points['flag_default'] = (0.2516184246, 2.784213993, -2.644195211) +boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + ( + 0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) +points['powerup_spawn1'] = (-3.555558641, 3.168458621, 0.3692836925) +points['powerup_spawn2'] = (3.625691848, 3.168458621, 0.4058534671) +points['powerup_spawn3'] = (3.625691848, 3.168458621, -4.987242873) +points['powerup_spawn4'] = (-3.555558641, 3.168458621, -5.023812647) +points['shadow_lower_bottom'] = (0.5236258282, 0.02085132358, 5.341226521) +points['shadow_lower_top'] = (0.5236258282, 1.206119006, 5.341226521) +points['shadow_upper_bottom'] = (0.5236258282, 6.359015684, 5.341226521) +points['shadow_upper_top'] = (0.5236258282, 10.12385584, 5.341226521) +points['spawn1'] = (-7.514831403, 3.803639368, + -2.102145502) + (0.0878727285, 1.0, 2.195980213) +points['spawn2'] = (7.462102032, 3.772786511, + -1.835207267) + (0.0288041898, 1.0, 2.221665995) diff --git a/dist/ba_data/python/bastd/mapdata/crag_castle.py b/dist/ba_data/python/bastd/mapdata/crag_castle.py new file mode 100644 index 0000000..ca5d794 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/crag_castle.py @@ -0,0 +1,43 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "crag_castle.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.7033834902, 6.55869393, -3.153439808) + ( + 0.0, 0.0, 0.0) + (16.73648528, 14.94789935, 11.60063102) +points['ffa_spawn1'] = (-4.04166076, 7.54589296, -3.542792409) + ( + 2.471508516, 1.156019141, 0.1791707664) +points['ffa_spawn2'] = (5.429881832, 7.582951102, + -3.497145747) + (2.415753564, 1.12871694, 0.17898173) +points['ffa_spawn3'] = (4.8635999, 9.311949436, + -6.013939259) + (1.61785329, 1.12871694, 0.17898173) +points['ffa_spawn4'] = (-3.628023052, 9.311949436, + -6.013939259) + (1.61785329, 1.12871694, 0.17898173) +points['ffa_spawn5'] = (-2.414363536, 5.930994442, + 0.03036413701) + (1.61785329, 1.12871694, 0.17898173) +points['ffa_spawn6'] = (3.520989196, 5.930994442, + 0.03036413701) + (1.61785329, 1.12871694, 0.17898173) +points['flag1'] = (-1.900164924, 9.363050076, -6.441041548) +points['flag2'] = (3.240019982, 9.319215955, -6.392759924) +points['flag3'] = (-6.883672142, 7.475761129, 0.2098388241) +points['flag4'] = (8.193957063, 7.478129652, 0.1536410508) +points['flag_default'] = (0.6296142785, 6.221901832, -0.0435909658) +boxes['map_bounds'] = (0.4799042306, 9.085075529, -3.267604531) + ( + 0.0, 0.0, 0.0) + (22.9573075, 9.908550511, 14.17997333) +points['powerup_spawn1'] = (7.916483636, 7.83853949, -5.990841203) +points['powerup_spawn2'] = (-0.6978591232, 7.883836528, -6.066674247) +points['powerup_spawn3'] = (1.858093733, 7.893059862, -6.076932659) +points['powerup_spawn4'] = (-6.671997388, 7.992307645, -6.121432603) +points['spawn1'] = (-5.169730601, 7.54589296, + -3.542792409) + (1.057384557, 1.156019141, 0.1791707664) +points['spawn2'] = (6.203092708, 7.582951102, + -3.497145747) + (1.009865407, 1.12871694, 0.17898173) +points['spawn_by_flag1'] = (-2.872146219, 9.363050076, -6.041110823) +points['spawn_by_flag2'] = (4.313355684, 9.363050076, -6.041110823) +points['spawn_by_flag3'] = (-6.634074097, 7.508585058, -0.5918910315) +points['spawn_by_flag4'] = (7.868759529, 7.508585058, -0.5918910315) +points['tnt1'] = (-5.038090498, 10.0136642, -6.158580823) +points['tnt2'] = (6.203368846, 10.0136642, -6.158580823) diff --git a/dist/ba_data/python/bastd/mapdata/doom_shroom.py b/dist/ba_data/python/bastd/mapdata/doom_shroom.py new file mode 100644 index 0000000..634a730 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/doom_shroom.py @@ -0,0 +1,36 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "doom_shroom.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.4687647786, 2.320345088, + -3.219423694) + (0.0, 0.0, 0.0) + ( + 21.34898078, 10.25529817, 14.67298352) +points['ffa_spawn1'] = (-5.828122667, 2.301094498, + -3.445694701) + (1.0, 1.0, 2.682935578) +points['ffa_spawn2'] = (6.496252674, 2.397778847, -3.573241388) + (1.0, 1.0, + 2.682935578) +points['ffa_spawn3'] = (0.8835145921, 2.307217208, + -0.3552854962) + (4.455517747, 1.0, 0.2723037175) +points['ffa_spawn4'] = (0.8835145921, 2.307217208, + -7.124335491) + (4.455517747, 1.0, 0.2723037175) +points['flag1'] = (-7.153737138, 2.251993091, -3.427368878) +points['flag2'] = (8.103769491, 2.320591215, -3.548878069) +points['flag_default'] = (0.5964565429, 2.373456481, -4.241969517) +boxes['map_bounds'] = (0.4566560559, 1.332051421, -3.80651373) + ( + 0.0, 0.0, 0.0) + (27.75073129, 14.44528216, 22.9896617) +points['powerup_spawn1'] = (5.180858712, 4.278900266, -7.282758712) +points['powerup_spawn2'] = (-3.236908759, 4.159702067, -0.3232556512) +points['powerup_spawn3'] = (5.082843398, 4.159702067, -0.3232556512) +points['powerup_spawn4'] = (-3.401729203, 4.278900266, -7.425891191) +points['shadow_lower_bottom'] = (0.5964565429, -0.2279530265, 3.368035253) +points['shadow_lower_top'] = (0.5964565429, 0.6982784189, 3.368035253) +points['shadow_upper_bottom'] = (0.5964565429, 5.413250948, 3.368035253) +points['shadow_upper_top'] = (0.5964565429, 7.891484473, 3.368035253) +points['spawn1'] = (-5.828122667, 2.301094498, -3.445694701) + (1.0, 1.0, + 2.682935578) +points['spawn2'] = (6.496252674, 2.397778847, -3.573241388) + (1.0, 1.0, + 2.682935578) diff --git a/dist/ba_data/python/bastd/mapdata/football_stadium.py b/dist/ba_data/python/bastd/mapdata/football_stadium.py new file mode 100644 index 0000000..b650238 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/football_stadium.py @@ -0,0 +1,32 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "football_stadium.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.0, 1.185751251, 0.4326226188) + ( + 0.0, 0.0, 0.0) + (29.8180273, 11.57249038, 18.89134176) +boxes['edge_box'] = (-0.103873591, 0.4133341891, 0.4294651013) + ( + 0.0, 0.0, 0.0) + (22.48295719, 1.290242794, 8.990252454) +points['ffa_spawn1'] = (-0.08015551329, 0.02275111462, + -4.373674593) + (8.895057015, 1.0, 0.444350722) +points['ffa_spawn2'] = (-0.08015551329, 0.02275111462, + 4.076288941) + (8.895057015, 1.0, 0.444350722) +points['flag1'] = (-10.99027878, 0.05744967453, 0.1095578275) +points['flag2'] = (11.01486398, 0.03986567039, 0.1095578275) +points['flag_default'] = (-0.1001374046, 0.04180340146, 0.1095578275) +boxes['goal1'] = (12.22454533, 1.0, + 0.1087926362) + (0.0, 0.0, 0.0) + (2.0, 2.0, 12.97466313) +boxes['goal2'] = (-12.15961605, 1.0, + 0.1097860203) + (0.0, 0.0, 0.0) + (2.0, 2.0, 13.11856424) +boxes['map_bounds'] = (0.0, 1.185751251, 0.4326226188) + (0.0, 0.0, 0.0) + ( + 42.09506485, 22.81173179, 29.76723155) +points['powerup_spawn1'] = (5.414681236, 0.9515026107, -5.037912441) +points['powerup_spawn2'] = (-5.555402285, 0.9515026107, -5.037912441) +points['powerup_spawn3'] = (5.414681236, 0.9515026107, 5.148223181) +points['powerup_spawn4'] = (-5.737266365, 0.9515026107, 5.148223181) +points['spawn1'] = (-10.03866341, 0.02275111462, 0.0) + (0.5, 1.0, 4.0) +points['spawn2'] = (9.823107149, 0.01092306765, 0.0) + (0.5, 1.0, 4.0) +points['tnt1'] = (-0.08421587483, 0.9515026107, -0.7762602271) diff --git a/dist/ba_data/python/bastd/mapdata/happy_thoughts.py b/dist/ba_data/python/bastd/mapdata/happy_thoughts.py new file mode 100644 index 0000000..c3e03ab --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/happy_thoughts.py @@ -0,0 +1,46 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "happy_thoughts.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (-1.045859963, 12.67722855, + -5.401537075) + (0.0, 0.0, 0.0) + ( + 34.46156851, 20.94044653, 0.6931564611) +points['ffa_spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) +points['ffa_spawn2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) +points['ffa_spawn3'] = (9.55724115, 11.30789446, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) +points['ffa_spawn4'] = (-11.55747023, 10.99170684, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) +points['ffa_spawn5'] = (-1.878892369, 9.46490571, -5.614479365) + ( + 1.337925849, 1.453808816, 0.04419853907) +points['ffa_spawn6'] = (-0.4912812943, 5.077006397, -5.521672101) + ( + 1.878332089, 1.453808816, 0.007578097856) +points['flag1'] = (-11.75152479, 8.057427485, -5.52) +points['flag2'] = (9.840909039, 8.188634282, -5.52) +points['flag3'] = (-0.2195258696, 5.010273907, -5.52) +points['flag4'] = (-0.04605809154, 12.73369108, -5.52) +points['flag_default'] = (-0.04201942896, 12.72374492, -5.52) +boxes['map_bounds'] = (-0.8748348681, 9.212941713, -5.729538885) + ( + 0.0, 0.0, 0.0) + (36.09666006, 26.19950145, 7.89541168) +points['powerup_spawn1'] = (1.160232442, 6.745963662, -5.469115985) +points['powerup_spawn2'] = (-1.899700206, 10.56447241, -5.505721177) +points['powerup_spawn3'] = (10.56098871, 12.25165669, -5.576232453) +points['powerup_spawn4'] = (-12.33530337, 12.25165669, -5.576232453) +points['spawn1'] = (-9.295167711, 8.010664315, + -5.44451005) + (1.555840357, 1.453808816, 0.1165648888) +points['spawn2'] = (7.484707127, 8.172681752, + -5.614479365) + (1.553861796, 1.453808816, 0.04419853907) +points['spawn_by_flag1'] = (-9.295167711, 8.010664315, -5.44451005) + ( + 1.555840357, 1.453808816, 0.1165648888) +points['spawn_by_flag2'] = (7.484707127, 8.172681752, -5.614479365) + ( + 1.553861796, 1.453808816, 0.04419853907) +points['spawn_by_flag3'] = (-1.45994593, 5.038762459, -5.535288724) + ( + 0.9516389866, 0.6666414677, 0.08607244075) +points['spawn_by_flag4'] = (0.4932087091, 12.74493212, -5.598987003) + ( + 0.5245740665, 0.5245740665, 0.01941146064) diff --git a/dist/ba_data/python/bastd/mapdata/hockey_stadium.py b/dist/ba_data/python/bastd/mapdata/hockey_stadium.py new file mode 100644 index 0000000..722797f --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/hockey_stadium.py @@ -0,0 +1,28 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "hockey_stadium.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.0, 0.7956858119, 0.0) + ( + 0.0, 0.0, 0.0) + (30.80223883, 0.5961646365, 13.88431707) +points['ffa_spawn1'] = (-0.001925625146, 0.02305323209, + -3.81971842) + (7.828121539, 1.0, 0.1588021252) +points['ffa_spawn2'] = (-0.001925625146, 0.02305323209, + 3.560115735) + (7.828121539, 1.0, 0.05859841271) +points['flag1'] = (-11.21689747, 0.09527878981, -0.07659307272) +points['flag2'] = (11.08204909, 0.04119542459, -0.07659307272) +points['flag_default'] = (-0.01690735171, 0.06139940044, -0.07659307272) +boxes['goal1'] = (8.45, 1.0, 0.0) + (0.0, 0.0, 0.0) + (0.4334079123, 1.6, 3.0) +boxes['goal2'] = (-8.45, 1.0, 0.0) + (0.0, 0.0, 0.0) + (0.4334079123, 1.6, 3.0) +boxes['map_bounds'] = (0.0, 0.7956858119, -0.4689020853) + (0.0, 0.0, 0.0) + ( + 35.16182389, 12.18696164, 21.52869693) +points['powerup_spawn1'] = (-3.654355317, 1.080990833, -4.765886164) +points['powerup_spawn2'] = (-3.654355317, 1.080990833, 4.599802158) +points['powerup_spawn3'] = (2.881071011, 1.080990833, -4.765886164) +points['powerup_spawn4'] = (2.881071011, 1.080990833, 4.599802158) +points['spawn1'] = (-6.835352227, 0.02305323209, 0.0) + (1.0, 1.0, 3.0) +points['spawn2'] = (6.857415055, 0.03938567998, 0.0) + (1.0, 1.0, 3.0) +points['tnt1'] = (-0.05791962398, 1.080990833, -4.765886164) diff --git a/dist/ba_data/python/bastd/mapdata/lake_frigid.py b/dist/ba_data/python/bastd/mapdata/lake_frigid.py new file mode 100644 index 0000000..a56b174 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/lake_frigid.py @@ -0,0 +1,77 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "lake_frigid.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.622753268, 3.958431362, -2.48708008) + ( + 0.0, 0.0, 0.0) + (20.62310543, 7.755670126, 12.33155049) +points['ffa_spawn1'] = (-5.782222813, 2.601256429, + -2.116763055) + (0.4872664751, 1.0, 2.99296869) +points['ffa_spawn2'] = (8.331810793, 2.563661107, + -2.362712466) + (0.4929678379, 1.0, 2.590481339) +points['ffa_spawn3'] = (-0.01917923364, 2.623757527, + -6.518902459) + (4.450854686, 1.0, 0.2494784408) +points['ffa_spawn4'] = (-0.01917923364, 2.620884201, + 2.154362669) + (4.987737689, 1.0, 0.1505089956) +points['flag1'] = (-5.965661853, 2.60868975, -2.428844806) +points['flag2'] = (7.469054879, 2.600634569, -2.218272564) +points['flag_default'] = (0.5814687904, 2.593249132, -6.083520531) +boxes['map_bounds'] = (0.6679698457, 6.090222998, -2.478650859) + ( + 0.0, 0.0, 0.0) + (26.78420476, 12.49722958, 19.09355242) +points['powerup_spawn1'] = (-3.178773331, 3.168458621, 1.526824762) +points['powerup_spawn2'] = (3.625691848, 3.168458621, 1.563394537) +points['powerup_spawn3'] = (3.625691848, 3.168458621, -5.768903171) +points['powerup_spawn4'] = (-3.178773331, 3.168458621, -5.805472946) +points['race_mine1'] = (-5.299824547, 2.523837916, 1.955977372) +points['race_mine10'] = (-0.713193354, 2.523837916, 3.114668201) +points['race_mine11'] = (9.390106796, 2.523837916, -1.647082264) +points['race_mine12'] = (5.745749508, 2.523837916, -2.297908403) +points['race_mine13'] = (6.214639992, 2.523837916, -0.8145917891) +points['race_mine14'] = (5.376973913, 2.523837916, -4.165899043) +points['race_mine15'] = (1.502206718, 2.523837916, -5.822493321) +points['race_mine16'] = (-1.686183167, 2.523837916, -5.237149734) +points['race_mine17'] = (-3.868444888, 2.523837916, -4.147761517) +points['race_mine18'] = (-7.414697421, 2.523837916, -1.500814191) +points['race_mine19'] = (-2.188054758, 2.523837916, 1.864328906) +points['race_mine2'] = (-5.29052862, 2.523837916, -5.866528803) +points['race_mine20'] = (8.027819188, 2.523837916, -0.009084902544) +points['race_mine21'] = (7.380356464, 2.523837916, -5.780049322) +points['race_mine22'] = (-4.568354273, 2.523837916, -5.03102897) +points['race_mine23'] = (-5.874455776, 2.523837916, -0.2827536691) +points['race_mine24'] = (2.786967886, 2.523837916, -7.903057897) +points['race_mine25'] = (5.824160391, 2.523837916, -6.591849727) +points['race_mine26'] = (-3.971925479, 2.523837916, -0.04187571107) +points['race_mine3'] = (6.491590735, 2.523837916, 1.526824762) +points['race_mine4'] = (6.777341469, 2.523837916, -4.811127322) +points['race_mine5'] = (1.530288599, 2.523837916, -7.238282813) +points['race_mine6'] = (-1.547487434, 2.523837916, -6.391185182) +points['race_mine7'] = (-4.356878305, 2.523837916, -2.04510117) +points['race_mine8'] = (-0.713193354, 2.523837916, -0.1340958729) +points['race_mine9'] = (-0.713193354, 2.523837916, 1.275675237) +points['race_point1'] = (0.5901776337, 2.544287937, 1.543598704) + ( + 0.2824957007, 3.950514538, 2.292534365) +points['race_point2'] = (4.7526567, 2.489758467, + 1.09551316) + (0.2824957007, 3.950514538, 2.392880724) +points['race_point3'] = (7.450800117, 2.601570758, -2.248040576) + ( + 2.167067932, 3.950514538, 0.2574992262) +points['race_point4'] = (5.064768438, 2.489758467, -5.820463576) + ( + 0.2824957007, 3.950514538, 2.392880724) +points['race_point5'] = (0.5901776337, 2.67667329, -6.165424036) + ( + 0.2824957007, 3.950514538, 2.156382533) +points['race_point6'] = (-3.057459058, 2.489758467, -6.114179652) + ( + 0.2824957007, 3.950514538, 2.323773344) +points['race_point7'] = (-5.814316926, 2.57969886, + -2.248040576) + (2.0364457, 3.950514538, 0.2574992262) +points['race_point8'] = (-2.958397223, 2.489758467, 1.360005754) + ( + 0.2824957007, 3.950514538, 2.529692681) +points['shadow_lower_bottom'] = (0.5236258282, 1.516338013, 5.341226521) +points['shadow_lower_top'] = (0.5236258282, 2.516776651, 5.341226521) +points['shadow_upper_bottom'] = (0.5236258282, 4.543246769, 5.341226521) +points['shadow_upper_top'] = (0.5236258282, 5.917963067, 5.341226521) +points['spawn1'] = (-5.945079307, 2.524666031, + -2.102145502) + (0.0878727285, 1.0, 2.195980213) +points['spawn2'] = (8.079733391, 2.506883995, + -2.364598145) + (0.0288041898, 1.0, 2.221665995) diff --git a/dist/ba_data/python/bastd/mapdata/monkey_face.py b/dist/ba_data/python/bastd/mapdata/monkey_face.py new file mode 100644 index 0000000..b041509 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/monkey_face.py @@ -0,0 +1,36 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "monkey_face.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (-1.657177611, 4.132574186, + -1.580485661) + (0.0, 0.0, 0.0) + ( + 17.36258946, 10.49020453, 12.31460338) +points['ffa_spawn1'] = (-8.026373566, 3.349937889, -2.542088202) + ( + 0.9450583628, 0.9450583628, 1.181509268) +points['ffa_spawn2'] = (4.73470012, 3.308679998, + -2.757871588) + (0.9335931003, 1.0, 1.217352295) +points['ffa_spawn3'] = (-1.907161509, 3.326830784, + -6.572223028) + (4.080767643, 1.0, 0.2880331593) +points['ffa_spawn4'] = (-1.672823345, 3.326830784, + 2.405442985) + (3.870724402, 1.0, 0.2880331593) +points['flag1'] = (-8.968414135, 3.35709348, -2.804123917) +points['flag2'] = (5.945128279, 3.354825248, -2.663635497) +points['flag_default'] = (-1.688166134, 3.392387172, -2.238613943) +boxes['map_bounds'] = (-1.615296127, 6.825502312, -2.200965435) + ( + 0.0, 0.0, 0.0) + (22.51905077, 12.21074608, 15.9079565) +points['powerup_spawn1'] = (-6.859406739, 4.429165244, -6.588618549) +points['powerup_spawn2'] = (-5.422572086, 4.228850685, 2.803988636) +points['powerup_spawn3'] = (3.148493267, 4.429165244, -6.588618549) +points['powerup_spawn4'] = (1.830377363, 4.228850685, 2.803988636) +points['shadow_lower_bottom'] = (-1.877364768, 0.9878677276, 5.50201662) +points['shadow_lower_top'] = (-1.877364768, 2.881511768, 5.50201662) +points['shadow_upper_bottom'] = (-1.877364768, 6.169020542, 5.50201662) +points['shadow_upper_top'] = (-1.877364768, 10.2492777, 5.50201662) +points['spawn1'] = (-8.026373566, 3.349937889, + -2.542088202) + (0.9450583628, 0.9450583628, 1.181509268) +points['spawn2'] = (4.73470012, 3.308679998, -2.757871588) + (0.9335931003, + 1.0, 1.217352295) diff --git a/dist/ba_data/python/bastd/mapdata/rampage.py b/dist/ba_data/python/bastd/mapdata/rampage.py new file mode 100644 index 0000000..1daff2d --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/rampage.py @@ -0,0 +1,32 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "rampage.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.3544110667, 5.616383286, + -4.066055072) + (0.0, 0.0, 0.0) + ( + 19.90053969, 10.34051135, 8.16221072) +boxes['edge_box'] = (0.3544110667, 5.438284793, -4.100357672) + ( + 0.0, 0.0, 0.0) + (12.57718032, 4.645176013, 3.605557343) +points['ffa_spawn1'] = (0.5006944438, 5.051501304, + -5.79356326) + (6.626174027, 1.0, 0.3402012662) +points['ffa_spawn2'] = (0.5006944438, 5.051501304, + -2.435321368) + (6.626174027, 1.0, 0.3402012662) +points['flag1'] = (-5.885814199, 5.112162255, -4.251754911) +points['flag2'] = (6.700855451, 5.10270501, -4.259912982) +points['flag_default'] = (0.3196701116, 5.110914413, -4.292515158) +boxes['map_bounds'] = (0.4528955042, 4.899663734, -3.543675157) + ( + 0.0, 0.0, 0.0) + (23.54502348, 14.19991443, 12.08017448) +points['powerup_spawn1'] = (-2.645358507, 6.426340583, -4.226597191) +points['powerup_spawn2'] = (3.540102796, 6.549722855, -4.198476335) +points['shadow_lower_bottom'] = (5.580073911, 3.136491026, 5.341226521) +points['shadow_lower_top'] = (5.580073911, 4.321758709, 5.341226521) +points['shadow_upper_bottom'] = (5.274539479, 8.425373402, 5.341226521) +points['shadow_upper_top'] = (5.274539479, 11.93458162, 5.341226521) +points['spawn1'] = (-4.745706238, 5.051501304, + -4.247934288) + (0.9186962739, 1.0, 0.5153189341) +points['spawn2'] = (5.838590388, 5.051501304, + -4.259627405) + (0.9186962739, 1.0, 0.5153189341) diff --git a/dist/ba_data/python/bastd/mapdata/roundabout.py b/dist/ba_data/python/bastd/mapdata/roundabout.py new file mode 100644 index 0000000..e9f7bdb --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/roundabout.py @@ -0,0 +1,32 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "roundabout.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (-1.552280404, 3.189001207, -2.40908495) + ( + 0.0, 0.0, 0.0) + (11.96255385, 8.857531648, 9.531689995) +points['ffa_spawn1'] = (-4.056288044, 3.85970651, + -4.6096757) + (0.9393824595, 1.0, 1.422669346) +points['ffa_spawn2'] = (0.9091263403, 3.849381394, + -4.673201431) + (0.9179219809, 1.0, 1.422669346) +points['ffa_spawn3'] = (-1.50312174, 1.498336991, + -0.7271163774) + (5.733928927, 1.0, 0.1877531607) +points['flag1'] = (-3.01567985, 3.846779683, -6.702828912) +points['flag2'] = (-0.01282460768, 3.828492613, -6.684991743) +points['flag_default'] = (-1.509110449, 1.447854976, -1.440324146) +boxes['map_bounds'] = (-1.615296127, 8.764115729, -2.663738363) + ( + 0.0, 0.0, 0.0) + (20.48886392, 18.92340529, 13.79786814) +points['powerup_spawn1'] = (-6.794510156, 2.660340814, 0.01205780317) +points['powerup_spawn2'] = (3.611953494, 2.660340814, 0.01205780317) +points['shadow_lower_bottom'] = (-1.848173322, 0.6339980822, 2.267036343) +points['shadow_lower_top'] = (-1.848173322, 1.077175164, 2.267036343) +points['shadow_upper_bottom'] = (-1.848173322, 6.04794944, 2.267036343) +points['shadow_upper_top'] = (-1.848173322, 9.186681264, 2.267036343) +points['spawn1'] = (-4.056288044, 3.85970651, -4.6096757) + (0.9393824595, 1.0, + 1.422669346) +points['spawn2'] = (0.9091263403, 3.849381394, + -4.673201431) + (0.9179219809, 1.0, 1.422669346) +points['tnt1'] = (-1.509110449, 2.457517361, 0.2340271555) diff --git a/dist/ba_data/python/bastd/mapdata/step_right_up.py b/dist/ba_data/python/bastd/mapdata/step_right_up.py new file mode 100644 index 0000000..5d709bd --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/step_right_up.py @@ -0,0 +1,48 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "step_right_up.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.3544110667, 6.07676405, -2.271833016) + ( + 0.0, 0.0, 0.0) + (22.55121262, 10.14644532, 14.66087273) +points['ffa_spawn1'] = (-6.989214197, 5.824902099, + -4.003708602) + (0.4063164993, 1.0, 3.6294637) +points['ffa_spawn2'] = (7.305179278, 5.866583139, + -4.003708602) + (0.4063164993, 1.0, 3.6294637) +points['ffa_spawn3'] = (2.641427041, 4.793721175, + -4.003708602) + (0.4063164993, 1.0, 3.6294637) +points['ffa_spawn4'] = (-2.36228023, 4.793721175, + -4.003708602) + (0.4063164993, 1.0, 3.6294637) +points['flag1'] = (-6.005199892, 5.824953504, -8.182477108) +points['flag2'] = (6.671556926, 5.819873617, -0.3196373496) +points['flag3'] = (-2.105198862, 4.785722143, -3.938339596) +points['flag4'] = (2.693244393, 4.785722143, -3.938339596) +points['flag_default'] = (0.2516184246, 4.163099318, -3.691279318) +boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + ( + 0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) +points['powerup_spawn1'] = (-5.250579743, 4.725015518, 2.81876755) +points['powerup_spawn2'] = (5.694682017, 4.725015518, 2.81876755) +points['powerup_spawn3'] = (7.897510583, 6.314188898, -0.7152878923) +points['powerup_spawn4'] = (-7.218887205, 6.310285546, -7.944219924) +points['powerup_spawn5'] = (-1.826019337, 5.246212934, -7.961997906) +points['powerup_spawn6'] = (2.527964103, 5.246212934, -0.412538132) +points['shadow_lower_bottom'] = (0.5236258282, 2.599698775, 5.341226521) +points['shadow_lower_top'] = (0.5236258282, 3.784966458, 5.341226521) +points['shadow_upper_bottom'] = (0.5236258282, 7.323868662, 5.341226521) +points['shadow_upper_top'] = (0.5236258282, 11.08870881, 5.341226521) +points['spawn1'] = (-4.265431979, 5.461124528, + -4.003708602) + (0.4063164993, 1.0, 2.195980213) +points['spawn2'] = (5.073366552, 5.444726373, + -4.063095718) + (0.3465292314, 1.0, 2.221665995) +points['spawn_by_flag1'] = (-6.663464996, 5.978624661, + -6.165495294) + (0.7518730647, 1.0, 0.8453811633) +points['spawn_by_flag2'] = (7.389476227, 5.978624661, + -1.709266011) + (0.7518730647, 1.0, 0.8453811633) +points['spawn_by_flag3'] = (-2.112456453, 4.802744618, + -3.947702091) + (0.7518730647, 1.0, 0.8453811633) +points['spawn_by_flag4'] = (2.701146355, 4.802744618, + -3.947702091) + (0.7518730647, 1.0, 0.8453811633) +points['tnt1'] = (0.258764453, 4.834253071, -4.306874943) diff --git a/dist/ba_data/python/bastd/mapdata/the_pad.py b/dist/ba_data/python/bastd/mapdata/the_pad.py new file mode 100644 index 0000000..7afe629 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/the_pad.py @@ -0,0 +1,37 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "the_pad.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.3544110667, 4.493562578, + -2.518391331) + (0.0, 0.0, 0.0) + ( + 16.64754831, 8.06138989, 18.5029888) +points['ffa_spawn1'] = (-3.812275836, 4.380655495, + -8.962074979) + (2.371946621, 1.0, 0.8737798622) +points['ffa_spawn2'] = (4.472503025, 4.406820459, + -9.007239732) + (2.708525168, 1.0, 0.8737798622) +points['ffa_spawn3'] = (6.972673935, 4.380775486, + -7.424407061) + (0.4850648533, 1.0, 1.597018665) +points['ffa_spawn4'] = (-6.36978974, 4.380775486, + -7.424407061) + (0.4850648533, 1.0, 1.597018665) +points['flag1'] = (-7.026110145, 4.308759233, -6.302807727) +points['flag2'] = (7.632557137, 4.366002373, -6.287969342) +points['flag_default'] = (0.4611826686, 4.382076338, 3.680881802) +boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + ( + 0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) +points['powerup_spawn1'] = (-4.166594349, 5.281834349, -6.427493781) +points['powerup_spawn2'] = (4.426873526, 5.342460464, -6.329745237) +points['powerup_spawn3'] = (-4.201686731, 5.123385835, 0.4400721376) +points['powerup_spawn4'] = (4.758924722, 5.123385835, 0.3494054559) +points['shadow_lower_bottom'] = (-0.2912522507, 2.020798381, 5.341226521) +points['shadow_lower_top'] = (-0.2912522507, 3.206066063, 5.341226521) +points['shadow_upper_bottom'] = (-0.2912522507, 6.062361813, 5.341226521) +points['shadow_upper_top'] = (-0.2912522507, 9.827201965, 5.341226521) +points['spawn1'] = (-3.902942148, 4.380655495, + -8.962074979) + (1.66339533, 1.0, 0.8737798622) +points['spawn2'] = (4.775040345, 4.406820459, -9.007239732) + (1.66339533, 1.0, + 0.8737798622) +points['tnt1'] = (0.4599593402, 4.044276501, -6.573537395) diff --git a/dist/ba_data/python/bastd/mapdata/tip_top.py b/dist/ba_data/python/bastd/mapdata/tip_top.py new file mode 100644 index 0000000..7e8a17d --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/tip_top.py @@ -0,0 +1,42 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "tip_top.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (0.004375512593, 7.141135803, + -0.01745294675) + (0.0, 0.0, 0.0) + ( + 21.12506141, 4.959977313, 16.6885592) +points['ffa_spawn1'] = (-4.211611443, 6.96684623, -3.792009469) + ( + 0.3862608373, 1.155037873, 0.301362335) +points['ffa_spawn2'] = (7.384331793, 5.212769921, + -2.788130319) + (1.155037873, 1.155037873, 1.155037873) +points['ffa_spawn3'] = (-7.264053816, 5.461241477, + -3.095884089) + (1.155037873, 1.155037873, 1.155037873) +points['ffa_spawn4'] = (0.02413253541, 5.367227206, + 4.075190968) + (1.875050182, 1.155037873, 0.2019443553) +points['ffa_spawn5'] = (-1.571185756, 7.042332385, -0.4760548825) + ( + 0.3862608373, 1.155037873, 0.301362335) +points['ffa_spawn6'] = (1.693597207, 7.042332385, -0.4760548825) + ( + 0.3862608373, 1.155037873, 0.301362335) +points['ffa_spawn7'] = (4.398059102, 6.96684623, -3.802802846) + ( + 0.3862608373, 1.155037873, 0.301362335) +points['flag1'] = (-7.006685836, 5.420897881, -2.717154638) +points['flag2'] = (7.166003893, 5.166226103, -2.651234621) +points['flag_default'] = (0.07287520555, 8.865234972, -4.988876512) +boxes['map_bounds'] = (-0.2103025678, 7.746661892, -0.3767425594) + ( + 0.0, 0.0, 0.0) + (23.8148841, 13.86473252, 16.37749544) +points['powerup_spawn1'] = (1.660037213, 8.050002248, -1.221221367) +points['powerup_spawn2'] = (-1.486576666, 7.912313704, -1.233393956) +points['powerup_spawn3'] = (2.629546191, 6.361794487, 1.399066775) +points['powerup_spawn4'] = (-2.695410503, 6.357885555, 1.428685156) +points['shadow_lower_bottom'] = (0.07287520555, 4.000760229, 6.31658856) +points['shadow_lower_top'] = (0.07287520555, 4.751182548, 6.31658856) +points['shadow_upper_bottom'] = (0.07287520555, 9.154987885, 6.31658856) +points['shadow_upper_top'] = (0.07287520555, 13.8166857, 6.31658856) +points['spawn1'] = (-7.264053816, 5.461241477, + -3.095884089) + (1.155037873, 1.155037873, 1.155037873) +points['spawn2'] = (7.384331793, 5.212769921, + -2.788130319) + (1.155037873, 1.155037873, 1.155037873) diff --git a/dist/ba_data/python/bastd/mapdata/tower_d.py b/dist/ba_data/python/bastd/mapdata/tower_d.py new file mode 100644 index 0000000..bc7b435 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/tower_d.py @@ -0,0 +1,63 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "tower_d.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (-0.4714933293, 2.887077774, + -1.505479919) + (0.0, 0.0, 0.0) + ( + 17.90145968, 6.188484831, 15.96149117) +boxes['b1'] = (-4.832680224, 2.581977222, -2.345017289) + (0.0, 0.0, 0.0) + ( + 0.9362069954, 2.750877211, 7.267441751) +boxes['b2'] = (4.74117666, 2.581977222, -3.341802598) + (0.0, 0.0, 0.0) + ( + 0.8162971446, 2.750877211, 7.222259865) +boxes['b3'] = (6.423991249, 2.581977222, -4.423628975) + (0.0, 0.0, 0.0) + ( + 0.8263776825, 4.534503966, 9.174814862) +boxes['b4'] = (-6.647568724, 3.229920028, -3.057507544) + (0.0, 0.0, 0.0) + ( + 0.9362069954, 2.750877211, 8.925082868) +boxes['b5'] = (3.537030094, 2.581977222, -2.366599524) + (0.0, 0.0, 0.0) + ( + 0.6331341496, 2.750877211, 5.327279595) +boxes['b6'] = (5.758481164, 2.581977222, 1.147676334) + (0.0, 0.0, 0.0) + ( + 2.300443227, 2.182113263, 0.5026852656) +boxes['b7'] = (-2.872805708, 2.581977222, -1.563390778) + (0.0, 0.0, 0.0) + ( + 0.9362069954, 2.750877211, 5.916439874) +boxes['b8'] = (-0.6884909563, 4.756572255, -5.317308535) + (0.0, 0.0, 0.0) + ( + 24.86081756, 9.001418153, 13.43159632) +boxes['b9'] = (-0.01526615369, 2.860860149, -0.9527080851) + ( + 0.0, 0.0, 0.0) + (4.899031047, 3.041987448, 6.142004808) +points['bot_spawn_bottom_left'] = (-7.400801881, 1.617640411, 5.384327397) + ( + 1.66339533, 1.0, 0.8737798622) +points['bot_spawn_bottom_right'] = (6.492270514, 1.617640411, 5.384327397) + ( + 1.66339533, 1.0, 0.8737798622) +points['bot_spawn_start'] = (-9.000552706, 3.1524, + 0.3095359717) + (1.66339533, 1.0, 0.8737798622) +boxes['edge_box'] = (-0.6502137344, 3.189969807, 4.099657551) + ( + 0.0, 0.0, 0.0) + (14.24474785, 1.469696777, 2.898807504) +boxes['edge_box2'] = (-0.144434237, 2.297109431, 0.3535332953) + ( + 0.0, 0.0, 0.0) + (1.744002325, 1.060637489, 4.905560105) +points['ffa_spawn1'] = (0.1024602894, 2.713599022, + -0.3639688715) + (1.66339533, 1.0, 0.8737798622) +points['flag1'] = (-7.751267587, 3.143417127, 0.266009523) +points['flag2'] = (6.851493725, 2.251048381, 0.3832888796) +points['flag_default'] = (0.01935110284, 2.227757733, -6.312658764) +boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + ( + 0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) +boxes['powerup_region'] = (0.3544110667, 4.036899334, 3.413130338) + ( + 0.0, 0.0, 0.0) + (12.48542377, 0.3837028864, 1.622880843) +points['powerup_spawn1'] = (-4.926642821, 2.397645612, 2.876782366) +points['powerup_spawn2'] = (-1.964335546, 2.397645612, 3.751374716) +points['powerup_spawn3'] = (1.64883201, 2.397645612, 3.751374716) +points['powerup_spawn4'] = (4.398865293, 2.397645612, 2.877618924) +boxes['score_region'] = (8.378672831, 3.049210364, 0.5079393073) + ( + 0.0, 0.0, 0.0) + (1.276586051, 1.368127109, 0.6915290191) +points['shadow_lower_bottom'] = (-0.2912522507, 0.9466821599, 5.341226521) +points['shadow_lower_top'] = (-0.2912522507, 2.131949842, 5.341226521) +points['shadow_upper_bottom'] = (-0.4529958189, 6.062361813, 5.341226521) +points['shadow_upper_top'] = (-0.2912522507, 9.827201965, 5.341226521) +points['spawn1'] = (0.1024602894, 2.713599022, + -0.3639688715) + (1.66339533, 1.0, 0.8737798622) +points['spawn2'] = (0.1449413466, 2.739700702, + -0.3431945526) + (1.66339533, 1.0, 0.8737798622) +points['tnt_loc'] = (0.008297003631, 2.777570118, 3.894548697) diff --git a/dist/ba_data/python/bastd/mapdata/zig_zag.py b/dist/ba_data/python/bastd/mapdata/zig_zag.py new file mode 100644 index 0000000..701d9f8 --- /dev/null +++ b/dist/ba_data/python/bastd/mapdata/zig_zag.py @@ -0,0 +1,46 @@ +# Released under the MIT License. See LICENSE for details. +# + +# This file was automatically generated from "zig_zag.ma" +# pylint: disable=all +points = {} +# noinspection PyDictCreation +boxes = {} +boxes['area_of_interest_bounds'] = (-1.807378035, 3.943412768, -1.61304303) + ( + 0.0, 0.0, 0.0) + (23.01413538, 13.27980464, 10.0098376) +points['ffa_spawn1'] = (-9.523537347, 4.645005984, + -3.193868606) + (0.8511546708, 1.0, 1.303629055) +points['ffa_spawn2'] = (6.217011718, 4.632396767, + -3.190279407) + (0.8844870264, 1.0, 1.303629055) +points['ffa_spawn3'] = (-4.433947713, 3.005988298, + -4.908942678) + (1.531254794, 1.0, 0.7550370795) +points['ffa_spawn4'] = (1.457496709, 3.01461103, + -4.907036892) + (1.531254794, 1.0, 0.7550370795) +points['flag1'] = (-9.969465388, 4.651689505, -4.965994534) +points['flag2'] = (6.844972512, 4.652142027, -4.951156148) +points['flag3'] = (1.155692239, 2.973774873, -4.890524808) +points['flag4'] = (-4.224099354, 3.006208239, -4.890524808) +points['flag_default'] = (-1.42651413, 3.018804771, 0.8808626995) +boxes['map_bounds'] = (-1.567840276, 8.764115729, -1.311948055) + ( + 0.0, 0.0, 0.0) + (28.76792809, 17.64963076, 19.52220249) +points['powerup_spawn1'] = (2.55551802, 4.369175386, -4.798598276) +points['powerup_spawn2'] = (-6.02484656, 4.369175386, -4.798598276) +points['powerup_spawn3'] = (5.557525662, 5.378865401, -4.798598276) +points['powerup_spawn4'] = (-8.792856883, 5.378865401, -4.816871147) +points['shadow_lower_bottom'] = (-1.42651413, 1.681630068, 4.790029929) +points['shadow_lower_top'] = (-1.42651413, 2.54918356, 4.790029929) +points['shadow_upper_bottom'] = (-1.42651413, 6.802574329, 4.790029929) +points['shadow_upper_top'] = (-1.42651413, 8.779767257, 4.790029929) +points['spawn1'] = (-9.523537347, 4.645005984, + -3.193868606) + (0.8511546708, 1.0, 1.303629055) +points['spawn2'] = (6.217011718, 4.632396767, + -3.190279407) + (0.8844870264, 1.0, 1.303629055) +points['spawn_by_flag1'] = (-9.523537347, 4.645005984, + -3.193868606) + (0.8511546708, 1.0, 1.303629055) +points['spawn_by_flag2'] = (6.217011718, 4.632396767, + -3.190279407) + (0.8844870264, 1.0, 1.303629055) +points['spawn_by_flag3'] = (1.457496709, 3.01461103, + -4.907036892) + (1.531254794, 1.0, 0.7550370795) +points['spawn_by_flag4'] = (-4.433947713, 3.005988298, + -4.908942678) + (1.531254794, 1.0, 0.7550370795) +points['tnt1'] = (-1.42651413, 4.045239665, 0.04094631341) diff --git a/dist/ba_data/python/bastd/maps.py b/dist/ba_data/python/bastd/maps.py new file mode 100644 index 0000000..03f4125 --- /dev/null +++ b/dist/ba_data/python/bastd/maps.py @@ -0,0 +1,1544 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Standard maps.""" +# pylint: disable=too-many-lines + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, List, Dict + + +class HockeyStadium(ba.Map): + """Stadium map used for ice hockey games.""" + + from bastd.mapdata import hockey_stadium as defs + name = 'Hockey Stadium' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'hockey', 'team_flag', 'keep_away'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'hockeyStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'models': (ba.getmodel('hockeyStadiumOuter'), + ba.getmodel('hockeyStadiumInner'), + ba.getmodel('hockeyStadiumStands')), + 'vr_fill_model': ba.getmodel('footballStadiumVRFill'), + 'collide_model': ba.getcollidemodel('hockeyStadiumCollide'), + 'tex': ba.gettexture('hockeyStadium'), + 'stands_tex': ba.gettexture('footballStadium') + } + mat = ba.Material() + mat.add_actions(actions=('modify_part_collision', 'friction', 0.01)) + data['ice_material'] = mat + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode('terrain', + delegate=self, + attrs={ + 'model': + self.preloaddata['models'][0], + 'collide_model': + self.preloaddata['collide_model'], + 'color_texture': + self.preloaddata['tex'], + 'materials': [ + shared.footing_material, + self.preloaddata['ice_material'] + ] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'vr_only': True, + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['stands_tex'] + }) + mats = [shared.footing_material, self.preloaddata['ice_material']] + self.floor = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['models'][1], + 'color_texture': self.preloaddata['tex'], + 'opacity': 0.92, + 'opacity_in_low_or_medium_quality': 1.0, + 'materials': mats + }) + self.stands = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['models'][2], + 'visible_in_reflections': False, + 'color_texture': self.preloaddata['stands_tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.floor_reflection = True + gnode.debris_friction = 0.3 + gnode.debris_kill_height = -0.3 + gnode.tint = (1.2, 1.3, 1.33) + gnode.ambient_color = (1.15, 1.25, 1.6) + gnode.vignette_outer = (0.66, 0.67, 0.73) + gnode.vignette_inner = (0.93, 0.93, 0.95) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + self.is_hockey = True + + +class FootballStadium(ba.Map): + """Stadium map for football games.""" + from bastd.mapdata import football_stadium as defs + + name = 'Football Stadium' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'football', 'team_flag', 'keep_away'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'footballStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('footballStadium'), + 'vr_fill_model': ba.getmodel('footballStadiumVRFill'), + 'collide_model': ba.getcollidemodel('footballStadiumCollide'), + 'tex': ba.gettexture('footballStadium') + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'model': self.preloaddata['model'], + 'collide_model': self.preloaddata['collide_model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + + +class Bridgit(ba.Map): + """Map with a narrow bridge in the middle.""" + from bastd.mapdata import bridgit as defs + + name = 'Bridgit' + dataname = 'bridgit' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + # print('getting playtypes', cls._getdata()['play_types']) + return ['melee', 'team_flag', 'keep_away'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'bridgitPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model_top': ba.getmodel('bridgitLevelTop'), + 'model_bottom': ba.getmodel('bridgitLevelBottom'), + 'model_bg': ba.getmodel('natureBackground'), + 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), + 'collide_model': ba.getcollidemodel('bridgitLevelCollide'), + 'tex': ba.gettexture('bridgitLevelColor'), + 'model_bg_tex': ba.gettexture('natureBackgroundColor'), + 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), + 'railing_collide_model': + (ba.getcollidemodel('bridgitLevelRailingCollide')), + 'bg_material': ba.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 10.0)) + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model_top'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['model_bottom'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['model_bg'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bg_vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + self.railing = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['railing_collide_model'], + 'materials': [shared.railing_material], + 'bumper': True + }) + self.bg_collide = ba.newnode('terrain', + attrs={ + 'collide_model': + self.preloaddata['collide_bg'], + 'materials': [ + shared.footing_material, + self.preloaddata['bg_material'], + shared.death_material + ] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.2, 1.3) + gnode.ambient_color = (1.1, 1.2, 1.3) + gnode.vignette_outer = (0.65, 0.6, 0.55) + gnode.vignette_inner = (0.9, 0.9, 0.93) + + +class BigG(ba.Map): + """Large G shaped map for racing""" + + from bastd.mapdata import big_g as defs + + name = 'Big G' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [ + 'race', 'melee', 'keep_away', 'team_flag', 'king_of_the_hill', + 'conquest' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'bigGPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model_top': ba.getmodel('bigG'), + 'model_bottom': ba.getmodel('bigGBottom'), + 'model_bg': ba.getmodel('natureBackground'), + 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), + 'collide_model': ba.getcollidemodel('bigGCollide'), + 'tex': ba.gettexture('bigG'), + 'model_bg_tex': ba.gettexture('natureBackgroundColor'), + 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), + 'bumper_collide_model': ba.getcollidemodel('bigGBumper'), + 'bg_material': ba.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 10.0)) + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'color': (0.7, 0.7, 0.7), + 'model': self.preloaddata['model_top'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['model_bottom'], + 'color': (0.7, 0.7, 0.7), + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['model_bg'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bg_vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + self.railing = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['bumper_collide_model'], + 'materials': [shared.railing_material], + 'bumper': True + }) + self.bg_collide = ba.newnode('terrain', + attrs={ + 'collide_model': + self.preloaddata['collide_bg'], + 'materials': [ + shared.footing_material, + self.preloaddata['bg_material'], + shared.death_material + ] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.2, 1.3) + gnode.ambient_color = (1.1, 1.2, 1.3) + gnode.vignette_outer = (0.65, 0.6, 0.55) + gnode.vignette_inner = (0.9, 0.9, 0.93) + + +class Roundabout(ba.Map): + """CTF map featuring two platforms and a long way around between them""" + + from bastd.mapdata import roundabout as defs + + name = 'Roundabout' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'roundaboutPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('roundaboutLevel'), + 'model_bottom': ba.getmodel('roundaboutLevelBottom'), + 'model_bg': ba.getmodel('natureBackground'), + 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), + 'collide_model': ba.getcollidemodel('roundaboutLevelCollide'), + 'tex': ba.gettexture('roundaboutLevelColor'), + 'model_bg_tex': ba.gettexture('natureBackgroundColor'), + 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), + 'railing_collide_model': + (ba.getcollidemodel('roundaboutLevelBumper')), + 'bg_material': ba.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 10.0)) + return data + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -1, 1)) + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['model_bottom'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['model_bg'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bg_vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + self.bg_collide = ba.newnode('terrain', + attrs={ + 'collide_model': + self.preloaddata['collide_bg'], + 'materials': [ + shared.footing_material, + self.preloaddata['bg_material'], + shared.death_material + ] + }) + self.railing = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['railing_collide_model'], + 'materials': [shared.railing_material], + 'bumper': True + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.0, 1.05, 1.1) + gnode.ambient_color = (1.0, 1.05, 1.1) + gnode.shadow_ortho = True + gnode.vignette_outer = (0.63, 0.65, 0.7) + gnode.vignette_inner = (0.97, 0.95, 0.93) + + +class MonkeyFace(ba.Map): + """Map sorta shaped like a monkey face; teehee!""" + + from bastd.mapdata import monkey_face as defs + + name = 'Monkey Face' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'monkeyFacePreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('monkeyFaceLevel'), + 'bottom_model': ba.getmodel('monkeyFaceLevelBottom'), + 'model_bg': ba.getmodel('natureBackground'), + 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), + 'collide_model': ba.getcollidemodel('monkeyFaceLevelCollide'), + 'tex': ba.gettexture('monkeyFaceLevelColor'), + 'model_bg_tex': ba.gettexture('natureBackgroundColor'), + 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), + 'railing_collide_model': + (ba.getcollidemodel('monkeyFaceLevelBumper')), + 'bg_material': ba.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 10.0)) + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bottom_model'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['model_bg'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bg_vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + self.bg_collide = ba.newnode('terrain', + attrs={ + 'collide_model': + self.preloaddata['collide_bg'], + 'materials': [ + shared.footing_material, + self.preloaddata['bg_material'], + shared.death_material + ] + }) + self.railing = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['railing_collide_model'], + 'materials': [shared.railing_material], + 'bumper': True + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.2, 1.2) + gnode.ambient_color = (1.2, 1.3, 1.3) + gnode.vignette_outer = (0.60, 0.62, 0.66) + gnode.vignette_inner = (0.97, 0.95, 0.93) + gnode.vr_camera_offset = (-1.4, 0, 0) + + +class ZigZag(ba.Map): + """A very long zig-zaggy map""" + + from bastd.mapdata import zig_zag as defs + + name = 'Zigzag' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [ + 'melee', 'keep_away', 'team_flag', 'conquest', 'king_of_the_hill' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'zigzagPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('zigZagLevel'), + 'model_bottom': ba.getmodel('zigZagLevelBottom'), + 'model_bg': ba.getmodel('natureBackground'), + 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), + 'collide_model': ba.getcollidemodel('zigZagLevelCollide'), + 'tex': ba.gettexture('zigZagLevelColor'), + 'model_bg_tex': ba.gettexture('natureBackgroundColor'), + 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), + 'railing_collide_model': ba.getcollidemodel('zigZagLevelBumper'), + 'bg_material': ba.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 10.0)) + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['model_bg'], + 'lighting': False, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['model_bottom'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bg_vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + self.bg_collide = ba.newnode('terrain', + attrs={ + 'collide_model': + self.preloaddata['collide_bg'], + 'materials': [ + shared.footing_material, + self.preloaddata['bg_material'], + shared.death_material + ] + }) + self.railing = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['railing_collide_model'], + 'materials': [shared.railing_material], + 'bumper': True + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.0, 1.15, 1.15) + gnode.ambient_color = (1.0, 1.15, 1.15) + gnode.vignette_outer = (0.57, 0.59, 0.63) + gnode.vignette_inner = (0.97, 0.95, 0.93) + gnode.vr_camera_offset = (-1.5, 0, 0) + + +class ThePad(ba.Map): + """A simple square shaped map with a raised edge.""" + + from bastd.mapdata import the_pad as defs + + name = 'The Pad' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag', 'king_of_the_hill'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'thePadPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('thePadLevel'), + 'bottom_model': ba.getmodel('thePadLevelBottom'), + 'collide_model': ba.getcollidemodel('thePadLevelCollide'), + 'tex': ba.gettexture('thePadLevelColor'), + 'bgtex': ba.gettexture('menuBG'), + 'bgmodel': ba.getmodel('thePadBG'), + 'railing_collide_model': ba.getcollidemodel('thePadLevelBumper'), + 'vr_fill_mound_model': ba.getmodel('thePadVRFillMound'), + 'vr_fill_mound_tex': ba.gettexture('vrFillMound') + } + # fixme should chop this into vr/non-vr sections for efficiency + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bottom_model'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.railing = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['railing_collide_model'], + 'materials': [shared.railing_material], + 'bumper': True + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_mound_model'], + 'lighting': False, + 'vr_only': True, + 'color': (0.56, 0.55, 0.47), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.1, 1.0) + gnode.ambient_color = (1.1, 1.1, 1.0) + gnode.vignette_outer = (0.7, 0.65, 0.75) + gnode.vignette_inner = (0.95, 0.95, 0.93) + + +class DoomShroom(ba.Map): + """A giant mushroom. Of doom!""" + + from bastd.mapdata import doom_shroom as defs + + name = 'Doom Shroom' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'doomShroomPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('doomShroomLevel'), + 'collide_model': ba.getcollidemodel('doomShroomLevelCollide'), + 'tex': ba.gettexture('doomShroomLevelColor'), + 'bgtex': ba.gettexture('doomShroomBGColor'), + 'bgmodel': ba.getmodel('doomShroomBG'), + 'vr_fill_model': ba.getmodel('doomShroomVRFill'), + 'stem_model': ba.getmodel('doomShroomStem'), + 'collide_bg': ba.getcollidemodel('doomShroomStemCollide') + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.stem = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['stem_model'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.bg_collide = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['collide_bg'], + 'materials': [shared.footing_material, shared.death_material] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (0.82, 1.10, 1.15) + gnode.ambient_color = (0.9, 1.3, 1.1) + gnode.shadow_ortho = False + gnode.vignette_outer = (0.76, 0.76, 0.76) + gnode.vignette_inner = (0.95, 0.95, 0.99) + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + xpos = point.x + zpos = point.z + x_adj = xpos * 0.125 + z_adj = (zpos + 3.7) * 0.2 + if running: + x_adj *= 1.4 + z_adj *= 1.4 + return x_adj * x_adj + z_adj * z_adj > 1.0 + + +class LakeFrigid(ba.Map): + """An icy lake fit for racing.""" + + from bastd.mapdata import lake_frigid as defs + + name = 'Lake Frigid' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag', 'race'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'lakeFrigidPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('lakeFrigid'), + 'model_top': ba.getmodel('lakeFrigidTop'), + 'model_reflections': ba.getmodel('lakeFrigidReflections'), + 'collide_model': ba.getcollidemodel('lakeFrigidCollide'), + 'tex': ba.gettexture('lakeFrigid'), + 'tex_reflections': ba.gettexture('lakeFrigidReflections'), + 'vr_fill_model': ba.getmodel('lakeFrigidVRFill') + } + mat = ba.Material() + mat.add_actions(actions=('modify_part_collision', 'friction', 0.01)) + data['ice_material'] = mat + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode('terrain', + delegate=self, + attrs={ + 'collide_model': + self.preloaddata['collide_model'], + 'model': + self.preloaddata['model'], + 'color_texture': + self.preloaddata['tex'], + 'materials': [ + shared.footing_material, + self.preloaddata['ice_material'] + ] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['model_top'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['model_reflections'], + 'lighting': False, + 'overlay': True, + 'opacity': 0.15, + 'color_texture': self.preloaddata['tex_reflections'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1, 1, 1) + gnode.ambient_color = (1, 1, 1) + gnode.shadow_ortho = True + gnode.vignette_outer = (0.86, 0.86, 0.86) + gnode.vignette_inner = (0.95, 0.95, 0.99) + gnode.vr_near_clip = 0.5 + self.is_hockey = True + + +class TipTop(ba.Map): + """A pointy map good for king-of-the-hill-ish games.""" + + from bastd.mapdata import tip_top as defs + + name = 'Tip Top' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag', 'king_of_the_hill'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'tipTopPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('tipTopLevel'), + 'bottom_model': ba.getmodel('tipTopLevelBottom'), + 'collide_model': ba.getcollidemodel('tipTopLevelCollide'), + 'tex': ba.gettexture('tipTopLevelColor'), + 'bgtex': ba.gettexture('tipTopBGColor'), + 'bgmodel': ba.getmodel('tipTopBG'), + 'railing_collide_model': ba.getcollidemodel('tipTopLevelBumper') + } + return data + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -0.2, 2.5)) + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'color': (0.7, 0.7, 0.7), + 'materials': [shared.footing_material] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bottom_model'], + 'lighting': False, + 'color': (0.7, 0.7, 0.7), + 'color_texture': self.preloaddata['tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'color': (0.4, 0.4, 0.4), + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.railing = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['railing_collide_model'], + 'materials': [shared.railing_material], + 'bumper': True + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (0.8, 0.9, 1.3) + gnode.ambient_color = (0.8, 0.9, 1.3) + gnode.vignette_outer = (0.79, 0.79, 0.69) + gnode.vignette_inner = (0.97, 0.97, 0.99) + + +class CragCastle(ba.Map): + """A lovely castle map.""" + + from bastd.mapdata import crag_castle as defs + + name = 'Crag Castle' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag', 'conquest'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'cragCastlePreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('cragCastleLevel'), + 'bottom_model': ba.getmodel('cragCastleLevelBottom'), + 'collide_model': ba.getcollidemodel('cragCastleLevelCollide'), + 'tex': ba.gettexture('cragCastleLevelColor'), + 'bgtex': ba.gettexture('menuBG'), + 'bgmodel': ba.getmodel('thePadBG'), + 'railing_collide_model': + (ba.getcollidemodel('cragCastleLevelBumper')), + 'vr_fill_mound_model': ba.getmodel('cragCastleVRFillMound'), + 'vr_fill_mound_tex': ba.gettexture('vrFillMound') + } + # fixme should chop this into vr/non-vr sections + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bottom_model'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.railing = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['railing_collide_model'], + 'materials': [shared.railing_material], + 'bumper': True + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_mound_model'], + 'lighting': False, + 'vr_only': True, + 'color': (0.2, 0.25, 0.2), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.shadow_ortho = True + gnode.shadow_offset = (0, 0, -5.0) + gnode.tint = (1.15, 1.05, 0.75) + gnode.ambient_color = (1.15, 1.05, 0.75) + gnode.vignette_outer = (0.6, 0.65, 0.6) + gnode.vignette_inner = (0.95, 0.95, 0.95) + gnode.vr_near_clip = 1.0 + + +class TowerD(ba.Map): + """Map used for runaround mini-game.""" + + from bastd.mapdata import tower_d as defs + + name = 'Tower D' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'towerDPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': + ba.getmodel('towerDLevel'), + 'model_bottom': + ba.getmodel('towerDLevelBottom'), + 'collide_model': + ba.getcollidemodel('towerDLevelCollide'), + 'tex': + ba.gettexture('towerDLevelColor'), + 'bgtex': + ba.gettexture('menuBG'), + 'bgmodel': + ba.getmodel('thePadBG'), + 'player_wall_collide_model': + ba.getcollidemodel('towerDPlayerWall'), + 'player_wall_material': + ba.Material() + } + # fixme should chop this into vr/non-vr sections + data['player_wall_material'].add_actions( + actions=('modify_part_collision', 'friction', 0.0)) + # anything that needs to hit the wall can apply this material + data['collide_with_wall_material'] = ba.Material() + data['player_wall_material'].add_actions( + conditions=('they_dont_have_material', + data['collide_with_wall_material']), + actions=('modify_part_collision', 'collide', False)) + data['vr_fill_mound_model'] = ba.getmodel('stepRightUpVRFillMound') + data['vr_fill_mound_tex'] = ba.gettexture('vrFillMound') + return data + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, 1, 1)) + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.node_bottom = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'model': self.preloaddata['model_bottom'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_mound_model'], + 'lighting': False, + 'vr_only': True, + 'color': (0.53, 0.57, 0.5), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.player_wall = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['player_wall_collide_model'], + 'affect_bg_dynamics': False, + 'materials': [self.preloaddata['player_wall_material']] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.15, 1.11, 1.03) + gnode.ambient_color = (1.2, 1.1, 1.0) + gnode.vignette_outer = (0.7, 0.73, 0.7) + gnode.vignette_inner = (0.95, 0.95, 0.95) + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + # see if we're within edge_box + boxes = self.defs.boxes + box_position = boxes['edge_box'][0:3] + box_scale = boxes['edge_box'][6:9] + box_position2 = boxes['edge_box2'][0:3] + box_scale2 = boxes['edge_box2'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + xpos2 = (point.x - box_position2[0]) / box_scale2[0] + zpos2 = (point.z - box_position2[2]) / box_scale2[2] + # if we're outside of *both* boxes we're near the edge + return ((xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5) and + (xpos2 < -0.5 or xpos2 > 0.5 or zpos2 < -0.5 or zpos2 > 0.5)) + + +class HappyThoughts(ba.Map): + """Flying map.""" + + from bastd.mapdata import happy_thoughts as defs + + name = 'Happy Thoughts' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [ + 'melee', 'keep_away', 'team_flag', 'conquest', 'king_of_the_hill' + ] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'alwaysLandPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('alwaysLandLevel'), + 'bottom_model': ba.getmodel('alwaysLandLevelBottom'), + 'bgmodel': ba.getmodel('alwaysLandBG'), + 'collide_model': ba.getcollidemodel('alwaysLandLevelCollide'), + 'tex': ba.gettexture('alwaysLandLevelColor'), + 'bgtex': ba.gettexture('alwaysLandBGColor'), + 'vr_fill_mound_model': ba.getmodel('alwaysLandVRFillMound'), + 'vr_fill_mound_tex': ba.gettexture('vrFillMound') + } + return data + + @classmethod + def get_music_type(cls) -> ba.MusicType: + return ba.MusicType.FLYING + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -3.7, 2.5)) + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bottom_model'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_mound_model'], + 'lighting': False, + 'vr_only': True, + 'color': (0.2, 0.25, 0.2), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.happy_thoughts_mode = True + gnode.shadow_offset = (0.0, 8.0, 5.0) + gnode.tint = (1.3, 1.23, 1.0) + gnode.ambient_color = (1.3, 1.23, 1.0) + gnode.vignette_outer = (0.64, 0.59, 0.69) + gnode.vignette_inner = (0.95, 0.95, 0.93) + gnode.vr_near_clip = 1.0 + self.is_flying = True + + # throw out some tips on flying + txt = ba.newnode('text', + attrs={ + 'text': ba.Lstr(resource='pressJumpToFlyText'), + 'scale': 1.2, + 'maxwidth': 800, + 'position': (0, 200), + 'shadow': 0.5, + 'flatness': 0.5, + 'h_align': 'center', + 'v_attach': 'bottom' + }) + cmb = ba.newnode('combine', + owner=txt, + attrs={ + 'size': 4, + 'input0': 0.3, + 'input1': 0.9, + 'input2': 0.0 + }) + ba.animate(cmb, 'input3', {3.0: 0, 4.0: 1, 9.0: 1, 10.0: 0}) + cmb.connectattr('output', txt, 'color') + ba.timer(10.0, txt.delete) + + +class StepRightUp(ba.Map): + """Wide stepped map good for CTF or Assault.""" + + from bastd.mapdata import step_right_up as defs + + name = 'Step Right Up' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag', 'conquest'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'stepRightUpPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('stepRightUpLevel'), + 'model_bottom': ba.getmodel('stepRightUpLevelBottom'), + 'collide_model': ba.getcollidemodel('stepRightUpLevelCollide'), + 'tex': ba.gettexture('stepRightUpLevelColor'), + 'bgtex': ba.gettexture('menuBG'), + 'bgmodel': ba.getmodel('thePadBG'), + 'vr_fill_mound_model': ba.getmodel('stepRightUpVRFillMound'), + 'vr_fill_mound_tex': ba.gettexture('vrFillMound') + } + # fixme should chop this into vr/non-vr chunks + return data + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, -1, 2)) + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.node_bottom = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'model': self.preloaddata['model_bottom'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_mound_model'], + 'lighting': False, + 'vr_only': True, + 'color': (0.53, 0.57, 0.5), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.2, 1.1, 1.0) + gnode.ambient_color = (1.2, 1.1, 1.0) + gnode.vignette_outer = (0.7, 0.65, 0.75) + gnode.vignette_inner = (0.95, 0.95, 0.93) + + +class Courtyard(ba.Map): + """A courtyard-ish looking map for co-op levels.""" + + from bastd.mapdata import courtyard as defs + + name = 'Courtyard' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'courtyardPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('courtyardLevel'), + 'model_bottom': ba.getmodel('courtyardLevelBottom'), + 'collide_model': ba.getcollidemodel('courtyardLevelCollide'), + 'tex': ba.gettexture('courtyardLevelColor'), + 'bgtex': ba.gettexture('menuBG'), + 'bgmodel': ba.getmodel('thePadBG'), + 'player_wall_collide_model': + (ba.getcollidemodel('courtyardPlayerWall')), + 'player_wall_material': ba.Material() + } + # FIXME: Chop this into vr and non-vr chunks. + data['player_wall_material'].add_actions( + actions=('modify_part_collision', 'friction', 0.0)) + # anything that needs to hit the wall should apply this. + data['collide_with_wall_material'] = ba.Material() + data['player_wall_material'].add_actions( + conditions=('they_dont_have_material', + data['collide_with_wall_material']), + actions=('modify_part_collision', 'collide', False)) + data['vr_fill_mound_model'] = ba.getmodel('stepRightUpVRFillMound') + data['vr_fill_mound_tex'] = ba.gettexture('vrFillMound') + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['model_bottom'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_mound_model'], + 'lighting': False, + 'vr_only': True, + 'color': (0.53, 0.57, 0.5), + 'background': True, + 'color_texture': self.preloaddata['vr_fill_mound_tex'] + }) + # in co-op mode games, put up a wall to prevent players + # from getting in the turrets (that would foil our brilliant AI) + if isinstance(ba.getsession(), ba.CoopSession): + cmodel = self.preloaddata['player_wall_collide_model'] + self.player_wall = ba.newnode( + 'terrain', + attrs={ + 'collide_model': cmodel, + 'affect_bg_dynamics': False, + 'materials': [self.preloaddata['player_wall_material']] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.2, 1.17, 1.1) + gnode.ambient_color = (1.2, 1.17, 1.1) + gnode.vignette_outer = (0.6, 0.6, 0.64) + gnode.vignette_inner = (0.95, 0.95, 0.93) + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + # count anything off our ground level as safe (for our platforms) + # see if we're within edge_box + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + + +class Rampage(ba.Map): + """Wee little map with ramps on the sides.""" + + from bastd.mapdata import rampage as defs + + name = 'Rampage' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee', 'keep_away', 'team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'rampagePreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('rampageLevel'), + 'bottom_model': ba.getmodel('rampageLevelBottom'), + 'collide_model': ba.getcollidemodel('rampageLevelCollide'), + 'tex': ba.gettexture('rampageLevelColor'), + 'bgtex': ba.gettexture('rampageBGColor'), + 'bgtex2': ba.gettexture('rampageBGColor2'), + 'bgmodel': ba.getmodel('rampageBG'), + 'bgmodel2': ba.getmodel('rampageBG2'), + 'vr_fill_model': ba.getmodel('rampageVRFill'), + 'railing_collide_model': ba.getcollidemodel('rampageBumper') + } + return data + + def __init__(self) -> None: + super().__init__(vr_overlay_offset=(0, 0, 2)) + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + self.bottom = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bottom_model'], + 'lighting': False, + 'color_texture': self.preloaddata['tex'] + }) + self.bg2 = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel2'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex2'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['bgtex2'] + }) + self.railing = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['railing_collide_model'], + 'materials': [shared.railing_material], + 'bumper': True + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.2, 1.1, 0.97) + gnode.ambient_color = (1.3, 1.2, 1.03) + gnode.vignette_outer = (0.62, 0.64, 0.69) + gnode.vignette_inner = (0.97, 0.95, 0.93) + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 diff --git a/dist/ba_data/python/bastd/session/__init__.py b/dist/ba_data/python/bastd/session/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/session/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/session/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/session/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/session/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/stdmap.py b/dist/ba_data/python/bastd/stdmap.py new file mode 100644 index 0000000..237ea4a --- /dev/null +++ b/dist/ba_data/python/bastd/stdmap.py @@ -0,0 +1,37 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines standard map type.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Dict, Any, Optional + + +def _get_map_data(name: str) -> Dict[str, Any]: + import json + print('Would get map data', name) + with open('ba_data/data/maps/' + name + '.json') as infile: + mapdata = json.loads(infile.read()) + assert isinstance(mapdata, dict) + return mapdata + + +class StdMap(ba.Map): + """A map completely defined by asset data. + + """ + _data: Optional[Dict[str, Any]] = None + + @classmethod + def _getdata(cls) -> Dict[str, Any]: + if cls._data is None: + cls._data = _get_map_data('bridgit') + return cls._data + + def __init__(self) -> None: + super().__init__() diff --git a/dist/ba_data/python/bastd/tutorial.py b/dist/ba_data/python/bastd/tutorial.py new file mode 100644 index 0000000..39bad74 --- /dev/null +++ b/dist/ba_data/python/bastd/tutorial.py @@ -0,0 +1,2403 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Wrangles the game tutorial sequence.""" + +# Not too concerned with keeping this old module pretty; +# don't expect to be revisiting it. +# pylint: disable=too-many-branches +# pylint: disable=too-many-statements +# pylint: disable=too-many-lines +# pylint: disable=missing-function-docstring, missing-class-docstring +# pylint: disable=invalid-name +# pylint: disable=too-many-locals +# pylint: disable=unused-argument +# pylint: disable=unused-variable + +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.actor import spaz as basespaz + +if TYPE_CHECKING: + from typing import (Any, Optional, Dict, List, Tuple, Callable, Sequence, + Union) + + +def _safesetattr(node: Optional[ba.Node], attr: str, value: Any) -> None: + if node: + setattr(node, attr, value) + + +class ButtonPress: + + def __init__(self, + button: str, + delay: int = 0, + release: bool = True, + release_delay: int = 0): + self._button = button + self._delay = delay + self._release = release + self._release_delay = release_delay + + def run(self, a: TutorialActivity) -> None: + s = a.current_spaz + assert s is not None + img: Optional[ba.Node] + release_call: Optional[Callable] + color: Optional[Sequence[float]] + if self._button == 'punch': + call = s.on_punch_press + release_call = s.on_punch_release + img = a.punch_image + color = a.punch_image_color + elif self._button == 'jump': + call = s.on_jump_press + release_call = s.on_jump_release + img = a.jump_image + color = a.jump_image_color + elif self._button == 'bomb': + call = s.on_bomb_press + release_call = s.on_bomb_release + img = a.bomb_image + color = a.bomb_image_color + elif self._button == 'pickUp': + call = s.on_pickup_press + release_call = s.on_pickup_release + img = a.pickup_image + color = a.pickup_image_color + elif self._button == 'run': + call = ba.Call(s.on_run, 1.0) + release_call = ba.Call(s.on_run, 0.0) + img = None + color = None + else: + raise Exception(f'invalid button: {self._button}') + + brightness = 4.0 + if color is not None: + c_bright = list(color) + c_bright[0] *= brightness + c_bright[1] *= brightness + c_bright[2] *= brightness + else: + c_bright = [1.0, 1.0, 1.0] + + if self._delay == 0: + call() + if img is not None: + img.color = c_bright + img.vr_depth = -40 + else: + ba.timer(self._delay, call, timeformat=ba.TimeFormat.MILLISECONDS) + if img is not None: + ba.timer(self._delay, + ba.Call(_safesetattr, img, 'color', c_bright), + timeformat=ba.TimeFormat.MILLISECONDS) + ba.timer(self._delay, + ba.Call(_safesetattr, img, 'vr_depth', -30), + timeformat=ba.TimeFormat.MILLISECONDS) + if self._release: + if self._delay == 0 and self._release_delay == 0: + release_call() + else: + ba.timer(0.001 * (self._delay + self._release_delay), + release_call) + if img is not None: + ba.timer(self._delay + self._release_delay + 100, + ba.Call(_safesetattr, img, 'color', color), + timeformat=ba.TimeFormat.MILLISECONDS) + ba.timer(self._delay + self._release_delay + 100, + ba.Call(_safesetattr, img, 'vr_depth', -20), + timeformat=ba.TimeFormat.MILLISECONDS) + + +class ButtonRelease: + + def __init__(self, button: str, delay: int = 0): + self._button = button + self._delay = delay + + def run(self, a: TutorialActivity) -> None: + s = a.current_spaz + assert s is not None + call: Optional[Callable] + img: Optional[ba.Node] + color: Optional[Sequence[float]] + if self._button == 'punch': + call = s.on_punch_release + img = a.punch_image + color = a.punch_image_color + elif self._button == 'jump': + call = s.on_jump_release + img = a.jump_image + color = a.jump_image_color + elif self._button == 'bomb': + call = s.on_bomb_release + img = a.bomb_image + color = a.bomb_image_color + elif self._button == 'pickUp': + call = s.on_pickup_press + img = a.pickup_image + color = a.pickup_image_color + elif self._button == 'run': + call = ba.Call(s.on_run, 0.0) + img = None + color = None + else: + raise Exception('invalid button: ' + self._button) + if self._delay == 0: + call() + else: + ba.timer(self._delay, call, timeformat=ba.TimeFormat.MILLISECONDS) + if img is not None: + ba.timer(self._delay + 100, + ba.Call(_safesetattr, img, 'color', color), + timeformat=ba.TimeFormat.MILLISECONDS) + ba.timer(self._delay + 100, + ba.Call(_safesetattr, img, 'vr_depth', -20), + timeformat=ba.TimeFormat.MILLISECONDS) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.pressed = False + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + pass + + +class TutorialActivity(ba.Activity[Player, Team]): + + def __init__(self, settings: dict = None): + from bastd.maps import Rampage + if settings is None: + settings = {} + super().__init__(settings) + self.current_spaz: Optional[basespaz.Spaz] = None + self._benchmark_type = getattr(ba.getsession(), 'benchmark_type', None) + self.last_start_time: Optional[int] = None + self.cycle_times: List[int] = [] + self.allow_pausing = True + self.allow_kick_idle_players = False + self._issued_warning = False + self._map_type = Rampage + self._map_type.preload() + self._jump_button_tex = ba.gettexture('buttonJump') + self._pick_up_button_tex = ba.gettexture('buttonPickUp') + self._bomb_button_tex = ba.gettexture('buttonBomb') + self._punch_button_tex = ba.gettexture('buttonPunch') + self._r = 'tutorial' + self._have_skipped = False + self.stick_image_position_x = self.stick_image_position_y = 0.0 + self.spawn_sound = ba.getsound('spawn') + self.map: Optional[ba.Map] = None + self.text: Optional[ba.Node] = None + self._skip_text: Optional[ba.Node] = None + self._skip_count_text: Optional[ba.Node] = None + self._scale: Optional[float] = None + self._stick_base_position: Tuple[float, float] = (0.0, 0.0) + self._stick_nub_position: Tuple[float, float] = (0.0, 0.0) + self._stick_base_image_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0) + self._stick_nub_image_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0) + self._time: int = -1 + self.punch_image_color = (1.0, 1.0, 1.0) + self.punch_image: Optional[ba.Node] = None + self.bomb_image: Optional[ba.Node] = None + self.jump_image: Optional[ba.Node] = None + self.pickup_image: Optional[ba.Node] = None + self._stick_base_image: Optional[ba.Node] = None + self._stick_nub_image: Optional[ba.Node] = None + self.bomb_image_color = (1.0, 1.0, 1.0) + self.pickup_image_color = (1.0, 1.0, 1.0) + self.control_ui_nodes: List[ba.Node] = [] + self.spazzes: Dict[int, basespaz.Spaz] = {} + self.jump_image_color = (1.0, 1.0, 1.0) + self._entries: List[Any] = [] + self._read_entries_timer: Optional[ba.Timer] = None + self._entry_timer: Optional[ba.Timer] = None + + def on_transition_in(self) -> None: + super().on_transition_in() + ba.setmusic(ba.MusicType.CHAR_SELECT, continuous=True) + self.map = self._map_type() + + def on_begin(self) -> None: + super().on_begin() + + ba.set_analytics_screen('Tutorial Start') + _ba.increment_analytics_count('Tutorial start') + + if bool(False): + # Buttons on top. + text_y = 140 + buttons_y = 250 + else: + # Buttons on bottom. + text_y = 260 + buttons_y = 160 + + # Need different versions of this: taps/buttons/keys. + self.text = ba.newnode('text', + attrs={ + 'text': '', + 'scale': 1.9, + 'position': (0, text_y), + 'maxwidth': 500, + 'flatness': 0.0, + 'shadow': 0.5, + 'h_align': 'center', + 'v_align': 'center', + 'v_attach': 'center' + }) + + # Need different versions of this: taps/buttons/keys. + txt = ba.Lstr( + resource=self._r + + '.cpuBenchmarkText') if self._benchmark_type == 'cpu' else ba.Lstr( + resource=self._r + '.toSkipPressAnythingText') + t = self._skip_text = ba.newnode('text', + attrs={ + 'text': txt, + 'maxwidth': 900, + 'scale': 1.1, + 'vr_depth': 100, + 'position': (0, 30), + 'h_align': 'center', + 'v_align': 'center', + 'v_attach': 'bottom' + }) + ba.animate(t, 'opacity', {1.0: 0.0, 2.0: 0.7}) + self._skip_count_text = ba.newnode('text', + attrs={ + 'text': '', + 'scale': 1.4, + 'vr_depth': 90, + 'position': (0, 70), + 'h_align': 'center', + 'v_align': 'center', + 'v_attach': 'bottom' + }) + + ouya = False + + self._scale = scale = 0.6 + center_offs = 130.0 * scale + offs = 65.0 * scale + position = (0, buttons_y) + image_size = 90.0 * scale + image_size_2 = 220.0 * scale + nub_size = 110.0 * scale + p = (position[0] + center_offs, position[1] - offs) + + def _sc(r: float, g: float, b: float) -> Tuple[float, float, float]: + return 0.6 * r, 0.6 * g, 0.6 * b + + self.jump_image_color = c = _sc(0.4, 1, 0.4) + self.jump_image = ba.newnode('image', + attrs={ + 'texture': self._jump_button_tex, + 'absolute_scale': True, + 'vr_depth': -20, + 'position': p, + 'scale': (image_size, image_size), + 'color': c + }) + p = (position[0] + center_offs - offs, position[1]) + self.punch_image_color = c = _sc(0.2, 0.6, 1) if ouya else _sc( + 1, 0.7, 0.3) + self.punch_image = ba.newnode( + 'image', + attrs={ + 'texture': ba.gettexture('buttonPunch'), + 'absolute_scale': True, + 'vr_depth': -20, + 'position': p, + 'scale': (image_size, image_size), + 'color': c + }) + p = (position[0] + center_offs + offs, position[1]) + self.bomb_image_color = c = _sc(1, 0.3, 0.3) + self.bomb_image = ba.newnode( + 'image', + attrs={ + 'texture': ba.gettexture('buttonBomb'), + 'absolute_scale': True, + 'vr_depth': -20, + 'position': p, + 'scale': (image_size, image_size), + 'color': c + }) + p = (position[0] + center_offs, position[1] + offs) + self.pickup_image_color = c = _sc(1, 0.8, 0.3) if ouya else _sc( + 0.5, 0.5, 1) + self.pickup_image = ba.newnode( + 'image', + attrs={ + 'texture': ba.gettexture('buttonPickUp'), + 'absolute_scale': True, + 'vr_depth': -20, + 'position': p, + 'scale': (image_size, image_size), + 'color': c + }) + + self._stick_base_position = p = (position[0] - center_offs, + position[1]) + self._stick_base_image_color = c2 = (0.25, 0.25, 0.25, 1.0) + self._stick_base_image = ba.newnode( + 'image', + attrs={ + 'texture': ba.gettexture('nub'), + 'absolute_scale': True, + 'vr_depth': -40, + 'position': p, + 'scale': (image_size_2, image_size_2), + 'color': c2 + }) + self._stick_nub_position = p = (position[0] - center_offs, position[1]) + self._stick_nub_image_color = c3 = (0.4, 0.4, 0.4, 1.0) + self._stick_nub_image = ba.newnode('image', + attrs={ + 'texture': ba.gettexture('nub'), + 'absolute_scale': True, + 'position': p, + 'scale': (nub_size, nub_size), + 'color': c3 + }) + self.control_ui_nodes = [ + self.jump_image, self.punch_image, self.bomb_image, + self.pickup_image, self._stick_base_image, self._stick_nub_image + ] + for n in self.control_ui_nodes: + n.opacity = 0.0 + self._read_entries() + + def set_stick_image_position(self, x: float, y: float) -> None: + + # Clamp this to a circle. + len_squared = x * x + y * y + if len_squared > 1.0: + length = math.sqrt(len_squared) + mult = 1.0 / length + x *= mult + y *= mult + + self.stick_image_position_x = x + self.stick_image_position_y = y + offs = 50.0 + assert self._scale is not None + p = [ + self._stick_nub_position[0] + x * offs * self._scale, + self._stick_nub_position[1] + y * offs * self._scale + ] + c = list(self._stick_nub_image_color) + if abs(x) > 0.1 or abs(y) > 0.1: + c[0] *= 2.0 + c[1] *= 4.0 + c[2] *= 2.0 + assert self._stick_nub_image is not None + self._stick_nub_image.position = p + self._stick_nub_image.color = c + c = list(self._stick_base_image_color) + if abs(x) > 0.1 or abs(y) > 0.1: + c[0] *= 1.5 + c[1] *= 1.5 + c[2] *= 1.5 + assert self._stick_base_image is not None + self._stick_base_image.color = c + + def _read_entries(self) -> None: + try: + + class Reset: + + def __init__(self) -> None: + pass + + def run(self, a: TutorialActivity) -> None: + + # if we're looping, print out how long each cycle took + # print out how long each cycle took.. + if a.last_start_time is not None: + tval = ba.time( + ba.TimeType.REAL, + ba.TimeFormat.MILLISECONDS) - a.last_start_time + assert isinstance(tval, int) + diff = tval + a.cycle_times.append(diff) + ba.screenmessage( + 'cycle time: ' + str(diff) + ' (average: ' + + str(sum(a.cycle_times) / len(a.cycle_times)) + ')') + tval = ba.time(ba.TimeType.REAL, + ba.TimeFormat.MILLISECONDS) + assert isinstance(tval, int) + a.last_start_time = tval + + assert a.text + a.text.text = '' + for spaz in list(a.spazzes.values()): + spaz.handlemessage(ba.DieMessage(immediate=True)) + a.spazzes = {} + a.current_spaz = None + for n in a.control_ui_nodes: + n.opacity = 0.0 + a.set_stick_image_position(0, 0) + + # Can be used for debugging. + class SetSpeed: + + def __init__(self, speed: int): + self._speed = speed + + def run(self, a: TutorialActivity) -> None: + print('setting to', self._speed) + _ba.set_debug_speed_exponent(self._speed) + + class RemoveGloves: + + def __init__(self) -> None: + pass + + def run(self, a: TutorialActivity) -> None: + # pylint: disable=protected-access + assert a.current_spaz is not None + # noinspection PyProtectedMember + a.current_spaz._gloves_wear_off() + + class KillSpaz: + + def __init__(self, num: int, explode: bool = False): + self._num = num + self._explode = explode + + def run(self, a: TutorialActivity) -> None: + if self._explode: + a.spazzes[self._num].shatter() + del a.spazzes[self._num] + + class SpawnSpaz: + + def __init__(self, + num: int, + position: Sequence[float], + color: Sequence[float] = (1.0, 1.0, 1.0), + make_current: bool = False, + relative_to: int = None, + name: Union[str, ba.Lstr] = '', + flash: bool = True, + angle: float = 0.0): + self._num = num + self._position = position + self._make_current = make_current + self._color = color + self._relative_to = relative_to + self._name = name + self._flash = flash + self._angle = angle + + def run(self, a: TutorialActivity) -> None: + + # if they gave a 'relative to' spaz, position is relative + # to them + pos: Sequence[float] + if self._relative_to is not None: + snode = a.spazzes[self._relative_to].node + assert snode + their_pos = snode.position + pos = (their_pos[0] + self._position[0], + their_pos[1] + self._position[1], + their_pos[2] + self._position[2]) + else: + pos = self._position + + # if there's already a spaz at this spot, insta-kill it + if self._num in a.spazzes: + a.spazzes[self._num].handlemessage( + ba.DieMessage(immediate=True)) + + s = a.spazzes[self._num] = basespaz.Spaz( + color=self._color, + start_invincible=self._flash, + demo_mode=True) + + # FIXME: Should extend spaz to support Lstr names. + assert s.node + if isinstance(self._name, ba.Lstr): + s.node.name = self._name.evaluate() + else: + s.node.name = self._name + s.node.name_color = self._color + s.handlemessage(ba.StandMessage(pos, self._angle)) + if self._make_current: + a.current_spaz = s + if self._flash: + ba.playsound(a.spawn_sound, position=pos) + + class Powerup: + + def __init__(self, + num: int, + position: Sequence[float], + color: Sequence[float] = (1.0, 1.0, 1.0), + make_current: bool = False, + relative_to: int = None): + self._position = position + self._relative_to = relative_to + + def run(self, a: TutorialActivity) -> None: + # If they gave a 'relative to' spaz, position is relative + # to them. + pos: Sequence[float] + if self._relative_to is not None: + snode = a.spazzes[self._relative_to].node + assert snode + their_pos = snode.position + pos = (their_pos[0] + self._position[0], + their_pos[1] + self._position[1], + their_pos[2] + self._position[2]) + else: + pos = self._position + from bastd.actor import powerupbox + powerupbox.PowerupBox(position=pos, + poweruptype='punch').autoretain() + + class Delay: + + def __init__(self, time: int) -> None: + self._time = time + + def run(self, a: TutorialActivity) -> int: + return self._time + + class AnalyticsScreen: + + def __init__(self, screen: str) -> None: + self._screen = screen + + def run(self, a: TutorialActivity) -> None: + ba.set_analytics_screen(self._screen) + + class DelayOld: + + def __init__(self, time: int) -> None: + self._time = time + + def run(self, a: TutorialActivity) -> int: + return int(0.9 * self._time) + + class DelayOld2: + + def __init__(self, time: int) -> None: + self._time = time + + def run(self, a: TutorialActivity) -> int: + return int(0.8 * self._time) + + class End: + + def __init__(self) -> None: + pass + + def run(self, a: TutorialActivity) -> None: + _ba.increment_analytics_count('Tutorial finish') + a.end() + + class Move: + + def __init__(self, x: float, y: float): + self._x = float(x) + self._y = float(y) + + def run(self, a: TutorialActivity) -> None: + s = a.current_spaz + assert s + # FIXME: Game should take floats for this. + x_clamped = self._x + y_clamped = self._y + s.on_move_left_right(x_clamped) + s.on_move_up_down(y_clamped) + a.set_stick_image_position(self._x, self._y) + + class MoveLR: + + def __init__(self, x: float): + self._x = float(x) + + def run(self, a: TutorialActivity) -> None: + s = a.current_spaz + assert s + # FIXME: Game should take floats for this. + x_clamped = self._x + s.on_move_left_right(x_clamped) + a.set_stick_image_position(self._x, + a.stick_image_position_y) + + class MoveUD: + + def __init__(self, y: float): + self._y = float(y) + + def run(self, a: TutorialActivity) -> None: + s = a.current_spaz + assert s + # FIXME: Game should take floats for this. + y_clamped = self._y + s.on_move_up_down(y_clamped) + a.set_stick_image_position(a.stick_image_position_x, + self._y) + + class Bomb(ButtonPress): + + def __init__(self, + delay: int = 0, + release: bool = True, + release_delay: int = 500): + ButtonPress.__init__(self, + 'bomb', + delay=delay, + release=release, + release_delay=release_delay) + + class Jump(ButtonPress): + + def __init__(self, + delay: int = 0, + release: bool = True, + release_delay: int = 500): + ButtonPress.__init__(self, + 'jump', + delay=delay, + release=release, + release_delay=release_delay) + + class Punch(ButtonPress): + + def __init__(self, + delay: int = 0, + release: bool = True, + release_delay: int = 500): + ButtonPress.__init__(self, + 'punch', + delay=delay, + release=release, + release_delay=release_delay) + + class PickUp(ButtonPress): + + def __init__(self, + delay: int = 0, + release: bool = True, + release_delay: int = 500): + ButtonPress.__init__(self, + 'pickUp', + delay=delay, + release=release, + release_delay=release_delay) + + class Run(ButtonPress): + + def __init__(self, + delay: int = 0, + release: bool = True, + release_delay: int = 500): + ButtonPress.__init__(self, + 'run', + delay=delay, + release=release, + release_delay=release_delay) + + class BombRelease(ButtonRelease): + + def __init__(self, delay: int = 0): + super().__init__('bomb', delay=delay) + + class JumpRelease(ButtonRelease): + + def __init__(self, delay: int = 0): + super().__init__('jump', delay=delay) + + class PunchRelease(ButtonRelease): + + def __init__(self, delay: int = 0): + super().__init__('punch', delay=delay) + + class PickUpRelease(ButtonRelease): + + def __init__(self, delay: int = 0): + super().__init__('pickUp', delay=delay) + + class RunRelease(ButtonRelease): + + def __init__(self, delay: int = 0): + super().__init__('run', delay=delay) + + class ShowControls: + + def __init__(self) -> None: + pass + + def run(self, a: TutorialActivity) -> None: + for n in a.control_ui_nodes: + ba.animate(n, 'opacity', {0.0: 0.0, 1.0: 1.0}) + + class Text: + + def __init__(self, text: Union[str, ba.Lstr]): + self.text = text + + def run(self, a: TutorialActivity) -> None: + assert a.text + a.text.text = self.text + + class PrintPos: + + def __init__(self, spaz_num: int = None): + self._spaz_num = spaz_num + + def run(self, a: TutorialActivity) -> None: + if self._spaz_num is None: + s = a.current_spaz + else: + s = a.spazzes[self._spaz_num] + assert s and s.node + t = list(s.node.position) + print('RestorePos(' + str((t[0], t[1] - 1.0, t[2])) + '),') + + class RestorePos: + + def __init__(self, pos: Sequence[float]) -> None: + self._pos = pos + + def run(self, a: TutorialActivity) -> None: + s = a.current_spaz + assert s + s.handlemessage(ba.StandMessage(self._pos, 0)) + + class Celebrate: + + def __init__(self, + celebrate_type: str = 'both', + spaz_num: int = None, + duration: int = 1000): + self._spaz_num = spaz_num + self._celebrate_type = celebrate_type + self._duration = duration + + def run(self, a: TutorialActivity) -> None: + if self._spaz_num is None: + s = a.current_spaz + else: + s = a.spazzes[self._spaz_num] + assert s and s.node + if self._celebrate_type == 'right': + s.node.handlemessage('celebrate_r', self._duration) + elif self._celebrate_type == 'left': + s.node.handlemessage('celebrate_l', self._duration) + elif self._celebrate_type == 'both': + s.node.handlemessage('celebrate', self._duration) + else: + raise Exception('invalid celebrate type ' + + self._celebrate_type) + + self._entries = [ + Reset(), + SpawnSpaz(0, (0, 5.5, -3.0), make_current=True), + DelayOld(1000), + AnalyticsScreen('Tutorial Section 1'), + Text(ba.Lstr(resource=self._r + '.phrase01Text')), # hi there + Celebrate('left'), + DelayOld(2000), + Text( + ba.Lstr(resource=self._r + '.phrase02Text', + subs=[ + ('${APP_NAME}', ba.Lstr(resource='titleText')) + ])), # welcome to + DelayOld(80), + Run(release=False), + Jump(release=False), + MoveLR(1), + MoveUD(0), + DelayOld(70), + RunRelease(), + JumpRelease(), + DelayOld(60), + MoveUD(1), + DelayOld(30), + MoveLR(0), + DelayOld(90), + MoveLR(-1), + DelayOld(20), + MoveUD(0), + DelayOld(70), + MoveUD(-1), + DelayOld(20), + MoveLR(0), + DelayOld(80), + MoveUD(0), + DelayOld(1500), + Text(ba.Lstr(resource=self._r + + '.phrase03Text')), # here's a few tips + DelayOld(1000), + ShowControls(), + DelayOld(1000), + Jump(), + DelayOld(1000), + Jump(), + DelayOld(1000), + AnalyticsScreen('Tutorial Section 2'), + Text( + ba.Lstr(resource=self._r + '.phrase04Text', + subs=[ + ('${APP_NAME}', ba.Lstr(resource='titleText')) + ])), # many things are based on physics + DelayOld(20), + MoveUD(0), + DelayOld(60), + MoveLR(0), + DelayOld(10), + MoveLR(0), + MoveUD(0), + DelayOld(10), + MoveLR(0), + MoveUD(0), + DelayOld(20), + MoveUD(-0.0575579), + DelayOld(10), + MoveUD(-0.207831), + DelayOld(30), + MoveUD(-0.309793), + DelayOld(10), + MoveUD(-0.474502), + DelayOld(10), + MoveLR(0.00390637), + MoveUD(-0.647053), + DelayOld(20), + MoveLR(-0.0745262), + MoveUD(-0.819605), + DelayOld(10), + MoveLR(-0.168645), + MoveUD(-0.937254), + DelayOld(30), + MoveLR(-0.294137), + MoveUD(-1), + DelayOld(10), + MoveLR(-0.411786), + DelayOld(10), + MoveLR(-0.639241), + DelayOld(30), + MoveLR(-0.75689), + DelayOld(10), + MoveLR(-0.905911), + DelayOld(20), + MoveLR(-1), + DelayOld(50), + MoveUD(-0.960784), + DelayOld(20), + MoveUD(-0.819605), + MoveUD(-0.61568), + DelayOld(20), + MoveUD(-0.427442), + DelayOld(20), + MoveUD(-0.231361), + DelayOld(10), + MoveUD(-0.00390637), + DelayOld(30), + MoveUD(0.333354), + MoveUD(0.584338), + DelayOld(20), + MoveUD(0.764733), + DelayOld(30), + MoveLR(-0.803949), + MoveUD(0.913755), + DelayOld(10), + MoveLR(-0.647084), + MoveUD(0.992187), + DelayOld(20), + MoveLR(-0.435316), + MoveUD(1), + DelayOld(20), + MoveLR(-0.168645), + MoveUD(0.976501), + MoveLR(0.0744957), + MoveUD(0.905911), + DelayOld(20), + MoveLR(0.270577), + MoveUD(0.843165), + DelayOld(20), + MoveLR(0.435286), + MoveUD(0.780419), + DelayOld(10), + MoveLR(0.66274), + MoveUD(0.647084), + DelayOld(30), + MoveLR(0.803919), + MoveUD(0.458846), + MoveLR(0.929411), + MoveUD(0.223548), + DelayOld(20), + MoveLR(0.95294), + MoveUD(0.137272), + DelayOld(20), + MoveLR(1), + MoveUD(-0.0509659), + DelayOld(20), + MoveUD(-0.247047), + DelayOld(20), + MoveUD(-0.443129), + DelayOld(20), + MoveUD(-0.694113), + MoveUD(-0.921567), + DelayOld(30), + MoveLR(0.858821), + MoveUD(-1), + DelayOld(10), + MoveLR(0.68627), + DelayOld(10), + MoveLR(0.364696), + DelayOld(20), + MoveLR(0.0509659), + DelayOld(20), + MoveLR(-0.223548), + DelayOld(10), + MoveLR(-0.600024), + MoveUD(-0.913724), + DelayOld(30), + MoveLR(-0.858852), + MoveUD(-0.717643), + MoveLR(-1), + MoveUD(-0.474502), + DelayOld(20), + MoveUD(-0.396069), + DelayOld(20), + MoveUD(-0.286264), + DelayOld(20), + MoveUD(-0.137242), + DelayOld(20), + MoveUD(0.0353099), + DelayOld(10), + MoveUD(0.32551), + DelayOld(20), + MoveUD(0.592181), + DelayOld(10), + MoveUD(0.851009), + DelayOld(10), + MoveUD(1), + DelayOld(30), + MoveLR(-0.764733), + DelayOld(20), + MoveLR(-0.403943), + MoveLR(-0.145116), + DelayOld(30), + MoveLR(0.0901822), + MoveLR(0.32548), + DelayOld(30), + MoveLR(0.560778), + MoveUD(0.929441), + DelayOld(20), + MoveLR(0.709799), + MoveUD(0.73336), + MoveLR(0.803919), + MoveUD(0.545122), + DelayOld(20), + MoveLR(0.882351), + MoveUD(0.356883), + DelayOld(10), + MoveLR(0.968627), + MoveUD(0.113742), + DelayOld(20), + MoveLR(0.992157), + MoveUD(-0.0823389), + DelayOld(30), + MoveUD(-0.309793), + DelayOld(10), + MoveUD(-0.545091), + DelayOld(20), + MoveLR(0.882351), + MoveUD(-0.874508), + DelayOld(20), + MoveLR(0.756859), + MoveUD(-1), + DelayOld(10), + MoveLR(0.576464), + DelayOld(20), + MoveLR(0.254891), + DelayOld(10), + MoveLR(-0.0274667), + DelayOld(10), + MoveLR(-0.356883), + DelayOld(30), + MoveLR(-0.592181), + MoveLR(-0.827479), + MoveUD(-0.921567), + DelayOld(20), + MoveLR(-1), + MoveUD(-0.749016), + DelayOld(20), + MoveUD(-0.61568), + DelayOld(10), + MoveUD(-0.403912), + DelayOld(20), + MoveUD(-0.207831), + DelayOld(10), + MoveUD(0.121586), + DelayOld(30), + MoveUD(0.34904), + DelayOld(10), + MoveUD(0.560808), + DelayOld(10), + MoveUD(0.827479), + DelayOld(30), + MoveUD(1), + DelayOld(20), + MoveLR(-0.976501), + MoveLR(-0.670614), + DelayOld(20), + MoveLR(-0.239235), + DelayOld(20), + MoveLR(0.160772), + DelayOld(20), + MoveLR(0.443129), + DelayOld(10), + MoveLR(0.68627), + MoveUD(0.976501), + DelayOld(30), + MoveLR(0.929411), + MoveUD(0.73336), + MoveLR(1), + MoveUD(0.482376), + DelayOld(20), + MoveUD(0.34904), + DelayOld(10), + MoveUD(0.160802), + DelayOld(30), + MoveUD(-0.0744957), + DelayOld(10), + MoveUD(-0.333323), + DelayOld(20), + MoveUD(-0.647053), + DelayOld(20), + MoveUD(-0.937254), + DelayOld(10), + MoveLR(0.858821), + MoveUD(-1), + DelayOld(10), + MoveLR(0.576464), + DelayOld(30), + MoveLR(0.184301), + DelayOld(10), + MoveLR(-0.121586), + DelayOld(10), + MoveLR(-0.474532), + DelayOld(30), + MoveLR(-0.670614), + MoveLR(-0.851009), + DelayOld(30), + MoveLR(-1), + MoveUD(-0.968627), + DelayOld(20), + MoveUD(-0.843135), + DelayOld(10), + MoveUD(-0.631367), + DelayOld(20), + MoveUD(-0.403912), + MoveUD(-0.176458), + DelayOld(20), + MoveUD(0.0902127), + DelayOld(20), + MoveUD(0.380413), + DelayOld(10), + MoveUD(0.717673), + DelayOld(30), + MoveUD(1), + DelayOld(10), + MoveLR(-0.741203), + DelayOld(20), + MoveLR(-0.458846), + DelayOld(10), + MoveLR(-0.145116), + DelayOld(10), + MoveLR(0.0980255), + DelayOld(20), + MoveLR(0.294107), + DelayOld(30), + MoveLR(0.466659), + MoveLR(0.717643), + MoveUD(0.796106), + DelayOld(20), + MoveLR(0.921567), + MoveUD(0.443159), + DelayOld(20), + MoveLR(1), + MoveUD(0.145116), + DelayOld(10), + MoveUD(-0.0274361), + DelayOld(30), + MoveUD(-0.223518), + MoveUD(-0.427442), + DelayOld(20), + MoveUD(-0.874508), + DelayOld(20), + MoveUD(-1), + DelayOld(10), + MoveLR(0.929411), + DelayOld(20), + MoveLR(0.68627), + DelayOld(20), + MoveLR(0.364696), + DelayOld(20), + MoveLR(0.0431227), + DelayOld(10), + MoveLR(-0.333354), + DelayOld(20), + MoveLR(-0.639241), + DelayOld(20), + MoveLR(-0.968657), + MoveUD(-0.968627), + DelayOld(20), + MoveLR(-1), + MoveUD(-0.890194), + MoveUD(-0.866665), + DelayOld(20), + MoveUD(-0.749016), + DelayOld(20), + MoveUD(-0.529405), + DelayOld(20), + MoveUD(-0.30195), + DelayOld(10), + MoveUD(-0.00390637), + DelayOld(10), + MoveUD(0.262764), + DelayOld(30), + MoveLR(-0.600024), + MoveUD(0.458846), + DelayOld(10), + MoveLR(-0.294137), + MoveUD(0.482376), + DelayOld(20), + MoveLR(-0.200018), + MoveUD(0.505905), + DelayOld(10), + MoveLR(-0.145116), + MoveUD(0.545122), + DelayOld(20), + MoveLR(-0.0353099), + MoveUD(0.584338), + DelayOld(20), + MoveLR(0.137242), + MoveUD(0.592181), + DelayOld(20), + MoveLR(0.30195), + DelayOld(10), + MoveLR(0.490188), + DelayOld(10), + MoveLR(0.599994), + MoveUD(0.529435), + DelayOld(30), + MoveLR(0.66274), + MoveUD(0.3961), + DelayOld(20), + MoveLR(0.670583), + MoveUD(0.231391), + MoveLR(0.68627), + MoveUD(0.0745262), + Move(0, -0.01), + DelayOld(100), + Move(0, 0), + DelayOld(1000), + Text(ba.Lstr(resource=self._r + + '.phrase05Text')), # for example when you punch.. + DelayOld(510), + Move(0, -0.01), + DelayOld(100), + Move(0, 0), + DelayOld(500), + SpawnSpaz(0, (-0.09249162673950195, 4.337906360626221, -2.3), + make_current=True, + flash=False), + SpawnSpaz(1, (-3.1, 4.3, -2.0), + make_current=False, + color=(1, 1, 0.4), + name=ba.Lstr(resource=self._r + '.randomName1Text')), + Move(-1.0, 0), + DelayOld(1050), + Move(0, -0.01), + DelayOld(100), + Move(0, 0), + DelayOld(1000), + Text(ba.Lstr(resource=self._r + + '.phrase06Text')), # your damage is based + DelayOld(1200), + Move(-0.05, 0), + DelayOld(200), + Punch(), + DelayOld(800), + Punch(), + DelayOld(800), + Punch(), + DelayOld(800), + Move(0, -0.01), + DelayOld(100), + Move(0, 0), + Text( + ba.Lstr(resource=self._r + '.phrase07Text', + subs=[('${NAME}', + ba.Lstr(resource=self._r + + '.randomName1Text')) + ])), # see that didn't hurt fred + DelayOld(2000), + Celebrate('right', spaz_num=1), + DelayOld(1400), + Text(ba.Lstr( + resource=self._r + + '.phrase08Text')), # lets jump and spin to get more speed + DelayOld(30), + MoveLR(0), + DelayOld(40), + MoveLR(0), + DelayOld(40), + MoveLR(0), + DelayOld(130), + MoveLR(0), + DelayOld(100), + MoveLR(0), + DelayOld(10), + MoveLR(0.0480667), + DelayOld(40), + MoveLR(0.056093), + MoveLR(0.0681173), + DelayOld(30), + MoveLR(0.0801416), + DelayOld(10), + MoveLR(0.184301), + DelayOld(10), + MoveLR(0.207831), + DelayOld(20), + MoveLR(0.231361), + DelayOld(30), + MoveLR(0.239204), + DelayOld(30), + MoveLR(0.254891), + DelayOld(40), + MoveLR(0.270577), + DelayOld(10), + MoveLR(0.30195), + DelayOld(20), + MoveLR(0.341166), + DelayOld(30), + MoveLR(0.388226), + MoveLR(0.435286), + DelayOld(30), + MoveLR(0.490188), + DelayOld(10), + MoveLR(0.560778), + DelayOld(20), + MoveLR(0.599994), + DelayOld(10), + MoveLR(0.647053), + DelayOld(10), + MoveLR(0.68627), + DelayOld(30), + MoveLR(0.733329), + DelayOld(20), + MoveLR(0.764702), + DelayOld(10), + MoveLR(0.827448), + DelayOld(20), + MoveLR(0.874508), + DelayOld(20), + MoveLR(0.929411), + DelayOld(10), + MoveLR(1), + DelayOld(830), + MoveUD(0.0274667), + DelayOld(10), + MoveLR(0.95294), + MoveUD(0.113742), + DelayOld(30), + MoveLR(0.780389), + MoveUD(0.184332), + DelayOld(10), + MoveLR(0.27842), + MoveUD(0.0745262), + DelayOld(20), + MoveLR(0), + MoveUD(0), + DelayOld(390), + MoveLR(0), + MoveLR(0), + DelayOld(20), + MoveLR(0), + DelayOld(20), + MoveLR(0), + DelayOld(10), + MoveLR(-0.0537431), + DelayOld(20), + MoveLR(-0.215705), + DelayOld(30), + MoveLR(-0.388256), + MoveLR(-0.529435), + DelayOld(30), + MoveLR(-0.694143), + DelayOld(20), + MoveLR(-0.851009), + MoveUD(0.0588397), + DelayOld(10), + MoveLR(-1), + MoveUD(0.0745262), + Run(release=False), + DelayOld(200), + MoveUD(0.0509964), + DelayOld(30), + MoveUD(0.0117801), + DelayOld(20), + MoveUD(-0.0901822), + MoveUD(-0.372539), + DelayOld(30), + MoveLR(-0.898068), + MoveUD(-0.890194), + Jump(release=False), + DelayOld(20), + MoveLR(-0.647084), + MoveUD(-1), + MoveLR(-0.427473), + DelayOld(20), + MoveLR(-0.00393689), + DelayOld(10), + MoveLR(0.537248), + DelayOld(30), + MoveLR(1), + DelayOld(50), + RunRelease(), + JumpRelease(), + DelayOld(50), + MoveUD(-0.921567), + MoveUD(-0.749016), + DelayOld(30), + MoveUD(-0.552934), + DelayOld(10), + MoveUD(-0.247047), + DelayOld(20), + MoveUD(0.200018), + DelayOld(20), + MoveUD(0.670614), + MoveUD(1), + DelayOld(70), + MoveLR(0.97647), + DelayOld(20), + MoveLR(0.764702), + DelayOld(20), + MoveLR(0.364696), + DelayOld(20), + MoveLR(0.00390637), + MoveLR(-0.309824), + DelayOld(20), + MoveLR(-0.576495), + DelayOld(30), + MoveLR(-0.898068), + DelayOld(10), + MoveLR(-1), + MoveUD(0.905911), + DelayOld(20), + MoveUD(0.498062), + DelayOld(20), + MoveUD(0.0274667), + MoveUD(-0.403912), + DelayOld(20), + MoveUD(-1), + Run(release=False), + Jump(release=False), + DelayOld(10), + Punch(release=False), + DelayOld(70), + JumpRelease(), + DelayOld(110), + MoveLR(-0.976501), + RunRelease(), + PunchRelease(), + DelayOld(10), + MoveLR(-0.952971), + DelayOld(20), + MoveLR(-0.905911), + MoveLR(-0.827479), + DelayOld(20), + MoveLR(-0.75689), + DelayOld(30), + MoveLR(-0.73336), + MoveLR(-0.694143), + DelayOld(20), + MoveLR(-0.670614), + DelayOld(30), + MoveLR(-0.66277), + DelayOld(10), + MoveUD(-0.960784), + DelayOld(20), + MoveLR(-0.623554), + MoveUD(-0.874508), + DelayOld(10), + MoveLR(-0.545122), + MoveUD(-0.694113), + DelayOld(20), + MoveLR(-0.505905), + MoveUD(-0.474502), + DelayOld(20), + MoveLR(-0.458846), + MoveUD(-0.356853), + MoveLR(-0.364727), + MoveUD(-0.27842), + DelayOld(20), + MoveLR(0.00390637), + Move(0, 0), + DelayOld(1000), + Text(ba.Lstr(resource=self._r + + '.phrase09Text')), # ah that's better + DelayOld(1900), + AnalyticsScreen('Tutorial Section 3'), + Text(ba.Lstr(resource=self._r + + '.phrase10Text')), # running also helps + DelayOld(100), + SpawnSpaz(0, (-3.2, 4.3, -4.4), make_current=True, + flash=False), + SpawnSpaz(1, (3.3, 4.2, -5.8), + make_current=False, + color=(0.9, 0.5, 1.0), + name=ba.Lstr(resource=self._r + '.randomName2Text')), + DelayOld(1800), + Text(ba.Lstr(resource=self._r + + '.phrase11Text')), # hold ANY button to run + DelayOld(300), + MoveUD(0), + DelayOld(20), + MoveUD(-0.0520646), + DelayOld(20), + MoveLR(0), + MoveUD(-0.223518), + Run(release=False), + Jump(release=False), + DelayOld(10), + MoveLR(0.0980255), + MoveUD(-0.309793), + DelayOld(30), + MoveLR(0.160772), + MoveUD(-0.427442), + DelayOld(20), + MoveLR(0.231361), + MoveUD(-0.545091), + DelayOld(10), + MoveLR(0.317637), + MoveUD(-0.678426), + DelayOld(20), + MoveLR(0.396069), + MoveUD(-0.819605), + MoveLR(0.482345), + MoveUD(-0.913724), + DelayOld(20), + MoveLR(0.560778), + MoveUD(-1), + DelayOld(20), + MoveLR(0.607837), + DelayOld(10), + MoveLR(0.623524), + DelayOld(30), + MoveLR(0.647053), + DelayOld(20), + MoveLR(0.670583), + MoveLR(0.694113), + DelayOld(30), + MoveLR(0.733329), + DelayOld(20), + MoveLR(0.764702), + MoveLR(0.788232), + DelayOld(20), + MoveLR(0.827448), + DelayOld(10), + MoveLR(0.858821), + DelayOld(20), + MoveLR(0.921567), + DelayOld(30), + MoveLR(0.97647), + MoveLR(1), + DelayOld(130), + MoveUD(-0.960784), + DelayOld(20), + MoveUD(-0.921567), + DelayOld(30), + MoveUD(-0.866665), + MoveUD(-0.819605), + DelayOld(30), + MoveUD(-0.772546), + MoveUD(-0.725486), + DelayOld(30), + MoveUD(-0.631367), + DelayOld(10), + MoveUD(-0.552934), + DelayOld(20), + MoveUD(-0.474502), + DelayOld(10), + MoveUD(-0.403912), + DelayOld(30), + MoveUD(-0.356853), + DelayOld(30), + MoveUD(-0.34901), + DelayOld(20), + MoveUD(-0.333323), + DelayOld(20), + MoveUD(-0.32548), + DelayOld(10), + MoveUD(-0.30195), + DelayOld(20), + MoveUD(-0.27842), + DelayOld(30), + MoveUD(-0.254891), + MoveUD(-0.231361), + DelayOld(30), + MoveUD(-0.207831), + DelayOld(20), + MoveUD(-0.199988), + MoveUD(-0.176458), + DelayOld(30), + MoveUD(-0.137242), + MoveUD(-0.0823389), + DelayOld(20), + MoveUD(-0.0274361), + DelayOld(20), + MoveUD(0.00393689), + DelayOld(40), + MoveUD(0.0353099), + DelayOld(20), + MoveUD(0.113742), + DelayOld(10), + MoveUD(0.137272), + DelayOld(20), + MoveUD(0.160802), + MoveUD(0.184332), + DelayOld(20), + MoveUD(0.207862), + DelayOld(30), + MoveUD(0.247078), + MoveUD(0.262764), + DelayOld(20), + MoveUD(0.270608), + DelayOld(30), + MoveUD(0.294137), + MoveUD(0.32551), + DelayOld(30), + MoveUD(0.37257), + Celebrate('left', 1), + DelayOld(20), + MoveUD(0.498062), + MoveUD(0.560808), + DelayOld(30), + MoveUD(0.654927), + MoveUD(0.694143), + DelayOld(30), + MoveUD(0.741203), + DelayOld(20), + MoveUD(0.780419), + MoveUD(0.819636), + DelayOld(20), + MoveUD(0.843165), + DelayOld(20), + MoveUD(0.882382), + DelayOld(10), + MoveUD(0.913755), + DelayOld(30), + MoveUD(0.968657), + MoveUD(1), + DelayOld(560), + Punch(release=False), + DelayOld(210), + MoveUD(0.968657), + DelayOld(30), + MoveUD(0.75689), + PunchRelease(), + DelayOld(20), + MoveLR(0.95294), + MoveUD(0.435316), + RunRelease(), + JumpRelease(), + MoveLR(0.811762), + MoveUD(0.270608), + DelayOld(20), + MoveLR(0.670583), + MoveUD(0.160802), + DelayOld(20), + MoveLR(0.466659), + MoveUD(0.0588397), + DelayOld(10), + MoveLR(0.317637), + MoveUD(-0.00390637), + DelayOld(20), + MoveLR(0.0801416), + DelayOld(10), + MoveLR(0), + DelayOld(20), + MoveLR(0), + DelayOld(30), + MoveLR(0), + DelayOld(30), + MoveLR(0), + DelayOld(20), + MoveLR(0), + DelayOld(100), + MoveLR(0), + DelayOld(30), + MoveUD(0), + DelayOld(30), + MoveUD(0), + DelayOld(50), + MoveUD(0), + MoveUD(0), + DelayOld(30), + MoveLR(0), + MoveUD(-0.0520646), + MoveLR(0), + MoveUD(-0.0640889), + DelayOld(20), + MoveLR(0), + MoveUD(-0.0881375), + DelayOld(30), + MoveLR(-0.0498978), + MoveUD(-0.199988), + MoveLR(-0.121586), + MoveUD(-0.207831), + DelayOld(20), + MoveLR(-0.145116), + MoveUD(-0.223518), + DelayOld(30), + MoveLR(-0.152959), + MoveUD(-0.231361), + MoveLR(-0.192175), + MoveUD(-0.262734), + DelayOld(30), + MoveLR(-0.200018), + MoveUD(-0.27842), + DelayOld(20), + MoveLR(-0.239235), + MoveUD(-0.30195), + MoveUD(-0.309793), + DelayOld(40), + MoveUD(-0.333323), + DelayOld(10), + MoveUD(-0.34901), + DelayOld(30), + MoveUD(-0.372539), + MoveUD(-0.396069), + DelayOld(20), + MoveUD(-0.443129), + DelayOld(20), + MoveUD(-0.458815), + DelayOld(10), + MoveUD(-0.474502), + DelayOld(50), + MoveUD(-0.482345), + DelayOld(30), + MoveLR(-0.215705), + DelayOld(30), + MoveLR(-0.200018), + DelayOld(10), + MoveLR(-0.192175), + DelayOld(10), + MoveLR(-0.176489), + DelayOld(30), + MoveLR(-0.152959), + DelayOld(20), + MoveLR(-0.145116), + MoveLR(-0.121586), + MoveUD(-0.458815), + DelayOld(30), + MoveLR(-0.098056), + MoveUD(-0.419599), + DelayOld(10), + MoveLR(-0.0745262), + MoveUD(-0.333323), + DelayOld(10), + MoveLR(0.00390637), + MoveUD(0), + DelayOld(990), + MoveLR(0), + DelayOld(660), + MoveUD(0), + AnalyticsScreen('Tutorial Section 4'), + Text( + ba.Lstr(resource=self._r + + '.phrase12Text')), # for extra-awesome punches,... + DelayOld(200), + SpawnSpaz( + 0, + (2.368781805038452, 4.337533950805664, -4.360159873962402), + make_current=True, + flash=False), + SpawnSpaz( + 1, + (-3.2, 4.3, -4.5), + make_current=False, + color=(1.0, 0.7, 0.3), + # name=R.randomName3Text), + name=ba.Lstr(resource=self._r + '.randomName3Text')), + DelayOld(100), + Powerup(1, (2.5, 0.0, 0), relative_to=0), + Move(1, 0), + DelayOld(1700), + Move(0, -0.1), + DelayOld(100), + Move(0, 0), + DelayOld(500), + DelayOld(320), + MoveLR(0), + DelayOld(20), + MoveLR(0), + DelayOld(10), + MoveLR(0), + DelayOld(20), + MoveLR(-0.333354), + MoveLR(-0.592181), + DelayOld(20), + MoveLR(-0.788263), + DelayOld(20), + MoveLR(-1), + MoveUD(0.0353099), + MoveUD(0.0588397), + DelayOld(10), + Run(release=False), + DelayOld(780), + MoveUD(0.0274667), + MoveUD(0.00393689), + DelayOld(10), + MoveUD(-0.00390637), + DelayOld(440), + MoveUD(0.0353099), + DelayOld(20), + MoveUD(0.0588397), + DelayOld(10), + MoveUD(0.0902127), + DelayOld(260), + MoveUD(0.0353099), + DelayOld(30), + MoveUD(0.00393689), + DelayOld(10), + MoveUD(-0.00390637), + MoveUD(-0.0274361), + Celebrate('left', 1), + DelayOld(10), + MoveUD(-0.0823389), + DelayOld(30), + MoveUD(-0.176458), + MoveUD(-0.286264), + DelayOld(20), + MoveUD(-0.498032), + Jump(release=False), + MoveUD(-0.764702), + DelayOld(30), + MoveLR(-0.858852), + MoveUD(-1), + MoveLR(-0.780419), + DelayOld(20), + MoveLR(-0.717673), + DelayOld(10), + MoveLR(-0.552965), + DelayOld(10), + MoveLR(-0.341197), + DelayOld(10), + MoveLR(-0.0274667), + DelayOld(10), + MoveLR(0.27842), + DelayOld(20), + MoveLR(0.811762), + MoveLR(1), + RunRelease(), + JumpRelease(), + DelayOld(260), + MoveLR(0.95294), + DelayOld(30), + MoveLR(0.756859), + DelayOld(10), + MoveLR(0.317637), + MoveLR(-0.00393689), + DelayOld(10), + MoveLR(-0.341197), + DelayOld(10), + MoveLR(-0.647084), + MoveUD(-0.921567), + DelayOld(10), + MoveLR(-1), + MoveUD(-0.599994), + MoveUD(-0.474502), + DelayOld(10), + MoveUD(-0.309793), + DelayOld(10), + MoveUD(-0.160772), + MoveUD(-0.0352794), + Delay(10), + MoveUD(0.176489), + Delay(10), + MoveUD(0.607868), + Run(release=False), + Jump(release=False), + DelayOld(20), + MoveUD(1), + DelayOld(30), + MoveLR(-0.921598), + DelayOld(10), + Punch(release=False), + MoveLR(-0.639241), + DelayOld(10), + MoveLR(-0.223548), + DelayOld(10), + MoveLR(0.254891), + DelayOld(10), + MoveLR(0.741172), + MoveLR(1), + DelayOld(40), + JumpRelease(), + DelayOld(40), + MoveUD(0.976501), + DelayOld(10), + MoveUD(0.73336), + DelayOld(10), + MoveUD(0.309824), + DelayOld(20), + MoveUD(-0.184301), + DelayOld(20), + MoveUD(-0.811762), + MoveUD(-1), + KillSpaz(1, explode=True), + DelayOld(10), + RunRelease(), + PunchRelease(), + DelayOld(110), + MoveLR(0.97647), + MoveLR(0.898038), + DelayOld(20), + MoveLR(0.788232), + DelayOld(20), + MoveLR(0.670583), + DelayOld(10), + MoveLR(0.505875), + DelayOld(10), + MoveLR(0.32548), + DelayOld(20), + MoveLR(0.137242), + DelayOld(10), + MoveLR(-0.00393689), + DelayOld(10), + MoveLR(-0.215705), + MoveLR(-0.356883), + DelayOld(20), + MoveLR(-0.451003), + DelayOld(10), + MoveLR(-0.552965), + DelayOld(20), + MoveLR(-0.670614), + MoveLR(-0.780419), + DelayOld(10), + MoveLR(-0.898068), + DelayOld(20), + MoveLR(-1), + DelayOld(370), + MoveLR(-0.976501), + DelayOld(10), + MoveLR(-0.952971), + DelayOld(10), + MoveLR(-0.929441), + MoveLR(-0.898068), + DelayOld(30), + MoveLR(-0.874538), + DelayOld(10), + MoveLR(-0.851009), + DelayOld(10), + MoveLR(-0.835322), + MoveUD(-0.968627), + DelayOld(10), + MoveLR(-0.827479), + MoveUD(-0.960784), + DelayOld(20), + MoveUD(-0.945097), + DelayOld(70), + MoveUD(-0.937254), + DelayOld(20), + MoveUD(-0.913724), + DelayOld(20), + MoveUD(-0.890194), + MoveLR(-0.780419), + MoveUD(-0.827448), + DelayOld(20), + MoveLR(0.317637), + MoveUD(0.3961), + MoveLR(0.0195929), + MoveUD(0.056093), + DelayOld(20), + MoveUD(0), + DelayOld(750), + MoveLR(0), + Text( + ba.Lstr(resource=self._r + '.phrase13Text', + subs=[('${NAME}', + ba.Lstr(resource=self._r + + '.randomName3Text')) + ])), # whoops sorry bill + RemoveGloves(), + DelayOld(2000), + AnalyticsScreen('Tutorial Section 5'), + Text( + ba.Lstr(resource=self._r + '.phrase14Text', + subs=[('${NAME}', + ba.Lstr(resource=self._r + + '.randomName4Text'))]) + ), # you can pick up and throw things such as chuck here + SpawnSpaz(0, (-4.0, 4.3, -2.5), + make_current=True, + flash=False, + angle=90), + SpawnSpaz(1, (5, 0, -1.0), + relative_to=0, + make_current=False, + color=(0.4, 1.0, 0.7), + name=ba.Lstr(resource=self._r + '.randomName4Text')), + DelayOld(1000), + Celebrate('left', 1, duration=1000), + Move(1, 0.2), + DelayOld(2000), + PickUp(), + DelayOld(200), + Move(0.5, 1.0), + DelayOld(1200), + PickUp(), + Move(0, 0), + DelayOld(1000), + Celebrate('left'), + DelayOld(1500), + Move(0, -1.0), + DelayOld(800), + Move(0, 0), + DelayOld(800), + SpawnSpaz(0, (1.5, 4.3, -4.0), + make_current=True, + flash=False, + angle=0), + AnalyticsScreen('Tutorial Section 6'), + Text(ba.Lstr(resource=self._r + + '.phrase15Text')), # lastly there's bombs + DelayOld(1900), + Text( + ba.Lstr(resource=self._r + + '.phrase16Text')), # throwing bombs takes practice + DelayOld(2000), + Bomb(), + Move(-0.1, -0.1), + DelayOld(100), + Move(0, 0), + DelayOld(500), + DelayOld(1000), + Bomb(), + DelayOld(2000), + Text(ba.Lstr(resource=self._r + + '.phrase17Text')), # not a very good throw + DelayOld(3000), + Text( + ba.Lstr(resource=self._r + + '.phrase18Text')), # moving helps you get distance + DelayOld(1000), + Bomb(), + DelayOld(500), + Move(-0.3, 0), + DelayOld(100), + Move(-0.6, 0), + DelayOld(100), + Move(-1, 0), + DelayOld(800), + Bomb(), + DelayOld(400), + Move(0, -0.1), + DelayOld(100), + Move(0, 0), + DelayOld(2500), + Text(ba.Lstr(resource=self._r + + '.phrase19Text')), # jumping helps you get height + DelayOld(2000), + Bomb(), + DelayOld(500), + Move(1, 0), + DelayOld(300), + Jump(release_delay=250), + DelayOld(500), + Jump(release_delay=250), + DelayOld(550), + Jump(release_delay=250), + DelayOld(160), + Punch(), + DelayOld(500), + Move(0, -0.1), + DelayOld(100), + Move(0, 0), + DelayOld(2000), + Text(ba.Lstr(resource=self._r + + '.phrase20Text')), # whiplash your bombs + DelayOld(1000), + Bomb(release=False), + DelayOld2(80), + RunRelease(), + BombRelease(), + DelayOld2(620), + MoveLR(0), + DelayOld2(10), + MoveLR(0), + DelayOld2(40), + MoveLR(0), + DelayOld2(10), + MoveLR(-0.0537431), + MoveUD(0), + DelayOld2(20), + MoveLR(-0.262764), + DelayOld2(20), + MoveLR(-0.498062), + DelayOld2(10), + MoveLR(-0.639241), + DelayOld2(20), + MoveLR(-0.73336), + DelayOld2(10), + MoveLR(-0.843165), + MoveUD(-0.0352794), + DelayOld2(30), + MoveLR(-1), + DelayOld2(10), + MoveUD(-0.0588092), + DelayOld2(10), + MoveUD(-0.160772), + DelayOld2(20), + MoveUD(-0.286264), + DelayOld2(20), + MoveUD(-0.427442), + DelayOld2(10), + MoveUD(-0.623524), + DelayOld2(20), + MoveUD(-0.843135), + DelayOld2(10), + MoveUD(-1), + DelayOld2(40), + MoveLR(-0.890225), + DelayOld2(10), + MoveLR(-0.670614), + DelayOld2(20), + MoveLR(-0.435316), + DelayOld2(20), + MoveLR(-0.184332), + DelayOld2(10), + MoveLR(0.00390637), + DelayOld2(20), + MoveLR(0.223518), + DelayOld2(10), + MoveLR(0.388226), + DelayOld2(20), + MoveLR(0.560778), + DelayOld2(20), + MoveLR(0.717643), + DelayOld2(10), + MoveLR(0.890194), + DelayOld2(20), + MoveLR(1), + DelayOld2(30), + MoveUD(-0.968627), + DelayOld2(20), + MoveUD(-0.898038), + DelayOld2(10), + MoveUD(-0.741172), + DelayOld2(20), + MoveUD(-0.498032), + DelayOld2(20), + MoveUD(-0.247047), + DelayOld2(10), + MoveUD(0.00393689), + DelayOld2(20), + MoveUD(0.239235), + DelayOld2(20), + MoveUD(0.458846), + DelayOld2(10), + MoveUD(0.70983), + DelayOld2(30), + MoveUD(1), + DelayOld2(10), + MoveLR(0.827448), + DelayOld2(10), + MoveLR(0.678426), + DelayOld2(20), + MoveLR(0.396069), + DelayOld2(10), + MoveLR(0.0980255), + DelayOld2(20), + MoveLR(-0.160802), + DelayOld2(20), + MoveLR(-0.388256), + DelayOld2(10), + MoveLR(-0.545122), + DelayOld2(30), + MoveLR(-0.73336), + DelayOld2(10), + MoveLR(-0.945128), + DelayOld2(10), + MoveLR(-1), + DelayOld2(50), + MoveUD(0.960814), + DelayOld2(20), + MoveUD(0.890225), + DelayOld2(10), + MoveUD(0.749046), + DelayOld2(20), + MoveUD(0.623554), + DelayOld2(20), + MoveUD(0.498062), + DelayOld2(10), + MoveUD(0.34904), + DelayOld2(20), + MoveUD(0.239235), + DelayOld2(20), + MoveUD(0.137272), + DelayOld2(10), + MoveUD(0.0117801), + DelayOld2(20), + MoveUD(-0.0117496), + DelayOld2(10), + MoveUD(-0.0274361), + DelayOld2(90), + MoveUD(-0.0352794), + Run(release=False), + Jump(release=False), + Delay(80), + Punch(release=False), + DelayOld2(60), + MoveLR(-0.968657), + DelayOld2(20), + MoveLR(-0.835322), + DelayOld2(10), + MoveLR(-0.70983), + JumpRelease(), + DelayOld2(30), + MoveLR(-0.592181), + MoveUD(-0.0588092), + DelayOld2(10), + MoveLR(-0.490219), + MoveUD(-0.0744957), + DelayOld2(10), + MoveLR(-0.41963), + DelayOld2(20), + MoveLR(0), + MoveUD(0), + DelayOld2(20), + MoveUD(0), + PunchRelease(), + RunRelease(), + DelayOld(500), + Move(0, -0.1), + DelayOld(100), + Move(0, 0), + DelayOld(2000), + AnalyticsScreen('Tutorial Section 7'), + Text(ba.Lstr( + resource=self._r + + '.phrase21Text')), # timing your bombs can be tricky + Move(-1, 0), + DelayOld(1000), + Move(0, -0.1), + DelayOld(100), + Move(0, 0), + SpawnSpaz(0, (-0.7, 4.3, -3.9), + make_current=True, + flash=False, + angle=-30), + SpawnSpaz(1, (6.5, 0, -0.75), + relative_to=0, + make_current=False, + color=(0.3, 0.8, 1.0), + name=ba.Lstr(resource=self._r + '.randomName5Text')), + DelayOld2(1000), + Move(-1, 0), + DelayOld2(1800), + Bomb(), + Move(0, 0), + DelayOld2(300), + Move(1, 0), + DelayOld2(600), + Jump(), + DelayOld2(150), + Punch(), + DelayOld2(800), + Move(-1, 0), + DelayOld2(1000), + Move(0, 0), + DelayOld2(1500), + Text(ba.Lstr(resource=self._r + '.phrase22Text')), # dang + Delay(1500), + Text(''), + Delay(200), + Text(ba.Lstr(resource=self._r + + '.phrase23Text')), # try cooking off + Delay(1500), + Bomb(), + Delay(800), + Move(1, 0.12), + Delay(1100), + Jump(), + Delay(100), + Punch(), + Delay(100), + Move(0, -0.1), + Delay(100), + Move(0, 0), + Delay(2000), + Text(ba.Lstr(resource=self._r + + '.phrase24Text')), # hooray nicely cooked + Celebrate(), + DelayOld(2000), + KillSpaz(1), + Text(''), + Move(0.5, -0.5), + DelayOld(1000), + Move(0, -0.1), + DelayOld(100), + Move(0, 0), + DelayOld(1000), + AnalyticsScreen('Tutorial Section 8'), + Text(ba.Lstr(resource=self._r + + '.phrase25Text')), # well that's just about it + DelayOld(2000), + Text(ba.Lstr(resource=self._r + + '.phrase26Text')), # go get em tiger + DelayOld(2000), + Text(ba.Lstr(resource=self._r + + '.phrase27Text')), # remember you training + DelayOld(3000), + Text(ba.Lstr(resource=self._r + + '.phrase28Text')), # well maybe + DelayOld(1600), + Text(ba.Lstr(resource=self._r + '.phrase29Text')), # good luck + Celebrate('right', duration=10000), + DelayOld(1000), + AnalyticsScreen('Tutorial Complete'), + End(), + ] + + except Exception: + ba.print_exception() + + # If we read some, exec them. + if self._entries: + self._run_next_entry() + # Otherwise try again in a few seconds. + else: + self._read_entries_timer = ba.Timer( + 3.0, ba.WeakCall(self._read_entries)) + + def _run_next_entry(self) -> None: + + while self._entries: + entry = self._entries.pop(0) + try: + result = entry.run(self) + except Exception: + result = None + ba.print_exception() + + # If the entry returns an int value, set a timer; + # otherwise just keep going. + if result is not None: + self._entry_timer = ba.Timer( + result, + ba.WeakCall(self._run_next_entry), + timeformat=ba.TimeFormat.MILLISECONDS) + return + + # Done with these entries.. start over soon. + self._read_entries_timer = ba.Timer(1.0, + ba.WeakCall(self._read_entries)) + + def _update_skip_votes(self) -> None: + count = sum(1 for player in self.players if player.pressed) + assert self._skip_count_text + self._skip_count_text.text = ba.Lstr( + resource=self._r + '.skipVoteCountText', + subs=[('${COUNT}', str(count)), + ('${TOTAL}', str(len(self.players)))]) if count > 0 else '' + if (count >= len(self.players) and self.players + and not self._have_skipped): + _ba.increment_analytics_count('Tutorial skip') + ba.set_analytics_screen('Tutorial Skip') + self._have_skipped = True + ba.playsound(ba.getsound('swish')) + # self._skip_count_text.text = self._r.skippingText + self._skip_count_text.text = ba.Lstr(resource=self._r + + '.skippingText') + assert self._skip_text + self._skip_text.text = '' + self.end() + + def _player_pressed_button(self, player: Player) -> None: + + # Special case: if there's only one player, we give them a + # warning on their first press (some players were thinking the + # on-screen guide meant they were supposed to press something). + if len(self.players) == 1 and not self._issued_warning: + self._issued_warning = True + assert self._skip_text + self._skip_text.text = ba.Lstr(resource=self._r + + '.skipConfirmText') + self._skip_text.color = (1, 1, 1) + self._skip_text.scale = 1.3 + incr = 50 + t = incr + for _i in range(6): + ba.timer(t, + ba.Call(setattr, self._skip_text, 'color', + (1, 0.5, 0.1)), + timeformat=ba.TimeFormat.MILLISECONDS) + t += incr + ba.timer(t, + ba.Call(setattr, self._skip_text, 'color', (1, 1, 0)), + timeformat=ba.TimeFormat.MILLISECONDS) + t += incr + ba.timer(6.0, ba.WeakCall(self._revert_confirm)) + return + + player.pressed = True + + # test... + if not all(self.players): + ba.print_error('Nonexistent player in _player_pressed_button: ' + + str([str(p) for p in self.players]) + ': we are ' + + str(player)) + + self._update_skip_votes() + + def _revert_confirm(self) -> None: + assert self._skip_text + self._skip_text.text = ba.Lstr(resource=self._r + + '.toSkipPressAnythingText') + self._skip_text.color = (1, 1, 1) + self._issued_warning = False + + def on_player_join(self, player: Player) -> None: + super().on_player_join(player) + + # We just wanna know if this player presses anything. + player.assigninput( + (ba.InputType.JUMP_PRESS, ba.InputType.PUNCH_PRESS, + ba.InputType.BOMB_PRESS, ba.InputType.PICK_UP_PRESS), + ba.Call(self._player_pressed_button, player)) + + def on_player_leave(self, player: Player) -> None: + if not all(self.players): + ba.print_error('Nonexistent player in on_player_leave: ' + + str([str(p) for p in self.players]) + ': we are ' + + str(player)) + super().on_player_leave(player) + # our leaving may influence the vote total needed/etc + self._update_skip_votes() diff --git a/dist/ba_data/python/bastd/ui/__init__.py b/dist/ba_data/python/bastd/ui/__init__.py new file mode 100644 index 0000000..15b7717 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/__init__.py @@ -0,0 +1,5 @@ +# Released under the MIT License. See LICENSE for details. +# +""" +Provide top level UI related functionality. +""" diff --git a/dist/ba_data/python/bastd/ui/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f8e5637 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..ff358f6 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/achievements.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/achievements.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a9a37e7 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/achievements.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/appinvite.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/appinvite.cpython-38.opt-1.pyc new file mode 100644 index 0000000..0914167 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/appinvite.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/characterpicker.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/characterpicker.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6075883 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/characterpicker.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/colorpicker.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/colorpicker.cpython-38.opt-1.pyc new file mode 100644 index 0000000..e1c6cca Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/colorpicker.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/config.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/config.cpython-38.opt-1.pyc new file mode 100644 index 0000000..33f5494 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/config.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/configerror.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/configerror.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9450202 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/configerror.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/confirm.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/confirm.cpython-38.opt-1.pyc new file mode 100644 index 0000000..11d01ba Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/confirm.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/confirm.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/confirm.cpython-38.pyc new file mode 100644 index 0000000..fdd4918 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/confirm.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/continues.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/continues.cpython-38.opt-1.pyc new file mode 100644 index 0000000..5f80077 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/continues.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/creditslist.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/creditslist.cpython-38.opt-1.pyc new file mode 100644 index 0000000..23eb471 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/creditslist.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/creditslist.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/creditslist.cpython-38.pyc new file mode 100644 index 0000000..7206338 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/creditslist.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/debug.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/debug.cpython-38.opt-1.pyc new file mode 100644 index 0000000..81cc45d Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/debug.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/feedback.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/feedback.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c859825 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/feedback.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/feedback.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/feedback.cpython-38.pyc new file mode 100644 index 0000000..860c000 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/feedback.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/fileselector.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/fileselector.cpython-38.opt-1.pyc new file mode 100644 index 0000000..3d8c463 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/fileselector.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-38.opt-1.pyc new file mode 100644 index 0000000..b47246e Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.opt-1.pyc new file mode 100644 index 0000000..0885db3 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.pyc new file mode 100644 index 0000000..ba7ed65 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/helpui.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/helpui.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7e0d85a Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/helpui.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/helpui.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/helpui.cpython-38.pyc new file mode 100644 index 0000000..40fea6c Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/helpui.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/iconpicker.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/iconpicker.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a2f6dc8 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/iconpicker.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/kiosk.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/kiosk.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a721abb Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/kiosk.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/kiosk.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/kiosk.cpython-38.pyc new file mode 100644 index 0000000..d02ed72 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/kiosk.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-38.opt-1.pyc new file mode 100644 index 0000000..238844e Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-38.pyc new file mode 100644 index 0000000..4b87df3 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-38.opt-1.pyc new file mode 100644 index 0000000..bc0a93a Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-38.pyc new file mode 100644 index 0000000..7e92432 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/party.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/party.cpython-38.opt-1.pyc new file mode 100644 index 0000000..aa00b07 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/party.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/party.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/party.cpython-38.pyc new file mode 100644 index 0000000..2cf0b9f Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/party.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/partyqueue.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/partyqueue.cpython-38.opt-1.pyc new file mode 100644 index 0000000..e62a12e Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/partyqueue.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/play.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/play.cpython-38.opt-1.pyc new file mode 100644 index 0000000..98964bf Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/play.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/play.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/play.cpython-38.pyc new file mode 100644 index 0000000..069acb5 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/play.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/playoptions.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/playoptions.cpython-38.opt-1.pyc new file mode 100644 index 0000000..083b666 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/playoptions.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/popup.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/popup.cpython-38.opt-1.pyc new file mode 100644 index 0000000..856409f Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/popup.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/popup.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/popup.cpython-38.pyc new file mode 100644 index 0000000..7f29a08 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/popup.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/promocode.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/promocode.cpython-38.opt-1.pyc new file mode 100644 index 0000000..de775c7 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/promocode.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/purchase.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/purchase.cpython-38.opt-1.pyc new file mode 100644 index 0000000..1e47ae9 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/purchase.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/qrcode.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/qrcode.cpython-38.opt-1.pyc new file mode 100644 index 0000000..b555e20 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/qrcode.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/radiogroup.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/radiogroup.cpython-38.opt-1.pyc new file mode 100644 index 0000000..3206690 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/radiogroup.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/report.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/report.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d6a6258 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/report.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/resourcetypeinfo.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/resourcetypeinfo.cpython-38.opt-1.pyc new file mode 100644 index 0000000..461c816 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/resourcetypeinfo.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/serverdialog.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/serverdialog.cpython-38.opt-1.pyc new file mode 100644 index 0000000..dd27bf4 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/serverdialog.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-38.opt-1.pyc new file mode 100644 index 0000000..911df88 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-38.pyc new file mode 100644 index 0000000..fca7a33 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/tabs.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/tabs.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f9b6c00 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/tabs.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/tabs.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/tabs.cpython-38.pyc new file mode 100644 index 0000000..7e9e500 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/tabs.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/teamnamescolors.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/teamnamescolors.cpython-38.opt-1.pyc new file mode 100644 index 0000000..04ea73a Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/teamnamescolors.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/telnet.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/telnet.cpython-38.opt-1.pyc new file mode 100644 index 0000000..e56a88d Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/telnet.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/tournamententry.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/tournamententry.cpython-38.opt-1.pyc new file mode 100644 index 0000000..8b8c405 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/tournamententry.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-38.opt-1.pyc new file mode 100644 index 0000000..474d9b6 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/trophies.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/trophies.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c3892d9 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/trophies.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/url.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/url.cpython-38.opt-1.pyc new file mode 100644 index 0000000..91d0e02 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/url.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/watch.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/__pycache__/watch.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4926fc5 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/watch.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/__pycache__/watch.cpython-38.pyc b/dist/ba_data/python/bastd/ui/__pycache__/watch.cpython-38.pyc new file mode 100644 index 0000000..20af930 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/__pycache__/watch.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/account/__init__.py b/dist/ba_data/python/bastd/ui/account/__init__.py new file mode 100644 index 0000000..9b1c446 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/account/__init__.py @@ -0,0 +1,29 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to accounts.""" + +from __future__ import annotations + +import _ba +import ba + + +def show_sign_in_prompt(account_type: str = None) -> None: + """Bring up a prompt telling the user they must sign in.""" + from bastd.ui import confirm + from bastd.ui.account import settings + if account_type == 'Google Play': + confirm.ConfirmWindow( + ba.Lstr(resource='notSignedInGooglePlayErrorText'), + lambda: _ba.sign_in('Google Play'), + ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'), + width=460, + height=130) + else: + confirm.ConfirmWindow( + ba.Lstr(resource='notSignedInErrorText'), + lambda: settings.AccountSettingsWindow(modal=True, + close_once_signed_in=True), + ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'), + width=460, + height=130) diff --git a/dist/ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..11ce502 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..a604f55 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/account/__pycache__/link.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/account/__pycache__/link.cpython-38.opt-1.pyc new file mode 100644 index 0000000..083f028 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/account/__pycache__/link.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/account/__pycache__/settings.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/account/__pycache__/settings.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f4227c5 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/account/__pycache__/settings.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/account/__pycache__/settings.cpython-38.pyc b/dist/ba_data/python/bastd/ui/account/__pycache__/settings.cpython-38.pyc new file mode 100644 index 0000000..29392fd Binary files /dev/null and b/dist/ba_data/python/bastd/ui/account/__pycache__/settings.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-38.opt-1.pyc new file mode 100644 index 0000000..54548be Binary files /dev/null and b/dist/ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4816f62 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/account/link.py b/dist/ba_data/python/bastd/ui/account/link.py new file mode 100644 index 0000000..f82985b --- /dev/null +++ b/dist/ba_data/python/bastd/ui/account/link.py @@ -0,0 +1,151 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for linking accounts.""" + +from __future__ import annotations + +import copy +import time +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Tuple, Optional, Dict + + +class AccountLinkWindow(ba.Window): + """Window for linking accounts.""" + + def __init__(self, origin_widget: ba.Widget = None): + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + transition = 'in_right' + bg_color = (0.4, 0.4, 0.5) + self._width = 560 + self._height = 420 + uiscale = ba.app.ui.uiscale + base_scale = (1.65 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.1) + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=base_scale, + scale_origin_stack_offset=scale_origin, + stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else (0, 0))) + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + position=(40, self._height - 45), + size=(50, 50), + scale=0.7, + label='', + color=bg_color, + on_activate_call=self._cancel, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + maxlinks = _ba.get_account_misc_read_val('maxLinkAccounts', 5) + ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.56), + size=(0, 0), + text=ba.Lstr(resource=( + 'accountSettingsWindow.linkAccountsInstructionsNewText'), + subs=[('${COUNT}', str(maxlinks))]), + maxwidth=self._width * 0.9, + color=ba.app.ui.infotextcolor, + max_height=self._height * 0.6, + h_align='center', + v_align='center') + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + ba.buttonwidget( + parent=self._root_widget, + position=(40, 30), + size=(200, 60), + label=ba.Lstr( + resource='accountSettingsWindow.linkAccountsGenerateCodeText'), + autoselect=True, + on_activate_call=self._generate_press) + self._enter_code_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - 240, 30), + size=(200, 60), + label=ba.Lstr( + resource='accountSettingsWindow.linkAccountsEnterCodeText'), + autoselect=True, + on_activate_call=self._enter_code_press) + + def _generate_press(self) -> None: + from bastd.ui import account + if _ba.get_account_state() != 'signed_in': + account.show_sign_in_prompt() + return + ba.screenmessage( + ba.Lstr(resource='gatherWindow.requestingAPromoCodeText'), + color=(0, 1, 0)) + _ba.add_transaction({ + 'type': 'ACCOUNT_LINK_CODE_REQUEST', + 'expire_time': time.time() + 5 + }) + _ba.run_transactions() + + def _enter_code_press(self) -> None: + from bastd.ui import promocode + promocode.PromoCodeWindow(modal=True, + origin_widget=self._enter_code_button) + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + + def _cancel(self) -> None: + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + + +class AccountLinkCodeWindow(ba.Window): + """Window showing code for account-linking.""" + + def __init__(self, data: Dict[str, Any]): + self._width = 350 + self._height = 200 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + color=(0.45, 0.63, 0.15), + transition='in_scale', + scale=(1.8 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0))) + self._data = copy.deepcopy(data) + ba.playsound(ba.getsound('cashRegister')) + ba.playsound(ba.getsound('swish')) + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + scale=0.5, + position=(40, self._height - 40), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=2.0, + h_align='center', + v_align='center', + text=data['code'], + maxwidth=self._width * 0.85) + + def close(self) -> None: + """close the window""" + ba.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/dist/ba_data/python/bastd/ui/account/settings.py b/dist/ba_data/python/bastd/ui/account/settings.py new file mode 100644 index 0000000..785f186 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/account/settings.py @@ -0,0 +1,1106 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for account functionality.""" +# pylint: disable=too-many-lines + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Optional, Tuple, List, Union + + +class AccountSettingsWindow(ba.Window): + """Window for account related functionality.""" + + def __init__(self, + transition: str = 'in_right', + modal: bool = False, + origin_widget: ba.Widget = None, + close_once_signed_in: bool = False): + # pylint: disable=too-many-statements + + self._close_once_signed_in = close_once_signed_in + ba.set_analytics_screen('Account Window') + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + self._r = 'accountSettingsWindow' + self._modal = modal + self._needs_refresh = False + self._signed_in = (_ba.get_account_state() == 'signed_in') + self._account_state_num = _ba.get_account_state_num() + self._show_linked = (self._signed_in and _ba.get_account_misc_read_val( + 'allowAccountLinking2', False)) + self._check_sign_in_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + + # Currently we can only reset achievements on game-center. + account_type: Optional[str] + if self._signed_in: + account_type = _ba.get_account_type() + else: + account_type = None + self._can_reset_achievements = (account_type == 'Game Center') + + app = ba.app + uiscale = app.ui.uiscale + + self._width = 760 if uiscale is ba.UIScale.SMALL else 660 + x_offs = 50 if uiscale is ba.UIScale.SMALL else 0 + self._height = (390 if uiscale is ba.UIScale.SMALL else + 430 if uiscale is ba.UIScale.MEDIUM else 490) + + self._sign_in_button = None + self._sign_in_text = None + + self._scroll_width = self._width - (100 + x_offs * 2) + self._scroll_height = self._height - 120 + self._sub_width = self._scroll_width - 20 + + # Determine which sign-in/sign-out buttons we should show. + self._show_sign_in_buttons: List[str] = [] + + if app.platform == 'android' and app.subplatform == 'google': + self._show_sign_in_buttons.append('Google Play') + + elif app.platform == 'android' and app.subplatform == 'amazon': + self._show_sign_in_buttons.append('Game Circle') + + # Local accounts are generally always available with a few key + # exceptions. + self._show_sign_in_buttons.append('Local') + + top_extra = 15 if uiscale is ba.UIScale.SMALL else 0 + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(2.09 if uiscale is ba.UIScale.SMALL else + 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -19) if uiscale is ba.UIScale.SMALL else (0, 0))) + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + self._back_button = None + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._back) + else: + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(51 + x_offs, self._height - 62), + size=(120, 60), + scale=0.8, + text_scale=1.2, + autoselect=True, + label=ba.Lstr( + resource='doneText' if self._modal else 'backText'), + button_type='regular' if self._modal else 'back', + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + if not self._modal: + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 56), + label=ba.charstr(ba.SpecialChar.BACK)) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 41), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.titleText'), + color=ba.app.ui.title_color, + maxwidth=self._width - 340, + h_align='center', + v_align='center') + + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + highlight=False, + position=((self._width - self._scroll_width) * 0.5, + self._height - 65 - self._scroll_height), + size=(self._scroll_width, self._scroll_height), + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + self._subcontainer: Optional[ba.Widget] = None + self._refresh() + self._restore_state() + + def _update(self) -> None: + + # If they want us to close once we're signed in, do so. + if self._close_once_signed_in and self._signed_in: + self._back() + return + + # Hmm should update this to use get_account_state_num. + # Theoretically if we switch from one signed-in account to another + # in the background this would break. + account_state_num = _ba.get_account_state_num() + account_state = _ba.get_account_state() + + show_linked = (self._signed_in and _ba.get_account_misc_read_val( + 'allowAccountLinking2', False)) + + if (account_state_num != self._account_state_num + or self._show_linked != show_linked or self._needs_refresh): + self._show_linked = show_linked + self._account_state_num = account_state_num + self._signed_in = (account_state == 'signed_in') + self._refresh() + + # Go ahead and refresh some individual things + # that may change under us. + self._update_linked_accounts_text() + self._update_unlink_accounts_button() + self._refresh_campaign_progress_text() + self._refresh_achievements() + self._refresh_tickets_text() + self._refresh_account_name_text() + + def _get_sign_in_text(self) -> ba.Lstr: + return ba.Lstr(resource=self._r + '.signInText') + + def _refresh(self) -> None: + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from bastd.ui import confirm + + account_state = _ba.get_account_state() + account_type = (_ba.get_account_type() + if account_state == 'signed_in' else 'unknown') + + is_google = account_type == 'Google Play' + + show_local_signed_in_as = False + local_signed_in_as_space = 50.0 + + show_signed_in_as = self._signed_in + signed_in_as_space = 95.0 + + show_sign_in_benefits = not self._signed_in + sign_in_benefits_space = 80.0 + + show_signing_in_text = account_state == 'signing_in' + signing_in_text_space = 80.0 + + show_google_play_sign_in_button = (account_state == 'signed_out' + and 'Google Play' + in self._show_sign_in_buttons) + show_game_circle_sign_in_button = (account_state == 'signed_out' + and 'Game Circle' + in self._show_sign_in_buttons) + show_ali_sign_in_button = (account_state == 'signed_out' + and 'Ali' in self._show_sign_in_buttons) + show_test_sign_in_button = (account_state == 'signed_out' + and 'Test' in self._show_sign_in_buttons) + show_device_sign_in_button = (account_state == 'signed_out' and 'Local' + in self._show_sign_in_buttons) + sign_in_button_space = 70.0 + + show_game_service_button = (self._signed_in and account_type + in ['Game Center', 'Game Circle']) + game_service_button_space = 60.0 + + show_linked_accounts_text = (self._signed_in + and _ba.get_account_misc_read_val( + 'allowAccountLinking2', False)) + linked_accounts_text_space = 60.0 + + show_achievements_button = (self._signed_in and account_type + in ('Google Play', 'Alibaba', 'Local', + 'OUYA', 'Test')) + achievements_button_space = 60.0 + + show_achievements_text = (self._signed_in + and not show_achievements_button) + achievements_text_space = 27.0 + + show_leaderboards_button = (self._signed_in and is_google) + leaderboards_button_space = 60.0 + + show_campaign_progress = self._signed_in + campaign_progress_space = 27.0 + + show_tickets = self._signed_in + tickets_space = 27.0 + + show_reset_progress_button = False + reset_progress_button_space = 70.0 + + show_player_profiles_button = self._signed_in + player_profiles_button_space = 100.0 + + show_link_accounts_button = (self._signed_in + and _ba.get_account_misc_read_val( + 'allowAccountLinking2', False)) + link_accounts_button_space = 70.0 + + show_unlink_accounts_button = show_link_accounts_button + unlink_accounts_button_space = 90.0 + + show_sign_out_button = (self._signed_in and account_type + in ['Test', 'Local', 'Google Play']) + sign_out_button_space = 70.0 + + if self._subcontainer is not None: + self._subcontainer.delete() + self._sub_height = 60.0 + if show_local_signed_in_as: + self._sub_height += local_signed_in_as_space + if show_signed_in_as: + self._sub_height += signed_in_as_space + if show_signing_in_text: + self._sub_height += signing_in_text_space + if show_google_play_sign_in_button: + self._sub_height += sign_in_button_space + if show_game_circle_sign_in_button: + self._sub_height += sign_in_button_space + if show_ali_sign_in_button: + self._sub_height += sign_in_button_space + if show_test_sign_in_button: + self._sub_height += sign_in_button_space + if show_device_sign_in_button: + self._sub_height += sign_in_button_space + if show_game_service_button: + self._sub_height += game_service_button_space + if show_linked_accounts_text: + self._sub_height += linked_accounts_text_space + if show_achievements_text: + self._sub_height += achievements_text_space + if show_achievements_button: + self._sub_height += achievements_button_space + if show_leaderboards_button: + self._sub_height += leaderboards_button_space + if show_campaign_progress: + self._sub_height += campaign_progress_space + if show_tickets: + self._sub_height += tickets_space + if show_sign_in_benefits: + self._sub_height += sign_in_benefits_space + if show_reset_progress_button: + self._sub_height += reset_progress_button_space + if show_player_profiles_button: + self._sub_height += player_profiles_button_space + if show_link_accounts_button: + self._sub_height += link_accounts_button_space + if show_unlink_accounts_button: + self._sub_height += unlink_accounts_button_space + if show_sign_out_button: + self._sub_height += sign_out_button_space + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + + first_selectable = None + v = self._sub_height - 10.0 + + if show_local_signed_in_as: + v -= local_signed_in_as_space * 0.6 + ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.5, v), + size=(0, 0), + text=ba.Lstr( + resource='accountSettingsWindow.deviceSpecificAccountText', + subs=[('${NAME}', _ba.get_account_display_string())]), + scale=0.7, + color=(0.5, 0.5, 0.6), + maxwidth=self._sub_width * 0.9, + flatness=1.0, + h_align='center', + v_align='center') + v -= local_signed_in_as_space * 0.4 + + self._account_name_text: Optional[ba.Widget] + if show_signed_in_as: + v -= signed_in_as_space * 0.2 + txt = ba.Lstr( + resource='accountSettingsWindow.youAreSignedInAsText', + fallback_resource='accountSettingsWindow.youAreLoggedInAsText') + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, v), + size=(0, 0), + text=txt, + scale=0.9, + color=ba.app.ui.title_color, + maxwidth=self._sub_width * 0.9, + h_align='center', + v_align='center') + v -= signed_in_as_space * 0.4 + self._account_name_text = ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.5, v), + size=(0, 0), + scale=1.5, + maxwidth=self._sub_width * 0.9, + res_scale=1.5, + color=(1, 1, 1, 1), + h_align='center', + v_align='center') + self._refresh_account_name_text() + v -= signed_in_as_space * 0.4 + else: + self._account_name_text = None + + if self._back_button is None: + bbtn = _ba.get_special_widget('back_button') + else: + bbtn = self._back_button + + if show_sign_in_benefits: + v -= sign_in_benefits_space + app = ba.app + extra: Optional[Union[str, ba.Lstr]] + if (app.platform in ['mac', 'ios'] + and app.subplatform == 'appstore'): + extra = ba.Lstr( + value='\n${S}', + subs=[('${S}', + ba.Lstr(resource='signInWithGameCenterText'))]) + else: + extra = '' + + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, + v + sign_in_benefits_space * 0.4), + size=(0, 0), + text=ba.Lstr(value='${A}${B}', + subs=[('${A}', + ba.Lstr(resource=self._r + + '.signInInfoText')), + ('${B}', extra)]), + max_height=sign_in_benefits_space * 0.9, + scale=0.9, + color=(0.75, 0.7, 0.8), + maxwidth=self._sub_width * 0.8, + h_align='center', + v_align='center') + + if show_signing_in_text: + v -= signing_in_text_space + + ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.5, + v + signing_in_text_space * 0.5), + size=(0, 0), + text=ba.Lstr(resource='accountSettingsWindow.signingInText'), + scale=0.9, + color=(0, 1, 0), + maxwidth=self._sub_width * 0.8, + h_align='center', + v_align='center') + + if show_google_play_sign_in_button: + button_width = 350 + v -= sign_in_button_space + self._sign_in_google_play_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v - 20), + autoselect=True, + size=(button_width, 60), + label=ba.Lstr( + value='${A}${B}', + subs=[('${A}', + ba.charstr(ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO)), + ('${B}', + ba.Lstr(resource=self._r + + '.signInWithGooglePlayText'))]), + on_activate_call=lambda: self._sign_in_press('Google Play')) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn) + ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) + self._sign_in_text = None + + if show_game_circle_sign_in_button: + button_width = 350 + v -= sign_in_button_space + self._sign_in_game_circle_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v - 20), + autoselect=True, + size=(button_width, 60), + label=ba.Lstr(value='${A}${B}', + subs=[('${A}', + ba.charstr( + ba.SpecialChar.GAME_CIRCLE_LOGO)), + ('${B}', + ba.Lstr(resource=self._r + + '.signInWithGameCircleText'))]), + on_activate_call=lambda: self._sign_in_press('Game Circle')) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn) + ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) + self._sign_in_text = None + + if show_ali_sign_in_button: + button_width = 350 + v -= sign_in_button_space + self._sign_in_ali_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v - 20), + autoselect=True, + size=(button_width, 60), + label=ba.Lstr(value='${A}${B}', + subs=[('${A}', + ba.charstr(ba.SpecialChar.ALIBABA_LOGO)), + ('${B}', + ba.Lstr(resource=self._r + '.signInText')) + ]), + on_activate_call=lambda: self._sign_in_press('Ali')) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn) + ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) + self._sign_in_text = None + + if show_device_sign_in_button: + button_width = 350 + v -= sign_in_button_space + self._sign_in_device_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v - 20), + autoselect=True, + size=(button_width, 60), + label='', + on_activate_call=lambda: self._sign_in_press('Local')) + ba.textwidget(parent=self._subcontainer, + draw_controller=btn, + h_align='center', + v_align='center', + size=(0, 0), + position=(self._sub_width * 0.5, v + 17), + text=ba.Lstr( + value='${A}${B}', + subs=[('${A}', + ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)), + ('${B}', + ba.Lstr(resource=self._r + + '.signInWithDeviceText'))]), + maxwidth=button_width * 0.8, + color=(0.75, 1.0, 0.7)) + ba.textwidget(parent=self._subcontainer, + draw_controller=btn, + h_align='center', + v_align='center', + size=(0, 0), + position=(self._sub_width * 0.5, v - 4), + text=ba.Lstr(resource=self._r + + '.signInWithDeviceInfoText'), + flatness=1.0, + scale=0.57, + maxwidth=button_width * 0.9, + color=(0.55, 0.8, 0.5)) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn) + ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) + self._sign_in_text = None + + # Old test-account option. + if show_test_sign_in_button: + button_width = 350 + v -= sign_in_button_space + self._sign_in_test_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v - 20), + autoselect=True, + size=(button_width, 60), + label='', + on_activate_call=lambda: self._sign_in_press('Test')) + ba.textwidget(parent=self._subcontainer, + draw_controller=btn, + h_align='center', + v_align='center', + size=(0, 0), + position=(self._sub_width * 0.5, v + 17), + text=ba.Lstr( + value='${A}${B}', + subs=[('${A}', + ba.charstr(ba.SpecialChar.TEST_ACCOUNT)), + ('${B}', + ba.Lstr(resource=self._r + + '.signInWithTestAccountText'))]), + maxwidth=button_width * 0.8, + color=(0.75, 1.0, 0.7)) + ba.textwidget(parent=self._subcontainer, + draw_controller=btn, + h_align='center', + v_align='center', + size=(0, 0), + position=(self._sub_width * 0.5, v - 4), + text=ba.Lstr(resource=self._r + + '.signInWithTestAccountInfoText'), + flatness=1.0, + scale=0.57, + maxwidth=button_width * 0.9, + color=(0.55, 0.8, 0.5)) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn) + ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) + self._sign_in_text = None + + if show_player_profiles_button: + button_width = 300 + v -= player_profiles_button_space + self._player_profiles_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v + 30), + autoselect=True, + size=(button_width, 60), + label=ba.Lstr(resource='playerProfilesWindow.titleText'), + color=(0.55, 0.5, 0.6), + icon=ba.gettexture('cuteSpaz'), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._player_profiles_press) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0) + + # the button to go to OS-Specific leaderboards/high-score-lists/etc. + if show_game_service_button: + button_width = 300 + v -= game_service_button_space * 0.85 + account_type = _ba.get_account_type() + if account_type == 'Game Center': + account_type_name = ba.Lstr(resource='gameCenterText') + elif account_type == 'Game Circle': + account_type_name = ba.Lstr(resource='gameCircleText') + else: + raise ValueError("unknown account type: '" + + str(account_type) + "'") + self._game_service_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + color=(0.55, 0.5, 0.6), + textcolor=(0.75, 0.7, 0.8), + autoselect=True, + on_activate_call=_ba.show_online_score_ui, + size=(button_width, 50), + label=account_type_name) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn) + v -= game_service_button_space * 0.15 + else: + self.game_service_button = None + + self._achievements_text: Optional[ba.Widget] + if show_achievements_text: + v -= achievements_text_space * 0.5 + self._achievements_text = ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.5, v), + size=(0, 0), + scale=0.9, + color=(0.75, 0.7, 0.8), + maxwidth=self._sub_width * 0.8, + h_align='center', + v_align='center') + v -= achievements_text_space * 0.5 + else: + self._achievements_text = None + + self._achievements_button: Optional[ba.Widget] + if show_achievements_button: + button_width = 300 + v -= achievements_button_space * 0.85 + self._achievements_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + color=(0.55, 0.5, 0.6), + textcolor=(0.75, 0.7, 0.8), + autoselect=True, + icon=ba.gettexture('googlePlayAchievementsIcon' + if is_google else 'achievementsIcon'), + icon_color=(0.8, 0.95, 0.7) if is_google else (0.85, 0.8, 0.9), + on_activate_call=self._on_achievements_press, + size=(button_width, 50), + label='') + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn) + v -= achievements_button_space * 0.15 + else: + self._achievements_button = None + + if show_achievements_text or show_achievements_button: + self._refresh_achievements() + + self._leaderboards_button: Optional[ba.Widget] + if show_leaderboards_button: + button_width = 300 + v -= leaderboards_button_space * 0.85 + self._leaderboards_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + color=(0.55, 0.5, 0.6), + textcolor=(0.75, 0.7, 0.8), + autoselect=True, + icon=ba.gettexture('googlePlayLeaderboardsIcon'), + icon_color=(0.8, 0.95, 0.7), + on_activate_call=self._on_leaderboards_press, + size=(button_width, 50), + label=ba.Lstr(resource='leaderboardsText')) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn) + v -= leaderboards_button_space * 0.15 + else: + self._leaderboards_button = None + + self._campaign_progress_text: Optional[ba.Widget] + if show_campaign_progress: + v -= campaign_progress_space * 0.5 + self._campaign_progress_text = ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.5, v), + size=(0, 0), + scale=0.9, + color=(0.75, 0.7, 0.8), + maxwidth=self._sub_width * 0.8, + h_align='center', + v_align='center') + v -= campaign_progress_space * 0.5 + self._refresh_campaign_progress_text() + else: + self._campaign_progress_text = None + + self._tickets_text: Optional[ba.Widget] + if show_tickets: + v -= tickets_space * 0.5 + self._tickets_text = ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, + v), + size=(0, 0), + scale=0.9, + color=(0.75, 0.7, 0.8), + maxwidth=self._sub_width * 0.8, + flatness=1.0, + h_align='center', + v_align='center') + v -= tickets_space * 0.5 + self._refresh_tickets_text() + + else: + self._tickets_text = None + + # bit of spacing before the reset/sign-out section + v -= 5 + + button_width = 250 + if show_reset_progress_button: + confirm_text = (ba.Lstr(resource=self._r + + '.resetProgressConfirmText') + if self._can_reset_achievements else ba.Lstr( + resource=self._r + + '.resetProgressConfirmNoAchievementsText')) + v -= reset_progress_button_space + self._reset_progress_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + color=(0.55, 0.5, 0.6), + textcolor=(0.75, 0.7, 0.8), + autoselect=True, + size=(button_width, 60), + label=ba.Lstr(resource=self._r + '.resetProgressText'), + on_activate_call=lambda: confirm.ConfirmWindow( + text=confirm_text, + width=500, + height=200, + action=self._reset_progress)) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn) + + self._linked_accounts_text: Optional[ba.Widget] + if show_linked_accounts_text: + v -= linked_accounts_text_space * 0.8 + self._linked_accounts_text = ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.5, v), + size=(0, 0), + scale=0.9, + color=(0.75, 0.7, 0.8), + maxwidth=self._sub_width * 0.95, + h_align='center', + v_align='center') + v -= linked_accounts_text_space * 0.2 + self._update_linked_accounts_text() + else: + self._linked_accounts_text = None + + if show_link_accounts_button: + v -= link_accounts_button_space + self._link_accounts_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + autoselect=True, + size=(button_width, 60), + label='', + color=(0.55, 0.5, 0.6), + on_activate_call=self._link_accounts_press) + ba.textwidget(parent=self._subcontainer, + draw_controller=btn, + h_align='center', + v_align='center', + size=(0, 0), + position=(self._sub_width * 0.5, v + 17 + 20), + text=ba.Lstr(resource=self._r + '.linkAccountsText'), + maxwidth=button_width * 0.8, + color=(0.75, 0.7, 0.8)) + ba.textwidget(parent=self._subcontainer, + draw_controller=btn, + h_align='center', + v_align='center', + size=(0, 0), + position=(self._sub_width * 0.5, v - 4 + 20), + text=ba.Lstr(resource=self._r + + '.linkAccountsInfoText'), + flatness=1.0, + scale=0.5, + maxwidth=button_width * 0.8, + color=(0.75, 0.7, 0.8)) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) + + self._unlink_accounts_button: Optional[ba.Widget] + if show_unlink_accounts_button: + v -= unlink_accounts_button_space + self._unlink_accounts_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v + 25), + autoselect=True, + size=(button_width, 60), + label='', + color=(0.55, 0.5, 0.6), + on_activate_call=self._unlink_accounts_press) + self._unlink_accounts_button_label = ba.textwidget( + parent=self._subcontainer, + draw_controller=btn, + h_align='center', + v_align='center', + size=(0, 0), + position=(self._sub_width * 0.5, v + 55), + text=ba.Lstr(resource=self._r + '.unlinkAccountsText'), + maxwidth=button_width * 0.8, + color=(0.75, 0.7, 0.8)) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) + self._update_unlink_accounts_button() + else: + self._unlink_accounts_button = None + + if show_sign_out_button: + v -= sign_out_button_space + self._sign_out_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + size=(button_width, 60), + label=ba.Lstr(resource=self._r + '.signOutText'), + color=(0.55, 0.5, 0.6), + textcolor=(0.75, 0.7, 0.8), + autoselect=True, + on_activate_call=self._sign_out_press) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) + + # Whatever the topmost selectable thing is, we want it to scroll all + # the way up when we select it. + if first_selectable is not None: + ba.widget(edit=first_selectable, + up_widget=bbtn, + show_buffer_top=400) + # (this should re-scroll us to the top..) + ba.containerwidget(edit=self._subcontainer, + visible_child=first_selectable) + self._needs_refresh = False + + def _on_achievements_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui import achievements + account_state = _ba.get_account_state() + account_type = (_ba.get_account_type() + if account_state == 'signed_in' else 'unknown') + # for google play we use the built-in UI; otherwise pop up our own + if account_type == 'Google Play': + ba.timer(0.15, + ba.Call(_ba.show_online_score_ui, 'achievements'), + timetype=ba.TimeType.REAL) + elif account_type != 'unknown': + assert self._achievements_button is not None + achievements.AchievementsWindow( + position=self._achievements_button.get_screen_space_center()) + else: + print('ERROR: unknown account type in on_achievements_press:', + account_type) + + def _on_leaderboards_press(self) -> None: + ba.timer(0.15, + ba.Call(_ba.show_online_score_ui, 'leaderboards'), + timetype=ba.TimeType.REAL) + + def _have_unlinkable_accounts(self) -> bool: + # if this is not present, we haven't had contact from the server so + # let's not proceed.. + if _ba.get_public_login_id() is None: + return False + accounts = _ba.get_account_misc_read_val_2('linkedAccounts', []) + return len(accounts) > 1 + + def _update_unlink_accounts_button(self) -> None: + if self._unlink_accounts_button is None: + return + if self._have_unlinkable_accounts(): + clr = (0.75, 0.7, 0.8, 1.0) + else: + clr = (1.0, 1.0, 1.0, 0.25) + ba.textwidget(edit=self._unlink_accounts_button_label, color=clr) + + def _update_linked_accounts_text(self) -> None: + if self._linked_accounts_text is None: + return + + # if this is not present, we haven't had contact from the server so + # let's not proceed.. + if _ba.get_public_login_id() is None: + num = int(time.time()) % 4 + accounts_str = num * '.' + (4 - num) * ' ' + else: + accounts = _ba.get_account_misc_read_val_2('linkedAccounts', []) + # our_account = _bs.get_account_display_string() + # accounts = [a for a in accounts if a != our_account] + # accounts_str = u', '.join(accounts) if accounts else + # ba.Lstr(translate=('settingNames', 'None')) + # UPDATE - we now just print the number here; not the actual + # accounts + # (they can see that in the unlink section if they're curious) + accounts_str = str(max(0, len(accounts) - 1)) + ba.textwidget(edit=self._linked_accounts_text, + text=ba.Lstr(value='${L} ${A}', + subs=[('${L}', + ba.Lstr(resource=self._r + + '.linkedAccountsText')), + ('${A}', accounts_str)])) + + def _refresh_campaign_progress_text(self) -> None: + from ba.internal import getcampaign + if self._campaign_progress_text is None: + return + p_str: Union[str, ba.Lstr] + try: + campaign = getcampaign('Default') + levels = campaign.levels + levels_complete = sum((1 if l.complete else 0) for l in levels) + + # Last level cant be completed; hence the -1; + progress = min(1.0, float(levels_complete) / (len(levels) - 1)) + p_str = ba.Lstr(resource=self._r + '.campaignProgressText', + subs=[('${PROGRESS}', + str(int(progress * 100.0)) + '%')]) + except Exception: + p_str = '?' + ba.print_exception('Error calculating co-op campaign progress.') + ba.textwidget(edit=self._campaign_progress_text, text=p_str) + + def _refresh_tickets_text(self) -> None: + if self._tickets_text is None: + return + try: + tc_str = str(_ba.get_account_ticket_count()) + except Exception: + ba.print_exception() + tc_str = '-' + ba.textwidget(edit=self._tickets_text, + text=ba.Lstr(resource=self._r + '.ticketsText', + subs=[('${COUNT}', tc_str)])) + + def _refresh_account_name_text(self) -> None: + if self._account_name_text is None: + return + try: + name_str = _ba.get_account_display_string() + except Exception: + ba.print_exception() + name_str = '??' + ba.textwidget(edit=self._account_name_text, text=name_str) + + def _refresh_achievements(self) -> None: + if (self._achievements_text is None + and self._achievements_button is None): + return + complete = sum(1 if a.complete else 0 for a in ba.app.ach.achievements) + total = len(ba.app.ach.achievements) + txt_final = ba.Lstr(resource=self._r + '.achievementProgressText', + subs=[('${COUNT}', str(complete)), + ('${TOTAL}', str(total))]) + + if self._achievements_text is not None: + ba.textwidget(edit=self._achievements_text, text=txt_final) + if self._achievements_button is not None: + ba.buttonwidget(edit=self._achievements_button, label=txt_final) + + def _link_accounts_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account import link + link.AccountLinkWindow(origin_widget=self._link_accounts_button) + + def _unlink_accounts_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account import unlink + if not self._have_unlinkable_accounts(): + ba.playsound(ba.getsound('error')) + return + unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button) + + def _player_profiles_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.profile import browser as pbrowser + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + pbrowser.ProfileBrowserWindow( + origin_widget=self._player_profiles_button) + + def _sign_out_press(self) -> None: + _ba.sign_out() + cfg = ba.app.config + + # Take note that its our *explicit* intention to not be signed in at + # this point. + cfg['Auto Account State'] = 'signed_out' + cfg.commit() + ba.buttonwidget(edit=self._sign_out_button, + label=ba.Lstr(resource=self._r + '.signingOutText')) + + def _sign_in_press(self, + account_type: str, + show_test_warning: bool = True) -> None: + del show_test_warning # unused + _ba.sign_in(account_type) + + # Make note of the type account we're *wanting* to be signed in with. + cfg = ba.app.config + cfg['Auto Account State'] = account_type + cfg.commit() + self._needs_refresh = True + ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) + + def _reset_progress(self) -> None: + try: + from ba.internal import getcampaign + # FIXME: This would need to happen server-side these days. + if self._can_reset_achievements: + ba.app.config['Achievements'] = {} + _ba.reset_achievements() + campaign = getcampaign('Default') + campaign.reset() # also writes the config.. + campaign = getcampaign('Challenges') + campaign.reset() # also writes the config.. + except Exception: + ba.print_exception('Error resetting co-op campaign progress.') + + ba.playsound(ba.getsound('shieldDown')) + self._refresh() + + def _back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.mainmenu import MainMenuWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + + if not self._modal: + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition='in_left').get_root_widget()) + + def _save_state(self) -> None: + try: + sel = self._root_widget.get_selected_child() + if sel == self._back_button: + sel_name = 'Back' + elif sel == self._scrollwidget: + sel_name = 'Scroll' + else: + raise ValueError('unrecognized selection') + ba.app.ui.window_states[type(self)] = sel_name + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + def _restore_state(self) -> None: + try: + sel_name = ba.app.ui.window_states.get(type(self)) + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'Scroll': + sel = self._scrollwidget + else: + sel = self._back_button + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') diff --git a/dist/ba_data/python/bastd/ui/account/unlink.py b/dist/ba_data/python/bastd/ui/account/unlink.py new file mode 100644 index 0000000..ef3b011 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/account/unlink.py @@ -0,0 +1,122 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for unlinking accounts.""" + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Optional, Tuple, Dict + + +class AccountUnlinkWindow(ba.Window): + """A window to kick off account unlinks.""" + + def __init__(self, origin_widget: ba.Widget = None): + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + transition = 'in_right' + bg_color = (0.4, 0.4, 0.5) + self._width = 540 + self._height = 350 + self._scroll_width = 400 + self._scroll_height = 200 + uiscale = ba.app.ui.uiscale + base_scale = (2.0 if uiscale is ba.UIScale.SMALL else + 1.6 if uiscale is ba.UIScale.MEDIUM else 1.1) + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=base_scale, + scale_origin_stack_offset=scale_origin, + stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else (0, 0))) + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + position=(30, self._height - 50), + size=(50, 50), + scale=0.7, + label='', + color=bg_color, + on_activate_call=self._cancel, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.88), + size=(0, 0), + text=ba.Lstr( + resource='accountSettingsWindow.unlinkAccountsInstructionsText' + ), + maxwidth=self._width * 0.7, + color=ba.app.ui.infotextcolor, + h_align='center', + v_align='center') + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + highlight=False, + position=((self._width - self._scroll_width) * 0.5, + self._height - 85 - self._scroll_height), + size=(self._scroll_width, self._scroll_height)) + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + self._columnwidget = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0, + left_border=10) + + our_login_id = _ba.get_public_login_id() + if our_login_id is None: + entries = [] + else: + account_infos = _ba.get_account_misc_read_val_2( + 'linkedAccounts2', []) + entries = [{ + 'name': ai['d'], + 'id': ai['id'] + } for ai in account_infos if ai['id'] != our_login_id] + + # (avoid getting our selection stuck on an empty column widget) + if not entries: + ba.containerwidget(edit=self._scrollwidget, selectable=False) + for i, entry in enumerate(entries): + txt = ba.textwidget(parent=self._columnwidget, + selectable=True, + text=entry['name'], + size=(self._scroll_width - 30, 30), + autoselect=True, + click_activate=True, + on_activate_call=ba.Call( + self._on_entry_selected, entry)) + ba.widget(edit=txt, left_widget=self._cancel_button) + if i == 0: + ba.widget(edit=txt, up_widget=self._cancel_button) + + def _on_entry_selected(self, entry: Dict[str, Any]) -> None: + ba.screenmessage(ba.Lstr(resource='pleaseWaitText', + fallback_resource='requestingText'), + color=(0, 1, 0)) + _ba.add_transaction({ + 'type': 'ACCOUNT_UNLINK_REQUEST', + 'accountID': entry['id'], + 'expire_time': time.time() + 5 + }) + _ba.run_transactions() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + + def _cancel(self) -> None: + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) diff --git a/dist/ba_data/python/bastd/ui/account/viewer.py b/dist/ba_data/python/bastd/ui/account/viewer.py new file mode 100644 index 0000000..a8ea39a --- /dev/null +++ b/dist/ba_data/python/bastd/ui/account/viewer.py @@ -0,0 +1,475 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a popup for displaying info about any account.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + from typing import Any, Tuple, Dict, Optional + + +class AccountViewerWindow(popup.PopupWindow): + """Popup window that displays info for an account.""" + + def __init__(self, + account_id: str, + profile_id: str = None, + position: Tuple[float, float] = (0.0, 0.0), + scale: float = None, + offset: Tuple[float, float] = (0.0, 0.0)): + from ba.internal import is_browser_likely_available, master_server_get + + self._account_id = account_id + self._profile_id = profile_id + + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (2.6 if uiscale is ba.UIScale.SMALL else + 1.8 if uiscale is ba.UIScale.MEDIUM else 1.4) + self._transitioning_out = False + + self._width = 400 + self._height = (300 if uiscale is ba.UIScale.SMALL else + 400 if uiscale is ba.UIScale.MEDIUM else 450) + self._subcontainer: Optional[ba.Widget] = None + + bg_color = (0.5, 0.4, 0.6) + + # Creates our _root_widget. + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=bg_color, + offset=offset) + + self._cancel_button = ba.buttonwidget( + parent=self.root_widget, + position=(50, self._height - 30), + size=(50, 50), + scale=0.5, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + + self._title_text = ba.textwidget( + parent=self.root_widget, + position=(self._width * 0.5, self._height - 20), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.6, + text=ba.Lstr(resource='playerInfoText'), + maxwidth=200, + color=(0.7, 0.7, 0.7, 0.7)) + + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + size=(self._width - 60, + self._height - 70), + position=(30, 30), + capture_arrows=True, + simple_culling_v=10) + ba.widget(edit=self._scrollwidget, autoselect=True) + + self._loading_text = ba.textwidget( + parent=self._scrollwidget, + scale=0.5, + text=ba.Lstr(value='${A}...', + subs=[('${A}', ba.Lstr(resource='loadingText'))]), + size=(self._width - 60, 100), + h_align='center', + v_align='center') + + # In cases where the user most likely has a browser/email, lets + # offer a 'report this user' button. + if (is_browser_likely_available() and _ba.get_account_misc_read_val( + 'showAccountExtrasMenu', False)): + + self._extras_menu_button = ba.buttonwidget( + parent=self.root_widget, + size=(20, 20), + position=(self._width - 60, self._height - 30), + autoselect=True, + label='...', + button_type='square', + color=(0.64, 0.52, 0.69), + textcolor=(0.57, 0.47, 0.57), + on_activate_call=self._on_extras_menu_press) + + ba.containerwidget(edit=self.root_widget, + cancel_button=self._cancel_button) + + master_server_get('bsAccountInfo', { + 'buildNumber': ba.app.build_number, + 'accountID': self._account_id, + 'profileID': self._profile_id + }, + callback=ba.WeakCall(self._on_query_response)) + + def popup_menu_selected_choice(self, window: popup.PopupMenu, + choice: str) -> None: + """Called when a menu entry is selected.""" + del window # Unused arg. + if choice == 'more': + self._on_more_press() + elif choice == 'report': + self._on_report_press() + elif choice == 'ban': + self._on_ban_press() + else: + print('ERROR: unknown account info extras menu item:', choice) + + def popup_menu_closing(self, window: popup.PopupMenu) -> None: + """Called when the popup menu is closing.""" + + def _on_extras_menu_press(self) -> None: + choices = ['more', 'report'] + choices_display = [ + ba.Lstr(resource='coopSelectWindow.seeMoreText'), + ba.Lstr(resource='reportThisPlayerText') + ] + is_admin = False + if is_admin: + ba.screenmessage('TEMP FORCING ADMIN ON') + choices.append('ban') + choices_display.append(ba.Lstr(resource='banThisPlayerText')) + + uiscale = ba.app.ui.uiscale + popup.PopupMenuWindow( + position=self._extras_menu_button.get_screen_space_center(), + scale=(2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23), + choices=choices, + choices_display=choices_display, + current_choice='more', + delegate=self) + + def _on_ban_press(self) -> None: + _ba.add_transaction({ + 'type': 'BAN_ACCOUNT', + 'account': self._account_id + }) + _ba.run_transactions() + + def _on_report_press(self) -> None: + from bastd.ui import report + report.ReportPlayerWindow(self._account_id, + origin_widget=self._extras_menu_button) + + def _on_more_press(self) -> None: + ba.open_url(_ba.get_master_server_address() + '/highscores?profile=' + + self._account_id) + + def _on_query_response(self, data: Optional[Dict[str, Any]]) -> None: + # FIXME: Tidy this up. + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-nested-blocks + if data is None: + ba.textwidget( + edit=self._loading_text, + text=ba.Lstr(resource='internal.unavailableNoConnectionText')) + else: + try: + self._loading_text.delete() + trophystr = '' + try: + trophystr = data['trophies'] + num = 10 + chunks = [ + trophystr[i:i + num] + for i in range(0, len(trophystr), num) + ] + trophystr = ('\n\n'.join(chunks)) + if trophystr == '': + trophystr = '-' + except Exception: + ba.print_exception('Error displaying trophies.') + account_name_spacing = 15 + tscale = 0.65 + ts_height = _ba.get_string_height(trophystr, + suppress_warning=True) + sub_width = self._width - 80 + sub_height = 200 + ts_height * tscale + \ + account_name_spacing * len(data['accountDisplayStrings']) + self._subcontainer = ba.containerwidget( + parent=self._scrollwidget, + size=(sub_width, sub_height), + background=False) + v = sub_height - 20 + + title_scale = 0.37 + center = 0.3 + maxwidth_scale = 0.45 + showing_character = False + if data['profileDisplayString'] is not None: + tint_color = (1, 1, 1) + try: + if data['profile'] is not None: + profile = data['profile'] + character = ba.app.spaz_appearances.get( + profile['character'], None) + if character is not None: + tint_color = (profile['color'] if 'color' + in profile else (1, 1, 1)) + tint2_color = (profile['highlight'] + if 'highlight' in profile else + (1, 1, 1)) + icon_tex = character.icon_texture + tint_tex = character.icon_mask_texture + mask_texture = ba.gettexture( + 'characterIconMask') + ba.imagewidget( + parent=self._subcontainer, + position=(sub_width * center - 40, v - 80), + size=(80, 80), + color=(1, 1, 1), + mask_texture=mask_texture, + texture=ba.gettexture(icon_tex), + tint_texture=ba.gettexture(tint_tex), + tint_color=tint_color, + tint2_color=tint2_color) + v -= 95 + except Exception: + ba.print_exception('Error displaying character.') + ba.textwidget( + parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.9, + color=ba.safecolor(tint_color, 0.7), + shadow=1.0, + text=ba.Lstr(value=data['profileDisplayString']), + maxwidth=sub_width * maxwidth_scale * 0.75) + showing_character = True + v -= 33 + + center = 0.75 if showing_character else 0.5 + maxwidth_scale = 0.45 if showing_character else 0.9 + + v = sub_height - 20 + if len(data['accountDisplayStrings']) <= 1: + account_title = ba.Lstr( + resource='settingsWindow.accountText') + else: + account_title = ba.Lstr( + resource='accountSettingsWindow.accountsText', + fallback_resource='settingsWindow.accountText') + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=ba.app.ui.infotextcolor, + text=account_title, + maxwidth=sub_width * maxwidth_scale) + draw_small = (showing_character + or len(data['accountDisplayStrings']) > 1) + v -= 14 if draw_small else 20 + for account_string in data['accountDisplayStrings']: + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.55 if draw_small else 0.8, + text=account_string, + maxwidth=sub_width * maxwidth_scale) + v -= account_name_spacing + + v += account_name_spacing + v -= 25 if showing_character else 29 + + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=ba.app.ui.infotextcolor, + text=ba.Lstr(resource='rankText'), + maxwidth=sub_width * maxwidth_scale) + v -= 14 + if data['rank'] is None: + rank_str = '-' + suffix_offset = None + else: + str_raw = ba.Lstr( + resource='league.rankInLeagueText').evaluate() + # FIXME: Would be nice to not have to eval this. + rank_str = ba.Lstr( + resource='league.rankInLeagueText', + subs=[('${RANK}', str(data['rank'][2])), + ('${NAME}', + ba.Lstr(translate=('leagueNames', + data['rank'][0]))), + ('${SUFFIX}', '')]).evaluate() + rank_str_width = min( + sub_width * maxwidth_scale, + _ba.get_string_width(rank_str, suppress_warning=True) * + 0.55) + + # Only tack our suffix on if its at the end and only for + # non-diamond leagues. + if (str_raw.endswith('${SUFFIX}') + and data['rank'][0] != 'Diamond'): + suffix_offset = rank_str_width * 0.5 + 2 + else: + suffix_offset = None + + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.55, + text=rank_str, + maxwidth=sub_width * maxwidth_scale) + if suffix_offset is not None: + assert data['rank'] is not None + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center + suffix_offset, + v + 3), + h_align='left', + v_align='center', + scale=0.29, + flatness=1.0, + text='[' + str(data['rank'][1]) + ']') + v -= 14 + + str_raw = ba.Lstr( + resource='league.rankInLeagueText').evaluate() + old_offs = -50 + prev_ranks_shown = 0 + for prev_rank in data['prevRanks']: + rank_str = ba.Lstr( + value='${S}: ${I}', + subs=[ + ('${S}', + ba.Lstr(resource='league.seasonText', + subs=[('${NUMBER}', str(prev_rank[0]))])), + ('${I}', + ba.Lstr(resource='league.rankInLeagueText', + subs=[('${RANK}', str(prev_rank[3])), + ('${NAME}', + ba.Lstr(translate=('leagueNames', + prev_rank[1]))), + ('${SUFFIX}', '')])) + ]).evaluate() + rank_str_width = min( + sub_width * maxwidth_scale, + _ba.get_string_width(rank_str, suppress_warning=True) * + 0.3) + + # Only tack our suffix on if its at the end and only for + # non-diamond leagues. + if (str_raw.endswith('${SUFFIX}') + and prev_rank[1] != 'Diamond'): + suffix_offset = rank_str_width + 2 + else: + suffix_offset = None + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center + old_offs, v), + h_align='left', + v_align='center', + scale=0.3, + text=rank_str, + flatness=1.0, + maxwidth=sub_width * maxwidth_scale) + if suffix_offset is not None: + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center + old_offs + + suffix_offset, v + 1), + h_align='left', + v_align='center', + scale=0.20, + flatness=1.0, + text='[' + str(prev_rank[2]) + ']') + prev_ranks_shown += 1 + v -= 10 + + v -= 13 + + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + flatness=1.0, + h_align='center', + v_align='center', + scale=title_scale, + color=ba.app.ui.infotextcolor, + text=ba.Lstr(resource='achievementsText'), + maxwidth=sub_width * maxwidth_scale) + v -= 14 + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=0.55, + text=str(data['achievementsCompleted']) + ' / ' + + str(len(ba.app.ach.achievements)), + maxwidth=sub_width * maxwidth_scale) + v -= 25 + + if prev_ranks_shown == 0 and showing_character: + v -= 20 + elif prev_ranks_shown == 1 and showing_character: + v -= 10 + + center = 0.5 + maxwidth_scale = 0.9 + + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + position=(sub_width * center, v), + h_align='center', + v_align='center', + scale=title_scale, + color=ba.app.ui.infotextcolor, + flatness=1.0, + text=ba.Lstr(resource='trophiesThisSeasonText', + fallback_resource='trophiesText'), + maxwidth=sub_width * maxwidth_scale) + v -= 19 + ba.textwidget(parent=self._subcontainer, + size=(0, ts_height), + position=(sub_width * 0.5, + v - ts_height * tscale), + h_align='center', + v_align='top', + corner_scale=tscale, + text=trophystr) + + except Exception: + ba.print_exception('Error displaying account info.') + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/achievements.py b/dist/ba_data/python/bastd/ui/achievements.py new file mode 100644 index 0000000..ca56c43 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/achievements.py @@ -0,0 +1,200 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a popup window to view achievements.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + from typing import Tuple + + +class AchievementsWindow(popup.PopupWindow): + """Popup window to view achievements.""" + + def __init__(self, position: Tuple[float, float], scale: float = None): + # pylint: disable=too-many-locals + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + self._transitioning_out = False + self._width = 450 + self._height = (300 if uiscale is ba.UIScale.SMALL else + 370 if uiscale is ba.UIScale.MEDIUM else 450) + bg_color = (0.5, 0.4, 0.6) + + # creates our _root_widget + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=bg_color) + + self._cancel_button = ba.buttonwidget( + parent=self.root_widget, + position=(50, self._height - 30), + size=(50, 50), + scale=0.5, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + + achievements = ba.app.ach.achievements + num_complete = len([a for a in achievements if a.complete]) + + txt_final = ba.Lstr( + resource='accountSettingsWindow.achievementProgressText', + subs=[('${COUNT}', str(num_complete)), + ('${TOTAL}', str(len(achievements)))]) + self._title_text = ba.textwidget(parent=self.root_widget, + position=(self._width * 0.5, + self._height - 20), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.6, + text=txt_final, + maxwidth=200, + color=(1, 1, 1, 0.4)) + + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + size=(self._width - 60, + self._height - 70), + position=(30, 30), + capture_arrows=True, + simple_culling_v=10) + ba.widget(edit=self._scrollwidget, autoselect=True) + + ba.containerwidget(edit=self.root_widget, + cancel_button=self._cancel_button) + + incr = 36 + sub_width = self._width - 90 + sub_height = 40 + len(achievements) * incr + + eq_rsrc = 'coopSelectWindow.powerRankingPointsEqualsText' + pts_rsrc = 'coopSelectWindow.powerRankingPointsText' + + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(sub_width, sub_height), + background=False) + + total_pts = 0 + for i, ach in enumerate(achievements): + complete = ach.complete + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 0.08 - 5, + sub_height - 20 - incr * i), + maxwidth=20, + scale=0.5, + color=(0.6, 0.6, 0.7) if complete else + (0.6, 0.6, 0.7, 0.2), + flatness=1.0, + shadow=0.0, + text=str(i + 1), + size=(0, 0), + h_align='right', + v_align='center') + + ba.imagewidget(parent=self._subcontainer, + position=(sub_width * 0.10 + 1, sub_height - 20 - + incr * i - 9) if complete else + (sub_width * 0.10 - 4, + sub_height - 20 - incr * i - 14), + size=(18, 18) if complete else (27, 27), + opacity=1.0 if complete else 0.3, + color=ach.get_icon_color(complete)[:3], + texture=ach.get_icon_texture(complete)) + if complete: + ba.imagewidget(parent=self._subcontainer, + position=(sub_width * 0.10 - 4, + sub_height - 25 - incr * i - 9), + size=(28, 28), + color=(2, 1.4, 0), + texture=ba.gettexture('achievementOutline')) + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 0.19, + sub_height - 19 - incr * i + 3), + maxwidth=sub_width * 0.62, + scale=0.6, + flatness=1.0, + shadow=0.0, + color=(1, 1, 1) if complete else (1, 1, 1, 0.2), + text=ach.display_name, + size=(0, 0), + h_align='left', + v_align='center') + + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 0.19, + sub_height - 19 - incr * i - 10), + maxwidth=sub_width * 0.62, + scale=0.4, + flatness=1.0, + shadow=0.0, + color=(0.83, 0.8, 0.85) if complete else + (0.8, 0.8, 0.8, 0.2), + text=ach.description_full_complete + if complete else ach.description_full, + size=(0, 0), + h_align='left', + v_align='center') + + pts = ach.power_ranking_value + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 0.92, + sub_height - 20 - incr * i), + maxwidth=sub_width * 0.15, + color=(0.7, 0.8, 1.0) if complete else + (0.9, 0.9, 1.0, 0.3), + flatness=1.0, + shadow=0.0, + scale=0.6, + text=ba.Lstr(resource=pts_rsrc, + subs=[('${NUMBER}', str(pts))]), + size=(0, 0), + h_align='center', + v_align='center') + if complete: + total_pts += pts + + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 1.0, + sub_height - 20 - incr * len(achievements)), + maxwidth=sub_width * 0.5, + scale=0.7, + color=(0.7, 0.8, 1.0), + flatness=1.0, + shadow=0.0, + text=ba.Lstr( + value='${A} ${B}', + subs=[ + ('${A}', + ba.Lstr(resource='coopSelectWindow.totalText')), + ('${B}', + ba.Lstr(resource=eq_rsrc, + subs=[('${NUMBER}', str(total_pts))])) + ]), + size=(0, 0), + h_align='right', + v_align='center') + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/appinvite.py b/dist/ba_data/python/bastd/ui/appinvite.py new file mode 100644 index 0000000..c058f76 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/appinvite.py @@ -0,0 +1,336 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to inviting people to try the game.""" + +from __future__ import annotations + +import copy +import time +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Optional, Dict, Union + + +class AppInviteWindow(ba.Window): + """Window for showing different ways to invite people to try the game.""" + + def __init__(self) -> None: + ba.set_analytics_screen('AppInviteWindow') + self._data: Optional[Dict[str, Any]] = None + self._width = 650 + self._height = 400 + + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition='in_scale', + scale=(1.8 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0))) + + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + scale=0.8, + position=(60, self._height - 50), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.4, 0.4, 0.6), + icon=ba.gettexture('crossOut'), + iconscale=1.2) + + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + ba.textwidget( + parent=self._root_widget, + size=(0, 0), + position=(self._width * 0.5, self._height * 0.5 + 110), + autoselect=True, + scale=0.8, + maxwidth=self._width * 0.9, + h_align='center', + v_align='center', + color=(0.3, 0.8, 0.3), + flatness=1.0, + text=ba.Lstr( + resource='gatherWindow.earnTicketsForRecommendingAmountText', + fallback_resource=( + 'gatherWindow.earnTicketsForRecommendingText'), + subs=[ + ('${COUNT}', + str(_ba.get_account_misc_read_val('friendTryTickets', + 300))), + ('${YOU_COUNT}', + str( + _ba.get_account_misc_read_val('friendTryAwardTickets', + 100))) + ])) + + or_text = ba.Lstr(resource='orText', + subs=[('${A}', ''), + ('${B}', '')]).evaluate().strip() + ba.buttonwidget( + parent=self._root_widget, + size=(250, 150), + position=(self._width * 0.5 - 125, self._height * 0.5 - 80), + autoselect=True, + button_type='square', + label=ba.Lstr(resource='gatherWindow.inviteFriendsText'), + on_activate_call=ba.WeakCall(self._google_invites)) + + ba.textwidget(parent=self._root_widget, + size=(0, 0), + position=(self._width * 0.5, self._height * 0.5 - 94), + autoselect=True, + scale=0.9, + h_align='center', + v_align='center', + color=(0.5, 0.5, 0.5), + flatness=1.0, + text=or_text) + + ba.buttonwidget( + parent=self._root_widget, + size=(180, 50), + position=(self._width * 0.5 - 90, self._height * 0.5 - 170), + autoselect=True, + color=(0.5, 0.5, 0.6), + textcolor=(0.7, 0.7, 0.8), + text_scale=0.8, + label=ba.Lstr(resource='gatherWindow.appInviteSendACodeText'), + on_activate_call=ba.WeakCall(self._send_code)) + + # kick off a transaction to get our code + _ba.add_transaction( + { + 'type': 'FRIEND_PROMO_CODE_REQUEST', + 'ali': False, + 'expire_time': time.time() + 20 + }, + callback=ba.WeakCall(self._on_code_result)) + _ba.run_transactions() + + def _on_code_result(self, result: Optional[Dict[str, Any]]) -> None: + if result is not None: + self._data = result + + def _send_code(self) -> None: + handle_app_invites_press(force_code=True) + + def _google_invites(self) -> None: + if self._data is None: + ba.screenmessage(ba.Lstr( + resource='getTicketsWindow.unavailableTemporarilyText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + if _ba.get_account_state() == 'signed_in': + ba.set_analytics_screen('App Invite UI') + _ba.show_app_invite( + ba.Lstr(resource='gatherWindow.appInviteTitleText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate(), + ba.Lstr(resource='gatherWindow.appInviteMessageText', + subs=[('${COUNT}', str(self._data['tickets'])), + ('${NAME}', _ba.get_account_name().split()[0]), + ('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate(), self._data['code']) + else: + ba.playsound(ba.getsound('error')) + + def close(self) -> None: + """Close the window.""" + ba.containerwidget(edit=self._root_widget, transition='out_scale') + + +class ShowFriendCodeWindow(ba.Window): + """Window showing a code for sharing with friends.""" + + def __init__(self, data: Dict[str, Any]): + from ba.internal import is_browser_likely_available + ba.set_analytics_screen('Friend Promo Code') + self._width = 650 + self._height = 400 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + color=(0.45, 0.63, 0.15), + transition='in_scale', + scale=(1.7 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0))) + self._data = copy.deepcopy(data) + ba.playsound(ba.getsound('cashRegister')) + ba.playsound(ba.getsound('swish')) + + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + scale=0.7, + position=(50, self._height - 50), + size=(60, 60), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.8), + size=(0, 0), + color=ba.app.ui.infotextcolor, + scale=1.0, + flatness=1.0, + h_align='center', + v_align='center', + text=ba.Lstr(resource='gatherWindow.shareThisCodeWithFriendsText'), + maxwidth=self._width * 0.85) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.645), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=2.0, + h_align='center', + v_align='center', + text=data['code'], + maxwidth=self._width * 0.85) + + award_str: Optional[Union[str, ba.Lstr]] + if self._data['awardTickets'] != 0: + award_str = ba.Lstr( + resource='gatherWindow.friendPromoCodeAwardText', + subs=[('${COUNT}', str(self._data['awardTickets']))]) + else: + award_str = '' + ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.37), + size=(0, 0), + color=ba.app.ui.infotextcolor, + scale=1.0, + flatness=1.0, + h_align='center', + v_align='center', + text=ba.Lstr( + value='${A}\n${B}\n${C}\n${D}', + subs=[ + ('${A}', + ba.Lstr( + resource='gatherWindow.friendPromoCodeRedeemLongText', + subs=[('${COUNT}', str(self._data['tickets'])), + ('${MAX_USES}', + str(self._data['usesRemaining']))])), + ('${B}', + ba.Lstr(resource=( + 'gatherWindow.friendPromoCodeWhereToEnterText'))), + ('${C}', award_str), + ('${D}', + ba.Lstr(resource='gatherWindow.friendPromoCodeExpireText', + subs=[('${EXPIRE_HOURS}', + str(self._data['expireHours']))])) + ]), + maxwidth=self._width * 0.9, + max_height=self._height * 0.35) + + if is_browser_likely_available(): + xoffs = 0 + ba.buttonwidget(parent=self._root_widget, + size=(200, 40), + position=(self._width * 0.5 - 100 + xoffs, 39), + autoselect=True, + label=ba.Lstr(resource='gatherWindow.emailItText'), + on_activate_call=ba.WeakCall(self._email)) + + def _google_invites(self) -> None: + ba.set_analytics_screen('App Invite UI') + _ba.show_app_invite( + ba.Lstr(resource='gatherWindow.appInviteTitleText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate(), + ba.Lstr(resource='gatherWindow.appInviteMessageText', + subs=[('${COUNT}', str(self._data['tickets'])), + ('${NAME}', _ba.get_account_name().split()[0]), + ('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate(), self._data['code']) + + def _email(self) -> None: + import urllib.parse + + # If somehow we got signed out. + if _ba.get_account_state() != 'signed_in': + ba.screenmessage(ba.Lstr(resource='notSignedInText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + ba.set_analytics_screen('Email Friend Code') + subject = (ba.Lstr(resource='gatherWindow.friendHasSentPromoCodeText'). + evaluate().replace( + '${NAME}', _ba.get_account_name()).replace( + '${APP_NAME}', + ba.Lstr(resource='titleText').evaluate()).replace( + '${COUNT}', str(self._data['tickets']))) + body = (ba.Lstr(resource='gatherWindow.youHaveBeenSentAPromoCodeText'). + evaluate().replace('${APP_NAME}', + ba.Lstr(resource='titleText').evaluate()) + + '\n\n' + str(self._data['code']) + '\n\n') + body += ( + (ba.Lstr(resource='gatherWindow.friendPromoCodeRedeemShortText'). + evaluate().replace('${COUNT}', str(self._data['tickets']))) + + '\n\n' + + ba.Lstr(resource='gatherWindow.friendPromoCodeInstructionsText'). + evaluate().replace('${APP_NAME}', + ba.Lstr(resource='titleText').evaluate()) + + '\n' + ba.Lstr(resource='gatherWindow.friendPromoCodeExpireText'). + evaluate().replace('${EXPIRE_HOURS}', str( + self._data['expireHours'])) + '\n' + + ba.Lstr(resource='enjoyText').evaluate()) + ba.open_url('mailto:?subject=' + urllib.parse.quote(subject) + + '&body=' + urllib.parse.quote(body)) + + def close(self) -> None: + """Close the window.""" + ba.containerwidget(edit=self._root_widget, transition='out_scale') + + +def handle_app_invites_press(force_code: bool = False) -> None: + """(internal)""" + app = ba.app + do_app_invites = (app.platform == 'android' and app.subplatform == 'google' + and _ba.get_account_misc_read_val( + 'enableAppInvites', False) and not app.on_tv) + if force_code: + do_app_invites = False + + # FIXME: Should update this to grab a code before showing the invite UI. + if do_app_invites: + AppInviteWindow() + else: + ba.screenmessage( + ba.Lstr(resource='gatherWindow.requestingAPromoCodeText'), + color=(0, 1, 0)) + + def handle_result(result: Optional[Dict[str, Any]]) -> None: + with ba.Context('ui'): + if result is None: + ba.screenmessage(ba.Lstr(resource='errorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + else: + ShowFriendCodeWindow(result) + + _ba.add_transaction( + { + 'type': 'FRIEND_PROMO_CODE_REQUEST', + 'ali': False, + 'expire_time': time.time() + 10 + }, + callback=handle_result) + _ba.run_transactions() diff --git a/dist/ba_data/python/bastd/ui/characterpicker.py b/dist/ba_data/python/bastd/ui/characterpicker.py new file mode 100644 index 0000000..6f57a20 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/characterpicker.py @@ -0,0 +1,179 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a picker for characters.""" + +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + from typing import Any, Tuple, Sequence + + +class CharacterPicker(popup.PopupWindow): + """Popup window for selecting characters.""" + + def __init__(self, + parent: ba.Widget, + position: Tuple[float, float] = (0.0, 0.0), + delegate: Any = None, + scale: float = None, + offset: Tuple[float, float] = (0.0, 0.0), + tint_color: Sequence[float] = (1.0, 1.0, 1.0), + tint2_color: Sequence[float] = (1.0, 1.0, 1.0), + selected_character: str = None): + # pylint: disable=too-many-locals + from bastd.actor import spazappearance + del parent # unused here + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (1.85 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + + self._delegate = delegate + self._transitioning_out = False + + # make a list of spaz icons + self._spazzes = spazappearance.get_appearances() + self._spazzes.sort() + self._icon_textures = [ + ba.gettexture(ba.app.spaz_appearances[s].icon_texture) + for s in self._spazzes + ] + self._icon_tint_textures = [ + ba.gettexture(ba.app.spaz_appearances[s].icon_mask_texture) + for s in self._spazzes + ] + + count = len(self._spazzes) + + columns = 3 + rows = int(math.ceil(float(count) / columns)) + + button_width = 100 + button_height = 100 + button_buffer_h = 10 + button_buffer_v = 15 + + self._width = (10 + columns * (button_width + 2 * button_buffer_h) * + (1.0 / 0.95) * (1.0 / 0.8)) + self._height = self._width * (0.8 + if uiscale is ba.UIScale.SMALL else 1.06) + + self._scroll_width = self._width * 0.8 + self._scroll_height = self._height * 0.8 + self._scroll_position = ((self._width - self._scroll_width) * 0.5, + (self._height - self._scroll_height) * 0.5) + + # creates our _root_widget + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=(0.5, 0.5, 0.5), + offset=offset, + focus_position=self._scroll_position, + focus_size=(self._scroll_width, + self._scroll_height)) + + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + size=(self._scroll_width, + self._scroll_height), + color=(0.55, 0.55, 0.55), + highlight=False, + position=self._scroll_position) + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 5 + rows * (button_height + + 2 * button_buffer_v) + 100 + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + index = 0 + mask_texture = ba.gettexture('characterIconMask') + for y in range(rows): + for x in range(columns): + pos = (x * (button_width + 2 * button_buffer_h) + + button_buffer_h, self._sub_height - (y + 1) * + (button_height + 2 * button_buffer_v) + 12) + btn = ba.buttonwidget( + parent=self._subcontainer, + button_type='square', + size=(button_width, button_height), + autoselect=True, + texture=self._icon_textures[index], + tint_texture=self._icon_tint_textures[index], + mask_texture=mask_texture, + label='', + color=(1, 1, 1), + tint_color=tint_color, + tint2_color=tint2_color, + on_activate_call=ba.Call(self._select_character, + self._spazzes[index]), + position=pos) + ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + if self._spazzes[index] == selected_character: + ba.containerwidget(edit=self._subcontainer, + selected_child=btn, + visible_child=btn) + name = ba.Lstr(translate=('characterNames', + self._spazzes[index])) + ba.textwidget(parent=self._subcontainer, + text=name, + position=(pos[0] + button_width * 0.5, + pos[1] - 12), + size=(0, 0), + scale=0.5, + maxwidth=button_width, + draw_controller=btn, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8, 0.8)) + index += 1 + + if index >= count: + break + if index >= count: + break + self._get_more_characters_button = btn = ba.buttonwidget( + parent=self._subcontainer, + size=(self._sub_width * 0.8, 60), + position=(self._sub_width * 0.1, 30), + label=ba.Lstr(resource='editProfileWindow.getMoreCharactersText'), + on_activate_call=self._on_store_press, + color=(0.6, 0.6, 0.6), + textcolor=(0.8, 0.8, 0.8), + autoselect=True) + ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) + + def _on_store_press(self) -> None: + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.store.browser import StoreBrowserWindow + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + self._transition_out() + StoreBrowserWindow(modal=True, + show_tab=StoreBrowserWindow.TabID.CHARACTERS, + origin_widget=self._get_more_characters_button) + + def _select_character(self, character: str) -> None: + if self._delegate is not None: + self._delegate.on_character_picker_pick(character) + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/colorpicker.py b/dist/ba_data/python/bastd/ui/colorpicker.py new file mode 100644 index 0000000..b9630a2 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/colorpicker.py @@ -0,0 +1,298 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides popup windows for choosing colors.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.ui.popup import PopupWindow + +if TYPE_CHECKING: + from typing import Any, Tuple, Sequence, List, Optional + + +class ColorPicker(PopupWindow): + """A popup UI to select from a set of colors. + + Passes the color to the delegate's color_picker_selected_color() method. + """ + + def __init__(self, + parent: ba.Widget, + position: Tuple[float, float], + initial_color: Sequence[float] = (1.0, 1.0, 1.0), + delegate: Any = None, + scale: float = None, + offset: Tuple[float, float] = (0.0, 0.0), + tag: Any = ''): + # pylint: disable=too-many-locals + from ba.internal import get_player_colors + + c_raw = get_player_colors() + assert len(c_raw) == 16 + self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] + + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + self._parent = parent + self._position = position + self._scale = scale + self._offset = offset + self._delegate = delegate + self._transitioning_out = False + self._tag = tag + self._initial_color = initial_color + + # Create our _root_widget. + PopupWindow.__init__(self, + position=position, + size=(210, 240), + scale=scale, + focus_position=(10, 10), + focus_size=(190, 220), + bg_color=(0.5, 0.5, 0.5), + offset=offset) + rows: List[List[ba.Widget]] = [] + closest_dist = 9999.0 + closest = (0, 0) + for y in range(4): + row: List[ba.Widget] = [] + rows.append(row) + for x in range(4): + color = self.colors[y][x] + dist = (abs(color[0] - initial_color[0]) + + abs(color[1] - initial_color[1]) + + abs(color[2] - initial_color[2])) + if dist < closest_dist: + closest = (x, y) + closest_dist = dist + btn = ba.buttonwidget(parent=self.root_widget, + position=(22 + 45 * x, 185 - 45 * y), + size=(35, 40), + label='', + button_type='square', + on_activate_call=ba.WeakCall( + self._select, x, y), + autoselect=True, + color=color, + extra_touch_border_scale=0.0) + row.append(btn) + other_button = ba.buttonwidget( + parent=self.root_widget, + position=(105 - 60, 13), + color=(0.7, 0.7, 0.7), + text_scale=0.5, + textcolor=(0.8, 0.8, 0.8), + size=(120, 30), + label=ba.Lstr(resource='otherText', + fallback_resource='coopSelectWindow.customText'), + autoselect=True, + on_activate_call=ba.WeakCall(self._select_other)) + + # Custom colors are limited to pro currently. + if not ba.app.accounts.have_pro(): + ba.imagewidget(parent=self.root_widget, + position=(50, 12), + size=(30, 30), + texture=ba.gettexture('lock'), + draw_controller=other_button) + + # If their color is close to one of our swatches, select it. + # Otherwise select 'other'. + if closest_dist < 0.03: + ba.containerwidget(edit=self.root_widget, + selected_child=rows[closest[1]][closest[0]]) + else: + ba.containerwidget(edit=self.root_widget, + selected_child=other_button) + + def get_tag(self) -> Any: + """Return this popup's tag.""" + return self._tag + + def _select_other(self) -> None: + from bastd.ui import purchase + + # Requires pro. + if not ba.app.accounts.have_pro(): + purchase.PurchaseWindow(items=['pro']) + self._transition_out() + return + ColorPickerExact(parent=self._parent, + position=self._position, + initial_color=self._initial_color, + delegate=self._delegate, + scale=self._scale, + offset=self._offset, + tag=self._tag) + + # New picker now 'owns' the delegate; we shouldn't send it any + # more messages. + self._delegate = None + self._transition_out() + + def _select(self, x: int, y: int) -> None: + if self._delegate: + self._delegate.color_picker_selected_color(self, self.colors[y][x]) + ba.timer(0.05, self._transition_out, timetype=ba.TimeType.REAL) + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + if self._delegate is not None: + self._delegate.color_picker_closing(self) + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + if not self._transitioning_out: + ba.playsound(ba.getsound('swish')) + self._transition_out() + + +class ColorPickerExact(PopupWindow): + """ pops up a ui to select from a set of colors. + passes the color to the delegate's color_picker_selected_color() method """ + + def __init__(self, + parent: ba.Widget, + position: Tuple[float, float], + initial_color: Sequence[float] = (1.0, 1.0, 1.0), + delegate: Any = None, + scale: float = None, + offset: Tuple[float, float] = (0.0, 0.0), + tag: Any = ''): + # pylint: disable=too-many-locals + del parent # Unused var. + from ba.internal import get_player_colors + c_raw = get_player_colors() + assert len(c_raw) == 16 + self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] + + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + self._delegate = delegate + self._transitioning_out = False + self._tag = tag + self._color = list(initial_color) + self._last_press_time = ba.time(ba.TimeType.REAL, + ba.TimeFormat.MILLISECONDS) + self._last_press_color_name: Optional[str] = None + self._last_press_increasing: Optional[bool] = None + self._change_speed = 1.0 + width = 180.0 + height = 240.0 + + # Creates our _root_widget. + PopupWindow.__init__(self, + position=position, + size=(width, height), + scale=scale, + focus_position=(10, 10), + focus_size=(width - 20, height - 20), + bg_color=(0.5, 0.5, 0.5), + offset=offset) + self._swatch = ba.imagewidget(parent=self.root_widget, + position=(width * 0.5 - 50, height - 70), + size=(100, 70), + texture=ba.gettexture('buttonSquare'), + color=(1, 0, 0)) + x = 50 + y = height - 90 + self._label_r: ba.Widget + self._label_g: ba.Widget + self._label_b: ba.Widget + for color_name, color_val in [('r', (1, 0.15, 0.15)), + ('g', (0.15, 1, 0.15)), + ('b', (0.15, 0.15, 1))]: + txt = ba.textwidget(parent=self.root_widget, + position=(x - 10, y), + size=(0, 0), + h_align='center', + color=color_val, + v_align='center', + text='0.12') + setattr(self, '_label_' + color_name, txt) + for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]: + ba.buttonwidget(parent=self.root_widget, + position=(x + bhval, y - 15), + scale=0.8, + repeat=True, + text_scale=1.3, + size=(40, 40), + label=b_label, + autoselect=True, + enable_sound=False, + on_activate_call=ba.WeakCall( + self._color_change_press, color_name, + binc)) + y -= 42 + + btn = ba.buttonwidget(parent=self.root_widget, + position=(width * 0.5 - 40, 10), + size=(80, 30), + text_scale=0.6, + color=(0.6, 0.6, 0.6), + textcolor=(0.7, 0.7, 0.7), + label=ba.Lstr(resource='doneText'), + on_activate_call=ba.WeakCall( + self._transition_out), + autoselect=True) + ba.containerwidget(edit=self.root_widget, start_button=btn) + + # Unlike the swatch picker, we stay open and constantly push our + # color to the delegate, so start doing that. + self._update_for_color() + + def _update_for_color(self) -> None: + if not self.root_widget: + return + ba.imagewidget(edit=self._swatch, color=self._color) + + # We generate these procedurally, so pylint misses them. + # FIXME: create static attrs instead. + ba.textwidget(edit=self._label_r, text='%.2f' % self._color[0]) + ba.textwidget(edit=self._label_g, text='%.2f' % self._color[1]) + ba.textwidget(edit=self._label_b, text='%.2f' % self._color[2]) + if self._delegate is not None: + self._delegate.color_picker_selected_color(self, self._color) + + def _color_change_press(self, color_name: str, increasing: bool) -> None: + # If we get rapid-fire presses, eventually start moving faster. + current_time = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) + since_last = current_time - self._last_press_time + if (since_last < 200 and self._last_press_color_name == color_name + and self._last_press_increasing == increasing): + self._change_speed += 0.25 + else: + self._change_speed = 1.0 + self._last_press_time = current_time + self._last_press_color_name = color_name + self._last_press_increasing = increasing + + color_index = ('r', 'g', 'b').index(color_name) + offs = int(self._change_speed) * (0.01 if increasing else -0.01) + self._color[color_index] = max( + 0.0, min(1.0, self._color[color_index] + offs)) + self._update_for_color() + + def get_tag(self) -> Any: + """Return this popup's tag value.""" + return self._tag + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + if self._delegate is not None: + self._delegate.color_picker_closing(self) + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + if not self._transitioning_out: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/config.py b/dist/ba_data/python/bastd/ui/config.py new file mode 100644 index 0000000..0c11ff4 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/config.py @@ -0,0 +1,162 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality for editing config values and applying them to the game.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, Tuple, Union, Callable + + +class ConfigCheckBox: + """A checkbox wired up to control a config value. + + It will automatically save and apply the config when its + value changes. + + Attributes: + + widget + The underlying ba.Widget instance. + """ + + def __init__(self, + parent: ba.Widget, + configkey: str, + position: Tuple[float, float], + size: Tuple[float, float], + displayname: Union[str, ba.Lstr] = None, + scale: float = None, + maxwidth: float = None, + autoselect: bool = True, + value_change_call: Callable[[Any], Any] = None): + if displayname is None: + displayname = configkey + self._value_change_call = value_change_call + self._configkey = configkey + self.widget = ba.checkboxwidget( + parent=parent, + autoselect=autoselect, + position=position, + size=size, + text=displayname, + textcolor=(0.8, 0.8, 0.8), + value=ba.app.config.resolve(configkey), + on_value_change_call=self._value_changed, + scale=scale, + maxwidth=maxwidth) + # complain if we outlive our checkbox + ba.uicleanupcheck(self, self.widget) + + def _value_changed(self, val: bool) -> None: + cfg = ba.app.config + cfg[self._configkey] = val + if self._value_change_call is not None: + self._value_change_call(val) + cfg.apply_and_commit() + + +class ConfigNumberEdit: + """A set of controls for editing a numeric config value. + + It will automatically save and apply the config when its + value changes. + + Attributes: + + nametext + The text widget displaying the name. + + valuetext + The text widget displaying the current value. + + minusbutton + The button widget used to reduce the value. + + plusbutton + The button widget used to increase the value. + """ + + def __init__(self, + parent: ba.Widget, + configkey: str, + position: Tuple[float, float], + minval: float = 0.0, + maxval: float = 100.0, + increment: float = 1.0, + callback: Callable[[float], Any] = None, + xoffset: float = 0.0, + displayname: Union[str, ba.Lstr] = None, + changesound: bool = True, + textscale: float = 1.0): + if displayname is None: + displayname = configkey + + self._configkey = configkey + self._minval = minval + self._maxval = maxval + self._increment = increment + self._callback = callback + self._value = ba.app.config.resolve(configkey) + + self.nametext = ba.textwidget(parent=parent, + position=position, + size=(100, 30), + text=displayname, + maxwidth=160 + xoffset, + color=(0.8, 0.8, 0.8, 1.0), + h_align='left', + v_align='center', + scale=textscale) + self.valuetext = ba.textwidget(parent=parent, + position=(246 + xoffset, position[1]), + size=(60, 28), + editable=False, + color=(0.3, 1.0, 0.3, 1.0), + h_align='right', + v_align='center', + text=str(self._value), + padding=2) + self.minusbutton = ba.buttonwidget( + parent=parent, + position=(330 + xoffset, position[1]), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=ba.Call(self._down), + repeat=True, + enable_sound=changesound) + self.plusbutton = ba.buttonwidget(parent=parent, + position=(380 + xoffset, + position[1]), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=ba.Call(self._up), + repeat=True, + enable_sound=changesound) + # Complain if we outlive our widgets. + ba.uicleanupcheck(self, self.nametext) + self._update_display() + + def _up(self) -> None: + self._value = min(self._maxval, self._value + self._increment) + self._changed() + + def _down(self) -> None: + self._value = max(self._minval, self._value - self._increment) + self._changed() + + def _changed(self) -> None: + self._update_display() + if self._callback: + self._callback(self._value) + ba.app.config[self._configkey] = self._value + ba.app.config.apply_and_commit() + + def _update_display(self) -> None: + ba.textwidget(edit=self.valuetext, text=f'{self._value:.1f}') diff --git a/dist/ba_data/python/bastd/ui/configerror.py b/dist/ba_data/python/bastd/ui/configerror.py new file mode 100644 index 0000000..b94c580 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/configerror.py @@ -0,0 +1,75 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI for dealing with broken config files.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + pass + + +class ConfigErrorWindow(ba.Window): + """Window for dealing with a broken config.""" + + def __init__(self) -> None: + self._config_file_path = ba.app.config_file_path + width = 800 + super().__init__( + ba.containerwidget(size=(width, 300), transition='in_right')) + padding = 20 + ba.textwidget( + parent=self._root_widget, + position=(padding, 220), + size=(width - 2 * padding, 100 - 2 * padding), + h_align='center', + v_align='top', + scale=0.73, + text=(f'Error reading {_ba.appnameupper()} config file' + ':\n\n\nCheck the console' + ' (press ~ twice) for details.\n\nWould you like to quit and' + ' try to fix it by hand\nor overwrite it with defaults?\n\n' + '(high scores, player profiles, etc will be lost if you' + ' overwrite)')) + ba.textwidget(parent=self._root_widget, + position=(padding, 198), + size=(width - 2 * padding, 100 - 2 * padding), + h_align='center', + v_align='top', + scale=0.5, + text=self._config_file_path) + quit_button = ba.buttonwidget(parent=self._root_widget, + position=(35, 30), + size=(240, 54), + label='Quit and Edit', + on_activate_call=self._quit) + ba.buttonwidget(parent=self._root_widget, + position=(width - 370, 30), + size=(330, 54), + label='Overwrite with Defaults', + on_activate_call=self._defaults) + ba.containerwidget(edit=self._root_widget, + cancel_button=quit_button, + selected_child=quit_button) + + def _quit(self) -> None: + ba.timer(0.001, self._edit_and_quit, timetype=ba.TimeType.REAL) + _ba.lock_all_input() + + def _edit_and_quit(self) -> None: + _ba.open_file_externally(self._config_file_path) + ba.timer(0.1, ba.quit, timetype=ba.TimeType.REAL) + + def _defaults(self) -> None: + from ba.internal import commit_app_config + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.playsound(ba.getsound('gunCocking')) + ba.screenmessage('settings reset.', color=(1, 1, 0)) + + # At this point settings are already set; lets just commit them + # to disk. + commit_app_config(force=True) diff --git a/dist/ba_data/python/bastd/ui/confirm.py b/dist/ba_data/python/bastd/ui/confirm.py new file mode 100644 index 0000000..fc65aaf --- /dev/null +++ b/dist/ba_data/python/bastd/ui/confirm.py @@ -0,0 +1,159 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides ConfirmWindow base class and commonly used derivatives.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Union, Callable, Tuple, Optional + + +class ConfirmWindow: + """Window for answering simple yes/no questions.""" + + def __init__(self, + text: Union[str, ba.Lstr] = 'Are you sure?', + action: Callable[[], Any] = None, + width: float = 360.0, + height: float = 100.0, + cancel_button: bool = True, + cancel_is_selected: bool = False, + color: Tuple[float, float, float] = (1, 1, 1), + text_scale: float = 1.0, + ok_text: Union[str, ba.Lstr] = None, + cancel_text: Union[str, ba.Lstr] = None, + origin_widget: ba.Widget = None): + # pylint: disable=too-many-locals + if ok_text is None: + ok_text = ba.Lstr(resource='okText') + if cancel_text is None: + cancel_text = ba.Lstr(resource='cancelText') + height += 40 + if width < 360: + width = 360 + self._action = action + + # if they provided an origin-widget, scale up from that + self._transition_out: Optional[str] + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = None + scale_origin = None + transition = 'in_right' + + uiscale = ba.app.ui.uiscale + self.root_widget = ba.containerwidget( + size=(width, height), + transition=transition, + toolbar_visibility='menu_minimal_no_back', + parent=_ba.get_special_widget('overlay_stack'), + scale=(2.1 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + scale_origin_stack_offset=scale_origin) + + ba.textwidget(parent=self.root_widget, + position=(width * 0.5, height - 5 - (height - 75) * 0.5), + size=(0, 0), + h_align='center', + v_align='center', + text=text, + scale=text_scale, + color=color, + maxwidth=width * 0.9, + max_height=height - 75) + + cbtn: Optional[ba.Widget] + if cancel_button: + cbtn = btn = ba.buttonwidget(parent=self.root_widget, + autoselect=True, + position=(20, 20), + size=(150, 50), + label=cancel_text, + on_activate_call=self._cancel) + ba.containerwidget(edit=self.root_widget, cancel_button=btn) + ok_button_h = width - 175 + else: + # if they don't want a cancel button, we still want back presses to + # be able to dismiss the window; just wire it up to do the ok + # button + ok_button_h = width * 0.5 - 75 + cbtn = None + btn = ba.buttonwidget(parent=self.root_widget, + autoselect=True, + position=(ok_button_h, 20), + size=(150, 50), + label=ok_text, + on_activate_call=self._ok) + + # if they didn't want a cancel button, we still want to be able to hit + # cancel/back/etc to dismiss the window + if not cancel_button: + ba.containerwidget(edit=self.root_widget, + on_cancel_call=btn.activate) + + ba.containerwidget(edit=self.root_widget, + selected_child=(cbtn if cbtn is not None + and cancel_is_selected else btn), + start_button=btn) + + def _cancel(self) -> None: + ba.containerwidget( + edit=self.root_widget, + transition=('out_right' if self._transition_out is None else + self._transition_out)) + + def _ok(self) -> None: + if not self.root_widget: + return + ba.containerwidget( + edit=self.root_widget, + transition=('out_left' if self._transition_out is None else + self._transition_out)) + if self._action is not None: + self._action() + + +class QuitWindow: + """Popup window to confirm quitting.""" + + def __init__(self, + swish: bool = False, + back: bool = False, + origin_widget: ba.Widget = None): + ui = ba.app.ui + app = ba.app + self._back = back + + # If there's already one of us up somewhere, kill it. + if ui.quit_window is not None: + ui.quit_window.delete() + ui.quit_window = None + if swish: + ba.playsound(ba.getsound('swish')) + quit_resource = ('quitGameText' + if app.platform == 'mac' else 'exitGameText') + self._root_widget = ui.quit_window = (ConfirmWindow( + ba.Lstr(resource=quit_resource, + subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))]), + self._fade_and_quit, + origin_widget=origin_widget).root_widget) + + def _fade_and_quit(self) -> None: + _ba.fade_screen(False, + time=0.2, + endcall=lambda: ba.quit(soft=True, back=self._back)) + _ba.lock_all_input() + + # Unlock and fade back in shortly.. just in case something goes wrong + # (or on android where quit just backs out of our activity and + # we may come back) + ba.timer(0.3, _ba.unlock_all_input, timetype=ba.TimeType.REAL) diff --git a/dist/ba_data/python/bastd/ui/continues.py b/dist/ba_data/python/bastd/ui/continues.py new file mode 100644 index 0000000..d5644a1 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/continues.py @@ -0,0 +1,206 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a popup window to continue a game.""" + +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + +class ContinuesWindow(ba.Window): + """A window to continue a game.""" + + def __init__(self, activity: ba.Activity, cost: int, + continue_call: Callable[[], Any], cancel_call: Callable[[], + Any]): + self._activity = weakref.ref(activity) + self._cost = cost + self._continue_call = continue_call + self._cancel_call = cancel_call + self._start_count = self._count = 20 + self._width = 300 + self._height = 200 + self._transitioning_out = False + super().__init__( + ba.containerwidget(size=(self._width, self._height), + background=False, + toolbar_visibility='menu_currency', + transition='in_scale', + scale=1.5)) + txt = (ba.Lstr( + resource='continuePurchaseText').evaluate().split('${PRICE}')) + t_left = txt[0] + t_left_width = _ba.get_string_width(t_left, suppress_warning=True) + t_price = ba.charstr(ba.SpecialChar.TICKET) + str(self._cost) + t_price_width = _ba.get_string_width(t_price, suppress_warning=True) + t_right = txt[-1] + t_right_width = _ba.get_string_width(t_right, suppress_warning=True) + width_total_half = (t_left_width + t_price_width + t_right_width) * 0.5 + + ba.textwidget(parent=self._root_widget, + text=t_left, + flatness=1.0, + shadow=1.0, + size=(0, 0), + h_align='left', + v_align='center', + position=(self._width * 0.5 - width_total_half, + self._height - 30)) + ba.textwidget(parent=self._root_widget, + text=t_price, + flatness=1.0, + shadow=1.0, + color=(0.2, 1.0, 0.2), + size=(0, 0), + position=(self._width * 0.5 - width_total_half + + t_left_width, self._height - 30), + h_align='left', + v_align='center') + ba.textwidget(parent=self._root_widget, + text=t_right, + flatness=1.0, + shadow=1.0, + size=(0, 0), + h_align='left', + v_align='center', + position=(self._width * 0.5 - width_total_half + + t_left_width + t_price_width + 5, + self._height - 30)) + + self._tickets_text_base: Optional[str] + self._tickets_text: Optional[ba.Widget] + if not ba.app.ui.use_toolbars: + self._tickets_text_base = ba.Lstr( + resource='getTicketsWindow.youHaveShortText', + fallback_resource='getTicketsWindow.youHaveText').evaluate() + self._tickets_text = ba.textwidget( + parent=self._root_widget, + text='', + flatness=1.0, + color=(0.2, 1.0, 0.2), + shadow=1.0, + position=(self._width * 0.5 + width_total_half, + self._height - 50), + size=(0, 0), + scale=0.35, + h_align='right', + v_align='center') + else: + self._tickets_text_base = None + self._tickets_text = None + + self._counter_text = ba.textwidget(parent=self._root_widget, + text=str(self._count), + color=(0.7, 0.7, 0.7), + scale=1.2, + size=(0, 0), + big=True, + position=(self._width * 0.5, + self._height - 80), + flatness=1.0, + shadow=1.0, + h_align='center', + v_align='center') + self._cancel_button = ba.buttonwidget( + parent=self._root_widget, + position=(30, 30), + size=(120, 50), + label=ba.Lstr(resource='endText', fallback_resource='cancelText'), + autoselect=True, + enable_sound=False, + on_activate_call=self._on_cancel_press) + self._continue_button = ba.buttonwidget( + parent=self._root_widget, + label=ba.Lstr(resource='continueText'), + autoselect=True, + position=(self._width - 130, 30), + size=(120, 50), + on_activate_call=self._on_continue_press) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button, + start_button=self._continue_button, + selected_child=self._cancel_button) + + self._counting_down = True + self._countdown_timer = ba.Timer(1.0, + ba.WeakCall(self._tick), + repeat=True, + timetype=ba.TimeType.REAL) + self._tick() + + def _tick(self) -> None: + # if our target activity is gone or has ended, go away + activity = self._activity() + if activity is None or activity.has_ended(): + self._on_cancel() + return + + if _ba.get_account_state() == 'signed_in': + sval = (ba.charstr(ba.SpecialChar.TICKET) + + str(_ba.get_account_ticket_count())) + else: + sval = '?' + if self._tickets_text is not None: + assert self._tickets_text_base is not None + ba.textwidget(edit=self._tickets_text, + text=self._tickets_text_base.replace( + '${COUNT}', sval)) + + if self._counting_down: + self._count -= 1 + ba.playsound(ba.getsound('tick')) + if self._count <= 0: + self._on_cancel() + else: + ba.textwidget(edit=self._counter_text, text=str(self._count)) + + def _on_cancel_press(self) -> None: + # disallow for first second + if self._start_count - self._count < 2: + ba.playsound(ba.getsound('error')) + else: + self._on_cancel() + + def _on_continue_press(self) -> None: + from bastd.ui import getcurrency + + # Disallow for first second. + if self._start_count - self._count < 2: + ba.playsound(ba.getsound('error')) + else: + # If somehow we got signed out... + if _ba.get_account_state() != 'signed_in': + ba.screenmessage(ba.Lstr(resource='notSignedInText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + # If it appears we don't have enough tickets, offer to buy more. + tickets = _ba.get_account_ticket_count() + if tickets < self._cost: + # FIXME: Should we start the timer back up again after? + self._counting_down = False + ba.textwidget(edit=self._counter_text, text='') + ba.playsound(ba.getsound('error')) + getcurrency.show_get_tickets_prompt() + return + if not self._transitioning_out: + ba.playsound(ba.getsound('swish')) + self._transitioning_out = True + ba.containerwidget(edit=self._root_widget, + transition='out_scale') + self._continue_call() + + def _on_cancel(self) -> None: + if not self._transitioning_out: + ba.playsound(ba.getsound('swish')) + self._transitioning_out = True + ba.containerwidget(edit=self._root_widget, transition='out_scale') + self._cancel_call() diff --git a/dist/ba_data/python/bastd/ui/coop/__init__.py b/dist/ba_data/python/bastd/ui/coop/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/coop/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/ui/coop/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/coop/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/coop/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/coop/__pycache__/browser.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/coop/__pycache__/browser.cpython-38.opt-1.pyc new file mode 100644 index 0000000..34453c4 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/coop/__pycache__/browser.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/coop/__pycache__/gamebutton.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/coop/__pycache__/gamebutton.cpython-38.opt-1.pyc new file mode 100644 index 0000000..dd63569 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/coop/__pycache__/gamebutton.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/coop/__pycache__/level.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/coop/__pycache__/level.cpython-38.opt-1.pyc new file mode 100644 index 0000000..dc24cc2 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/coop/__pycache__/level.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/coop/browser.py b/dist/ba_data/python/bastd/ui/coop/browser.py new file mode 100644 index 0000000..c8d0eee --- /dev/null +++ b/dist/ba_data/python/bastd/ui/coop/browser.py @@ -0,0 +1,1592 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI for browsing available co-op levels/games/etc.""" +# FIXME: Break this up. +# pylint: disable=too-many-lines + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui.store.button import StoreButton +from bastd.ui.league.rankbutton import LeagueRankButton +from bastd.ui.store.browser import StoreBrowserWindow + +if TYPE_CHECKING: + from typing import Any, Optional, Tuple, Dict, List, Union + + +class CoopBrowserWindow(ba.Window): + """Window for browsing co-op levels/games/etc.""" + + def _update_corner_button_positions(self) -> None: + uiscale = ba.app.ui.uiscale + offs = (-55 if uiscale is ba.UIScale.SMALL + and _ba.is_party_icon_visible() else 0) + if self._league_rank_button is not None: + self._league_rank_button.set_position( + (self._width - 282 + offs - self._x_inset, self._height - 85 - + (4 if uiscale is ba.UIScale.SMALL else 0))) + if self._store_button is not None: + self._store_button.set_position( + (self._width - 170 + offs - self._x_inset, self._height - 85 - + (4 if uiscale is ba.UIScale.SMALL else 0))) + + def __init__(self, + transition: Optional[str] = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=cyclic-import + import threading + + # Preload some modules we use in a background thread so we won't + # have a visual hitch when the user taps them. + threading.Thread(target=self._preload_modules).start() + + ba.set_analytics_screen('Coop Window') + + app = ba.app + cfg = app.config + + # Quick note to players that tourneys won't work in ballistica + # core builds. (need to split the word so it won't get subbed out) + if 'ballistica' + 'core' == _ba.appname(): + ba.timer(1.0, + lambda: ba.screenmessage( + ba.Lstr(resource='noTournamentsInTestBuildText'), + color=(1, 1, 0), + ), + timetype=ba.TimeType.REAL) + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + # Try to recreate the same number of buttons we had last time so our + # re-selection code works. + self._tournament_button_count = app.config.get('Tournament Rows', 0) + assert isinstance(self._tournament_button_count, int) + + self._easy_button: Optional[ba.Widget] = None + self._hard_button: Optional[ba.Widget] = None + self._hard_button_lock_image: Optional[ba.Widget] = None + self._campaign_percent_text: Optional[ba.Widget] = None + + uiscale = ba.app.ui.uiscale + self._width = 1320 if uiscale is ba.UIScale.SMALL else 1120 + self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (657 if uiscale is ba.UIScale.SMALL else + 730 if uiscale is ba.UIScale.MEDIUM else 800) + app.ui.set_main_menu_location('Coop Select') + self._r = 'coopSelectWindow' + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + + self._tourney_data_up_to_date = False + + self._campaign_difficulty = _ba.get_account_misc_val( + 'campaignDifficulty', 'easy') + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + toolbar_visibility='menu_full', + scale_origin_stack_offset=scale_origin, + stack_offset=((0, -15) if uiscale is ba.UIScale.SMALL else ( + 0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0)), + transition=transition, + scale=(1.2 if uiscale is ba.UIScale.SMALL else + 0.8 if uiscale is ba.UIScale.MEDIUM else 0.75))) + + if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + self._back_button = None + else: + self._back_button = ba.buttonwidget( + parent=self._root_widget, + position=(75 + x_inset, self._height - 87 - + (4 if uiscale is ba.UIScale.SMALL else 0)), + size=(120, 60), + scale=1.2, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back') + + self._league_rank_button: Optional[LeagueRankButton] + self._store_button: Optional[StoreButton] + self._store_button_widget: Optional[ba.Widget] + self._league_rank_button_widget: Optional[ba.Widget] + + if not app.ui.use_toolbars: + prb = self._league_rank_button = LeagueRankButton( + parent=self._root_widget, + position=(self._width - (282 + x_inset), self._height - 85 - + (4 if uiscale is ba.UIScale.SMALL else 0)), + size=(100, 60), + color=(0.4, 0.4, 0.9), + textcolor=(0.9, 0.9, 2.0), + scale=1.05, + on_activate_call=ba.WeakCall(self._switch_to_league_rankings)) + self._league_rank_button_widget = prb.get_button() + + sbtn = self._store_button = StoreButton( + parent=self._root_widget, + position=(self._width - (170 + x_inset), self._height - 85 - + (4 if uiscale is ba.UIScale.SMALL else 0)), + size=(100, 60), + color=(0.6, 0.4, 0.7), + show_tickets=True, + button_type='square', + sale_scale=0.85, + textcolor=(0.9, 0.7, 1.0), + scale=1.05, + on_activate_call=ba.WeakCall(self._switch_to_score, None)) + self._store_button_widget = sbtn.get_button() + ba.widget(edit=self._back_button, + right_widget=self._league_rank_button_widget) + ba.widget(edit=self._league_rank_button_widget, + left_widget=self._back_button) + else: + self._league_rank_button = None + self._store_button = None + self._store_button_widget = None + self._league_rank_button_widget = None + + # Move our corner buttons dynamically to keep them out of the way of + # the party icon :-( + self._update_corner_button_positions() + self._update_corner_button_positions_timer = ba.Timer( + 1.0, + ba.WeakCall(self._update_corner_button_positions), + repeat=True, + timetype=ba.TimeType.REAL) + + self._last_tournament_query_time: Optional[float] = None + self._last_tournament_query_response_time: Optional[float] = None + self._doing_tournament_query = False + + self._selected_campaign_level = (cfg.get( + 'Selected Coop Campaign Level', None)) + self._selected_custom_level = (cfg.get('Selected Coop Custom Level', + None)) + self._selected_challenge_level = (cfg.get( + 'Selected Coop Challenge Level', None)) + + # Don't want initial construction affecting our last-selected. + self._do_selection_callbacks = False + v = self._height - 95 + txt = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, + v + 40 - (0 if uiscale is ba.UIScale.SMALL else 0)), + size=(0, 0), + text=ba.Lstr(resource='playModes.singlePlayerCoopText', + fallback_resource='playModes.coopText'), + h_align='center', + color=app.ui.title_color, + scale=1.5, + maxwidth=500, + v_align='center') + + if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + ba.textwidget(edit=txt, text='') + + if self._back_button is not None: + ba.buttonwidget( + edit=self._back_button, + button_type='backSmall', + size=(60, 50), + position=(75 + x_inset, self._height - 87 - + (4 if uiscale is ba.UIScale.SMALL else 0) + 6), + label=ba.charstr(ba.SpecialChar.BACK)) + + self._selected_row = cfg.get('Selected Coop Row', None) + + self.star_tex = ba.gettexture('star') + self.lsbt = ba.getmodel('level_select_button_transparent') + self.lsbo = ba.getmodel('level_select_button_opaque') + self.a_outline_tex = ba.gettexture('achievementOutline') + self.a_outline_model = ba.getmodel('achievementOutline') + + self._scroll_width = self._width - (130 + 2 * x_inset) + self._scroll_height = (self._height - + (190 if uiscale is ba.UIScale.SMALL + and app.ui.use_toolbars else 160)) + + self._subcontainerwidth = 800.0 + self._subcontainerheight = 1400.0 + + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + highlight=False, + position=(65 + x_inset, 120) if uiscale is ba.UIScale.SMALL + and app.ui.use_toolbars else (65 + x_inset, 70), + size=(self._scroll_width, self._scroll_height), + simple_culling_v=10.0, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + self._subcontainer: Optional[ba.Widget] = None + + # Take note of our account state; we'll refresh later if this changes. + self._account_state_num = _ba.get_account_state_num() + + # Same for fg/bg state. + self._fg_state = app.fg_state + + self._refresh() + self._restore_state() + + # Even though we might display cached tournament data immediately, we + # don't consider it valid until we've pinged. + # the server for an update + self._tourney_data_up_to_date = False + + # If we've got a cached tournament list for our account and info for + # each one of those tournaments, go ahead and display it as a + # starting point. + if (app.accounts.account_tournament_list is not None + and app.accounts.account_tournament_list[0] + == _ba.get_account_state_num() + and all(t_id in app.accounts.tournament_info + for t_id in app.accounts.account_tournament_list[1])): + tourney_data = [ + app.accounts.tournament_info[t_id] + for t_id in app.accounts.account_tournament_list[1] + ] + self._update_for_data(tourney_data) + + # This will pull new data periodically, update timers, etc. + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + self._update() + + @staticmethod + def _preload_modules() -> None: + """Preload modules we use (called in bg thread).""" + import bastd.ui.purchase as _unused1 + import bastd.ui.coop.gamebutton as _unused2 + import bastd.ui.confirm as _unused3 + import bastd.ui.account as _unused4 + import bastd.ui.league.rankwindow as _unused5 + import bastd.ui.store.browser as _unused6 + import bastd.ui.account.viewer as _unused7 + import bastd.ui.tournamentscores as _unused8 + import bastd.ui.tournamententry as _unused9 + import bastd.ui.play as _unused10 + + def _update(self) -> None: + # Do nothing if we've somehow outlived our actual UI. + if not self._root_widget: + return + + cur_time = ba.time(ba.TimeType.REAL) + + # If its been a while since we got a tournament update, consider the + # data invalid (prevents us from joining tournaments if our internet + # connection goes down for a while). + if (self._last_tournament_query_response_time is None + or ba.time(ba.TimeType.REAL) - + self._last_tournament_query_response_time > 60.0 * 2): + self._tourney_data_up_to_date = False + + # If our account state has changed, do a full request. + account_state_num = _ba.get_account_state_num() + if account_state_num != self._account_state_num: + self._account_state_num = account_state_num + self._save_state() + self._refresh() + + # Also encourage a new tournament query since this will clear out + # our current results. + if not self._doing_tournament_query: + self._last_tournament_query_time = None + + # If we've been backgrounded/foregrounded, invalidate our + # tournament entries (they will be refreshed below asap). + if self._fg_state != ba.app.fg_state: + self._tourney_data_up_to_date = False + + # Send off a new tournament query if its been long enough or whatnot. + if not self._doing_tournament_query and ( + self._last_tournament_query_time is None + or cur_time - self._last_tournament_query_time > 30.0 + or self._fg_state != ba.app.fg_state): + self._fg_state = ba.app.fg_state + self._last_tournament_query_time = cur_time + self._doing_tournament_query = True + _ba.tournament_query( + args={ + 'source': 'coop window refresh', + 'numScores': 1 + }, + callback=ba.WeakCall(self._on_tournament_query_response), + ) + + # Decrement time on our tournament buttons. + ads_enabled = _ba.have_incentivized_ad() + for tbtn in self._tournament_buttons: + tbtn['time_remaining'] = max(0, tbtn['time_remaining'] - 1) + if tbtn['time_remaining_value_text'] is not None: + ba.textwidget( + edit=tbtn['time_remaining_value_text'], + text=ba.timestring(tbtn['time_remaining'], + centi=False, + suppress_format_warning=True) if + (tbtn['has_time_remaining'] + and self._tourney_data_up_to_date) else '-') + + # Also adjust the ad icon visibility. + if tbtn.get('allow_ads', False) and _ba.has_video_ads(): + ba.imagewidget(edit=tbtn['entry_fee_ad_image'], + opacity=1.0 if ads_enabled else 0.25) + ba.textwidget(edit=tbtn['entry_fee_text_remaining'], + color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2)) + + self._update_hard_mode_lock_image() + + def _update_hard_mode_lock_image(self) -> None: + try: + ba.imagewidget( + edit=self._hard_button_lock_image, + opacity=0.0 if ba.app.accounts.have_pro_options() else 1.0) + except Exception: + ba.print_exception('Error updating campaign lock.') + + def _update_for_data(self, data: Optional[List[Dict[str, Any]]]) -> None: + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + from ba.internal import getcampaign, get_tournament_prize_strings + + # If the number of tournaments or challenges in the data differs from + # our current arrangement, refresh with the new number. + if (((data is None and (self._tournament_button_count != 0)) + or (data is not None and + (len(data) != self._tournament_button_count)))): + self._tournament_button_count = len( + data) if data is not None else 0 + ba.app.config['Tournament Rows'] = self._tournament_button_count + self._refresh() + + # Update all of our tourney buttons based on whats in data. + for i, tbtn in enumerate(self._tournament_buttons): + assert data is not None + entry: Dict[str, Any] = data[i] + prize_y_offs = (34 if 'prizeRange3' in entry else + 20 if 'prizeRange2' in entry else 12) + x_offs = 90 + + # This seems to be a false alarm. + # pylint: disable=unbalanced-tuple-unpacking + pr1, pv1, pr2, pv2, pr3, pv3 = ( + get_tournament_prize_strings(entry)) + # pylint: enable=unbalanced-tuple-unpacking + enabled = 'requiredLeague' not in entry + ba.buttonwidget(edit=tbtn['button'], + color=(0.5, 0.7, 0.2) if enabled else + (0.5, 0.5, 0.5)) + ba.imagewidget(edit=tbtn['lock_image'], + opacity=0.0 if enabled else 1.0) + ba.textwidget(edit=tbtn['prize_range_1_text'], + text='-' if pr1 == '' else pr1, + position=(tbtn['button_x'] + 365 + x_offs, + tbtn['button_y'] + tbtn['button_scale_y'] - + 93 + prize_y_offs)) + + # We want to draw values containing tickets a bit smaller + # (scratch that; we now draw medals a bit bigger). + ticket_char = ba.charstr(ba.SpecialChar.TICKET_BACKING) + prize_value_scale_large = 1.0 + prize_value_scale_small = 1.0 + + ba.textwidget(edit=tbtn['prize_value_1_text'], + text='-' if pv1 == '' else pv1, + scale=prize_value_scale_large if ticket_char + not in pv1 else prize_value_scale_small, + position=(tbtn['button_x'] + 380 + x_offs, + tbtn['button_y'] + tbtn['button_scale_y'] - + 93 + prize_y_offs)) + + ba.textwidget(edit=tbtn['prize_range_2_text'], + text=pr2, + position=(tbtn['button_x'] + 365 + x_offs, + tbtn['button_y'] + tbtn['button_scale_y'] - + 93 - 45 + prize_y_offs)) + ba.textwidget(edit=tbtn['prize_value_2_text'], + text=pv2, + scale=prize_value_scale_large if ticket_char + not in pv2 else prize_value_scale_small, + position=(tbtn['button_x'] + 380 + x_offs, + tbtn['button_y'] + tbtn['button_scale_y'] - + 93 - 45 + prize_y_offs)) + + ba.textwidget(edit=tbtn['prize_range_3_text'], + text=pr3, + position=(tbtn['button_x'] + 365 + x_offs, + tbtn['button_y'] + tbtn['button_scale_y'] - + 93 - 90 + prize_y_offs)) + ba.textwidget(edit=tbtn['prize_value_3_text'], + text=pv3, + scale=prize_value_scale_large if ticket_char + not in pv3 else prize_value_scale_small, + position=(tbtn['button_x'] + 380 + x_offs, + tbtn['button_y'] + tbtn['button_scale_y'] - + 93 - 90 + prize_y_offs)) + + leader_name = '-' + leader_score: Union[str, ba.Lstr] = '-' + if entry['scores']: + score = tbtn['leader'] = copy.deepcopy(entry['scores'][0]) + leader_name = score[1] + leader_score = (ba.timestring( + score[0] * 10, + centi=True, + timeformat=ba.TimeFormat.MILLISECONDS, + suppress_format_warning=True) if entry['scoreType'] + == 'time' else str(score[0])) + else: + tbtn['leader'] = None + + ba.textwidget(edit=tbtn['current_leader_name_text'], + text=ba.Lstr(value=leader_name)) + self._tournament_leader_score_type = (entry['scoreType']) + ba.textwidget(edit=tbtn['current_leader_score_text'], + text=leader_score) + ba.buttonwidget(edit=tbtn['more_scores_button'], + label=ba.Lstr(resource=self._r + '.seeMoreText')) + out_of_time_text: Union[str, ba.Lstr] = ( + '-' if 'totalTime' not in entry else ba.Lstr( + resource=self._r + '.ofTotalTimeText', + subs=[('${TOTAL}', + ba.timestring(entry['totalTime'], + centi=False, + suppress_format_warning=True))])) + ba.textwidget(edit=tbtn['time_remaining_out_of_text'], + text=out_of_time_text) + + tbtn['time_remaining'] = entry['timeRemaining'] + tbtn['has_time_remaining'] = entry is not None + tbtn['tournament_id'] = entry['tournamentID'] + tbtn['required_league'] = (None if 'requiredLeague' not in entry + else entry['requiredLeague']) + + game = ba.app.accounts.tournament_info[ + tbtn['tournament_id']]['game'] + + if game is None: + ba.textwidget(edit=tbtn['button_text'], text='-') + ba.imagewidget(edit=tbtn['image'], + texture=ba.gettexture('black'), + opacity=0.2) + else: + campaignname, levelname = game.split(':') + campaign = getcampaign(campaignname) + max_players = ba.app.accounts.tournament_info[ + tbtn['tournament_id']]['maxPlayers'] + txt = ba.Lstr( + value='${A} ${B}', + subs=[('${A}', campaign.getlevel(levelname).displayname), + ('${B}', + ba.Lstr(resource='playerCountAbbreviatedText', + subs=[('${COUNT}', str(max_players))]))]) + ba.textwidget(edit=tbtn['button_text'], text=txt) + ba.imagewidget( + edit=tbtn['image'], + texture=campaign.getlevel(levelname).get_preview_texture(), + opacity=1.0 if enabled else 0.5) + + fee = entry['fee'] + + if fee is None: + fee_var = None + elif fee == 4: + fee_var = 'price.tournament_entry_4' + elif fee == 3: + fee_var = 'price.tournament_entry_3' + elif fee == 2: + fee_var = 'price.tournament_entry_2' + elif fee == 1: + fee_var = 'price.tournament_entry_1' + else: + if fee != 0: + print('Unknown fee value:', fee) + fee_var = 'price.tournament_entry_0' + + tbtn['allow_ads'] = allow_ads = entry['allowAds'] + + final_fee: Optional[int] = (None if fee_var is None else + _ba.get_account_misc_read_val( + fee_var, '?')) + + final_fee_str: Union[str, ba.Lstr] + if fee_var is None: + final_fee_str = '' + else: + if final_fee == 0: + final_fee_str = ba.Lstr( + resource='getTicketsWindow.freeText') + else: + final_fee_str = ( + ba.charstr(ba.SpecialChar.TICKET_BACKING) + + str(final_fee)) + + ad_tries_remaining = ba.app.accounts.tournament_info[ + tbtn['tournament_id']]['adTriesRemaining'] + free_tries_remaining = ba.app.accounts.tournament_info[ + tbtn['tournament_id']]['freeTriesRemaining'] + + # Now, if this fee allows ads and we support video ads, show + # the 'or ad' version. + if allow_ads and _ba.has_video_ads(): + ads_enabled = _ba.have_incentivized_ad() + ba.imagewidget(edit=tbtn['entry_fee_ad_image'], + opacity=1.0 if ads_enabled else 0.25) + or_text = ba.Lstr(resource='orText', + subs=[('${A}', ''), + ('${B}', '')]).evaluate().strip() + ba.textwidget(edit=tbtn['entry_fee_text_or'], text=or_text) + ba.textwidget( + edit=tbtn['entry_fee_text_top'], + position=(tbtn['button_x'] + 360, + tbtn['button_y'] + tbtn['button_scale_y'] - 60), + scale=1.3, + text=final_fee_str) + + # Possibly show number of ad-plays remaining. + ba.textwidget( + edit=tbtn['entry_fee_text_remaining'], + position=(tbtn['button_x'] + 360, + tbtn['button_y'] + tbtn['button_scale_y'] - 146), + text='' if ad_tries_remaining in [None, 0] else + ('' + str(ad_tries_remaining)), + color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2)) + else: + ba.imagewidget(edit=tbtn['entry_fee_ad_image'], opacity=0.0) + ba.textwidget(edit=tbtn['entry_fee_text_or'], text='') + ba.textwidget( + edit=tbtn['entry_fee_text_top'], + position=(tbtn['button_x'] + 360, + tbtn['button_y'] + tbtn['button_scale_y'] - 80), + scale=1.3, + text=final_fee_str) + + # Possibly show number of free-plays remaining. + ba.textwidget( + edit=tbtn['entry_fee_text_remaining'], + position=(tbtn['button_x'] + 360, + tbtn['button_y'] + tbtn['button_scale_y'] - 100), + text=('' if (free_tries_remaining in [None, 0] + or final_fee != 0) else + ('' + str(free_tries_remaining))), + color=(0.6, 0.6, 0.6, 1)) + + def _on_tournament_query_response(self, data: Optional[Dict[str, + Any]]) -> None: + accounts = ba.app.accounts + if data is not None: + tournament_data = data['t'] # This used to be the whole payload. + self._last_tournament_query_response_time = ba.time( + ba.TimeType.REAL) + else: + tournament_data = None + + # Keep our cached tourney info up to date. + if data is not None: + self._tourney_data_up_to_date = True + accounts.cache_tournament_info(tournament_data) + + # Also cache the current tourney list/order for this account. + accounts.account_tournament_list = (_ba.get_account_state_num(), [ + e['tournamentID'] for e in tournament_data + ]) + + self._doing_tournament_query = False + self._update_for_data(tournament_data) + + def _set_campaign_difficulty(self, difficulty: str) -> None: + # pylint: disable=cyclic-import + from bastd.ui.purchase import PurchaseWindow + if difficulty != self._campaign_difficulty: + if difficulty == 'hard' and not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + ba.playsound(ba.getsound('gunCocking')) + if difficulty not in ('easy', 'hard'): + print('ERROR: invalid campaign difficulty:', difficulty) + difficulty = 'easy' + self._campaign_difficulty = difficulty + _ba.add_transaction({ + 'type': 'SET_MISC_VAL', + 'name': 'campaignDifficulty', + 'value': difficulty + }) + self._refresh_campaign_row() + else: + ba.playsound(ba.getsound('click01')) + + def _refresh_campaign_row(self) -> None: + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from ba.internal import getcampaign + from bastd.ui.coop.gamebutton import GameButton + parent_widget = self._campaign_sub_container + + # Clear out anything in the parent widget already. + for child in parent_widget.get_children(): + child.delete() + + next_widget_down = self._tournament_info_button + + h = 0 + v2 = -2 + sel_color = (0.75, 0.85, 0.5) + sel_color_hard = (0.4, 0.7, 0.2) + un_sel_color = (0.5, 0.5, 0.5) + sel_textcolor = (2, 2, 0.8) + un_sel_textcolor = (0.6, 0.6, 0.6) + self._easy_button = ba.buttonwidget( + parent=parent_widget, + position=(h + 30, v2 + 105), + size=(120, 70), + label=ba.Lstr(resource='difficultyEasyText'), + button_type='square', + autoselect=True, + enable_sound=False, + on_activate_call=ba.Call(self._set_campaign_difficulty, 'easy'), + on_select_call=ba.Call(self.sel_change, 'campaign', 'easyButton'), + color=sel_color + if self._campaign_difficulty == 'easy' else un_sel_color, + textcolor=sel_textcolor + if self._campaign_difficulty == 'easy' else un_sel_textcolor) + ba.widget(edit=self._easy_button, show_buffer_left=100) + if self._selected_campaign_level == 'easyButton': + ba.containerwidget(edit=parent_widget, + selected_child=self._easy_button, + visible_child=self._easy_button) + lock_tex = ba.gettexture('lock') + + self._hard_button = ba.buttonwidget( + parent=parent_widget, + position=(h + 30, v2 + 32), + size=(120, 70), + label=ba.Lstr(resource='difficultyHardText'), + button_type='square', + autoselect=True, + enable_sound=False, + on_activate_call=ba.Call(self._set_campaign_difficulty, 'hard'), + on_select_call=ba.Call(self.sel_change, 'campaign', 'hardButton'), + color=sel_color_hard + if self._campaign_difficulty == 'hard' else un_sel_color, + textcolor=sel_textcolor + if self._campaign_difficulty == 'hard' else un_sel_textcolor) + self._hard_button_lock_image = ba.imagewidget( + parent=parent_widget, + size=(30, 30), + draw_controller=self._hard_button, + position=(h + 30 - 10, v2 + 32 + 70 - 35), + texture=lock_tex) + self._update_hard_mode_lock_image() + ba.widget(edit=self._hard_button, show_buffer_left=100) + if self._selected_campaign_level == 'hardButton': + ba.containerwidget(edit=parent_widget, + selected_child=self._hard_button, + visible_child=self._hard_button) + + ba.widget(edit=self._hard_button, down_widget=next_widget_down) + h_spacing = 200 + campaign_buttons = [] + if self._campaign_difficulty == 'easy': + campaignname = 'Easy' + else: + campaignname = 'Default' + items = [ + campaignname + ':Onslaught Training', + campaignname + ':Rookie Onslaught', + campaignname + ':Rookie Football', campaignname + ':Pro Onslaught', + campaignname + ':Pro Football', campaignname + ':Pro Runaround', + campaignname + ':Uber Onslaught', campaignname + ':Uber Football', + campaignname + ':Uber Runaround' + ] + items += [campaignname + ':The Last Stand'] + if self._selected_campaign_level is None: + self._selected_campaign_level = items[0] + h = 150 + for i in items: + is_last_sel = (i == self._selected_campaign_level) + campaign_buttons.append( + GameButton(self, parent_widget, i, h, v2, is_last_sel, + 'campaign').get_button()) + h += h_spacing + + ba.widget(edit=campaign_buttons[0], left_widget=self._easy_button) + + if self._back_button is not None: + ba.widget(edit=self._easy_button, up_widget=self._back_button) + for btn in campaign_buttons: + ba.widget(edit=btn, + up_widget=self._back_button, + down_widget=next_widget_down) + + # Update our existing percent-complete text. + campaign = getcampaign(campaignname) + levels = campaign.levels + levels_complete = sum((1 if l.complete else 0) for l in levels) + + # Last level cant be completed; hence the -1. + progress = min(1.0, float(levels_complete) / (len(levels) - 1)) + p_str = str(int(progress * 100.0)) + '%' + + self._campaign_percent_text = ba.textwidget( + edit=self._campaign_percent_text, + text=ba.Lstr(value='${C} (${P})', + subs=[('${C}', + ba.Lstr(resource=self._r + '.campaignText')), + ('${P}', p_str)])) + + def _on_tournament_info_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.confirm import ConfirmWindow + txt = ba.Lstr(resource=self._r + '.tournamentInfoText') + ConfirmWindow(txt, + cancel_button=False, + width=550, + height=260, + origin_widget=self._tournament_info_button) + + def _refresh(self) -> None: + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from bastd.ui.coop.gamebutton import GameButton + + # (Re)create the sub-container if need be. + if self._subcontainer is not None: + self._subcontainer.delete() + + tourney_row_height = 200 + self._subcontainerheight = ( + 620 + self._tournament_button_count * tourney_row_height) + + self._subcontainer = ba.containerwidget( + parent=self._scrollwidget, + size=(self._subcontainerwidth, self._subcontainerheight), + background=False, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + if self._back_button is not None: + ba.containerwidget(edit=self._root_widget, + cancel_button=self._back_button) + + w_parent = self._subcontainer + h_base = 6 + + v = self._subcontainerheight - 73 + + self._campaign_percent_text = ba.textwidget( + parent=w_parent, + position=(h_base + 27, v + 30), + size=(0, 0), + text='', + h_align='left', + v_align='center', + color=ba.app.ui.title_color, + scale=1.1) + + row_v_show_buffer = 100 + v -= 198 + + h_scroll = ba.hscrollwidget( + parent=w_parent, + size=(self._scroll_width - 10, 205), + position=(-5, v), + simple_culling_h=70, + highlight=False, + border_opacity=0.0, + color=(0.45, 0.4, 0.5), + on_select_call=lambda: self._on_row_selected('campaign')) + self._campaign_h_scroll = h_scroll + ba.widget(edit=h_scroll, + show_buffer_top=row_v_show_buffer, + show_buffer_bottom=row_v_show_buffer, + autoselect=True) + if self._selected_row == 'campaign': + ba.containerwidget(edit=w_parent, + selected_child=h_scroll, + visible_child=h_scroll) + ba.containerwidget(edit=h_scroll, claims_left_right=True) + self._campaign_sub_container = ba.containerwidget(parent=h_scroll, + size=(180 + 200 * 10, + 200), + background=False) + + # Tournaments + + self._tournament_buttons: List[Dict[str, Any]] = [] + + v -= 53 + # FIXME shouldn't use hard-coded strings here. + txt = ba.Lstr(resource='tournamentsText', + fallback_resource='tournamentText').evaluate() + t_width = _ba.get_string_width(txt, suppress_warning=True) + ba.textwidget(parent=w_parent, + position=(h_base + 27, v + 30), + size=(0, 0), + text=txt, + h_align='left', + v_align='center', + color=ba.app.ui.title_color, + scale=1.1) + self._tournament_info_button = ba.buttonwidget( + parent=w_parent, + label='?', + size=(20, 20), + text_scale=0.6, + position=(h_base + 27 + t_width * 1.1 + 15, v + 18), + button_type='square', + color=(0.6, 0.5, 0.65), + textcolor=(0.7, 0.6, 0.75), + autoselect=True, + up_widget=self._campaign_h_scroll, + on_activate_call=self._on_tournament_info_press) + ba.widget(edit=self._tournament_info_button, + left_widget=self._tournament_info_button, + right_widget=self._tournament_info_button) + + # Say 'unavailable' if there are zero tournaments, and if we're not + # signed in add that as well (that's probably why we see + # no tournaments). + if self._tournament_button_count == 0: + unavailable_text = ba.Lstr(resource='unavailableText') + if _ba.get_account_state() != 'signed_in': + unavailable_text = ba.Lstr( + value='${A} (${B})', + subs=[('${A}', unavailable_text), + ('${B}', ba.Lstr(resource='notSignedInText'))]) + ba.textwidget(parent=w_parent, + position=(h_base + 47, v), + size=(0, 0), + text=unavailable_text, + h_align='left', + v_align='center', + color=ba.app.ui.title_color, + scale=0.9) + v -= 40 + v -= 198 + + tournament_h_scroll = None + if self._tournament_button_count > 0: + for i in range(self._tournament_button_count): + tournament_h_scroll = h_scroll = ba.hscrollwidget( + parent=w_parent, + size=(self._scroll_width - 10, 205), + position=(-5, v), + highlight=False, + border_opacity=0.0, + color=(0.45, 0.4, 0.5), + on_select_call=ba.Call(self._on_row_selected, + 'tournament' + str(i + 1))) + ba.widget(edit=h_scroll, + show_buffer_top=row_v_show_buffer, + show_buffer_bottom=row_v_show_buffer, + autoselect=True) + if self._selected_row == 'tournament' + str(i + 1): + ba.containerwidget(edit=w_parent, + selected_child=h_scroll, + visible_child=h_scroll) + ba.containerwidget(edit=h_scroll, claims_left_right=True) + sc2 = ba.containerwidget(parent=h_scroll, + size=(self._scroll_width - 24, 200), + background=False) + h = 0 + v2 = -2 + is_last_sel = True + self._tournament_buttons.append( + self._tournament_button(sc2, h, v2, is_last_sel)) + v -= 200 + + # Custom Games. + v -= 50 + ba.textwidget(parent=w_parent, + position=(h_base + 27, v + 30 + 198), + size=(0, 0), + text=ba.Lstr( + resource='practiceText', + fallback_resource='coopSelectWindow.customText'), + h_align='left', + v_align='center', + color=ba.app.ui.title_color, + scale=1.1) + + items = [ + 'Challenges:Infinite Onslaught', + 'Challenges:Infinite Runaround', + 'Challenges:Ninja Fight', + 'Challenges:Pro Ninja Fight', + 'Challenges:Meteor Shower', + 'Challenges:Target Practice B', + 'Challenges:Target Practice', + ] + + # Show easter-egg-hunt either if its easter or we own it. + if _ba.get_account_misc_read_val( + 'easter', False) or _ba.get_purchased('games.easter_egg_hunt'): + items = [ + 'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt' + ] + items + + # add all custom user levels here.. + # items += [ + # 'User:' + l.getname() + # for l in getcampaign('User').getlevels() + # ] + + self._custom_h_scroll = custom_h_scroll = h_scroll = ba.hscrollwidget( + parent=w_parent, + size=(self._scroll_width - 10, 205), + position=(-5, v), + highlight=False, + border_opacity=0.0, + color=(0.45, 0.4, 0.5), + on_select_call=ba.Call(self._on_row_selected, 'custom')) + ba.widget(edit=h_scroll, + show_buffer_top=row_v_show_buffer, + show_buffer_bottom=1.5 * row_v_show_buffer, + autoselect=True) + if self._selected_row == 'custom': + ba.containerwidget(edit=w_parent, + selected_child=h_scroll, + visible_child=h_scroll) + ba.containerwidget(edit=h_scroll, claims_left_right=True) + sc2 = ba.containerwidget(parent=h_scroll, + size=(max(self._scroll_width - 24, + 30 + 200 * len(items)), 200), + background=False) + h_spacing = 200 + self._custom_buttons: List[GameButton] = [] + h = 0 + v2 = -2 + for item in items: + is_last_sel = (item == self._selected_custom_level) + self._custom_buttons.append( + GameButton(self, sc2, item, h, v2, is_last_sel, 'custom')) + h += h_spacing + + # We can't fill in our campaign row until tourney buttons are in place. + # (for wiring up) + self._refresh_campaign_row() + + for i in range(len(self._tournament_buttons)): + ba.widget( + edit=self._tournament_buttons[i]['button'], + up_widget=self._tournament_info_button + if i == 0 else self._tournament_buttons[i - 1]['button'], + down_widget=self._tournament_buttons[(i + 1)]['button'] + if i + 1 < len(self._tournament_buttons) else custom_h_scroll) + ba.widget( + edit=self._tournament_buttons[i]['more_scores_button'], + down_widget=self._tournament_buttons[( + i + 1)]['current_leader_name_text'] + if i + 1 < len(self._tournament_buttons) else custom_h_scroll) + ba.widget( + edit=self._tournament_buttons[i]['current_leader_name_text'], + up_widget=self._tournament_info_button if i == 0 else + self._tournament_buttons[i - 1]['more_scores_button']) + + for btn in self._custom_buttons: + try: + ba.widget( + edit=btn.get_button(), + up_widget=tournament_h_scroll if self._tournament_buttons + else self._tournament_info_button) + except Exception: + ba.print_exception('Error wiring up custom buttons.') + + if self._back_button is not None: + ba.buttonwidget(edit=self._back_button, + on_activate_call=self._back) + else: + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._back) + + # There's probably several 'onSelected' callbacks pushed onto the + # event queue.. we need to push ours too so we're enabled *after* them. + ba.pushcall(self._enable_selectable_callback) + + def _on_row_selected(self, row: str) -> None: + if self._do_selection_callbacks: + if self._selected_row != row: + self._selected_row = row + + def _enable_selectable_callback(self) -> None: + self._do_selection_callbacks = True + + def _tournament_button(self, parent: ba.Widget, x: float, y: float, + select: bool) -> Dict[str, Any]: + sclx = 300 + scly = 195.0 + data: Dict[str, Any] = { + 'tournament_id': None, + 'time_remaining': 0, + 'has_time_remaining': False, + 'leader': None + } + data['button'] = btn = ba.buttonwidget( + parent=parent, + position=(x + 23, y + 4), + size=(sclx, scly), + label='', + button_type='square', + autoselect=True, + on_activate_call=lambda: self.run(None, tournament_button=data)) + ba.widget(edit=btn, + show_buffer_bottom=50, + show_buffer_top=50, + show_buffer_left=400, + show_buffer_right=200) + if select: + ba.containerwidget(edit=parent, + selected_child=btn, + visible_child=btn) + image_width = sclx * 0.85 * 0.75 + + data['image'] = ba.imagewidget( + parent=parent, + draw_controller=btn, + position=(x + 21 + sclx * 0.5 - image_width * 0.5, y + scly - 150), + size=(image_width, image_width * 0.5), + model_transparent=self.lsbt, + model_opaque=self.lsbo, + texture=ba.gettexture('black'), + opacity=0.2, + mask_texture=ba.gettexture('mapPreviewMask')) + + data['lock_image'] = ba.imagewidget( + parent=parent, + draw_controller=btn, + position=(x + 21 + sclx * 0.5 - image_width * 0.25, + y + scly - 150), + size=(image_width * 0.5, image_width * 0.5), + texture=ba.gettexture('lock'), + opacity=0.0) + + data['button_text'] = ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 20 + sclx * 0.5, + y + scly - 35), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=sclx * 0.76, + scale=0.85, + color=(0.8, 1.0, 0.8, 1.0)) + + header_color = (0.43, 0.4, 0.5, 1) + value_color = (0.6, 0.6, 0.6, 1) + + x_offs = 0 + ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 360, y + scly - 20), + size=(0, 0), + h_align='center', + text=ba.Lstr(resource=self._r + '.entryFeeText'), + v_align='center', + maxwidth=100, + scale=0.9, + color=header_color, + flatness=1.0) + + data['entry_fee_text_top'] = ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 360, + y + scly - 60), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=60, + scale=1.3, + color=value_color, + flatness=1.0) + data['entry_fee_text_or'] = ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 360, + y + scly - 90), + size=(0, 0), + h_align='center', + text='', + v_align='center', + maxwidth=60, + scale=0.5, + color=value_color, + flatness=1.0) + data['entry_fee_text_remaining'] = ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 360, y + + scly - 90), + size=(0, 0), + h_align='center', + text='', + v_align='center', + maxwidth=60, + scale=0.5, + color=value_color, + flatness=1.0) + + data['entry_fee_ad_image'] = ba.imagewidget( + parent=parent, + size=(40, 40), + draw_controller=btn, + position=(x + 360 - 20, y + scly - 140), + opacity=0.0, + texture=ba.gettexture('tv')) + + x_offs += 50 + + ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 447 + x_offs, y + scly - 20), + size=(0, 0), + h_align='center', + text=ba.Lstr(resource=self._r + '.prizesText'), + v_align='center', + maxwidth=130, + scale=0.9, + color=header_color, + flatness=1.0) + + data['button_x'] = x + data['button_y'] = y + data['button_scale_y'] = scly + + xo2 = 0 + prize_value_scale = 1.5 + + data['prize_range_1_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 355 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=50, + text='-', + scale=0.8, + color=header_color, + flatness=1.0) + data['prize_value_1_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 380 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='left', + text='-', + v_align='center', + maxwidth=100, + scale=prize_value_scale, + color=value_color, + flatness=1.0) + + data['prize_range_2_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 355 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=50, + scale=0.8, + color=header_color, + flatness=1.0) + data['prize_value_2_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 380 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='left', + text='', + v_align='center', + maxwidth=100, + scale=prize_value_scale, + color=value_color, + flatness=1.0) + + data['prize_range_3_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 355 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=50, + scale=0.8, + color=header_color, + flatness=1.0) + data['prize_value_3_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 380 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='left', + text='', + v_align='center', + maxwidth=100, + scale=prize_value_scale, + color=value_color, + flatness=1.0) + + ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 620 + x_offs, y + scly - 20), + size=(0, 0), + h_align='center', + text=ba.Lstr(resource=self._r + '.currentBestText'), + v_align='center', + maxwidth=180, + scale=0.9, + color=header_color, + flatness=1.0) + data['current_leader_name_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 620 + x_offs - (170 / 1.4) * 0.5, + y + scly - 60 - 40 * 0.5), + selectable=True, + click_activate=True, + autoselect=True, + on_activate_call=lambda: self._show_leader(tournament_button=data), + size=(170 / 1.4, 40), + h_align='center', + text='-', + v_align='center', + maxwidth=170, + scale=1.4, + color=value_color, + flatness=1.0) + data['current_leader_score_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 620 + x_offs, y + scly - 113 + 10), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=170, + scale=1.8, + color=value_color, + flatness=1.0) + + data['more_scores_button'] = ba.buttonwidget( + parent=parent, + position=(x + 620 + x_offs - 60, y + scly - 50 - 125), + color=(0.5, 0.5, 0.6), + textcolor=(0.7, 0.7, 0.8), + label='-', + size=(120, 40), + autoselect=True, + up_widget=data['current_leader_name_text'], + text_scale=0.6, + on_activate_call=lambda: self._show_scores(tournament_button=data)) + ba.widget(edit=data['current_leader_name_text'], + down_widget=data['more_scores_button']) + + ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 820 + x_offs, y + scly - 20), + size=(0, 0), + h_align='center', + text=ba.Lstr(resource=self._r + '.timeRemainingText'), + v_align='center', + maxwidth=180, + scale=0.9, + color=header_color, + flatness=1.0) + data['time_remaining_value_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 820 + x_offs, y + scly - 68), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=180, + scale=2.0, + color=value_color, + flatness=1.0) + data['time_remaining_out_of_text'] = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 820 + x_offs, y + scly - 110), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=120, + scale=0.72, + color=(0.4, 0.4, 0.5), + flatness=1.0) + return data + + def _switch_to_league_rankings(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.league.rankwindow import LeagueRankWindow + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + assert self._league_rank_button is not None + ba.app.ui.set_main_menu_window( + LeagueRankWindow(origin_widget=self._league_rank_button.get_button( + )).get_root_widget()) + + def _switch_to_score( + self, + show_tab: Optional[ + StoreBrowserWindow.TabID] = StoreBrowserWindow.TabID.EXTRAS + ) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account import show_sign_in_prompt + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + assert self._store_button is not None + ba.app.ui.set_main_menu_window( + StoreBrowserWindow( + origin_widget=self._store_button.get_button(), + show_tab=show_tab, + back_location='CoopBrowserWindow').get_root_widget()) + + def _show_leader(self, tournament_button: Dict[str, Any]) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account.viewer import AccountViewerWindow + tournament_id = tournament_button['tournament_id'] + + # FIXME: This assumes a single player entry in leader; should expand + # this to work with multiple. + if tournament_id is None or tournament_button['leader'] is None or len( + tournament_button['leader'][2]) != 1: + ba.playsound(ba.getsound('error')) + return + ba.playsound(ba.getsound('swish')) + AccountViewerWindow( + account_id=tournament_button['leader'][2][0].get('a', None), + profile_id=tournament_button['leader'][2][0].get('p', None), + position=tournament_button['current_leader_name_text']. + get_screen_space_center()) + + def _show_scores(self, tournament_button: Dict[str, Any]) -> None: + # pylint: disable=cyclic-import + from bastd.ui.tournamentscores import TournamentScoresWindow + tournament_id = tournament_button['tournament_id'] + if tournament_id is None: + ba.playsound(ba.getsound('error')) + return + + TournamentScoresWindow( + tournament_id=tournament_id, + position=tournament_button['more_scores_button']. + get_screen_space_center()) + + def is_tourney_data_up_to_date(self) -> bool: + """Return whether our tourney data is up to date.""" + return self._tourney_data_up_to_date + + def run(self, + game: Optional[str], + tournament_button: Dict[str, Any] = None) -> None: + """Run the provided game.""" + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-return-statements + # pylint: disable=cyclic-import + from bastd.ui.confirm import ConfirmWindow + from bastd.ui.tournamententry import TournamentEntryWindow + from bastd.ui.purchase import PurchaseWindow + from bastd.ui.account import show_sign_in_prompt + args: Dict[str, Any] = {} + + # Do a bit of pre-flight for tournament options. + if tournament_button is not None: + + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + + if not self._tourney_data_up_to_date: + ba.screenmessage( + ba.Lstr(resource='tournamentCheckingStateText'), + color=(1, 1, 0)) + ba.playsound(ba.getsound('error')) + return + + if tournament_button['tournament_id'] is None: + ba.screenmessage( + ba.Lstr(resource='internal.unavailableNoConnectionText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + if tournament_button['required_league'] is not None: + ba.screenmessage(ba.Lstr( + resource='league.tournamentLeagueText', + subs=[ + ('${NAME}', + ba.Lstr( + translate=('leagueNames', + tournament_button['required_league']))) + ]), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + if tournament_button['time_remaining'] <= 0: + ba.screenmessage(ba.Lstr(resource='tournamentEndedText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + # Game is whatever the tournament tells us it is. + game = ba.app.accounts.tournament_info[ + tournament_button['tournament_id']]['game'] + + if tournament_button is None and game == 'Easy:The Last Stand': + ConfirmWindow(ba.Lstr(resource='difficultyHardUnlockOnlyText', + fallback_resource='difficultyHardOnlyText'), + cancel_button=False, + width=460, + height=130) + return + + # Infinite onslaught/runaround require pro; bring up a store link if + # need be. + if tournament_button is None and game in ( + 'Challenges:Infinite Runaround', + 'Challenges:Infinite Onslaught' + ) and not ba.app.accounts.have_pro(): + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + else: + PurchaseWindow(items=['pro']) + return + + required_purchase: Optional[str] + if game in ['Challenges:Meteor Shower']: + required_purchase = 'games.meteor_shower' + elif game in [ + 'Challenges:Target Practice', 'Challenges:Target Practice B' + ]: + required_purchase = 'games.target_practice' + elif game in ['Challenges:Ninja Fight']: + required_purchase = 'games.ninja_fight' + elif game in ['Challenges:Pro Ninja Fight']: + required_purchase = 'games.ninja_fight' + elif game in [ + 'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt' + ]: + required_purchase = 'games.easter_egg_hunt' + else: + required_purchase = None + + if (tournament_button is None and required_purchase is not None + and not _ba.get_purchased(required_purchase)): + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + else: + PurchaseWindow(items=[required_purchase]) + return + + self._save_state() + + # For tournaments, we pop up the entry window. + if tournament_button is not None: + TournamentEntryWindow( + tournament_id=tournament_button['tournament_id'], + position=tournament_button['button'].get_screen_space_center()) + else: + # Otherwise just dive right in. + assert game is not None + if ba.app.launch_coop_game(game, args=args): + ba.containerwidget(edit=self._root_widget, + transition='out_left') + + def _back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.play import PlayWindow + + # If something is selected, store it. + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + PlayWindow(transition='in_left').get_root_widget()) + + def _restore_state(self) -> None: + try: + sel_name = ba.app.ui.window_states.get(type(self), + {}).get('sel_name') + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'Scroll': + sel = self._scrollwidget + elif sel_name == 'PowerRanking': + sel = self._league_rank_button_widget + elif sel_name == 'Store': + sel = self._store_button_widget + else: + sel = self._scrollwidget + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') + + def _save_state(self) -> None: + cfg = ba.app.config + try: + sel = self._root_widget.get_selected_child() + if sel == self._back_button: + sel_name = 'Back' + elif sel == self._store_button_widget: + sel_name = 'Store' + elif sel == self._league_rank_button_widget: + sel_name = 'PowerRanking' + elif sel == self._scrollwidget: + sel_name = 'Scroll' + else: + raise ValueError('unrecognized selection') + ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + cfg['Selected Coop Row'] = self._selected_row + cfg['Selected Coop Custom Level'] = self._selected_custom_level + cfg['Selected Coop Challenge Level'] = self._selected_challenge_level + cfg['Selected Coop Campaign Level'] = self._selected_campaign_level + cfg.commit() + + def sel_change(self, row: str, game: str) -> None: + """(internal)""" + if self._do_selection_callbacks: + if row == 'custom': + self._selected_custom_level = game + if row == 'challenges': + self._selected_challenge_level = game + elif row == 'campaign': + self._selected_campaign_level = game diff --git a/dist/ba_data/python/bastd/ui/coop/gamebutton.py b/dist/ba_data/python/bastd/ui/coop/gamebutton.py new file mode 100644 index 0000000..8aee88b --- /dev/null +++ b/dist/ba_data/python/bastd/ui/coop/gamebutton.py @@ -0,0 +1,242 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines button for co-op games.""" + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Optional, List, Tuple + from bastd.ui.coop.browser import CoopBrowserWindow + + +class GameButton: + """Button for entering co-op games.""" + + def __init__(self, window: CoopBrowserWindow, parent: ba.Widget, game: str, + x: float, y: float, select: bool, row: str): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + from ba.internal import getcampaign + self._game = game + sclx = 195.0 + scly = 195.0 + + campaignname, levelname = game.split(':') + + # Hack: The Last Stand doesn't actually exist in the easy + # tourney. We just want it for display purposes. Map it to + # the hard-mode version. + if game == 'Easy:The Last Stand': + campaignname = 'Default' + + rating: Optional[float] + campaign = getcampaign(campaignname) + rating = campaign.getlevel(levelname).rating + + if game == 'Easy:The Last Stand': + rating = None + + if rating is None or rating == 0.0: + stars = 0 + elif rating >= 9.5: + stars = 3 + elif rating >= 7.5: + stars = 2 + else: + stars = 1 + + self._button = btn = ba.buttonwidget( + parent=parent, + position=(x + 23, y + 4), + size=(sclx, scly), + label='', + on_activate_call=ba.Call(window.run, game), + button_type='square', + autoselect=True, + on_select_call=ba.Call(window.sel_change, row, game)) + ba.widget(edit=btn, + show_buffer_bottom=50, + show_buffer_top=50, + show_buffer_left=400, + show_buffer_right=200) + if select: + ba.containerwidget(edit=parent, + selected_child=btn, + visible_child=btn) + image_width = sclx * 0.85 * 0.75 + self._preview_widget = ba.imagewidget( + parent=parent, + draw_controller=btn, + position=(x + 21 + sclx * 0.5 - image_width * 0.5, y + scly - 104), + size=(image_width, image_width * 0.5), + model_transparent=window.lsbt, + model_opaque=window.lsbo, + texture=campaign.getlevel(levelname).get_preview_texture(), + mask_texture=ba.gettexture('mapPreviewMask')) + + translated = campaign.getlevel(levelname).displayname + self._achievements = ba.app.ach.achievements_for_coop_level(game) + + self._name_widget = ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 20 + sclx * 0.5, + y + scly - 27), + size=(0, 0), + h_align='center', + text=translated, + v_align='center', + maxwidth=sclx * 0.76, + scale=0.85) + xscl = x + (67 if self._achievements else 50) + yscl = y + scly - (137 if self._achievements else 157) + + starscale = 35.0 if self._achievements else 45.0 + + self._star_widgets: List[ba.Widget] = [] + for _i in range(stars): + imw = ba.imagewidget(parent=parent, + draw_controller=btn, + position=(xscl, yscl), + size=(starscale, starscale), + texture=window.star_tex) + self._star_widgets.append(imw) + xscl += starscale + for _i in range(3 - stars): + ba.imagewidget(parent=parent, + draw_controller=btn, + position=(xscl, yscl), + size=(starscale, starscale), + color=(0, 0, 0), + texture=window.star_tex, + opacity=0.3) + xscl += starscale + + xach = x + 69 + yach = y + scly - 168 + a_scale = 30.0 + self._achievement_widgets: List[Tuple[ba.Widget, ba.Widget]] = [] + for ach in self._achievements: + a_complete = ach.complete + imw = ba.imagewidget( + parent=parent, + draw_controller=btn, + position=(xach, yach), + size=(a_scale, a_scale), + color=tuple(ach.get_icon_color(a_complete)[:3]) + if a_complete else (1.2, 1.2, 1.2), + texture=ach.get_icon_texture(a_complete)) + imw2 = ba.imagewidget(parent=parent, + draw_controller=btn, + position=(xach, yach), + size=(a_scale, a_scale), + color=(2, 1.4, 0.4), + texture=window.a_outline_tex, + model_transparent=window.a_outline_model) + self._achievement_widgets.append((imw, imw2)) + # if a_complete: + xach += a_scale * 1.2 + + # if not unlocked: + self._lock_widget = ba.imagewidget(parent=parent, + draw_controller=btn, + position=(x - 8 + sclx * 0.5, + y + scly * 0.5 - 20), + size=(60, 60), + opacity=0.0, + texture=ba.gettexture('lock')) + + # give a quasi-random update increment to spread the load.. + self._update_timer = ba.Timer(0.001 * (900 + random.randrange(200)), + ba.WeakCall(self._update), + repeat=True, + timetype=ba.TimeType.REAL) + self._update() + + def get_button(self) -> ba.Widget: + """Return the underlying button ba.Widget.""" + return self._button + + def _update(self) -> None: + # pylint: disable=too-many-boolean-expressions + from ba.internal import getcampaign + + # In case we stick around after our UI... + if not self._button: + return + + game = self._game + campaignname, levelname = game.split(':') + + # Hack - The Last Stand doesn't actually exist in the + # easy tourney; we just want it for display purposes. Map it to + # the hard-mode version. + if game == 'Easy:The Last Stand': + campaignname = 'Default' + + campaign = getcampaign(campaignname) + + # If this campaign is sequential, make sure we've unlocked + # everything up to here. + unlocked = True + if campaign.sequential: + for level in campaign.levels: + if level.name == levelname: + break + if not level.complete: + unlocked = False + break + + # We never actually allow playing last-stand on easy mode. + if game == 'Easy:The Last Stand': + unlocked = False + + # Hard-code games we haven't unlocked. + if ((game in ('Challenges:Infinite Runaround', + 'Challenges:Infinite Onslaught') + and not ba.app.accounts.have_pro()) + or (game in ('Challenges:Meteor Shower', ) + and not _ba.get_purchased('games.meteor_shower')) + or (game in ('Challenges:Target Practice', + 'Challenges:Target Practice B') + and not _ba.get_purchased('games.target_practice')) + or (game in ('Challenges:Ninja Fight', ) + and not _ba.get_purchased('games.ninja_fight')) + or (game in ('Challenges:Pro Ninja Fight', ) + and not _ba.get_purchased('games.ninja_fight')) + or (game in ('Challenges:Easter Egg Hunt', + 'Challenges:Pro Easter Egg Hunt') + and not _ba.get_purchased('games.easter_egg_hunt'))): + unlocked = False + + # Let's tint levels a slightly different color when easy mode + # is selected. + unlocked_color = (0.85, 0.95, + 0.5) if game.startswith('Easy:') else (0.5, 0.7, 0.2) + + ba.buttonwidget(edit=self._button, + color=unlocked_color if unlocked else (0.5, 0.5, 0.5)) + + ba.imagewidget(edit=self._lock_widget, + opacity=0.0 if unlocked else 1.0) + ba.imagewidget(edit=self._preview_widget, + opacity=1.0 if unlocked else 0.3) + ba.textwidget(edit=self._name_widget, + color=(0.8, 1.0, 0.8, 1.0) if unlocked else + (0.7, 0.7, 0.7, 0.7)) + for widget in self._star_widgets: + ba.imagewidget(edit=widget, + opacity=1.0 if unlocked else 0.3, + color=(2.2, 1.2, 0.3) if unlocked else (1, 1, 1)) + for i, ach in enumerate(self._achievements): + a_complete = ach.complete + ba.imagewidget(edit=self._achievement_widgets[i][0], + opacity=1.0 if (a_complete and unlocked) else 0.3) + ba.imagewidget(edit=self._achievement_widgets[i][1], + opacity=(1.0 if (a_complete and unlocked) else + 0.2 if a_complete else 0.0)) diff --git a/dist/ba_data/python/bastd/ui/coop/level.py b/dist/ba_data/python/bastd/ui/coop/level.py new file mode 100644 index 0000000..c244469 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/coop/level.py @@ -0,0 +1,59 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Bits of utility functionality related to co-op levels.""" + +from __future__ import annotations + +import ba + + +class CoopLevelLockedWindow(ba.Window): + """Window showing that a level is locked.""" + + def __init__(self, name: ba.Lstr, dep_name: ba.Lstr): + width = 550.0 + height = 250.0 + lock_tex = ba.gettexture('lock') + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition='in_right', + scale=(1.7 if uiscale is ba.UIScale.SMALL else + 1.3 if uiscale is ba.UIScale.MEDIUM else 1.0))) + ba.textwidget(parent=self._root_widget, + position=(150 - 20, height * 0.63), + size=(0, 0), + h_align='left', + v_align='center', + text=ba.Lstr(resource='levelIsLockedText', + subs=[('${LEVEL}', name)]), + maxwidth=400, + color=(1, 0.8, 0.3, 1), + scale=1.1) + ba.textwidget(parent=self._root_widget, + position=(150 - 20, height * 0.48), + size=(0, 0), + h_align='left', + v_align='center', + text=ba.Lstr(resource='levelMustBeCompletedFirstText', + subs=[('${LEVEL}', dep_name)]), + maxwidth=400, + color=ba.app.ui.infotextcolor, + scale=0.8) + ba.imagewidget(parent=self._root_widget, + position=(56 - 20, height * 0.39), + size=(80, 80), + texture=lock_tex, + opacity=1.0) + btn = ba.buttonwidget(parent=self._root_widget, + position=((width - 140) / 2, 30), + size=(140, 50), + label=ba.Lstr(resource='okText'), + on_activate_call=self._ok) + ba.containerwidget(edit=self._root_widget, + selected_child=btn, + start_button=btn) + ba.playsound(ba.getsound('error')) + + def _ok(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_left') diff --git a/dist/ba_data/python/bastd/ui/creditslist.py b/dist/ba_data/python/bastd/ui/creditslist.py new file mode 100644 index 0000000..b5caebc --- /dev/null +++ b/dist/ba_data/python/bastd/ui/creditslist.py @@ -0,0 +1,269 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a window to display game credits.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Tuple, Optional, Sequence + + +class CreditsListWindow(ba.Window): + """Window for displaying game credits.""" + + def __init__(self, origin_widget: ba.Widget = None): + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + import json + ba.set_analytics_screen('Credits Window') + + # if they provided an origin-widget, scale up from that + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + transition = 'in_right' + + uiscale = ba.app.ui.uiscale + width = 870 if uiscale is ba.UIScale.SMALL else 670 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + height = 398 if uiscale is ba.UIScale.SMALL else 500 + + self._r = 'creditsWindow' + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(2.0 if uiscale is ba.UIScale.SMALL else + 1.3 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -8) if uiscale is ba.UIScale.SMALL else (0, 0))) + + if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._back) + else: + btn = ba.buttonwidget( + parent=self._root_widget, + position=(40 + x_inset, height - + (68 if uiscale is ba.UIScale.SMALL else 62)), + size=(140, 60), + scale=0.8, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._back, + autoselect=True) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.buttonwidget( + edit=btn, + button_type='backSmall', + position=(40 + x_inset, height - + (68 if uiscale is ba.UIScale.SMALL else 62) + 5), + size=(60, 48), + label=ba.charstr(ba.SpecialChar.BACK)) + + ba.textwidget(parent=self._root_widget, + position=(0, height - + (59 if uiscale is ba.UIScale.SMALL else 54)), + size=(width, 30), + text=ba.Lstr(resource=self._r + '.titleText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText'))]), + h_align='center', + color=ba.app.ui.title_color, + maxwidth=330, + v_align='center') + + scroll = ba.scrollwidget(parent=self._root_widget, + position=(40 + x_inset, 35), + size=(width - (80 + 2 * x_inset), + height - 100), + capture_arrows=True) + + if ba.app.ui.use_toolbars: + ba.widget(edit=scroll, + right_widget=_ba.get_special_widget('party_button')) + if uiscale is ba.UIScale.SMALL: + ba.widget(edit=scroll, + left_widget=_ba.get_special_widget('back_button')) + + def _format_names(names2: Sequence[str], inset: float) -> str: + sval = '' + # measure a series since there's overlaps and stuff.. + space_width = _ba.get_string_width(' ' * 10, + suppress_warning=True) / 10.0 + spacing = 330.0 + col1 = inset + col2 = col1 + spacing + col3 = col2 + spacing + line_width = 0.0 + nline = '' + for name in names2: + # move to the next column (or row) and print + if line_width > col3: + sval += nline + '\n' + nline = '' + line_width = 0 + + if line_width > col2: + target = col3 + elif line_width > col1: + target = col2 + else: + target = col1 + spacingstr = ' ' * int((target - line_width) / space_width) + nline += spacingstr + nline += name + line_width = _ba.get_string_width(nline, suppress_warning=True) + if nline != '': + sval += nline + '\n' + return sval + + sound_and_music = ba.Lstr(resource=self._r + + '.songCreditText').evaluate() + sound_and_music = sound_and_music.replace( + '${TITLE}', "'William Tell (Trumpet Entry)'") + sound_and_music = sound_and_music.replace( + '${PERFORMER}', 'The Apollo Symphony Orchestra') + sound_and_music = sound_and_music.replace( + '${PERFORMER}', 'The Apollo Symphony Orchestra') + sound_and_music = sound_and_music.replace('${COMPOSER}', + 'Gioacchino Rossini') + sound_and_music = sound_and_music.replace('${ARRANGER}', 'Chris Worth') + sound_and_music = sound_and_music.replace('${PUBLISHER}', 'BMI') + sound_and_music = sound_and_music.replace('${SOURCE}', + 'www.AudioSparx.com') + spc = ' ' + sound_and_music = spc + sound_and_music.replace('\n', '\n' + spc) + names = [ + 'HubOfTheUniverseProd', 'Jovica', 'LG', 'Leady', 'Percy Duke', + 'PhreaKsAccount', 'Pogotron', 'Rock Savage', 'anamorphosis', + 'benboncan', 'cdrk', 'chipfork', 'guitarguy1985', 'jascha', + 'joedeshon', 'loofa', 'm_O_m', 'mich3d', 'sandyrb', 'shakaharu', + 'sirplus', 'stickman', 'thanvannispen', 'virotic', 'zimbot' + ] + names.sort(key=lambda x: x.lower()) + freesound_names = _format_names(names, 90) + + try: + with open('ba_data/data/langdata.json') as infile: + translation_contributors = (json.loads( + infile.read())['translation_contributors']) + except Exception: + ba.print_exception('Error reading translation contributors.') + translation_contributors = [] + + translation_names = _format_names(translation_contributors, 60) + + # Need to bake this out and chop it up since we're passing our + # 65535 vertex limit for meshes.. + # We can remove that limit once we drop support for GL ES2.. :-/ + # (or add mesh splitting under the hood) + credits_text = ( + ' ' + ba.Lstr(resource=self._r + + '.codingGraphicsAudioText').evaluate().replace( + '${NAME}', 'Eric Froemling') + '\n' + '\n' + ' ' + ba.Lstr(resource=self._r + + '.additionalAudioArtIdeasText').evaluate().replace( + '${NAME}', 'Raphael Suter') + '\n' + '\n' + ' ' + + ba.Lstr(resource=self._r + '.soundAndMusicText').evaluate() + '\n' + '\n' + sound_and_music + '\n' + '\n' + ' ' + ba.Lstr(resource=self._r + + '.publicDomainMusicViaText').evaluate().replace( + '${NAME}', 'Musopen.com') + '\n' + ' ' + + ba.Lstr(resource=self._r + + '.thanksEspeciallyToText').evaluate().replace( + '${NAME}', 'the US Army, Navy, and Marine Bands') + + '\n' + '\n' + ' ' + ba.Lstr(resource=self._r + + '.additionalMusicFromText').evaluate().replace( + '${NAME}', 'The YouTube Audio Library') + + '\n' + '\n' + ' ' + + ba.Lstr(resource=self._r + '.soundsText').evaluate().replace( + '${SOURCE}', 'Freesound.org') + '\n' + '\n' + freesound_names + '\n' + '\n' + ' ' + ba.Lstr(resource=self._r + + '.languageTranslationsText').evaluate() + '\n' + '\n' + '\n'.join(translation_names.splitlines()[:146]) + + '\n'.join(translation_names.splitlines()[146:]) + '\n' + '\n' + ' Shout Out to Awesome Mods / Modders:\n\n' + ' BombDash ModPack\n' + ' TheMikirog & SoK - BombSquad Joyride Modpack\n' + ' Mrmaxmeier - BombSquad-Community-Mod-Manager\n' + '\n' + ' Holiday theme vector art designed by Freepik\n' + '\n' + ' ' + + ba.Lstr(resource=self._r + '.specialThanksText').evaluate() + '\n' + '\n' + ' Todd, Laura, and Robert Froemling\n' + ' ' + + ba.Lstr(resource=self._r + '.allMyFamilyText').evaluate().replace( + '\n', '\n ') + '\n' + ' ' + ba.Lstr(resource=self._r + + '.whoeverInventedCoffeeText').evaluate() + '\n' + '\n' + ' ' + ba.Lstr(resource=self._r + '.legalText').evaluate() + '\n' + '\n' + ' ' + ba.Lstr(resource=self._r + + '.softwareBasedOnText').evaluate().replace( + '${NAME}', 'the Khronos Group') + '\n' + '\n' + ' ' + ' www.froemling.net\n') + + txt = credits_text + lines = txt.splitlines() + line_height = 20 + + scale = 0.55 + self._sub_width = width - 80 + self._sub_height = line_height * len(lines) + 40 + + container = self._subcontainer = ba.containerwidget( + parent=scroll, + size=(self._sub_width, self._sub_height), + background=False, + claims_left_right=False, + claims_tab=False) + + voffs = 0 + for line in lines: + ba.textwidget(parent=container, + padding=4, + color=(0.7, 0.9, 0.7, 1.0), + scale=scale, + flatness=1.0, + size=(0, 0), + position=(0, self._sub_height - 20 + voffs), + h_align='left', + v_align='top', + text=ba.Lstr(value=line)) + voffs -= line_height + + def _back(self) -> None: + from bastd.ui.mainmenu import MainMenuWindow + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/debug.py b/dist/ba_data/python/bastd/ui/debug.py new file mode 100644 index 0000000..63f3878 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/debug.py @@ -0,0 +1,319 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UIs for debugging purposes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +import ba + +if TYPE_CHECKING: + pass + + +class DebugWindow(ba.Window): + """Window for debugging internal values.""" + + def __init__(self, transition: str = 'in_right'): + # pylint: disable=too-many-statements + # pylint: disable=cyclic-import + from bastd.ui import popup + + uiscale = ba.app.ui.uiscale + self._width = width = 580 + self._height = height = (350 if uiscale is ba.UIScale.SMALL else + 420 if uiscale is ba.UIScale.MEDIUM else 520) + + self._scroll_width = self._width - 100 + self._scroll_height = self._height - 120 + + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 520 + + self._stress_test_game_type = 'Random' + self._stress_test_playlist = '__default__' + self._stress_test_player_count = 8 + self._stress_test_round_duration = 30 + + self._r = 'debugWindow' + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition=transition, + scale=(2.35 if uiscale is ba.UIScale.SMALL else + 1.55 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -30) if uiscale is ba.UIScale.SMALL else (0, 0))) + + self._done_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(40, height - 67), + size=(120, 60), + scale=0.8, + autoselect=True, + label=ba.Lstr(resource='doneText'), + on_activate_call=self._done) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + ba.textwidget(parent=self._root_widget, + position=(0, height - 60), + size=(width, 30), + text=ba.Lstr(resource=self._r + '.titleText'), + h_align='center', + color=ba.app.ui.title_color, + v_align='center', + maxwidth=260) + + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + highlight=False, + size=(self._scroll_width, self._scroll_height), + position=((self._width - self._scroll_width) * 0.5, 50)) + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + + v = self._sub_height - 70 + button_width = 300 + btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + size=(button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.runCPUBenchmarkText'), + on_activate_call=self._run_cpu_benchmark_pressed) + ba.widget(edit=btn, + up_widget=self._done_button, + left_widget=self._done_button) + v -= 60 + + ba.buttonwidget(parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + size=(button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + + '.runGPUBenchmarkText'), + on_activate_call=self._run_gpu_benchmark_pressed) + v -= 60 + + ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + size=(button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.runMediaReloadBenchmarkText'), + on_activate_call=self._run_media_reload_benchmark_pressed) + v -= 60 + + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, v + 22), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.stressTestTitleText'), + maxwidth=200, + color=ba.app.ui.heading_color, + scale=0.85, + h_align='center', + v_align='center') + v -= 45 + + x_offs = 165 + ba.textwidget(parent=self._subcontainer, + position=(x_offs - 10, v + 22), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.stressTestPlaylistTypeText'), + maxwidth=130, + color=ba.app.ui.heading_color, + scale=0.65, + h_align='right', + v_align='center') + + popup.PopupMenu( + parent=self._subcontainer, + position=(x_offs, v), + width=150, + choices=['Random', 'Teams', 'Free-For-All'], + choices_display=[ + ba.Lstr(resource=a) for a in [ + 'randomText', 'playModes.teamsText', + 'playModes.freeForAllText' + ] + ], + current_choice='Auto', + on_value_change_call=self._stress_test_game_type_selected) + + v -= 46 + ba.textwidget(parent=self._subcontainer, + position=(x_offs - 10, v + 22), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.stressTestPlaylistNameText'), + maxwidth=130, + color=ba.app.ui.heading_color, + scale=0.65, + h_align='right', + v_align='center') + + self._stress_test_playlist_name_field = ba.textwidget( + parent=self._subcontainer, + position=(x_offs + 5, v - 5), + size=(250, 46), + text=self._stress_test_playlist, + h_align='left', + v_align='center', + autoselect=True, + color=(0.9, 0.9, 0.9, 1.0), + description=ba.Lstr(resource=self._r + + '.stressTestPlaylistDescriptionText'), + editable=True, + padding=4) + v -= 29 + x_sub = 60 + + # Player count. + ba.textwidget(parent=self._subcontainer, + position=(x_offs - 10, v), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.stressTestPlayerCountText'), + color=(0.8, 0.8, 0.8, 1.0), + h_align='right', + v_align='center', + scale=0.65, + maxwidth=130) + self._stress_test_player_count_text = ba.textwidget( + parent=self._subcontainer, + position=(246 - x_sub, v - 14), + size=(60, 28), + editable=False, + color=(0.3, 1.0, 0.3, 1.0), + h_align='right', + v_align='center', + text=str(self._stress_test_player_count), + padding=2) + ba.buttonwidget(parent=self._subcontainer, + position=(330 - x_sub, v - 11), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=ba.Call( + self._stress_test_player_count_decrement), + repeat=True, + enable_sound=True) + ba.buttonwidget(parent=self._subcontainer, + position=(380 - x_sub, v - 11), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=ba.Call( + self._stress_test_player_count_increment), + repeat=True, + enable_sound=True) + v -= 42 + + # Round duration. + ba.textwidget(parent=self._subcontainer, + position=(x_offs - 10, v), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.stressTestRoundDurationText'), + color=(0.8, 0.8, 0.8, 1.0), + h_align='right', + v_align='center', + scale=0.65, + maxwidth=130) + self._stress_test_round_duration_text = ba.textwidget( + parent=self._subcontainer, + position=(246 - x_sub, v - 14), + size=(60, 28), + editable=False, + color=(0.3, 1.0, 0.3, 1.0), + h_align='right', + v_align='center', + text=str(self._stress_test_round_duration), + padding=2) + ba.buttonwidget(parent=self._subcontainer, + position=(330 - x_sub, v - 11), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=ba.Call( + self._stress_test_round_duration_decrement), + repeat=True, + enable_sound=True) + ba.buttonwidget(parent=self._subcontainer, + position=(380 - x_sub, v - 11), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=ba.Call( + self._stress_test_round_duration_increment), + repeat=True, + enable_sound=True) + v -= 82 + btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v), + size=(button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.runStressTestText'), + on_activate_call=self._stress_test_pressed) + ba.widget(btn, show_buffer_bottom=50) + + def _stress_test_player_count_decrement(self) -> None: + self._stress_test_player_count = max( + 1, self._stress_test_player_count - 1) + ba.textwidget(edit=self._stress_test_player_count_text, + text=str(self._stress_test_player_count)) + + def _stress_test_player_count_increment(self) -> None: + self._stress_test_player_count = self._stress_test_player_count + 1 + ba.textwidget(edit=self._stress_test_player_count_text, + text=str(self._stress_test_player_count)) + + def _stress_test_round_duration_decrement(self) -> None: + self._stress_test_round_duration = max( + 10, self._stress_test_round_duration - 10) + ba.textwidget(edit=self._stress_test_round_duration_text, + text=str(self._stress_test_round_duration)) + + def _stress_test_round_duration_increment(self) -> None: + self._stress_test_round_duration = (self._stress_test_round_duration + + 10) + ba.textwidget(edit=self._stress_test_round_duration_text, + text=str(self._stress_test_round_duration)) + + def _stress_test_game_type_selected(self, game_type: str) -> None: + self._stress_test_game_type = game_type + + def _run_cpu_benchmark_pressed(self) -> None: + from ba.internal import run_cpu_benchmark + run_cpu_benchmark() + + def _run_gpu_benchmark_pressed(self) -> None: + from ba.internal import run_gpu_benchmark + run_gpu_benchmark() + + def _run_media_reload_benchmark_pressed(self) -> None: + from ba.internal import run_media_reload_benchmark + run_media_reload_benchmark() + + def _stress_test_pressed(self) -> None: + from ba.internal import run_stress_test + run_stress_test( + playlist_type=self._stress_test_game_type, + playlist_name=cast( + str, + ba.textwidget(query=self._stress_test_playlist_name_field)), + player_count=self._stress_test_player_count, + round_duration=self._stress_test_round_duration) + ba.containerwidget(edit=self._root_widget, transition='out_right') + + def _done(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.advanced import AdvancedSettingsWindow + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + AdvancedSettingsWindow(transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/feedback.py b/dist/ba_data/python/bastd/ui/feedback.py new file mode 100644 index 0000000..7d4db7c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/feedback.py @@ -0,0 +1,87 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to users rating the game.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Optional + + +def ask_for_rating() -> Optional[ba.Widget]: + """(internal)""" + app = ba.app + platform = app.platform + subplatform = app.subplatform + + # FIXME: should whitelist platforms we *do* want this for. + if ba.app.test_build: + return None + if not (platform == 'mac' or (platform == 'android' + and subplatform in ['google', 'cardboard'])): + return None + width = 700 + height = 400 + spacing = 40 + uiscale = ba.app.ui.uiscale + dlg = ba.containerwidget( + size=(width, height), + transition='in_right', + scale=(1.6 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0)) + v = height - 50 + v -= spacing + v -= 140 + ba.imagewidget(parent=dlg, + position=(width / 2 - 100, v + 10), + size=(200, 200), + texture=ba.gettexture('cuteSpaz')) + ba.textwidget(parent=dlg, + position=(15, v - 55), + size=(width - 30, 30), + color=ba.app.ui.infotextcolor, + text=ba.Lstr(resource='pleaseRateText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText'))]), + maxwidth=width * 0.95, + max_height=130, + scale=0.85, + h_align='center', + v_align='center') + + def do_rating() -> None: + import _ba + if platform == 'android': + appname = _ba.appname() + if subplatform == 'google': + url = f'market://details?id=net.froemling.{appname}' + else: + url = 'market://details?id=net.froemling.{appname}cb' + else: + url = 'macappstore://itunes.apple.com/app/id416482767?ls=1&mt=12' + + ba.open_url(url) + ba.containerwidget(edit=dlg, transition='out_left') + + ba.buttonwidget(parent=dlg, + position=(60, 20), + size=(200, 60), + label=ba.Lstr(resource='wellSureText'), + autoselect=True, + on_activate_call=do_rating) + + def close() -> None: + ba.containerwidget(edit=dlg, transition='out_left') + + btn = ba.buttonwidget(parent=dlg, + position=(width - 270, 20), + size=(200, 60), + label=ba.Lstr(resource='noThanksText'), + autoselect=True, + on_activate_call=close) + ba.containerwidget(edit=dlg, cancel_button=btn, selected_child=btn) + return dlg diff --git a/dist/ba_data/python/bastd/ui/fileselector.py b/dist/ba_data/python/bastd/ui/fileselector.py new file mode 100644 index 0000000..da6e2b2 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/fileselector.py @@ -0,0 +1,386 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for selecting files.""" + +from __future__ import annotations + +import os +import threading +import time +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Callable, Sequence, List, Optional + + +class FileSelectorWindow(ba.Window): + """Window for selecting files.""" + + def __init__(self, + path: str, + callback: Callable[[Optional[str]], Any] = None, + show_base_path: bool = True, + valid_file_extensions: Sequence[str] = None, + allow_folders: bool = False): + if valid_file_extensions is None: + valid_file_extensions = [] + uiscale = ba.app.ui.uiscale + self._width = 700 if uiscale is ba.UIScale.SMALL else 600 + self._x_inset = x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + self._height = 365 if uiscale is ba.UIScale.SMALL else 418 + self._callback = callback + self._base_path = path + self._path: Optional[str] = None + self._recent_paths: List[str] = [] + self._show_base_path = show_base_path + self._valid_file_extensions = [ + '.' + ext for ext in valid_file_extensions + ] + self._allow_folders = allow_folders + self._subcontainer: Optional[ba.Widget] = None + self._subcontainerheight: Optional[float] = None + self._scroll_width = self._width - (80 + 2 * x_inset) + self._scroll_height = self._height - 170 + self._r = 'fileSelectorWindow' + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition='in_right', + scale=(2.23 if uiscale is ba.UIScale.SMALL else + 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -35) if uiscale is ba.UIScale.SMALL else (0, 0))) + ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 42), + size=(0, 0), + color=ba.app.ui.title_color, + h_align='center', + v_align='center', + text=ba.Lstr(resource=self._r + '.titleFolderText') if + (allow_folders and not valid_file_extensions) else ba.Lstr( + resource=self._r + + '.titleFileText') if not allow_folders else ba.Lstr( + resource=self._r + '.titleFileFolderText'), + maxwidth=210) + + self._button_width = 146 + self._cancel_button = ba.buttonwidget( + parent=self._root_widget, + position=(35 + x_inset, self._height - 67), + autoselect=True, + size=(self._button_width, 50), + label=ba.Lstr(resource='cancelText'), + on_activate_call=self._cancel) + ba.widget(edit=self._cancel_button, left_widget=self._cancel_button) + + b_color = (0.6, 0.53, 0.63) + + self._back_button = ba.buttonwidget( + parent=self._root_widget, + button_type='square', + position=(43 + x_inset, self._height - 113), + color=b_color, + textcolor=(0.75, 0.7, 0.8), + enable_sound=False, + size=(55, 35), + label=ba.charstr(ba.SpecialChar.LEFT_ARROW), + on_activate_call=self._on_back_press) + + self._folder_tex = ba.gettexture('folder') + self._folder_color = (1.1, 0.8, 0.2) + self._file_tex = ba.gettexture('file') + self._file_color = (1, 1, 1) + self._use_folder_button: Optional[ba.Widget] = None + self._folder_center = self._width * 0.5 + 15 + self._folder_icon = ba.imagewidget(parent=self._root_widget, + size=(40, 40), + position=(40, self._height - 117), + texture=self._folder_tex, + color=self._folder_color) + self._path_text = ba.textwidget(parent=self._root_widget, + position=(self._folder_center, + self._height - 98), + size=(0, 0), + color=ba.app.ui.title_color, + h_align='center', + v_align='center', + text=self._path, + maxwidth=self._width * 0.9) + self._scrollwidget: Optional[ba.Widget] = None + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + self._set_path(path) + + def _on_up_press(self) -> None: + self._on_entry_activated('..') + + def _on_back_press(self) -> None: + if len(self._recent_paths) > 1: + ba.playsound(ba.getsound('swish')) + self._recent_paths.pop() + self._set_path(self._recent_paths.pop()) + else: + ba.playsound(ba.getsound('error')) + + def _on_folder_entry_activated(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_right') + if self._callback is not None: + assert self._path is not None + self._callback(self._path) + + def _on_entry_activated(self, entry: str) -> None: + # pylint: disable=too-many-branches + new_path = None + try: + assert self._path is not None + if entry == '..': + chunks = self._path.split('/') + if len(chunks) > 1: + new_path = '/'.join(chunks[:-1]) + if new_path == '': + new_path = '/' + else: + ba.playsound(ba.getsound('error')) + else: + if self._path == '/': + test_path = self._path + entry + else: + test_path = self._path + '/' + entry + if os.path.isdir(test_path): + ba.playsound(ba.getsound('swish')) + new_path = test_path + elif os.path.isfile(test_path): + if self._is_valid_file_path(test_path): + ba.playsound(ba.getsound('swish')) + ba.containerwidget(edit=self._root_widget, + transition='out_right') + if self._callback is not None: + self._callback(test_path) + else: + ba.playsound(ba.getsound('error')) + else: + print(('Error: FileSelectorWindow found non-file/dir:', + test_path)) + except Exception: + ba.print_exception( + 'Error in FileSelectorWindow._on_entry_activated().') + + if new_path is not None: + self._set_path(new_path) + + class _RefreshThread(threading.Thread): + + def __init__(self, path: str, + callback: Callable[[List[str], Optional[str]], Any]): + super().__init__() + self._callback = callback + self._path = path + + def run(self) -> None: + try: + starttime = time.time() + files = os.listdir(self._path) + duration = time.time() - starttime + min_time = 0.1 + + # Make sure this takes at least 1/10 second so the user + # has time to see the selection highlight. + if duration < min_time: + time.sleep(min_time - duration) + ba.pushcall(ba.Call(self._callback, files, None), + from_other_thread=True) + except Exception as exc: + # Ignore permission-denied. + if 'Errno 13' not in str(exc): + ba.print_exception() + nofiles: List[str] = [] + ba.pushcall(ba.Call(self._callback, nofiles, str(exc)), + from_other_thread=True) + + def _set_path(self, path: str, add_to_recent: bool = True) -> None: + self._path = path + if add_to_recent: + self._recent_paths.append(path) + self._RefreshThread(path, self._refresh).start() + + def _refresh(self, file_names: List[str], error: Optional[str]) -> None: + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + if not self._root_widget: + return + + scrollwidget_selected = (self._scrollwidget is None + or self._root_widget.get_selected_child() + == self._scrollwidget) + + in_top_folder = (self._path == self._base_path) + hide_top_folder = in_top_folder and self._show_base_path is False + + if hide_top_folder: + folder_name = '' + elif self._path == '/': + folder_name = '/' + else: + assert self._path is not None + folder_name = os.path.basename(self._path) + + b_color = (0.6, 0.53, 0.63) + b_color_disabled = (0.65, 0.65, 0.65) + + if len(self._recent_paths) < 2: + ba.buttonwidget(edit=self._back_button, + color=b_color_disabled, + textcolor=(0.5, 0.5, 0.5)) + else: + ba.buttonwidget(edit=self._back_button, + color=b_color, + textcolor=(0.75, 0.7, 0.8)) + + max_str_width = 300.0 + str_width = min( + max_str_width, + _ba.get_string_width(folder_name, suppress_warning=True)) + ba.textwidget(edit=self._path_text, + text=folder_name, + maxwidth=max_str_width) + ba.imagewidget(edit=self._folder_icon, + position=(self._folder_center - str_width * 0.5 - 40, + self._height - 117), + opacity=0.0 if hide_top_folder else 1.0) + + if self._scrollwidget is not None: + self._scrollwidget.delete() + + if self._use_folder_button is not None: + self._use_folder_button.delete() + ba.widget(edit=self._cancel_button, right_widget=self._back_button) + + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + position=((self._width - self._scroll_width) * 0.5, + self._height - self._scroll_height - 119), + size=(self._scroll_width, self._scroll_height)) + + if scrollwidget_selected: + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + + # show error case.. + if error is not None: + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._scroll_width, + self._scroll_height), + background=False) + ba.textwidget(parent=self._subcontainer, + color=(1, 1, 0, 1), + text=error, + maxwidth=self._scroll_width * 0.9, + position=(self._scroll_width * 0.48, + self._scroll_height * 0.57), + size=(0, 0), + h_align='center', + v_align='center') + + else: + file_names = [f for f in file_names if not f.startswith('.')] + file_names.sort(key=lambda x: x[0].lower()) + + entries = file_names + entry_height = 35 + folder_entry_height = 100 + show_folder_entry = False + show_use_folder_button = (self._allow_folders + and not in_top_folder) + + self._subcontainerheight = entry_height * len(entries) + ( + folder_entry_height if show_folder_entry else 0) + v = self._subcontainerheight - (folder_entry_height + if show_folder_entry else 0) + + self._subcontainer = ba.containerwidget( + parent=self._scrollwidget, + size=(self._scroll_width, self._subcontainerheight), + background=False) + + ba.containerwidget(edit=self._scrollwidget, + claims_left_right=False, + claims_tab=False) + ba.containerwidget(edit=self._subcontainer, + claims_left_right=False, + claims_tab=False, + selection_loops=False, + print_list_exit_instructions=False) + ba.widget(edit=self._subcontainer, up_widget=self._back_button) + + if show_use_folder_button: + self._use_folder_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - self._button_width - 35 - + self._x_inset, self._height - 67), + size=(self._button_width, 50), + label=ba.Lstr(resource=self._r + + '.useThisFolderButtonText'), + on_activate_call=self._on_folder_entry_activated) + ba.widget(edit=btn, + left_widget=self._cancel_button, + down_widget=self._scrollwidget) + ba.widget(edit=self._cancel_button, right_widget=btn) + ba.containerwidget(edit=self._root_widget, start_button=btn) + + folder_icon_size = 35 + for num, entry in enumerate(entries): + cnt = ba.containerwidget( + parent=self._subcontainer, + position=(0, v - entry_height), + size=(self._scroll_width, entry_height), + root_selectable=True, + background=False, + click_activate=True, + on_activate_call=ba.Call(self._on_entry_activated, entry)) + if num == 0: + ba.widget(edit=cnt, up_widget=self._back_button) + is_valid_file_path = self._is_valid_file_path(entry) + assert self._path is not None + is_dir = os.path.isdir(self._path + '/' + entry) + if is_dir: + ba.imagewidget(parent=cnt, + size=(folder_icon_size, folder_icon_size), + position=(10, 0.5 * entry_height - + folder_icon_size * 0.5), + draw_controller=cnt, + texture=self._folder_tex, + color=self._folder_color) + else: + ba.imagewidget(parent=cnt, + size=(folder_icon_size, folder_icon_size), + position=(10, 0.5 * entry_height - + folder_icon_size * 0.5), + opacity=1.0 if is_valid_file_path else 0.5, + draw_controller=cnt, + texture=self._file_tex, + color=self._file_color) + ba.textwidget(parent=cnt, + draw_controller=cnt, + text=entry, + h_align='left', + v_align='center', + position=(10 + folder_icon_size * 1.05, + entry_height * 0.5), + size=(0, 0), + maxwidth=self._scroll_width * 0.93 - 50, + color=(1, 1, 1, 1) if + (is_valid_file_path or is_dir) else + (0.5, 0.5, 0.5, 1)) + v -= entry_height + + def _is_valid_file_path(self, path: str) -> bool: + return any(path.lower().endswith(ext) + for ext in self._valid_file_extensions) + + def _cancel(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_right') + if self._callback is not None: + self._callback(None) diff --git a/dist/ba_data/python/bastd/ui/gather/__init__.py b/dist/ba_data/python/bastd/ui/gather/__init__.py new file mode 100644 index 0000000..3d21747 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/gather/__init__.py @@ -0,0 +1,331 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for inviting/joining friends.""" + +from __future__ import annotations + +import weakref +from enum import Enum +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui.tabs import TabRow + +if TYPE_CHECKING: + from typing import (Any, Optional, Tuple, Dict, List, Union, Callable, + Type) + + +class GatherTab: + """Defines a tab for use in the gather UI.""" + + def __init__(self, window: GatherWindow) -> None: + self._window = weakref.ref(window) + + @property + def window(self) -> GatherWindow: + """The GatherWindow that this tab belongs to.""" + window = self._window() + if window is None: + raise ba.NotFoundError("GatherTab's window no longer exists.") + return window + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + """Called when the tab becomes the active one. + + The tab should create and return a container widget covering the + specified region. + """ + + def on_deactivate(self) -> None: + """Called when the tab will no longer be the active one.""" + + def save_state(self) -> None: + """Called when the parent window is saving state.""" + + def restore_state(self) -> None: + """Called when the parent window is restoring state.""" + + +class GatherWindow(ba.Window): + """Window for joining/inviting friends.""" + + class TabID(Enum): + """Our available tab types.""" + ABOUT = 'about' + INTERNET = 'internet' + PRIVATE = 'private' + NEARBY = 'nearby' + MANUAL = 'manual' + + def __init__(self, + transition: Optional[str] = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from bastd.ui.gather.abouttab import AboutGatherTab + from bastd.ui.gather.manualtab import ManualGatherTab + from bastd.ui.gather.privatetab import PrivateGatherTab + from bastd.ui.gather.publictab import PublicGatherTab + from bastd.ui.gather.nearbytab import NearbyGatherTab + + ba.set_analytics_screen('Gather Window') + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + ba.app.ui.set_main_menu_location('Gather') + _ba.set_party_icon_always_visible(True) + uiscale = ba.app.ui.uiscale + self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 + x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (582 if uiscale is ba.UIScale.SMALL else + 680 if uiscale is ba.UIScale.MEDIUM else 800) + self._current_tab: Optional[GatherWindow.TabID] = None + extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 + self._r = 'gatherWindow' + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + extra_top), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(1.3 if uiscale is ba.UIScale.SMALL else + 0.97 if uiscale is ba.UIScale.MEDIUM else 0.8), + stack_offset=(0, -11) if uiscale is ba.UIScale.SMALL else ( + 0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0))) + + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._back) + self._back_button = None + else: + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(70 + x_offs, self._height - 74), + size=(140, 60), + scale=1.1, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + ba.buttonwidget(edit=btn, + button_type='backSmall', + position=(70 + x_offs, self._height - 78), + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + condensed = uiscale is not ba.UIScale.LARGE + t_offs_y = (0 if not condensed else + 25 if uiscale is ba.UIScale.MEDIUM else 17) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, + self._height - 42 + t_offs_y), + size=(0, 0), + color=ba.app.ui.title_color, + scale=(1.5 if not condensed else + 1.0 if uiscale is ba.UIScale.MEDIUM else 0.6), + h_align='center', + v_align='center', + text=ba.Lstr(resource=self._r + '.titleText'), + maxwidth=550) + + scroll_buffer_h = 130 + 2 * x_offs + tab_buffer_h = ((320 if condensed else 250) + 2 * x_offs) + + # Build up the set of tabs we want. + tabdefs: List[Tuple[GatherWindow.TabID, ba.Lstr]] = [ + (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText')) + ] + if _ba.get_account_misc_read_val('enablePublicParties', True): + tabdefs.append((self.TabID.INTERNET, + ba.Lstr(resource=self._r + '.publicText'))) + tabdefs.append( + (self.TabID.PRIVATE, ba.Lstr(resource=self._r + '.privateText'))) + tabdefs.append( + (self.TabID.NEARBY, ba.Lstr(resource=self._r + '.nearbyText'))) + tabdefs.append( + (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText'))) + + # On small UI, push our tabs up closer to the top of the screen to + # save a bit of space. + tabs_top_extra = 42 if condensed else 0 + self._tab_row = TabRow(self._root_widget, + tabdefs, + pos=(tab_buffer_h * 0.5, + self._height - 130 + tabs_top_extra), + size=(self._width - tab_buffer_h, 50), + on_select_call=ba.WeakCall(self._set_tab)) + + # Now instantiate handlers for these tabs. + tabtypes: Dict[GatherWindow.TabID, Type[GatherTab]] = { + self.TabID.ABOUT: AboutGatherTab, + self.TabID.MANUAL: ManualGatherTab, + self.TabID.PRIVATE: PrivateGatherTab, + self.TabID.INTERNET: PublicGatherTab, + self.TabID.NEARBY: NearbyGatherTab + } + self._tabs: Dict[GatherWindow.TabID, GatherTab] = {} + for tab_id in self._tab_row.tabs: + tabtype = tabtypes.get(tab_id) + if tabtype is not None: + self._tabs[tab_id] = tabtype(self) + + if ba.app.ui.use_toolbars: + ba.widget(edit=self._tab_row.tabs[tabdefs[-1][0]].button, + right_widget=_ba.get_special_widget('party_button')) + if uiscale is ba.UIScale.SMALL: + ba.widget(edit=self._tab_row.tabs[tabdefs[0][0]].button, + left_widget=_ba.get_special_widget('back_button')) + + self._scroll_width = self._width - scroll_buffer_h + self._scroll_height = self._height - 180.0 + tabs_top_extra + + self._scroll_left = (self._width - self._scroll_width) * 0.5 + self._scroll_bottom = (self._height - self._scroll_height - 79 - 48 + + tabs_top_extra) + buffer_h = 10 + buffer_v = 4 + + # Not actually using a scroll widget anymore; just an image. + ba.imagewidget(parent=self._root_widget, + position=(self._scroll_left - buffer_h, + self._scroll_bottom - buffer_v), + size=(self._scroll_width + 2 * buffer_h, + self._scroll_height + 2 * buffer_v), + texture=ba.gettexture('scrollWidget'), + model_transparent=ba.getmodel('softEdgeOutside')) + self._tab_container: Optional[ba.Widget] = None + + self._restore_state() + + def __del__(self) -> None: + _ba.set_party_icon_always_visible(False) + + def playlist_select(self, origin_widget: ba.Widget) -> None: + """Called by the private-hosting tab to select a playlist.""" + from bastd.ui.play import PlayWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.selecting_private_party_playlist = True + ba.app.ui.set_main_menu_window( + PlayWindow(origin_widget=origin_widget).get_root_widget()) + + def _set_tab(self, tab_id: TabID) -> None: + if self._current_tab is tab_id: + return + prev_tab_id = self._current_tab + self._current_tab = tab_id + + # We wanna preserve our current tab between runs. + cfg = ba.app.config + cfg['Gather Tab'] = tab_id.value + cfg.commit() + + # Update tab colors based on which is selected. + self._tab_row.update_appearance(tab_id) + + if prev_tab_id is not None: + prev_tab = self._tabs.get(prev_tab_id) + if prev_tab is not None: + prev_tab.on_deactivate() + + # Clear up prev container if it hasn't been done. + if self._tab_container: + self._tab_container.delete() + + tab = self._tabs.get(tab_id) + if tab is not None: + self._tab_container = tab.on_activate( + self._root_widget, + self._tab_row.tabs[tab_id].button, + self._scroll_width, + self._scroll_height, + self._scroll_left, + self._scroll_bottom, + ) + return + + def _save_state(self) -> None: + try: + for tab in self._tabs.values(): + tab.save_state() + + sel = self._root_widget.get_selected_child() + selected_tab_ids = [ + tab_id for tab_id, tab in self._tab_row.tabs.items() + if sel == tab.button + ] + if sel == self._back_button: + sel_name = 'Back' + elif selected_tab_ids: + assert len(selected_tab_ids) == 1 + sel_name = f'Tab:{selected_tab_ids[0].value}' + elif sel == self._tab_container: + sel_name = 'TabContainer' + else: + raise ValueError(f'unrecognized selection: \'{sel}\'') + ba.app.ui.window_states[type(self)] = { + 'sel_name': sel_name, + } + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + def _restore_state(self) -> None: + from efro.util import enum_by_value + try: + for tab in self._tabs.values(): + tab.restore_state() + + sel: Optional[ba.Widget] + winstate = ba.app.ui.window_states.get(type(self), {}) + sel_name = winstate.get('sel_name', None) + assert isinstance(sel_name, (str, type(None))) + current_tab = self.TabID.ABOUT + gather_tab_val = ba.app.config.get('Gather Tab') + try: + stored_tab = enum_by_value(self.TabID, gather_tab_val) + if stored_tab in self._tab_row.tabs: + current_tab = stored_tab + except ValueError: + pass + self._set_tab(current_tab) + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'TabContainer': + sel = self._tab_container + elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): + try: + sel_tab_id = enum_by_value(self.TabID, + sel_name.split(':')[-1]) + except ValueError: + sel_tab_id = self.TabID.ABOUT + sel = self._tab_row.tabs[sel_tab_id].button + else: + sel = self._tab_row.tabs[current_tab].button + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception('Error restoring gather-win state.') + + def _back(self) -> None: + from bastd.ui.mainmenu import MainMenuWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..8fd7f3c Binary files /dev/null and b/dist/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..33dbdb2 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-38.opt-1.pyc new file mode 100644 index 0000000..1844ba8 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6d21488 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4d0067f Binary files /dev/null and b/dist/ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-38.opt-1.pyc new file mode 100644 index 0000000..1555551 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-38.opt-1.pyc new file mode 100644 index 0000000..3949c66 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/gather/abouttab.py b/dist/ba_data/python/bastd/ui/gather/abouttab.py new file mode 100644 index 0000000..a76eb11 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/gather/abouttab.py @@ -0,0 +1,110 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the about tab in the gather UI.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +from bastd.ui.gather import GatherTab + +if TYPE_CHECKING: + from typing import Optional + from bastd.ui.gather import GatherWindow + + +class AboutGatherTab(GatherTab): + """The about tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._container: Optional[ba.Widget] = None + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + message = ba.Lstr( + resource='gatherWindow.aboutDescriptionText', + subs=[('${PARTY}', ba.charstr(ba.SpecialChar.PARTY_ICON)), + ('${BUTTON}', ba.charstr(ba.SpecialChar.TOP_BUTTON))], + ) + + # Let's not talk about sharing in vr-mode; its tricky to fit more + # than one head in a VR-headset ;-) + if not ba.app.vr_mode: + message = ba.Lstr( + value='${A}\n\n${B}', + subs=[('${A}', message), + ('${B}', + ba.Lstr(resource='gatherWindow.' + 'aboutDescriptionLocalMultiplayerExtraText'))]) + string_height = 400 + include_invite = True + msc_scale = 1.1 + c_height_2 = min(region_height, string_height * msc_scale + 100) + try_tickets = _ba.get_account_misc_read_val('friendTryTickets', None) + if try_tickets is None: + include_invite = False + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - c_height_2) * 0.5), + size=(region_width, c_height_2), + background=False, + selectable=include_invite) + ba.widget(edit=self._container, up_widget=tab_button) + + ba.textwidget(parent=self._container, + position=(region_width * 0.5, c_height_2 * + (0.58 if include_invite else 0.5)), + color=(0.6, 1.0, 0.6), + scale=msc_scale, + size=(0, 0), + maxwidth=region_width * 0.9, + max_height=c_height_2 * (0.7 if include_invite else 0.9), + h_align='center', + v_align='center', + text=message) + + if include_invite: + ba.textwidget(parent=self._container, + position=(region_width * 0.57, 35), + color=(0, 1, 0), + scale=0.6, + size=(0, 0), + maxwidth=region_width * 0.5, + h_align='right', + v_align='center', + flatness=1.0, + text=ba.Lstr( + resource='gatherWindow.inviteAFriendText', + subs=[('${COUNT}', str(try_tickets))])) + ba.buttonwidget( + parent=self._container, + position=(region_width * 0.59, 10), + size=(230, 50), + color=(0.54, 0.42, 0.56), + textcolor=(0, 1, 0), + label=ba.Lstr(resource='gatherWindow.inviteFriendsText', + fallback_resource=( + 'gatherWindow.getFriendInviteCodeText')), + autoselect=True, + on_activate_call=ba.WeakCall(self._invite_to_try_press), + up_widget=tab_button) + return self._container + + def _invite_to_try_press(self) -> None: + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.appinvite import handle_app_invites_press + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + handle_app_invites_press() diff --git a/dist/ba_data/python/bastd/ui/gather/manualtab.py b/dist/ba_data/python/bastd/ui/gather/manualtab.py new file mode 100644 index 0000000..7ae3b1c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/gather/manualtab.py @@ -0,0 +1,833 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the manual tab in the gather UI.""" + +from __future__ import annotations + +import threading +from typing import TYPE_CHECKING, cast + +from enum import Enum +from dataclasses import dataclass +from bastd.ui.gather import GatherTab + +import _ba +import ba +if TYPE_CHECKING: + from typing import Any, Optional, Dict, List, Tuple, Type, Union, Callable + from bastd.ui.gather import GatherWindow + from bastd.ui.confirm import ConfirmWindow + + +def _safe_set_text(txt: Optional[ba.Widget], + val: Union[str, ba.Lstr], + success: bool = True) -> None: + if txt: + ba.textwidget(edit=txt, + text=val, + color=(0, 1, 0) if success else (1, 1, 0)) + + +class _HostLookupThread(threading.Thread): + """Thread to fetch an addr.""" + + def __init__(self, name: str, port: int, + call: Callable[[Optional[str], int], Any]): + super().__init__() + self._name = name + self._port = port + self._call = call + + def run(self) -> None: + result: Optional[str] + try: + import socket + result = socket.gethostbyname(self._name) + except Exception: + result = None + ba.pushcall(lambda: self._call(result, self._port), + from_other_thread=True) + + +class SubTabType(Enum): + """Available sub-tabs.""" + JOIN_BY_ADDRESS = 'join_by_address' + FAVORITES = 'favorites' + + +@dataclass +class State: + """State saved/restored only while the app is running.""" + sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS + + +class ManualGatherTab(GatherTab): + """The manual tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._check_button: Optional[ba.Widget] = None + self._doing_access_check: Optional[bool] = None + self._access_check_count: Optional[int] = None + self._sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS + self._t_addr: Optional[ba.Widget] = None + self._t_accessible: Optional[ba.Widget] = None + self._t_accessible_extra: Optional[ba.Widget] = None + self._access_check_timer: Optional[ba.Timer] = None + self._checking_state_text: Optional[ba.Widget] = None + self._container: Optional[ba.Widget] = None + self._join_by_address_text: Optional[ba.Widget] = None + self._favorites_text: Optional[ba.Widget] = None + self._width: Optional[int] = None + self._height: Optional[int] = None + self._scroll_width: Optional[int] = None + self._scroll_height: Optional[int] = None + self._favorites_scroll_width: Optional[int] = None + self._favorites_connect_button: Optional[ba.Widget] = None + self._scrollwidget: Optional[ba.Widget] = None + self._columnwidget: Optional[ba.Widget] = None + self._favorite_selected: Optional[str] = None + self._favorite_rename_window: Optional[ba.Widget] = None + self._party_rename_text: Optional[ba.Widget] = None + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + + c_width = region_width + c_height = region_height - 20 + + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - c_height) * 0.5), + size=(c_width, c_height), + background=False, + selection_loops_to_parent=True) + v = c_height - 30 + self._join_by_address_text = ba.textwidget( + parent=self._container, + position=(c_width * 0.5 - 245, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='center', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_sub_tab( + SubTabType.JOIN_BY_ADDRESS, + region_width, + region_height, + playsound=True, + ), + text=ba.Lstr(resource='gatherWindow.manualJoinSectionText')) + self._favorites_text = ba.textwidget( + parent=self._container, + position=(c_width * 0.5 + 45, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='center', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_sub_tab( + SubTabType.FAVORITES, + region_width, + region_height, + playsound=True, + ), + text=ba.Lstr(resource='gatherWindow.favoritesText')) + ba.widget(edit=self._join_by_address_text, up_widget=tab_button) + ba.widget(edit=self._favorites_text, + left_widget=self._join_by_address_text, + up_widget=tab_button) + ba.widget(edit=tab_button, down_widget=self._favorites_text) + ba.widget(edit=self._join_by_address_text, + right_widget=self._favorites_text) + self._set_sub_tab(self._sub_tab, region_width, region_height) + + return self._container + + def save_state(self) -> None: + ba.app.ui.window_states[type(self)] = State(sub_tab=self._sub_tab) + + def restore_state(self) -> None: + state = ba.app.ui.window_states.get(type(self)) + if state is None: + state = State() + assert isinstance(state, State) + self._sub_tab = state.sub_tab + + def _set_sub_tab(self, + value: SubTabType, + region_width: float, + region_height: float, + playsound: bool = False) -> None: + assert self._container + if playsound: + ba.playsound(ba.getsound('click01')) + + self._sub_tab = value + active_color = (0.6, 1.0, 0.6) + inactive_color = (0.5, 0.4, 0.5) + ba.textwidget(edit=self._join_by_address_text, + color=active_color if value is SubTabType.JOIN_BY_ADDRESS + else inactive_color) + ba.textwidget(edit=self._favorites_text, + color=active_color + if value is SubTabType.FAVORITES else inactive_color) + + # Clear anything existing in the old sub-tab. + for widget in self._container.get_children(): + if widget and widget not in { + self._favorites_text, self._join_by_address_text + }: + widget.delete() + + if value is SubTabType.JOIN_BY_ADDRESS: + self._build_join_by_address_tab(region_width, region_height) + + if value is SubTabType.FAVORITES: + self._build_favorites_tab(region_height) + + # The old manual tab + def _build_join_by_address_tab(self, region_width: float, + region_height: float) -> None: + c_width = region_width + c_height = region_height - 20 + last_addr = ba.app.config.get('Last Manual Party Connect Address', '') + v = c_height - 70 + v -= 70 + ba.textwidget(parent=self._container, + position=(c_width * 0.5 - 260 - 50, v), + color=(0.6, 1.0, 0.6), + scale=1.0, + size=(0, 0), + maxwidth=130, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'manualAddressText')) + txt = ba.textwidget(parent=self._container, + editable=True, + description=ba.Lstr(resource='gatherWindow.' + 'manualAddressText'), + position=(c_width * 0.5 - 240 - 50, v - 30), + text=last_addr, + autoselect=True, + v_align='center', + scale=1.0, + size=(420, 60)) + ba.widget(edit=self._join_by_address_text, down_widget=txt) + ba.widget(edit=self._favorites_text, down_widget=txt) + ba.textwidget(parent=self._container, + position=(c_width * 0.5 - 260 + 490, v), + color=(0.6, 1.0, 0.6), + scale=1.0, + size=(0, 0), + maxwidth=80, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'portText')) + txt2 = ba.textwidget(parent=self._container, + editable=True, + description=ba.Lstr(resource='gatherWindow.' + 'portText'), + text='43210', + autoselect=True, + max_chars=5, + position=(c_width * 0.5 - 240 + 490, v - 30), + v_align='center', + scale=1.0, + size=(170, 60)) + + v -= 110 + + btn = ba.buttonwidget(parent=self._container, + size=(300, 70), + label=ba.Lstr(resource='gatherWindow.' + 'manualConnectText'), + position=(c_width * 0.5 - 300, v), + autoselect=True, + on_activate_call=ba.Call(self._connect, txt, + txt2)) + savebutton = ba.buttonwidget( + parent=self._container, + size=(300, 70), + label=ba.Lstr(resource='gatherWindow.favoritesSaveText'), + position=(c_width * 0.5 - 240 + 490 - 200, v), + autoselect=True, + on_activate_call=ba.Call(self._save_server, txt, txt2)) + ba.widget(edit=btn, right_widget=savebutton) + ba.widget(edit=savebutton, left_widget=btn, up_widget=txt2) + ba.textwidget(edit=txt, on_return_press_call=btn.activate) + ba.textwidget(edit=txt2, on_return_press_call=btn.activate) + v -= 45 + + self._check_button = ba.textwidget( + parent=self._container, + size=(250, 60), + text=ba.Lstr(resource='gatherWindow.' + 'showMyAddressText'), + v_align='center', + h_align='center', + click_activate=True, + position=(c_width * 0.5 - 125, v - 30), + autoselect=True, + color=(0.5, 0.9, 0.5), + scale=0.8, + selectable=True, + on_activate_call=ba.Call(self._on_show_my_address_button_press, v, + self._container, c_width)) + ba.widget(edit=self._check_button, up_widget=btn) + + # Tab containing saved favorite addresses + def _build_favorites_tab(self, region_height: float) -> None: + + c_height = region_height - 20 + v = c_height - 35 - 25 - 30 + + uiscale = ba.app.ui.uiscale + self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (578 if uiscale is ba.UIScale.SMALL else + 670 if uiscale is ba.UIScale.MEDIUM else 800) + + self._scroll_width = self._width - 130 + 2 * x_inset + self._scroll_height = self._height - 180 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + + c_height = self._scroll_height - 20 + sub_scroll_height = c_height - 63 + self._favorites_scroll_width = sub_scroll_width = ( + 680 if uiscale is ba.UIScale.SMALL else 640) + + v = c_height - 30 + + b_width = 140 if uiscale is ba.UIScale.SMALL else 178 + b_height = (107 if uiscale is ba.UIScale.SMALL else + 142 if uiscale is ba.UIScale.MEDIUM else 190) + b_space_extra = (0 if uiscale is ba.UIScale.SMALL else + -2 if uiscale is ba.UIScale.MEDIUM else -5) + + btnv = (c_height - (48 if uiscale is ba.UIScale.SMALL else + 45 if uiscale is ba.UIScale.MEDIUM else 40) - + b_height) + + self._favorites_connect_button = btn1 = ba.buttonwidget( + parent=self._container, + size=(b_width, b_height), + position=(40 if uiscale is ba.UIScale.SMALL else 40, btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_favorites_connect_press, + text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, + label=ba.Lstr(resource='gatherWindow.manualConnectText'), + autoselect=True) + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + ba.widget(edit=btn1, + left_widget=_ba.get_special_widget('back_button')) + btnv -= b_height + b_space_extra + ba.buttonwidget(parent=self._container, + size=(b_width, b_height), + position=(40 if uiscale is ba.UIScale.SMALL else 40, + btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_favorites_rename_press, + text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, + label=ba.Lstr(resource='renameText'), + autoselect=True) + btnv -= b_height + b_space_extra + ba.buttonwidget(parent=self._container, + size=(b_width, b_height), + position=(40 if uiscale is ba.UIScale.SMALL else 40, + btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_favorite_delete_press, + text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, + label=ba.Lstr(resource='deleteText'), + autoselect=True) + + v -= sub_scroll_height + 23 + self._scrollwidget = scrlw = ba.scrollwidget( + parent=self._container, + position=(190 if uiscale is ba.UIScale.SMALL else 225, v), + size=(sub_scroll_width, sub_scroll_height), + claims_left_right=True) + ba.widget(edit=self._favorites_connect_button, + right_widget=self._scrollwidget) + self._columnwidget = ba.columnwidget(parent=scrlw, + left_border=10, + border=2, + margin=0, + claims_left_right=True) + + self._favorite_selected = None + self._refresh_favorites() + + def _no_favorite_selected_error(self) -> None: + ba.screenmessage(ba.Lstr(resource='nothingIsSelectedErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + + def _on_favorites_connect_press(self) -> None: + if self._favorite_selected is None: + self._no_favorite_selected_error() + + else: + config = ba.app.config['Saved Servers'][self._favorite_selected] + _HostLookupThread(name=config['addr'], + port=config['port'], + call=ba.WeakCall( + self._host_lookup_result)).start() + + def _on_favorites_rename_press(self) -> None: + if self._favorite_selected is None: + self._no_favorite_selected_error() + return + + c_width = 600 + c_height = 250 + uiscale = ba.app.ui.uiscale + self._favorite_rename_window = cnt = ba.containerwidget( + scale=(1.8 if uiscale is ba.UIScale.SMALL else + 1.55 if uiscale is ba.UIScale.MEDIUM else 1.0), + size=(c_width, c_height), + transition='in_scale') + + ba.textwidget(parent=cnt, + size=(0, 0), + h_align='center', + v_align='center', + text='Enter Name of Party', + maxwidth=c_width * 0.8, + position=(c_width * 0.5, c_height - 60)) + self._party_rename_text = txt = ba.textwidget( + parent=cnt, + size=(c_width * 0.8, 40), + h_align='left', + v_align='center', + text=ba.app.config['Saved Servers'][ + self._favorite_selected]['name'], + editable=True, + description='Server name text', + position=(c_width * 0.1, c_height - 140), + autoselect=True, + maxwidth=c_width * 0.7, + max_chars=200) + cbtn = ba.buttonwidget( + parent=cnt, + label=ba.Lstr(resource='cancelText'), + on_activate_call=ba.Call( + lambda c: ba.containerwidget(edit=c, transition='out_scale'), + cnt), + size=(180, 60), + position=(30, 30), + autoselect=True) + okb = ba.buttonwidget(parent=cnt, + label='Rename', + size=(180, 60), + position=(c_width - 230, 30), + on_activate_call=ba.Call( + self._rename_saved_party), + autoselect=True) + ba.widget(edit=cbtn, right_widget=okb) + ba.widget(edit=okb, left_widget=cbtn) + ba.textwidget(edit=txt, on_return_press_call=okb.activate) + ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) + + def _rename_saved_party(self) -> None: + + server = self._favorite_selected + if self._favorite_selected is None: + self._no_favorite_selected_error() + return + if not self._party_rename_text: + return + new_name_raw = cast(str, ba.textwidget(query=self._party_rename_text)) + ba.app.config['Saved Servers'][server]['name'] = new_name_raw + ba.app.config.commit() + ba.playsound(ba.getsound('gunCocking')) + self._refresh_favorites() + + ba.containerwidget(edit=self._favorite_rename_window, + transition='out_scale') + + def _on_favorite_delete_press(self) -> None: + from bastd.ui import confirm + if self._favorite_selected is None: + self._no_favorite_selected_error() + return + confirm.ConfirmWindow( + ba.Lstr(resource='gameListWindow.deleteConfirmText', + subs=[('${LIST}', ba.app.config['Saved Servers'][ + self._favorite_selected]['name'])]), + self._delete_saved_party, 450, 150) + + def _delete_saved_party(self) -> None: + if self._favorite_selected is None: + self._no_favorite_selected_error() + return + config = ba.app.config['Saved Servers'] + del config[self._favorite_selected] + self._favorite_selected = None + ba.app.config.commit() + ba.playsound(ba.getsound('shieldDown')) + self._refresh_favorites() + + def _on_favorite_select(self, server: str) -> None: + self._favorite_selected = server + + def _refresh_favorites(self) -> None: + assert self._columnwidget is not None + for child in self._columnwidget.get_children(): + child.delete() + t_scale = 1.6 + + config = ba.app.config + if 'Saved Servers' in config: + servers = config['Saved Servers'] + + else: + servers = [] + + assert self._favorites_scroll_width is not None + assert self._favorites_connect_button is not None + for i, server in enumerate(servers): + txt = ba.textwidget( + parent=self._columnwidget, + size=(self._favorites_scroll_width / t_scale, 30), + selectable=True, + color=(1.0, 1, 0.4), + always_highlight=True, + on_select_call=ba.Call(self._on_favorite_select, server), + on_activate_call=self._favorites_connect_button.activate, + text=(config['Saved Servers'][server]['name'] + if config['Saved Servers'][server]['name'] != '' else + config['Saved Servers'][server]['addr'] + ' ' + + str(config['Saved Servers'][server]['port'])), + h_align='left', + v_align='center', + corner_scale=t_scale, + maxwidth=(self._favorites_scroll_width / t_scale) * 0.93) + if i == 0: + ba.widget(edit=txt, up_widget=self._favorites_text) + ba.widget(edit=txt, + left_widget=self._favorites_connect_button, + right_widget=txt) + + # If there's no servers, allow selecting out of the scroll area + ba.containerwidget(edit=self._scrollwidget, + claims_left_right=bool(servers), + claims_up_down=bool(servers)) + ba.widget(edit=self._scrollwidget, + up_widget=self._favorites_text, + left_widget=self._favorites_connect_button) + + def on_deactivate(self) -> None: + self._access_check_timer = None + + def _connect(self, textwidget: ba.Widget, + port_textwidget: ba.Widget) -> None: + addr = cast(str, ba.textwidget(query=textwidget)) + if addr == '': + ba.screenmessage( + ba.Lstr(resource='internal.invalidAddressErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + try: + port = int(cast(str, ba.textwidget(query=port_textwidget))) + except ValueError: + port = -1 + if port > 65535 or port < 0: + ba.screenmessage(ba.Lstr(resource='internal.invalidPortErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + _HostLookupThread(name=addr, + port=port, + call=ba.WeakCall(self._host_lookup_result)).start() + + def _save_server(self, textwidget: ba.Widget, + port_textwidget: ba.Widget) -> None: + addr = cast(str, ba.textwidget(query=textwidget)) + if addr == '': + ba.screenmessage( + ba.Lstr(resource='internal.invalidAddressErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + try: + port = int(cast(str, ba.textwidget(query=port_textwidget))) + except ValueError: + port = -1 + if port > 65535 or port < 0: + ba.screenmessage(ba.Lstr(resource='internal.invalidPortErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + config = ba.app.config + + if addr: + if not isinstance(config.get('Saved Servers'), dict): + config['Saved Servers'] = {} + config['Saved Servers'][f'{addr}@{port}'] = { + 'addr': addr, + 'port': port, + 'name': addr + } + config.commit() + ba.playsound(ba.getsound('gunCocking')) + else: + ba.screenmessage('Invalid Address', color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + + def _host_lookup_result(self, resolved_address: Optional[str], + port: int) -> None: + if resolved_address is None: + ba.screenmessage( + ba.Lstr(resource='internal.unableToResolveHostText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + else: + # Store for later. + config = ba.app.config + config['Last Manual Party Connect Address'] = resolved_address + config.commit() + _ba.connect_to_party(resolved_address, port=port) + + def _run_addr_fetch(self) -> None: + try: + # FIXME: Update this to work with IPv6. + import socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.connect(('8.8.8.8', 80)) + val = sock.getsockname()[0] + sock.close() + ba.pushcall( + ba.Call( + _safe_set_text, + self._checking_state_text, + val, + ), + from_other_thread=True, + ) + except Exception as exc: + err_str = str(exc) + + # FIXME: Should look at exception types here, + # not strings. + if 'Network is unreachable' in err_str: + ba.pushcall(ba.Call( + _safe_set_text, self._checking_state_text, + ba.Lstr(resource='gatherWindow.' + 'noConnectionText'), False), + from_other_thread=True) + else: + ba.pushcall(ba.Call( + _safe_set_text, self._checking_state_text, + ba.Lstr(resource='gatherWindow.' + 'addressFetchErrorText'), False), + from_other_thread=True) + ba.pushcall(ba.Call(ba.print_error, + 'error in AddrFetchThread: ' + str(exc)), + from_other_thread=True) + + def _on_show_my_address_button_press(self, v2: float, + container: Optional[ba.Widget], + c_width: float) -> None: + if not container: + return + + tscl = 0.85 + tspc = 25 + + ba.playsound(ba.getsound('swish')) + ba.textwidget(parent=container, + position=(c_width * 0.5 - 10, v2), + color=(0.6, 1.0, 0.6), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'manualYourLocalAddressText')) + self._checking_state_text = ba.textwidget( + parent=container, + position=(c_width * 0.5, v2), + color=(0.5, 0.5, 0.5), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='left', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'checkingText')) + + threading.Thread(target=self._run_addr_fetch).start() + + v2 -= tspc + ba.textwidget(parent=container, + position=(c_width * 0.5 - 10, v2), + color=(0.6, 1.0, 0.6), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'manualYourAddressFromInternetText')) + + t_addr = ba.textwidget(parent=container, + position=(c_width * 0.5, v2), + color=(0.5, 0.5, 0.5), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + h_align='left', + v_align='center', + flatness=1.0, + text=ba.Lstr(resource='gatherWindow.' + 'checkingText')) + v2 -= tspc + ba.textwidget(parent=container, + position=(c_width * 0.5 - 10, v2), + color=(0.6, 1.0, 0.6), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='right', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'manualJoinableFromInternetText')) + + t_accessible = ba.textwidget(parent=container, + position=(c_width * 0.5, v2), + color=(0.5, 0.5, 0.5), + scale=tscl, + size=(0, 0), + maxwidth=c_width * 0.45, + flatness=1.0, + h_align='left', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'checkingText')) + v2 -= 28 + t_accessible_extra = ba.textwidget(parent=container, + position=(c_width * 0.5, v2), + color=(1, 0.5, 0.2), + scale=0.7, + size=(0, 0), + maxwidth=c_width * 0.9, + flatness=1.0, + h_align='center', + v_align='center', + text='') + + self._doing_access_check = False + self._access_check_count = 0 # Cap our refreshes eventually. + self._access_check_timer = ba.Timer( + 10.0, + ba.WeakCall(self._access_check_update, t_addr, t_accessible, + t_accessible_extra), + repeat=True, + timetype=ba.TimeType.REAL) + + # Kick initial off. + self._access_check_update(t_addr, t_accessible, t_accessible_extra) + if self._check_button: + self._check_button.delete() + + def _access_check_update(self, t_addr: ba.Widget, t_accessible: ba.Widget, + t_accessible_extra: ba.Widget) -> None: + from ba.internal import master_server_get + + # If we don't have an outstanding query, start one.. + assert self._doing_access_check is not None + assert self._access_check_count is not None + if not self._doing_access_check and self._access_check_count < 100: + self._doing_access_check = True + self._access_check_count += 1 + self._t_addr = t_addr + self._t_accessible = t_accessible + self._t_accessible_extra = t_accessible_extra + master_server_get('bsAccessCheck', {'b': ba.app.build_number}, + callback=ba.WeakCall( + self._on_accessible_response)) + + def _on_accessible_response(self, data: Optional[Dict[str, Any]]) -> None: + t_addr = self._t_addr + t_accessible = self._t_accessible + t_accessible_extra = self._t_accessible_extra + self._doing_access_check = False + color_bad = (1, 1, 0) + color_good = (0, 1, 0) + if data is None or 'address' not in data or 'accessible' not in data: + if t_addr: + ba.textwidget(edit=t_addr, + text=ba.Lstr(resource='gatherWindow.' + 'noConnectionText'), + color=color_bad) + if t_accessible: + ba.textwidget(edit=t_accessible, + text=ba.Lstr(resource='gatherWindow.' + 'noConnectionText'), + color=color_bad) + if t_accessible_extra: + ba.textwidget(edit=t_accessible_extra, + text='', + color=color_bad) + return + if t_addr: + ba.textwidget(edit=t_addr, text=data['address'], color=color_good) + if t_accessible: + if data['accessible']: + ba.textwidget(edit=t_accessible, + text=ba.Lstr(resource='gatherWindow.' + 'manualJoinableYesText'), + color=color_good) + if t_accessible_extra: + ba.textwidget(edit=t_accessible_extra, + text='', + color=color_good) + else: + ba.textwidget( + edit=t_accessible, + text=ba.Lstr(resource='gatherWindow.' + 'manualJoinableNoWithAsteriskText'), + color=color_bad, + ) + if t_accessible_extra: + ba.textwidget( + edit=t_accessible_extra, + text=ba.Lstr(resource='gatherWindow.' + 'manualRouterForwardingText', + subs=[('${PORT}', + str(_ba.get_game_port()))]), + color=color_bad, + ) diff --git a/dist/ba_data/python/bastd/ui/gather/nearbytab.py b/dist/ba_data/python/bastd/ui/gather/nearbytab.py new file mode 100644 index 0000000..19fe1c5 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/gather/nearbytab.py @@ -0,0 +1,146 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the nearby tab in the gather UI.""" + +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING + +import ba +import _ba +from bastd.ui.gather import GatherTab + +if TYPE_CHECKING: + from typing import Optional, Dict, Any + from bastd.ui.gather import GatherWindow + + +class NetScanner: + """Class for scanning for games on the lan.""" + + def __init__(self, tab: GatherTab, scrollwidget: ba.Widget, + tab_button: ba.Widget, width: float): + self._tab = weakref.ref(tab) + self._scrollwidget = scrollwidget + self._tab_button = tab_button + self._columnwidget = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0, + left_border=10) + ba.widget(edit=self._columnwidget, up_widget=tab_button) + self._width = width + self._last_selected_host: Optional[Dict[str, Any]] = None + + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self.update), + timetype=ba.TimeType.REAL, + repeat=True) + # Go ahead and run a few *almost* immediately so we don't + # have to wait a second. + self.update() + ba.timer(0.25, ba.WeakCall(self.update), timetype=ba.TimeType.REAL) + + def __del__(self) -> None: + _ba.end_host_scanning() + + def _on_select(self, host: Dict[str, Any]) -> None: + self._last_selected_host = host + + def _on_activate(self, host: Dict[str, Any]) -> None: + _ba.connect_to_party(host['address']) + + def update(self) -> None: + """(internal)""" + + # In case our UI was killed from under us. + if not self._columnwidget: + print(f'ERROR: NetScanner running without UI at time' + f' {ba.time(timetype=ba.TimeType.REAL)}.') + return + + t_scale = 1.6 + for child in self._columnwidget.get_children(): + child.delete() + + # Grab this now this since adding widgets will change it. + last_selected_host = self._last_selected_host + hosts = _ba.host_scan_cycle() + for i, host in enumerate(hosts): + txt3 = ba.textwidget(parent=self._columnwidget, + size=(self._width / t_scale, 30), + selectable=True, + color=(1, 1, 1), + on_select_call=ba.Call(self._on_select, host), + on_activate_call=ba.Call( + self._on_activate, host), + click_activate=True, + text=host['display_string'], + h_align='left', + v_align='center', + corner_scale=t_scale, + maxwidth=(self._width / t_scale) * 0.93) + if host == last_selected_host: + ba.containerwidget(edit=self._columnwidget, + selected_child=txt3, + visible_child=txt3) + if i == 0: + ba.widget(edit=txt3, up_widget=self._tab_button) + + +class NearbyGatherTab(GatherTab): + """The nearby tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._net_scanner: Optional[NetScanner] = None + self._container: Optional[ba.Widget] = None + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + c_width = region_width + c_height = region_height - 20 + sub_scroll_height = c_height - 85 + sub_scroll_width = 650 + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - c_height) * 0.5), + size=(c_width, c_height), + background=False, + selection_loops_to_parent=True) + v = c_height - 30 + ba.textwidget(parent=self._container, + position=(c_width * 0.5, v - 3), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(0, 0), + maxwidth=c_width * 0.9, + h_align='center', + v_align='center', + text=ba.Lstr(resource='gatherWindow.' + 'localNetworkDescriptionText')) + v -= 15 + v -= sub_scroll_height + 23 + scrollw = ba.scrollwidget(parent=self._container, + position=((region_width - sub_scroll_width) * + 0.5, v), + size=(sub_scroll_width, sub_scroll_height)) + + self._net_scanner = NetScanner(self, + scrollw, + tab_button, + width=sub_scroll_width) + + ba.widget(edit=scrollw, autoselect=True, up_widget=tab_button) + return self._container + + def on_deactivate(self) -> None: + self._net_scanner = None diff --git a/dist/ba_data/python/bastd/ui/gather/privatetab.py b/dist/ba_data/python/bastd/ui/gather/privatetab.py new file mode 100644 index 0000000..b14575d --- /dev/null +++ b/dist/ba_data/python/bastd/ui/gather/privatetab.py @@ -0,0 +1,870 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines the Private tab in the gather UI.""" + +from __future__ import annotations + +import os +import copy +import time +from enum import Enum +from dataclasses import dataclass, asdict +from typing import TYPE_CHECKING, cast + +import ba +import _ba +from efro.dataclasses import dataclass_from_dict +from bastd.ui.gather import GatherTab +from bastd.ui import getcurrency + +if TYPE_CHECKING: + from typing import Optional, Dict, Any, List, Type + from bastd.ui.gather import GatherWindow + +# Print a bit of info about queries, etc. +DEBUG_SERVER_COMMUNICATION = os.environ.get('BA_DEBUG_PPTABCOM') == '1' + + +class SubTabType(Enum): + """Available sub-tabs.""" + JOIN = 'join' + HOST = 'host' + + +@dataclass +class State: + """Our core state that persists while the app is running.""" + sub_tab: SubTabType = SubTabType.JOIN + + +@dataclass +class ConnectResult: + """Info about a server we get back when connecting.""" + error: Optional[str] = None + addr: Optional[str] = None + port: Optional[int] = None + + +@dataclass +class HostingState: + """Our combined state of whether we're hosting, whether we can, etc.""" + unavailable_error: Optional[str] = None + party_code: Optional[str] = None + able_to_host: bool = False + tickets_to_host_now: int = 0 + minutes_until_free_host: Optional[float] = None + free_host_minutes_remaining: Optional[float] = None + + +@dataclass +class HostingConfig: + """Config we provide when hosting.""" + session_type: str = 'ffa' + playlist_name: str = 'Unknown' + randomize: bool = False + tutorial: bool = False + custom_team_names: Optional[List[str]] = None + custom_team_colors: Optional[List[List[float]]] = None + playlist: Optional[List[Dict[str, Any]]] = None + + +class PrivateGatherTab(GatherTab): + """The private tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._container: Optional[ba.Widget] = None + self._state: State = State() + self._hostingstate = HostingState() + self._join_sub_tab_text: Optional[ba.Widget] = None + self._host_sub_tab_text: Optional[ba.Widget] = None + self._update_timer: Optional[ba.Timer] = None + self._join_party_code_text: Optional[ba.Widget] = None + self._c_width: float = 0.0 + self._c_height: float = 0.0 + self._last_hosting_state_query_time: Optional[float] = None + self._waiting_for_initial_state = True + self._waiting_for_start_stop_response = True + self._host_playlist_button: Optional[ba.Widget] = None + self._host_copy_button: Optional[ba.Widget] = None + self._host_connect_button: Optional[ba.Widget] = None + self._host_start_stop_button: Optional[ba.Widget] = None + self._get_tickets_button: Optional[ba.Widget] = None + self._ticket_count_text: Optional[ba.Widget] = None + self._showing_not_signed_in_screen = False + self._create_time = time.time() + self._last_action_send_time: Optional[float] = None + self._connect_press_time: Optional[float] = None + try: + self._hostingconfig = self._build_hosting_config() + except Exception: + ba.print_exception('Error building hosting config') + self._hostingconfig = HostingConfig() + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + self._c_width = region_width + self._c_height = region_height - 20 + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - self._c_height) * 0.5), + size=(self._c_width, self._c_height), + background=False, + selection_loops_to_parent=True) + v = self._c_height - 30.0 + self._join_sub_tab_text = ba.textwidget( + parent=self._container, + position=(self._c_width * 0.5 - 245, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='left', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_sub_tab( + SubTabType.JOIN, + playsound=True, + ), + text=ba.Lstr(resource='gatherWindow.privatePartyJoinText')) + self._host_sub_tab_text = ba.textwidget( + parent=self._container, + position=(self._c_width * 0.5 + 45, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='left', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_sub_tab( + SubTabType.HOST, + playsound=True, + ), + text=ba.Lstr(resource='gatherWindow.privatePartyHostText')) + ba.widget(edit=self._join_sub_tab_text, up_widget=tab_button) + ba.widget(edit=self._host_sub_tab_text, + left_widget=self._join_sub_tab_text, + up_widget=tab_button) + ba.widget(edit=self._join_sub_tab_text, + right_widget=self._host_sub_tab_text) + + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + repeat=True, + timetype=ba.TimeType.REAL) + + # Prevent taking any action until we've updated our state. + self._waiting_for_initial_state = True + + # This will get a state query sent out immediately. + self._last_action_send_time = None # Ensure we don't ignore response. + self._last_hosting_state_query_time = None + self._update() + + self._set_sub_tab(self._state.sub_tab) + + return self._container + + def _build_hosting_config(self) -> HostingConfig: + from bastd.ui.playlist import PlaylistTypeVars + from ba.internal import filter_playlist + hcfg = HostingConfig() + cfg = ba.app.config + sessiontypestr = cfg.get('Private Party Host Session Type', 'ffa') + if not isinstance(sessiontypestr, str): + raise RuntimeError(f'Invalid sessiontype {sessiontypestr}') + hcfg.session_type = sessiontypestr + + sessiontype: Type[ba.Session] + if hcfg.session_type == 'ffa': + sessiontype = ba.FreeForAllSession + elif hcfg.session_type == 'teams': + sessiontype = ba.DualTeamSession + else: + raise RuntimeError('fInvalid sessiontype: {hcfg.session_type}') + pvars = PlaylistTypeVars(sessiontype) + + playlist_name = ba.app.config.get( + f'{pvars.config_name} Playlist Selection') + if not isinstance(playlist_name, str): + playlist_name = '__default__' + hcfg.playlist_name = (pvars.default_list_name.evaluate() + if playlist_name == '__default__' else + playlist_name) + + if playlist_name == '__default__': + playlist = pvars.get_default_list_call() + else: + playlist = cfg[f'{pvars.config_name} Playlists'][playlist_name] + hcfg.playlist = filter_playlist(playlist, sessiontype) + + randomize = cfg.get(f'{pvars.config_name} Playlist Randomize') + if not isinstance(randomize, bool): + randomize = False + hcfg.randomize = randomize + + tutorial = cfg.get('Show Tutorial') + if not isinstance(tutorial, bool): + tutorial = False + hcfg.tutorial = tutorial + + if hcfg.session_type == 'teams': + hcfg.custom_team_names = copy.copy(cfg.get('Custom Team Names')) + hcfg.custom_team_colors = copy.copy(cfg.get('Custom Team Colors')) + + return hcfg + + def on_deactivate(self) -> None: + self._update_timer = None + + def _update_currency_ui(self) -> None: + # Keep currency count up to date if applicable. + try: + t_str = str(_ba.get_account_ticket_count()) + except Exception: + t_str = '?' + if self._get_tickets_button: + ba.buttonwidget(edit=self._get_tickets_button, + label=ba.charstr(ba.SpecialChar.TICKET) + t_str) + if self._ticket_count_text: + ba.textwidget(edit=self._ticket_count_text, + text=ba.charstr(ba.SpecialChar.TICKET) + t_str) + + def _update(self) -> None: + """Periodic updating.""" + + now = ba.time(ba.TimeType.REAL) + + self._update_currency_ui() + + if self._state.sub_tab is SubTabType.HOST: + + # If we're not signed in, just refresh to show that. + if (_ba.get_account_state() != 'signed_in' + and self._showing_not_signed_in_screen): + self._refresh_sub_tab() + else: + + # Query an updated state periodically. + if (self._last_hosting_state_query_time is None + or now - self._last_hosting_state_query_time > 15.0): + self._debug_server_comm('querying private party state') + if _ba.get_account_state() == 'signed_in': + _ba.add_transaction( + {'type': 'PRIVATE_PARTY_QUERY'}, + callback=ba.WeakCall( + self._hosting_state_idle_response), + ) + _ba.run_transactions() + else: + self._hosting_state_idle_response(None) + self._last_hosting_state_query_time = now + + def _hosting_state_idle_response(self, + result: Optional[Dict[str, Any]]) -> None: + + # This simply passes through to our standard response handler. + # The one exception is if we've recently sent an action to the + # server (start/stop hosting/etc.) In that case we want to ignore + # idle background updates and wait for the response to our action. + # (this keeps the button showing 'one moment...' until the change + # takes effect, etc.) + if (self._last_action_send_time is not None + and time.time() - self._last_action_send_time < 5.0): + self._debug_server_comm('ignoring private party state response' + ' due to recent action') + return + self._hosting_state_response(result) + + def _hosting_state_response(self, result: Optional[Dict[str, + Any]]) -> None: + + # Its possible for this to come back to us after our UI is dead; + # ignore in that case. + if not self._container: + return + + state: Optional[HostingState] = None + if result is not None: + self._debug_server_comm('got private party state response') + try: + state = dataclass_from_dict(HostingState, result) + except Exception: + pass + else: + self._debug_server_comm('private party state response errored') + + # Hmm I guess let's just ignore failed responses?... + # Or should we show some sort of error state to the user?... + if result is None or state is None: + return + + self._waiting_for_initial_state = False + self._waiting_for_start_stop_response = False + self._hostingstate = state + self._refresh_sub_tab() + + def _set_sub_tab(self, value: SubTabType, playsound: bool = False) -> None: + assert self._container + if playsound: + ba.playsound(ba.getsound('click01')) + + # If switching from join to host, do a fresh state query. + if self._state.sub_tab is SubTabType.JOIN and value is SubTabType.HOST: + # Prevent taking any action until we've gotten a fresh state. + self._waiting_for_initial_state = True + + # This will get a state query sent out immediately. + self._last_hosting_state_query_time = None + self._last_action_send_time = None # So we don't ignore response. + self._update() + + self._state.sub_tab = value + active_color = (0.6, 1.0, 0.6) + inactive_color = (0.5, 0.4, 0.5) + ba.textwidget( + edit=self._join_sub_tab_text, + color=active_color if value is SubTabType.JOIN else inactive_color) + ba.textwidget( + edit=self._host_sub_tab_text, + color=active_color if value is SubTabType.HOST else inactive_color) + + self._refresh_sub_tab() + + # Kick off an update to get any needed messages sent/etc. + ba.pushcall(self._update) + + def _selwidgets(self) -> List[Optional[ba.Widget]]: + """An indexed list of widgets we can use for saving/restoring sel.""" + return [ + self._host_playlist_button, self._host_copy_button, + self._host_connect_button, self._host_start_stop_button, + self._get_tickets_button + ] + + def _refresh_sub_tab(self) -> None: + assert self._container + + # Store an index for our current selection so we can + # reselect the equivalent recreated widget if possible. + selindex: Optional[int] = None + selchild = self._container.get_selected_child() + if selchild is not None: + try: + selindex = self._selwidgets().index(selchild) + except ValueError: + pass + + # Clear anything existing in the old sub-tab. + for widget in self._container.get_children(): + if widget and widget not in { + self._host_sub_tab_text, + self._join_sub_tab_text, + }: + widget.delete() + + if self._state.sub_tab is SubTabType.JOIN: + self._build_join_tab() + elif self._state.sub_tab is SubTabType.HOST: + self._build_host_tab() + else: + raise RuntimeError('Invalid state.') + + # Select the new equivalent widget if there is one. + if selindex is not None: + selwidget = self._selwidgets()[selindex] + if selwidget: + ba.containerwidget(edit=self._container, + selected_child=selwidget) + + def _build_join_tab(self) -> None: + + ba.textwidget(parent=self._container, + position=(self._c_width * 0.5, self._c_height - 140), + color=(0.5, 0.46, 0.5), + scale=1.5, + size=(0, 0), + maxwidth=250, + h_align='center', + v_align='center', + text=ba.Lstr(resource='gatherWindow.partyCodeText')) + + self._join_party_code_text = ba.textwidget( + parent=self._container, + position=(self._c_width * 0.5 - 150, self._c_height - 250), + flatness=1.0, + scale=1.5, + size=(300, 50), + editable=True, + description=ba.Lstr(resource='gatherWindow.partyCodeText'), + autoselect=True, + maxwidth=250, + h_align='left', + v_align='center', + text='') + btn = ba.buttonwidget(parent=self._container, + size=(300, 70), + label=ba.Lstr(resource='gatherWindow.' + 'manualConnectText'), + position=(self._c_width * 0.5 - 150, + self._c_height - 350), + on_activate_call=self._join_connect_press, + autoselect=True) + ba.textwidget(edit=self._join_party_code_text, + on_return_press_call=btn.activate) + + def _on_get_tickets_press(self) -> None: + + if self._waiting_for_start_stop_response: + return + + # Bring up get-tickets window and then kill ourself (we're on the + # overlay layer so we'd show up above it). + getcurrency.GetCurrencyWindow(modal=True, + origin_widget=self._get_tickets_button) + + def _build_host_tab(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + + if _ba.get_account_state() != 'signed_in': + ba.textwidget(parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=200, + scale=0.8, + color=(0.6, 0.56, 0.6), + position=(self._c_width * 0.5, self._c_height * 0.5), + text=ba.Lstr(resource='notSignedInErrorText')) + self._showing_not_signed_in_screen = True + return + self._showing_not_signed_in_screen = False + + # At first we don't want to show anything until we've gotten a state. + # Update: In this situation we now simply show our existing state + # but give the start/stop button a loading message and disallow its + # use. This keeps things a lot less jumpy looking and allows selecting + # playlists/etc without having to wait for the server each time + # back to the ui. + if self._waiting_for_initial_state and bool(False): + ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=200, + scale=0.8, + color=(0.6, 0.56, 0.6), + position=(self._c_width * 0.5, self._c_height * 0.5), + text=ba.Lstr( + value='${A}...', + subs=[('${A}', ba.Lstr(resource='store.loadingText'))], + ), + ) + return + + # If we're not currently hosting and hosting requires tickets, + # Show our count (possibly with a link to purchase more). + if (not self._waiting_for_initial_state + and self._hostingstate.party_code is None + and self._hostingstate.tickets_to_host_now != 0): + if not ba.app.ui.use_toolbars: + if ba.app.allow_ticket_purchases: + self._get_tickets_button = ba.buttonwidget( + parent=self._container, + position=(self._c_width - 210 + 125, + self._c_height - 44), + autoselect=True, + scale=0.6, + size=(120, 60), + textcolor=(0.2, 1, 0.2), + label=ba.charstr(ba.SpecialChar.TICKET), + color=(0.65, 0.5, 0.8), + on_activate_call=self._on_get_tickets_press) + else: + self._ticket_count_text = ba.textwidget( + parent=self._container, + scale=0.6, + position=(self._c_width - 210 + 125, + self._c_height - 44), + color=(0.2, 1, 0.2), + h_align='center', + v_align='center') + # Set initial ticket count. + self._update_currency_ui() + + v = self._c_height - 90 + if self._hostingstate.party_code is None: + ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=self._c_width * 0.9, + scale=0.7, + flatness=1.0, + color=(0.5, 0.46, 0.5), + position=(self._c_width * 0.5, v), + text=ba.Lstr( + resource='gatherWindow.privatePartyCloudDescriptionText')) + + v -= 100 + if self._hostingstate.party_code is None: + # We've got no current party running; show options to set one up. + ba.textwidget(parent=self._container, + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=200, + scale=0.8, + color=(0.6, 0.56, 0.6), + position=(self._c_width * 0.5 - 210, v), + text=ba.Lstr(resource='playlistText')) + self._host_playlist_button = ba.buttonwidget( + parent=self._container, + size=(400, 70), + color=(0.6, 0.5, 0.6), + textcolor=(0.8, 0.75, 0.8), + label=self._hostingconfig.playlist_name, + on_activate_call=self._playlist_press, + position=(self._c_width * 0.5 - 200, v - 35), + up_widget=self._host_sub_tab_text, + autoselect=True) + + # If it appears we're coming back from playlist selection, + # re-select our playlist button. + if ba.app.ui.selecting_private_party_playlist: + ba.containerwidget(edit=self._container, + selected_child=self._host_playlist_button) + ba.app.ui.selecting_private_party_playlist = False + else: + # We've got a current party; show its info. + ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=600, + scale=0.9, + color=(0.7, 0.64, 0.7), + position=(self._c_width * 0.5, v + 90), + text=ba.Lstr(resource='gatherWindow.partyServerRunningText')) + ba.textwidget(parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=600, + scale=0.7, + color=(0.7, 0.64, 0.7), + position=(self._c_width * 0.5, v + 50), + text=ba.Lstr(resource='gatherWindow.partyCodeText')) + ba.textwidget(parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + scale=2.0, + color=(0.0, 1.0, 0.0), + position=(self._c_width * 0.5, v + 10), + text=self._hostingstate.party_code) + + # Also action buttons to copy it and connect to it. + if ba.clipboard_is_supported(): + cbtnoffs = 10 + self._host_copy_button = ba.buttonwidget( + parent=self._container, + size=(140, 40), + color=(0.6, 0.5, 0.6), + textcolor=(0.8, 0.75, 0.8), + label=ba.Lstr(resource='gatherWindow.copyCodeText'), + on_activate_call=self._host_copy_press, + position=(self._c_width * 0.5 - 150, v - 70), + autoselect=True) + else: + cbtnoffs = -70 + self._host_connect_button = ba.buttonwidget( + parent=self._container, + size=(140, 40), + color=(0.6, 0.5, 0.6), + textcolor=(0.8, 0.75, 0.8), + label=ba.Lstr(resource='gatherWindow.manualConnectText'), + on_activate_call=self._host_connect_press, + position=(self._c_width * 0.5 + cbtnoffs, v - 70), + autoselect=True) + + v -= 120 + + # Line above the main action button: + + # If we don't want to show anything until we get a state: + if self._waiting_for_initial_state: + pass + elif self._hostingstate.unavailable_error is not None: + # If hosting is unavailable, show the associated reason. + ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=self._c_width * 0.9, + scale=0.7, + flatness=1.0, + color=(1.0, 0.0, 0.0), + position=(self._c_width * 0.5, v), + text=ba.Lstr(translate=('serverResponses', + self._hostingstate.unavailable_error))) + elif self._hostingstate.free_host_minutes_remaining is not None: + # If we've been pre-approved to start/stop for free, show that. + ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=self._c_width * 0.9, + scale=0.7, + flatness=1.0, + color=((0.7, 0.64, 0.7) if self._hostingstate.party_code else + (0.0, 1.0, 0.0)), + position=(self._c_width * 0.5, v), + text=ba.Lstr( + resource='gatherWindow.startStopHostingMinutesText', + subs=[( + '${MINUTES}', + f'{self._hostingstate.free_host_minutes_remaining:.0f}' + )])) + else: + # Otherwise tell whether the free cloud server is available + # or will be at some point. + if self._hostingstate.party_code is None: + if self._hostingstate.tickets_to_host_now == 0: + ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=self._c_width * 0.9, + scale=0.7, + flatness=1.0, + color=(0.0, 1.0, 0.0), + position=(self._c_width * 0.5, v), + text=ba.Lstr( + resource= + 'gatherWindow.freeCloudServerAvailableNowText')) + else: + if self._hostingstate.minutes_until_free_host is None: + ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=self._c_width * 0.9, + scale=0.7, + flatness=1.0, + color=(1.0, 0.6, 0.0), + position=(self._c_width * 0.5, v), + text=ba.Lstr( + resource= + 'gatherWindow.freeCloudServerNotAvailableText') + ) + else: + availmins = self._hostingstate.minutes_until_free_host + ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=self._c_width * 0.9, + scale=0.7, + flatness=1.0, + color=(1.0, 0.6, 0.0), + position=(self._c_width * 0.5, v), + text=ba.Lstr(resource='gatherWindow.' + 'freeCloudServerAvailableMinutesText', + subs=[('${MINUTES}', + f'{availmins:.0f}')])) + + v -= 100 + + if (self._waiting_for_start_stop_response + or self._waiting_for_initial_state): + btnlabel = ba.Lstr(resource='oneMomentText') + else: + if self._hostingstate.unavailable_error is not None: + btnlabel = ba.Lstr( + resource='gatherWindow.hostingUnavailableText') + elif self._hostingstate.party_code is None: + ticon = _ba.charstr(ba.SpecialChar.TICKET) + nowtickets = self._hostingstate.tickets_to_host_now + if nowtickets > 0: + btnlabel = ba.Lstr( + resource='gatherWindow.startHostingPaidText', + subs=[('${COST}', f'{ticon}{nowtickets}')]) + else: + btnlabel = ba.Lstr( + resource='gatherWindow.startHostingText') + else: + btnlabel = ba.Lstr(resource='gatherWindow.stopHostingText') + + disabled = (self._hostingstate.unavailable_error is not None + or self._waiting_for_initial_state) + waiting = self._waiting_for_start_stop_response + self._host_start_stop_button = ba.buttonwidget( + parent=self._container, + size=(400, 80), + color=((0.6, 0.6, 0.6) if disabled else + (0.5, 1.0, 0.5) if waiting else None), + enable_sound=False, + label=btnlabel, + textcolor=((0.7, 0.7, 0.7) if disabled else None), + position=(self._c_width * 0.5 - 200, v), + on_activate_call=self._start_stop_button_press, + autoselect=True) + + def _playlist_press(self) -> None: + assert self._host_playlist_button is not None + self.window.playlist_select(origin_widget=self._host_playlist_button) + + def _host_copy_press(self) -> None: + assert self._hostingstate.party_code is not None + ba.clipboard_set_text(self._hostingstate.party_code) + ba.screenmessage(ba.Lstr(resource='gatherWindow.copyCodeConfirmText')) + + def _host_connect_press(self) -> None: + assert self._hostingstate.party_code is not None + self._connect_to_party_code(self._hostingstate.party_code) + + def _debug_server_comm(self, msg: str) -> None: + if DEBUG_SERVER_COMMUNICATION: + print(f'PPTABCOM: {msg} at time ' + f'{time.time()-self._create_time:.2f}') + + def _connect_to_party_code(self, code: str) -> None: + + # Ignore attempted followup sends for a few seconds. + # (this will reset if we get a response) + now = time.time() + if (self._connect_press_time is not None + and now - self._connect_press_time < 5.0): + self._debug_server_comm( + 'not sending private party connect (too soon)') + return + self._connect_press_time = now + + self._debug_server_comm('sending private party connect') + _ba.add_transaction( + { + 'type': 'PRIVATE_PARTY_CONNECT', + 'code': code + }, + callback=ba.WeakCall(self._connect_response), + ) + _ba.run_transactions() + + def _start_stop_button_press(self) -> None: + if (self._waiting_for_start_stop_response + or self._waiting_for_initial_state): + return + + if _ba.get_account_state() != 'signed_in': + ba.screenmessage(ba.Lstr(resource='notSignedInErrorText')) + ba.playsound(ba.getsound('error')) + self._refresh_sub_tab() + return + + if self._hostingstate.unavailable_error is not None: + ba.playsound(ba.getsound('error')) + return + + # If we're not hosting, start. + if self._hostingstate.party_code is None: + + # If there's a ticket cost, make sure we have enough tickets. + if self._hostingstate.tickets_to_host_now > 0: + ticket_count: Optional[int] + try: + ticket_count = _ba.get_account_ticket_count() + except Exception: + # FIXME: should add a ba.NotSignedInError we can use here. + ticket_count = None + ticket_cost = self._hostingstate.tickets_to_host_now + if ticket_count is not None and ticket_count < ticket_cost: + getcurrency.show_get_tickets_prompt() + ba.playsound(ba.getsound('error')) + return + self._last_action_send_time = time.time() + _ba.add_transaction( + { + 'type': 'PRIVATE_PARTY_START', + 'config': asdict(self._hostingconfig) + }, + callback=ba.WeakCall(self._hosting_state_response)) + _ba.run_transactions() + + else: + self._last_action_send_time = time.time() + _ba.add_transaction({'type': 'PRIVATE_PARTY_STOP'}, + callback=ba.WeakCall( + self._hosting_state_response)) + _ba.run_transactions() + ba.playsound(ba.getsound('click01')) + + self._waiting_for_start_stop_response = True + self._refresh_sub_tab() + + def _join_connect_press(self) -> None: + + # Error immediately if its an empty code. + code: Optional[str] = None + if self._join_party_code_text: + code = cast(str, ba.textwidget(query=self._join_party_code_text)) + if not code: + ba.screenmessage( + ba.Lstr(resource='internal.invalidAddressErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + self._connect_to_party_code(code) + + def _connect_response(self, result: Optional[Dict[str, Any]]) -> None: + try: + self._connect_press_time = None + if result is None: + raise RuntimeError() + cresult = dataclass_from_dict(ConnectResult, result) + if cresult.error is not None: + self._debug_server_comm('got error connect response') + ba.screenmessage( + ba.Lstr(translate=('serverResponses', cresult.error)), + (1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + self._debug_server_comm('got valid connect response') + assert cresult.addr is not None and cresult.port is not None + _ba.connect_to_party(cresult.addr, port=cresult.port) + except Exception: + self._debug_server_comm('got connect response error') + ba.playsound(ba.getsound('error')) + + def save_state(self) -> None: + ba.app.ui.window_states[type(self)] = copy.deepcopy(self._state) + + def restore_state(self) -> None: + state = ba.app.ui.window_states.get(type(self)) + if state is None: + state = State() + assert isinstance(state, State) + self._state = state diff --git a/dist/ba_data/python/bastd/ui/gather/publictab.py b/dist/ba_data/python/bastd/ui/gather/publictab.py new file mode 100644 index 0000000..b1a326b --- /dev/null +++ b/dist/ba_data/python/bastd/ui/gather/publictab.py @@ -0,0 +1,1275 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=too-many-lines +"""Defines the public tab in the gather UI.""" + +from __future__ import annotations + +import copy +import time +import threading +from enum import Enum +from dataclasses import dataclass +from typing import TYPE_CHECKING, cast + +import _ba +import ba +from bastd.ui.gather import GatherTab + +if TYPE_CHECKING: + from typing import Callable, Any, Optional, Dict, Union, Tuple, List + from bastd.ui.gather import GatherWindow + +# Print a bit of info about pings, queries, etc. +DEBUG_SERVER_COMMUNICATION = False +DEBUG_PROCESSING = False + + +class SubTabType(Enum): + """Available sub-tabs.""" + JOIN = 'join' + HOST = 'host' + + +@dataclass +class PartyEntry: + """Info about a public party.""" + address: str + index: int + queue: Optional[str] = None + port: int = -1 + name: str = '' + size: int = -1 + size_max: int = -1 + claimed: bool = False + ping: Optional[float] = None + ping_interval: float = -1.0 + next_ping_time: float = -1.0 + ping_attempts: int = 0 + ping_responses: int = 0 + stats_addr: Optional[str] = None + clean_display_index: Optional[int] = None + + def get_key(self) -> str: + """Return the key used to store this party.""" + return f'{self.address}_{self.port}' + + +class UIRow: + """Wrangles UI for a row in the party list.""" + + def __init__(self) -> None: + self._name_widget: Optional[ba.Widget] = None + self._size_widget: Optional[ba.Widget] = None + self._ping_widget: Optional[ba.Widget] = None + self._stats_button: Optional[ba.Widget] = None + + def __del__(self) -> None: + self._clear() + + def _clear(self) -> None: + for widget in [ + self._name_widget, self._size_widget, self._ping_widget, + self._stats_button + ]: + if widget: + widget.delete() + + def update(self, index: int, party: PartyEntry, sub_scroll_width: float, + sub_scroll_height: float, lineheight: float, + columnwidget: ba.Widget, join_text: ba.Widget, + filter_text: ba.Widget, existing_selection: Optional[Selection], + tab: PublicGatherTab) -> None: + """Update for the given data.""" + # pylint: disable=too-many-locals + + # Quick-out: if we've been marked clean for a certain index and + # we're still at that index, we're done. + if party.clean_display_index == index: + return + + ping_good = _ba.get_account_misc_read_val('pingGood', 100) + ping_med = _ba.get_account_misc_read_val('pingMed', 500) + + self._clear() + hpos = 20 + vpos = sub_scroll_height - lineheight * index - 50 + self._name_widget = ba.textwidget( + text=ba.Lstr(value=party.name), + parent=columnwidget, + size=(sub_scroll_width * 0.63, 20), + position=(0 + hpos, 4 + vpos), + selectable=True, + on_select_call=ba.WeakCall( + tab.set_public_party_selection, + Selection(party.get_key(), SelectionComponent.NAME)), + on_activate_call=ba.WeakCall(tab.on_public_party_activate, party), + click_activate=True, + maxwidth=sub_scroll_width * 0.45, + corner_scale=1.4, + autoselect=True, + color=(1, 1, 1, 0.3 if party.ping is None else 1.0), + h_align='left', + v_align='center') + ba.widget(edit=self._name_widget, + left_widget=join_text, + show_buffer_top=64.0, + show_buffer_bottom=64.0) + if existing_selection == Selection(party.get_key(), + SelectionComponent.NAME): + ba.containerwidget(edit=columnwidget, + selected_child=self._name_widget) + if party.stats_addr: + url = party.stats_addr.replace( + '${ACCOUNT}', + _ba.get_account_misc_read_val_2('resolvedAccountID', + 'UNKNOWN')) + self._stats_button = ba.buttonwidget( + color=(0.3, 0.6, 0.94), + textcolor=(1.0, 1.0, 1.0), + label=ba.Lstr(resource='statsText'), + parent=columnwidget, + autoselect=True, + on_activate_call=ba.Call(ba.open_url, url), + on_select_call=ba.WeakCall( + tab.set_public_party_selection, + Selection(party.get_key(), + SelectionComponent.STATS_BUTTON)), + size=(120, 40), + position=(sub_scroll_width * 0.66 + hpos, 1 + vpos), + scale=0.9) + if existing_selection == Selection( + party.get_key(), SelectionComponent.STATS_BUTTON): + ba.containerwidget(edit=columnwidget, + selected_child=self._stats_button) + + self._size_widget = ba.textwidget( + text=str(party.size) + '/' + str(party.size_max), + parent=columnwidget, + size=(0, 0), + position=(sub_scroll_width * 0.86 + hpos, 20 + vpos), + scale=0.7, + color=(0.8, 0.8, 0.8), + h_align='right', + v_align='center') + + if index == 0: + ba.widget(edit=self._name_widget, up_widget=filter_text) + if self._stats_button: + ba.widget(edit=self._stats_button, up_widget=filter_text) + + self._ping_widget = ba.textwidget(parent=columnwidget, + size=(0, 0), + position=(sub_scroll_width * 0.94 + + hpos, 20 + vpos), + scale=0.7, + h_align='right', + v_align='center') + if party.ping is None: + ba.textwidget(edit=self._ping_widget, + text='-', + color=(0.5, 0.5, 0.5)) + else: + ba.textwidget(edit=self._ping_widget, + text=str(int(party.ping)), + color=(0, 1, 0) if party.ping <= ping_good else + (1, 1, 0) if party.ping <= ping_med else (1, 0, 0)) + + party.clean_display_index = index + + +@dataclass +class State: + """State saved/restored only while the app is running.""" + sub_tab: SubTabType = SubTabType.JOIN + parties: Optional[List[Tuple[str, PartyEntry]]] = None + next_entry_index: int = 0 + filter_value: str = '' + have_server_list_response: bool = False + have_valid_server_list: bool = False + + +class SelectionComponent(Enum): + """Describes what part of an entry is selected.""" + NAME = 'name' + STATS_BUTTON = 'stats_button' + + +@dataclass +class Selection: + """Describes the currently selected list element.""" + entry_key: str + component: SelectionComponent + + +class AddrFetchThread(threading.Thread): + """Thread for fetching an address in the bg.""" + + def __init__(self, call: Callable[[Any], Any]): + super().__init__() + self._call = call + + def run(self) -> None: + try: + # FIXME: Update this to work with IPv6 at some point. + import socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.connect(('8.8.8.8', 80)) + val = sock.getsockname()[0] + sock.close() + ba.pushcall(ba.Call(self._call, val), from_other_thread=True) + except Exception as exc: + # Ignore expected network errors; log others. + import errno + if isinstance(exc, OSError) and exc.errno == errno.ENETUNREACH: + pass + else: + ba.print_exception() + + +class PingThread(threading.Thread): + """Thread for sending out game pings.""" + + def __init__(self, address: str, port: int, + call: Callable[[str, int, Optional[float]], Optional[int]]): + super().__init__() + self._address = address + self._port = port + self._call = call + + def run(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + ba.app.ping_thread_count += 1 + sock: Optional[socket.socket] = None + try: + import socket + from ba.internal import get_ip_address_type + socket_type = get_ip_address_type(self._address) + sock = socket.socket(socket_type, socket.SOCK_DGRAM) + sock.connect((self._address, self._port)) + + accessible = False + starttime = time.time() + + # Send a few pings and wait a second for + # a response. + sock.settimeout(1) + for _i in range(3): + sock.send(b'\x0b') + result: Optional[bytes] + try: + # 11: BA_PACKET_SIMPLE_PING + result = sock.recv(10) + except Exception: + result = None + if result == b'\x0c': + # 12: BA_PACKET_SIMPLE_PONG + accessible = True + break + time.sleep(1) + ping = (time.time() - starttime) * 1000.0 + ba.pushcall(ba.Call(self._call, self._address, self._port, + ping if accessible else None), + from_other_thread=True) + except ConnectionRefusedError: + # Fine, server; sorry we pinged you. Hmph. + pass + except OSError as exc: + import errno + + # Ignore harmless errors. + if exc.errno in { + errno.EHOSTUNREACH, errno.ENETUNREACH, errno.EINVAL, + errno.EPERM, errno.EACCES + }: + pass + elif exc.errno == 10022: + # Windows 'invalid argument' error. + pass + elif exc.errno == 10051: + # Windows 'a socket operation was attempted + # to an unreachable network' error. + pass + elif exc.errno == errno.EADDRNOTAVAIL: + if self._port == 0: + # This has happened. Ignore. + pass + elif ba.do_once(): + print(f'Got EADDRNOTAVAIL on gather ping' + f' for addr {self._address}' + f' port {self._port}.') + else: + ba.print_exception( + f'Error on gather ping ' + f'(errno={exc.errno})', once=True) + except Exception: + ba.print_exception('Error on gather ping', once=True) + finally: + try: + if sock is not None: + sock.close() + except Exception: + ba.print_exception('Error on gather ping cleanup', once=True) + + ba.app.ping_thread_count -= 1 + + +class PublicGatherTab(GatherTab): + """The public tab in the gather UI""" + + def __init__(self, window: GatherWindow) -> None: + super().__init__(window) + self._container: Optional[ba.Widget] = None + self._join_text: Optional[ba.Widget] = None + self._host_text: Optional[ba.Widget] = None + self._filter_text: Optional[ba.Widget] = None + self._local_address: Optional[str] = None + self._last_connect_attempt_time: Optional[float] = None + self._sub_tab: SubTabType = SubTabType.JOIN + self._selection: Optional[Selection] = None + self._refreshing_list = False + self._update_timer: Optional[ba.Timer] = None + self._host_scrollwidget: Optional[ba.Widget] = None + self._host_name_text: Optional[ba.Widget] = None + self._host_toggle_button: Optional[ba.Widget] = None + self._last_server_list_query_time: Optional[float] = None + self._join_list_column: Optional[ba.Widget] = None + self._join_status_text: Optional[ba.Widget] = None + self._host_max_party_size_value: Optional[ba.Widget] = None + self._host_max_party_size_minus_button: (Optional[ba.Widget]) = None + self._host_max_party_size_plus_button: (Optional[ba.Widget]) = None + self._host_status_text: Optional[ba.Widget] = None + self._signed_in = False + self._ui_rows: List[UIRow] = [] + self._refresh_ui_row = 0 + self._have_user_selected_row = False + self._first_valid_server_list_time: Optional[float] = None + + # Parties indexed by id: + self._parties: Dict[str, PartyEntry] = {} + + # Parties sorted in display order: + self._parties_sorted: List[Tuple[str, PartyEntry]] = [] + self._party_lists_dirty = True + + # Sorted parties with filter applied: + self._parties_displayed: Dict[str, PartyEntry] = {} + + self._next_entry_index = 0 + self._have_server_list_response = False + self._have_valid_server_list = False + self._filter_value = '' + self._pending_party_infos: List[Dict[str, Any]] = [] + self._last_sub_scroll_height = 0.0 + + def on_activate( + self, + parent_widget: ba.Widget, + tab_button: ba.Widget, + region_width: float, + region_height: float, + region_left: float, + region_bottom: float, + ) -> ba.Widget: + c_width = region_width + c_height = region_height - 20 + self._container = ba.containerwidget( + parent=parent_widget, + position=(region_left, + region_bottom + (region_height - c_height) * 0.5), + size=(c_width, c_height), + background=False, + selection_loops_to_parent=True) + v = c_height - 30 + self._join_text = ba.textwidget( + parent=self._container, + position=(c_width * 0.5 - 245, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='left', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_sub_tab( + SubTabType.JOIN, + region_width, + region_height, + playsound=True, + ), + text=ba.Lstr(resource='gatherWindow.' + 'joinPublicPartyDescriptionText')) + self._host_text = ba.textwidget( + parent=self._container, + position=(c_width * 0.5 + 45, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='left', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_sub_tab( + SubTabType.HOST, + region_width, + region_height, + playsound=True, + ), + text=ba.Lstr(resource='gatherWindow.' + 'hostPublicPartyDescriptionText')) + ba.widget(edit=self._join_text, up_widget=tab_button) + ba.widget(edit=self._host_text, + left_widget=self._join_text, + up_widget=tab_button) + ba.widget(edit=self._join_text, right_widget=self._host_text) + + # Attempt to fetch our local address so we have it for error messages. + if self._local_address is None: + AddrFetchThread(ba.WeakCall(self._fetch_local_addr_cb)).start() + + self._set_sub_tab(self._sub_tab, region_width, region_height) + self._update_timer = ba.Timer(0.1, + ba.WeakCall(self._update), + repeat=True, + timetype=ba.TimeType.REAL) + return self._container + + def on_deactivate(self) -> None: + self._update_timer = None + + def save_state(self) -> None: + + # Save off a small number of parties with the lowest ping; we'll + # display these immediately when our UI comes back up which should + # be enough to make things feel nice and crisp while we do a full + # server re-query or whatnot. + ba.app.ui.window_states[type(self)] = State( + sub_tab=self._sub_tab, + parties=[(i, copy.copy(p)) for i, p in self._parties_sorted[:40]], + next_entry_index=self._next_entry_index, + filter_value=self._filter_value, + have_server_list_response=self._have_server_list_response, + have_valid_server_list=self._have_valid_server_list) + + def restore_state(self) -> None: + state = ba.app.ui.window_states.get(type(self)) + if state is None: + state = State() + assert isinstance(state, State) + self._sub_tab = state.sub_tab + + # Restore the parties we stored. + if state.parties: + self._parties = { + key: copy.copy(party) + for key, party in state.parties + } + self._parties_sorted = list(self._parties.items()) + self._party_lists_dirty = True + + self._next_entry_index = state.next_entry_index + + # FIXME: should save/restore these too?.. + self._have_server_list_response = state.have_server_list_response + self._have_valid_server_list = state.have_valid_server_list + self._filter_value = state.filter_value + + def _set_sub_tab(self, + value: SubTabType, + region_width: float, + region_height: float, + playsound: bool = False) -> None: + assert self._container + if playsound: + ba.playsound(ba.getsound('click01')) + + # Reset our selection. + # (prevents selecting something way down the list if we switched away + # and came back) + self._selection = None + self._have_user_selected_row = False + + # Reset refresh to the top and make sure everything refreshes. + self._refresh_ui_row = 0 + for party in self._parties.values(): + party.clean_display_index = None + + self._sub_tab = value + active_color = (0.6, 1.0, 0.6) + inactive_color = (0.5, 0.4, 0.5) + ba.textwidget( + edit=self._join_text, + color=active_color if value is SubTabType.JOIN else inactive_color) + ba.textwidget( + edit=self._host_text, + color=active_color if value is SubTabType.HOST else inactive_color) + + # Clear anything existing in the old sub-tab. + for widget in self._container.get_children(): + if widget and widget not in {self._host_text, self._join_text}: + widget.delete() + + if value is SubTabType.JOIN: + self._build_join_tab(region_width, region_height) + + if value is SubTabType.HOST: + self._build_host_tab(region_width, region_height) + + def _build_join_tab(self, region_width: float, + region_height: float) -> None: + c_width = region_width + c_height = region_height - 20 + sub_scroll_height = c_height - 125 + sub_scroll_width = 830 + v = c_height - 35 + v -= 60 + filter_txt = ba.Lstr(resource='filterText') + self._filter_text = ba.textwidget(parent=self._container, + text=self._filter_value, + size=(350, 45), + position=(290, v - 10), + h_align='left', + v_align='center', + editable=True, + description=filter_txt) + ba.widget(edit=self._filter_text, up_widget=self._join_text) + ba.textwidget(text=filter_txt, + parent=self._container, + size=(0, 0), + position=(270, v + 13), + maxwidth=150, + scale=0.8, + color=(0.5, 0.46, 0.5), + flatness=1.0, + h_align='right', + v_align='center') + + ba.textwidget(text=ba.Lstr(resource='nameText'), + parent=self._container, + size=(0, 0), + position=(90, v - 8), + maxwidth=60, + scale=0.6, + color=(0.5, 0.46, 0.5), + flatness=1.0, + h_align='center', + v_align='center') + ba.textwidget(text=ba.Lstr(resource='gatherWindow.partySizeText'), + parent=self._container, + size=(0, 0), + position=(755, v - 8), + maxwidth=60, + scale=0.6, + color=(0.5, 0.46, 0.5), + flatness=1.0, + h_align='center', + v_align='center') + ba.textwidget(text=ba.Lstr(resource='gatherWindow.pingText'), + parent=self._container, + size=(0, 0), + position=(825, v - 8), + maxwidth=60, + scale=0.6, + color=(0.5, 0.46, 0.5), + flatness=1.0, + h_align='center', + v_align='center') + v -= sub_scroll_height + 23 + self._host_scrollwidget = scrollw = ba.scrollwidget( + parent=self._container, + simple_culling_v=10, + position=((c_width - sub_scroll_width) * 0.5, v), + size=(sub_scroll_width, sub_scroll_height), + claims_up_down=False, + claims_left_right=True, + autoselect=True) + self._join_list_column = ba.containerwidget(parent=scrollw, + background=False, + size=(400, 400), + claims_left_right=True) + self._join_status_text = ba.textwidget(parent=self._container, + text='', + size=(0, 0), + scale=0.9, + flatness=1.0, + shadow=0.0, + h_align='center', + v_align='top', + maxwidth=c_width, + color=(0.6, 0.6, 0.6), + position=(c_width * 0.5, + c_height * 0.5)) + + def _build_host_tab(self, region_width: float, + region_height: float) -> None: + c_width = region_width + c_height = region_height - 20 + v = c_height - 35 + v -= 25 + is_public_enabled = _ba.get_public_party_enabled() + v -= 30 + + ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=c_width * 0.9, + scale=0.7, + flatness=1.0, + color=(0.5, 0.46, 0.5), + position=(region_width * 0.5, v + 10), + text=ba.Lstr(resource='gatherWindow.publicHostRouterConfigText')) + v -= 30 + + party_name_text = ba.Lstr( + resource='gatherWindow.partyNameText', + fallback_resource='editGameListWindow.nameText') + ba.textwidget(parent=self._container, + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=200, + scale=0.8, + color=ba.app.ui.infotextcolor, + position=(210, v - 9), + text=party_name_text) + self._host_name_text = ba.textwidget(parent=self._container, + editable=True, + size=(535, 40), + position=(230, v - 30), + text=ba.app.config.get( + 'Public Party Name', ''), + maxwidth=494, + shadow=0.3, + flatness=1.0, + description=party_name_text, + autoselect=True, + v_align='center', + corner_scale=1.0) + + v -= 60 + ba.textwidget(parent=self._container, + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=200, + scale=0.8, + color=ba.app.ui.infotextcolor, + position=(210, v - 9), + text=ba.Lstr(resource='maxPartySizeText', + fallback_resource='maxConnectionsText')) + self._host_max_party_size_value = ba.textwidget( + parent=self._container, + size=(0, 0), + h_align='center', + v_align='center', + scale=1.2, + color=(1, 1, 1), + position=(240, v - 9), + text=str(_ba.get_public_party_max_size())) + btn1 = self._host_max_party_size_minus_button = (ba.buttonwidget( + parent=self._container, + size=(40, 40), + on_activate_call=ba.WeakCall( + self._on_max_public_party_size_minus_press), + position=(280, v - 26), + label='-', + autoselect=True)) + btn2 = self._host_max_party_size_plus_button = (ba.buttonwidget( + parent=self._container, + size=(40, 40), + on_activate_call=ba.WeakCall( + self._on_max_public_party_size_plus_press), + position=(350, v - 26), + label='+', + autoselect=True)) + v -= 50 + v -= 70 + if is_public_enabled: + label = ba.Lstr( + resource='gatherWindow.makePartyPrivateText', + fallback_resource='gatherWindow.stopAdvertisingText') + else: + label = ba.Lstr( + resource='gatherWindow.makePartyPublicText', + fallback_resource='gatherWindow.startAdvertisingText') + self._host_toggle_button = ba.buttonwidget( + parent=self._container, + label=label, + size=(400, 80), + on_activate_call=self._on_stop_advertising_press + if is_public_enabled else self._on_start_advertizing_press, + position=(c_width * 0.5 - 200, v), + autoselect=True, + up_widget=btn2) + ba.widget(edit=self._host_name_text, down_widget=btn2) + ba.widget(edit=btn2, up_widget=self._host_name_text) + ba.widget(edit=btn1, up_widget=self._host_name_text) + ba.widget(edit=self._join_text, down_widget=self._host_name_text) + v -= 10 + self._host_status_text = ba.textwidget( + parent=self._container, + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusNotPublicText'), + size=(0, 0), + scale=0.7, + flatness=1.0, + h_align='center', + v_align='top', + maxwidth=c_width * 0.9, + color=(0.6, 0.56, 0.6), + position=(c_width * 0.5, v)) + v -= 90 + ba.textwidget( + parent=self._container, + text=ba.Lstr(resource='gatherWindow.dedicatedServerInfoText'), + size=(0, 0), + scale=0.7, + flatness=1.0, + h_align='center', + v_align='center', + maxwidth=c_width * 0.9, + color=(0.5, 0.46, 0.5), + position=(c_width * 0.5, v)) + + # If public sharing is already on, + # launch a status-check immediately. + if _ba.get_public_party_enabled(): + self._do_status_check() + + def _on_public_party_query_result( + self, result: Optional[Dict[str, Any]]) -> None: + starttime = time.time() + self._have_server_list_response = True + + if result is None: + self._have_valid_server_list = False + return + + if not self._have_valid_server_list: + self._first_valid_server_list_time = time.time() + + self._have_valid_server_list = True + parties_in = result['l'] + + assert isinstance(parties_in, list) + self._pending_party_infos += parties_in + + # To avoid causing a stutter here, we do most processing of + # these entries incrementally in our _update() method. + # The one thing we do here is prune parties not contained in + # this result. + for partyval in list(self._parties.values()): + partyval.claimed = False + for party_in in parties_in: + addr = party_in['a'] + assert isinstance(addr, str) + port = party_in['p'] + assert isinstance(port, int) + party_key = f'{addr}_{port}' + party = self._parties.get(party_key) + if party is not None: + party.claimed = True + self._parties = { + key: val + for key, val in list(self._parties.items()) if val.claimed + } + self._parties_sorted = [ + p for p in self._parties_sorted if p[1].claimed + ] + self._party_lists_dirty = True + + # self._update_server_list() + if DEBUG_PROCESSING: + print(f'Handled public party query results in ' + f'{time.time()-starttime:.5f}s.') + + def _update(self) -> None: + """Periodic updating.""" + + # Special case: if a party-queue window is up, don't do any of this + # (keeps things smoother). + # if ba.app.ui.have_party_queue_window: + # return + + if self._sub_tab is SubTabType.JOIN: + + # Keep our filter-text up to date from the UI. + text = self._filter_text + if text: + filter_value = cast(str, ba.textwidget(query=text)) + if filter_value != self._filter_value: + self._filter_value = filter_value + self._party_lists_dirty = True + + # Also wipe out party clean-row states. + # (otherwise if a party disappears from a row due to + # filtering and then reappears on that same row when + # the filter is removed it may not update) + for party in self._parties.values(): + party.clean_display_index = None + + self._query_party_list_periodically() + self._ping_parties_periodically() + + # If any new party infos have come in, apply some of them. + self._process_pending_party_infos() + + # Anytime we sign in/out, make sure we refresh our list. + signed_in = _ba.get_account_state() == 'signed_in' + if self._signed_in != signed_in: + self._signed_in = signed_in + self._party_lists_dirty = True + + # Update sorting to account for ping updates, new parties, etc. + self._update_party_lists() + + # If we've got a party-name text widget, keep its value plugged + # into our public host name. + text = self._host_name_text + if text: + name = cast(str, ba.textwidget(query=self._host_name_text)) + _ba.set_public_party_name(name) + + # Update status text. + status_text = self._join_status_text + if status_text: + if not signed_in: + ba.textwidget(edit=status_text, + text=ba.Lstr(resource='notSignedInText')) + else: + # If we have a valid list, show no status; just the list. + # Otherwise show either 'loading...' or 'error' depending + # on whether this is our first go-round. + if self._have_valid_server_list: + ba.textwidget(edit=status_text, text='') + else: + if self._have_server_list_response: + ba.textwidget(edit=status_text, + text=ba.Lstr(resource='errorText')) + else: + ba.textwidget( + edit=status_text, + text=ba.Lstr( + value='${A}...', + subs=[('${A}', + ba.Lstr(resource='store.loadingText'))], + )) + + self._update_party_rows() + + def _update_party_rows(self) -> None: + columnwidget = self._join_list_column + if not columnwidget: + return + + assert self._join_text + assert self._filter_text + + # Janky - allow escaping when there's nothing in our list. + assert self._host_scrollwidget + ba.containerwidget(edit=self._host_scrollwidget, + claims_up_down=(len(self._parties_displayed) > 0)) + + # Clip if we have more UI rows than parties to show. + clipcount = len(self._ui_rows) - len(self._parties_displayed) + if clipcount > 0: + clipcount = max(clipcount, 50) + self._ui_rows = self._ui_rows[:-clipcount] + + # If we have no parties to show, we're done. + if not self._parties_displayed: + return + + sub_scroll_width = 830 + lineheight = 42 + sub_scroll_height = lineheight * len(self._parties_displayed) + 50 + ba.containerwidget(edit=columnwidget, + size=(sub_scroll_width, sub_scroll_height)) + + # Any time our height changes, reset the refresh back to the top + # so we don't see ugly empty spaces appearing during initial list + # filling. + if sub_scroll_height != self._last_sub_scroll_height: + self._refresh_ui_row = 0 + self._last_sub_scroll_height = sub_scroll_height + + # Also note that we need to redisplay everything since its pos + # will have changed.. :( + for party in self._parties.values(): + party.clean_display_index = None + + # Ew; this rebuilding generates deferred selection callbacks + # so we need to push deferred notices so we know to ignore them. + def refresh_on() -> None: + self._refreshing_list = True + + ba.pushcall(refresh_on) + + # Ok, now here's the deal: we want to avoid creating/updating this + # entire list at one time because it will lead to hitches. So we + # refresh individual rows quickly in a loop. + rowcount = min(12, len(self._parties_displayed)) + + party_vals_displayed = list(self._parties_displayed.values()) + while rowcount > 0: + refresh_row = self._refresh_ui_row % len(self._parties_displayed) + if refresh_row >= len(self._ui_rows): + self._ui_rows.append(UIRow()) + refresh_row = len(self._ui_rows) - 1 + + # For the first few seconds after getting our first server-list, + # refresh only the top section of the list; this allows the lowest + # ping servers to show up more quickly. + if self._first_valid_server_list_time is not None: + if time.time() - self._first_valid_server_list_time < 4.0: + if refresh_row > 40: + refresh_row = 0 + + self._ui_rows[refresh_row].update( + refresh_row, + party_vals_displayed[refresh_row], + sub_scroll_width=sub_scroll_width, + sub_scroll_height=sub_scroll_height, + lineheight=lineheight, + columnwidget=columnwidget, + join_text=self._join_text, + existing_selection=self._selection, + filter_text=self._filter_text, + tab=self) + self._refresh_ui_row = refresh_row + 1 + rowcount -= 1 + + # So our selection callbacks can start firing.. + def refresh_off() -> None: + self._refreshing_list = False + + ba.pushcall(refresh_off) + + def _process_pending_party_infos(self) -> None: + starttime = time.time() + + # We want to do this in small enough pieces to not cause UI hitches. + chunksize = 30 + parties_in = self._pending_party_infos[:chunksize] + self._pending_party_infos = self._pending_party_infos[chunksize:] + for party_in in parties_in: + addr = party_in['a'] + assert isinstance(addr, str) + port = party_in['p'] + assert isinstance(port, int) + party_key = f'{addr}_{port}' + party = self._parties.get(party_key) + if party is None: + # If this party is new to us, init it. + party = PartyEntry(address=addr, + next_ping_time=ba.time(ba.TimeType.REAL) + + 0.001 * party_in['pd'], + index=self._next_entry_index) + self._parties[party_key] = party + self._parties_sorted.append((party_key, party)) + self._party_lists_dirty = True + self._next_entry_index += 1 + assert isinstance(party.address, str) + assert isinstance(party.next_ping_time, float) + + # Now, new or not, update its values. + party.queue = party_in.get('q') + assert isinstance(party.queue, (str, type(None))) + party.port = port + party.name = party_in['n'] + assert isinstance(party.name, str) + party.size = party_in['s'] + assert isinstance(party.size, int) + party.size_max = party_in['sm'] + assert isinstance(party.size_max, int) + + # Server provides this in milliseconds; we use seconds. + party.ping_interval = 0.001 * party_in['pi'] + assert isinstance(party.ping_interval, float) + party.stats_addr = party_in['sa'] + assert isinstance(party.stats_addr, (str, type(None))) + + # Make sure the party's UI gets updated. + party.clean_display_index = None + + if DEBUG_PROCESSING and parties_in: + print(f'Processed {len(parties_in)} raw party infos in' + f' {time.time()-starttime:.5f}s.') + + def _update_party_lists(self) -> None: + if not self._party_lists_dirty: + return + starttime = time.time() + assert len(self._parties_sorted) == len(self._parties) + + self._parties_sorted.sort(key=lambda p: ( + p[1].queue is None, # Show non-queued last. + p[1].ping if p[1].ping is not None else 999999.0, + p[1].index)) + + # If signed out or errored, show no parties. + if (_ba.get_account_state() != 'signed_in' + or not self._have_valid_server_list): + self._parties_displayed = {} + else: + if self._filter_value: + filterval = self._filter_value.lower() + self._parties_displayed = { + k: v + for k, v in self._parties_sorted + if filterval in v.name.lower() + } + else: + self._parties_displayed = dict(self._parties_sorted) + + # Any time our selection disappears from the displayed list, go back to + # auto-selecting the top entry. + if (self._selection is not None + and self._selection.entry_key not in self._parties_displayed): + self._have_user_selected_row = False + + # Whenever the user hasn't selected something, keep the first visible + # row selected. + if not self._have_user_selected_row and self._parties_displayed: + firstpartykey = next(iter(self._parties_displayed)) + self._selection = Selection(firstpartykey, SelectionComponent.NAME) + + self._party_lists_dirty = False + if DEBUG_PROCESSING: + print(f'Sorted {len(self._parties_sorted)} parties in' + f' {time.time()-starttime:.5f}s.') + + def _query_party_list_periodically(self) -> None: + now = ba.time(ba.TimeType.REAL) + + # Fire off a new public-party query periodically. + if (self._last_server_list_query_time is None + or now - self._last_server_list_query_time > 0.001 * + _ba.get_account_misc_read_val('pubPartyRefreshMS', 10000)): + self._last_server_list_query_time = now + if DEBUG_SERVER_COMMUNICATION: + print('REQUESTING SERVER LIST') + if _ba.get_account_state() == 'signed_in': + _ba.add_transaction( + { + 'type': 'PUBLIC_PARTY_QUERY', + 'proto': ba.app.protocol_version, + 'lang': ba.app.lang.language + }, + callback=ba.WeakCall(self._on_public_party_query_result)) + _ba.run_transactions() + else: + self._on_public_party_query_result(None) + + def _ping_parties_periodically(self) -> None: + now = ba.time(ba.TimeType.REAL) + + # Go through our existing public party entries firing off pings + # for any that have timed out. + for party in list(self._parties.values()): + if party.next_ping_time <= now and ba.app.ping_thread_count < 15: + + # Crank the interval up for high-latency or non-responding + # parties to save us some useless work. + mult = 1 + if party.ping_responses == 0: + if party.ping_attempts > 4: + mult = 10 + elif party.ping_attempts > 2: + mult = 5 + if party.ping is not None: + mult = (10 if party.ping > 300 else + 5 if party.ping > 150 else 2) + + interval = party.ping_interval * mult + if DEBUG_SERVER_COMMUNICATION: + print(f'pinging #{party.index} cur={party.ping} ' + f'interval={interval} ' + f'({party.ping_responses}/{party.ping_attempts})') + + party.next_ping_time = now + party.ping_interval * mult + party.ping_attempts += 1 + + PingThread(party.address, party.port, + ba.WeakCall(self._ping_callback)).start() + + def _ping_callback(self, address: str, port: Optional[int], + result: Optional[float]) -> None: + # Look for a widget corresponding to this target. + # If we find one, update our list. + party_key = f'{address}_{port}' + party = self._parties.get(party_key) + if party is not None: + if result is not None: + party.ping_responses += 1 + + # We now smooth ping a bit to reduce jumping around in the list + # (only where pings are relatively good). + current_ping = party.ping + if (current_ping is not None and result is not None + and result < 150): + smoothing = 0.7 + party.ping = (smoothing * current_ping + + (1.0 - smoothing) * result) + else: + party.ping = result + + # Need to re-sort the list and update the row display. + party.clean_display_index = None + self._party_lists_dirty = True + + def _fetch_local_addr_cb(self, val: str) -> None: + self._local_address = str(val) + + def _on_public_party_accessible_response( + self, data: Optional[Dict[str, Any]]) -> None: + + # If we've got status text widgets, update them. + text = self._host_status_text + if text: + if data is None: + ba.textwidget( + edit=text, + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusNoConnectionText'), + color=(1, 0, 0), + ) + else: + if not data.get('accessible', False): + ex_line: Union[str, ba.Lstr] + if self._local_address is not None: + ex_line = ba.Lstr( + value='\n${A} ${B}', + subs=[('${A}', + ba.Lstr(resource='gatherWindow.' + 'manualYourLocalAddressText')), + ('${B}', self._local_address)]) + else: + ex_line = '' + ba.textwidget( + edit=text, + text=ba.Lstr( + value='${A}\n${B}${C}', + subs=[('${A}', + ba.Lstr(resource='gatherWindow.' + 'partyStatusNotJoinableText')), + ('${B}', + ba.Lstr(resource='gatherWindow.' + 'manualRouterForwardingText', + subs=[('${PORT}', + str(_ba.get_game_port()))])), + ('${C}', ex_line)]), + color=(1, 0, 0)) + else: + ba.textwidget(edit=text, + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusJoinableText'), + color=(0, 1, 0)) + + def _do_status_check(self) -> None: + from ba.internal import master_server_get + ba.textwidget(edit=self._host_status_text, + color=(1, 1, 0), + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusCheckingText')) + master_server_get('bsAccessCheck', {'b': ba.app.build_number}, + callback=ba.WeakCall( + self._on_public_party_accessible_response)) + + def _on_start_advertizing_press(self) -> None: + from bastd.ui.account import show_sign_in_prompt + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + + name = cast(str, ba.textwidget(query=self._host_name_text)) + if name == '': + ba.screenmessage(ba.Lstr(resource='internal.invalidNameErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + _ba.set_public_party_name(name) + cfg = ba.app.config + cfg['Public Party Name'] = name + cfg.commit() + ba.playsound(ba.getsound('shieldUp')) + _ba.set_public_party_enabled(True) + + # In GUI builds we want to authenticate clients only when hosting + # public parties. + _ba.set_authenticate_clients(True) + + self._do_status_check() + ba.buttonwidget( + edit=self._host_toggle_button, + label=ba.Lstr( + resource='gatherWindow.makePartyPrivateText', + fallback_resource='gatherWindow.stopAdvertisingText'), + on_activate_call=self._on_stop_advertising_press) + + def _on_stop_advertising_press(self) -> None: + _ba.set_public_party_enabled(False) + + # In GUI builds we want to authenticate clients only when hosting + # public parties. + _ba.set_authenticate_clients(False) + ba.playsound(ba.getsound('shieldDown')) + text = self._host_status_text + if text: + ba.textwidget( + edit=text, + text=ba.Lstr(resource='gatherWindow.' + 'partyStatusNotPublicText'), + color=(0.6, 0.6, 0.6), + ) + ba.buttonwidget( + edit=self._host_toggle_button, + label=ba.Lstr( + resource='gatherWindow.makePartyPublicText', + fallback_resource='gatherWindow.startAdvertisingText'), + on_activate_call=self._on_start_advertizing_press) + + def on_public_party_activate(self, party: PartyEntry) -> None: + """Called when a party is clicked or otherwise activated.""" + if party.queue is not None: + from bastd.ui.partyqueue import PartyQueueWindow + ba.playsound(ba.getsound('swish')) + PartyQueueWindow(party.queue, party.address, party.port) + else: + address = party.address + port = party.port + + # Rate limit this a bit. + now = time.time() + last_connect_time = self._last_connect_attempt_time + if last_connect_time is None or now - last_connect_time > 2.0: + _ba.connect_to_party(address, port=port) + self._last_connect_attempt_time = now + + def set_public_party_selection(self, sel: Selection) -> None: + """Set the sel.""" + if self._refreshing_list: + return + self._selection = sel + self._have_user_selected_row = True + + def _on_max_public_party_size_minus_press(self) -> None: + val = _ba.get_public_party_max_size() + val -= 1 + if val < 1: + val = 1 + _ba.set_public_party_max_size(val) + ba.textwidget(edit=self._host_max_party_size_value, text=str(val)) + + def _on_max_public_party_size_plus_press(self) -> None: + val = _ba.get_public_party_max_size() + val += 1 + _ba.set_public_party_max_size(val) + ba.textwidget(edit=self._host_max_party_size_value, text=str(val)) diff --git a/dist/ba_data/python/bastd/ui/getcurrency.py b/dist/ba_data/python/bastd/ui/getcurrency.py new file mode 100644 index 0000000..db19be2 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/getcurrency.py @@ -0,0 +1,611 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for purchasing/acquiring currency.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Optional, Tuple, Union, Dict + + +class GetCurrencyWindow(ba.Window): + """Window for purchasing/acquiring currency.""" + + def __init__(self, + transition: str = 'in_right', + from_modal_store: bool = False, + modal: bool = False, + origin_widget: ba.Widget = None, + store_back_location: str = None): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + + ba.set_analytics_screen('Get Tickets Window') + + self._transitioning_out = False + self._store_back_location = store_back_location # ew. + + self._ad_button_greyed = False + self._smooth_update_timer: Optional[ba.Timer] = None + self._ad_button = None + self._ad_label = None + self._ad_image = None + self._ad_time_text = None + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + uiscale = ba.app.ui.uiscale + self._width = 1000.0 if uiscale is ba.UIScale.SMALL else 800.0 + x_inset = 100.0 if uiscale is ba.UIScale.SMALL else 0.0 + self._height = 480.0 + + self._modal = modal + self._from_modal_store = from_modal_store + self._r = 'getTicketsWindow' + + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + scale_origin_stack_offset=scale_origin, + color=(0.4, 0.37, 0.55), + scale=(1.63 if uiscale is ba.UIScale.SMALL else + 1.2 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -3) if uiscale is ba.UIScale.SMALL else (0, 0))) + + btn = ba.buttonwidget( + parent=self._root_widget, + position=(55 + x_inset, self._height - 79), + size=(140, 60), + scale=1.0, + autoselect=True, + label=ba.Lstr(resource='doneText' if modal else 'backText'), + button_type='regular' if modal else 'back', + on_activate_call=self._back) + + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 55), + size=(0, 0), + color=ba.app.ui.title_color, + scale=1.2, + h_align='center', + v_align='center', + text=ba.Lstr(resource=self._r + '.titleText'), + maxwidth=290) + + if not modal: + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + b_size = (220.0, 180.0) + v = self._height - b_size[1] - 80 + spacing = 1 + + self._ad_button = None + + def _add_button(item: str, + position: Tuple[float, float], + size: Tuple[float, float], + label: ba.Lstr, + price: str = None, + tex_name: str = None, + tex_opacity: float = 1.0, + tex_scale: float = 1.0, + enabled: bool = True, + text_scale: float = 1.0) -> ba.Widget: + btn2 = ba.buttonwidget( + parent=self._root_widget, + position=position, + button_type='square', + size=size, + label='', + autoselect=True, + color=None if enabled else (0.5, 0.5, 0.5), + on_activate_call=(ba.Call(self._purchase, item) + if enabled else self._disabled_press)) + txt = ba.textwidget(parent=self._root_widget, + text=label, + position=(position[0] + size[0] * 0.5, + position[1] + size[1] * 0.3), + scale=text_scale, + maxwidth=size[0] * 0.75, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn2, + color=(0.7, 0.9, 0.7, 1.0 if enabled else 0.2)) + if price is not None and enabled: + ba.textwidget(parent=self._root_widget, + text=price, + position=(position[0] + size[0] * 0.5, + position[1] + size[1] * 0.17), + scale=0.7, + maxwidth=size[0] * 0.75, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn2, + color=(0.4, 0.9, 0.4, 1.0)) + i = None + if tex_name is not None: + tex_size = 90.0 * tex_scale + i = ba.imagewidget( + parent=self._root_widget, + texture=ba.gettexture(tex_name), + position=(position[0] + size[0] * 0.5 - tex_size * 0.5, + position[1] + size[1] * 0.66 - tex_size * 0.5), + size=(tex_size, tex_size), + draw_controller=btn2, + opacity=tex_opacity * (1.0 if enabled else 0.25)) + if item == 'ad': + self._ad_button = btn2 + self._ad_label = txt + assert i is not None + self._ad_image = i + self._ad_time_text = ba.textwidget( + parent=self._root_widget, + text='1m 10s', + position=(position[0] + size[0] * 0.5, + position[1] + size[1] * 0.5), + scale=text_scale * 1.2, + maxwidth=size[0] * 0.85, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn2, + color=(0.4, 0.9, 0.4, 1.0)) + return btn2 + + rsrc = self._r + '.ticketsText' + + c2txt = ba.Lstr( + resource=rsrc, + subs=[('${COUNT}', + str(_ba.get_account_misc_read_val('tickets2Amount', 500)))]) + c3txt = ba.Lstr( + resource=rsrc, + subs=[('${COUNT}', + str(_ba.get_account_misc_read_val('tickets3Amount', + 1500)))]) + c4txt = ba.Lstr( + resource=rsrc, + subs=[('${COUNT}', + str(_ba.get_account_misc_read_val('tickets4Amount', + 5000)))]) + c5txt = ba.Lstr( + resource=rsrc, + subs=[('${COUNT}', + str(_ba.get_account_misc_read_val('tickets5Amount', + 15000)))]) + + h = 110.0 + + # enable buttons if we have prices.. + tickets2_price = _ba.get_price('tickets2') + tickets3_price = _ba.get_price('tickets3') + tickets4_price = _ba.get_price('tickets4') + tickets5_price = _ba.get_price('tickets5') + + # TEMP + # tickets1_price = '$0.99' + # tickets2_price = '$4.99' + # tickets3_price = '$9.99' + # tickets4_price = '$19.99' + # tickets5_price = '$49.99' + + _add_button('tickets2', + enabled=(tickets2_price is not None), + position=(self._width * 0.5 - spacing * 1.5 - + b_size[0] * 2.0 + h, v), + size=b_size, + label=c2txt, + price=tickets2_price, + tex_name='ticketsMore') # 0.99-ish + _add_button('tickets3', + enabled=(tickets3_price is not None), + position=(self._width * 0.5 - spacing * 0.5 - + b_size[0] * 1.0 + h, v), + size=b_size, + label=c3txt, + price=tickets3_price, + tex_name='ticketRoll') # 4.99-ish + v -= b_size[1] - 5 + _add_button('tickets4', + enabled=(tickets4_price is not None), + position=(self._width * 0.5 - spacing * 1.5 - + b_size[0] * 2.0 + h, v), + size=b_size, + label=c4txt, + price=tickets4_price, + tex_name='ticketRollBig', + tex_scale=1.2) # 9.99-ish + _add_button('tickets5', + enabled=(tickets5_price is not None), + position=(self._width * 0.5 - spacing * 0.5 - + b_size[0] * 1.0 + h, v), + size=b_size, + label=c5txt, + price=tickets5_price, + tex_name='ticketRolls', + tex_scale=1.2) # 19.99-ish + + self._enable_ad_button = _ba.has_video_ads() + h = self._width * 0.5 + 110.0 + v = self._height - b_size[1] - 115.0 + + if self._enable_ad_button: + h_offs = 35 + b_size_3 = (150, 120) + cdb = _add_button( + 'ad', + position=(h + h_offs, v), + size=b_size_3, + label=ba.Lstr(resource=self._r + '.ticketsFromASponsorText', + subs=[('${COUNT}', + str( + _ba.get_account_misc_read_val( + 'sponsorTickets', 5)))]), + tex_name='ticketsMore', + enabled=self._enable_ad_button, + tex_opacity=0.6, + tex_scale=0.7, + text_scale=0.7) + ba.buttonwidget(edit=cdb, + color=(0.65, 0.5, + 0.7) if self._enable_ad_button else + (0.5, 0.5, 0.5)) + + self._ad_free_text = ba.textwidget( + parent=self._root_widget, + text=ba.Lstr(resource=self._r + '.freeText'), + position=(h + h_offs + b_size_3[0] * 0.5, + v + b_size_3[1] * 0.5 + 25), + size=(0, 0), + color=(1, 1, 0, 1.0) if self._enable_ad_button else + (1, 1, 1, 0.2), + draw_controller=cdb, + rotate=15, + shadow=1.0, + maxwidth=150, + h_align='center', + v_align='center', + scale=1.0) + v -= 125 + else: + v -= 20 + + if True: # pylint: disable=using-constant-test + h_offs = 35 + b_size_3 = (150, 120) + cdb = _add_button( + 'app_invite', + position=(h + h_offs, v), + size=b_size_3, + label=ba.Lstr( + resource='gatherWindow.earnTicketsForRecommendingText', + subs=[ + ('${COUNT}', + str(_ba.get_account_misc_read_val( + 'sponsorTickets', 5))) + ]), + tex_name='ticketsMore', + enabled=True, + tex_opacity=0.6, + tex_scale=0.7, + text_scale=0.7) + ba.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7)) + + ba.textwidget(parent=self._root_widget, + text=ba.Lstr(resource=self._r + '.freeText'), + position=(h + h_offs + b_size_3[0] * 0.5, + v + b_size_3[1] * 0.5 + 25), + size=(0, 0), + color=(1, 1, 0, 1.0), + draw_controller=cdb, + rotate=15, + shadow=1.0, + maxwidth=150, + h_align='center', + v_align='center', + scale=1.0) + tc_y_offs = 0 + + h = self._width - (185 + x_inset) + v = self._height - 95 + tc_y_offs + + txt1 = (ba.Lstr( + resource=self._r + + '.youHaveText').evaluate().split('${COUNT}')[0].strip()) + txt2 = (ba.Lstr( + resource=self._r + + '.youHaveText').evaluate().split('${COUNT}')[-1].strip()) + + ba.textwidget(parent=self._root_widget, + text=txt1, + position=(h, v), + size=(0, 0), + color=(0.5, 0.5, 0.6), + maxwidth=200, + h_align='center', + v_align='center', + scale=0.8) + v -= 30 + self._ticket_count_text = ba.textwidget(parent=self._root_widget, + position=(h, v), + size=(0, 0), + color=(0.2, 1.0, 0.2), + maxwidth=200, + h_align='center', + v_align='center', + scale=1.6) + v -= 30 + ba.textwidget(parent=self._root_widget, + text=txt2, + position=(h, v), + size=(0, 0), + color=(0.5, 0.5, 0.6), + maxwidth=200, + h_align='center', + v_align='center', + scale=0.8) + + # update count now and once per second going forward.. + self._ticking_node: Optional[ba.Node] = None + self._smooth_ticket_count: Optional[float] = None + self._ticket_count = 0 + self._update() + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + self._smooth_increase_speed = 1.0 + + def __del__(self) -> None: + if self._ticking_node is not None: + self._ticking_node.delete() + self._ticking_node = None + + def _smooth_update(self) -> None: + if not self._ticket_count_text: + self._smooth_update_timer = None + return + + finished = False + + # if we're going down, do it immediately + assert self._smooth_ticket_count is not None + if int(self._smooth_ticket_count) >= self._ticket_count: + self._smooth_ticket_count = float(self._ticket_count) + finished = True + else: + # we're going up; start a sound if need be + self._smooth_ticket_count = min( + self._smooth_ticket_count + 1.0 * self._smooth_increase_speed, + self._ticket_count) + if int(self._smooth_ticket_count) >= self._ticket_count: + finished = True + self._smooth_ticket_count = float(self._ticket_count) + elif self._ticking_node is None: + with ba.Context('ui'): + self._ticking_node = ba.newnode( + 'sound', + attrs={ + 'sound': ba.getsound('scoreIncrease'), + 'positional': False + }) + + ba.textwidget(edit=self._ticket_count_text, + text=str(int(self._smooth_ticket_count))) + + # if we've reached the target, kill the timer/sound/etc + if finished: + self._smooth_update_timer = None + if self._ticking_node is not None: + self._ticking_node.delete() + self._ticking_node = None + ba.playsound(ba.getsound('cashRegister2')) + + def _update(self) -> None: + import datetime + + # if we somehow get signed out, just die.. + if _ba.get_account_state() != 'signed_in': + self._back() + return + + self._ticket_count = _ba.get_account_ticket_count() + + # update our incentivized ad button depending on whether ads are + # available + if self._ad_button is not None: + next_reward_ad_time = _ba.get_account_misc_read_val_2( + 'nextRewardAdTime', None) + if next_reward_ad_time is not None: + next_reward_ad_time = datetime.datetime.utcfromtimestamp( + next_reward_ad_time) + now = datetime.datetime.utcnow() + if (_ba.have_incentivized_ad() and + (next_reward_ad_time is None or next_reward_ad_time <= now)): + self._ad_button_greyed = False + ba.buttonwidget(edit=self._ad_button, color=(0.65, 0.5, 0.7)) + ba.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 1.0)) + ba.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 1)) + ba.imagewidget(edit=self._ad_image, opacity=0.6) + ba.textwidget(edit=self._ad_time_text, text='') + else: + self._ad_button_greyed = True + ba.buttonwidget(edit=self._ad_button, color=(0.5, 0.5, 0.5)) + ba.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 0.2)) + ba.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 0.2)) + ba.imagewidget(edit=self._ad_image, opacity=0.6 * 0.25) + sval: Union[str, ba.Lstr] + if (next_reward_ad_time is not None + and next_reward_ad_time > now): + sval = ba.timestring( + (next_reward_ad_time - now).total_seconds() * 1000.0, + centi=False, + timeformat=ba.TimeFormat.MILLISECONDS) + else: + sval = '' + ba.textwidget(edit=self._ad_time_text, text=sval) + + # if this is our first update, assign immediately; otherwise kick + # off a smooth transition if the value has changed + if self._smooth_ticket_count is None: + self._smooth_ticket_count = float(self._ticket_count) + self._smooth_update() # will set the text widget + + elif (self._ticket_count != int(self._smooth_ticket_count) + and self._smooth_update_timer is None): + self._smooth_update_timer = ba.Timer(0.05, + ba.WeakCall( + self._smooth_update), + repeat=True, + timetype=ba.TimeType.REAL) + diff = abs(float(self._ticket_count) - self._smooth_ticket_count) + self._smooth_increase_speed = (diff / + 100.0 if diff >= 5000 else diff / + 50.0 if diff >= 1500 else diff / + 30.0 if diff >= 500 else diff / + 15.0) + + def _disabled_press(self) -> None: + + # if we're on a platform without purchases, inform the user they + # can link their accounts and buy stuff elsewhere + app = ba.app + if ((app.test_build or + (app.platform == 'android' + and app.subplatform in ['oculus', 'cardboard'])) and + _ba.get_account_misc_read_val('allowAccountLinking2', False)): + ba.screenmessage(ba.Lstr(resource=self._r + + '.unavailableLinkAccountText'), + color=(1, 0.5, 0)) + else: + ba.screenmessage(ba.Lstr(resource=self._r + '.unavailableText'), + color=(1, 0.5, 0)) + ba.playsound(ba.getsound('error')) + + def _purchase(self, item: str) -> None: + from bastd.ui import account + from bastd.ui import appinvite + from ba.internal import master_server_get + if item == 'app_invite': + if _ba.get_account_state() != 'signed_in': + account.show_sign_in_prompt() + return + appinvite.handle_app_invites_press() + return + # here we ping the server to ask if it's valid for us to + # purchase this.. (better to fail now than after we've paid locally) + app = ba.app + master_server_get('bsAccountPurchaseCheck', { + 'item': item, + 'platform': app.platform, + 'subplatform': app.subplatform, + 'version': app.version, + 'buildNumber': app.build_number + }, + callback=ba.WeakCall(self._purchase_check_result, + item)) + + def _purchase_check_result(self, item: str, + result: Optional[Dict[str, Any]]) -> None: + if result is None: + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource='internal.unavailableNoConnectionText'), + color=(1, 0, 0)) + else: + if result['allow']: + self._do_purchase(item) + else: + if result['reason'] == 'versionTooOld': + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource='getTicketsWindow.versionTooOldText'), + color=(1, 0, 0)) + else: + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + + # actually start the purchase locally.. + def _do_purchase(self, item: str) -> None: + if item == 'ad': + import datetime + # if ads are disabled until some time, error.. + next_reward_ad_time = _ba.get_account_misc_read_val_2( + 'nextRewardAdTime', None) + if next_reward_ad_time is not None: + next_reward_ad_time = datetime.datetime.utcfromtimestamp( + next_reward_ad_time) + now = datetime.datetime.utcnow() + if ((next_reward_ad_time is not None and next_reward_ad_time > now) + or self._ad_button_greyed): + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr( + resource='getTicketsWindow.unavailableTemporarilyText'), + color=(1, 0, 0)) + elif self._enable_ad_button: + _ba.app.ads.show_ad('tickets') + else: + _ba.purchase(item) + + def _back(self) -> None: + from bastd.ui.store import browser + if self._transitioning_out: + return + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + if not self._modal: + window = browser.StoreBrowserWindow( + transition='in_left', + modal=self._from_modal_store, + back_location=self._store_back_location).get_root_widget() + if not self._from_modal_store: + ba.app.ui.set_main_menu_window(window) + self._transitioning_out = True + + +def show_get_tickets_prompt() -> None: + """Show a 'not enough tickets' prompt with an option to purchase more. + + Note that the purchase option may not always be available + depending on the build of the game. + """ + from bastd.ui.confirm import ConfirmWindow + if ba.app.allow_ticket_purchases: + ConfirmWindow( + ba.Lstr(translate=('serverResponses', + 'You don\'t have enough tickets for this!')), + lambda: GetCurrencyWindow(modal=True), + ok_text=ba.Lstr(resource='getTicketsWindow.titleText'), + width=460, + height=130) + else: + ConfirmWindow( + ba.Lstr(translate=('serverResponses', + 'You don\'t have enough tickets for this!')), + cancel_button=False, + width=460, + height=130) diff --git a/dist/ba_data/python/bastd/ui/getremote.py b/dist/ba_data/python/bastd/ui/getremote.py new file mode 100644 index 0000000..4f8f401 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/getremote.py @@ -0,0 +1,71 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a popup telling the user about the BSRemote app.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + pass + + +class GetBSRemoteWindow(popup.PopupWindow): + """Popup telling the user about BSRemote app.""" + + def __init__(self) -> None: + position = (0.0, 0.0) + uiscale = ba.app.ui.uiscale + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + self._transitioning_out = False + self._width = 570 + self._height = 350 + bg_color = (0.5, 0.4, 0.6) + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=bg_color) + self._cancel_button = ba.buttonwidget( + parent=self.root_widget, + position=(50, self._height - 30), + size=(50, 50), + scale=0.5, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.imagewidget(parent=self.root_widget, + position=(self._width * 0.5 - 110, + self._height * 0.67 - 110), + size=(220, 220), + texture=ba.gettexture('multiplayerExamples')) + ba.textwidget(parent=self.root_widget, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=self._width * 0.9, + position=(self._width * 0.5, 60), + text=ba.Lstr( + resource='remoteAppInfoShortText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')), + ('${REMOTE_APP_NAME}', + ba.Lstr(resource='remote_app.app_name'))])) + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/helpui.py b/dist/ba_data/python/bastd/ui/helpui.py new file mode 100644 index 0000000..b49e51c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/helpui.py @@ -0,0 +1,590 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides help related ui.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Optional, Tuple + + +class HelpWindow(ba.Window): + """A window providing help on how to play.""" + + def __init__(self, + main_menu: bool = False, + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + from ba.internal import get_remote_app_name + ba.set_analytics_screen('Help Window') + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + transition = 'in_right' + + self._r = 'helpWindow' + + getres = ba.app.lang.get_resource + + self._main_menu = main_menu + uiscale = ba.app.ui.uiscale + width = 950 if uiscale is ba.UIScale.SMALL else 750 + x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 + height = (460 if uiscale is ba.UIScale.SMALL else + 530 if uiscale is ba.UIScale.MEDIUM else 600) + + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(1.77 if uiscale is ba.UIScale.SMALL else + 1.25 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -30) if uiscale is ba.UIScale.SMALL else ( + 0, 15) if uiscale is ba.UIScale.MEDIUM else (0, 0))) + + ba.textwidget(parent=self._root_widget, + position=(0, height - + (50 if uiscale is ba.UIScale.SMALL else 45)), + size=(width, 25), + text=ba.Lstr(resource=self._r + '.titleText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText'))]), + color=ba.app.ui.title_color, + h_align='center', + v_align='top') + + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + position=(44 + x_offs, 55 if uiscale is ba.UIScale.SMALL else 55), + simple_culling_v=100.0, + size=(width - (88 + 2 * x_offs), + height - 120 + (5 if uiscale is ba.UIScale.SMALL else 0)), + capture_arrows=True) + + if ba.app.ui.use_toolbars: + ba.widget(edit=self._scrollwidget, + right_widget=_ba.get_special_widget('party_button')) + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + + # ugly: create this last so it gets first dibs at touch events (since + # we have it close to the scroll widget) + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._close) + ba.widget(edit=self._scrollwidget, + left_widget=_ba.get_special_widget('back_button')) + else: + btn = ba.buttonwidget( + parent=self._root_widget, + position=(x_offs + + (40 + 0 if uiscale is ba.UIScale.SMALL else 70), + height - + (59 if uiscale is ba.UIScale.SMALL else 50)), + size=(140, 60), + scale=0.7 if uiscale is ba.UIScale.SMALL else 0.8, + label=ba.Lstr( + resource='backText') if self._main_menu else 'Close', + button_type='back' if self._main_menu else None, + extra_touch_border_scale=2.0, + autoselect=True, + on_activate_call=self._close) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + if self._main_menu: + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 55), + label=ba.charstr(ba.SpecialChar.BACK)) + + self._sub_width = 660 + self._sub_height = 1590 + ba.app.lang.get_resource( + self._r + '.someDaysExtraSpace') + ba.app.lang.get_resource( + self._r + '.orPunchingSomethingExtraSpace') + + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False, + claims_left_right=False, + claims_tab=False) + + spacing = 1.0 + h = self._sub_width * 0.5 + v = self._sub_height - 55 + logo_tex = ba.gettexture('logo') + icon_buffer = 1.1 + header = (0.7, 1.0, 0.7, 1.0) + header2 = (0.8, 0.8, 1.0, 1.0) + paragraph = (0.8, 0.8, 1.0, 1.0) + + txt = ba.Lstr(resource=self._r + '.welcomeText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate() + txt_scale = 1.4 + txt_maxwidth = 480 + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + scale=txt_scale, + flatness=0.5, + res_scale=1.5, + text=txt, + h_align='center', + color=header, + v_align='center', + maxwidth=txt_maxwidth) + txt_width = min( + txt_maxwidth, + _ba.get_string_width(txt, suppress_warning=True) * txt_scale) + + icon_size = 70 + hval2 = h - (txt_width * 0.5 + icon_size * 0.5 * icon_buffer) + ba.imagewidget(parent=self._subcontainer, + size=(icon_size, icon_size), + position=(hval2 - 0.5 * icon_size, + v - 0.45 * icon_size), + texture=logo_tex) + + force_test = False + app = ba.app + if (app.platform == 'android' + and app.subplatform == 'alibaba') or force_test: + v -= 120.0 + txtv = ( + '\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\x80\xe4\xb8\xaa\xe5\x8f\xaf' + '\xe4\xbb\xa5\xe5\x92\x8c\xe5\xae\xb6\xe4\xba\xba\xe6\x9c\x8b' + '\xe5\x8f\x8b\xe4\xb8\x80\xe8\xb5\xb7\xe7\x8e\xa9\xe7\x9a\x84' + '\xe6\xb8\xb8\xe6\x88\x8f,\xe5\x90\x8c\xe6\x97\xb6\xe6\x94\xaf' + '\xe6\x8c\x81\xe8\x81\x94 \xe2\x80\xa8\xe7\xbd\x91\xe5\xaf\xb9' + '\xe6\x88\x98\xe3\x80\x82\n' + '\xe5\xa6\x82\xe6\xb2\xa1\xe6\x9c\x89\xe6\xb8\xb8\xe6\x88\x8f' + '\xe6\x89\x8b\xe6\x9f\x84,\xe5\x8f\xaf\xe4\xbb\xa5\xe4\xbd\xbf' + '\xe7\x94\xa8\xe7\xa7\xbb\xe5\x8a\xa8\xe8\xae\xbe\xe5\xa4\x87' + '\xe6\x89\xab\xe7\xa0\x81\xe4\xb8\x8b\xe8\xbd\xbd\xe2\x80\x9c' + '\xe9\x98\xbf\xe9\x87\x8c\xc2' + '\xa0TV\xc2\xa0\xe5\x8a\xa9\xe6\x89' + '\x8b\xe2\x80\x9d\xe7\x94\xa8 \xe6\x9d\xa5\xe4\xbb\xa3\xe6\x9b' + '\xbf\xe5\xa4\x96\xe8\xae\xbe\xe3\x80\x82\n' + '\xe6\x9c\x80\xe5\xa4\x9a\xe6\x94\xaf\xe6\x8c\x81\xe6\x8e\xa5' + '\xe5\x85\xa5\xc2\xa08\xc2\xa0\xe4\xb8\xaa\xe5\xa4\x96\xe8' + '\xae\xbe') + ba.textwidget(parent=self._subcontainer, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=self._sub_width * 0.9, + position=(self._sub_width * 0.5, v - 180), + text=txtv) + ba.imagewidget(parent=self._subcontainer, + position=(self._sub_width - 320, v - 120), + size=(200, 200), + texture=ba.gettexture('aliControllerQR')) + ba.imagewidget(parent=self._subcontainer, + position=(90, v - 130), + size=(210, 210), + texture=ba.gettexture('multiplayerExamples')) + v -= 120.0 + + else: + v -= spacing * 50.0 + txt = ba.Lstr(resource=self._r + '.someDaysText').evaluate() + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + scale=1.2, + maxwidth=self._sub_width * 0.9, + text=txt, + h_align='center', + color=paragraph, + v_align='center', + flatness=1.0) + v -= (spacing * 25.0 + getres(self._r + '.someDaysExtraSpace')) + txt_scale = 0.66 + txt = ba.Lstr(resource=self._r + + '.orPunchingSomethingText').evaluate() + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + scale=txt_scale, + maxwidth=self._sub_width * 0.9, + text=txt, + h_align='center', + color=paragraph, + v_align='center', + flatness=1.0) + v -= (spacing * 27.0 + + getres(self._r + '.orPunchingSomethingExtraSpace')) + txt_scale = 1.0 + txt = ba.Lstr(resource=self._r + '.canHelpText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate() + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + scale=txt_scale, + flatness=1.0, + text=txt, + h_align='center', + color=paragraph, + v_align='center') + + v -= spacing * 70.0 + txt_scale = 1.0 + txt = ba.Lstr(resource=self._r + '.toGetTheMostText').evaluate() + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + scale=txt_scale, + maxwidth=self._sub_width * 0.9, + text=txt, + h_align='center', + color=header, + v_align='center', + flatness=1.0) + + v -= spacing * 40.0 + txt_scale = 0.74 + txt = ba.Lstr(resource=self._r + '.friendsText').evaluate() + hval2 = h - 220 + ba.textwidget(parent=self._subcontainer, + position=(hval2, v), + size=(0, 0), + scale=txt_scale, + maxwidth=100, + text=txt, + h_align='right', + color=header, + v_align='center', + flatness=1.0) + + txt = ba.Lstr(resource=self._r + '.friendsGoodText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate() + txt_scale = 0.7 + ba.textwidget(parent=self._subcontainer, + position=(hval2 + 10, v + 8), + size=(0, 0), + scale=txt_scale, + maxwidth=500, + text=txt, + h_align='left', + color=paragraph, + flatness=1.0) + + app = ba.app + + v -= spacing * 45.0 + txt = (ba.Lstr(resource=self._r + '.devicesText').evaluate() + if app.vr_mode else ba.Lstr(resource=self._r + + '.controllersText').evaluate()) + txt_scale = 0.74 + hval2 = h - 220 + ba.textwidget(parent=self._subcontainer, + position=(hval2, v), + size=(0, 0), + scale=txt_scale, + maxwidth=100, + text=txt, + h_align='right', + color=header, + v_align='center', + flatness=1.0) + + txt_scale = 0.7 + if not app.vr_mode: + txt = ba.Lstr(resource=self._r + '.controllersInfoText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText')), + ('${REMOTE_APP_NAME}', + get_remote_app_name())]).evaluate() + else: + txt = ba.Lstr(resource=self._r + '.devicesInfoText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText')) + ]).evaluate() + + ba.textwidget(parent=self._subcontainer, + position=(hval2 + 10, v + 8), + size=(0, 0), + scale=txt_scale, + maxwidth=500, + max_height=105, + text=txt, + h_align='left', + color=paragraph, + flatness=1.0) + + v -= spacing * 150.0 + + txt = ba.Lstr(resource=self._r + '.controlsText').evaluate() + txt_scale = 1.4 + txt_maxwidth = 480 + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + scale=txt_scale, + flatness=0.5, + text=txt, + h_align='center', + color=header, + v_align='center', + res_scale=1.5, + maxwidth=txt_maxwidth) + txt_width = min( + txt_maxwidth, + _ba.get_string_width(txt, suppress_warning=True) * txt_scale) + icon_size = 70 + + hval2 = h - (txt_width * 0.5 + icon_size * 0.5 * icon_buffer) + ba.imagewidget(parent=self._subcontainer, + size=(icon_size, icon_size), + position=(hval2 - 0.5 * icon_size, + v - 0.45 * icon_size), + texture=logo_tex) + + v -= spacing * 45.0 + + txt_scale = 0.7 + txt = ba.Lstr(resource=self._r + '.controlsSubtitleText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate() + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + scale=txt_scale, + maxwidth=self._sub_width * 0.9, + flatness=1.0, + text=txt, + h_align='center', + color=paragraph, + v_align='center') + v -= spacing * 160.0 + + sep = 70 + icon_size = 100 + # icon_size_2 = 30 + hval2 = h - sep + vval2 = v + ba.imagewidget(parent=self._subcontainer, + size=(icon_size, icon_size), + position=(hval2 - 0.5 * icon_size, + vval2 - 0.5 * icon_size), + texture=ba.gettexture('buttonPunch'), + color=(1, 0.7, 0.3)) + + txt_scale = getres(self._r + '.punchInfoTextScale') + txt = ba.Lstr(resource=self._r + '.punchInfoText').evaluate() + ba.textwidget(parent=self._subcontainer, + position=(h - sep - 185 + 70, v + 120), + size=(0, 0), + scale=txt_scale, + flatness=1.0, + text=txt, + h_align='center', + color=(1, 0.7, 0.3, 1.0), + v_align='top') + + hval2 = h + sep + vval2 = v + ba.imagewidget(parent=self._subcontainer, + size=(icon_size, icon_size), + position=(hval2 - 0.5 * icon_size, + vval2 - 0.5 * icon_size), + texture=ba.gettexture('buttonBomb'), + color=(1, 0.3, 0.3)) + + txt = ba.Lstr(resource=self._r + '.bombInfoText').evaluate() + txt_scale = getres(self._r + '.bombInfoTextScale') + ba.textwidget(parent=self._subcontainer, + position=(h + sep + 50 + 60, v - 35), + size=(0, 0), + scale=txt_scale, + flatness=1.0, + maxwidth=270, + text=txt, + h_align='center', + color=(1, 0.3, 0.3, 1.0), + v_align='top') + + hval2 = h + vval2 = v + sep + ba.imagewidget(parent=self._subcontainer, + size=(icon_size, icon_size), + position=(hval2 - 0.5 * icon_size, + vval2 - 0.5 * icon_size), + texture=ba.gettexture('buttonPickUp'), + color=(0.5, 0.5, 1)) + + txtl = ba.Lstr(resource=self._r + '.pickUpInfoText') + txt_scale = getres(self._r + '.pickUpInfoTextScale') + ba.textwidget(parent=self._subcontainer, + position=(h + 60 + 120, v + sep + 50), + size=(0, 0), + scale=txt_scale, + flatness=1.0, + text=txtl, + h_align='center', + color=(0.5, 0.5, 1, 1.0), + v_align='top') + + hval2 = h + vval2 = v - sep + ba.imagewidget(parent=self._subcontainer, + size=(icon_size, icon_size), + position=(hval2 - 0.5 * icon_size, + vval2 - 0.5 * icon_size), + texture=ba.gettexture('buttonJump'), + color=(0.4, 1, 0.4)) + + txt = ba.Lstr(resource=self._r + '.jumpInfoText').evaluate() + txt_scale = getres(self._r + '.jumpInfoTextScale') + ba.textwidget(parent=self._subcontainer, + position=(h - 250 + 75, v - sep - 15 + 30), + size=(0, 0), + scale=txt_scale, + flatness=1.0, + text=txt, + h_align='center', + color=(0.4, 1, 0.4, 1.0), + v_align='top') + + txt = ba.Lstr(resource=self._r + '.runInfoText').evaluate() + txt_scale = getres(self._r + '.runInfoTextScale') + ba.textwidget(parent=self._subcontainer, + position=(h, v - sep - 100), + size=(0, 0), + scale=txt_scale, + maxwidth=self._sub_width * 0.93, + flatness=1.0, + text=txt, + h_align='center', + color=(0.7, 0.7, 1.0, 1.0), + v_align='center') + + v -= spacing * 280.0 + + txt = ba.Lstr(resource=self._r + '.powerupsText').evaluate() + txt_scale = 1.4 + txt_maxwidth = 480 + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + scale=txt_scale, + flatness=0.5, + text=txt, + h_align='center', + color=header, + v_align='center', + maxwidth=txt_maxwidth) + txt_width = min( + txt_maxwidth, + _ba.get_string_width(txt, suppress_warning=True) * txt_scale) + icon_size = 70 + hval2 = h - (txt_width * 0.5 + icon_size * 0.5 * icon_buffer) + ba.imagewidget(parent=self._subcontainer, + size=(icon_size, icon_size), + position=(hval2 - 0.5 * icon_size, + v - 0.45 * icon_size), + texture=logo_tex) + + v -= spacing * 50.0 + txt_scale = getres(self._r + '.powerupsSubtitleTextScale') + txt = ba.Lstr(resource=self._r + '.powerupsSubtitleText').evaluate() + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + scale=txt_scale, + maxwidth=self._sub_width * 0.9, + text=txt, + h_align='center', + color=paragraph, + v_align='center', + flatness=1.0) + + v -= spacing * 1.0 + + mm1 = -270 + mm2 = -215 + mm3 = 0 + icon_size = 50 + shadow_size = 80 + shadow_offs_x = 3 + shadow_offs_y = -4 + t_big = 1.1 + t_small = 0.65 + + shadow_tex = ba.gettexture('shadowSharp') + + for tex in [ + 'powerupPunch', 'powerupShield', 'powerupBomb', + 'powerupHealth', 'powerupIceBombs', 'powerupImpactBombs', + 'powerupStickyBombs', 'powerupLandMines', 'powerupCurse' + ]: + name = ba.Lstr(resource=self._r + '.' + tex + 'NameText') + desc = ba.Lstr(resource=self._r + '.' + tex + 'DescriptionText') + + v -= spacing * 60.0 + + ba.imagewidget( + parent=self._subcontainer, + size=(shadow_size, shadow_size), + position=(h + mm1 + shadow_offs_x - 0.5 * shadow_size, + v + shadow_offs_y - 0.5 * shadow_size), + texture=shadow_tex, + color=(0, 0, 0), + opacity=0.5) + ba.imagewidget(parent=self._subcontainer, + size=(icon_size, icon_size), + position=(h + mm1 - 0.5 * icon_size, + v - 0.5 * icon_size), + texture=ba.gettexture(tex)) + + txt_scale = t_big + txtl = name + ba.textwidget(parent=self._subcontainer, + position=(h + mm2, v + 3), + size=(0, 0), + scale=txt_scale, + maxwidth=200, + flatness=1.0, + text=txtl, + h_align='left', + color=header2, + v_align='center') + txt_scale = t_small + txtl = desc + ba.textwidget(parent=self._subcontainer, + position=(h + mm3, v), + size=(0, 0), + scale=txt_scale, + maxwidth=300, + flatness=1.0, + text=txtl, + h_align='left', + color=paragraph, + v_align='center', + res_scale=0.5) + + def _close(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.mainmenu import MainMenuWindow + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + if self._main_menu: + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/iconpicker.py b/dist/ba_data/python/bastd/ui/iconpicker.py new file mode 100644 index 0000000..1d1820f --- /dev/null +++ b/dist/ba_data/python/bastd/ui/iconpicker.py @@ -0,0 +1,160 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a picker for icons.""" + +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + from typing import Any, Tuple, Sequence + + +class IconPicker(popup.PopupWindow): + """Picker for icons.""" + + def __init__(self, + parent: ba.Widget, + position: Tuple[float, float] = (0.0, 0.0), + delegate: Any = None, + scale: float = None, + offset: Tuple[float, float] = (0.0, 0.0), + tint_color: Sequence[float] = (1.0, 1.0, 1.0), + tint2_color: Sequence[float] = (1.0, 1.0, 1.0), + selected_icon: str = None): + # pylint: disable=too-many-locals + del parent # unused here + del tint_color # unused_here + del tint2_color # unused here + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (1.85 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + + self._delegate = delegate + self._transitioning_out = False + + self._icons = [ba.charstr(ba.SpecialChar.LOGO) + ] + ba.app.accounts.get_purchased_icons() + count = len(self._icons) + columns = 4 + rows = int(math.ceil(float(count) / columns)) + + button_width = 50 + button_height = 50 + button_buffer_h = 10 + button_buffer_v = 5 + + self._width = (10 + columns * (button_width + 2 * button_buffer_h) * + (1.0 / 0.95) * (1.0 / 0.8)) + self._height = (self._width * + (0.8 if uiscale is ba.UIScale.SMALL else 1.06)) + + self._scroll_width = self._width * 0.8 + self._scroll_height = self._height * 0.8 + self._scroll_position = ((self._width - self._scroll_width) * 0.5, + (self._height - self._scroll_height) * 0.5) + + # creates our _root_widget + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=(0.5, 0.5, 0.5), + offset=offset, + focus_position=self._scroll_position, + focus_size=(self._scroll_width, + self._scroll_height)) + + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + size=(self._scroll_width, + self._scroll_height), + color=(0.55, 0.55, 0.55), + highlight=False, + position=self._scroll_position) + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 5 + rows * (button_height + + 2 * button_buffer_v) + 100 + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + index = 0 + for y in range(rows): + for x in range(columns): + pos = (x * (button_width + 2 * button_buffer_h) + + button_buffer_h, self._sub_height - (y + 1) * + (button_height + 2 * button_buffer_v) + 0) + btn = ba.buttonwidget(parent=self._subcontainer, + button_type='square', + size=(button_width, button_height), + autoselect=True, + text_scale=1.2, + label='', + color=(0.65, 0.65, 0.65), + on_activate_call=ba.Call( + self._select_icon, + self._icons[index]), + position=pos) + ba.textwidget(parent=self._subcontainer, + h_align='center', + v_align='center', + size=(0, 0), + position=(pos[0] + 0.5 * button_width - 1, + pos[1] + 15), + draw_controller=btn, + text=self._icons[index], + scale=1.8) + ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + if self._icons[index] == selected_icon: + ba.containerwidget(edit=self._subcontainer, + selected_child=btn, + visible_child=btn) + index += 1 + + if index >= count: + break + if index >= count: + break + self._get_more_icons_button = btn = ba.buttonwidget( + parent=self._subcontainer, + size=(self._sub_width * 0.8, 60), + position=(self._sub_width * 0.1, 30), + label=ba.Lstr(resource='editProfileWindow.getMoreIconsText'), + on_activate_call=self._on_store_press, + color=(0.6, 0.6, 0.6), + textcolor=(0.8, 0.8, 0.8), + autoselect=True) + ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) + + def _on_store_press(self) -> None: + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.store.browser import StoreBrowserWindow + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + self._transition_out() + StoreBrowserWindow(modal=True, + show_tab=StoreBrowserWindow.TabID.ICONS, + origin_widget=self._get_more_icons_button) + + def _select_icon(self, icon: str) -> None: + if self._delegate is not None: + self._delegate.on_icon_picker_pick(icon) + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/kiosk.py b/dist/ba_data/python/bastd/ui/kiosk.py new file mode 100644 index 0000000..c75caec --- /dev/null +++ b/dist/ba_data/python/bastd/ui/kiosk.py @@ -0,0 +1,448 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for running the game in kiosk mode.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Optional + + +class KioskWindow(ba.Window): + """Kiosk mode window.""" + + def __init__(self, transition: str = 'in_right'): + # pylint: disable=too-many-locals, too-many-statements + from bastd.ui.confirm import QuitWindow + self._width = 720.0 + self._height = 340.0 + + def _do_cancel() -> None: + QuitWindow(swish=True, back=True) + + super().__init__( + root_widget=ba.containerwidget(size=(self._width, self._height), + transition=transition, + on_cancel_call=_do_cancel, + background=False, + stack_offset=(0, -130))) + + self._r = 'kioskWindow' + + self._show_multiplayer = False + + # Let's reset all random player names every time we hit the main menu. + _ba.reset_random_player_names() + + # Reset achievements too (at least locally). + ba.app.config['Achievements'] = {} + + t_delay_base = 0.0 + t_delay_scale = 0.0 + if not ba.app.did_menu_intro: + t_delay_base = 1.0 + t_delay_scale = 1.0 + + model_opaque = ba.getmodel('level_select_button_opaque') + model_transparent = ba.getmodel('level_select_button_transparent') + mask_tex = ba.gettexture('mapPreviewMask') + + y_extra = 130.0 + (0.0 if self._show_multiplayer else -130.0) + b_width = 250.0 + b_height = 200.0 + b_space = 280.0 + b_v = 80.0 + y_extra + label_height = 130.0 + y_extra + img_width = 180.0 + img_v = 158.0 + y_extra + + if self._show_multiplayer: + tdelay = t_delay_base + t_delay_scale * 1.3 + ba.textwidget( + parent=self._root_widget, + size=(0, 0), + position=(self._width * 0.5, self._height + y_extra - 44), + transition_delay=tdelay, + text=ba.Lstr(resource=self._r + '.singlePlayerExamplesText'), + flatness=1.0, + scale=1.2, + h_align='center', + v_align='center', + shadow=1.0) + else: + tdelay = t_delay_base + t_delay_scale * 0.7 + ba.textwidget( + parent=self._root_widget, + size=(0, 0), + position=(self._width * 0.5, self._height + y_extra - 34), + transition_delay=tdelay, + text=(ba.Lstr(resource='demoText', + fallback_resource='mainMenu.demoMenuText') + if ba.app.demo_mode else 'ARCADE'), + flatness=1.0, + scale=1.2, + h_align='center', + v_align='center', + shadow=1.0) + h = self._width * 0.5 - b_space + tdelay = t_delay_base + t_delay_scale * 0.7 + self._b1 = btn = ba.buttonwidget(parent=self._root_widget, + autoselect=True, + size=(b_width, b_height), + on_activate_call=ba.Call( + self._do_game, 'easy'), + transition_delay=tdelay, + position=(h - b_width * 0.5, b_v), + label='', + button_type='square') + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + transition_delay=tdelay, + size=(0, 0), + position=(h, label_height), + maxwidth=b_width * 0.7, + text=ba.Lstr(resource=self._r + '.easyText'), + scale=1.3, + h_align='center', + v_align='center') + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + size=(img_width, 0.5 * img_width), + transition_delay=tdelay, + position=(h - img_width * 0.5, img_v), + texture=ba.gettexture('doomShroomPreview'), + model_opaque=model_opaque, + model_transparent=model_transparent, + mask_texture=mask_tex) + h = self._width * 0.5 + tdelay = t_delay_base + t_delay_scale * 0.65 + self._b2 = btn = ba.buttonwidget(parent=self._root_widget, + autoselect=True, + size=(b_width, b_height), + on_activate_call=ba.Call( + self._do_game, 'medium'), + position=(h - b_width * 0.5, b_v), + label='', + button_type='square', + transition_delay=tdelay) + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + transition_delay=tdelay, + size=(0, 0), + position=(h, label_height), + maxwidth=b_width * 0.7, + text=ba.Lstr(resource=self._r + '.mediumText'), + scale=1.3, + h_align='center', + v_align='center') + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + size=(img_width, 0.5 * img_width), + transition_delay=tdelay, + position=(h - img_width * 0.5, img_v), + texture=ba.gettexture('footballStadiumPreview'), + model_opaque=model_opaque, + model_transparent=model_transparent, + mask_texture=mask_tex) + h = self._width * 0.5 + b_space + tdelay = t_delay_base + t_delay_scale * 0.6 + self._b3 = btn = ba.buttonwidget(parent=self._root_widget, + autoselect=True, + size=(b_width, b_height), + on_activate_call=ba.Call( + self._do_game, 'hard'), + transition_delay=tdelay, + position=(h - b_width * 0.5, b_v), + label='', + button_type='square') + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + transition_delay=tdelay, + size=(0, 0), + position=(h, label_height), + maxwidth=b_width * 0.7, + text='Hard', + scale=1.3, + h_align='center', + v_align='center') + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + transition_delay=tdelay, + size=(img_width, 0.5 * img_width), + position=(h - img_width * 0.5, img_v), + texture=ba.gettexture('courtyardPreview'), + model_opaque=model_opaque, + model_transparent=model_transparent, + mask_texture=mask_tex) + if not ba.app.did_menu_intro: + ba.app.did_menu_intro = True + + self._b4: Optional[ba.Widget] + self._b5: Optional[ba.Widget] + self._b6: Optional[ba.Widget] + + if bool(False): + ba.textwidget( + parent=self._root_widget, + size=(0, 0), + position=(self._width * 0.5, self._height + y_extra - 44), + transition_delay=tdelay, + text=ba.Lstr(resource=self._r + '.versusExamplesText'), + flatness=1.0, + scale=1.2, + h_align='center', + v_align='center', + shadow=1.0) + h = self._width * 0.5 - b_space + tdelay = t_delay_base + t_delay_scale * 0.7 + self._b4 = btn = ba.buttonwidget(parent=self._root_widget, + autoselect=True, + size=(b_width, b_height), + on_activate_call=ba.Call( + self._do_game, 'ctf'), + transition_delay=tdelay, + position=(h - b_width * 0.5, b_v), + label='', + button_type='square') + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + transition_delay=tdelay, + size=(0, 0), + position=(h, label_height), + maxwidth=b_width * 0.7, + text=ba.Lstr(translate=('gameNames', + 'Capture the Flag')), + scale=1.3, + h_align='center', + v_align='center') + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + size=(img_width, 0.5 * img_width), + transition_delay=tdelay, + position=(h - img_width * 0.5, img_v), + texture=ba.gettexture('bridgitPreview'), + model_opaque=model_opaque, + model_transparent=model_transparent, + mask_texture=mask_tex) + + h = self._width * 0.5 + tdelay = t_delay_base + t_delay_scale * 0.65 + self._b5 = btn = ba.buttonwidget(parent=self._root_widget, + autoselect=True, + size=(b_width, b_height), + on_activate_call=ba.Call( + self._do_game, 'hockey'), + position=(h - b_width * 0.5, b_v), + label='', + button_type='square', + transition_delay=tdelay) + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + transition_delay=tdelay, + size=(0, 0), + position=(h, label_height), + maxwidth=b_width * 0.7, + text=ba.Lstr(translate=('gameNames', 'Hockey')), + scale=1.3, + h_align='center', + v_align='center') + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + size=(img_width, 0.5 * img_width), + transition_delay=tdelay, + position=(h - img_width * 0.5, img_v), + texture=ba.gettexture('hockeyStadiumPreview'), + model_opaque=model_opaque, + model_transparent=model_transparent, + mask_texture=mask_tex) + h = self._width * 0.5 + b_space + tdelay = t_delay_base + t_delay_scale * 0.6 + self._b6 = btn = ba.buttonwidget(parent=self._root_widget, + autoselect=True, + size=(b_width, b_height), + on_activate_call=ba.Call( + self._do_game, 'epic'), + transition_delay=tdelay, + position=(h - b_width * 0.5, b_v), + label='', + button_type='square') + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + transition_delay=tdelay, + size=(0, 0), + position=(h, label_height), + maxwidth=b_width * 0.7, + text=ba.Lstr(resource=self._r + '.epicModeText'), + scale=1.3, + h_align='center', + v_align='center') + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + transition_delay=tdelay, + size=(img_width, 0.5 * img_width), + position=(h - img_width * 0.5, img_v), + texture=ba.gettexture('tipTopPreview'), + model_opaque=model_opaque, + model_transparent=model_transparent, + mask_texture=mask_tex) + else: + self._b4 = self._b5 = self._b6 = None + + self._b7: Optional[ba.Widget] + if ba.app.arcade_mode: + self._b7 = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + size=(b_width, 50), + color=(0.45, 0.55, 0.45), + textcolor=(0.7, 0.8, 0.7), + scale=0.5, + position=(self._width * 0.5 - 60.0, b_v - 70.0), + transition_delay=tdelay, + label=ba.Lstr(resource=self._r + '.fullMenuText'), + on_activate_call=self._do_full_menu) + else: + self._b7 = None + self._restore_state() + self._update() + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + + def _restore_state(self) -> None: + sel_name = ba.app.ui.window_states.get(type(self)) + sel: Optional[ba.Widget] + if sel_name == 'b1': + sel = self._b1 + elif sel_name == 'b2': + sel = self._b2 + elif sel_name == 'b3': + sel = self._b3 + elif sel_name == 'b4': + sel = self._b4 + elif sel_name == 'b5': + sel = self._b5 + elif sel_name == 'b6': + sel = self._b6 + elif sel_name == 'b7': + sel = self._b7 + else: + sel = self._b1 + if sel: + ba.containerwidget(edit=self._root_widget, selected_child=sel) + + def _save_state(self) -> None: + sel = self._root_widget.get_selected_child() + if sel == self._b1: + sel_name = 'b1' + elif sel == self._b2: + sel_name = 'b2' + elif sel == self._b3: + sel_name = 'b3' + elif sel == self._b4: + sel_name = 'b4' + elif sel == self._b5: + sel_name = 'b5' + elif sel == self._b6: + sel_name = 'b6' + elif sel == self._b7: + sel_name = 'b7' + else: + sel_name = 'b1' + ba.app.ui.window_states[type(self)] = sel_name + + def _update(self) -> None: + # Kiosk-mode is designed to be used signed-out... try for force + # the issue. + if _ba.get_account_state() == 'signed_in': + # _bs.sign_out() + # FIXME: Try to delete player profiles here too. + pass + else: + # Also make sure there's no player profiles. + appconfig = ba.app.config + appconfig['Player Profiles'] = {} + + def _do_game(self, mode: str) -> None: + self._save_state() + if mode in ['epic', 'ctf', 'hockey']: + appconfig = ba.app.config + if 'Team Tournament Playlists' not in appconfig: + appconfig['Team Tournament Playlists'] = {} + if 'Free-for-All Playlists' not in appconfig: + appconfig['Free-for-All Playlists'] = {} + appconfig['Show Tutorial'] = False + if mode == 'epic': + appconfig['Free-for-All Playlists']['Just Epic Elim'] = [{ + 'settings': { + 'Epic Mode': 1, + 'Lives Per Player': 1, + 'Respawn Times': 1.0, + 'Time Limit': 0, + 'map': 'Tip Top' + }, + 'type': 'bs_elimination.EliminationGame' + }] + appconfig['Free-for-All Playlist Selection'] = 'Just Epic Elim' + _ba.fade_screen(False, + endcall=ba.Call( + ba.pushcall, + ba.Call(_ba.new_host_session, + ba.FreeForAllSession))) + else: + if mode == 'ctf': + appconfig['Team Tournament Playlists']['Just CTF'] = [{ + 'settings': { + 'Epic Mode': False, + 'Flag Idle Return Time': 30, + 'Flag Touch Return Time': 0, + 'Respawn Times': 1.0, + 'Score to Win': 3, + 'Time Limit': 0, + 'map': 'Bridgit' + }, + 'type': 'bs_capture_the_flag.CTFGame' + }] + appconfig[ + 'Team Tournament Playlist Selection'] = 'Just CTF' + else: + appconfig['Team Tournament Playlists']['Just Hockey'] = [{ + 'settings': { + 'Respawn Times': 1.0, + 'Score to Win': 1, + 'Time Limit': 0, + 'map': 'Hockey Stadium' + }, + 'type': 'bs_hockey.HockeyGame' + }] + appconfig['Team Tournament Playlist Selection'] = ( + 'Just Hockey') + _ba.fade_screen(False, + endcall=ba.Call( + ba.pushcall, + ba.Call(_ba.new_host_session, + ba.DualTeamSession))) + ba.containerwidget(edit=self._root_widget, transition='out_left') + return + + game = ('Easy:Onslaught Training' + if mode == 'easy' else 'Easy:Rookie Football' + if mode == 'medium' else 'Easy:Uber Onslaught') + cfg = ba.app.config + cfg['Selected Coop Game'] = game + cfg.commit() + if ba.app.launch_coop_game(game, force=True): + ba.containerwidget(edit=self._root_widget, transition='out_left') + + def _do_full_menu(self) -> None: + from bastd.ui.mainmenu import MainMenuWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.did_menu_intro = True # prevent delayed transition-in + ba.app.ui.set_main_menu_window(MainMenuWindow().get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/league/__init__.py b/dist/ba_data/python/bastd/ui/league/__init__.py new file mode 100644 index 0000000..bb57266 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/league/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""League related UI functionality.""" diff --git a/dist/ba_data/python/bastd/ui/league/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/league/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..bfa22e4 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/league/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/league/__pycache__/rankbutton.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/league/__pycache__/rankbutton.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9836d09 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/league/__pycache__/rankbutton.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/league/__pycache__/rankwindow.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/league/__pycache__/rankwindow.cpython-38.opt-1.pyc new file mode 100644 index 0000000..020fc9e Binary files /dev/null and b/dist/ba_data/python/bastd/ui/league/__pycache__/rankwindow.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/league/rankbutton.py b/dist/ba_data/python/bastd/ui/league/rankbutton.py new file mode 100644 index 0000000..f299f5f --- /dev/null +++ b/dist/ba_data/python/bastd/ui/league/rankbutton.py @@ -0,0 +1,377 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a button showing league rank.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Tuple, Optional, Callable, Dict, Union + + +class LeagueRankButton: + """Button showing league rank.""" + + def __init__(self, + parent: ba.Widget, + position: Tuple[float, float], + size: Tuple[float, float], + scale: float, + on_activate_call: Callable[[], Any] = None, + transition_delay: float = None, + color: Tuple[float, float, float] = None, + textcolor: Tuple[float, float, float] = None, + smooth_update_delay: float = None): + if on_activate_call is None: + on_activate_call = ba.WeakCall(self._default_on_activate_call) + self._on_activate_call = on_activate_call + if smooth_update_delay is None: + smooth_update_delay = 1000 + self._smooth_update_delay = smooth_update_delay + self._size = size + self._scale = scale + if color is None: + color = (0.5, 0.6, 0.5) + if textcolor is None: + textcolor = (1, 1, 1) + self._color = color + self._textcolor = textcolor + self._header_color = (0.8, 0.8, 2.0) + self._parent = parent + self._position: Tuple[float, float] = (0.0, 0.0) + + self._button = ba.buttonwidget(parent=parent, + size=size, + label='', + button_type='square', + scale=scale, + autoselect=True, + on_activate_call=self._on_activate, + transition_delay=transition_delay, + color=color) + + self._title_text = ba.textwidget( + parent=parent, + size=(0, 0), + draw_controller=self._button, + h_align='center', + v_align='center', + maxwidth=size[0] * scale * 0.85, + text=ba.Lstr( + resource='league.leagueRankText', + fallback_resource='coopSelectWindow.powerRankingText'), + color=self._header_color, + flatness=1.0, + shadow=1.0, + scale=scale * 0.5, + transition_delay=transition_delay) + + self._value_text = ba.textwidget(parent=parent, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=size[0] * scale * 0.85, + text='-', + draw_controller=self._button, + big=True, + scale=scale, + transition_delay=transition_delay, + color=textcolor) + + self._smooth_percent: Optional[float] = None + self._percent: Optional[int] = None + self._smooth_rank: Optional[float] = None + self._rank: Optional[int] = None + self._ticking_node: Optional[ba.Node] = None + self._smooth_increase_speed = 1.0 + self._league: Optional[str] = None + self._improvement_text: Optional[str] = None + + self._smooth_update_timer: Optional[ba.Timer] = None + + # Take note of our account state; we'll refresh later if this changes. + self._account_state_num = _ba.get_account_state_num() + self._last_power_ranking_query_time: Optional[float] = None + self._doing_power_ranking_query = False + self.set_position(position) + self._bg_flash = False + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + self._update() + + # If we've got cached power-ranking data already, apply it. + data = ba.app.accounts.get_cached_league_rank_data() + if data is not None: + self._update_for_league_rank_data(data) + + def _on_activate(self) -> None: + _ba.increment_analytics_count('League rank button press') + self._on_activate_call() + + def __del__(self) -> None: + if self._ticking_node is not None: + self._ticking_node.delete() + + def _start_smooth_update(self) -> None: + self._smooth_update_timer = ba.Timer(0.05, + ba.WeakCall(self._smooth_update), + repeat=True, + timetype=ba.TimeType.REAL) + + def _smooth_update(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + try: + if not self._button: + return + if self._ticking_node is None: + with ba.Context('ui'): + self._ticking_node = ba.newnode( + 'sound', + attrs={ + 'sound': ba.getsound('scoreIncrease'), + 'positional': False + }) + self._bg_flash = (not self._bg_flash) + color_used = ((self._color[0] * 2, self._color[1] * 2, + self._color[2] * + 2) if self._bg_flash else self._color) + textcolor_used = ((1, 1, 1) if self._bg_flash else self._textcolor) + header_color_used = ((1, 1, + 1) if self._bg_flash else self._header_color) + + if self._rank is not None: + assert self._smooth_rank is not None + self._smooth_rank -= 1.0 * self._smooth_increase_speed + finished = (int(self._smooth_rank) <= self._rank) + elif self._smooth_percent is not None: + self._smooth_percent += 1.0 * self._smooth_increase_speed + assert self._percent is not None + finished = (int(self._smooth_percent) >= self._percent) + else: + finished = True + if finished: + if self._rank is not None: + self._smooth_rank = float(self._rank) + elif self._percent is not None: + self._smooth_percent = float(self._percent) + color_used = self._color + textcolor_used = self._textcolor + self._smooth_update_timer = None + if self._ticking_node is not None: + self._ticking_node.delete() + self._ticking_node = None + ba.playsound(ba.getsound('cashRegister2')) + assert self._improvement_text is not None + diff_text = ba.textwidget( + parent=self._parent, + size=(0, 0), + h_align='center', + v_align='center', + text='+' + self._improvement_text + '!', + position=(self._position[0] + + self._size[0] * 0.5 * self._scale, + self._position[1] + + self._size[1] * -0.2 * self._scale), + color=(0, 1, 0), + flatness=1.0, + shadow=0.0, + scale=self._scale * 0.7) + + def safe_delete(widget: ba.Widget) -> None: + if widget: + widget.delete() + + ba.timer(2.0, + ba.Call(safe_delete, diff_text), + timetype=ba.TimeType.REAL) + status_text: Union[str, ba.Lstr] + if self._rank is not None: + assert self._smooth_rank is not None + status_text = ba.Lstr(resource='numberText', + subs=[('${NUMBER}', + str(int(self._smooth_rank)))]) + elif self._smooth_percent is not None: + status_text = str(int(self._smooth_percent)) + '%' + else: + status_text = '-' + ba.textwidget(edit=self._value_text, + text=status_text, + color=textcolor_used) + ba.textwidget(edit=self._title_text, color=header_color_used) + ba.buttonwidget(edit=self._button, color=color_used) + + except Exception: + ba.print_exception('Error doing smooth update.') + self._smooth_update_timer = None + + def _update_for_league_rank_data(self, data: Optional[Dict[str, + Any]]) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + + # If our button has died, ignore. + if not self._button: + return + + status_text: Union[str, ba.Lstr] + + in_top = data is not None and data['rank'] is not None + do_percent = False + if data is None or _ba.get_account_state() != 'signed_in': + self._percent = self._rank = None + status_text = '-' + elif in_top: + self._percent = None + self._rank = data['rank'] + prev_league = self._league + self._league = data['l'] + + # If this is the first set, league has changed, or rank has gotten + # worse, snap the smooth value immediately. + assert self._rank is not None + if (self._smooth_rank is None or prev_league != self._league + or self._rank > int(self._smooth_rank)): + self._smooth_rank = float(self._rank) + status_text = ba.Lstr(resource='numberText', + subs=[('${NUMBER}', + str(int(self._smooth_rank)))]) + else: + try: + if not data['scores'] or data['scores'][-1][1] <= 0: + self._percent = self._rank = None + status_text = '-' + else: + our_points = ba.app.accounts.get_league_rank_points(data) + progress = float(our_points) / data['scores'][-1][1] + self._percent = int(progress * 100.0) + self._rank = None + do_percent = True + prev_league = self._league + self._league = data['l'] + + # If this is the first set, league has changed, or percent + # has decreased, snap the smooth value immediately. + if (self._smooth_percent is None + or prev_league != self._league + or self._percent < int(self._smooth_percent)): + self._smooth_percent = float(self._percent) + status_text = str(int(self._smooth_percent)) + '%' + + except Exception: + ba.print_exception('Error updating power ranking.') + self._percent = self._rank = None + status_text = '-' + + # If we're doing a smooth update, set a timer. + if (self._rank is not None and self._smooth_rank is not None + and int(self._smooth_rank) != self._rank): + self._improvement_text = str(-(int(self._rank) - + int(self._smooth_rank))) + diff = abs(self._rank - self._smooth_rank) + if diff > 100: + self._smooth_increase_speed = diff / 80.0 + elif diff > 50: + self._smooth_increase_speed = diff / 70.0 + elif diff > 25: + self._smooth_increase_speed = diff / 55.0 + else: + self._smooth_increase_speed = diff / 40.0 + self._smooth_increase_speed = max(0.4, self._smooth_increase_speed) + ba.timer(self._smooth_update_delay, + ba.WeakCall(self._start_smooth_update), + timetype=ba.TimeType.REAL, + timeformat=ba.TimeFormat.MILLISECONDS) + + if (self._percent is not None and self._smooth_percent is not None + and int(self._smooth_percent) != self._percent): + self._improvement_text = str( + (int(self._percent) - int(self._smooth_percent))) + self._smooth_increase_speed = 0.3 + ba.timer(self._smooth_update_delay, + ba.WeakCall(self._start_smooth_update), + timetype=ba.TimeType.REAL, + timeformat=ba.TimeFormat.MILLISECONDS) + + if do_percent: + ba.textwidget( + edit=self._title_text, + text=ba.Lstr(resource='coopSelectWindow.toRankedText')) + else: + try: + assert data is not None + txt = ba.Lstr( + resource='league.leagueFullText', + subs=[ + ( + '${NAME}', + ba.Lstr(translate=('leagueNames', data['l']['n'])), + ), + ], + ) + t_color = data['l']['c'] + except Exception: + txt = ba.Lstr( + resource='league.leagueRankText', + fallback_resource='coopSelectWindow.powerRankingText') + t_color = ba.app.ui.title_color + ba.textwidget(edit=self._title_text, text=txt, color=t_color) + ba.textwidget(edit=self._value_text, text=status_text) + + def _on_power_ranking_query_response( + self, data: Optional[Dict[str, Any]]) -> None: + self._doing_power_ranking_query = False + ba.app.accounts.cache_league_rank_data(data) + self._update_for_league_rank_data(data) + + def _update(self) -> None: + cur_time = ba.time(ba.TimeType.REAL) + + # If our account state has changed, refresh our UI. + account_state_num = _ba.get_account_state_num() + if account_state_num != self._account_state_num: + self._account_state_num = account_state_num + + # And power ranking too... + if not self._doing_power_ranking_query: + self._last_power_ranking_query_time = None + + # Send off a new power-ranking query if its been + # long enough or whatnot. + if not self._doing_power_ranking_query and ( + self._last_power_ranking_query_time is None + or cur_time - self._last_power_ranking_query_time > 30.0): + self._last_power_ranking_query_time = cur_time + self._doing_power_ranking_query = True + _ba.power_ranking_query( + callback=ba.WeakCall(self._on_power_ranking_query_response)) + + def _default_on_activate_call(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.league.rankwindow import LeagueRankWindow + LeagueRankWindow(modal=True, origin_widget=self._button) + + def set_position(self, position: Tuple[float, float]) -> None: + """Set the button's position.""" + self._position = position + if not self._button: + return + ba.buttonwidget(edit=self._button, position=self._position) + ba.textwidget( + edit=self._title_text, + position=(self._position[0] + self._size[0] * 0.5 * self._scale, + self._position[1] + self._size[1] * 0.82 * self._scale)) + ba.textwidget( + edit=self._value_text, + position=(self._position[0] + self._size[0] * 0.5 * self._scale, + self._position[1] + self._size[1] * 0.36 * self._scale)) + + def get_button(self) -> ba.Widget: + """Return the underlying button ba.Widget>""" + return self._button diff --git a/dist/ba_data/python/bastd/ui/league/rankwindow.py b/dist/ba_data/python/bastd/ui/league/rankwindow.py new file mode 100644 index 0000000..0ee0c9d --- /dev/null +++ b/dist/ba_data/python/bastd/ui/league/rankwindow.py @@ -0,0 +1,921 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI related to league rank.""" + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui import popup as popup_ui + +if TYPE_CHECKING: + from typing import Any, Optional, Tuple, List, Dict, Union + + +class LeagueRankWindow(ba.Window): + """Window for showing league rank.""" + + def __init__(self, + transition: str = 'in_right', + modal: bool = False, + origin_widget: ba.Widget = None): + ba.set_analytics_screen('League Rank Window') + + self._league_rank_data: Optional[Dict[str, Any]] = None + self._modal = modal + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + uiscale = ba.app.ui.uiscale + self._width = 1320 if uiscale is ba.UIScale.SMALL else 1120 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (657 if uiscale is ba.UIScale.SMALL else + 710 if uiscale is ba.UIScale.MEDIUM else 800) + self._r = 'coopSelectWindow' + self._rdict = ba.app.lang.get_resource(self._r) + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + + self._league_url_arg = '' + + self._is_current_season = False + self._can_do_more_button = True + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + stack_offset=(0, -15) if uiscale is ba.UIScale.SMALL else ( + 0, 10) if uiscale is ba.UIScale.MEDIUM else (0, 0), + transition=transition, + scale_origin_stack_offset=scale_origin, + scale=(1.2 if uiscale is ba.UIScale.SMALL else + 0.93 if uiscale is ba.UIScale.MEDIUM else 0.8))) + + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(75 + x_inset, self._height - 87 - + (4 if uiscale is ba.UIScale.SMALL else 0)), + size=(120, 60), + scale=1.2, + autoselect=True, + label=ba.Lstr(resource='doneText' if self._modal else 'backText'), + button_type=None if self._modal else 'back', + on_activate_call=self._back) + + self._title_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 56), + size=(0, 0), + text=ba.Lstr( + resource='league.leagueRankText', + fallback_resource='coopSelectWindow.powerRankingText'), + h_align='center', + color=ba.app.ui.title_color, + scale=1.4, + maxwidth=600, + v_align='center') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + position=(75 + x_inset, self._height - 87 - + (2 if uiscale is ba.UIScale.SMALL else 0)), + size=(60, 55), + label=ba.charstr(ba.SpecialChar.BACK)) + + self._scroll_width = self._width - (130 + 2 * x_inset) + self._scroll_height = self._height - 160 + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + highlight=False, + position=(65 + x_inset, 70), + size=(self._scroll_width, + self._scroll_height), + center_small_content=True) + ba.widget(edit=self._scrollwidget, autoselect=True) + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._back_button, + selected_child=self._back_button) + + self._last_power_ranking_query_time: Optional[float] = None + self._doing_power_ranking_query = False + + self._subcontainer: Optional[ba.Widget] = None + self._subcontainerwidth = 800 + self._subcontainerheight = 483 + self._power_ranking_score_widgets: List[ba.Widget] = [] + + self._season_popup_menu: Optional[popup_ui.PopupMenu] = None + self._requested_season: Optional[str] = None + self._season: Optional[str] = None + + # take note of our account state; we'll refresh later if this changes + self._account_state = _ba.get_account_state() + + self._refresh() + self._restore_state() + + # if we've got cached power-ranking data already, display it + info = ba.app.accounts.get_cached_league_rank_data() + if info is not None: + self._update_for_league_rank_data(info) + + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + self._update(show=(info is None)) + + def _on_achievements_press(self) -> None: + from bastd.ui import achievements + # only allow this for all-time or the current season + # (we currently don't keep specific achievement data for old seasons) + if self._season == 'a' or self._is_current_season: + achievements.AchievementsWindow( + position=(self._power_ranking_achievements_button. + get_screen_space_center())) + else: + ba.screenmessage(ba.Lstr( + resource='achievementsUnavailableForOldSeasonsText', + fallback_resource='unavailableText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + + def _on_activity_mult_press(self) -> None: + from bastd.ui import confirm + txt = ba.Lstr( + resource='coopSelectWindow.activenessAllTimeInfoText' + if self._season == 'a' else 'coopSelectWindow.activenessInfoText', + subs=[('${MAX}', + str(_ba.get_account_misc_read_val('activenessMax', 1.0)))]) + confirm.ConfirmWindow(txt, + cancel_button=False, + width=460, + height=150, + origin_widget=self._activity_mult_button) + + def _on_pro_mult_press(self) -> None: + from bastd.ui import confirm + txt = ba.Lstr( + resource='coopSelectWindow.proMultInfoText', + subs=[ + ('${PERCENT}', + str(_ba.get_account_misc_read_val('proPowerRankingBoost', + 10))), + ('${PRO}', + ba.Lstr(resource='store.bombSquadProNameText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) + ])) + ]) + confirm.ConfirmWindow(txt, + cancel_button=False, + width=460, + height=130, + origin_widget=self._pro_mult_button) + + def _on_trophies_press(self) -> None: + from bastd.ui.trophies import TrophiesWindow + info = self._league_rank_data + if info is not None: + TrophiesWindow(position=self._power_ranking_trophies_button. + get_screen_space_center(), + data=info) + else: + ba.playsound(ba.getsound('error')) + + def _on_power_ranking_query_response( + self, data: Optional[Dict[str, Any]]) -> None: + self._doing_power_ranking_query = False + # important: *only* cache this if we requested the current season.. + if data is not None and data.get('s', None) is None: + ba.app.accounts.cache_league_rank_data(data) + # always store a copy locally though (even for other seasons) + self._league_rank_data = copy.deepcopy(data) + self._update_for_league_rank_data(data) + + def _restore_state(self) -> None: + pass + + def _update(self, show: bool = False) -> None: + + cur_time = ba.time(ba.TimeType.REAL) + + # if our account state has changed, refresh our UI + account_state = _ba.get_account_state() + if account_state != self._account_state: + self._account_state = account_state + self._save_state() + self._refresh() + + # and power ranking too... + if not self._doing_power_ranking_query: + self._last_power_ranking_query_time = None + + # send off a new power-ranking query if its been long enough or our + # requested season has changed or whatnot.. + if not self._doing_power_ranking_query and ( + self._last_power_ranking_query_time is None + or cur_time - self._last_power_ranking_query_time > 30.0): + try: + if show: + ba.textwidget(edit=self._league_title_text, text='') + ba.textwidget(edit=self._league_text, text='') + ba.textwidget(edit=self._league_number_text, text='') + ba.textwidget( + edit=self._your_power_ranking_text, + text=ba.Lstr(value='${A}...', + subs=[('${A}', + ba.Lstr(resource='loadingText'))])) + ba.textwidget(edit=self._to_ranked_text, text='') + ba.textwidget(edit=self._power_ranking_rank_text, text='') + ba.textwidget(edit=self._season_ends_text, text='') + ba.textwidget(edit=self._trophy_counts_reset_text, text='') + except Exception: + ba.print_exception('Error showing updated rank info.') + + self._last_power_ranking_query_time = cur_time + self._doing_power_ranking_query = True + _ba.power_ranking_query(season=self._requested_season, + callback=ba.WeakCall( + self._on_power_ranking_query_response)) + + def _refresh(self) -> None: + # pylint: disable=too-many-statements + + # (re)create the sub-container if need be.. + if self._subcontainer is not None: + self._subcontainer.delete() + self._subcontainer = ba.containerwidget( + parent=self._scrollwidget, + size=(self._subcontainerwidth, self._subcontainerheight), + background=False) + + w_parent = self._subcontainer + v = self._subcontainerheight - 20 + + v -= 0 + + h2 = 80 + v2 = v - 60 + worth_color = (0.6, 0.6, 0.65) + tally_color = (0.5, 0.6, 0.8) + spc = 43 + + h_offs_tally = 150 + tally_maxwidth = 120 + v2 -= 70 + + ba.textwidget(parent=w_parent, + position=(h2 - 60, v2 + 106), + size=(0, 0), + flatness=1.0, + shadow=0.0, + text=ba.Lstr(resource='coopSelectWindow.pointsText'), + h_align='left', + v_align='center', + scale=0.8, + color=(1, 1, 1, 0.3), + maxwidth=200) + + self._power_ranking_achievements_button = ba.buttonwidget( + parent=w_parent, + position=(h2 - 60, v2 + 10), + size=(200, 80), + icon=ba.gettexture('achievementsIcon'), + autoselect=True, + on_activate_call=ba.WeakCall(self._on_achievements_press), + up_widget=self._back_button, + left_widget=self._back_button, + color=(0.5, 0.5, 0.6), + textcolor=(0.7, 0.7, 0.8), + label='') + + self._power_ranking_achievement_total_text = ba.textwidget( + parent=w_parent, + position=(h2 + h_offs_tally, v2 + 45), + size=(0, 0), + flatness=1.0, + shadow=0.0, + text='-', + h_align='left', + v_align='center', + scale=0.8, + color=tally_color, + maxwidth=tally_maxwidth) + + v2 -= 80 + + self._power_ranking_trophies_button = ba.buttonwidget( + parent=w_parent, + position=(h2 - 60, v2 + 10), + size=(200, 80), + icon=ba.gettexture('medalSilver'), + autoselect=True, + on_activate_call=ba.WeakCall(self._on_trophies_press), + left_widget=self._back_button, + color=(0.5, 0.5, 0.6), + textcolor=(0.7, 0.7, 0.8), + label='') + self._power_ranking_trophies_total_text = ba.textwidget( + parent=w_parent, + position=(h2 + h_offs_tally, v2 + 45), + size=(0, 0), + flatness=1.0, + shadow=0.0, + text='-', + h_align='left', + v_align='center', + scale=0.8, + color=tally_color, + maxwidth=tally_maxwidth) + + v2 -= 100 + + ba.textwidget( + parent=w_parent, + position=(h2 - 60, v2 + 86), + size=(0, 0), + flatness=1.0, + shadow=0.0, + text=ba.Lstr(resource='coopSelectWindow.multipliersText'), + h_align='left', + v_align='center', + scale=0.8, + color=(1, 1, 1, 0.3), + maxwidth=200) + + self._activity_mult_button: Optional[ba.Widget] + if _ba.get_account_misc_read_val('act', False): + self._activity_mult_button = ba.buttonwidget( + parent=w_parent, + position=(h2 - 60, v2 + 10), + size=(200, 60), + icon=ba.gettexture('heart'), + icon_color=(0.5, 0, 0.5), + label=ba.Lstr(resource='coopSelectWindow.activityText'), + autoselect=True, + on_activate_call=ba.WeakCall(self._on_activity_mult_press), + left_widget=self._back_button, + color=(0.5, 0.5, 0.6), + textcolor=(0.7, 0.7, 0.8)) + + self._activity_mult_text = ba.textwidget( + parent=w_parent, + position=(h2 + h_offs_tally, v2 + 40), + size=(0, 0), + flatness=1.0, + shadow=0.0, + text='-', + h_align='left', + v_align='center', + scale=0.8, + color=tally_color, + maxwidth=tally_maxwidth) + v2 -= 65 + else: + self._activity_mult_button = None + + self._pro_mult_button = ba.buttonwidget( + parent=w_parent, + position=(h2 - 60, v2 + 10), + size=(200, 60), + icon=ba.gettexture('logo'), + icon_color=(0.3, 0, 0.3), + label=ba.Lstr(resource='store.bombSquadProNameText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) + ]), + autoselect=True, + on_activate_call=ba.WeakCall(self._on_pro_mult_press), + left_widget=self._back_button, + color=(0.5, 0.5, 0.6), + textcolor=(0.7, 0.7, 0.8)) + + self._pro_mult_text = ba.textwidget(parent=w_parent, + position=(h2 + h_offs_tally, + v2 + 40), + size=(0, 0), + flatness=1.0, + shadow=0.0, + text='-', + h_align='left', + v_align='center', + scale=0.8, + color=tally_color, + maxwidth=tally_maxwidth) + v2 -= 30 + + v2 -= spc + ba.textwidget(parent=w_parent, + position=(h2 + h_offs_tally - 10 - 40, v2 + 35), + size=(0, 0), + flatness=1.0, + shadow=0.0, + text=ba.Lstr(resource='finalScoreText'), + h_align='right', + v_align='center', + scale=0.9, + color=worth_color, + maxwidth=150) + self._power_ranking_total_text = ba.textwidget( + parent=w_parent, + position=(h2 + h_offs_tally - 40, v2 + 35), + size=(0, 0), + flatness=1.0, + shadow=0.0, + text='-', + h_align='left', + v_align='center', + scale=0.9, + color=tally_color, + maxwidth=tally_maxwidth) + + self._season_show_text = ba.textwidget( + parent=w_parent, + position=(390 - 15, v - 20), + size=(0, 0), + color=(0.6, 0.6, 0.7), + maxwidth=200, + text=ba.Lstr(resource='showText'), + h_align='right', + v_align='center', + scale=0.8, + shadow=0, + flatness=1.0) + + self._league_title_text = ba.textwidget(parent=w_parent, + position=(470, v - 97), + size=(0, 0), + color=(0.6, 0.6, 0.7), + maxwidth=230, + text='', + h_align='center', + v_align='center', + scale=0.9, + shadow=0, + flatness=1.0) + + self._league_text_scale = 1.8 + self._league_text_maxwidth = 210 + self._league_text = ba.textwidget(parent=w_parent, + position=(470, v - 140), + size=(0, 0), + color=(1, 1, 1), + maxwidth=self._league_text_maxwidth, + text='-', + h_align='center', + v_align='center', + scale=self._league_text_scale, + shadow=1.0, + flatness=1.0) + self._league_number_base_pos = (470, v - 140) + self._league_number_text = ba.textwidget(parent=w_parent, + position=(470, v - 140), + size=(0, 0), + color=(1, 1, 1), + maxwidth=100, + text='', + h_align='left', + v_align='center', + scale=0.8, + shadow=1.0, + flatness=1.0) + + self._your_power_ranking_text = ba.textwidget(parent=w_parent, + position=(470, + v - 142 - 70), + size=(0, 0), + color=(0.6, 0.6, 0.7), + maxwidth=230, + text='', + h_align='center', + v_align='center', + scale=0.9, + shadow=0, + flatness=1.0) + + self._to_ranked_text = ba.textwidget(parent=w_parent, + position=(470, v - 250 - 70), + size=(0, 0), + color=(0.6, 0.6, 0.7), + maxwidth=230, + text='', + h_align='center', + v_align='center', + scale=0.8, + shadow=0, + flatness=1.0) + + self._power_ranking_rank_text = ba.textwidget(parent=w_parent, + position=(473, + v - 210 - 70), + size=(0, 0), + big=False, + text='-', + h_align='center', + v_align='center', + scale=1.0) + + self._season_ends_text = ba.textwidget(parent=w_parent, + position=(470, v - 380), + size=(0, 0), + color=(0.6, 0.6, 0.6), + maxwidth=230, + text='', + h_align='center', + v_align='center', + scale=0.9, + shadow=0, + flatness=1.0) + self._trophy_counts_reset_text = ba.textwidget( + parent=w_parent, + position=(470, v - 410), + size=(0, 0), + color=(0.5, 0.5, 0.5), + maxwidth=230, + text='Trophy counts will reset next season.', + h_align='center', + v_align='center', + scale=0.8, + shadow=0, + flatness=1.0) + + self._power_ranking_score_widgets = [] + + self._power_ranking_score_v = v - 56 + + h = 707 + v -= 451 + + self._see_more_button = ba.buttonwidget(parent=w_parent, + label=self._rdict.seeMoreText, + position=(h, v), + color=(0.5, 0.5, 0.6), + textcolor=(0.7, 0.7, 0.8), + size=(230, 60), + autoselect=True, + on_activate_call=ba.WeakCall( + self._on_more_press)) + + def _on_more_press(self) -> None: + our_login_id = _ba.get_public_login_id() + # our_login_id = _bs.get_account_misc_read_val_2( + # 'resolvedAccountID', None) + if not self._can_do_more_button or our_login_id is None: + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource='unavailableText'), + color=(1, 0, 0)) + return + if self._season is None: + season_str = '' + else: + season_str = ( + '&season=' + + ('all_time' if self._season == 'a' else self._season)) + if self._league_url_arg != '': + league_str = '&league=' + self._league_url_arg + else: + league_str = '' + ba.open_url(_ba.get_master_server_address() + + '/highscores?list=powerRankings&v=2' + league_str + + season_str + '&player=' + our_login_id) + + def _update_for_league_rank_data(self, data: Optional[Dict[str, + Any]]) -> None: + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + if not self._root_widget: + return + accounts = ba.app.accounts + in_top = (data is not None and data['rank'] is not None) + eq_text = self._rdict.powerRankingPointsEqualsText + pts_txt = self._rdict.powerRankingPointsText + num_text = ba.Lstr(resource='numberText').evaluate() + do_percent = False + finished_season_unranked = False + self._can_do_more_button = True + extra_text = '' + if _ba.get_account_state() != 'signed_in': + status_text = '(' + ba.Lstr( + resource='notSignedInText').evaluate() + ')' + elif in_top: + assert data is not None + status_text = num_text.replace('${NUMBER}', str(data['rank'])) + elif data is not None: + try: + # handle old seasons where we didn't wind up ranked + # at the end.. + if not data['scores']: + status_text = ( + self._rdict.powerRankingFinishedSeasonUnrankedText) + extra_text = '' + finished_season_unranked = True + self._can_do_more_button = False + else: + our_points = accounts.get_league_rank_points(data) + progress = float(our_points) / max(1, + data['scores'][-1][1]) + status_text = str(int(progress * 100.0)) + '%' + extra_text = ( + '\n' + + self._rdict.powerRankingPointsToRankedText.replace( + '${CURRENT}', str(our_points)).replace( + '${REMAINING}', str(data['scores'][-1][1]))) + do_percent = True + except Exception: + ba.print_exception('Error updating power ranking.') + status_text = self._rdict.powerRankingNotInTopText.replace( + '${NUMBER}', str(data['listSize'])) + extra_text = '' + else: + status_text = '-' + + self._season = data['s'] if data is not None else None + + v = self._subcontainerheight - 20 + popup_was_selected = False + if self._season_popup_menu is not None: + btn = self._season_popup_menu.get_button() + assert self._subcontainer + if self._subcontainer.get_selected_child() == btn: + popup_was_selected = True + btn.delete() + season_choices = [] + season_choices_display = [] + did_first = False + self._is_current_season = False + if data is not None: + # build our list of seasons we have available + for ssn in data['sl']: + season_choices.append(ssn) + if ssn != 'a' and not did_first: + season_choices_display.append( + ba.Lstr(resource='league.currentSeasonText', + subs=[('${NUMBER}', ssn)])) + did_first = True + # if we either did not specify a season or specified the + # first, we're looking at the current.. + if self._season in [ssn, None]: + self._is_current_season = True + elif ssn == 'a': + season_choices_display.append( + ba.Lstr(resource='league.allTimeText')) + else: + season_choices_display.append( + ba.Lstr(resource='league.seasonText', + subs=[('${NUMBER}', ssn)])) + assert self._subcontainer + self._season_popup_menu = popup_ui.PopupMenu( + parent=self._subcontainer, + position=(390, v - 45), + width=150, + button_size=(200, 50), + choices=season_choices, + on_value_change_call=ba.WeakCall(self._on_season_change), + choices_display=season_choices_display, + current_choice=self._season) + if popup_was_selected: + ba.containerwidget( + edit=self._subcontainer, + selected_child=self._season_popup_menu.get_button()) + ba.widget(edit=self._see_more_button, show_buffer_bottom=100) + ba.widget(edit=self._season_popup_menu.get_button(), + up_widget=self._back_button) + ba.widget(edit=self._back_button, + down_widget=self._power_ranking_achievements_button, + right_widget=self._season_popup_menu.get_button()) + + ba.textwidget(edit=self._league_title_text, + text='' if self._season == 'a' else ba.Lstr( + resource='league.leagueText')) + + if data is None: + lname = '' + lnum = '' + lcolor = (1, 1, 1) + self._league_url_arg = '' + elif self._season == 'a': + lname = ba.Lstr(resource='league.allTimeText').evaluate() + lnum = '' + lcolor = (1, 1, 1) + self._league_url_arg = '' + else: + lnum = ('[' + str(data['l']['i']) + ']') if data['l']['i2'] else '' + lname = ba.Lstr(translate=('leagueNames', + data['l']['n'])).evaluate() + lcolor = data['l']['c'] + self._league_url_arg = (data['l']['n'] + '_' + + str(data['l']['i'])).lower() + + to_end_string: Union[ba.Lstr, str] + if data is None or self._season == 'a' or data['se'] is None: + to_end_string = '' + show_season_end = False + else: + show_season_end = True + days_to_end = data['se'][0] + minutes_to_end = data['se'][1] + if days_to_end > 0: + to_end_string = ba.Lstr(resource='league.seasonEndsDaysText', + subs=[('${NUMBER}', str(days_to_end))]) + elif days_to_end == 0 and minutes_to_end >= 60: + to_end_string = ba.Lstr(resource='league.seasonEndsHoursText', + subs=[('${NUMBER}', + str(minutes_to_end // 60))]) + elif days_to_end == 0 and minutes_to_end >= 0: + to_end_string = ba.Lstr( + resource='league.seasonEndsMinutesText', + subs=[('${NUMBER}', str(minutes_to_end))]) + else: + to_end_string = ba.Lstr( + resource='league.seasonEndedDaysAgoText', + subs=[('${NUMBER}', str(-(days_to_end + 1)))]) + + ba.textwidget(edit=self._season_ends_text, text=to_end_string) + ba.textwidget(edit=self._trophy_counts_reset_text, + text=ba.Lstr(resource='league.trophyCountsResetText') + if self._is_current_season and show_season_end else '') + + ba.textwidget(edit=self._league_text, text=lname, color=lcolor) + l_text_width = min( + self._league_text_maxwidth, + _ba.get_string_width(lname, suppress_warning=True) * + self._league_text_scale) + ba.textwidget( + edit=self._league_number_text, + text=lnum, + color=lcolor, + position=(self._league_number_base_pos[0] + l_text_width * 0.5 + 8, + self._league_number_base_pos[1] + 10)) + ba.textwidget( + edit=self._to_ranked_text, + text=ba.Lstr(resource='coopSelectWindow.toRankedText').evaluate() + + '' + extra_text if do_percent else '') + + ba.textwidget( + edit=self._your_power_ranking_text, + text=ba.Lstr( + resource='rankText', + fallback_resource='coopSelectWindow.yourPowerRankingText') if + (not do_percent) else '') + + ba.textwidget(edit=self._power_ranking_rank_text, + position=(473, v - 70 - (170 if do_percent else 220)), + text=status_text, + big=(in_top or do_percent), + scale=3.0 if (in_top or do_percent) else + 0.7 if finished_season_unranked else 1.0) + + if self._activity_mult_button is not None: + if data is None or data['act'] is None: + ba.buttonwidget(edit=self._activity_mult_button, + textcolor=(0.7, 0.7, 0.8, 0.5), + icon_color=(0.5, 0, 0.5, 0.3)) + ba.textwidget(edit=self._activity_mult_text, text=' -') + else: + ba.buttonwidget(edit=self._activity_mult_button, + textcolor=(0.7, 0.7, 0.8, 1.0), + icon_color=(0.5, 0, 0.5, 1.0)) + ba.textwidget(edit=self._activity_mult_text, + text='x ' + ('%.2f' % data['act'])) + + have_pro = False if data is None else data['p'] + pro_mult = 1.0 + float( + _ba.get_account_misc_read_val('proPowerRankingBoost', 0.0)) * 0.01 + ba.textwidget(edit=self._pro_mult_text, + text=' -' if + (data is None or not have_pro) else 'x ' + + ('%.2f' % pro_mult)) + ba.buttonwidget(edit=self._pro_mult_button, + textcolor=(0.7, 0.7, 0.8, (1.0 if have_pro else 0.5)), + icon_color=(0.5, 0, 0.5) if have_pro else + (0.5, 0, 0.5, 0.2)) + ba.buttonwidget(edit=self._power_ranking_achievements_button, + label=('' if data is None else + (str(data['a']) + ' ')) + + ba.Lstr(resource='achievementsText').evaluate()) + + # for the achievement value, use the number they gave us for + # non-current seasons; otherwise calc our own + total_ach_value = 0 + for ach in ba.app.ach.achievements: + if ach.complete: + total_ach_value += ach.power_ranking_value + if self._season != 'a' and not self._is_current_season: + if data is not None and 'at' in data: + total_ach_value = data['at'] + + ba.textwidget(edit=self._power_ranking_achievement_total_text, + text='-' if data is None else + ('+ ' + + pts_txt.replace('${NUMBER}', str(total_ach_value)))) + + total_trophies_count = (accounts.get_league_rank_points( + data, 'trophyCount')) + total_trophies_value = (accounts.get_league_rank_points( + data, 'trophies')) + ba.buttonwidget(edit=self._power_ranking_trophies_button, + label=('' if data is None else + (str(total_trophies_count) + ' ')) + + ba.Lstr(resource='trophiesText').evaluate()) + ba.textwidget( + edit=self._power_ranking_trophies_total_text, + text='-' if data is None else + ('+ ' + pts_txt.replace('${NUMBER}', str(total_trophies_value)))) + + ba.textwidget( + edit=self._power_ranking_total_text, + text='-' if data is None else eq_text.replace( + '${NUMBER}', str(accounts.get_league_rank_points(data)))) + for widget in self._power_ranking_score_widgets: + widget.delete() + self._power_ranking_score_widgets = [] + + scores = data['scores'] if data is not None else [] + tally_color = (0.5, 0.6, 0.8) + w_parent = self._subcontainer + v2 = self._power_ranking_score_v + + for score in scores: + h2 = 680 + is_us = score[3] + self._power_ranking_score_widgets.append( + ba.textwidget(parent=w_parent, + position=(h2 - 20, v2), + size=(0, 0), + color=(1, 1, 1) if is_us else (0.6, 0.6, 0.7), + maxwidth=40, + flatness=1.0, + shadow=0.0, + text=num_text.replace('${NUMBER}', + str(score[0])), + h_align='right', + v_align='center', + scale=0.5)) + self._power_ranking_score_widgets.append( + ba.textwidget(parent=w_parent, + position=(h2 + 20, v2), + size=(0, 0), + color=(1, 1, 1) if is_us else tally_color, + maxwidth=60, + text=str(score[1]), + flatness=1.0, + shadow=0.0, + h_align='center', + v_align='center', + scale=0.7)) + txt = ba.textwidget(parent=w_parent, + position=(h2 + 60, v2 - (28 * 0.5) / 0.9), + size=(210 / 0.9, 28), + color=(1, 1, 1) if is_us else (0.6, 0.6, 0.6), + maxwidth=210, + flatness=1.0, + shadow=0.0, + autoselect=True, + selectable=True, + click_activate=True, + text=score[2], + h_align='left', + v_align='center', + scale=0.9) + self._power_ranking_score_widgets.append(txt) + ba.textwidget(edit=txt, + on_activate_call=ba.Call(self._show_account_info, + score[4], txt)) + assert self._season_popup_menu is not None + ba.widget(edit=txt, + left_widget=self._season_popup_menu.get_button()) + v2 -= 28 + + def _show_account_info(self, account_id: str, + textwidget: ba.Widget) -> None: + from bastd.ui.account import viewer + ba.playsound(ba.getsound('swish')) + viewer.AccountViewerWindow( + account_id=account_id, + position=textwidget.get_screen_space_center()) + + def _on_season_change(self, value: str) -> None: + self._requested_season = value + self._last_power_ranking_query_time = None # make sure we update asap + self._update(show=True) + + def _save_state(self) -> None: + pass + + def _back(self) -> None: + from bastd.ui.coop.browser import CoopBrowserWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + if not self._modal: + ba.app.ui.set_main_menu_window( + CoopBrowserWindow(transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/mainmenu.py b/dist/ba_data/python/bastd/ui/mainmenu.py new file mode 100644 index 0000000..c82c7eb --- /dev/null +++ b/dist/ba_data/python/bastd/ui/mainmenu.py @@ -0,0 +1,1044 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Implements the main menu window.""" +# pylint: disable=too-many-lines + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba + +if TYPE_CHECKING: + from typing import Any, Callable, List, Dict, Tuple, Optional, Union + + +class MainMenuWindow(ba.Window): + """The main menu window, both in-game and in the main menu session.""" + + def __init__(self, transition: Optional[str] = 'in_right'): + # pylint: disable=cyclic-import + import threading + from bastd.mainmenu import MainMenuSession + self._in_game = not isinstance(_ba.get_foreground_host_session(), + MainMenuSession) + + # Preload some modules we use in a background thread so we won't + # have a visual hitch when the user taps them. + threading.Thread(target=self._preload_modules).start() + + if not self._in_game: + ba.set_analytics_screen('Main Menu') + self._show_remote_app_info_on_first_launch() + + # Make a vanilla container; we'll modify it to our needs in refresh. + super().__init__(root_widget=ba.containerwidget( + transition=transition, + toolbar_visibility='menu_minimal_no_back' if self. + _in_game else 'menu_minimal_no_back')) + + # Grab this stuff in case it changes. + self._is_demo = ba.app.demo_mode + self._is_arcade = ba.app.arcade_mode + self._is_iircade = ba.app.iircade_mode + + self._tdelay = 0.0 + self._t_delay_inc = 0.02 + self._t_delay_play = 1.7 + self._p_index = 0 + self._use_autoselect = True + self._button_width = 200.0 + self._button_height = 45.0 + self._width = 100.0 + self._height = 100.0 + self._demo_menu_button: Optional[ba.Widget] = None + self._gather_button: Optional[ba.Widget] = None + self._start_button: Optional[ba.Widget] = None + self._watch_button: Optional[ba.Widget] = None + self._gc_button: Optional[ba.Widget] = None + self._how_to_play_button: Optional[ba.Widget] = None + self._credits_button: Optional[ba.Widget] = None + self._settings_button: Optional[ba.Widget] = None + + self._store_char_tex = self._get_store_char_tex() + + self._refresh() + self._restore_state() + + # Keep an eye on a few things and refresh if they change. + self._account_state = _ba.get_account_state() + self._account_state_num = _ba.get_account_state_num() + self._account_type = (_ba.get_account_type() + if self._account_state == 'signed_in' else None) + self._refresh_timer = ba.Timer(1.0, + ba.WeakCall(self._check_refresh), + repeat=True, + timetype=ba.TimeType.REAL) + + @staticmethod + def _preload_modules() -> None: + """Preload modules we use (called in bg thread).""" + import bastd.ui.getremote as _unused + import bastd.ui.confirm as _unused2 + import bastd.ui.store.button as _unused3 + import bastd.ui.kiosk as _unused4 + import bastd.ui.account.settings as _unused5 + import bastd.ui.store.browser as _unused6 + import bastd.ui.creditslist as _unused7 + import bastd.ui.helpui as _unused8 + import bastd.ui.settings.allsettings as _unused9 + import bastd.ui.gather as _unused10 + import bastd.ui.watch as _unused11 + import bastd.ui.play as _unused12 + + def _show_remote_app_info_on_first_launch(self) -> None: + # The first time the non-in-game menu pops up, we might wanna show + # a 'get-remote-app' dialog in front of it. + if ba.app.first_main_menu: + ba.app.first_main_menu = False + try: + app = ba.app + force_test = False + _ba.get_local_active_input_devices_count() + if (((app.on_tv or app.platform == 'mac') + and ba.app.config.get('launchCount', 0) <= 1) + or force_test): + + def _check_show_bs_remote_window() -> None: + try: + from bastd.ui.getremote import GetBSRemoteWindow + ba.playsound(ba.getsound('swish')) + GetBSRemoteWindow() + except Exception: + ba.print_exception( + 'Error showing get-remote window.') + + ba.timer(2.5, + _check_show_bs_remote_window, + timetype=ba.TimeType.REAL) + except Exception: + ba.print_exception('Error showing get-remote-app info') + + def _get_store_char_tex(self) -> str: + return ('storeCharacterXmas' if _ba.get_account_misc_read_val( + 'xmas', False) else + 'storeCharacterEaster' if _ba.get_account_misc_read_val( + 'easter', False) else 'storeCharacter') + + def _check_refresh(self) -> None: + if not self._root_widget: + return + + # Don't refresh for the first few seconds the game is up so we don't + # interrupt the transition in. + ba.app.main_menu_window_refresh_check_count += 1 + if ba.app.main_menu_window_refresh_check_count < 4: + return + + store_char_tex = self._get_store_char_tex() + account_state_num = _ba.get_account_state_num() + if (account_state_num != self._account_state_num + or store_char_tex != self._store_char_tex): + self._store_char_tex = store_char_tex + self._account_state_num = account_state_num + account_state = self._account_state = (_ba.get_account_state()) + self._account_type = (_ba.get_account_type() + if account_state == 'signed_in' else None) + self._save_state() + self._refresh() + self._restore_state() + + def get_play_button(self) -> Optional[ba.Widget]: + """Return the play button.""" + return self._start_button + + def _refresh(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + from bastd.ui.confirm import QuitWindow + from bastd.ui.store.button import StoreButton + + # Clear everything that was there. + children = self._root_widget.get_children() + for child in children: + child.delete() + + self._tdelay = 0.0 + self._t_delay_inc = 0.0 + self._t_delay_play = 0.0 + self._button_width = 200.0 + self._button_height = 45.0 + + self._r = 'mainMenu' + + app = ba.app + self._have_quit_button = (app.ui.uiscale is ba.UIScale.LARGE + or (app.platform == 'windows' + and app.subplatform == 'oculus')) + + self._have_store_button = not self._in_game + + self._have_settings_button = ( + (not self._in_game or not app.toolbar_test) + and not (self._is_demo or self._is_arcade or self._is_iircade)) + + self._input_device = input_device = _ba.get_ui_input_device() + self._input_player = input_device.player if input_device else None + self._connected_to_remote_player = ( + input_device.is_connected_to_remote_player() + if input_device else False) + + positions: List[Tuple[float, float, float]] = [] + self._p_index = 0 + + if self._in_game: + h, v, scale = self._refresh_in_game(positions) + else: + h, v, scale = self._refresh_not_in_game(positions) + + if self._have_settings_button: + h, v, scale = positions[self._p_index] + self._p_index += 1 + self._settings_button = ba.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width * 0.5 * scale, v), + size=(self._button_width, self._button_height), + scale=scale, + autoselect=self._use_autoselect, + label=ba.Lstr(resource=self._r + '.settingsText'), + transition_delay=self._tdelay, + on_activate_call=self._settings) + + # Scattered eggs on easter. + if _ba.get_account_misc_read_val('easter', + False) and not self._in_game: + icon_size = 34 + ba.imagewidget(parent=self._root_widget, + position=(h - icon_size * 0.5 - 15, + v + self._button_height * scale - + icon_size * 0.24 + 1.5), + transition_delay=self._tdelay, + size=(icon_size, icon_size), + texture=ba.gettexture('egg3'), + tilt_scale=0.0) + + self._tdelay += self._t_delay_inc + + if self._in_game: + h, v, scale = positions[self._p_index] + self._p_index += 1 + + # If we're in a replay, we have a 'Leave Replay' button. + if _ba.is_in_replay(): + ba.buttonwidget(parent=self._root_widget, + position=(h - self._button_width * 0.5 * scale, + v), + scale=scale, + size=(self._button_width, self._button_height), + autoselect=self._use_autoselect, + label=ba.Lstr(resource='replayEndText'), + on_activate_call=self._confirm_end_replay) + elif _ba.get_foreground_host_session() is not None: + ba.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width * 0.5 * scale, v), + scale=scale, + size=(self._button_width, self._button_height), + autoselect=self._use_autoselect, + label=ba.Lstr(resource=self._r + '.endGameText'), + on_activate_call=self._confirm_end_game) + # Assume we're in a client-session. + else: + ba.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width * 0.5 * scale, v), + scale=scale, + size=(self._button_width, self._button_height), + autoselect=self._use_autoselect, + label=ba.Lstr(resource=self._r + '.leavePartyText'), + on_activate_call=self._confirm_leave_party) + + self._store_button: Optional[ba.Widget] + if self._have_store_button: + this_b_width = self._button_width + h, v, scale = positions[self._p_index] + self._p_index += 1 + + sbtn = self._store_button_instance = StoreButton( + parent=self._root_widget, + position=(h - this_b_width * 0.5 * scale, v), + size=(this_b_width, self._button_height), + scale=scale, + on_activate_call=ba.WeakCall(self._on_store_pressed), + sale_scale=1.3, + transition_delay=self._tdelay) + self._store_button = store_button = sbtn.get_button() + uiscale = ba.app.ui.uiscale + icon_size = (55 if uiscale is ba.UIScale.SMALL else + 55 if uiscale is ba.UIScale.MEDIUM else 70) + ba.imagewidget( + parent=self._root_widget, + position=(h - icon_size * 0.5, + v + self._button_height * scale - icon_size * 0.23), + transition_delay=self._tdelay, + size=(icon_size, icon_size), + texture=ba.gettexture(self._store_char_tex), + tilt_scale=0.0, + draw_controller=store_button) + + self._tdelay += self._t_delay_inc + else: + self._store_button = None + + self._quit_button: Optional[ba.Widget] + if not self._in_game and self._have_quit_button: + h, v, scale = positions[self._p_index] + self._p_index += 1 + self._quit_button = quit_button = ba.buttonwidget( + parent=self._root_widget, + autoselect=self._use_autoselect, + position=(h - self._button_width * 0.5 * scale, v), + size=(self._button_width, self._button_height), + scale=scale, + label=ba.Lstr(resource=self._r + + ('.quitText' if 'Mac' in + ba.app.user_agent_string else '.exitGameText')), + on_activate_call=self._quit, + transition_delay=self._tdelay) + + # Scattered eggs on easter. + if _ba.get_account_misc_read_val('easter', False): + icon_size = 30 + ba.imagewidget(parent=self._root_widget, + position=(h - icon_size * 0.5 + 25, + v + self._button_height * scale - + icon_size * 0.24 + 1.5), + transition_delay=self._tdelay, + size=(icon_size, icon_size), + texture=ba.gettexture('egg1'), + tilt_scale=0.0) + + ba.containerwidget(edit=self._root_widget, + cancel_button=quit_button) + self._tdelay += self._t_delay_inc + else: + self._quit_button = None + + # If we're not in-game, have no quit button, and this is android, + # we want back presses to quit our activity. + if (not self._in_game and not self._have_quit_button + and ba.app.platform == 'android'): + + def _do_quit() -> None: + QuitWindow(swish=True, back=True) + + ba.containerwidget(edit=self._root_widget, + on_cancel_call=_do_quit) + + # Add speed-up/slow-down buttons for replays. + # (ideally this should be part of a fading-out playback bar like most + # media players but this works for now). + if _ba.is_in_replay(): + b_size = 50.0 + b_buffer = 10.0 + t_scale = 0.75 + uiscale = ba.app.ui.uiscale + if uiscale is ba.UIScale.SMALL: + b_size *= 0.6 + b_buffer *= 1.0 + v_offs = -40 + t_scale = 0.5 + elif uiscale is ba.UIScale.MEDIUM: + v_offs = -70 + else: + v_offs = -100 + self._replay_speed_text = ba.textwidget( + parent=self._root_widget, + text=ba.Lstr(resource='watchWindow.playbackSpeedText', + subs=[('${SPEED}', str(1.23))]), + position=(h, v + v_offs + 7 * t_scale), + h_align='center', + v_align='center', + size=(0, 0), + scale=t_scale) + + # Update to current value. + self._change_replay_speed(0) + + # Keep updating in a timer in case it gets changed elsewhere. + self._change_replay_speed_timer = ba.Timer( + 0.25, + ba.WeakCall(self._change_replay_speed, 0), + timetype=ba.TimeType.REAL, + repeat=True) + btn = ba.buttonwidget(parent=self._root_widget, + position=(h - b_size - b_buffer, + v - b_size - b_buffer + v_offs), + button_type='square', + size=(b_size, b_size), + label='', + autoselect=True, + on_activate_call=ba.Call( + self._change_replay_speed, -1)) + ba.textwidget( + parent=self._root_widget, + draw_controller=btn, + text='-', + position=(h - b_size * 0.5 - b_buffer, + v - b_size * 0.5 - b_buffer + 5 * t_scale + v_offs), + h_align='center', + v_align='center', + size=(0, 0), + scale=3.0 * t_scale) + btn = ba.buttonwidget( + parent=self._root_widget, + position=(h + b_buffer, v - b_size - b_buffer + v_offs), + button_type='square', + size=(b_size, b_size), + label='', + autoselect=True, + on_activate_call=ba.Call(self._change_replay_speed, 1)) + ba.textwidget( + parent=self._root_widget, + draw_controller=btn, + text='+', + position=(h + b_size * 0.5 + b_buffer, + v - b_size * 0.5 - b_buffer + 5 * t_scale + v_offs), + h_align='center', + v_align='center', + size=(0, 0), + scale=3.0 * t_scale) + + def _refresh_not_in_game( + self, positions: List[Tuple[float, float, + float]]) -> Tuple[float, float, float]: + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + if not ba.app.did_menu_intro: + self._tdelay = 2.0 + self._t_delay_inc = 0.02 + self._t_delay_play = 1.7 + ba.app.did_menu_intro = True + self._width = 400.0 + self._height = 200.0 + enable_account_button = True + account_type_name: Union[str, ba.Lstr] + if _ba.get_account_state() == 'signed_in': + account_type_name = _ba.get_account_display_string() + account_type_icon = None + account_textcolor = (1.0, 1.0, 1.0) + else: + account_type_name = ba.Lstr( + resource='notSignedInText', + fallback_resource='accountSettingsWindow.titleText') + account_type_icon = None + account_textcolor = (1.0, 0.2, 0.2) + account_type_icon_color = (1.0, 1.0, 1.0) + account_type_call = self._show_account_window + account_type_enable_button_sound = True + b_count = 3 # play, help, credits + if self._have_settings_button: + b_count += 1 + if enable_account_button: + b_count += 1 + if self._have_quit_button: + b_count += 1 + if self._have_store_button: + b_count += 1 + uiscale = ba.app.ui.uiscale + if uiscale is ba.UIScale.SMALL: + root_widget_scale = 1.6 + play_button_width = self._button_width * 0.65 + play_button_height = self._button_height * 1.1 + small_button_scale = 0.51 if b_count > 6 else 0.63 + button_y_offs = -20.0 + button_y_offs2 = -60.0 + self._button_height *= 1.3 + button_spacing = 1.04 + elif uiscale is ba.UIScale.MEDIUM: + root_widget_scale = 1.3 + play_button_width = self._button_width * 0.65 + play_button_height = self._button_height * 1.1 + small_button_scale = 0.6 + button_y_offs = -55.0 + button_y_offs2 = -75.0 + self._button_height *= 1.25 + button_spacing = 1.1 + else: + root_widget_scale = 1.0 + play_button_width = self._button_width * 0.65 + play_button_height = self._button_height * 1.1 + small_button_scale = 0.75 + button_y_offs = -80.0 + button_y_offs2 = -100.0 + self._button_height *= 1.2 + button_spacing = 1.1 + spc = self._button_width * small_button_scale * button_spacing + ba.containerwidget(edit=self._root_widget, + size=(self._width, self._height), + background=False, + scale=root_widget_scale) + assert not positions + positions.append((self._width * 0.5, button_y_offs, 1.7)) + x_offs = self._width * 0.5 - (spc * (b_count - 1) * 0.5) + (spc * 0.5) + for i in range(b_count - 1): + positions.append( + (x_offs + spc * i - 1.0, button_y_offs + button_y_offs2, + small_button_scale)) + # In kiosk mode, provide a button to get back to the kiosk menu. + if ba.app.demo_mode or ba.app.arcade_mode: + h, v, scale = positions[self._p_index] + this_b_width = self._button_width * 0.4 * scale + demo_menu_delay = 0.0 if self._t_delay_play == 0.0 else max( + 0, self._t_delay_play + 0.1) + self._demo_menu_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width * 0.5 - this_b_width * 0.5, v + 90), + size=(this_b_width, 45), + autoselect=True, + color=(0.45, 0.55, 0.45), + textcolor=(0.7, 0.8, 0.7), + label=ba.Lstr(resource='modeArcadeText' if ba.app. + arcade_mode else 'modeDemoText'), + transition_delay=demo_menu_delay, + on_activate_call=self._demo_menu_press) + else: + self._demo_menu_button = None + uiscale = ba.app.ui.uiscale + foof = (-1 if uiscale is ba.UIScale.SMALL else + 1 if uiscale is ba.UIScale.MEDIUM else 3) + h, v, scale = positions[self._p_index] + v = v + foof + gather_delay = 0.0 if self._t_delay_play == 0.0 else max( + 0.0, self._t_delay_play + 0.1) + assert play_button_width is not None + assert play_button_height is not None + this_h = h - play_button_width * 0.5 * scale - 40 * scale + this_b_width = self._button_width * 0.25 * scale + this_b_height = self._button_height * 0.82 * scale + self._gather_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(this_h - this_b_width * 0.5, v), + size=(this_b_width, this_b_height), + autoselect=self._use_autoselect, + button_type='square', + label='', + transition_delay=gather_delay, + on_activate_call=self._gather_press) + ba.textwidget(parent=self._root_widget, + position=(this_h, v + self._button_height * 0.33), + size=(0, 0), + scale=0.75, + transition_delay=gather_delay, + draw_controller=btn, + color=(0.75, 1.0, 0.7), + maxwidth=self._button_width * 0.33, + text=ba.Lstr(resource='gatherWindow.titleText'), + h_align='center', + v_align='center') + icon_size = this_b_width * 0.6 + ba.imagewidget(parent=self._root_widget, + size=(icon_size, icon_size), + draw_controller=btn, + transition_delay=gather_delay, + position=(this_h - 0.5 * icon_size, + v + 0.31 * this_b_height), + texture=ba.gettexture('usersButton')) + + # Play button. + h, v, scale = positions[self._p_index] + self._p_index += 1 + self._start_button = start_button = ba.buttonwidget( + parent=self._root_widget, + position=(h - play_button_width * 0.5 * scale, v), + size=(play_button_width, play_button_height), + autoselect=self._use_autoselect, + scale=scale, + text_res_scale=2.0, + label=ba.Lstr(resource='playText'), + transition_delay=self._t_delay_play, + on_activate_call=self._play_press) + ba.containerwidget(edit=self._root_widget, + start_button=start_button, + selected_child=start_button) + v = v + foof + watch_delay = 0.0 if self._t_delay_play == 0.0 else max( + 0.0, self._t_delay_play - 0.1) + this_h = h + play_button_width * 0.5 * scale + 40 * scale + this_b_width = self._button_width * 0.25 * scale + this_b_height = self._button_height * 0.82 * scale + self._watch_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(this_h - this_b_width * 0.5, v), + size=(this_b_width, this_b_height), + autoselect=self._use_autoselect, + button_type='square', + label='', + transition_delay=watch_delay, + on_activate_call=self._watch_press) + ba.textwidget(parent=self._root_widget, + position=(this_h, v + self._button_height * 0.33), + size=(0, 0), + scale=0.75, + transition_delay=watch_delay, + color=(0.75, 1.0, 0.7), + draw_controller=btn, + maxwidth=self._button_width * 0.33, + text=ba.Lstr(resource='watchWindow.titleText'), + h_align='center', + v_align='center') + icon_size = this_b_width * 0.55 + ba.imagewidget(parent=self._root_widget, + size=(icon_size, icon_size), + draw_controller=btn, + transition_delay=watch_delay, + position=(this_h - 0.5 * icon_size, + v + 0.33 * this_b_height), + texture=ba.gettexture('tv')) + if not self._in_game and enable_account_button: + this_b_width = self._button_width + h, v, scale = positions[self._p_index] + self._p_index += 1 + self._gc_button = ba.buttonwidget( + parent=self._root_widget, + position=(h - this_b_width * 0.5 * scale, v), + size=(this_b_width, self._button_height), + scale=scale, + label=account_type_name, + autoselect=self._use_autoselect, + on_activate_call=account_type_call, + textcolor=account_textcolor, + icon=account_type_icon, + icon_color=account_type_icon_color, + transition_delay=self._tdelay, + enable_sound=account_type_enable_button_sound) + + # Scattered eggs on easter. + if _ba.get_account_misc_read_val('easter', + False) and not self._in_game: + icon_size = 32 + ba.imagewidget(parent=self._root_widget, + position=(h - icon_size * 0.5 + 35, + v + self._button_height * scale - + icon_size * 0.24 + 1.5), + transition_delay=self._tdelay, + size=(icon_size, icon_size), + texture=ba.gettexture('egg2'), + tilt_scale=0.0) + self._tdelay += self._t_delay_inc + else: + self._gc_button = None + + # How-to-play button. + h, v, scale = positions[self._p_index] + self._p_index += 1 + btn = ba.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width * 0.5 * scale, v), + scale=scale, + autoselect=self._use_autoselect, + size=(self._button_width, self._button_height), + label=ba.Lstr(resource=self._r + '.howToPlayText'), + transition_delay=self._tdelay, + on_activate_call=self._howtoplay) + self._how_to_play_button = btn + + # Scattered eggs on easter. + if _ba.get_account_misc_read_val('easter', + False) and not self._in_game: + icon_size = 28 + ba.imagewidget(parent=self._root_widget, + position=(h - icon_size * 0.5 + 30, + v + self._button_height * scale - + icon_size * 0.24 + 1.5), + transition_delay=self._tdelay, + size=(icon_size, icon_size), + texture=ba.gettexture('egg4'), + tilt_scale=0.0) + # Credits button. + self._tdelay += self._t_delay_inc + h, v, scale = positions[self._p_index] + self._p_index += 1 + self._credits_button = ba.buttonwidget( + parent=self._root_widget, + position=(h - self._button_width * 0.5 * scale, v), + size=(self._button_width, self._button_height), + autoselect=self._use_autoselect, + label=ba.Lstr(resource=self._r + '.creditsText'), + scale=scale, + transition_delay=self._tdelay, + on_activate_call=self._credits) + self._tdelay += self._t_delay_inc + return h, v, scale + + def _refresh_in_game( + self, positions: List[Tuple[float, float, + float]]) -> Tuple[float, float, float]: + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + custom_menu_entries: List[Dict[str, Any]] = [] + session = _ba.get_foreground_host_session() + if session is not None: + try: + custom_menu_entries = session.get_custom_menu_entries() + for cme in custom_menu_entries: + if (not isinstance(cme, dict) or 'label' not in cme + or not isinstance(cme['label'], (str, ba.Lstr)) + or 'call' not in cme or not callable(cme['call'])): + raise ValueError('invalid custom menu entry: ' + + str(cme)) + except Exception: + custom_menu_entries = [] + ba.print_exception( + f'Error getting custom menu entries for {session}') + self._width = 250.0 + self._height = 250.0 if self._input_player else 180.0 + if (self._is_demo or self._is_arcade) and self._input_player: + self._height -= 40 + if not self._have_settings_button: + self._height -= 50 + if self._connected_to_remote_player: + # In this case we have a leave *and* a disconnect button. + self._height += 50 + self._height += 50 * (len(custom_menu_entries)) + uiscale = ba.app.ui.uiscale + ba.containerwidget( + edit=self._root_widget, + size=(self._width, self._height), + scale=(2.15 if uiscale is ba.UIScale.SMALL else + 1.6 if uiscale is ba.UIScale.MEDIUM else 1.0)) + h = 125.0 + v = (self._height - 80.0 if self._input_player else self._height - 60) + h_offset = 0 + d_h_offset = 0 + v_offset = -50 + for _i in range(6 + len(custom_menu_entries)): + positions.append((h, v, 1.0)) + v += v_offset + h += h_offset + h_offset += d_h_offset + self._start_button = None + ba.app.pause() + + # Player name if applicable. + if self._input_player: + player_name = self._input_player.getname() + h, v, scale = positions[self._p_index] + v += 35 + ba.textwidget(parent=self._root_widget, + position=(h - self._button_width / 2, v), + size=(self._button_width, self._button_height), + color=(1, 1, 1, 0.5), + scale=0.7, + h_align='center', + text=ba.Lstr(value=player_name)) + else: + player_name = '' + h, v, scale = positions[self._p_index] + self._p_index += 1 + btn = ba.buttonwidget(parent=self._root_widget, + position=(h - self._button_width / 2, v), + size=(self._button_width, self._button_height), + scale=scale, + label=ba.Lstr(resource=self._r + '.resumeText'), + autoselect=self._use_autoselect, + on_activate_call=self._resume) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + # Add any custom options defined by the current game. + for entry in custom_menu_entries: + h, v, scale = positions[self._p_index] + self._p_index += 1 + + # Ask the entry whether we should resume when we call + # it (defaults to true). + resume = bool(entry.get('resume_on_call', True)) + + if resume: + call = ba.Call(self._resume_and_call, entry['call']) + else: + call = ba.Call(entry['call'], ba.WeakCall(self._resume)) + + ba.buttonwidget(parent=self._root_widget, + position=(h - self._button_width / 2, v), + size=(self._button_width, self._button_height), + scale=scale, + on_activate_call=call, + label=entry['label'], + autoselect=self._use_autoselect) + # Add a 'leave' button if the menu-owner has a player. + if ((self._input_player or self._connected_to_remote_player) + and not (self._is_demo or self._is_arcade)): + h, v, scale = positions[self._p_index] + self._p_index += 1 + btn = ba.buttonwidget(parent=self._root_widget, + position=(h - self._button_width / 2, v), + size=(self._button_width, + self._button_height), + scale=scale, + on_activate_call=self._leave, + label='', + autoselect=self._use_autoselect) + + if (player_name != '' and player_name[0] != '<' + and player_name[-1] != '>'): + txt = ba.Lstr(resource=self._r + '.justPlayerText', + subs=[('${NAME}', player_name)]) + else: + txt = ba.Lstr(value=player_name) + ba.textwidget(parent=self._root_widget, + position=(h, v + self._button_height * + (0.64 if player_name != '' else 0.5)), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.leaveGameText'), + scale=(0.83 if player_name != '' else 1.0), + color=(0.75, 1.0, 0.7), + h_align='center', + v_align='center', + draw_controller=btn, + maxwidth=self._button_width * 0.9) + ba.textwidget(parent=self._root_widget, + position=(h, v + self._button_height * 0.27), + size=(0, 0), + text=txt, + color=(0.75, 1.0, 0.7), + h_align='center', + v_align='center', + draw_controller=btn, + scale=0.45, + maxwidth=self._button_width * 0.9) + return h, v, scale + + def _change_replay_speed(self, offs: int) -> None: + if not self._replay_speed_text: + if ba.do_once(): + print('_change_replay_speed called without widget') + return + _ba.set_replay_speed_exponent(_ba.get_replay_speed_exponent() + offs) + actual_speed = pow(2.0, _ba.get_replay_speed_exponent()) + ba.textwidget(edit=self._replay_speed_text, + text=ba.Lstr(resource='watchWindow.playbackSpeedText', + subs=[('${SPEED}', str(actual_speed))])) + + def _quit(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.confirm import QuitWindow + QuitWindow(origin_widget=self._quit_button) + + def _demo_menu_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.kiosk import KioskWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + KioskWindow(transition='in_left').get_root_widget()) + + def _show_account_window(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account.settings import AccountSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + AccountSettingsWindow( + origin_widget=self._gc_button).get_root_widget()) + + def _on_store_pressed(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.store.browser import StoreBrowserWindow + from bastd.ui.account import show_sign_in_prompt + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + StoreBrowserWindow( + origin_widget=self._store_button).get_root_widget()) + + def _confirm_end_game(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.confirm import ConfirmWindow + # FIXME: Currently we crash calling this on client-sessions. + + # Select cancel by default; this occasionally gets called by accident + # in a fit of button mashing and this will help reduce damage. + ConfirmWindow(ba.Lstr(resource=self._r + '.exitToMenuText'), + self._end_game, + cancel_is_selected=True) + + def _confirm_end_replay(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.confirm import ConfirmWindow + + # Select cancel by default; this occasionally gets called by accident + # in a fit of button mashing and this will help reduce damage. + ConfirmWindow(ba.Lstr(resource=self._r + '.exitToMenuText'), + self._end_game, + cancel_is_selected=True) + + def _confirm_leave_party(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.confirm import ConfirmWindow + + # Select cancel by default; this occasionally gets called by accident + # in a fit of button mashing and this will help reduce damage. + ConfirmWindow(ba.Lstr(resource=self._r + '.leavePartyConfirmText'), + self._leave_party, + cancel_is_selected=True) + + def _leave_party(self) -> None: + _ba.disconnect_from_host() + + def _end_game(self) -> None: + if not self._root_widget: + return + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.return_to_main_menu_session_gracefully(reset_ui=False) + + def _leave(self) -> None: + if self._input_player: + self._input_player.remove_from_game() + elif self._connected_to_remote_player: + if self._input_device: + self._input_device.remove_remote_player_from_game() + self._resume() + + def _credits(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.creditslist import CreditsListWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + CreditsListWindow( + origin_widget=self._credits_button).get_root_widget()) + + def _howtoplay(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.helpui import HelpWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + HelpWindow( + main_menu=True, + origin_widget=self._how_to_play_button).get_root_widget()) + + def _settings(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.allsettings import AllSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + AllSettingsWindow( + origin_widget=self._settings_button).get_root_widget()) + + def _resume_and_call(self, call: Callable[[], Any]) -> None: + self._resume() + call() + + def _do_game_service_press(self) -> None: + self._save_state() + _ba.show_online_score_ui() + + def _save_state(self) -> None: + + # Don't do this for the in-game menu. + if self._in_game: + return + sel = self._root_widget.get_selected_child() + if sel == self._start_button: + ba.app.ui.main_menu_selection = 'Start' + elif sel == self._gather_button: + ba.app.ui.main_menu_selection = 'Gather' + elif sel == self._watch_button: + ba.app.ui.main_menu_selection = 'Watch' + elif sel == self._how_to_play_button: + ba.app.ui.main_menu_selection = 'HowToPlay' + elif sel == self._credits_button: + ba.app.ui.main_menu_selection = 'Credits' + elif sel == self._settings_button: + ba.app.ui.main_menu_selection = 'Settings' + elif sel == self._gc_button: + ba.app.ui.main_menu_selection = 'GameService' + elif sel == self._store_button: + ba.app.ui.main_menu_selection = 'Store' + elif sel == self._quit_button: + ba.app.ui.main_menu_selection = 'Quit' + elif sel == self._demo_menu_button: + ba.app.ui.main_menu_selection = 'DemoMenu' + else: + print('unknown widget in main menu store selection:', sel) + ba.app.ui.main_menu_selection = 'Start' + + def _restore_state(self) -> None: + # pylint: disable=too-many-branches + + # Don't do this for the in-game menu. + if self._in_game: + return + sel_name = ba.app.ui.main_menu_selection + sel: Optional[ba.Widget] + if sel_name is None: + sel_name = 'Start' + if sel_name == 'HowToPlay': + sel = self._how_to_play_button + elif sel_name == 'Gather': + sel = self._gather_button + elif sel_name == 'Watch': + sel = self._watch_button + elif sel_name == 'Credits': + sel = self._credits_button + elif sel_name == 'Settings': + sel = self._settings_button + elif sel_name == 'GameService': + sel = self._gc_button + elif sel_name == 'Store': + sel = self._store_button + elif sel_name == 'Quit': + sel = self._quit_button + elif sel_name == 'DemoMenu': + sel = self._demo_menu_button + else: + sel = self._start_button + if sel is not None: + ba.containerwidget(edit=self._root_widget, selected_child=sel) + + def _gather_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.gather import GatherWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + GatherWindow(origin_widget=self._gather_button).get_root_widget()) + + def _watch_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.watch import WatchWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + WatchWindow(origin_widget=self._watch_button).get_root_widget()) + + def _play_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.play import PlayWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + + ba.app.ui.selecting_private_party_playlist = False + ba.app.ui.set_main_menu_window( + PlayWindow(origin_widget=self._start_button).get_root_widget()) + + def _resume(self) -> None: + ba.app.resume() + if self._root_widget: + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.clear_main_menu_window() + + # If there's callbacks waiting for this window to go away, call them. + for call in ba.app.main_menu_resume_callbacks: + call() + del ba.app.main_menu_resume_callbacks[:] diff --git a/dist/ba_data/python/bastd/ui/onscreenkeyboard.py b/dist/ba_data/python/bastd/ui/onscreenkeyboard.py new file mode 100644 index 0000000..d5ad875 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/onscreenkeyboard.py @@ -0,0 +1,383 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides the built-in on screen keyboard UI.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +import _ba +import ba +from ba import charstr +from ba import SpecialChar as SpCh + +if TYPE_CHECKING: + from typing import List, Tuple, Optional + + +class OnScreenKeyboardWindow(ba.Window): + """Simple built-in on-screen keyboard.""" + + def __init__(self, textwidget: ba.Widget, label: str, max_chars: int): + self._target_text = textwidget + self._width = 700 + self._height = 400 + uiscale = ba.app.ui.uiscale + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + super().__init__(root_widget=ba.containerwidget( + parent=_ba.get_special_widget('overlay_stack'), + size=(self._width, self._height + top_extra), + transition='in_scale', + scale_origin_stack_offset=self._target_text. + get_screen_space_center(), + scale=(2.0 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, 0) if uiscale is ba.UIScale.SMALL else ( + 0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0))) + self._done_button = ba.buttonwidget(parent=self._root_widget, + position=(self._width - 200, 44), + size=(140, 60), + autoselect=True, + label=ba.Lstr(resource='doneText'), + on_activate_call=self._done) + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._cancel, + start_button=self._done_button) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 41), + size=(0, 0), + scale=0.95, + text=label, + maxwidth=self._width - 140, + color=ba.app.ui.title_color, + h_align='center', + v_align='center') + + self._text_field = ba.textwidget( + parent=self._root_widget, + position=(70, self._height - 116), + max_chars=max_chars, + text=cast(str, ba.textwidget(query=self._target_text)), + on_return_press_call=self._done, + autoselect=True, + size=(self._width - 140, 55), + v_align='center', + editable=True, + maxwidth=self._width - 175, + force_internal_editing=True, + always_show_carat=True) + + self._key_color_lit = (1.4, 1.2, 1.4) + self._key_color = (0.69, 0.6, 0.74) + self._key_color_dark = (0.55, 0.55, 0.71) + + self._shift_button: Optional[ba.Widget] = None + self._backspace_button: Optional[ba.Widget] = None + self._space_button: Optional[ba.Widget] = None + self._double_press_shift = False + self._num_mode_button: Optional[ba.Widget] = None + self._emoji_button: Optional[ba.Widget] = None + self._char_keys: List[ba.Widget] = [] + self._keyboard_index = 0 + self._last_space_press = 0.0 + self._double_space_interval = 0.3 + + self._keyboard: ba.Keyboard + self._chars: List[str] + self._modes: List[str] + self._mode: str + self._mode_index: int + self._load_keyboard() + + def _load_keyboard(self) -> None: + # pylint: disable=too-many-locals + self._keyboard = self._get_keyboard() + # We want to get just chars without column data, etc. + self._chars = [j for i in self._keyboard.chars for j in i] + self._modes = ['normal'] + list(self._keyboard.pages) + self._mode_index = 0 + self._mode = self._modes[self._mode_index] + + v = self._height - 180.0 + key_width = 46 * 10 / len(self._keyboard.chars[0]) + key_height = 46 * 3 / len(self._keyboard.chars) + key_textcolor = (1, 1, 1) + row_starts = (69.0, 95.0, 151.0) + key_color = self._key_color + key_color_dark = self._key_color_dark + + self._click_sound = ba.getsound('click01') + + # kill prev char keys + for key in self._char_keys: + key.delete() + self._char_keys = [] + + # dummy data just used for row/column lengths... we don't actually + # set things until refresh + chars: List[Tuple[str, ...]] = self._keyboard.chars + + for row_num, row in enumerate(chars): + h = row_starts[row_num] + # shift key before row 3 + if row_num == 2 and self._shift_button is None: + self._shift_button = ba.buttonwidget( + parent=self._root_widget, + position=(h - key_width * 2.0, v), + size=(key_width * 1.7, key_height), + autoselect=True, + textcolor=key_textcolor, + color=key_color_dark, + label=charstr(SpCh.SHIFT), + enable_sound=False, + extra_touch_border_scale=0.3, + button_type='square', + ) + + for _ in row: + btn = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(key_width, key_height), + autoselect=True, + enable_sound=False, + textcolor=key_textcolor, + color=key_color, + label='', + button_type='square', + extra_touch_border_scale=0.1, + ) + self._char_keys.append(btn) + h += key_width + 10 + + # Add delete key at end of third row. + if row_num == 2: + if self._backspace_button is not None: + self._backspace_button.delete() + + self._backspace_button = ba.buttonwidget( + parent=self._root_widget, + position=(h + 4, v), + size=(key_width * 1.8, key_height), + autoselect=True, + enable_sound=False, + repeat=True, + textcolor=key_textcolor, + color=key_color_dark, + label=charstr(SpCh.DELETE), + button_type='square', + on_activate_call=self._del) + v -= (key_height + 9) + # Do space bar and stuff. + if row_num == 2: + if self._num_mode_button is None: + self._num_mode_button = ba.buttonwidget( + parent=self._root_widget, + position=(112, v - 8), + size=(key_width * 2, key_height + 5), + enable_sound=False, + button_type='square', + extra_touch_border_scale=0.3, + autoselect=True, + textcolor=key_textcolor, + color=key_color_dark, + label='', + ) + if self._emoji_button is None: + self._emoji_button = ba.buttonwidget( + parent=self._root_widget, + position=(56, v - 8), + size=(key_width, key_height + 5), + autoselect=True, + enable_sound=False, + textcolor=key_textcolor, + color=key_color_dark, + label=charstr(SpCh.LOGO_FLAT), + extra_touch_border_scale=0.3, + button_type='square', + ) + btn1 = self._num_mode_button + if self._space_button is None: + self._space_button = ba.buttonwidget( + parent=self._root_widget, + position=(210, v - 12), + size=(key_width * 6.1, key_height + 15), + extra_touch_border_scale=0.3, + enable_sound=False, + autoselect=True, + textcolor=key_textcolor, + color=key_color_dark, + label=ba.Lstr(resource='spaceKeyText'), + on_activate_call=ba.Call(self._type_char, ' ')) + + # Show change instructions only if we have more than one + # keyboard option. + if (ba.app.meta.metascan is not None + and len(ba.app.meta.metascan.keyboards) > 1): + ba.textwidget( + parent=self._root_widget, + h_align='center', + position=(210, v - 70), + size=(key_width * 6.1, key_height + 15), + text=ba.Lstr( + resource='keyboardChangeInstructionsText'), + scale=0.75) + btn2 = self._space_button + btn3 = self._emoji_button + ba.widget(edit=btn1, right_widget=btn2, left_widget=btn3) + ba.widget(edit=btn2, + left_widget=btn1, + right_widget=self._done_button) + ba.widget(edit=btn3, left_widget=btn1) + ba.widget(edit=self._done_button, left_widget=btn2) + + ba.containerwidget(edit=self._root_widget, + selected_child=self._char_keys[14]) + + self._refresh() + + def _get_keyboard(self) -> ba.Keyboard: + assert ba.app.meta.metascan is not None + classname = ba.app.meta.metascan.keyboards[self._keyboard_index] + kbclass = ba.getclass(classname, ba.Keyboard) + return kbclass() + + def _refresh(self) -> None: + chars: Optional[List[str]] = None + if self._mode in ['normal', 'caps']: + chars = list(self._chars) + if self._mode == 'caps': + chars = [c.upper() for c in chars] + ba.buttonwidget(edit=self._shift_button, + color=self._key_color_lit + if self._mode == 'caps' else self._key_color_dark, + label=charstr(SpCh.SHIFT), + on_activate_call=self._shift) + ba.buttonwidget(edit=self._num_mode_button, + label='123#&*', + on_activate_call=self._num_mode) + ba.buttonwidget(edit=self._emoji_button, + color=self._key_color_dark, + label=charstr(SpCh.LOGO_FLAT), + on_activate_call=self._next_mode) + else: + if self._mode == 'num': + chars = list(self._keyboard.nums) + else: + chars = list(self._keyboard.pages[self._mode]) + ba.buttonwidget(edit=self._shift_button, + color=self._key_color_dark, + label='', + on_activate_call=self._null_press) + ba.buttonwidget(edit=self._num_mode_button, + label='abc', + on_activate_call=self._abc_mode) + ba.buttonwidget(edit=self._emoji_button, + color=self._key_color_dark, + label=charstr(SpCh.LOGO_FLAT), + on_activate_call=self._next_mode) + + for i, btn in enumerate(self._char_keys): + assert chars is not None + have_char = True + if i >= len(chars): + # No such char. + have_char = False + pagename = self._mode + ba.print_error( + f'Size of page "{pagename}" of keyboard' + f' "{self._keyboard.name}" is incorrect:' + f' {len(chars)} != {len(self._chars)}' + f' (size of default "normal" page)', + once=True) + ba.buttonwidget(edit=btn, + label=chars[i] if have_char else ' ', + on_activate_call=ba.Call( + self._type_char, + chars[i] if have_char else ' ')) + + def _null_press(self) -> None: + ba.playsound(self._click_sound) + + def _abc_mode(self) -> None: + ba.playsound(self._click_sound) + self._mode = 'normal' + self._refresh() + + def _num_mode(self) -> None: + ba.playsound(self._click_sound) + self._mode = 'num' + self._refresh() + + def _next_mode(self) -> None: + ba.playsound(self._click_sound) + self._mode_index = (self._mode_index + 1) % len(self._modes) + self._mode = self._modes[self._mode_index] + self._refresh() + + def _next_keyboard(self) -> None: + assert ba.app.meta.metascan is not None + self._keyboard_index = (self._keyboard_index + 1) % len( + ba.app.meta.metascan.keyboards) + self._load_keyboard() + if len(ba.app.meta.metascan.keyboards) < 2: + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'), + color=(1, 0, 0)) + else: + ba.screenmessage(ba.Lstr(resource='keyboardSwitchText', + subs=[('${NAME}', self._keyboard.name)]), + color=(0, 1, 0)) + + def _shift(self) -> None: + ba.playsound(self._click_sound) + if self._mode == 'normal': + self._mode = 'caps' + self._double_press_shift = False + elif self._mode == 'caps': + if not self._double_press_shift: + self._double_press_shift = True + else: + self._mode = 'normal' + self._refresh() + + def _del(self) -> None: + ba.playsound(self._click_sound) + txt = cast(str, ba.textwidget(query=self._text_field)) + # pylint: disable=unsubscriptable-object + txt = txt[:-1] + ba.textwidget(edit=self._text_field, text=txt) + + def _type_char(self, char: str) -> None: + ba.playsound(self._click_sound) + if char.isspace(): + if (ba.time(ba.TimeType.REAL) - self._last_space_press < + self._double_space_interval): + self._last_space_press = 0 + self._next_keyboard() + self._del() # We typed unneeded space around 1s ago. + return + self._last_space_press = ba.time(ba.TimeType.REAL) + + # Operate in unicode so we don't do anything funky like chop utf-8 + # chars in half. + txt = cast(str, ba.textwidget(query=self._text_field)) + txt += char + ba.textwidget(edit=self._text_field, text=txt) + + # If we were caps, go back only if not Shift is pressed twice. + if self._mode == 'caps' and not self._double_press_shift: + self._mode = 'normal' + self._refresh() + + def _cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + ba.containerwidget(edit=self._root_widget, transition='out_scale') + + def _done(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_scale') + if self._target_text: + ba.textwidget(edit=self._target_text, + text=cast(str, + ba.textwidget(query=self._text_field))) diff --git a/dist/ba_data/python/bastd/ui/party.py b/dist/ba_data/python/bastd/ui/party.py new file mode 100644 index 0000000..99fb7ab --- /dev/null +++ b/dist/ba_data/python/bastd/ui/party.py @@ -0,0 +1,465 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides party related UI.""" + +from __future__ import annotations + +import math +import weakref +from typing import TYPE_CHECKING, cast + +import _ba +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + from typing import List, Sequence, Optional, Dict, Any + + +class PartyWindow(ba.Window): + """Party list/chat window.""" + + def __del__(self) -> None: + _ba.set_party_window_open(False) + + def __init__(self, origin: Sequence[float] = (0, 0)): + _ba.set_party_window_open(True) + self._r = 'partyWindow' + self._popup_type: Optional[str] = None + self._popup_party_member_client_id: Optional[int] = None + self._popup_party_member_is_host: Optional[bool] = None + self._width = 500 + uiscale = ba.app.ui.uiscale + self._height = (365 if uiscale is ba.UIScale.SMALL else + 480 if uiscale is ba.UIScale.MEDIUM else 600) + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition='in_scale', + color=(0.40, 0.55, 0.20), + parent=_ba.get_special_widget('overlay_stack'), + on_outside_click_call=self.close_with_sound, + scale_origin_stack_offset=origin, + scale=(2.0 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( + 240, 0) if uiscale is ba.UIScale.MEDIUM else (330, 20))) + + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + scale=0.7, + position=(30, self._height - 47), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + self._menu_button = ba.buttonwidget( + parent=self._root_widget, + scale=0.7, + position=(self._width - 60, self._height - 47), + size=(50, 50), + label='...', + autoselect=True, + button_type='square', + on_activate_call=ba.WeakCall(self._on_menu_button_press), + color=(0.55, 0.73, 0.25), + iconscale=1.2) + + info = _ba.get_connection_to_host_info() + if info.get('name', '') != '': + title = ba.Lstr(value=info['name']) + else: + title = ba.Lstr(resource=self._r + '.titleText') + + self._title_text = ba.textwidget(parent=self._root_widget, + scale=0.9, + color=(0.5, 0.7, 0.5), + text=title, + size=(0, 0), + position=(self._width * 0.5, + self._height - 29), + maxwidth=self._width * 0.7, + h_align='center', + v_align='center') + + self._empty_str = ba.textwidget(parent=self._root_widget, + scale=0.75, + size=(0, 0), + position=(self._width * 0.5, + self._height - 65), + maxwidth=self._width * 0.85, + h_align='center', + v_align='center') + + self._scroll_width = self._width - 50 + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + size=(self._scroll_width, + self._height - 200), + position=(30, 80), + color=(0.4, 0.6, 0.3)) + self._columnwidget = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0) + ba.widget(edit=self._menu_button, down_widget=self._columnwidget) + + self._muted_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + h_align='center', + v_align='center', + text=ba.Lstr(resource='chatMutedText')) + self._chat_texts: List[ba.Widget] = [] + + # add all existing messages if chat is not muted + if not ba.app.config.resolve('Chat Muted'): + msgs = _ba.get_chat_messages() + for msg in msgs: + self._add_msg(msg) + + self._text_field = txt = ba.textwidget( + parent=self._root_widget, + editable=True, + size=(530, 40), + position=(44, 39), + text='', + maxwidth=494, + shadow=0.3, + flatness=1.0, + description=ba.Lstr(resource=self._r + '.chatMessageText'), + autoselect=True, + v_align='center', + corner_scale=0.7) + + ba.widget(edit=self._scrollwidget, + autoselect=True, + left_widget=self._cancel_button, + up_widget=self._cancel_button, + down_widget=self._text_field) + ba.widget(edit=self._columnwidget, + autoselect=True, + up_widget=self._cancel_button, + down_widget=self._text_field) + ba.containerwidget(edit=self._root_widget, selected_child=txt) + btn = ba.buttonwidget(parent=self._root_widget, + size=(50, 35), + label=ba.Lstr(resource=self._r + '.sendText'), + button_type='square', + autoselect=True, + position=(self._width - 70, 35), + on_activate_call=self._send_chat_message) + ba.textwidget(edit=txt, on_return_press_call=btn.activate) + self._name_widgets: List[ba.Widget] = [] + self._roster: Optional[List[Dict[str, Any]]] = None + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + repeat=True, + timetype=ba.TimeType.REAL) + self._update() + + def on_chat_message(self, msg: str) -> None: + """Called when a new chat message comes through.""" + if not ba.app.config.resolve('Chat Muted'): + self._add_msg(msg) + + def _add_msg(self, msg: str) -> None: + txt = ba.textwidget(parent=self._columnwidget, + text=msg, + h_align='left', + v_align='center', + size=(0, 13), + scale=0.55, + maxwidth=self._scroll_width * 0.94, + shadow=0.3, + flatness=1.0) + self._chat_texts.append(txt) + if len(self._chat_texts) > 40: + first = self._chat_texts.pop(0) + first.delete() + ba.containerwidget(edit=self._columnwidget, visible_child=txt) + + def _on_menu_button_press(self) -> None: + is_muted = ba.app.config.resolve('Chat Muted') + uiscale = ba.app.ui.uiscale + popup.PopupMenuWindow( + position=self._menu_button.get_screen_space_center(), + scale=(2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23), + choices=['unmute' if is_muted else 'mute'], + choices_display=[ + ba.Lstr( + resource='chatUnMuteText' if is_muted else 'chatMuteText') + ], + current_choice='unmute' if is_muted else 'mute', + delegate=self) + self._popup_type = 'menu' + + def _update(self) -> None: + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-nested-blocks + + # update muted state + if ba.app.config.resolve('Chat Muted'): + ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.3)) + # clear any chat texts we're showing + if self._chat_texts: + while self._chat_texts: + first = self._chat_texts.pop() + first.delete() + else: + ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0)) + + # update roster section + roster = _ba.get_game_roster() + if roster != self._roster: + self._roster = roster + + # clear out old + for widget in self._name_widgets: + widget.delete() + self._name_widgets = [] + if not self._roster: + top_section_height = 60 + ba.textwidget(edit=self._empty_str, + text=ba.Lstr(resource=self._r + '.emptyText')) + ba.scrollwidget(edit=self._scrollwidget, + size=(self._width - 50, + self._height - top_section_height - 110), + position=(30, 80)) + else: + columns = 1 if len( + self._roster) == 1 else 2 if len(self._roster) == 2 else 3 + rows = int(math.ceil(float(len(self._roster)) / columns)) + c_width = (self._width * 0.9) / max(3, columns) + c_width_total = c_width * columns + c_height = 24 + c_height_total = c_height * rows + for y in range(rows): + for x in range(columns): + index = y * columns + x + if index < len(self._roster): + t_scale = 0.65 + pos = (self._width * 0.53 - c_width_total * 0.5 + + c_width * x - 23, + self._height - 65 - c_height * y - 15) + + # if there are players present for this client, use + # their names as a display string instead of the + # client spec-string + try: + if self._roster[index]['players']: + # if there's just one, use the full name; + # otherwise combine short names + if len(self._roster[index] + ['players']) == 1: + p_str = self._roster[index]['players'][ + 0]['name_full'] + else: + p_str = ('/'.join([ + entry['name'] for entry in + self._roster[index]['players'] + ])) + if len(p_str) > 25: + p_str = p_str[:25] + '...' + else: + p_str = self._roster[index][ + 'display_string'] + except Exception: + ba.print_exception( + 'Error calcing client name str.') + p_str = '???' + + widget = ba.textwidget(parent=self._root_widget, + position=(pos[0], pos[1]), + scale=t_scale, + size=(c_width * 0.85, 30), + maxwidth=c_width * 0.85, + color=(1, 1, + 1) if index == 0 else + (1, 1, 1), + selectable=True, + autoselect=True, + click_activate=True, + text=ba.Lstr(value=p_str), + h_align='left', + v_align='center') + self._name_widgets.append(widget) + + # in newer versions client_id will be present and + # we can use that to determine who the host is. + # in older versions we assume the first client is + # host + if self._roster[index]['client_id'] is not None: + is_host = self._roster[index][ + 'client_id'] == -1 + else: + is_host = (index == 0) + + # FIXME: Should pass client_id to these sort of + # calls; not spec-string (perhaps should wait till + # client_id is more readily available though). + ba.textwidget(edit=widget, + on_activate_call=ba.Call( + self._on_party_member_press, + self._roster[index]['client_id'], + is_host, widget)) + pos = (self._width * 0.53 - c_width_total * 0.5 + + c_width * x, + self._height - 65 - c_height * y) + + # Make the assumption that the first roster + # entry is the server. + # FIXME: Shouldn't do this. + if is_host: + twd = min( + c_width * 0.85, + _ba.get_string_width( + p_str, suppress_warning=True) * + t_scale) + self._name_widgets.append( + ba.textwidget( + parent=self._root_widget, + position=(pos[0] + twd + 1, + pos[1] - 0.5), + size=(0, 0), + h_align='left', + v_align='center', + maxwidth=c_width * 0.96 - twd, + color=(0.1, 1, 0.1, 0.5), + text=ba.Lstr(resource=self._r + + '.hostText'), + scale=0.4, + shadow=0.1, + flatness=1.0)) + ba.textwidget(edit=self._empty_str, text='') + ba.scrollwidget(edit=self._scrollwidget, + size=(self._width - 50, + max(100, self._height - 139 - + c_height_total)), + position=(30, 80)) + + def popup_menu_selected_choice(self, popup_window: popup.PopupMenuWindow, + choice: str) -> None: + """Called when a choice is selected in the popup.""" + del popup_window # unused + if self._popup_type == 'partyMemberPress': + if self._popup_party_member_is_host: + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource='internal.cantKickHostError'), + color=(1, 0, 0)) + else: + assert self._popup_party_member_client_id is not None + + # Ban for 5 minutes. + result = _ba.disconnect_client( + self._popup_party_member_client_id, ban_time=5 * 60) + if not result: + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + elif self._popup_type == 'menu': + if choice in ('mute', 'unmute'): + cfg = ba.app.config + cfg['Chat Muted'] = (choice == 'mute') + cfg.apply_and_commit() + self._update() + else: + print('unhandled popup type: ' + str(self._popup_type)) + + def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None: + """Called when the popup is closing.""" + + def _on_party_member_press(self, client_id: int, is_host: bool, + widget: ba.Widget) -> None: + # if we're the host, pop up 'kick' options for all non-host members + if _ba.get_foreground_host_session() is not None: + kick_str = ba.Lstr(resource='kickText') + else: + # kick-votes appeared in build 14248 + if (_ba.get_connection_to_host_info().get('build_number', 0) < + 14248): + return + kick_str = ba.Lstr(resource='kickVoteText') + uiscale = ba.app.ui.uiscale + popup.PopupMenuWindow( + position=widget.get_screen_space_center(), + scale=(2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23), + choices=['kick'], + choices_display=[kick_str], + current_choice='kick', + delegate=self) + self._popup_type = 'partyMemberPress' + self._popup_party_member_client_id = client_id + self._popup_party_member_is_host = is_host + + def _send_chat_message(self) -> None: + _ba.chatmessage(cast(str, ba.textwidget(query=self._text_field))) + ba.textwidget(edit=self._text_field, text='') + + def close(self) -> None: + """Close the window.""" + ba.containerwidget(edit=self._root_widget, transition='out_scale') + + def close_with_sound(self) -> None: + """Close the window and make a lovely sound.""" + ba.playsound(ba.getsound('swish')) + self.close() + + +def handle_party_invite(name: str, invite_id: str) -> None: + """Handle an incoming party invitation.""" + from bastd import mainmenu + from bastd.ui import confirm + ba.playsound(ba.getsound('fanfare')) + + # if we're not in the main menu, just print the invite + # (don't want to screw up an in-progress game) + in_game = not isinstance(_ba.get_foreground_host_session(), + mainmenu.MainMenuSession) + if in_game: + ba.screenmessage(ba.Lstr( + value='${A}\n${B}', + subs=[('${A}', + ba.Lstr(resource='gatherWindow.partyInviteText', + subs=[('${NAME}', name)])), + ('${B}', + ba.Lstr( + resource='gatherWindow.partyInviteGooglePlayExtraText')) + ]), + color=(0.5, 1, 0)) + else: + + def do_accept(inv_id: str) -> None: + _ba.accept_party_invitation(inv_id) + + conf = confirm.ConfirmWindow( + ba.Lstr(resource='gatherWindow.partyInviteText', + subs=[('${NAME}', name)]), + ba.Call(do_accept, invite_id), + width=500, + height=150, + color=(0.75, 1.0, 0.0), + ok_text=ba.Lstr(resource='gatherWindow.partyInviteAcceptText'), + cancel_text=ba.Lstr(resource='gatherWindow.partyInviteIgnoreText')) + + # FIXME: Ugly. + # Let's store the invite-id away on the confirm window so we know if + # we need to kill it later. + conf.party_invite_id = invite_id # type: ignore + + # store a weak-ref so we can get at this later + ba.app.invite_confirm_windows.append(weakref.ref(conf)) + + # go ahead and prune our weak refs while we're here. + ba.app.invite_confirm_windows = [ + w for w in ba.app.invite_confirm_windows if w() is not None + ] diff --git a/dist/ba_data/python/bastd/ui/partyqueue.py b/dist/ba_data/python/bastd/ui/partyqueue.py new file mode 100644 index 0000000..fd8df48 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/partyqueue.py @@ -0,0 +1,532 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI related to waiting in line for a party.""" + +from __future__ import annotations + +import random +import time +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Optional, Sequence, List, Dict + + +class PartyQueueWindow(ba.Window): + """Window showing players waiting to join a server.""" + + # ewww this needs quite a bit of de-linting if/when i revisit it.. + # pylint: disable=invalid-name + # pylint: disable=consider-using-dict-comprehension + class Dude: + """Represents a single dude waiting in a server line.""" + + def __init__(self, parent: PartyQueueWindow, distance: float, + initial_offset: float, is_player: bool, account_id: str, + name: str): + self.claimed = False + self._line_left = parent.get_line_left() + self._line_width = parent.get_line_width() + self._line_bottom = parent.get_line_bottom() + self._target_distance = distance + self._distance = distance + initial_offset + self._boost_brightness = 0.0 + self._debug = False + self._sc = sc = 1.1 if is_player else 0.6 + random.random() * 0.2 + self._y_offs = -30.0 if is_player else -47.0 * sc + self._last_boost_time = 0.0 + self._color = (0.2, 1.0, + 0.2) if is_player else (0.5 + 0.3 * random.random(), + 0.4 + 0.2 * random.random(), + 0.5 + 0.3 * random.random()) + self._eye_color = (0.7 * 1.0 + 0.3 * self._color[0], + 0.7 * 1.0 + 0.3 * self._color[1], + 0.7 * 1.0 + 0.3 * self._color[2]) + self._body_image = ba.buttonwidget( + parent=parent.get_root_widget(), + selectable=True, + label='', + size=(sc * 60, sc * 80), + color=self._color, + texture=parent.lineup_tex, + model_transparent=parent.lineup_1_transparent_model) + ba.buttonwidget(edit=self._body_image, + on_activate_call=ba.WeakCall( + parent.on_account_press, account_id, + self._body_image)) + ba.widget(edit=self._body_image, autoselect=True) + self._eyes_image = ba.imagewidget( + parent=parent.get_root_widget(), + size=(sc * 36, sc * 18), + texture=parent.lineup_tex, + color=self._eye_color, + model_transparent=parent.eyes_model) + self._name_text = ba.textwidget(parent=parent.get_root_widget(), + size=(0, 0), + shadow=0, + flatness=1.0, + text=name, + maxwidth=100, + h_align='center', + v_align='center', + scale=0.75, + color=(1, 1, 1, 0.6)) + self._update_image() + + # DEBUG: vis target pos.. + self._body_image_target: Optional[ba.Widget] + self._eyes_image_target: Optional[ba.Widget] + if self._debug: + self._body_image_target = ba.imagewidget( + parent=parent.get_root_widget(), + size=(sc * 60, sc * 80), + color=self._color, + texture=parent.lineup_tex, + model_transparent=parent.lineup_1_transparent_model) + self._eyes_image_target = ba.imagewidget( + parent=parent.get_root_widget(), + size=(sc * 36, sc * 18), + texture=parent.lineup_tex, + color=self._eye_color, + model_transparent=parent.eyes_model) + # (updates our image positions) + self.set_target_distance(self._target_distance) + else: + self._body_image_target = self._eyes_image_target = None + + def __del__(self) -> None: + + # ew. our destructor here may get called as part of an internal + # widget tear-down. + # running further widget calls here can quietly break stuff, so we + # need to push a deferred call to kill these as necessary instead. + # (should bulletproof internal widget code to give a clean error + # in this case) + def kill_widgets(widgets: Sequence[Optional[ba.Widget]]) -> None: + for widget in widgets: + if widget: + widget.delete() + + ba.pushcall( + ba.Call(kill_widgets, [ + self._body_image, self._eyes_image, + self._body_image_target, self._eyes_image_target, + self._name_text + ])) + + def set_target_distance(self, dist: float) -> None: + """Set distance for a dude.""" + self._target_distance = dist + if self._debug: + sc = self._sc + position = (self._line_left + self._line_width * + (1.0 - self._target_distance), + self._line_bottom - 30) + ba.imagewidget(edit=self._body_image_target, + position=(position[0] - sc * 30, + position[1] - sc * 25 - 70)) + ba.imagewidget(edit=self._eyes_image_target, + position=(position[0] - sc * 18, + position[1] + sc * 31 - 70)) + + def step(self, smoothing: float) -> None: + """Step this dude.""" + self._distance = (smoothing * self._distance + + (1.0 - smoothing) * self._target_distance) + self._update_image() + self._boost_brightness *= 0.9 + + def _update_image(self) -> None: + sc = self._sc + position = (self._line_left + self._line_width * + (1.0 - self._distance), self._line_bottom + 40) + brightness = 1.0 + self._boost_brightness + ba.buttonwidget(edit=self._body_image, + position=(position[0] - sc * 30, + position[1] - sc * 25 + self._y_offs), + color=(self._color[0] * brightness, + self._color[1] * brightness, + self._color[2] * brightness)) + ba.imagewidget(edit=self._eyes_image, + position=(position[0] - sc * 18, + position[1] + sc * 31 + self._y_offs), + color=(self._eye_color[0] * brightness, + self._eye_color[1] * brightness, + self._eye_color[2] * brightness)) + ba.textwidget(edit=self._name_text, + position=(position[0] - sc * 0, + position[1] + sc * 40.0)) + + def boost(self, amount: float, smoothing: float) -> None: + """Boost this dude.""" + del smoothing # unused arg + self._distance = max(0.0, self._distance - amount) + self._update_image() + self._last_boost_time = time.time() + self._boost_brightness += 0.6 + + def __init__(self, queue_id: str, address: str, port: int): + ba.app.ui.have_party_queue_window = True + self._address = address + self._port = port + self._queue_id = queue_id + self._width = 800 + self._height = 400 + self._last_connect_attempt_time: Optional[float] = None + self._last_transaction_time: Optional[float] = None + self._boost_button: Optional[ba.Widget] = None + self._boost_price: Optional[ba.Widget] = None + self._boost_label: Optional[ba.Widget] = None + self._field_shown = False + self._dudes: List[PartyQueueWindow.Dude] = [] + self._dudes_by_id: Dict[int, PartyQueueWindow.Dude] = {} + self._line_left = 40.0 + self._line_width = self._width - 190 + self._line_bottom = self._height * 0.4 + self.lineup_tex = ba.gettexture('playerLineup') + self._smoothing = 0.0 + self._initial_offset = 0.0 + self._boost_tickets = 0 + self._boost_strength = 0.0 + self._angry_computer_transparent_model = ba.getmodel( + 'angryComputerTransparent') + self._angry_computer_image: Optional[ba.Widget] = None + self.lineup_1_transparent_model = ba.getmodel( + 'playerLineup1Transparent') + self._lineup_2_transparent_model = ba.getmodel( + 'playerLineup2Transparent') + self._lineup_3_transparent_model = ba.getmodel( + 'playerLineup3Transparent') + self._lineup_4_transparent_model = ba.getmodel( + 'playerLineup4Transparent') + self._line_image: Optional[ba.Widget] = None + self.eyes_model = ba.getmodel('plasticEyesTransparent') + self._white_tex = ba.gettexture('white') + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + color=(0.45, 0.63, 0.15), + transition='in_scale', + scale=(1.4 if uiscale is ba.UIScale.SMALL else + 1.2 if uiscale is ba.UIScale.MEDIUM else 1.0))) + + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + scale=1.0, + position=(60, self._height - 80), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + self._title_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.55), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=1.3, + h_align='center', + v_align='center', + text=ba.Lstr(resource='internal.connectingToPartyText'), + maxwidth=self._width * 0.65) + + self._tickets_text = ba.textwidget(parent=self._root_widget, + position=(self._width - 180, + self._height - 20), + size=(0, 0), + color=(0.2, 1.0, 0.2), + scale=0.7, + h_align='center', + v_align='center', + text='') + + # update at roughly 30fps + self._update_timer = ba.Timer(0.033, + ba.WeakCall(self.update), + repeat=True, + timetype=ba.TimeType.REAL) + self.update() + + def __del__(self) -> None: + try: + ba.app.ui.have_party_queue_window = False + _ba.add_transaction({ + 'type': 'PARTY_QUEUE_REMOVE', + 'q': self._queue_id + }) + _ba.run_transactions() + except Exception: + ba.print_exception('Error removing self from party queue.') + + def get_line_left(self) -> float: + """(internal)""" + return self._line_left + + def get_line_width(self) -> float: + """(internal)""" + return self._line_width + + def get_line_bottom(self) -> float: + """(internal)""" + return self._line_bottom + + def on_account_press(self, account_id: Optional[str], + origin_widget: ba.Widget) -> None: + """A dude was clicked so we should show his account info.""" + from bastd.ui.account import viewer + if account_id is None: + ba.playsound(ba.getsound('error')) + return + viewer.AccountViewerWindow( + account_id=account_id, + position=origin_widget.get_screen_space_center()) + + def close(self) -> None: + """Close the ui.""" + ba.containerwidget(edit=self._root_widget, transition='out_scale') + + def _update_field(self, response: Dict[str, Any]) -> None: + if self._angry_computer_image is None: + self._angry_computer_image = ba.imagewidget( + parent=self._root_widget, + position=(self._width - 180, self._height * 0.5 - 65), + size=(150, 150), + texture=self.lineup_tex, + model_transparent=self._angry_computer_transparent_model) + if self._line_image is None: + self._line_image = ba.imagewidget( + parent=self._root_widget, + color=(0.0, 0.0, 0.0), + opacity=0.2, + position=(self._line_left, self._line_bottom - 2.0), + size=(self._line_width, 4.0), + texture=self._white_tex) + + # now go through the data they sent, creating dudes for us and our + # enemies as needed and updating target positions on all of them.. + + # mark all as unclaimed so we know which ones to kill off.. + for dude in self._dudes: + dude.claimed = False + + # always have a dude for ourself.. + if -1 not in self._dudes_by_id: + dude = self.Dude( + self, response['d'], self._initial_offset, True, + _ba.get_account_misc_read_val_2('resolvedAccountID', None), + _ba.get_account_display_string()) + self._dudes_by_id[-1] = dude + self._dudes.append(dude) + else: + self._dudes_by_id[-1].set_target_distance(response['d']) + self._dudes_by_id[-1].claimed = True + + # now create/destroy enemies + for (enemy_id, enemy_distance, enemy_account_id, + enemy_name) in response['e']: + if enemy_id not in self._dudes_by_id: + dude = self.Dude(self, enemy_distance, self._initial_offset, + False, enemy_account_id, enemy_name) + self._dudes_by_id[enemy_id] = dude + self._dudes.append(dude) + else: + self._dudes_by_id[enemy_id].set_target_distance(enemy_distance) + self._dudes_by_id[enemy_id].claimed = True + + # remove unclaimed dudes from both of our lists + self._dudes_by_id = dict([ + item for item in list(self._dudes_by_id.items()) if item[1].claimed + ]) + self._dudes = [dude for dude in self._dudes if dude.claimed] + + def _hide_field(self) -> None: + if self._angry_computer_image: + self._angry_computer_image.delete() + self._angry_computer_image = None + if self._line_image: + self._line_image.delete() + self._line_image = None + self._dudes = [] + self._dudes_by_id = {} + + def on_update_response(self, response: Optional[Dict[str, Any]]) -> None: + """We've received a response from an update to the server.""" + # pylint: disable=too-many-branches + if not self._root_widget: + return + + # Seeing this in logs; debugging... + if not self._title_text: + print('PartyQueueWindows update: Have root but no title_text.') + return + + if response is not None: + should_show_field = (response.get('d') is not None) + self._smoothing = response['s'] + self._initial_offset = response['o'] + + # If they gave us a position, show the field. + if should_show_field: + ba.textwidget(edit=self._title_text, + text=ba.Lstr(resource='waitingInLineText'), + position=(self._width * 0.5, + self._height * 0.85)) + self._update_field(response) + self._field_shown = True + if not should_show_field and self._field_shown: + ba.textwidget( + edit=self._title_text, + text=ba.Lstr(resource='internal.connectingToPartyText'), + position=(self._width * 0.5, self._height * 0.55)) + self._hide_field() + self._field_shown = False + + # if they told us there's a boost button, update.. + if response.get('bt') is not None: + self._boost_tickets = response['bt'] + self._boost_strength = response['ba'] + if self._boost_button is None: + self._boost_button = ba.buttonwidget( + parent=self._root_widget, + scale=1.0, + position=(self._width * 0.5 - 75, 20), + size=(150, 100), + button_type='square', + label='', + on_activate_call=self.on_boost_press, + enable_sound=False, + color=(0, 1, 0), + autoselect=True) + self._boost_label = ba.textwidget( + parent=self._root_widget, + draw_controller=self._boost_button, + position=(self._width * 0.5, 88), + size=(0, 0), + color=(0.8, 1.0, 0.8), + scale=1.5, + h_align='center', + v_align='center', + text=ba.Lstr(resource='boostText'), + maxwidth=150) + self._boost_price = ba.textwidget( + parent=self._root_widget, + draw_controller=self._boost_button, + position=(self._width * 0.5, 50), + size=(0, 0), + color=(0, 1, 0), + scale=0.9, + h_align='center', + v_align='center', + text=ba.charstr(ba.SpecialChar.TICKET) + + str(self._boost_tickets), + maxwidth=150) + else: + if self._boost_button is not None: + self._boost_button.delete() + self._boost_button = None + if self._boost_price is not None: + self._boost_price.delete() + self._boost_price = None + if self._boost_label is not None: + self._boost_label.delete() + self._boost_label = None + + # if they told us to go ahead and try and connect, do so.. + # (note: servers will disconnect us if we try to connect before + # getting this go-ahead, so don't get any bright ideas...) + if response.get('c', False): + # enforce a delay between connection attempts + # (in case they're jamming on the boost button) + now = time.time() + if (self._last_connect_attempt_time is None + or now - self._last_connect_attempt_time > 10.0): + _ba.connect_to_party(address=self._address, + port=self._port, + print_progress=False) + self._last_connect_attempt_time = now + + def on_boost_press(self) -> None: + """Boost was pressed.""" + from bastd.ui import account + from bastd.ui import getcurrency + if _ba.get_account_state() != 'signed_in': + account.show_sign_in_prompt() + return + + if _ba.get_account_ticket_count() < self._boost_tickets: + ba.playsound(ba.getsound('error')) + getcurrency.show_get_tickets_prompt() + return + + ba.playsound(ba.getsound('laserReverse')) + _ba.add_transaction( + { + 'type': 'PARTY_QUEUE_BOOST', + 't': self._boost_tickets, + 'q': self._queue_id + }, + callback=ba.WeakCall(self.on_update_response)) + # lets not run these immediately (since they may be rapid-fire, + # just bucket them until the next tick) + + # the transaction handles the local ticket change, but we apply our + # local boost vis manually here.. + # (our visualization isn't really wired up to be transaction-based) + our_dude = self._dudes_by_id.get(-1) + if our_dude is not None: + our_dude.boost(self._boost_strength, self._smoothing) + + def update(self) -> None: + """Update!""" + if not self._root_widget: + return + + # Update boost-price. + if self._boost_price is not None: + ba.textwidget(edit=self._boost_price, + text=ba.charstr(ba.SpecialChar.TICKET) + + str(self._boost_tickets)) + + # Update boost button color based on if we have enough moola. + if self._boost_button is not None: + can_boost = ( + (_ba.get_account_state() == 'signed_in' + and _ba.get_account_ticket_count() >= self._boost_tickets)) + ba.buttonwidget(edit=self._boost_button, + color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7)) + + # Update ticket-count. + if self._tickets_text is not None: + if self._boost_button is not None: + if _ba.get_account_state() == 'signed_in': + val = ba.charstr(ba.SpecialChar.TICKET) + str( + _ba.get_account_ticket_count()) + else: + val = ba.charstr(ba.SpecialChar.TICKET) + '???' + ba.textwidget(edit=self._tickets_text, text=val) + else: + ba.textwidget(edit=self._tickets_text, text='') + + current_time = ba.time(ba.TimeType.REAL) + if (self._last_transaction_time is None + or current_time - self._last_transaction_time > + 0.001 * _ba.get_account_misc_read_val('pqInt', 5000)): + self._last_transaction_time = current_time + _ba.add_transaction( + { + 'type': 'PARTY_QUEUE_QUERY', + 'q': self._queue_id + }, + callback=ba.WeakCall(self.on_update_response)) + _ba.run_transactions() + + # step our dudes + for dude in self._dudes: + dude.step(self._smoothing) diff --git a/dist/ba_data/python/bastd/ui/play.py b/dist/ba_data/python/bastd/ui/play.py new file mode 100644 index 0000000..620b7a0 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/play.py @@ -0,0 +1,588 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides the top level play window.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Optional, Tuple + + +class PlayWindow(ba.Window): + """Window for selecting overall play type.""" + + def __init__(self, + transition: str = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + import threading + + # Preload some modules we use in a background thread so we won't + # have a visual hitch when the user taps them. + threading.Thread(target=self._preload_modules).start() + + # We can currently be used either for main menu duty or for selecting + # playlists (should make this more elegant/general). + self._is_main_menu = not ba.app.ui.selecting_private_party_playlist + + uiscale = ba.app.ui.uiscale + width = 1000 if uiscale is ba.UIScale.SMALL else 800 + x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 + height = 550 + button_width = 400 + + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + self._r = 'playWindow' + + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition=transition, + toolbar_visibility='menu_full', + scale_origin_stack_offset=scale_origin, + scale=(1.6 if uiscale is ba.UIScale.SMALL else + 0.9 if uiscale is ba.UIScale.MEDIUM else 0.8), + stack_offset=(0, 0) if uiscale is ba.UIScale.SMALL else (0, 0))) + self._back_button = back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(55 + x_offs, height - 132), + size=(120, 60), + scale=1.1, + text_res_scale=1.5, + text_scale=1.2, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back') + + txt = ba.textwidget( + parent=self._root_widget, + position=(width * 0.5, height - 101), + # position=(width * 0.5, height - + # (101 if main_menu else 61)), + size=(0, 0), + text=ba.Lstr(resource=( + self._r + + '.titleText') if self._is_main_menu else 'playlistsText'), + scale=1.7, + res_scale=2.0, + maxwidth=400, + color=ba.app.ui.heading_color, + h_align='center', + v_align='center') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + ba.textwidget(edit=txt, text='') + + v = height - (110 if self._is_main_menu else 90) + v -= 100 + clr = (0.6, 0.7, 0.6, 1.0) + v -= 280 if self._is_main_menu else 180 + v += (30 + if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL else 0) + hoffs = x_offs + 80 if self._is_main_menu else x_offs - 100 + scl = 1.13 if self._is_main_menu else 0.68 + + self._lineup_tex = ba.gettexture('playerLineup') + angry_computer_transparent_model = ba.getmodel( + 'angryComputerTransparent') + self._lineup_1_transparent_model = ba.getmodel( + 'playerLineup1Transparent') + self._lineup_2_transparent_model = ba.getmodel( + 'playerLineup2Transparent') + self._lineup_3_transparent_model = ba.getmodel( + 'playerLineup3Transparent') + self._lineup_4_transparent_model = ba.getmodel( + 'playerLineup4Transparent') + self._eyes_model = ba.getmodel('plasticEyesTransparent') + + self._coop_button: Optional[ba.Widget] = None + + # Only show coop button in main-menu variant. + if self._is_main_menu: + self._coop_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(hoffs, v + (scl * 15 if self._is_main_menu else 0)), + size=(scl * button_width, + scl * (300 if self._is_main_menu else 360)), + extra_touch_border_scale=0.1, + autoselect=True, + label='', + button_type='square', + text_scale=1.13, + on_activate_call=self._coop) + + if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + ba.widget(edit=btn, + left_widget=_ba.get_special_widget('back_button')) + ba.widget(edit=btn, + up_widget=_ba.get_special_widget('account_button')) + ba.widget( + edit=btn, + down_widget=_ba.get_special_widget('settings_button')) + + self._draw_dude(0, + btn, + hoffs, + v, + scl, + position=(140, 30), + color=(0.72, 0.4, 1.0)) + self._draw_dude(1, + btn, + hoffs, + v, + scl, + position=(185, 53), + color=(0.71, 0.5, 1.0)) + self._draw_dude(2, + btn, + hoffs, + v, + scl, + position=(220, 27), + color=(0.67, 0.44, 1.0)) + self._draw_dude(3, + btn, + hoffs, + v, + scl, + position=(255, 57), + color=(0.7, 0.3, 1.0)) + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * 230, v + scl * 153), + size=(scl * 115, scl * 115), + texture=self._lineup_tex, + model_transparent=angry_computer_transparent_model) + + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (-10), v + scl * 95), + size=(scl * button_width, scl * 50), + text=ba.Lstr( + resource='playModes.singlePlayerCoopText', + fallback_resource='playModes.coopText'), + maxwidth=scl * button_width * 0.7, + res_scale=1.5, + h_align='center', + v_align='center', + color=(0.7, 0.9, 0.7, 1.0), + scale=scl * 2.3) + + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (-10), v + (scl * 54)), + size=(scl * button_width, scl * 30), + text=ba.Lstr(resource=self._r + + '.oneToFourPlayersText'), + h_align='center', + v_align='center', + scale=0.83 * scl, + flatness=1.0, + maxwidth=scl * button_width * 0.7, + color=clr) + + scl = 0.5 if self._is_main_menu else 0.68 + hoffs += 440 if self._is_main_menu else 216 + v += 180 if self._is_main_menu else -68 + + self._teams_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(hoffs, v + (scl * 15 if self._is_main_menu else 0)), + size=(scl * button_width, + scl * (300 if self._is_main_menu else 360)), + extra_touch_border_scale=0.1, + autoselect=True, + label='', + button_type='square', + text_scale=1.13, + on_activate_call=self._team_tourney) + + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + up_widget=_ba.get_special_widget('tickets_plus_button'), + right_widget=_ba.get_special_widget('party_button')) + + xxx = -14 + self._draw_dude(2, + btn, + hoffs, + v, + scl, + position=(xxx + 148, 30), + color=(0.2, 0.4, 1.0)) + self._draw_dude(3, + btn, + hoffs, + v, + scl, + position=(xxx + 181, 53), + color=(0.3, 0.4, 1.0)) + self._draw_dude(1, + btn, + hoffs, + v, + scl, + position=(xxx + 216, 33), + color=(0.3, 0.5, 1.0)) + self._draw_dude(0, + btn, + hoffs, + v, + scl, + position=(xxx + 245, 57), + color=(0.3, 0.5, 1.0)) + + xxx = 155 + self._draw_dude(0, + btn, + hoffs, + v, + scl, + position=(xxx + 151, 30), + color=(1.0, 0.5, 0.4)) + self._draw_dude(1, + btn, + hoffs, + v, + scl, + position=(xxx + 189, 53), + color=(1.0, 0.58, 0.58)) + self._draw_dude(3, + btn, + hoffs, + v, + scl, + position=(xxx + 223, 27), + color=(1.0, 0.5, 0.5)) + self._draw_dude(2, + btn, + hoffs, + v, + scl, + position=(xxx + 257, 57), + color=(1.0, 0.5, 0.5)) + + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (-10), v + scl * 95), + size=(scl * button_width, scl * 50), + text=ba.Lstr(resource='playModes.teamsText', + fallback_resource='teamsText'), + res_scale=1.5, + maxwidth=scl * button_width * 0.7, + h_align='center', + v_align='center', + color=(0.7, 0.9, 0.7, 1.0), + scale=scl * 2.3) + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (-10), v + (scl * 54)), + size=(scl * button_width, scl * 30), + text=ba.Lstr(resource=self._r + + '.twoToEightPlayersText'), + h_align='center', + v_align='center', + res_scale=1.5, + scale=0.9 * scl, + flatness=1.0, + maxwidth=scl * button_width * 0.7, + color=clr) + + hoffs += 0 if self._is_main_menu else 300 + v -= 155 if self._is_main_menu else 0 + self._free_for_all_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(hoffs, v + (scl * 15 if self._is_main_menu else 0)), + size=(scl * button_width, + scl * (300 if self._is_main_menu else 360)), + extra_touch_border_scale=0.1, + autoselect=True, + label='', + button_type='square', + text_scale=1.13, + on_activate_call=self._free_for_all) + + xxx = -5 + self._draw_dude(0, + btn, + hoffs, + v, + scl, + position=(xxx + 140, 30), + color=(0.4, 1.0, 0.4)) + self._draw_dude(3, + btn, + hoffs, + v, + scl, + position=(xxx + 185, 53), + color=(1.0, 0.4, 0.5)) + self._draw_dude(1, + btn, + hoffs, + v, + scl, + position=(xxx + 220, 27), + color=(0.4, 0.5, 1.0)) + self._draw_dude(2, + btn, + hoffs, + v, + scl, + position=(xxx + 255, 57), + color=(0.5, 1.0, 0.4)) + xxx = 140 + self._draw_dude(2, + btn, + hoffs, + v, + scl, + position=(xxx + 148, 30), + color=(1.0, 0.9, 0.4)) + self._draw_dude(0, + btn, + hoffs, + v, + scl, + position=(xxx + 182, 53), + color=(0.7, 1.0, 0.5)) + self._draw_dude(3, + btn, + hoffs, + v, + scl, + position=(xxx + 233, 27), + color=(0.7, 0.5, 0.9)) + self._draw_dude(1, + btn, + hoffs, + v, + scl, + position=(xxx + 266, 53), + color=(0.4, 0.5, 0.8)) + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (-10), v + scl * 95), + size=(scl * button_width, scl * 50), + text=ba.Lstr(resource='playModes.freeForAllText', + fallback_resource='freeForAllText'), + maxwidth=scl * button_width * 0.7, + h_align='center', + v_align='center', + color=(0.7, 0.9, 0.7, 1.0), + scale=scl * 1.9) + ba.textwidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (-10), v + (scl * 54)), + size=(scl * button_width, scl * 30), + text=ba.Lstr(resource=self._r + + '.twoToEightPlayersText'), + h_align='center', + v_align='center', + scale=0.9 * scl, + flatness=1.0, + maxwidth=scl * button_width * 0.7, + color=clr) + + if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + back_button.delete() + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._back, + selected_child=self._coop_button + if self._is_main_menu else self._teams_button) + else: + ba.buttonwidget(edit=back_button, on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, + cancel_button=back_button, + selected_child=self._coop_button + if self._is_main_menu else self._teams_button) + + self._restore_state() + + @staticmethod + def _preload_modules() -> None: + """Preload modules we use (called in bg thread).""" + import bastd.ui.mainmenu as _unused1 + import bastd.ui.account as _unused2 + import bastd.ui.coop.browser as _unused3 + import bastd.ui.playlist.browser as _unused4 + + def _back(self) -> None: + # pylint: disable=cyclic-import + if self._is_main_menu: + from bastd.ui.mainmenu import MainMenuWindow + self._save_state() + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition='in_left').get_root_widget()) + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + else: + from bastd.ui.gather import GatherWindow + self._save_state() + ba.app.ui.set_main_menu_window( + GatherWindow(transition='in_left').get_root_widget()) + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + + def _coop(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.coop.browser import CoopBrowserWindow + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + CoopBrowserWindow( + origin_widget=self._coop_button).get_root_widget()) + + def _team_tourney(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playlist.browser import PlaylistBrowserWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + PlaylistBrowserWindow( + origin_widget=self._teams_button, + sessiontype=ba.DualTeamSession).get_root_widget()) + + def _free_for_all(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playlist.browser import PlaylistBrowserWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + PlaylistBrowserWindow( + origin_widget=self._free_for_all_button, + sessiontype=ba.FreeForAllSession).get_root_widget()) + + def _draw_dude(self, i: int, btn: ba.Widget, hoffs: float, v: float, + scl: float, position: Tuple[float, float], + color: Tuple[float, float, float]) -> None: + h_extra = -100 + v_extra = 130 + eye_color = (0.7 * 1.0 + 0.3 * color[0], 0.7 * 1.0 + 0.3 * color[1], + 0.7 * 1.0 + 0.3 * color[2]) + if i == 0: + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (h_extra + position[0]), + v + scl * (v_extra + position[1])), + size=(scl * 60, scl * 80), + color=color, + texture=self._lineup_tex, + model_transparent=self._lineup_1_transparent_model) + ba.imagewidget( + parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (h_extra + position[0] + 12), + v + scl * (v_extra + position[1] + 53)), + size=(scl * 36, scl * 18), + texture=self._lineup_tex, + color=eye_color, + model_transparent=self._eyes_model) + elif i == 1: + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (h_extra + position[0]), + v + scl * (v_extra + position[1])), + size=(scl * 45, scl * 90), + color=color, + texture=self._lineup_tex, + model_transparent=self._lineup_2_transparent_model) + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (h_extra + position[0] + 5), + v + scl * (v_extra + position[1] + 67)), + size=(scl * 32, scl * 16), + texture=self._lineup_tex, + color=eye_color, + model_transparent=self._eyes_model) + elif i == 2: + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (h_extra + position[0]), + v + scl * (v_extra + position[1])), + size=(scl * 45, scl * 90), + color=color, + texture=self._lineup_tex, + model_transparent=self._lineup_3_transparent_model) + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (h_extra + position[0] + 5), + v + scl * (v_extra + position[1] + 59)), + size=(scl * 34, scl * 17), + texture=self._lineup_tex, + color=eye_color, + model_transparent=self._eyes_model) + elif i == 3: + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (h_extra + position[0]), + v + scl * (v_extra + position[1])), + size=(scl * 48, scl * 96), + color=color, + texture=self._lineup_tex, + model_transparent=self._lineup_4_transparent_model) + ba.imagewidget(parent=self._root_widget, + draw_controller=btn, + position=(hoffs + scl * (h_extra + position[0] + 2), + v + scl * (v_extra + position[1] + 62)), + size=(scl * 38, scl * 19), + texture=self._lineup_tex, + color=eye_color, + model_transparent=self._eyes_model) + + def _save_state(self) -> None: + try: + sel = self._root_widget.get_selected_child() + if sel == self._teams_button: + sel_name = 'Team Games' + elif self._coop_button is not None and sel == self._coop_button: + sel_name = 'Co-op Games' + elif sel == self._free_for_all_button: + sel_name = 'Free-for-All Games' + elif sel == self._back_button: + sel_name = 'Back' + else: + raise ValueError(f'unrecognized selection {sel}') + ba.app.ui.window_states[type(self)] = sel_name + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + def _restore_state(self) -> None: + try: + sel_name = ba.app.ui.window_states.get(type(self)) + if sel_name == 'Team Games': + sel = self._teams_button + elif sel_name == 'Co-op Games' and self._coop_button is not None: + sel = self._coop_button + elif sel_name == 'Free-for-All Games': + sel = self._free_for_all_button + elif sel_name == 'Back': + sel = self._back_button + else: + sel = (self._coop_button if self._coop_button is not None else + self._teams_button) + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') diff --git a/dist/ba_data/python/bastd/ui/playlist/__init__.py b/dist/ba_data/python/bastd/ui/playlist/__init__.py new file mode 100644 index 0000000..483008c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playlist/__init__.py @@ -0,0 +1,53 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Playlist ui functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Type + + +# FIXME: Could change this to be a classmethod of session types? +class PlaylistTypeVars: + """Defines values for a playlist type (config names to use, etc).""" + + def __init__(self, sessiontype: Type[ba.Session]): + from ba.internal import (get_default_teams_playlist, + get_default_free_for_all_playlist) + self.sessiontype: Type[ba.Session] + + if issubclass(sessiontype, ba.DualTeamSession): + play_mode_name = ba.Lstr(resource='playModes.teamsText', + fallback_resource='teamsText') + self.get_default_list_call = get_default_teams_playlist + self.session_type_name = 'ba.DualTeamSession' + self.config_name = 'Team Tournament' + self.window_title_name = ba.Lstr(resource='playModes.teamsText', + fallback_resource='teamsText') + self.sessiontype = ba.DualTeamSession + + elif issubclass(sessiontype, ba.FreeForAllSession): + play_mode_name = ba.Lstr(resource='playModes.freeForAllText', + fallback_resource='freeForAllText') + self.get_default_list_call = get_default_free_for_all_playlist + self.session_type_name = 'ba.FreeForAllSession' + self.config_name = 'Free-for-All' + self.window_title_name = ba.Lstr( + resource='playModes.freeForAllText', + fallback_resource='freeForAllText') + self.sessiontype = ba.FreeForAllSession + + else: + raise RuntimeError( + f'Playlist type vars undefined for sessiontype: {sessiontype}') + self.default_list_name = ba.Lstr(resource='defaultGameListNameText', + subs=[('${PLAYMODE}', play_mode_name) + ]) + self.default_new_list_name = ba.Lstr( + resource='defaultNewGameListNameText', + subs=[('${PLAYMODE}', play_mode_name)]) diff --git a/dist/ba_data/python/bastd/ui/playlist/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/playlist/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6420ec6 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/playlist/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-38.opt-1.pyc new file mode 100644 index 0000000..bdf1616 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/playlist/__pycache__/browser.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/playlist/__pycache__/browser.cpython-38.opt-1.pyc new file mode 100644 index 0000000..23b2133 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/playlist/__pycache__/browser.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/playlist/__pycache__/customizebrowser.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/playlist/__pycache__/customizebrowser.cpython-38.opt-1.pyc new file mode 100644 index 0000000..70fb452 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/playlist/__pycache__/customizebrowser.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/playlist/__pycache__/edit.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/playlist/__pycache__/edit.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d27deb6 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/playlist/__pycache__/edit.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7099cf4 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6421803 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/playlist/__pycache__/mapselect.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/playlist/__pycache__/mapselect.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c83d449 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/playlist/__pycache__/mapselect.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/playlist/__pycache__/share.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/playlist/__pycache__/share.cpython-38.opt-1.pyc new file mode 100644 index 0000000..438f941 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/playlist/__pycache__/share.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/playlist/addgame.py b/dist/ba_data/python/bastd/ui/playlist/addgame.py new file mode 100644 index 0000000..af51c57 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playlist/addgame.py @@ -0,0 +1,205 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a window for selecting a game type to add to a playlist.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Type, Optional + from bastd.ui.playlist.editcontroller import PlaylistEditController + + +class PlaylistAddGameWindow(ba.Window): + """Window for selecting a game type to add to a playlist.""" + + def __init__(self, + editcontroller: PlaylistEditController, + transition: str = 'in_right'): + self._editcontroller = editcontroller + self._r = 'addGameWindow' + uiscale = ba.app.ui.uiscale + self._width = 750 if uiscale is ba.UIScale.SMALL else 650 + x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + self._height = (346 if uiscale is ba.UIScale.SMALL else + 380 if uiscale is ba.UIScale.MEDIUM else 440) + top_extra = 30 if uiscale is ba.UIScale.SMALL else 20 + self._scroll_width = 210 + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + scale=(2.17 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, 1) if uiscale is ba.UIScale.SMALL else (0, 0))) + + self._back_button = ba.buttonwidget(parent=self._root_widget, + position=(58 + x_inset, + self._height - 53), + size=(165, 70), + scale=0.75, + text_scale=1.2, + label=ba.Lstr(resource='backText'), + autoselect=True, + button_type='back', + on_activate_call=self._back) + self._select_button = select_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - (172 + x_inset), self._height - 50), + autoselect=True, + size=(160, 60), + scale=0.75, + text_scale=1.2, + label=ba.Lstr(resource='selectText'), + on_activate_call=self._add) + + if ba.app.ui.use_toolbars: + ba.widget(edit=select_button, + right_widget=_ba.get_special_widget('party_button')) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 28), + size=(0, 0), + scale=1.0, + text=ba.Lstr(resource=self._r + '.titleText'), + h_align='center', + color=ba.app.ui.title_color, + maxwidth=250, + v_align='center') + v = self._height - 64 + + self._selected_title_text = ba.textwidget( + parent=self._root_widget, + position=(x_inset + self._scroll_width + 50 + 30, v - 15), + size=(0, 0), + scale=1.0, + color=(0.7, 1.0, 0.7, 1.0), + maxwidth=self._width - self._scroll_width - 150 - x_inset * 2, + h_align='left', + v_align='center') + v -= 30 + + self._selected_description_text = ba.textwidget( + parent=self._root_widget, + position=(x_inset + self._scroll_width + 50 + 30, v), + size=(0, 0), + scale=0.7, + color=(0.5, 0.8, 0.5, 1.0), + maxwidth=self._width - self._scroll_width - 150 - x_inset * 2, + h_align='left') + + scroll_height = self._height - 100 + + v = self._height - 60 + + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + position=(x_inset + 61, + v - scroll_height), + size=(self._scroll_width, + scroll_height), + highlight=False) + ba.widget(edit=self._scrollwidget, + up_widget=self._back_button, + left_widget=self._back_button, + right_widget=select_button) + self._column: Optional[ba.Widget] = None + + v -= 35 + ba.containerwidget(edit=self._root_widget, + cancel_button=self._back_button, + start_button=select_button) + self._selected_game_type: Optional[Type[ba.GameActivity]] = None + + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + + self._refresh() + + def _refresh(self, select_get_more_games_button: bool = False) -> None: + + if self._column is not None: + self._column.delete() + + self._column = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0) + + gametypes = [ + gt for gt in ba.app.meta.get_game_types() if + gt.supports_session_type(self._editcontroller.get_session_type()) + ] + + # Sort in the current language. + gametypes.sort(key=lambda g: g.get_display_string().evaluate()) + + for i, gametype in enumerate(gametypes): + + def _doit() -> None: + if self._select_button: + ba.timer(0.1, + self._select_button.activate, + timetype=ba.TimeType.REAL) + + txt = ba.textwidget(parent=self._column, + position=(0, 0), + size=(self._width - 88, 24), + text=gametype.get_display_string(), + h_align='left', + v_align='center', + color=(0.8, 0.8, 0.8, 1.0), + maxwidth=self._scroll_width * 0.8, + on_select_call=ba.Call( + self._set_selected_game_type, gametype), + always_highlight=True, + selectable=True, + on_activate_call=_doit) + if i == 0: + ba.widget(edit=txt, up_widget=self._back_button) + + self._get_more_games_button = ba.buttonwidget( + parent=self._column, + autoselect=True, + label=ba.Lstr(resource=self._r + '.getMoreGamesText'), + color=(0.54, 0.52, 0.67), + textcolor=(0.7, 0.65, 0.7), + on_activate_call=self._on_get_more_games_press, + size=(178, 50)) + if select_get_more_games_button: + ba.containerwidget(edit=self._column, + selected_child=self._get_more_games_button, + visible_child=self._get_more_games_button) + + def _on_get_more_games_press(self) -> None: + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.store.browser import StoreBrowserWindow + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + StoreBrowserWindow(modal=True, + show_tab=StoreBrowserWindow.TabID.MINIGAMES, + on_close_call=self._on_store_close, + origin_widget=self._get_more_games_button) + + def _on_store_close(self) -> None: + self._refresh(select_get_more_games_button=True) + + def _add(self) -> None: + _ba.lock_all_input() # Make sure no more commands happen. + ba.timer(0.1, _ba.unlock_all_input, timetype=ba.TimeType.REAL) + assert self._selected_game_type is not None + self._editcontroller.add_game_type_selected(self._selected_game_type) + + def _set_selected_game_type(self, gametype: Type[ba.GameActivity]) -> None: + self._selected_game_type = gametype + ba.textwidget(edit=self._selected_title_text, + text=gametype.get_display_string()) + ba.textwidget(edit=self._selected_description_text, + text=gametype.get_description_display_string( + self._editcontroller.get_session_type())) + + def _back(self) -> None: + self._editcontroller.add_game_cancelled() diff --git a/dist/ba_data/python/bastd/ui/playlist/browser.py b/dist/ba_data/python/bastd/ui/playlist/browser.py new file mode 100644 index 0000000..0a3c7ef --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playlist/browser.py @@ -0,0 +1,646 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a window for browsing and launching game playlists.""" + +from __future__ import annotations + +import copy +import math +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Type, Optional, Tuple, Union + + +class PlaylistBrowserWindow(ba.Window): + """Window for starting teams games.""" + + def __init__(self, + sessiontype: Type[ba.Session], + transition: Optional[str] = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=cyclic-import + from bastd.ui.playlist import PlaylistTypeVars + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + # Store state for when we exit the next game. + if issubclass(sessiontype, ba.DualTeamSession): + ba.app.ui.set_main_menu_location('Team Game Select') + ba.set_analytics_screen('Teams Window') + elif issubclass(sessiontype, ba.FreeForAllSession): + ba.app.ui.set_main_menu_location('Free-for-All Game Select') + ba.set_analytics_screen('FreeForAll Window') + else: + raise TypeError(f'Invalid sessiontype: {sessiontype}.') + self._pvars = PlaylistTypeVars(sessiontype) + + self._sessiontype = sessiontype + + self._customize_button: Optional[ba.Widget] = None + self._sub_width: Optional[float] = None + self._sub_height: Optional[float] = None + + self._ensure_standard_playlists_exist() + + # Get the current selection (if any). + self._selected_playlist = ba.app.config.get(self._pvars.config_name + + ' Playlist Selection') + + uiscale = ba.app.ui.uiscale + self._width = 900 if uiscale is ba.UIScale.SMALL else 800 + x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + self._height = (480 if uiscale is ba.UIScale.SMALL else + 510 if uiscale is ba.UIScale.MEDIUM else 580) + + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + toolbar_visibility='menu_full', + scale_origin_stack_offset=scale_origin, + scale=(1.69 if uiscale is ba.UIScale.SMALL else + 1.05 if uiscale is ba.UIScale.MEDIUM else 0.9), + stack_offset=(0, -26) if uiscale is ba.UIScale.SMALL else (0, 0))) + + self._back_button: Optional[ba.Widget] = ba.buttonwidget( + parent=self._root_widget, + position=(59 + x_inset, self._height - 70), + size=(120, 60), + scale=1.0, + on_activate_call=self._on_back_press, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back') + ba.containerwidget(edit=self._root_widget, + cancel_button=self._back_button) + txt = self._title_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 41), + size=(0, 0), + text=self._pvars.window_title_name, + scale=1.3, + res_scale=1.5, + color=ba.app.ui.heading_color, + h_align='center', + v_align='center') + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + ba.textwidget(edit=txt, text='') + + ba.buttonwidget(edit=self._back_button, + button_type='backSmall', + size=(60, 54), + position=(59 + x_inset, self._height - 67), + label=ba.charstr(ba.SpecialChar.BACK)) + + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + self._back_button.delete() + self._back_button = None + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._on_back_press) + scroll_offs = 33 + else: + scroll_offs = 0 + self._scroll_width = self._width - (100 + 2 * x_inset) + self._scroll_height = (self._height - + (146 if uiscale is ba.UIScale.SMALL + and ba.app.ui.use_toolbars else 136)) + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + highlight=False, + size=(self._scroll_width, self._scroll_height), + position=((self._width - self._scroll_width) * 0.5, + 65 + scroll_offs)) + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + self._subcontainer: Optional[ba.Widget] = None + self._config_name_full = self._pvars.config_name + ' Playlists' + self._last_config = None + + # Update now and once per second. + # (this should do our initial refresh) + self._update() + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + + def _ensure_standard_playlists_exist(self) -> None: + # On new installations, go ahead and create a few playlists + # besides the hard-coded default one: + if not _ba.get_account_misc_val('madeStandardPlaylists', False): + _ba.add_transaction({ + 'type': + 'ADD_PLAYLIST', + 'playlistType': + 'Free-for-All', + 'playlistName': + ba.Lstr(resource='singleGamePlaylistNameText' + ).evaluate().replace( + '${GAME}', + ba.Lstr(translate=('gameNames', + 'Death Match')).evaluate()), + 'playlist': [ + { + 'type': 'bs_death_match.DeathMatchGame', + 'settings': { + 'Epic Mode': False, + 'Kills to Win Per Player': 10, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Doom Shroom' + } + }, + { + 'type': 'bs_death_match.DeathMatchGame', + 'settings': { + 'Epic Mode': False, + 'Kills to Win Per Player': 10, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'map': 'Crag Castle' + } + }, + ] + }) + _ba.add_transaction({ + 'type': + 'ADD_PLAYLIST', + 'playlistType': + 'Team Tournament', + 'playlistName': + ba.Lstr( + resource='singleGamePlaylistNameText' + ).evaluate().replace( + '${GAME}', + ba.Lstr(translate=('gameNames', + 'Capture the Flag')).evaluate()), + 'playlist': [ + { + 'type': 'bs_capture_the_flag.CTFGame', + 'settings': { + 'map': 'Bridgit', + 'Score to Win': 3, + 'Flag Idle Return Time': 30, + 'Flag Touch Return Time': 0, + 'Respawn Times': 1.0, + 'Time Limit': 600, + 'Epic Mode': False + } + }, + { + 'type': 'bs_capture_the_flag.CTFGame', + 'settings': { + 'map': 'Roundabout', + 'Score to Win': 2, + 'Flag Idle Return Time': 30, + 'Flag Touch Return Time': 0, + 'Respawn Times': 1.0, + 'Time Limit': 600, + 'Epic Mode': False + } + }, + { + 'type': 'bs_capture_the_flag.CTFGame', + 'settings': { + 'map': 'Tip Top', + 'Score to Win': 2, + 'Flag Idle Return Time': 30, + 'Flag Touch Return Time': 3, + 'Respawn Times': 1.0, + 'Time Limit': 300, + 'Epic Mode': False + } + }, + ] + }) + _ba.add_transaction({ + 'type': + 'ADD_PLAYLIST', + 'playlistType': + 'Team Tournament', + 'playlistName': + ba.Lstr(translate=('playlistNames', 'Just Sports') + ).evaluate(), + 'playlist': [ + { + 'type': 'bs_hockey.HockeyGame', + 'settings': { + 'Time Limit': 0, + 'map': 'Hockey Stadium', + 'Score to Win': 1, + 'Respawn Times': 1.0 + } + }, + { + 'type': 'bs_football.FootballTeamGame', + 'settings': { + 'Time Limit': 0, + 'map': 'Football Stadium', + 'Score to Win': 21, + 'Respawn Times': 1.0 + } + }, + ] + }) + _ba.add_transaction({ + 'type': + 'ADD_PLAYLIST', + 'playlistType': + 'Free-for-All', + 'playlistName': + ba.Lstr(translate=('playlistNames', 'Just Epic') + ).evaluate(), + 'playlist': [{ + 'type': 'bs_elimination.EliminationGame', + 'settings': { + 'Time Limit': 120, + 'map': 'Tip Top', + 'Respawn Times': 1.0, + 'Lives Per Player': 1, + 'Epic Mode': 1 + } + }] + }) + _ba.add_transaction({ + 'type': 'SET_MISC_VAL', + 'name': 'madeStandardPlaylists', + 'value': True + }) + _ba.run_transactions() + + def _refresh(self) -> None: + # FIXME: Should tidy this up. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-nested-blocks + from efro.util import asserttype + from ba.internal import get_map_class, filter_playlist + if not self._root_widget: + return + if self._subcontainer is not None: + self._save_state() + self._subcontainer.delete() + + # Make sure config exists. + if self._config_name_full not in ba.app.config: + ba.app.config[self._config_name_full] = {} + + items = list(ba.app.config[self._config_name_full].items()) + + # Make sure everything is unicode. + items = [(i[0].decode(), i[1]) if not isinstance(i[0], str) else i + for i in items] + + items.sort(key=lambda x2: asserttype(x2[0], str).lower()) + items = [['__default__', None]] + items # default is always first + + count = len(items) + columns = 3 + rows = int(math.ceil(float(count) / columns)) + button_width = 230 + button_height = 230 + button_buffer_h = -3 + button_buffer_v = 0 + + self._sub_width = self._scroll_width + self._sub_height = 40 + rows * (button_height + + 2 * button_buffer_v) + 90 + assert self._sub_width is not None + assert self._sub_height is not None + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + + children = self._subcontainer.get_children() + for child in children: + child.delete() + + ba.textwidget(parent=self._subcontainer, + text=ba.Lstr(resource='playlistsText'), + position=(40, self._sub_height - 26), + size=(0, 0), + scale=1.0, + maxwidth=400, + color=ba.app.ui.title_color, + h_align='left', + v_align='center') + + index = 0 + appconfig = ba.app.config + + model_opaque = ba.getmodel('level_select_button_opaque') + model_transparent = ba.getmodel('level_select_button_transparent') + mask_tex = ba.gettexture('mapPreviewMask') + + h_offs = 225 if count == 1 else 115 if count == 2 else 0 + h_offs_bottom = 0 + + uiscale = ba.app.ui.uiscale + for y in range(rows): + for x in range(columns): + name = items[index][0] + assert name is not None + pos = (x * (button_width + 2 * button_buffer_h) + + button_buffer_h + 8 + h_offs, self._sub_height - 47 - + (y + 1) * (button_height + 2 * button_buffer_v)) + btn = ba.buttonwidget(parent=self._subcontainer, + button_type='square', + size=(button_width, button_height), + autoselect=True, + label='', + position=pos) + + if (x == 0 and ba.app.ui.use_toolbars + and uiscale is ba.UIScale.SMALL): + ba.widget( + edit=btn, + left_widget=_ba.get_special_widget('back_button')) + if (x == columns - 1 and ba.app.ui.use_toolbars + and uiscale is ba.UIScale.SMALL): + ba.widget( + edit=btn, + right_widget=_ba.get_special_widget('party_button')) + ba.buttonwidget( + edit=btn, + on_activate_call=ba.Call(self._on_playlist_press, btn, + name), + on_select_call=ba.Call(self._on_playlist_select, name)) + ba.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50) + + if self._selected_playlist == name: + ba.containerwidget(edit=self._subcontainer, + selected_child=btn, + visible_child=btn) + + if self._back_button is not None: + if y == 0: + ba.widget(edit=btn, up_widget=self._back_button) + if x == 0: + ba.widget(edit=btn, left_widget=self._back_button) + + print_name: Optional[Union[str, ba.Lstr]] + if name == '__default__': + print_name = self._pvars.default_list_name + else: + print_name = name + ba.textwidget(parent=self._subcontainer, + text=print_name, + position=(pos[0] + button_width * 0.5, + pos[1] + button_height * 0.79), + size=(0, 0), + scale=button_width * 0.003, + maxwidth=button_width * 0.7, + draw_controller=btn, + h_align='center', + v_align='center') + + # Poke into this playlist and see if we can display some of + # its maps. + map_images = [] + try: + map_textures = [] + map_texture_entries = [] + if name == '__default__': + playlist = self._pvars.get_default_list_call() + else: + if name not in appconfig[self._pvars.config_name + + ' Playlists']: + print( + 'NOT FOUND ERR', + appconfig[self._pvars.config_name + + ' Playlists']) + playlist = appconfig[self._pvars.config_name + + ' Playlists'][name] + playlist = filter_playlist(playlist, + self._sessiontype, + remove_unowned=False, + mark_unowned=True) + for entry in playlist: + mapname = entry['settings']['map'] + maptype: Optional[Type[ba.Map]] + try: + maptype = get_map_class(mapname) + except ba.NotFoundError: + maptype = None + if maptype is not None: + tex_name = maptype.get_preview_texture_name() + if tex_name is not None: + map_textures.append(tex_name) + map_texture_entries.append(entry) + if len(map_textures) >= 6: + break + + if len(map_textures) > 4: + img_rows = 3 + img_columns = 2 + scl = 0.33 + h_offs_img = 30 + v_offs_img = 126 + elif len(map_textures) > 2: + img_rows = 2 + img_columns = 2 + scl = 0.35 + h_offs_img = 24 + v_offs_img = 110 + elif len(map_textures) > 1: + img_rows = 2 + img_columns = 1 + scl = 0.5 + h_offs_img = 47 + v_offs_img = 105 + else: + img_rows = 1 + img_columns = 1 + scl = 0.75 + h_offs_img = 20 + v_offs_img = 65 + + v = None + for row in range(img_rows): + for col in range(img_columns): + tex_index = row * img_columns + col + if tex_index < len(map_textures): + entry = map_texture_entries[tex_index] + + owned = not (('is_unowned_map' in entry + and entry['is_unowned_map']) or + ('is_unowned_game' in entry + and entry['is_unowned_game'])) + + tex_name = map_textures[tex_index] + h = pos[0] + h_offs_img + scl * 250 * col + v = pos[1] + v_offs_img - scl * 130 * row + map_images.append( + ba.imagewidget( + parent=self._subcontainer, + size=(scl * 250.0, scl * 125.0), + position=(h, v), + texture=ba.gettexture(tex_name), + opacity=1.0 if owned else 0.25, + draw_controller=btn, + model_opaque=model_opaque, + model_transparent=model_transparent, + mask_texture=mask_tex)) + if not owned: + ba.imagewidget( + parent=self._subcontainer, + size=(scl * 100.0, scl * 100.0), + position=(h + scl * 75, v + scl * 10), + texture=ba.gettexture('lock'), + draw_controller=btn) + if v is not None: + v -= scl * 130.0 + + except Exception: + ba.print_exception('Error listing playlist maps.') + + if not map_images: + ba.textwidget(parent=self._subcontainer, + text='???', + scale=1.5, + size=(0, 0), + color=(1, 1, 1, 0.5), + h_align='center', + v_align='center', + draw_controller=btn, + position=(pos[0] + button_width * 0.5, + pos[1] + button_height * 0.5)) + + index += 1 + + if index >= count: + break + if index >= count: + break + self._customize_button = btn = ba.buttonwidget( + parent=self._subcontainer, + size=(100, 30), + position=(34 + h_offs_bottom, 50), + text_scale=0.6, + label=ba.Lstr(resource='customizeText'), + on_activate_call=self._on_customize_press, + color=(0.54, 0.52, 0.67), + textcolor=(0.7, 0.65, 0.7), + autoselect=True) + ba.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28) + self._restore_state() + + def on_play_options_window_run_game(self) -> None: + """(internal)""" + if not self._root_widget: + return + ba.containerwidget(edit=self._root_widget, transition='out_left') + + def _on_playlist_select(self, playlist_name: str) -> None: + self._selected_playlist = playlist_name + + def _update(self) -> None: + + # make sure config exists + if self._config_name_full not in ba.app.config: + ba.app.config[self._config_name_full] = {} + + cfg = ba.app.config[self._config_name_full] + if cfg != self._last_config: + self._last_config = copy.deepcopy(cfg) + self._refresh() + + def _on_playlist_press(self, button: ba.Widget, + playlist_name: str) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playoptions import PlayOptionsWindow + + # Make sure the target playlist still exists. + exists = (playlist_name == '__default__' + or playlist_name in ba.app.config.get( + self._config_name_full, {})) + if not exists: + return + + self._save_state() + PlayOptionsWindow(sessiontype=self._sessiontype, + scale_origin=button.get_screen_space_center(), + playlist=playlist_name, + delegate=self) + + def _on_customize_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playlist.customizebrowser import ( + PlaylistCustomizeBrowserWindow) + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + PlaylistCustomizeBrowserWindow( + origin_widget=self._customize_button, + sessiontype=self._sessiontype).get_root_widget()) + + def _on_back_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.play import PlayWindow + + # Store our selected playlist if that's changed. + if self._selected_playlist is not None: + prev_sel = ba.app.config.get(self._pvars.config_name + + ' Playlist Selection') + if self._selected_playlist != prev_sel: + cfg = ba.app.config + cfg[self._pvars.config_name + + ' Playlist Selection'] = self._selected_playlist + cfg.commit() + + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + PlayWindow(transition='in_left').get_root_widget()) + + def _save_state(self) -> None: + try: + sel = self._root_widget.get_selected_child() + if sel == self._back_button: + sel_name = 'Back' + elif sel == self._scrollwidget: + assert self._subcontainer is not None + subsel = self._subcontainer.get_selected_child() + if subsel == self._customize_button: + sel_name = 'Customize' + else: + sel_name = 'Scroll' + else: + raise Exception('unrecognized selected widget') + ba.app.ui.window_states[type(self)] = sel_name + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + def _restore_state(self) -> None: + try: + sel_name = ba.app.ui.window_states.get(type(self)) + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'Scroll': + sel = self._scrollwidget + elif sel_name == 'Customize': + sel = self._scrollwidget + ba.containerwidget(edit=self._subcontainer, + selected_child=self._customize_button, + visible_child=self._customize_button) + else: + sel = self._scrollwidget + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') diff --git a/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py b/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py new file mode 100644 index 0000000..4ee5b29 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py @@ -0,0 +1,594 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for viewing/creating/editing playlists.""" + +from __future__ import annotations + +import copy +import time +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Type, Optional, Tuple, List, Dict + + +class PlaylistCustomizeBrowserWindow(ba.Window): + """Window for viewing a playlist.""" + + def __init__(self, + sessiontype: Type[ba.Session], + transition: str = 'in_right', + select_playlist: str = None, + origin_widget: ba.Widget = None): + # Yes this needs tidying. + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + # pylint: disable=cyclic-import + from bastd.ui import playlist + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + self._sessiontype = sessiontype + self._pvars = playlist.PlaylistTypeVars(sessiontype) + self._max_playlists = 30 + self._r = 'gameListWindow' + uiscale = ba.app.ui.uiscale + self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0 + x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 + self._height = (380.0 if uiscale is ba.UIScale.SMALL else + 420.0 if uiscale is ba.UIScale.MEDIUM else 500.0) + top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0 + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + scale_origin_stack_offset=scale_origin, + scale=(2.05 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else (0, 0))) + + self._back_button = back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(43 + x_inset, self._height - 60), + size=(160, 68), + scale=0.77, + autoselect=True, + text_scale=1.3, + label=ba.Lstr(resource='backText'), + button_type='back') + + ba.textwidget(parent=self._root_widget, + position=(0, self._height - 47), + size=(self._width, 25), + text=ba.Lstr(resource=self._r + '.titleText', + subs=[('${TYPE}', + self._pvars.window_title_name)]), + color=ba.app.ui.heading_color, + maxwidth=290, + h_align='center', + v_align='center') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + v = self._height - 59.0 + h = 41 + x_inset + b_color = (0.6, 0.53, 0.63) + b_textcolor = (0.75, 0.7, 0.8) + self._lock_images: List[ba.Widget] = [] + lock_tex = ba.gettexture('lock') + + scl = (1.1 if uiscale is ba.UIScale.SMALL else + 1.27 if uiscale is ba.UIScale.MEDIUM else 1.57) + scl *= 0.63 + v -= 65.0 * scl + new_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(90, 58.0 * scl), + on_activate_call=self._new_playlist, + color=b_color, + autoselect=True, + button_type='square', + textcolor=b_textcolor, + text_scale=0.7, + label=ba.Lstr(resource='newText', + fallback_resource=self._r + '.newText')) + self._lock_images.append( + ba.imagewidget(parent=self._root_widget, + size=(30, 30), + draw_controller=btn, + position=(h - 10, v + 58.0 * scl - 28), + texture=lock_tex)) + + v -= 65.0 * scl + self._edit_button = edit_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(90, 58.0 * scl), + on_activate_call=self._edit_playlist, + color=b_color, + autoselect=True, + textcolor=b_textcolor, + button_type='square', + text_scale=0.7, + label=ba.Lstr(resource='editText', + fallback_resource=self._r + '.editText')) + self._lock_images.append( + ba.imagewidget(parent=self._root_widget, + size=(30, 30), + draw_controller=btn, + position=(h - 10, v + 58.0 * scl - 28), + texture=lock_tex)) + + v -= 65.0 * scl + duplicate_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(90, 58.0 * scl), + on_activate_call=self._duplicate_playlist, + color=b_color, + autoselect=True, + textcolor=b_textcolor, + button_type='square', + text_scale=0.7, + label=ba.Lstr(resource='duplicateText', + fallback_resource=self._r + '.duplicateText')) + self._lock_images.append( + ba.imagewidget(parent=self._root_widget, + size=(30, 30), + draw_controller=btn, + position=(h - 10, v + 58.0 * scl - 28), + texture=lock_tex)) + + v -= 65.0 * scl + delete_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(90, 58.0 * scl), + on_activate_call=self._delete_playlist, + color=b_color, + autoselect=True, + textcolor=b_textcolor, + button_type='square', + text_scale=0.7, + label=ba.Lstr(resource='deleteText', + fallback_resource=self._r + '.deleteText')) + self._lock_images.append( + ba.imagewidget(parent=self._root_widget, + size=(30, 30), + draw_controller=btn, + position=(h - 10, v + 58.0 * scl - 28), + texture=lock_tex)) + v -= 65.0 * scl + self._import_button = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(90, 58.0 * scl), + on_activate_call=self._import_playlist, + color=b_color, + autoselect=True, + textcolor=b_textcolor, + button_type='square', + text_scale=0.7, + label=ba.Lstr(resource='importText')) + v -= 65.0 * scl + btn = ba.buttonwidget(parent=self._root_widget, + position=(h, v), + size=(90, 58.0 * scl), + on_activate_call=self._share_playlist, + color=b_color, + autoselect=True, + textcolor=b_textcolor, + button_type='square', + text_scale=0.7, + label=ba.Lstr(resource='shareText')) + self._lock_images.append( + ba.imagewidget(parent=self._root_widget, + size=(30, 30), + draw_controller=btn, + position=(h - 10, v + 58.0 * scl - 28), + texture=lock_tex)) + + v = self._height - 75 + self._scroll_height = self._height - 119 + scrollwidget = ba.scrollwidget(parent=self._root_widget, + position=(140 + x_inset, + v - self._scroll_height), + size=(self._width - (180 + 2 * x_inset), + self._scroll_height + 10), + highlight=False) + ba.widget(edit=back_button, right_widget=scrollwidget) + self._columnwidget = ba.columnwidget(parent=scrollwidget, + border=2, + margin=0) + + h = 145 + + self._do_randomize_val = ba.app.config.get( + self._pvars.config_name + ' Playlist Randomize', 0) + + h += 210 + + for btn in [new_button, delete_button, edit_button, duplicate_button]: + ba.widget(edit=btn, right_widget=scrollwidget) + ba.widget(edit=scrollwidget, + left_widget=new_button, + right_widget=_ba.get_special_widget('party_button') + if ba.app.ui.use_toolbars else None) + + # make sure config exists + self._config_name_full = self._pvars.config_name + ' Playlists' + + if self._config_name_full not in ba.app.config: + ba.app.config[self._config_name_full] = {} + + self._selected_playlist_name: Optional[str] = None + self._selected_playlist_index: Optional[int] = None + self._playlist_widgets: List[ba.Widget] = [] + + self._refresh(select_playlist=select_playlist) + + ba.buttonwidget(edit=back_button, on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=back_button) + + ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget) + + # Keep our lock images up to date/etc. + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + self._update() + + def _update(self) -> None: + have = ba.app.accounts.have_pro_options() + for lock in self._lock_images: + ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0) + + def _back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playlist import browser + if self._selected_playlist_name is not None: + cfg = ba.app.config + cfg[self._pvars.config_name + + ' Playlist Selection'] = self._selected_playlist_name + cfg.commit() + + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + browser.PlaylistBrowserWindow( + transition='in_left', + sessiontype=self._sessiontype).get_root_widget()) + + def _select(self, name: str, index: int) -> None: + self._selected_playlist_name = name + self._selected_playlist_index = index + + def _run_selected_playlist(self) -> None: + # pylint: disable=cyclic-import + _ba.unlock_all_input() + try: + _ba.new_host_session(self._sessiontype) + except Exception: + from bastd import mainmenu + ba.print_exception(f'Error running session {self._sessiontype}.') + + # Drop back into a main menu session. + _ba.new_host_session(mainmenu.MainMenuSession) + + def _choose_playlist(self) -> None: + if self._selected_playlist_name is None: + return + self._save_playlist_selection() + ba.containerwidget(edit=self._root_widget, transition='out_left') + _ba.fade_screen(False, endcall=self._run_selected_playlist) + _ba.lock_all_input() + + def _refresh(self, select_playlist: str = None) -> None: + from efro.util import asserttype + old_selection = self._selected_playlist_name + + # If there was no prev selection, look in prefs. + if old_selection is None: + old_selection = ba.app.config.get(self._pvars.config_name + + ' Playlist Selection') + + old_selection_index = self._selected_playlist_index + + # Delete old. + while self._playlist_widgets: + self._playlist_widgets.pop().delete() + + items = list(ba.app.config[self._config_name_full].items()) + + # Make sure everything is unicode now. + items = [(i[0].decode(), i[1]) if not isinstance(i[0], str) else i + for i in items] + + items.sort(key=lambda x: asserttype(x[0], str).lower()) + + items = [['__default__', None]] + items # Default is always first. + index = 0 + for pname, _ in items: + assert pname is not None + txtw = ba.textwidget( + parent=self._columnwidget, + size=(self._width - 40, 30), + maxwidth=self._width - 110, + text=self._get_playlist_display_name(pname), + h_align='left', + v_align='center', + color=(0.6, 0.6, 0.7, 1.0) if pname == '__default__' else + (0.85, 0.85, 0.85, 1), + always_highlight=True, + on_select_call=ba.Call(self._select, pname, index), + on_activate_call=ba.Call(self._edit_button.activate), + selectable=True) + ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) + + # Hitting up from top widget should jump to 'back' + if index == 0: + ba.widget(edit=txtw, up_widget=self._back_button) + + self._playlist_widgets.append(txtw) + + # Select this one if the user requested it. + if select_playlist is not None: + if pname == select_playlist: + ba.columnwidget(edit=self._columnwidget, + selected_child=txtw, + visible_child=txtw) + else: + # Select this one if it was previously selected. + # Go by index if there's one. + if old_selection_index is not None: + if index == old_selection_index: + ba.columnwidget(edit=self._columnwidget, + selected_child=txtw, + visible_child=txtw) + else: # Otherwise look by name. + if pname == old_selection: + ba.columnwidget(edit=self._columnwidget, + selected_child=txtw, + visible_child=txtw) + + index += 1 + + def _save_playlist_selection(self) -> None: + # Store the selected playlist in prefs. + # This serves dual purposes of letting us re-select it next time + # if we want and also lets us pass it to the game (since we reset + # the whole python environment that's not actually easy). + cfg = ba.app.config + cfg[self._pvars.config_name + + ' Playlist Selection'] = self._selected_playlist_name + cfg[self._pvars.config_name + + ' Playlist Randomize'] = self._do_randomize_val + cfg.commit() + + def _new_playlist(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playlist.editcontroller import PlaylistEditController + from bastd.ui.purchase import PurchaseWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + + # Clamp at our max playlist number. + if len(ba.app.config[self._config_name_full]) > self._max_playlists: + ba.screenmessage( + ba.Lstr(translate=('serverResponses', + 'Max number of playlists reached.')), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + # In case they cancel so we can return to this state. + self._save_playlist_selection() + + # Kick off the edit UI. + PlaylistEditController(sessiontype=self._sessiontype) + ba.containerwidget(edit=self._root_widget, transition='out_left') + + def _edit_playlist(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playlist.editcontroller import PlaylistEditController + from bastd.ui.purchase import PurchaseWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + if self._selected_playlist_name is None: + return + if self._selected_playlist_name == '__default__': + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource=self._r + + '.cantEditDefaultText')) + return + self._save_playlist_selection() + PlaylistEditController( + existing_playlist_name=self._selected_playlist_name, + sessiontype=self._sessiontype) + ba.containerwidget(edit=self._root_widget, transition='out_left') + + def _do_delete_playlist(self) -> None: + _ba.add_transaction({ + 'type': 'REMOVE_PLAYLIST', + 'playlistType': self._pvars.config_name, + 'playlistName': self._selected_playlist_name + }) + _ba.run_transactions() + ba.playsound(ba.getsound('shieldDown')) + + # (we don't use len()-1 here because the default list adds one) + assert self._selected_playlist_index is not None + if self._selected_playlist_index > len( + ba.app.config[self._pvars.config_name + ' Playlists']): + self._selected_playlist_index = len( + ba.app.config[self._pvars.config_name + ' Playlists']) + self._refresh() + + def _import_playlist(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playlist import share + + # Gotta be signed in for this to work. + if _ba.get_account_state() != 'signed_in': + ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + share.SharePlaylistImportWindow(origin_widget=self._import_button, + on_success_callback=ba.WeakCall( + self._on_playlist_import_success)) + + def _on_playlist_import_success(self) -> None: + self._refresh() + + def _on_share_playlist_response(self, name: str, response: Any) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playlist import share + if response is None: + ba.screenmessage( + ba.Lstr(resource='internal.unavailableNoConnectionText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + share.SharePlaylistResultsWindow(name, response) + + def _share_playlist(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.purchase import PurchaseWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + + # Gotta be signed in for this to work. + if _ba.get_account_state() != 'signed_in': + ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + if self._selected_playlist_name == '__default__': + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource=self._r + + '.cantShareDefaultText'), + color=(1, 0, 0)) + return + + if self._selected_playlist_name is None: + return + + _ba.add_transaction( + { + 'type': 'SHARE_PLAYLIST', + 'expire_time': time.time() + 5, + 'playlistType': self._pvars.config_name, + 'playlistName': self._selected_playlist_name + }, + callback=ba.WeakCall(self._on_share_playlist_response, + self._selected_playlist_name)) + _ba.run_transactions() + ba.screenmessage(ba.Lstr(resource='sharingText')) + + def _delete_playlist(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.purchase import PurchaseWindow + from bastd.ui.confirm import ConfirmWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + + if self._selected_playlist_name is None: + return + if self._selected_playlist_name == '__default__': + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource=self._r + '.cantDeleteDefaultText')) + else: + ConfirmWindow( + ba.Lstr(resource=self._r + '.deleteConfirmText', + subs=[('${LIST}', self._selected_playlist_name)]), + self._do_delete_playlist, 450, 150) + + def _get_playlist_display_name(self, playlist: str) -> ba.Lstr: + if playlist == '__default__': + return self._pvars.default_list_name + return playlist if isinstance(playlist, ba.Lstr) else ba.Lstr( + value=playlist) + + def _duplicate_playlist(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=cyclic-import + from bastd.ui.purchase import PurchaseWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + if self._selected_playlist_name is None: + return + plst: Optional[List[Dict[str, Any]]] + if self._selected_playlist_name == '__default__': + plst = self._pvars.get_default_list_call() + else: + plst = ba.app.config[self._config_name_full].get( + self._selected_playlist_name) + if plst is None: + ba.playsound(ba.getsound('error')) + return + + # clamp at our max playlist number + if len(ba.app.config[self._config_name_full]) > self._max_playlists: + ba.screenmessage( + ba.Lstr(translate=('serverResponses', + 'Max number of playlists reached.')), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + copy_text = ba.Lstr(resource='copyOfText').evaluate() + # get just 'Copy' or whatnot + copy_word = copy_text.replace('${NAME}', '').strip() + # find a valid dup name that doesn't exist + + test_index = 1 + base_name = self._get_playlist_display_name( + self._selected_playlist_name).evaluate() + + # If it looks like a copy, strip digits and spaces off the end. + if copy_word in base_name: + while base_name[-1].isdigit() or base_name[-1] == ' ': + base_name = base_name[:-1] + while True: + if copy_word in base_name: + test_name = base_name + else: + test_name = copy_text.replace('${NAME}', base_name) + if test_index > 1: + test_name += ' ' + str(test_index) + if test_name not in ba.app.config[self._config_name_full]: + break + test_index += 1 + + _ba.add_transaction({ + 'type': 'ADD_PLAYLIST', + 'playlistType': self._pvars.config_name, + 'playlistName': test_name, + 'playlist': copy.deepcopy(plst) + }) + _ba.run_transactions() + + ba.playsound(ba.getsound('gunCocking')) + self._refresh(select_playlist=test_name) diff --git a/dist/ba_data/python/bastd/ui/playlist/edit.py b/dist/ba_data/python/bastd/ui/playlist/edit.py new file mode 100644 index 0000000..cb5388a --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playlist/edit.py @@ -0,0 +1,396 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a window for editing individual game playlists.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +import ba +import _ba + +if TYPE_CHECKING: + from typing import Optional, List + from bastd.ui.playlist.editcontroller import PlaylistEditController + + +class PlaylistEditWindow(ba.Window): + """Window for editing an individual game playlist.""" + + def __init__(self, + editcontroller: PlaylistEditController, + transition: str = 'in_right'): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + prev_selection: Optional[str] + self._editcontroller = editcontroller + self._r = 'editGameListWindow' + prev_selection = self._editcontroller.get_edit_ui_selection() + + uiscale = ba.app.ui.uiscale + self._width = 770 if uiscale is ba.UIScale.SMALL else 670 + x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + self._height = (400 if uiscale is ba.UIScale.SMALL else + 470 if uiscale is ba.UIScale.MEDIUM else 540) + + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + scale=(2.0 if uiscale is ba.UIScale.SMALL else + 1.3 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -16) if uiscale is ba.UIScale.SMALL else (0, 0))) + cancel_button = ba.buttonwidget(parent=self._root_widget, + position=(35 + x_inset, + self._height - 60), + scale=0.8, + size=(175, 60), + autoselect=True, + label=ba.Lstr(resource='cancelText'), + text_scale=1.2) + save_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - (195 + x_inset), self._height - 60), + scale=0.8, + size=(190, 60), + autoselect=True, + left_widget=cancel_button, + label=ba.Lstr(resource='saveText'), + text_scale=1.2) + + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + + ba.widget(edit=cancel_button, + left_widget=cancel_button, + right_widget=save_button) + + ba.textwidget(parent=self._root_widget, + position=(-10, self._height - 50), + size=(self._width, 25), + text=ba.Lstr(resource=self._r + '.titleText'), + color=ba.app.ui.title_color, + scale=1.05, + h_align='center', + v_align='center', + maxwidth=270) + + v = self._height - 115.0 + + self._scroll_width = self._width - (205 + 2 * x_inset) + + ba.textwidget(parent=self._root_widget, + text=ba.Lstr(resource=self._r + '.listNameText'), + position=(196 + x_inset, v + 31), + maxwidth=150, + color=(0.8, 0.8, 0.8, 0.5), + size=(0, 0), + scale=0.75, + h_align='right', + v_align='center') + + self._text_field = ba.textwidget( + parent=self._root_widget, + position=(210 + x_inset, v + 7), + size=(self._scroll_width - 53, 43), + text=self._editcontroller.getname(), + h_align='left', + v_align='center', + max_chars=40, + autoselect=True, + color=(0.9, 0.9, 0.9, 1.0), + description=ba.Lstr(resource=self._r + '.listNameText'), + editable=True, + padding=4, + on_return_press_call=self._save_press_with_sound) + ba.widget(edit=cancel_button, down_widget=self._text_field) + + self._list_widgets: List[ba.Widget] = [] + + h = 40 + x_inset + v = self._height - 172.0 + + b_color = (0.6, 0.53, 0.63) + b_textcolor = (0.75, 0.7, 0.8) + + v -= 2.0 + v += 63 + + scl = (1.03 if uiscale is ba.UIScale.SMALL else + 1.36 if uiscale is ba.UIScale.MEDIUM else 1.74) + v -= 63.0 * scl + + add_game_button = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, 61.0 * scl), + on_activate_call=self._add, + on_select_call=ba.Call(self._set_ui_selection, 'add_button'), + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label=ba.Lstr(resource=self._r + '.addGameText')) + ba.widget(edit=add_game_button, up_widget=self._text_field) + v -= 63.0 * scl + + self._edit_button = edit_game_button = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(110, 61.0 * scl), + on_activate_call=self._edit, + on_select_call=ba.Call(self._set_ui_selection, 'editButton'), + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + text_scale=0.8, + label=ba.Lstr(resource=self._r + '.editGameText')) + v -= 63.0 * scl + + remove_game_button = ba.buttonwidget(parent=self._root_widget, + position=(h, v), + size=(110, 61.0 * scl), + text_scale=0.8, + on_activate_call=self._remove, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + label=ba.Lstr(resource=self._r + + '.removeGameText')) + v -= 40 + h += 9 + ba.buttonwidget(parent=self._root_widget, + position=(h, v), + size=(42, 35), + on_activate_call=self._move_up, + label=ba.charstr(ba.SpecialChar.UP_ARROW), + button_type='square', + color=b_color, + textcolor=b_textcolor, + autoselect=True, + repeat=True) + h += 52 + ba.buttonwidget(parent=self._root_widget, + position=(h, v), + size=(42, 35), + on_activate_call=self._move_down, + autoselect=True, + button_type='square', + color=b_color, + textcolor=b_textcolor, + label=ba.charstr(ba.SpecialChar.DOWN_ARROW), + repeat=True) + + v = self._height - 100 + scroll_height = self._height - 155 + scrollwidget = ba.scrollwidget( + parent=self._root_widget, + position=(160 + x_inset, v - scroll_height), + highlight=False, + on_select_call=ba.Call(self._set_ui_selection, 'gameList'), + size=(self._scroll_width, (scroll_height - 15))) + ba.widget(edit=scrollwidget, + left_widget=add_game_button, + right_widget=scrollwidget) + self._columnwidget = ba.columnwidget(parent=scrollwidget, + border=2, + margin=0) + ba.widget(edit=self._columnwidget, up_widget=self._text_field) + + for button in [add_game_button, edit_game_button, remove_game_button]: + ba.widget(edit=button, + left_widget=button, + right_widget=scrollwidget) + + self._refresh() + + ba.buttonwidget(edit=cancel_button, on_activate_call=self._cancel) + ba.containerwidget(edit=self._root_widget, + cancel_button=cancel_button, + selected_child=scrollwidget) + + ba.buttonwidget(edit=save_button, on_activate_call=self._save_press) + ba.containerwidget(edit=self._root_widget, start_button=save_button) + + if prev_selection == 'add_button': + ba.containerwidget(edit=self._root_widget, + selected_child=add_game_button) + elif prev_selection == 'editButton': + ba.containerwidget(edit=self._root_widget, + selected_child=edit_game_button) + elif prev_selection == 'gameList': + ba.containerwidget(edit=self._root_widget, + selected_child=scrollwidget) + + def _set_ui_selection(self, selection: str) -> None: + self._editcontroller.set_edit_ui_selection(selection) + + def _cancel(self) -> None: + from bastd.ui.playlist.customizebrowser import ( + PlaylistCustomizeBrowserWindow) + ba.playsound(ba.getsound('powerdown01')) + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + PlaylistCustomizeBrowserWindow( + transition='in_left', + sessiontype=self._editcontroller.get_session_type(), + select_playlist=self._editcontroller. + get_existing_playlist_name()).get_root_widget()) + + def _add(self) -> None: + # Store list name then tell the session to perform an add. + self._editcontroller.setname( + cast(str, ba.textwidget(query=self._text_field))) + self._editcontroller.add_game_pressed() + + def _edit(self) -> None: + # Store list name then tell the session to perform an add. + self._editcontroller.setname( + cast(str, ba.textwidget(query=self._text_field))) + self._editcontroller.edit_game_pressed() + + def _save_press(self) -> None: + from bastd.ui.playlist.customizebrowser import ( + PlaylistCustomizeBrowserWindow) + new_name = cast(str, ba.textwidget(query=self._text_field)) + if (new_name != self._editcontroller.get_existing_playlist_name() + and new_name + in ba.app.config[self._editcontroller.get_config_name() + + ' Playlists']): + ba.screenmessage( + ba.Lstr(resource=self._r + '.cantSaveAlreadyExistsText')) + ba.playsound(ba.getsound('error')) + return + if not new_name: + ba.playsound(ba.getsound('error')) + return + if not self._editcontroller.get_playlist(): + ba.screenmessage( + ba.Lstr(resource=self._r + '.cantSaveEmptyListText')) + ba.playsound(ba.getsound('error')) + return + + # We couldn't actually replace the default list anyway, but disallow + # using its exact name to avoid confusion. + if new_name == self._editcontroller.get_default_list_name().evaluate(): + ba.screenmessage( + ba.Lstr(resource=self._r + '.cantOverwriteDefaultText')) + ba.playsound(ba.getsound('error')) + return + + # If we had an old one, delete it. + if self._editcontroller.get_existing_playlist_name() is not None: + _ba.add_transaction({ + 'type': + 'REMOVE_PLAYLIST', + 'playlistType': + self._editcontroller.get_config_name(), + 'playlistName': + self._editcontroller.get_existing_playlist_name() + }) + + _ba.add_transaction({ + 'type': 'ADD_PLAYLIST', + 'playlistType': self._editcontroller.get_config_name(), + 'playlistName': new_name, + 'playlist': self._editcontroller.get_playlist() + }) + _ba.run_transactions() + + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.playsound(ba.getsound('gunCocking')) + ba.app.ui.set_main_menu_window( + PlaylistCustomizeBrowserWindow( + transition='in_left', + sessiontype=self._editcontroller.get_session_type(), + select_playlist=new_name).get_root_widget()) + + def _save_press_with_sound(self) -> None: + ba.playsound(ba.getsound('swish')) + self._save_press() + + def _select(self, index: int) -> None: + self._editcontroller.set_selected_index(index) + + def _refresh(self) -> None: + from ba.internal import getclass + + # Need to grab this here as rebuilding the list will + # change it otherwise. + old_selection_index = self._editcontroller.get_selected_index() + + while self._list_widgets: + self._list_widgets.pop().delete() + for index, pentry in enumerate(self._editcontroller.get_playlist()): + + try: + cls = getclass(pentry['type'], subclassof=ba.GameActivity) + desc = cls.get_settings_display_string(pentry) + except Exception: + ba.print_exception() + desc = "(invalid: '" + pentry['type'] + "')" + + txtw = ba.textwidget(parent=self._columnwidget, + size=(self._width - 80, 30), + on_select_call=ba.Call(self._select, index), + always_highlight=True, + color=(0.8, 0.8, 0.8, 1.0), + padding=0, + maxwidth=self._scroll_width * 0.93, + text=desc, + on_activate_call=self._edit_button.activate, + v_align='center', + selectable=True) + ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) + + # Wanna be able to jump up to the text field from the top one. + if index == 0: + ba.widget(edit=txtw, up_widget=self._text_field) + self._list_widgets.append(txtw) + if old_selection_index == index: + ba.columnwidget(edit=self._columnwidget, + selected_child=txtw, + visible_child=txtw) + + def _move_down(self) -> None: + playlist = self._editcontroller.get_playlist() + index = self._editcontroller.get_selected_index() + if index >= len(playlist) - 1: + return + tmp = playlist[index] + playlist[index] = playlist[index + 1] + playlist[index + 1] = tmp + index += 1 + self._editcontroller.set_playlist(playlist) + self._editcontroller.set_selected_index(index) + self._refresh() + + def _move_up(self) -> None: + playlist = self._editcontroller.get_playlist() + index = self._editcontroller.get_selected_index() + if index < 1: + return + tmp = playlist[index] + playlist[index] = (playlist[index - 1]) + playlist[index - 1] = tmp + index -= 1 + self._editcontroller.set_playlist(playlist) + self._editcontroller.set_selected_index(index) + self._refresh() + + def _remove(self) -> None: + playlist = self._editcontroller.get_playlist() + index = self._editcontroller.get_selected_index() + if not playlist: + return + del playlist[index] + if index >= len(playlist): + index = len(playlist) - 1 + self._editcontroller.set_playlist(playlist) + self._editcontroller.set_selected_index(index) + ba.playsound(ba.getsound('shieldDown')) + self._refresh() diff --git a/dist/ba_data/python/bastd/ui/playlist/editcontroller.py b/dist/ba_data/python/bastd/ui/playlist/editcontroller.py new file mode 100644 index 0000000..5c37c53 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playlist/editcontroller.py @@ -0,0 +1,208 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines a controller for wrangling playlist edit UIs.""" + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Optional + + +class PlaylistEditController: + """Coordinates various UIs involved in playlist editing.""" + + def __init__(self, + sessiontype: Type[ba.Session], + existing_playlist_name: str = None, + transition: str = 'in_right', + playlist: List[Dict[str, Any]] = None, + playlist_name: str = None): + from ba.internal import preload_map_preview_media, filter_playlist + from bastd.ui.playlist import PlaylistTypeVars + from bastd.ui.playlist.edit import PlaylistEditWindow + + appconfig = ba.app.config + + # Since we may be showing our map list momentarily, + # lets go ahead and preload all map preview textures. + preload_map_preview_media() + self._sessiontype = sessiontype + + self._editing_game = False + self._editing_game_type: Optional[Type[ba.GameActivity]] = None + self._pvars = PlaylistTypeVars(sessiontype) + self._existing_playlist_name = existing_playlist_name + self._config_name_full = self._pvars.config_name + ' Playlists' + + # Make sure config exists. + if self._config_name_full not in appconfig: + appconfig[self._config_name_full] = {} + + self._selected_index = 0 + if existing_playlist_name: + self._name = existing_playlist_name + + # Filter out invalid games. + self._playlist = filter_playlist( + appconfig[self._pvars.config_name + + ' Playlists'][existing_playlist_name], + sessiontype=sessiontype, + remove_unowned=False) + self._edit_ui_selection = None + else: + if playlist is not None: + self._playlist = playlist + else: + self._playlist = [] + if playlist_name is not None: + self._name = playlist_name + else: + + # Find a good unused name. + i = 1 + while True: + self._name = ( + self._pvars.default_new_list_name.evaluate() + + ((' ' + str(i)) if i > 1 else '')) + if self._name not in appconfig[self._pvars.config_name + + ' Playlists']: + break + i += 1 + + # Also we want it to start with 'add' highlighted since its empty + # and that's all they can do. + self._edit_ui_selection = 'add_button' + + ba.app.ui.set_main_menu_window( + PlaylistEditWindow(editcontroller=self, + transition=transition).get_root_widget()) + + def get_config_name(self) -> str: + """(internal)""" + return self._pvars.config_name + + def get_existing_playlist_name(self) -> Optional[str]: + """(internal)""" + return self._existing_playlist_name + + def get_edit_ui_selection(self) -> Optional[str]: + """(internal)""" + return self._edit_ui_selection + + def set_edit_ui_selection(self, selection: str) -> None: + """(internal)""" + self._edit_ui_selection = selection + + def getname(self) -> str: + """(internal)""" + return self._name + + def setname(self, name: str) -> None: + """(internal)""" + self._name = name + + def get_playlist(self) -> List[Dict[str, Any]]: + """Return the current state of the edited playlist.""" + return copy.deepcopy(self._playlist) + + def set_playlist(self, playlist: List[Dict[str, Any]]) -> None: + """Set the playlist contents.""" + self._playlist = copy.deepcopy(playlist) + + def get_session_type(self) -> Type[ba.Session]: + """Return the ba.Session type for this edit-session.""" + return self._sessiontype + + def get_selected_index(self) -> int: + """Return the index of the selected playlist.""" + return self._selected_index + + def get_default_list_name(self) -> ba.Lstr: + """(internal)""" + return self._pvars.default_list_name + + def set_selected_index(self, index: int) -> None: + """Sets the selected playlist index.""" + self._selected_index = index + + def add_game_pressed(self) -> None: + """(internal)""" + from bastd.ui.playlist.addgame import PlaylistAddGameWindow + ba.app.ui.clear_main_menu_window(transition='out_left') + ba.app.ui.set_main_menu_window( + PlaylistAddGameWindow(editcontroller=self).get_root_widget()) + + def edit_game_pressed(self) -> None: + """Should be called by supplemental UIs when a game is to be edited.""" + from ba.internal import getclass + if not self._playlist: + return + self._show_edit_ui(gametype=getclass( + self._playlist[self._selected_index]['type'], + subclassof=ba.GameActivity), + settings=self._playlist[self._selected_index]) + + def add_game_cancelled(self) -> None: + """(internal)""" + from bastd.ui.playlist.edit import PlaylistEditWindow + ba.app.ui.clear_main_menu_window(transition='out_right') + ba.app.ui.set_main_menu_window( + PlaylistEditWindow(editcontroller=self, + transition='in_left').get_root_widget()) + + def _show_edit_ui(self, gametype: Type[ba.GameActivity], + settings: Optional[Dict[str, Any]]) -> None: + self._editing_game = (settings is not None) + self._editing_game_type = gametype + assert self._sessiontype is not None + gametype.create_settings_ui(self._sessiontype, copy.deepcopy(settings), + self._edit_game_done) + + def add_game_type_selected(self, gametype: Type[ba.GameActivity]) -> None: + """(internal)""" + self._show_edit_ui(gametype=gametype, settings=None) + + def _edit_game_done(self, config: Optional[Dict[str, Any]]) -> None: + from bastd.ui.playlist.edit import PlaylistEditWindow + from bastd.ui.playlist.addgame import PlaylistAddGameWindow + from ba.internal import get_type_name + if config is None: + # If we were editing, go back to our list. + if self._editing_game: + ba.playsound(ba.getsound('powerdown01')) + ba.app.ui.clear_main_menu_window(transition='out_right') + ba.app.ui.set_main_menu_window( + PlaylistEditWindow(editcontroller=self, + transition='in_left').get_root_widget()) + + # Otherwise we were adding; go back to the add type choice list. + else: + ba.app.ui.clear_main_menu_window(transition='out_right') + ba.app.ui.set_main_menu_window( + PlaylistAddGameWindow( + editcontroller=self, + transition='in_left').get_root_widget()) + else: + # Make sure type is in there. + assert self._editing_game_type is not None + config['type'] = get_type_name(self._editing_game_type) + + if self._editing_game: + self._playlist[self._selected_index] = copy.deepcopy(config) + else: + # Add a new entry to the playlist. + insert_index = min(len(self._playlist), + self._selected_index + 1) + self._playlist.insert(insert_index, copy.deepcopy(config)) + self._selected_index = insert_index + + ba.playsound(ba.getsound('gunCocking')) + ba.app.ui.clear_main_menu_window(transition='out_right') + ba.app.ui.set_main_menu_window( + PlaylistEditWindow(editcontroller=self, + transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/playlist/editgame.py b/dist/ba_data/python/bastd/ui/playlist/editgame.py new file mode 100644 index 0000000..8533cc1 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playlist/editgame.py @@ -0,0 +1,484 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for editing a game config.""" + +from __future__ import annotations + +import copy +import random +from typing import TYPE_CHECKING, cast + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Type, Any, Dict, Callable, Optional, Union, List + + +class PlaylistEditGameWindow(ba.Window): + """Window for editing a game config.""" + + def __init__(self, + gametype: Type[ba.GameActivity], + sessiontype: Type[ba.Session], + config: Optional[Dict[str, Any]], + completion_call: Callable[[Optional[Dict[str, Any]]], Any], + default_selection: str = None, + transition: str = 'in_right', + edit_info: Dict[str, Any] = None): + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + from ba.internal import (get_unowned_maps, get_filtered_map_name, + get_map_class, get_map_display_string) + self._gametype = gametype + self._sessiontype = sessiontype + + # If we're within an editing session we get passed edit_info + # (returning from map selection window, etc). + if edit_info is not None: + self._edit_info = edit_info + + # ..otherwise determine whether we're adding or editing a game based + # on whether an existing config was passed to us. + else: + if config is None: + self._edit_info = {'editType': 'add'} + else: + self._edit_info = {'editType': 'edit'} + + self._r = 'gameSettingsWindow' + + valid_maps = gametype.get_supported_maps(sessiontype) + if not valid_maps: + ba.screenmessage(ba.Lstr(resource='noValidMapsErrorText')) + raise Exception('No valid maps') + + self._settings_defs = gametype.get_available_settings(sessiontype) + self._completion_call = completion_call + + # To start with, pick a random map out of the ones we own. + unowned_maps = get_unowned_maps() + valid_maps_owned = [m for m in valid_maps if m not in unowned_maps] + if valid_maps_owned: + self._map = valid_maps[random.randrange(len(valid_maps_owned))] + + # Hmmm.. we own none of these maps.. just pick a random un-owned one + # I guess.. should this ever happen? + else: + self._map = valid_maps[random.randrange(len(valid_maps))] + + is_add = (self._edit_info['editType'] == 'add') + + # If there's a valid map name in the existing config, use that. + try: + if (config is not None and 'settings' in config + and 'map' in config['settings']): + filtered_map_name = get_filtered_map_name( + config['settings']['map']) + if filtered_map_name in valid_maps: + self._map = filtered_map_name + except Exception: + ba.print_exception('Error getting map for editor.') + + if config is not None and 'settings' in config: + self._settings = config['settings'] + else: + self._settings = {} + + self._choice_selections: Dict[str, int] = {} + + uiscale = ba.app.ui.uiscale + width = 720 if uiscale is ba.UIScale.SMALL else 620 + x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + height = (365 if uiscale is ba.UIScale.SMALL else + 460 if uiscale is ba.UIScale.MEDIUM else 550) + spacing = 52 + y_extra = 15 + y_extra2 = 21 + + map_tex_name = (get_map_class(self._map).get_preview_texture_name()) + if map_tex_name is None: + raise Exception('no map preview tex found for' + self._map) + map_tex = ba.gettexture(map_tex_name) + + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + super().__init__(root_widget=ba.containerwidget( + size=(width, height + top_extra), + transition=transition, + scale=(2.19 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -17) if uiscale is ba.UIScale.SMALL else (0, 0))) + + btn = ba.buttonwidget( + parent=self._root_widget, + position=(45 + x_inset, height - 82 + y_extra2), + size=(180, 70) if is_add else (180, 65), + label=ba.Lstr(resource='backText') if is_add else ba.Lstr( + resource='cancelText'), + button_type='back' if is_add else None, + autoselect=True, + scale=0.75, + text_scale=1.3, + on_activate_call=ba.Call(self._cancel)) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + add_button = ba.buttonwidget( + parent=self._root_widget, + position=(width - (193 + x_inset), height - 82 + y_extra2), + size=(200, 65), + scale=0.75, + text_scale=1.3, + label=ba.Lstr(resource=self._r + + '.addGameText') if is_add else ba.Lstr( + resource='doneText')) + + if ba.app.ui.use_toolbars: + pbtn = _ba.get_special_widget('party_button') + ba.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn) + + ba.textwidget(parent=self._root_widget, + position=(-8, height - 70 + y_extra2), + size=(width, 25), + text=gametype.get_display_string(), + color=ba.app.ui.title_color, + maxwidth=235, + scale=1.1, + h_align='center', + v_align='center') + + map_height = 100 + + scroll_height = map_height + 10 # map select and margin + + # Calc our total height we'll need + scroll_height += spacing * len(self._settings_defs) + + scroll_width = width - (86 + 2 * x_inset) + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + position=(44 + x_inset, + 35 + y_extra), + size=(scroll_width, height - 116), + highlight=False, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(scroll_width, + scroll_height), + background=False, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + + v = scroll_height - 5 + h = -40 + + # Keep track of all the selectable widgets we make so we can wire + # them up conveniently. + widget_column: List[List[ba.Widget]] = [] + + # Map select button. + ba.textwidget(parent=self._subcontainer, + position=(h + 49, v - 63), + size=(100, 30), + maxwidth=110, + text=ba.Lstr(resource='mapText'), + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center') + + ba.imagewidget( + parent=self._subcontainer, + size=(256 * 0.7, 125 * 0.7), + position=(h + 261 - 128 + 128.0 * 0.56, v - 90), + texture=map_tex, + model_opaque=ba.getmodel('level_select_button_opaque'), + model_transparent=ba.getmodel('level_select_button_transparent'), + mask_texture=ba.gettexture('mapPreviewMask')) + map_button = btn = ba.buttonwidget( + parent=self._subcontainer, + size=(140, 60), + position=(h + 448, v - 72), + on_activate_call=ba.Call(self._select_map), + scale=0.7, + label=ba.Lstr(resource='mapSelectText')) + widget_column.append([btn]) + + ba.textwidget(parent=self._subcontainer, + position=(h + 363 - 123, v - 114), + size=(100, 30), + flatness=1.0, + shadow=1.0, + scale=0.55, + maxwidth=256 * 0.7 * 0.8, + text=get_map_display_string(self._map), + h_align='center', + color=(0.6, 1.0, 0.6, 1.0), + v_align='center') + v -= map_height + + for setting in self._settings_defs: + value = setting.default + value_type = type(value) + + # Now, if there's an existing value for it in the config, + # override with that. + try: + if (config is not None and 'settings' in config + and setting.name in config['settings']): + value = value_type(config['settings'][setting.name]) + except Exception: + ba.print_exception() + + # Shove the starting value in there to start. + self._settings[setting.name] = value + + name_translated = self._get_localized_setting_name(setting.name) + + mw1 = 280 + mw2 = 70 + + # Handle types with choices specially: + if isinstance(setting, ba.ChoiceSetting): + for choice in setting.choices: + if len(choice) != 2: + raise ValueError( + "Expected 2-member tuples for 'choices'; got: " + + repr(choice)) + if not isinstance(choice[0], str): + raise TypeError( + 'First value for choice tuple must be a str; got: ' + + repr(choice)) + if not isinstance(choice[1], value_type): + raise TypeError( + 'Choice type does not match default value; choice:' + + repr(choice) + '; setting:' + repr(setting)) + if value_type not in (int, float): + raise TypeError( + 'Choice type setting must have int or float default; ' + 'got: ' + repr(setting)) + + # Start at the choice corresponding to the default if possible. + self._choice_selections[setting.name] = 0 + for index, choice in enumerate(setting.choices): + if choice[1] == value: + self._choice_selections[setting.name] = index + break + + v -= spacing + ba.textwidget(parent=self._subcontainer, + position=(h + 50, v), + size=(100, 30), + maxwidth=mw1, + text=name_translated, + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center') + txt = ba.textwidget( + parent=self._subcontainer, + position=(h + 509 - 95, v), + size=(0, 28), + text=self._get_localized_setting_name(setting.choices[ + self._choice_selections[setting.name]][0]), + editable=False, + color=(0.6, 1.0, 0.6, 1.0), + maxwidth=mw2, + h_align='right', + v_align='center', + padding=2) + btn1 = ba.buttonwidget(parent=self._subcontainer, + position=(h + 509 - 50 - 1, v), + size=(28, 28), + label='<', + autoselect=True, + on_activate_call=ba.Call( + self._choice_inc, setting.name, txt, + setting, -1), + repeat=True) + btn2 = ba.buttonwidget(parent=self._subcontainer, + position=(h + 509 + 5, v), + size=(28, 28), + label='>', + autoselect=True, + on_activate_call=ba.Call( + self._choice_inc, setting.name, txt, + setting, 1), + repeat=True) + widget_column.append([btn1, btn2]) + + elif isinstance(setting, (ba.IntSetting, ba.FloatSetting)): + v -= spacing + min_value = setting.min_value + max_value = setting.max_value + increment = setting.increment + ba.textwidget(parent=self._subcontainer, + position=(h + 50, v), + size=(100, 30), + text=name_translated, + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + maxwidth=mw1) + txt = ba.textwidget(parent=self._subcontainer, + position=(h + 509 - 95, v), + size=(0, 28), + text=str(value), + editable=False, + color=(0.6, 1.0, 0.6, 1.0), + maxwidth=mw2, + h_align='right', + v_align='center', + padding=2) + btn1 = ba.buttonwidget(parent=self._subcontainer, + position=(h + 509 - 50 - 1, v), + size=(28, 28), + label='-', + autoselect=True, + on_activate_call=ba.Call( + self._inc, txt, min_value, + max_value, -increment, value_type, + setting.name), + repeat=True) + btn2 = ba.buttonwidget(parent=self._subcontainer, + position=(h + 509 + 5, v), + size=(28, 28), + label='+', + autoselect=True, + on_activate_call=ba.Call( + self._inc, txt, min_value, + max_value, increment, value_type, + setting.name), + repeat=True) + widget_column.append([btn1, btn2]) + + elif value_type == bool: + v -= spacing + ba.textwidget(parent=self._subcontainer, + position=(h + 50, v), + size=(100, 30), + text=name_translated, + h_align='left', + color=(0.8, 0.8, 0.8, 1.0), + v_align='center', + maxwidth=mw1) + txt = ba.textwidget( + parent=self._subcontainer, + position=(h + 509 - 95, v), + size=(0, 28), + text=ba.Lstr(resource='onText') if value else ba.Lstr( + resource='offText'), + editable=False, + color=(0.6, 1.0, 0.6, 1.0), + maxwidth=mw2, + h_align='right', + v_align='center', + padding=2) + cbw = ba.checkboxwidget(parent=self._subcontainer, + text='', + position=(h + 505 - 50 - 5, v - 2), + size=(200, 30), + autoselect=True, + textcolor=(0.8, 0.8, 0.8), + value=value, + on_value_change_call=ba.Call( + self._check_value_change, + setting.name, txt)) + widget_column.append([cbw]) + + else: + raise Exception() + + # Ok now wire up the column. + try: + # pylint: disable=unsubscriptable-object + prev_widgets: Optional[List[ba.Widget]] = None + for cwdg in widget_column: + if prev_widgets is not None: + # Wire our rightmost to their rightmost. + ba.widget(edit=prev_widgets[-1], down_widget=cwdg[-1]) + ba.widget(cwdg[-1], up_widget=prev_widgets[-1]) + + # Wire our leftmost to their leftmost. + ba.widget(edit=prev_widgets[0], down_widget=cwdg[0]) + ba.widget(cwdg[0], up_widget=prev_widgets[0]) + prev_widgets = cwdg + except Exception: + ba.print_exception( + 'Error wiring up game-settings-select widget column.') + + ba.buttonwidget(edit=add_button, on_activate_call=ba.Call(self._add)) + ba.containerwidget(edit=self._root_widget, + selected_child=add_button, + start_button=add_button) + + if default_selection == 'map': + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + ba.containerwidget(edit=self._subcontainer, + selected_child=map_button) + + def _get_localized_setting_name(self, name: str) -> ba.Lstr: + return ba.Lstr(translate=('settingNames', name)) + + def _select_map(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.playlist.mapselect import PlaylistMapSelectWindow + + # Replace ourself with the map-select UI. + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + PlaylistMapSelectWindow(self._gametype, self._sessiontype, + copy.deepcopy(self._getconfig()), + self._edit_info, + self._completion_call).get_root_widget()) + + def _choice_inc(self, setting_name: str, widget: ba.Widget, + setting: ba.ChoiceSetting, increment: int) -> None: + choices = setting.choices + if increment > 0: + self._choice_selections[setting_name] = min( + len(choices) - 1, self._choice_selections[setting_name] + 1) + else: + self._choice_selections[setting_name] = max( + 0, self._choice_selections[setting_name] - 1) + ba.textwidget(edit=widget, + text=self._get_localized_setting_name( + choices[self._choice_selections[setting_name]][0])) + self._settings[setting_name] = choices[ + self._choice_selections[setting_name]][1] + + def _cancel(self) -> None: + self._completion_call(None) + + def _check_value_change(self, setting_name: str, widget: ba.Widget, + value: int) -> None: + ba.textwidget(edit=widget, + text=ba.Lstr(resource='onText') if value else ba.Lstr( + resource='offText')) + self._settings[setting_name] = value + + def _getconfig(self) -> Dict[str, Any]: + settings = copy.deepcopy(self._settings) + settings['map'] = self._map + return {'settings': settings} + + def _add(self) -> None: + self._completion_call(copy.deepcopy(self._getconfig())) + + def _inc(self, ctrl: ba.Widget, min_val: Union[int, float], + max_val: Union[int, float], increment: Union[int, float], + setting_type: Type, setting_name: str) -> None: + if setting_type == float: + val = float(cast(str, ba.textwidget(query=ctrl))) + else: + val = int(cast(str, ba.textwidget(query=ctrl))) + val += increment + val = max(min_val, min(val, max_val)) + if setting_type == float: + ba.textwidget(edit=ctrl, text=str(round(val, 2))) + elif setting_type == int: + ba.textwidget(edit=ctrl, text=str(int(val))) + else: + raise TypeError('invalid vartype: ' + str(setting_type)) + self._settings[setting_name] = val diff --git a/dist/ba_data/python/bastd/ui/playlist/mapselect.py b/dist/ba_data/python/bastd/ui/playlist/mapselect.py new file mode 100644 index 0000000..2381f6c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playlist/mapselect.py @@ -0,0 +1,256 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for selecting maps in playlists.""" + +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Type, Any, Callable, Dict, List, Tuple, Optional + + +class PlaylistMapSelectWindow(ba.Window): + """Window to select a map.""" + + def __init__(self, + gametype: Type[ba.GameActivity], + sessiontype: Type[ba.Session], + config: Dict[str, Any], + edit_info: Dict[str, Any], + completion_call: Callable[[Optional[Dict[str, Any]]], Any], + transition: str = 'in_right'): + from ba.internal import get_filtered_map_name + self._gametype = gametype + self._sessiontype = sessiontype + self._config = config + self._completion_call = completion_call + self._edit_info = edit_info + self._maps: List[Tuple[str, ba.Texture]] = [] + try: + self._previous_map = get_filtered_map_name( + config['settings']['map']) + except Exception: + self._previous_map = '' + + uiscale = ba.app.ui.uiscale + width = 715 if uiscale is ba.UIScale.SMALL else 615 + x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + height = (400 if uiscale is ba.UIScale.SMALL else + 480 if uiscale is ba.UIScale.MEDIUM else 600) + + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + super().__init__(root_widget=ba.containerwidget( + size=(width, height + top_extra), + transition=transition, + scale=(2.17 if uiscale is ba.UIScale.SMALL else + 1.3 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -27) if uiscale is ba.UIScale.SMALL else (0, 0))) + + self._cancel_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(38 + x_inset, height - 67), + size=(140, 50), + scale=0.9, + text_scale=1.0, + autoselect=True, + label=ba.Lstr(resource='cancelText'), + on_activate_call=self._cancel) + + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, height - 46), + size=(0, 0), + maxwidth=260, + scale=1.1, + text=ba.Lstr(resource='mapSelectTitleText', + subs=[('${GAME}', + self._gametype.get_display_string()) + ]), + color=ba.app.ui.title_color, + h_align='center', + v_align='center') + v = height - 70 + self._scroll_width = width - (80 + 2 * x_inset) + self._scroll_height = height - 140 + + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + position=(40 + x_inset, v - self._scroll_height), + size=(self._scroll_width, self._scroll_height)) + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + + self._subcontainer: Optional[ba.Widget] = None + self._refresh() + + def _refresh(self, select_get_more_maps_button: bool = False) -> None: + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + from ba.internal import (get_unowned_maps, get_map_class, + get_map_display_string) + + # Kill old. + if self._subcontainer is not None: + self._subcontainer.delete() + + model_opaque = ba.getmodel('level_select_button_opaque') + model_transparent = ba.getmodel('level_select_button_transparent') + + self._maps = [] + map_list = self._gametype.get_supported_maps(self._sessiontype) + map_list_sorted = list(map_list) + map_list_sorted.sort() + unowned_maps = get_unowned_maps() + + for mapname in map_list_sorted: + + # Disallow ones we don't own. + if mapname in unowned_maps: + continue + map_tex_name = (get_map_class(mapname).get_preview_texture_name()) + if map_tex_name is not None: + try: + map_tex = ba.gettexture(map_tex_name) + self._maps.append((mapname, map_tex)) + except Exception: + print(f'Invalid map preview texture: "{map_tex_name}".') + else: + print('Error: no map preview texture for map:', mapname) + + count = len(self._maps) + columns = 2 + rows = int(math.ceil(float(count) / columns)) + button_width = 220 + button_height = button_width * 0.5 + button_buffer_h = 16 + button_buffer_v = 19 + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 5 + rows * (button_height + + 2 * button_buffer_v) + 100 + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + index = 0 + mask_texture = ba.gettexture('mapPreviewMask') + h_offs = 130 if len(self._maps) == 1 else 0 + for y in range(rows): + for x in range(columns): + pos = (x * (button_width + 2 * button_buffer_h) + + button_buffer_h + h_offs, self._sub_height - (y + 1) * + (button_height + 2 * button_buffer_v) + 12) + btn = ba.buttonwidget(parent=self._subcontainer, + button_type='square', + size=(button_width, button_height), + autoselect=True, + texture=self._maps[index][1], + mask_texture=mask_texture, + model_opaque=model_opaque, + model_transparent=model_transparent, + label='', + color=(1, 1, 1), + on_activate_call=ba.Call( + self._select_with_delay, + self._maps[index][0]), + position=pos) + if x == 0: + ba.widget(edit=btn, left_widget=self._cancel_button) + if y == 0: + ba.widget(edit=btn, up_widget=self._cancel_button) + if x == columns - 1 and ba.app.ui.use_toolbars: + ba.widget( + edit=btn, + right_widget=_ba.get_special_widget('party_button')) + + ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + if self._maps[index][0] == self._previous_map: + ba.containerwidget(edit=self._subcontainer, + selected_child=btn, + visible_child=btn) + name = get_map_display_string(self._maps[index][0]) + ba.textwidget(parent=self._subcontainer, + text=name, + position=(pos[0] + button_width * 0.5, + pos[1] - 12), + size=(0, 0), + scale=0.5, + maxwidth=button_width, + draw_controller=btn, + h_align='center', + v_align='center', + color=(0.8, 0.8, 0.8, 0.8)) + index += 1 + + if index >= count: + break + if index >= count: + break + self._get_more_maps_button = btn = ba.buttonwidget( + parent=self._subcontainer, + size=(self._sub_width * 0.8, 60), + position=(self._sub_width * 0.1, 30), + label=ba.Lstr(resource='mapSelectGetMoreMapsText'), + on_activate_call=self._on_store_press, + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + autoselect=True) + ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) + if select_get_more_maps_button: + ba.containerwidget(edit=self._subcontainer, + selected_child=btn, + visible_child=btn) + + def _on_store_press(self) -> None: + from bastd.ui import account + from bastd.ui.store.browser import StoreBrowserWindow + if _ba.get_account_state() != 'signed_in': + account.show_sign_in_prompt() + return + StoreBrowserWindow(modal=True, + show_tab=StoreBrowserWindow.TabID.MAPS, + on_close_call=self._on_store_close, + origin_widget=self._get_more_maps_button) + + def _on_store_close(self) -> None: + self._refresh(select_get_more_maps_button=True) + + def _select(self, map_name: str) -> None: + from bastd.ui.playlist.editgame import PlaylistEditGameWindow + self._config['settings']['map'] = map_name + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + PlaylistEditGameWindow( + self._gametype, + self._sessiontype, + self._config, + self._completion_call, + default_selection='map', + transition='in_left', + edit_info=self._edit_info).get_root_widget()) + + def _select_with_delay(self, map_name: str) -> None: + _ba.lock_all_input() + ba.timer(0.1, _ba.unlock_all_input, timetype=ba.TimeType.REAL) + ba.timer(0.1, + ba.WeakCall(self._select, map_name), + timetype=ba.TimeType.REAL) + + def _cancel(self) -> None: + from bastd.ui.playlist.editgame import PlaylistEditGameWindow + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + PlaylistEditGameWindow( + self._gametype, + self._sessiontype, + self._config, + self._completion_call, + default_selection='map', + transition='in_left', + edit_info=self._edit_info).get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/playlist/share.py b/dist/ba_data/python/bastd/ui/playlist/share.py new file mode 100644 index 0000000..0cd1ea4 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playlist/share.py @@ -0,0 +1,133 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for importing shared playlists.""" + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui import promocode + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Tuple + + +class SharePlaylistImportWindow(promocode.PromoCodeWindow): + """Window for importing a shared playlist.""" + + def __init__(self, + origin_widget: ba.Widget = None, + on_success_callback: Callable[[], Any] = None): + promocode.PromoCodeWindow.__init__(self, + modal=True, + origin_widget=origin_widget) + self._on_success_callback = on_success_callback + + def _on_import_response(self, response: Optional[Dict[str, Any]]) -> None: + if response is None: + ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + if response['playlistType'] == 'Team Tournament': + playlist_type_name = ba.Lstr(resource='playModes.teamsText') + elif response['playlistType'] == 'Free-for-All': + playlist_type_name = ba.Lstr(resource='playModes.freeForAllText') + else: + playlist_type_name = ba.Lstr(value=response['playlistType']) + + ba.screenmessage(ba.Lstr(resource='importPlaylistSuccessText', + subs=[('${TYPE}', playlist_type_name), + ('${NAME}', response['playlistName'])]), + color=(0, 1, 0)) + ba.playsound(ba.getsound('gunCocking')) + if self._on_success_callback is not None: + self._on_success_callback() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + + def _do_enter(self) -> None: + _ba.add_transaction( + { + 'type': 'IMPORT_PLAYLIST', + 'expire_time': time.time() + 5, + 'code': ba.textwidget(query=self._text_field) + }, + callback=ba.WeakCall(self._on_import_response)) + _ba.run_transactions() + ba.screenmessage(ba.Lstr(resource='importingText')) + + +class SharePlaylistResultsWindow(ba.Window): + """Window for sharing playlists.""" + + def __init__(self, + name: str, + data: str, + origin: Tuple[float, float] = (0.0, 0.0)): + del origin # unused arg + self._width = 450 + self._height = 300 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + color=(0.45, 0.63, 0.15), + transition='in_scale', + scale=(1.8 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0))) + ba.playsound(ba.getsound('cashRegister')) + ba.playsound(ba.getsound('swish')) + + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + scale=0.7, + position=(40, self._height - 40), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.745), + size=(0, 0), + color=ba.app.ui.infotextcolor, + scale=1.0, + flatness=1.0, + h_align='center', + v_align='center', + text=ba.Lstr(resource='exportSuccessText', + subs=[('${NAME}', name)]), + maxwidth=self._width * 0.85) + + ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.645), + size=(0, 0), + color=ba.app.ui.infotextcolor, + scale=0.6, + flatness=1.0, + h_align='center', + v_align='center', + text=ba.Lstr(resource='importPlaylistCodeInstructionsText'), + maxwidth=self._width * 0.85) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.4), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=2.3, + h_align='center', + v_align='center', + text=data, + maxwidth=self._width * 0.85) + + def close(self) -> None: + """Close the window.""" + ba.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/dist/ba_data/python/bastd/ui/playoptions.py b/dist/ba_data/python/bastd/ui/playoptions.py new file mode 100644 index 0000000..af6725d --- /dev/null +++ b/dist/ba_data/python/bastd/ui/playoptions.py @@ -0,0 +1,437 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a window for configuring play options.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + from typing import Any, Type, Tuple, Optional, Union + + +class PlayOptionsWindow(popup.PopupWindow): + """A popup window for configuring play options.""" + + def __init__(self, + sessiontype: Type[ba.Session], + playlist: str, + scale_origin: Tuple[float, float], + delegate: Any = None): + # FIXME: Tidy this up. + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + from ba.internal import get_map_class, getclass, filter_playlist + from bastd.ui.playlist import PlaylistTypeVars + + self._r = 'gameListWindow' + self._delegate = delegate + self._pvars = PlaylistTypeVars(sessiontype) + self._transitioning_out = False + + # We behave differently if we're being used for playlist selection + # vs starting a game directly (should make this more elegant). + self._selecting_mode = ba.app.ui.selecting_private_party_playlist + + self._do_randomize_val = (ba.app.config.get( + self._pvars.config_name + ' Playlist Randomize', 0)) + + self._sessiontype = sessiontype + self._playlist = playlist + + self._width = 500.0 + self._height = 330.0 - 50.0 + + # In teams games, show the custom names/colors button. + if self._sessiontype is ba.DualTeamSession: + self._height += 50.0 + + self._row_height = 45.0 + + # Grab our maps to display. + model_opaque = ba.getmodel('level_select_button_opaque') + model_transparent = ba.getmodel('level_select_button_transparent') + mask_tex = ba.gettexture('mapPreviewMask') + + # Poke into this playlist and see if we can display some of its maps. + map_textures = [] + map_texture_entries = [] + rows = 0 + columns = 0 + game_count = 0 + scl = 0.35 + c_width_total = 0.0 + try: + max_columns = 5 + name = playlist + if name == '__default__': + plst = self._pvars.get_default_list_call() + else: + try: + plst = ba.app.config[self._pvars.config_name + + ' Playlists'][name] + except Exception: + print('ERROR INFO: self._config_name is:', + self._pvars.config_name) + print( + 'ERROR INFO: playlist names are:', + list(ba.app.config[self._pvars.config_name + + ' Playlists'].keys())) + raise + plst = filter_playlist(plst, + self._sessiontype, + remove_unowned=False, + mark_unowned=True) + game_count = len(plst) + for entry in plst: + mapname = entry['settings']['map'] + maptype: Optional[Type[ba.Map]] + try: + maptype = get_map_class(mapname) + except ba.NotFoundError: + maptype = None + if maptype is not None: + tex_name = maptype.get_preview_texture_name() + if tex_name is not None: + map_textures.append(tex_name) + map_texture_entries.append(entry) + rows = (max(0, len(map_textures) - 1) // max_columns) + 1 + columns = min(max_columns, len(map_textures)) + + if len(map_textures) == 1: + scl = 1.1 + elif len(map_textures) == 2: + scl = 0.7 + elif len(map_textures) == 3: + scl = 0.55 + else: + scl = 0.35 + self._row_height = 128.0 * scl + c_width_total = scl * 250.0 * columns + if map_textures: + self._height += self._row_height * rows + + except Exception: + ba.print_exception('Error listing playlist maps.') + + show_shuffle_check_box = game_count > 1 + + if show_shuffle_check_box: + self._height += 40 + + # Creates our _root_widget. + uiscale = ba.app.ui.uiscale + scale = (1.69 if uiscale is ba.UIScale.SMALL else + 1.1 if uiscale is ba.UIScale.MEDIUM else 0.85) + super().__init__(position=scale_origin, + size=(self._width, self._height), + scale=scale) + + playlist_name: Union[str, ba.Lstr] = (self._pvars.default_list_name + if playlist == '__default__' else + playlist) + self._title_text = ba.textwidget(parent=self.root_widget, + position=(self._width * 0.5, + self._height - 89 + 51), + size=(0, 0), + text=playlist_name, + scale=1.4, + color=(1, 1, 1), + maxwidth=self._width * 0.7, + h_align='center', + v_align='center') + + self._cancel_button = ba.buttonwidget( + parent=self.root_widget, + position=(25, self._height - 53), + size=(50, 50), + scale=0.7, + label='', + color=(0.42, 0.73, 0.2), + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + + h_offs_img = self._width * 0.5 - c_width_total * 0.5 + v_offs_img = self._height - 118 - scl * 125.0 + 50 + bottom_row_buttons = [] + self._have_at_least_one_owned = False + + for row in range(rows): + for col in range(columns): + tex_index = row * columns + col + if tex_index < len(map_textures): + tex_name = map_textures[tex_index] + h = h_offs_img + scl * 250 * col + v = v_offs_img - self._row_height * row + entry = map_texture_entries[tex_index] + owned = not (('is_unowned_map' in entry + and entry['is_unowned_map']) or + ('is_unowned_game' in entry + and entry['is_unowned_game'])) + + if owned: + self._have_at_least_one_owned = True + + try: + desc = getclass(entry['type'], + subclassof=ba.GameActivity + ).get_settings_display_string(entry) + if not owned: + desc = ba.Lstr( + value='${DESC}\n${UNLOCK}', + subs=[ + ('${DESC}', desc), + ('${UNLOCK}', + ba.Lstr( + resource='unlockThisInTheStoreText')) + ]) + desc_color = (0, 1, 0) if owned else (1, 0, 0) + except Exception: + desc = ba.Lstr(value='(invalid)') + desc_color = (1, 0, 0) + + btn = ba.buttonwidget( + parent=self.root_widget, + size=(scl * 240.0, scl * 120.0), + position=(h, v), + texture=ba.gettexture(tex_name if owned else 'empty'), + model_opaque=model_opaque if owned else None, + on_activate_call=ba.Call(ba.screenmessage, desc, + desc_color), + label='', + color=(1, 1, 1), + autoselect=True, + extra_touch_border_scale=0.0, + model_transparent=model_transparent if owned else None, + mask_texture=mask_tex if owned else None) + if row == 0 and col == 0: + ba.widget(edit=self._cancel_button, down_widget=btn) + if row == rows - 1: + bottom_row_buttons.append(btn) + if not owned: + + # Ewww; buttons don't currently have alpha so in this + # case we draw an image over our button with an empty + # texture on it. + ba.imagewidget(parent=self.root_widget, + size=(scl * 260.0, scl * 130.0), + position=(h - 10.0 * scl, + v - 4.0 * scl), + draw_controller=btn, + color=(1, 1, 1), + texture=ba.gettexture(tex_name), + model_opaque=model_opaque, + opacity=0.25, + model_transparent=model_transparent, + mask_texture=mask_tex) + + ba.imagewidget(parent=self.root_widget, + size=(scl * 100, scl * 100), + draw_controller=btn, + position=(h + scl * 70, v + scl * 10), + texture=ba.gettexture('lock')) + + # Team names/colors. + self._custom_colors_names_button: Optional[ba.Widget] + if self._sessiontype is ba.DualTeamSession: + y_offs = 50 if show_shuffle_check_box else 0 + self._custom_colors_names_button = ba.buttonwidget( + parent=self.root_widget, + position=(100, 200 + y_offs), + size=(290, 35), + on_activate_call=ba.WeakCall(self._custom_colors_names_press), + autoselect=True, + textcolor=(0.8, 0.8, 0.8), + label=ba.Lstr(resource='teamNamesColorText')) + if not ba.app.accounts.have_pro(): + ba.imagewidget( + parent=self.root_widget, + size=(30, 30), + position=(95, 202 + y_offs), + texture=ba.gettexture('lock'), + draw_controller=self._custom_colors_names_button) + else: + self._custom_colors_names_button = None + + # Shuffle. + def _cb_callback(val: bool) -> None: + self._do_randomize_val = val + cfg = ba.app.config + cfg[self._pvars.config_name + + ' Playlist Randomize'] = self._do_randomize_val + cfg.commit() + + if show_shuffle_check_box: + self._shuffle_check_box = ba.checkboxwidget( + parent=self.root_widget, + position=(110, 200), + scale=1.0, + size=(250, 30), + autoselect=True, + text=ba.Lstr(resource=self._r + '.shuffleGameOrderText'), + maxwidth=300, + textcolor=(0.8, 0.8, 0.8), + value=self._do_randomize_val, + on_value_change_call=_cb_callback) + + # Show tutorial. + show_tutorial = bool(ba.app.config.get('Show Tutorial', True)) + + def _cb_callback_2(val: bool) -> None: + cfg = ba.app.config + cfg['Show Tutorial'] = val + cfg.commit() + + self._show_tutorial_check_box = ba.checkboxwidget( + parent=self.root_widget, + position=(110, 151), + scale=1.0, + size=(250, 30), + autoselect=True, + text=ba.Lstr(resource=self._r + '.showTutorialText'), + maxwidth=300, + textcolor=(0.8, 0.8, 0.8), + value=show_tutorial, + on_value_change_call=_cb_callback_2) + + # Grumble: current autoselect doesn't do a very good job + # with checkboxes. + if self._custom_colors_names_button is not None: + for btn in bottom_row_buttons: + ba.widget(edit=btn, + down_widget=self._custom_colors_names_button) + if show_shuffle_check_box: + ba.widget(edit=self._custom_colors_names_button, + down_widget=self._shuffle_check_box) + ba.widget(edit=self._shuffle_check_box, + up_widget=self._custom_colors_names_button) + else: + ba.widget(edit=self._custom_colors_names_button, + down_widget=self._show_tutorial_check_box) + ba.widget(edit=self._show_tutorial_check_box, + up_widget=self._custom_colors_names_button) + + self._ok_button = ba.buttonwidget( + parent=self.root_widget, + position=(70, 44), + size=(200, 45), + scale=1.8, + text_res_scale=1.5, + on_activate_call=self._on_ok_press, + autoselect=True, + label=ba.Lstr( + resource='okText' if self._selecting_mode else 'playText')) + + ba.widget(edit=self._ok_button, + up_widget=self._show_tutorial_check_box) + + ba.containerwidget(edit=self.root_widget, + start_button=self._ok_button, + cancel_button=self._cancel_button, + selected_child=self._ok_button) + + # Update now and once per second. + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + self._update() + + def _custom_colors_names_press(self) -> None: + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.teamnamescolors import TeamNamesColorsWindow + from bastd.ui.purchase import PurchaseWindow + if not ba.app.accounts.have_pro(): + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + else: + PurchaseWindow(items=['pro']) + self._transition_out() + return + assert self._custom_colors_names_button + TeamNamesColorsWindow(scale_origin=self._custom_colors_names_button. + get_screen_space_center()) + + def _does_target_playlist_exist(self) -> bool: + if self._playlist == '__default__': + return True + return self._playlist in ba.app.config.get( + self._pvars.config_name + ' Playlists', {}) + + def _update(self) -> None: + # All we do here is make sure our targeted playlist still exists, + # and close ourself if not. + if not self._does_target_playlist_exist(): + self._transition_out() + + def _transition_out(self, transition: str = 'out_scale') -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition=transition) + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _on_ok_press(self) -> None: + + # Disallow if our playlist has disappeared. + if not self._does_target_playlist_exist(): + return + + # Disallow if we have no unlocked games. + if not self._have_at_least_one_owned: + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource='playlistNoValidGamesErrorText'), + color=(1, 0, 0)) + return + + cfg = ba.app.config + cfg[self._pvars.config_name + ' Playlist Selection'] = self._playlist + + # Head back to the gather window in playlist-select mode + # or start the game in regular mode. + if self._selecting_mode: + from bastd.ui.gather import GatherWindow + if self._sessiontype is ba.FreeForAllSession: + typename = 'ffa' + elif self._sessiontype is ba.DualTeamSession: + typename = 'teams' + else: + raise RuntimeError('Only teams and ffa currently supported') + cfg['Private Party Host Session Type'] = typename + ba.playsound(ba.getsound('gunCocking')) + ba.app.ui.set_main_menu_window( + GatherWindow(transition='in_right').get_root_widget()) + self._transition_out(transition='out_left') + if self._delegate is not None: + self._delegate.on_play_options_window_run_game() + else: + _ba.fade_screen(False, endcall=self._run_selected_playlist) + _ba.lock_all_input() + self._transition_out(transition='out_left') + if self._delegate is not None: + self._delegate.on_play_options_window_run_game() + + cfg.commit() + + def _run_selected_playlist(self) -> None: + _ba.unlock_all_input() + try: + _ba.new_host_session(self._sessiontype) + except Exception: + from bastd import mainmenu + ba.print_exception('exception running session', self._sessiontype) + + # Drop back into a main menu session. + _ba.new_host_session(mainmenu.MainMenuSession) diff --git a/dist/ba_data/python/bastd/ui/popup.py b/dist/ba_data/python/bastd/ui/popup.py new file mode 100644 index 0000000..500c379 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/popup.py @@ -0,0 +1,378 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Popup window/menu related functionality.""" + +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Tuple, Any, Sequence, Callable, Optional, List, Union + + +class PopupWindow: + """A transient window that positions and scales itself for visibility.""" + + def __init__(self, + position: Tuple[float, float], + size: Tuple[float, float], + scale: float = 1.0, + offset: Tuple[float, float] = (0, 0), + bg_color: Tuple[float, float, float] = (0.35, 0.55, 0.15), + focus_position: Tuple[float, float] = (0, 0), + focus_size: Tuple[float, float] = None, + toolbar_visibility: str = 'menu_minimal_no_back'): + # pylint: disable=too-many-locals + if focus_size is None: + focus_size = size + + # In vr mode we can't have windows going outside the screen. + if ba.app.vr_mode: + focus_size = size + focus_position = (0, 0) + + width = focus_size[0] + height = focus_size[1] + + # Ok, we've been given a desired width, height, and scale; + # we now need to ensure that we're all onscreen by scaling down if + # need be and clamping it to the UI bounds. + bounds = ba.app.ui_bounds + edge_buffer = 15 + bounds_width = (bounds[1] - bounds[0] - edge_buffer * 2) + bounds_height = (bounds[3] - bounds[2] - edge_buffer * 2) + + fin_width = width * scale + fin_height = height * scale + if fin_width > bounds_width: + scale /= (fin_width / bounds_width) + fin_width = width * scale + fin_height = height * scale + if fin_height > bounds_height: + scale /= (fin_height / bounds_height) + fin_width = width * scale + fin_height = height * scale + + x_min = bounds[0] + edge_buffer + fin_width * 0.5 + y_min = bounds[2] + edge_buffer + fin_height * 0.5 + x_max = bounds[1] - edge_buffer - fin_width * 0.5 + y_max = bounds[3] - edge_buffer - fin_height * 0.5 + + x_fin = min(max(x_min, position[0] + offset[0]), x_max) + y_fin = min(max(y_min, position[1] + offset[1]), y_max) + + # ok, we've calced a valid x/y position and a scale based on or + # focus area. ..now calc the difference between the center of our + # focus area and the center of our window to come up with the + # offset we'll need to plug in to the window + x_offs = ((focus_position[0] + focus_size[0] * 0.5) - + (size[0] * 0.5)) * scale + y_offs = ((focus_position[1] + focus_size[1] * 0.5) - + (size[1] * 0.5)) * scale + + self.root_widget = ba.containerwidget( + transition='in_scale', + scale=scale, + toolbar_visibility=toolbar_visibility, + size=size, + parent=_ba.get_special_widget('overlay_stack'), + stack_offset=(x_fin - x_offs, y_fin - y_offs), + scale_origin_stack_offset=(position[0], position[1]), + on_outside_click_call=self.on_popup_cancel, + claim_outside_clicks=True, + color=bg_color, + on_cancel_call=self.on_popup_cancel) + # complain if we outlive our root widget + ba.uicleanupcheck(self, self.root_widget) + + def on_popup_cancel(self) -> None: + """Called when the popup is canceled. + + Cancels can occur due to clicking outside the window, + hitting escape, etc. + """ + + +class PopupMenuWindow(PopupWindow): + """A menu built using popup-window functionality.""" + + def __init__(self, + position: Tuple[float, float], + choices: Sequence[str], + current_choice: str, + delegate: Any = None, + width: float = 230.0, + maxwidth: float = None, + scale: float = 1.0, + choices_disabled: Sequence[str] = None, + choices_display: Sequence[ba.Lstr] = None): + # FIXME: Clean up a bit. + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + if choices_disabled is None: + choices_disabled = [] + if choices_display is None: + choices_display = [] + + # FIXME: For the moment we base our width on these strings so + # we need to flatten them. + choices_display_fin: List[str] = [] + for choice_display in choices_display: + choices_display_fin.append(choice_display.evaluate()) + + if maxwidth is None: + maxwidth = width * 1.5 + + self._transitioning_out = False + self._choices = list(choices) + self._choices_display = list(choices_display_fin) + self._current_choice = current_choice + self._choices_disabled = list(choices_disabled) + self._done_building = False + if not choices: + raise TypeError('Must pass at least one choice') + self._width = width + self._scale = scale + if len(choices) > 8: + self._height = 280 + self._use_scroll = True + else: + self._height = 20 + len(choices) * 33 + self._use_scroll = False + self._delegate = None # don't want this stuff called just yet.. + + # extend width to fit our longest string (or our max-width) + for index, choice in enumerate(choices): + if len(choices_display_fin) == len(choices): + choice_display_name = choices_display_fin[index] + else: + choice_display_name = choice + if self._use_scroll: + self._width = max( + self._width, + min( + maxwidth, + _ba.get_string_width(choice_display_name, + suppress_warning=True)) + 75) + else: + self._width = max( + self._width, + min( + maxwidth, + _ba.get_string_width(choice_display_name, + suppress_warning=True)) + 60) + + # init parent class - this will rescale and reposition things as + # needed and create our root widget + PopupWindow.__init__(self, + position, + size=(self._width, self._height), + scale=self._scale) + + if self._use_scroll: + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + position=(20, 20), + highlight=False, + color=(0.35, 0.55, 0.15), + size=(self._width - 40, + self._height - 40)) + self._columnwidget = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0) + else: + self._offset_widget = ba.containerwidget(parent=self.root_widget, + position=(30, 15), + size=(self._width - 40, + self._height), + background=False) + self._columnwidget = ba.columnwidget(parent=self._offset_widget, + border=2, + margin=0) + for index, choice in enumerate(choices): + if len(choices_display_fin) == len(choices): + choice_display_name = choices_display_fin[index] + else: + choice_display_name = choice + inactive = (choice in self._choices_disabled) + wdg = ba.textwidget(parent=self._columnwidget, + size=(self._width - 40, 28), + on_select_call=ba.Call(self._select, index), + click_activate=True, + color=(0.5, 0.5, 0.5, 0.5) if inactive else + ((0.5, 1, 0.5, + 1) if choice == self._current_choice else + (0.8, 0.8, 0.8, 1.0)), + padding=0, + maxwidth=maxwidth, + text=choice_display_name, + on_activate_call=self._activate, + v_align='center', + selectable=(not inactive)) + if choice == self._current_choice: + ba.containerwidget(edit=self._columnwidget, + selected_child=wdg, + visible_child=wdg) + + # ok from now on our delegate can be called + self._delegate = weakref.ref(delegate) + self._done_building = True + + def _select(self, index: int) -> None: + if self._done_building: + self._current_choice = self._choices[index] + + def _activate(self) -> None: + ba.playsound(ba.getsound('swish')) + ba.timer(0.05, self._transition_out, timetype=ba.TimeType.REAL) + delegate = self._getdelegate() + if delegate is not None: + # Call this in a timer so it doesn't interfere with us killing + # our widgets and whatnot. + call = ba.Call(delegate.popup_menu_selected_choice, self, + self._current_choice) + ba.timer(0, call, timetype=ba.TimeType.REAL) + + def _getdelegate(self) -> Any: + return None if self._delegate is None else self._delegate() + + def _transition_out(self) -> None: + if not self.root_widget: + return + if not self._transitioning_out: + self._transitioning_out = True + delegate = self._getdelegate() + if delegate is not None: + delegate.popup_menu_closing(self) + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + if not self._transitioning_out: + ba.playsound(ba.getsound('swish')) + self._transition_out() + + +class PopupMenu: + """A complete popup-menu control. + + This creates a button and wrangles its pop-up menu. + """ + + def __init__(self, + parent: ba.Widget, + position: Tuple[float, float], + choices: Sequence[str], + current_choice: str = None, + on_value_change_call: Callable[[str], Any] = None, + opening_call: Callable[[], Any] = None, + closing_call: Callable[[], Any] = None, + width: float = 230.0, + maxwidth: float = None, + scale: float = None, + choices_disabled: Sequence[str] = None, + choices_display: Sequence[ba.Lstr] = None, + button_size: Tuple[float, float] = (160.0, 50.0), + autoselect: bool = True): + # pylint: disable=too-many-locals + if choices_disabled is None: + choices_disabled = [] + if choices_display is None: + choices_display = [] + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + if current_choice not in choices: + current_choice = None + self._choices = list(choices) + if not choices: + raise TypeError('no choices given') + self._choices_display = list(choices_display) + self._choices_disabled = list(choices_disabled) + self._width = width + self._maxwidth = maxwidth + self._scale = scale + self._current_choice = (current_choice if current_choice is not None + else self._choices[0]) + self._position = position + self._parent = parent + if not choices: + raise TypeError('Must pass at least one choice') + self._parent = parent + self._button_size = button_size + + self._button = ba.buttonwidget( + parent=self._parent, + position=(self._position[0], self._position[1]), + autoselect=autoselect, + size=self._button_size, + scale=1.0, + label='', + on_activate_call=lambda: ba.timer( + 0, self._make_popup, timetype=ba.TimeType.REAL)) + self._on_value_change_call = None # Don't wanna call for initial set. + self._opening_call = opening_call + self._autoselect = autoselect + self._closing_call = closing_call + self.set_choice(self._current_choice) + self._on_value_change_call = on_value_change_call + self._window_widget: Optional[ba.Widget] = None + + # Complain if we outlive our button. + ba.uicleanupcheck(self, self._button) + + def _make_popup(self) -> None: + if not self._button: + return + if self._opening_call: + self._opening_call() + self._window_widget = PopupMenuWindow( + position=self._button.get_screen_space_center(), + delegate=self, + width=self._width, + maxwidth=self._maxwidth, + scale=self._scale, + choices=self._choices, + current_choice=self._current_choice, + choices_disabled=self._choices_disabled, + choices_display=self._choices_display).root_widget + + def get_button(self) -> ba.Widget: + """Return the menu's button widget.""" + return self._button + + def get_window_widget(self) -> Optional[ba.Widget]: + """Return the menu's window widget (or None if nonexistent).""" + return self._window_widget + + def popup_menu_selected_choice(self, popup_window: PopupWindow, + choice: str) -> None: + """Called when a choice is selected.""" + del popup_window # Unused here. + self.set_choice(choice) + if self._on_value_change_call: + self._on_value_change_call(choice) + + def popup_menu_closing(self, popup_window: PopupWindow) -> None: + """Called when the menu is closing.""" + del popup_window # Unused here. + if self._button: + ba.containerwidget(edit=self._parent, selected_child=self._button) + self._window_widget = None + if self._closing_call: + self._closing_call() + + def set_choice(self, choice: str) -> None: + """Set the selected choice.""" + self._current_choice = choice + displayname: Union[str, ba.Lstr] + if len(self._choices_display) == len(self._choices): + displayname = self._choices_display[self._choices.index(choice)] + else: + displayname = choice + if self._button: + ba.buttonwidget(edit=self._button, label=displayname) diff --git a/dist/ba_data/python/bastd/ui/profile/__init__.py b/dist/ba_data/python/bastd/ui/profile/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/profile/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..3f06739 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6a2e789 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-38.pyc b/dist/ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-38.pyc new file mode 100644 index 0000000..2f5c40f Binary files /dev/null and b/dist/ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/profile/__pycache__/edit.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/profile/__pycache__/edit.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7d023bd Binary files /dev/null and b/dist/ba_data/python/bastd/ui/profile/__pycache__/edit.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/profile/__pycache__/upgrade.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/profile/__pycache__/upgrade.cpython-38.opt-1.pyc new file mode 100644 index 0000000..1d71f28 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/profile/__pycache__/upgrade.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/profile/browser.py b/dist/ba_data/python/bastd/ui/profile/browser.py new file mode 100644 index 0000000..e82fd29 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/profile/browser.py @@ -0,0 +1,376 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to browsing player profiles.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Optional, Tuple, List, Dict + + +class ProfileBrowserWindow(ba.Window): + """Window for browsing player profiles.""" + + def __init__(self, + transition: str = 'in_right', + in_main_menu: bool = True, + selected_profile: str = None, + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + self._in_main_menu = in_main_menu + if self._in_main_menu: + back_label = ba.Lstr(resource='backText') + else: + back_label = ba.Lstr(resource='doneText') + uiscale = ba.app.ui.uiscale + self._width = 700.0 if uiscale is ba.UIScale.SMALL else 600.0 + x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 + self._height = (360.0 if uiscale is ba.UIScale.SMALL else + 385.0 if uiscale is ba.UIScale.MEDIUM else 410.0) + + # If we're being called up standalone, handle pause/resume ourself. + if not self._in_main_menu: + ba.app.pause() + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + self._r = 'playerProfilesWindow' + + # Ensure we've got an account-profile in cases where we're signed in. + ba.app.accounts.ensure_have_account_player_profile() + + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + scale_origin_stack_offset=scale_origin, + scale=(2.2 if uiscale is ba.UIScale.SMALL else + 1.6 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -14) if uiscale is ba.UIScale.SMALL else (0, 0))) + + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(40 + x_inset, self._height - 59), + size=(120, 60), + scale=0.8, + label=back_label, + button_type='back' if self._in_main_menu else None, + autoselect=True, + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 36), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.titleText'), + maxwidth=300, + color=ba.app.ui.title_color, + scale=0.9, + h_align='center', + v_align='center') + + if self._in_main_menu: + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + scroll_height = self._height - 140.0 + self._scroll_width = self._width - (188 + x_inset * 2) + v = self._height - 84.0 + h = 50 + x_inset + b_color = (0.6, 0.53, 0.63) + + scl = (1.055 if uiscale is ba.UIScale.SMALL else + 1.18 if uiscale is ba.UIScale.MEDIUM else 1.3) + v -= 70.0 * scl + self._new_button = ba.buttonwidget(parent=self._root_widget, + position=(h, v), + size=(80, 66.0 * scl), + on_activate_call=self._new_profile, + color=b_color, + button_type='square', + autoselect=True, + textcolor=(0.75, 0.7, 0.8), + text_scale=0.7, + label=ba.Lstr(resource=self._r + + '.newButtonText')) + v -= 70.0 * scl + self._edit_button = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(80, 66.0 * scl), + on_activate_call=self._edit_profile, + color=b_color, + button_type='square', + autoselect=True, + textcolor=(0.75, 0.7, 0.8), + text_scale=0.7, + label=ba.Lstr(resource=self._r + '.editButtonText')) + v -= 70.0 * scl + self._delete_button = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(80, 66.0 * scl), + on_activate_call=self._delete_profile, + color=b_color, + button_type='square', + autoselect=True, + textcolor=(0.75, 0.7, 0.8), + text_scale=0.7, + label=ba.Lstr(resource=self._r + '.deleteButtonText')) + + v = self._height - 87 + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 71), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.explanationText'), + color=ba.app.ui.infotextcolor, + maxwidth=self._width * 0.83, + scale=0.6, + h_align='center', + v_align='center') + + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + highlight=False, + position=(140 + x_inset, + v - scroll_height), + size=(self._scroll_width, + scroll_height)) + ba.widget(edit=self._scrollwidget, + autoselect=True, + left_widget=self._new_button) + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + self._columnwidget = ba.columnwidget(parent=self._scrollwidget, + border=2, + margin=0) + v -= 255 + self._profiles: Optional[Dict[str, Dict[str, Any]]] = None + self._selected_profile = selected_profile + self._profile_widgets: List[ba.Widget] = [] + self._refresh() + self._restore_state() + + def _new_profile(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.profile.edit import EditProfileWindow + from bastd.ui.purchase import PurchaseWindow + + # Limit to a handful profiles if they don't have pro-options. + max_non_pro_profiles = _ba.get_account_misc_read_val('mnpp', 5) + assert self._profiles is not None + if (not ba.app.accounts.have_pro_options() + and len(self._profiles) >= max_non_pro_profiles): + PurchaseWindow(items=['pro'], + header_text=ba.Lstr( + resource='unlockThisProfilesText', + subs=[('${NUM}', str(max_non_pro_profiles))])) + return + + # Clamp at 100 profiles (otherwise the server will and that's less + # elegant looking). + if len(self._profiles) > 100: + ba.screenmessage( + ba.Lstr(translate=('serverResponses', + 'Max number of profiles reached.')), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + EditProfileWindow( + existing_profile=None, + in_main_menu=self._in_main_menu).get_root_widget()) + + def _delete_profile(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui import confirm + if self._selected_profile is None: + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource='nothingIsSelectedErrorText'), + color=(1, 0, 0)) + return + if self._selected_profile == '__account__': + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource=self._r + + '.cantDeleteAccountProfileText'), + color=(1, 0, 0)) + return + confirm.ConfirmWindow( + ba.Lstr(resource=self._r + '.deleteConfirmText', + subs=[('${PROFILE}', self._selected_profile)]), + self._do_delete_profile, 350) + + def _do_delete_profile(self) -> None: + _ba.add_transaction({ + 'type': 'REMOVE_PLAYER_PROFILE', + 'name': self._selected_profile + }) + _ba.run_transactions() + ba.playsound(ba.getsound('shieldDown')) + self._refresh() + + # Select profile list. + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + + def _edit_profile(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.profile.edit import EditProfileWindow + if self._selected_profile is None: + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource='nothingIsSelectedErrorText'), + color=(1, 0, 0)) + return + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + EditProfileWindow( + self._selected_profile, + in_main_menu=self._in_main_menu).get_root_widget()) + + def _select(self, name: str, index: int) -> None: + del index # Unused. + self._selected_profile = name + + def _back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account.settings import AccountSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + if self._in_main_menu: + ba.app.ui.set_main_menu_window( + AccountSettingsWindow(transition='in_left').get_root_widget()) + + # If we're being called up standalone, handle pause/resume ourself. + else: + ba.app.resume() + + def _refresh(self) -> None: + # pylint: disable=too-many-locals + from efro.util import asserttype + from ba.internal import (PlayerProfilesChangedMessage, + get_player_profile_colors, + get_player_profile_icon) + old_selection = self._selected_profile + + # Delete old. + while self._profile_widgets: + self._profile_widgets.pop().delete() + self._profiles = ba.app.config.get('Player Profiles', {}) + assert self._profiles is not None + items = list(self._profiles.items()) + items.sort(key=lambda x: asserttype(x[0], str).lower()) + index = 0 + account_name: Optional[str] + if _ba.get_account_state() == 'signed_in': + account_name = _ba.get_account_display_string() + else: + account_name = None + widget_to_select = None + for p_name, _ in items: + if p_name == '__account__' and account_name is None: + continue + color, _highlight = get_player_profile_colors(p_name) + scl = 1.1 + tval = (account_name if p_name == '__account__' else + get_player_profile_icon(p_name) + p_name) + assert isinstance(tval, str) + txtw = ba.textwidget( + parent=self._columnwidget, + position=(0, 32), + size=((self._width - 40) / scl, 28), + text=ba.Lstr(value=tval), + h_align='left', + v_align='center', + on_select_call=ba.WeakCall(self._select, p_name, index), + maxwidth=self._scroll_width * 0.92, + corner_scale=scl, + color=ba.safecolor(color, 0.4), + always_highlight=True, + on_activate_call=ba.Call(self._edit_button.activate), + selectable=True) + if index == 0: + ba.widget(edit=txtw, up_widget=self._back_button) + ba.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) + self._profile_widgets.append(txtw) + + # Select/show this one if it was previously selected + # (but defer till after this loop since our height is + # still changing). + if p_name == old_selection: + widget_to_select = txtw + + index += 1 + + if widget_to_select is not None: + ba.columnwidget(edit=self._columnwidget, + selected_child=widget_to_select, + visible_child=widget_to_select) + + # If there's a team-chooser in existence, tell it the profile-list + # has probably changed. + session = _ba.get_foreground_host_session() + if session is not None: + session.handlemessage(PlayerProfilesChangedMessage()) + + def _save_state(self) -> None: + try: + sel = self._root_widget.get_selected_child() + if sel == self._new_button: + sel_name = 'New' + elif sel == self._edit_button: + sel_name = 'Edit' + elif sel == self._delete_button: + sel_name = 'Delete' + elif sel == self._scrollwidget: + sel_name = 'Scroll' + else: + sel_name = 'Back' + ba.app.ui.window_states[type(self)] = sel_name + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + def _restore_state(self) -> None: + try: + sel_name = ba.app.ui.window_states.get(type(self)) + if sel_name == 'Scroll': + sel = self._scrollwidget + elif sel_name == 'New': + sel = self._new_button + elif sel_name == 'Delete': + sel = self._delete_button + elif sel_name == 'Edit': + sel = self._edit_button + elif sel_name == 'Back': + sel = self._back_button + else: + # By default we select our scroll widget if we have profiles; + # otherwise our new widget. + if not self._profile_widgets: + sel = self._new_button + else: + sel = self._scrollwidget + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') diff --git a/dist/ba_data/python/bastd/ui/profile/edit.py b/dist/ba_data/python/bastd/ui/profile/edit.py new file mode 100644 index 0000000..c63cd16 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/profile/edit.py @@ -0,0 +1,673 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI to edit a player profile.""" + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING, cast + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Tuple, Optional, List + from bastd.ui.colorpicker import ColorPicker + + +class EditProfileWindow(ba.Window): + """Window for editing a player profile.""" + + # FIXME: WILL NEED TO CHANGE THIS FOR UILOCATION. + def reload_window(self) -> None: + """Transitions out and recreates ourself.""" + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + EditProfileWindow(self.getname(), + self._in_main_menu).get_root_widget()) + + def __init__(self, + existing_profile: Optional[str], + in_main_menu: bool, + transition: str = 'in_right'): + # FIXME: Tidy this up a bit. + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + from ba.internal import get_player_profile_colors + self._in_main_menu = in_main_menu + self._existing_profile = existing_profile + self._r = 'editProfileWindow' + self._spazzes: List[str] = [] + self._icon_textures: List[ba.Texture] = [] + self._icon_tint_textures: List[ba.Texture] = [] + + # Grab profile colors or pick random ones. + self._color, self._highlight = get_player_profile_colors( + existing_profile) + uiscale = ba.app.ui.uiscale + self._width = width = 780.0 if uiscale is ba.UIScale.SMALL else 680.0 + self._x_inset = x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 + self._height = height = ( + 350.0 if uiscale is ba.UIScale.SMALL else + 400.0 if uiscale is ba.UIScale.MEDIUM else 450.0) + spacing = 40 + self._base_scale = (2.05 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0) + top_extra = 15 if uiscale is ba.UIScale.SMALL else 15 + super().__init__(root_widget=ba.containerwidget( + size=(width, height + top_extra), + transition=transition, + scale=self._base_scale, + stack_offset=(0, 15) if uiscale is ba.UIScale.SMALL else (0, 0))) + cancel_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(52 + x_inset, height - 60), + size=(155, 60), + scale=0.8, + autoselect=True, + label=ba.Lstr(resource='cancelText'), + on_activate_call=self._cancel) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + save_button = btn = ba.buttonwidget(parent=self._root_widget, + position=(width - (177 + x_inset), + height - 60), + size=(155, 60), + autoselect=True, + scale=0.8, + label=ba.Lstr(resource='saveText')) + ba.widget(edit=save_button, left_widget=cancel_button) + ba.widget(edit=cancel_button, right_widget=save_button) + ba.containerwidget(edit=self._root_widget, start_button=btn) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, height - 38), + size=(0, 0), + text=(ba.Lstr(resource=self._r + '.titleNewText') + if existing_profile is None else ba.Lstr( + resource=self._r + '.titleEditText')), + color=ba.app.ui.title_color, + maxwidth=290, + scale=1.0, + h_align='center', + v_align='center') + + # Make a list of spaz icons. + self.refresh_characters() + profile = ba.app.config.get('Player Profiles', + {}).get(self._existing_profile, {}) + + if 'global' in profile: + self._global = profile['global'] + else: + self._global = False + + if 'icon' in profile: + self._icon = profile['icon'] + else: + self._icon = ba.charstr(ba.SpecialChar.LOGO) + + assigned_random_char = False + + # Look for existing character choice or pick random one otherwise. + try: + icon_index = self._spazzes.index(profile['character']) + except Exception: + # Let's set the default icon to spaz for our first profile; after + # that we go random. + # (SCRATCH THAT.. we now hard-code account-profiles to start with + # spaz which has a similar effect) + # try: p_len = len(ba.app.config['Player Profiles']) + # except Exception: p_len = 0 + # if p_len == 0: icon_index = self._spazzes.index('Spaz') + # else: + random.seed() + icon_index = random.randrange(len(self._spazzes)) + assigned_random_char = True + self._icon_index = icon_index + ba.buttonwidget(edit=save_button, on_activate_call=self.save) + + v = height - 115.0 + self._name = ('' if self._existing_profile is None else + self._existing_profile) + self._is_account_profile = (self._name == '__account__') + + # If we just picked a random character, see if it has specific + # colors/highlights associated with it and assign them if so. + if assigned_random_char: + clr = ba.app.spaz_appearances[ + self._spazzes[icon_index]].default_color + if clr is not None: + self._color = clr + highlight = ba.app.spaz_appearances[ + self._spazzes[icon_index]].default_highlight + if highlight is not None: + self._highlight = highlight + + # Assign a random name if they had none. + if self._name == '': + names = _ba.get_random_names() + self._name = names[random.randrange(len(names))] + + self._clipped_name_text = ba.textwidget(parent=self._root_widget, + text='', + position=(540 + x_inset, + v - 8), + flatness=1.0, + shadow=0.0, + scale=0.55, + size=(0, 0), + maxwidth=100, + h_align='center', + v_align='center', + color=(1, 1, 0, 0.5)) + + if not self._is_account_profile and not self._global: + ba.textwidget(parent=self._root_widget, + text=ba.Lstr(resource=self._r + '.nameText'), + position=(200 + x_inset, v - 6), + size=(0, 0), + h_align='right', + v_align='center', + color=(1, 1, 1, 0.5), + scale=0.9) + + self._upgrade_button = None + if self._is_account_profile: + if _ba.get_account_state() == 'signed_in': + sval = _ba.get_account_display_string() + else: + sval = '??' + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, v - 7), + size=(0, 0), + scale=1.2, + text=sval, + maxwidth=270, + h_align='center', + v_align='center') + txtl = ba.Lstr( + resource='editProfileWindow.accountProfileText').evaluate() + b_width = min( + 270.0, + _ba.get_string_width(txtl, suppress_warning=True) * 0.6) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, v - 39), + size=(0, 0), + scale=0.6, + color=ba.app.ui.infotextcolor, + text=txtl, + maxwidth=270, + h_align='center', + v_align='center') + self._account_type_info_button = ba.buttonwidget( + parent=self._root_widget, + label='?', + size=(15, 15), + text_scale=0.6, + position=(self._width * 0.5 + b_width * 0.5 + 13, v - 47), + button_type='square', + color=(0.6, 0.5, 0.65), + autoselect=True, + on_activate_call=self.show_account_profile_info) + elif self._global: + + b_size = 60 + self._icon_button = btn = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(self._width * 0.5 - 160 - b_size * 0.5, v - 38 - 15), + size=(b_size, b_size), + color=(0.6, 0.5, 0.6), + label='', + button_type='square', + text_scale=1.2, + on_activate_call=self._on_icon_press) + self._icon_button_label = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5 - 160, v - 35), + draw_controller=btn, + h_align='center', + v_align='center', + size=(0, 0), + color=(1, 1, 1), + text='', + scale=2.0) + + ba.textwidget(parent=self._root_widget, + h_align='center', + v_align='center', + position=(self._width * 0.5 - 160, v - 55 - 15), + size=(0, 0), + draw_controller=btn, + text=ba.Lstr(resource=self._r + '.iconText'), + scale=0.7, + color=ba.app.ui.title_color, + maxwidth=120) + + self._update_icon() + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, v - 7), + size=(0, 0), + scale=1.2, + text=self._name, + maxwidth=240, + h_align='center', + v_align='center') + # FIXME hard coded strings are bad + txtl = ba.Lstr( + resource='editProfileWindow.globalProfileText').evaluate() + b_width = min( + 240.0, + _ba.get_string_width(txtl, suppress_warning=True) * 0.6) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, v - 39), + size=(0, 0), + scale=0.6, + color=ba.app.ui.infotextcolor, + text=txtl, + maxwidth=240, + h_align='center', + v_align='center') + self._account_type_info_button = ba.buttonwidget( + parent=self._root_widget, + label='?', + size=(15, 15), + text_scale=0.6, + position=(self._width * 0.5 + b_width * 0.5 + 13, v - 47), + button_type='square', + color=(0.6, 0.5, 0.65), + autoselect=True, + on_activate_call=self.show_global_profile_info) + else: + self._text_field = ba.textwidget( + parent=self._root_widget, + position=(220 + x_inset, v - 30), + size=(265, 40), + text=self._name, + h_align='left', + v_align='center', + max_chars=16, + description=ba.Lstr(resource=self._r + '.nameDescriptionText'), + autoselect=True, + editable=True, + padding=4, + color=(0.9, 0.9, 0.9, 1.0), + on_return_press_call=ba.Call(save_button.activate)) + + # FIXME hard coded strings are bad + txtl = ba.Lstr( + resource='editProfileWindow.localProfileText').evaluate() + b_width = min( + 270.0, + _ba.get_string_width(txtl, suppress_warning=True) * 0.6) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, v - 43), + size=(0, 0), + scale=0.6, + color=ba.app.ui.infotextcolor, + text=txtl, + maxwidth=270, + h_align='center', + v_align='center') + self._account_type_info_button = ba.buttonwidget( + parent=self._root_widget, + label='?', + size=(15, 15), + text_scale=0.6, + position=(self._width * 0.5 + b_width * 0.5 + 13, v - 50), + button_type='square', + color=(0.6, 0.5, 0.65), + autoselect=True, + on_activate_call=self.show_local_profile_info) + self._upgrade_button = ba.buttonwidget( + parent=self._root_widget, + label=ba.Lstr(resource='upgradeText'), + size=(40, 17), + text_scale=1.0, + button_type='square', + position=(self._width * 0.5 + b_width * 0.5 + 13 + 43, v - 51), + color=(0.6, 0.5, 0.65), + autoselect=True, + on_activate_call=self.upgrade_profile) + + self._update_clipped_name() + self._clipped_name_timer = ba.Timer(0.333, + ba.WeakCall( + self._update_clipped_name), + timetype=ba.TimeType.REAL, + repeat=True) + + v -= spacing * 3.0 + b_size = 80 + b_size_2 = 100 + b_offs = 150 + self._color_button = btn = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(self._width * 0.5 - b_offs - b_size * 0.5, v - 50), + size=(b_size, b_size), + color=self._color, + label='', + button_type='square') + origin = self._color_button.get_screen_space_center() + ba.buttonwidget(edit=self._color_button, + on_activate_call=ba.WeakCall(self._make_picker, + 'color', origin)) + ba.textwidget(parent=self._root_widget, + h_align='center', + v_align='center', + position=(self._width * 0.5 - b_offs, v - 65), + size=(0, 0), + draw_controller=btn, + text=ba.Lstr(resource=self._r + '.colorText'), + scale=0.7, + color=ba.app.ui.title_color, + maxwidth=120) + + self._character_button = btn = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(self._width * 0.5 - b_size_2 * 0.5, v - 60), + up_widget=self._account_type_info_button, + on_activate_call=self._on_character_press, + size=(b_size_2, b_size_2), + label='', + color=(1, 1, 1), + mask_texture=ba.gettexture('characterIconMask')) + if not self._is_account_profile and not self._global: + ba.containerwidget(edit=self._root_widget, + selected_child=self._text_field) + ba.textwidget(parent=self._root_widget, + h_align='center', + v_align='center', + position=(self._width * 0.5, v - 80), + size=(0, 0), + draw_controller=btn, + text=ba.Lstr(resource=self._r + '.characterText'), + scale=0.7, + color=ba.app.ui.title_color, + maxwidth=130) + + self._highlight_button = btn = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(self._width * 0.5 + b_offs - b_size * 0.5, v - 50), + up_widget=self._upgrade_button if self._upgrade_button is not None + else self._account_type_info_button, + size=(b_size, b_size), + color=self._highlight, + label='', + button_type='square') + + if not self._is_account_profile and not self._global: + ba.widget(edit=cancel_button, down_widget=self._text_field) + ba.widget(edit=save_button, down_widget=self._text_field) + ba.widget(edit=self._color_button, up_widget=self._text_field) + ba.widget(edit=self._account_type_info_button, + down_widget=self._character_button) + + origin = self._highlight_button.get_screen_space_center() + ba.buttonwidget(edit=self._highlight_button, + on_activate_call=ba.WeakCall(self._make_picker, + 'highlight', origin)) + ba.textwidget(parent=self._root_widget, + h_align='center', + v_align='center', + position=(self._width * 0.5 + b_offs, v - 65), + size=(0, 0), + draw_controller=btn, + text=ba.Lstr(resource=self._r + '.highlightText'), + scale=0.7, + color=ba.app.ui.title_color, + maxwidth=120) + self._update_character() + + def upgrade_profile(self) -> None: + """Attempt to ugrade the profile to global.""" + from bastd.ui import account + from bastd.ui.profile import upgrade as pupgrade + if _ba.get_account_state() != 'signed_in': + account.show_sign_in_prompt() + return + + pupgrade.ProfileUpgradeWindow(self) + + def show_account_profile_info(self) -> None: + """Show an explanation of account profiles.""" + from bastd.ui.confirm import ConfirmWindow + icons_str = ' '.join([ + ba.charstr(n) for n in [ + ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO, + ba.SpecialChar.GAME_CENTER_LOGO, + ba.SpecialChar.GAME_CIRCLE_LOGO, ba.SpecialChar.OUYA_LOGO, + ba.SpecialChar.LOCAL_ACCOUNT, ba.SpecialChar.ALIBABA_LOGO, + ba.SpecialChar.OCULUS_LOGO, ba.SpecialChar.NVIDIA_LOGO + ] + ]) + txtl = ba.Lstr(resource='editProfileWindow.accountProfileInfoText', + subs=[('${ICONS}', icons_str)]) + ConfirmWindow(txtl, + cancel_button=False, + width=500, + height=300, + origin_widget=self._account_type_info_button) + + def show_local_profile_info(self) -> None: + """Show an explanation of local profiles.""" + from bastd.ui.confirm import ConfirmWindow + txtl = ba.Lstr(resource='editProfileWindow.localProfileInfoText') + ConfirmWindow(txtl, + cancel_button=False, + width=600, + height=250, + origin_widget=self._account_type_info_button) + + def show_global_profile_info(self) -> None: + """Show an explanation of global profiles.""" + from bastd.ui.confirm import ConfirmWindow + txtl = ba.Lstr(resource='editProfileWindow.globalProfileInfoText') + ConfirmWindow(txtl, + cancel_button=False, + width=600, + height=250, + origin_widget=self._account_type_info_button) + + def refresh_characters(self) -> None: + """Refresh available characters/icons.""" + from bastd.actor import spazappearance + self._spazzes = spazappearance.get_appearances() + self._spazzes.sort() + self._icon_textures = [ + ba.gettexture(ba.app.spaz_appearances[s].icon_texture) + for s in self._spazzes + ] + self._icon_tint_textures = [ + ba.gettexture(ba.app.spaz_appearances[s].icon_mask_texture) + for s in self._spazzes + ] + + def on_icon_picker_pick(self, icon: str) -> None: + """An icon has been selected by the picker.""" + self._icon = icon + self._update_icon() + + def on_character_picker_pick(self, character: str) -> None: + """A character has been selected by the picker.""" + if not self._root_widget: + return + + # The player could have bought a new one while the picker was up. + self.refresh_characters() + self._icon_index = self._spazzes.index( + character) if character in self._spazzes else 0 + self._update_character() + + def _on_character_press(self) -> None: + from bastd.ui import characterpicker + characterpicker.CharacterPicker( + parent=self._root_widget, + position=self._character_button.get_screen_space_center(), + selected_character=self._spazzes[self._icon_index], + delegate=self, + tint_color=self._color, + tint2_color=self._highlight) + + def _on_icon_press(self) -> None: + from bastd.ui import iconpicker + iconpicker.IconPicker( + parent=self._root_widget, + position=self._icon_button.get_screen_space_center(), + selected_icon=self._icon, + delegate=self, + tint_color=self._color, + tint2_color=self._highlight) + + def _make_picker(self, picker_type: str, origin: Tuple[float, + float]) -> None: + from bastd.ui import colorpicker + if picker_type == 'color': + initial_color = self._color + elif picker_type == 'highlight': + initial_color = self._highlight + else: + raise ValueError('invalid picker_type: ' + picker_type) + colorpicker.ColorPicker( + parent=self._root_widget, + position=origin, + offset=(self._base_scale * + (-100 if picker_type == 'color' else 100), 0), + initial_color=initial_color, + delegate=self, + tag=picker_type) + + def _cancel(self) -> None: + from bastd.ui.profile.browser import ProfileBrowserWindow + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + ProfileBrowserWindow( + 'in_left', + selected_profile=self._existing_profile, + in_main_menu=self._in_main_menu).get_root_widget()) + + def _set_color(self, color: Tuple[float, float, float]) -> None: + self._color = color + if self._color_button: + ba.buttonwidget(edit=self._color_button, color=color) + + def _set_highlight(self, color: Tuple[float, float, float]) -> None: + self._highlight = color + if self._highlight_button: + ba.buttonwidget(edit=self._highlight_button, color=color) + + def color_picker_closing(self, picker: ColorPicker) -> None: + """Called when a color picker is closing.""" + if not self._root_widget: + return + tag = picker.get_tag() + if tag == 'color': + ba.containerwidget(edit=self._root_widget, + selected_child=self._color_button) + elif tag == 'highlight': + ba.containerwidget(edit=self._root_widget, + selected_child=self._highlight_button) + else: + print('color_picker_closing got unknown tag ' + str(tag)) + + def color_picker_selected_color(self, picker: ColorPicker, + color: Tuple[float, float, float]) -> None: + """Called when a color is selected in a color picker.""" + if not self._root_widget: + return + tag = picker.get_tag() + if tag == 'color': + self._set_color(color) + elif tag == 'highlight': + self._set_highlight(color) + else: + print('color_picker_selected_color got unknown tag ' + str(tag)) + self._update_character() + + def _update_clipped_name(self) -> None: + if not self._clipped_name_text: + return + name = self.getname() + if name == '__account__': + name = (_ba.get_account_name() + if _ba.get_account_state() == 'signed_in' else '???') + if len(name) > 10 and not (self._global or self._is_account_profile): + ba.textwidget(edit=self._clipped_name_text, + text=ba.Lstr(resource='inGameClippedNameText', + subs=[('${NAME}', name[:10] + '...')])) + else: + ba.textwidget(edit=self._clipped_name_text, text='') + + def _update_character(self, change: int = 0) -> None: + self._icon_index = (self._icon_index + change) % len(self._spazzes) + if self._character_button: + ba.buttonwidget( + edit=self._character_button, + texture=self._icon_textures[self._icon_index], + tint_texture=self._icon_tint_textures[self._icon_index], + tint_color=self._color, + tint2_color=self._highlight) + + def _update_icon(self) -> None: + if self._icon_button_label: + ba.textwidget(edit=self._icon_button_label, text=self._icon) + + def getname(self) -> str: + """Return the current profile name value.""" + if self._is_account_profile: + new_name = '__account__' + elif self._global: + new_name = self._name + else: + new_name = cast(str, ba.textwidget(query=self._text_field)) + return new_name + + def save(self, transition_out: bool = True) -> bool: + """Save has been selected.""" + from bastd.ui.profile.browser import ProfileBrowserWindow + new_name = self.getname().strip() + + if not new_name: + ba.screenmessage(ba.Lstr(resource='nameNotEmptyText')) + ba.playsound(ba.getsound('error')) + return False + + if transition_out: + ba.playsound(ba.getsound('gunCocking')) + + # Delete old in case we're renaming. + if self._existing_profile and self._existing_profile != new_name: + _ba.add_transaction({ + 'type': 'REMOVE_PLAYER_PROFILE', + 'name': self._existing_profile + }) + + # Also lets be aware we're no longer global if we're taking a + # new name (will need to re-request it). + self._global = False + + _ba.add_transaction({ + 'type': 'ADD_PLAYER_PROFILE', + 'name': new_name, + 'profile': { + 'character': self._spazzes[self._icon_index], + 'color': list(self._color), + 'global': self._global, + 'icon': self._icon, + 'highlight': list(self._highlight) + } + }) + + if transition_out: + _ba.run_transactions() + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + ProfileBrowserWindow( + 'in_left', + selected_profile=new_name, + in_main_menu=self._in_main_menu).get_root_widget()) + return True diff --git a/dist/ba_data/python/bastd/ui/profile/upgrade.py b/dist/ba_data/python/bastd/ui/profile/upgrade.py new file mode 100644 index 0000000..c28777f --- /dev/null +++ b/dist/ba_data/python/bastd/ui/profile/upgrade.py @@ -0,0 +1,239 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI for player profile upgrades.""" + +from __future__ import annotations + +import time +import weakref +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Optional, Dict + from bastd.ui.profile.edit import EditProfileWindow + + +class ProfileUpgradeWindow(ba.Window): + """Window for player profile upgrades to global.""" + + def __init__(self, + edit_profile_window: EditProfileWindow, + transition: str = 'in_right'): + from ba.internal import master_server_get + self._r = 'editProfileWindow' + + self._width = 680 + self._height = 350 + uiscale = ba.app.ui.uiscale + self._base_scale = (2.05 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.2) + self._upgrade_start_time: Optional[float] = None + self._name = edit_profile_window.getname() + self._edit_profile_window = weakref.ref(edit_profile_window) + + top_extra = 15 if uiscale is ba.UIScale.SMALL else 15 + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + toolbar_visibility='menu_currency', + transition=transition, + scale=self._base_scale, + stack_offset=(0, 15) if uiscale is ba.UIScale.SMALL else (0, 0))) + cancel_button = ba.buttonwidget(parent=self._root_widget, + position=(52, 30), + size=(155, 60), + scale=0.8, + autoselect=True, + label=ba.Lstr(resource='cancelText'), + on_activate_call=self._cancel) + self._upgrade_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - 190, 30), + size=(155, 60), + scale=0.8, + autoselect=True, + label=ba.Lstr(resource='upgradeText'), + on_activate_call=self._on_upgrade_press) + ba.containerwidget(edit=self._root_widget, + cancel_button=cancel_button, + start_button=self._upgrade_button, + selected_child=self._upgrade_button) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 38), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.upgradeToGlobalProfileText'), + color=ba.app.ui.title_color, + maxwidth=self._width * 0.45, + scale=1.0, + h_align='center', + v_align='center') + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 100), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.upgradeProfileInfoText'), + color=ba.app.ui.infotextcolor, + maxwidth=self._width * 0.8, + scale=0.7, + h_align='center', + v_align='center') + + self._status_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 160), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.checkingAvailabilityText', + subs=[('${NAME}', self._name)]), + color=(0.8, 0.4, 0.0), + maxwidth=self._width * 0.8, + scale=0.65, + h_align='center', + v_align='center') + + self._price_text = ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, + self._height - 230), + size=(0, 0), + text='', + color=(0.2, 1, 0.2), + maxwidth=self._width * 0.8, + scale=1.5, + h_align='center', + v_align='center') + + self._tickets_text: Optional[ba.Widget] + if not ba.app.ui.use_toolbars: + self._tickets_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.9 - 5, self._height - 30), + size=(0, 0), + text=ba.charstr(ba.SpecialChar.TICKET) + '123', + color=(0.2, 1, 0.2), + maxwidth=100, + scale=0.5, + h_align='right', + v_align='center') + else: + self._tickets_text = None + + master_server_get('bsGlobalProfileCheck', { + 'name': self._name, + 'b': ba.app.build_number + }, + callback=ba.WeakCall(self._profile_check_result)) + self._cost = _ba.get_account_misc_read_val('price.global_profile', 500) + self._status: Optional[str] = 'waiting' + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + self._update() + + def _profile_check_result(self, result: Optional[Dict[str, Any]]) -> None: + if result is None: + ba.textwidget( + edit=self._status_text, + text=ba.Lstr(resource='internal.unavailableNoConnectionText'), + color=(1, 0, 0)) + self._status = 'error' + ba.buttonwidget(edit=self._upgrade_button, + color=(0.4, 0.4, 0.4), + textcolor=(0.5, 0.5, 0.5)) + else: + if result['available']: + ba.textwidget(edit=self._status_text, + text=ba.Lstr(resource=self._r + '.availableText', + subs=[('${NAME}', self._name)]), + color=(0, 1, 0)) + ba.textwidget(edit=self._price_text, + text=ba.charstr(ba.SpecialChar.TICKET) + + str(self._cost)) + self._status = None + else: + ba.textwidget(edit=self._status_text, + text=ba.Lstr(resource=self._r + + '.unavailableText', + subs=[('${NAME}', self._name)]), + color=(1, 0, 0)) + self._status = 'unavailable' + ba.buttonwidget(edit=self._upgrade_button, + color=(0.4, 0.4, 0.4), + textcolor=(0.5, 0.5, 0.5)) + + def _on_upgrade_press(self) -> None: + from bastd.ui import getcurrency + if self._status is None: + # If it appears we don't have enough tickets, offer to buy more. + tickets = _ba.get_account_ticket_count() + if tickets < self._cost: + ba.playsound(ba.getsound('error')) + getcurrency.show_get_tickets_prompt() + return + ba.screenmessage(ba.Lstr(resource='purchasingText'), + color=(0, 1, 0)) + self._status = 'pre_upgrading' + + # Now we tell the original editor to save the profile, add an + # upgrade transaction, and then sit and wait for everything to + # go through. + edit_profile_window = self._edit_profile_window() + if edit_profile_window is None: + print('profile upgrade: original edit window gone') + return + success = edit_profile_window.save(transition_out=False) + if not success: + print('profile upgrade: error occurred saving profile') + ba.screenmessage(ba.Lstr(resource='errorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + _ba.add_transaction({ + 'type': 'UPGRADE_PROFILE', + 'name': self._name + }) + _ba.run_transactions() + self._status = 'upgrading' + self._upgrade_start_time = time.time() + else: + ba.playsound(ba.getsound('error')) + + def _update(self) -> None: + try: + t_str = str(_ba.get_account_ticket_count()) + except Exception: + t_str = '?' + if self._tickets_text is not None: + ba.textwidget(edit=self._tickets_text, + text=ba.Lstr( + resource='getTicketsWindow.youHaveShortText', + subs=[('${COUNT}', + ba.charstr(ba.SpecialChar.TICKET) + t_str) + ])) + + # Once we've kicked off an upgrade attempt and all transactions go + # through, we're done. + if (self._status == 'upgrading' + and not _ba.have_outstanding_transactions()): + self._status = 'exiting' + ba.containerwidget(edit=self._root_widget, transition='out_right') + edit_profile_window = self._edit_profile_window() + if edit_profile_window is None: + print('profile upgrade transition out:' + ' original edit window gone') + return + ba.playsound(ba.getsound('gunCocking')) + edit_profile_window.reload_window() + + def _cancel(self) -> None: + # If we recently sent out an upgrade request, disallow canceling + # for a bit. + if (self._upgrade_start_time is not None + and time.time() - self._upgrade_start_time < 10.0): + ba.playsound(ba.getsound('error')) + return + ba.containerwidget(edit=self._root_widget, transition='out_right') diff --git a/dist/ba_data/python/bastd/ui/promocode.py b/dist/ba_data/python/bastd/ui/promocode.py new file mode 100644 index 0000000..dfd1fdd --- /dev/null +++ b/dist/ba_data/python/bastd/ui/promocode.py @@ -0,0 +1,118 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for entering promo codes.""" + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Optional, Tuple + + +class PromoCodeWindow(ba.Window): + """Window for entering promo codes.""" + + def __init__(self, modal: bool = False, origin_widget: ba.Widget = None): + + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + transition = 'in_right' + + width = 450 + height = 230 + + self._modal = modal + self._r = 'promoCodeWindow' + + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition=transition, + toolbar_visibility='menu_minimal_no_back', + scale_origin_stack_offset=scale_origin, + scale=(2.0 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0))) + + btn = ba.buttonwidget(parent=self._root_widget, + scale=0.5, + position=(40, height - 40), + size=(60, 60), + label='', + on_activate_call=self._do_back, + autoselect=True, + color=(0.55, 0.5, 0.6), + icon=ba.gettexture('crossOut'), + iconscale=1.2) + + ba.textwidget(parent=self._root_widget, + text=ba.Lstr(resource=self._r + '.codeText'), + position=(22, height - 113), + color=(0.8, 0.8, 0.8, 1.0), + size=(90, 30), + h_align='right') + self._text_field = ba.textwidget( + parent=self._root_widget, + position=(125, height - 121), + size=(280, 46), + text='', + h_align='left', + v_align='center', + max_chars=64, + color=(0.9, 0.9, 0.9, 1.0), + description=ba.Lstr(resource=self._r + '.codeText'), + editable=True, + padding=4, + on_return_press_call=self._activate_enter_button) + ba.widget(edit=btn, down_widget=self._text_field) + + b_width = 200 + self._enter_button = btn2 = ba.buttonwidget( + parent=self._root_widget, + position=(width * 0.5 - b_width * 0.5, height - 200), + size=(b_width, 60), + scale=1.0, + label=ba.Lstr(resource='submitText', + fallback_resource=self._r + '.enterText'), + on_activate_call=self._do_enter) + ba.containerwidget(edit=self._root_widget, + cancel_button=btn, + start_button=btn2, + selected_child=self._text_field) + + def _do_back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.advanced import AdvancedSettingsWindow + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + if not self._modal: + ba.app.ui.set_main_menu_window( + AdvancedSettingsWindow(transition='in_left').get_root_widget()) + + def _activate_enter_button(self) -> None: + self._enter_button.activate() + + def _do_enter(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.advanced import AdvancedSettingsWindow + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + if not self._modal: + ba.app.ui.set_main_menu_window( + AdvancedSettingsWindow(transition='in_left').get_root_widget()) + _ba.add_transaction({ + 'type': 'PROMO_CODE', + 'expire_time': time.time() + 5, + 'code': ba.textwidget(query=self._text_field) + }) + _ba.run_transactions() diff --git a/dist/ba_data/python/bastd/ui/purchase.py b/dist/ba_data/python/bastd/ui/purchase.py new file mode 100644 index 0000000..519fb48 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/purchase.py @@ -0,0 +1,152 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI related to purchasing items.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Dict, List, Optional + + +class PurchaseWindow(ba.Window): + """Window for purchasing one or more items.""" + + def __init__(self, + items: List[str], + transition: str = 'in_right', + header_text: ba.Lstr = None): + from ba.internal import get_store_item_display_size + from bastd.ui.store import item as storeitemui + if header_text is None: + header_text = ba.Lstr(resource='unlockThisText', + fallback_resource='unlockThisInTheStoreText') + if len(items) != 1: + raise ValueError('expected exactly 1 item') + self._items = list(items) + self._width = 580 + self._height = 520 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition=transition, + toolbar_visibility='menu_currency', + scale=(1.2 if uiscale is ba.UIScale.SMALL else + 1.1 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -15) if uiscale is ba.UIScale.SMALL else (0, 0))) + self._is_double = False + self._title_text = ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, + self._height - 30), + size=(0, 0), + text=header_text, + h_align='center', + v_align='center', + maxwidth=self._width * 0.9 - 120, + scale=1.2, + color=(1, 0.8, 0.3, 1)) + size = get_store_item_display_size(items[0]) + display: Dict[str, Any] = {} + storeitemui.instantiate_store_item_display( + items[0], + display, + parent_widget=self._root_widget, + b_pos=(self._width * 0.5 - size[0] * 0.5 + 10 - + ((size[0] * 0.5 + 30) if self._is_double else 0), + self._height * 0.5 - size[1] * 0.5 + 30 + + (20 if self._is_double else 0)), + b_width=size[0], + b_height=size[1], + button=False) + + # Wire up the parts we need. + if self._is_double: + pass # not working + else: + if self._items == ['pro']: + price_str = _ba.get_price(self._items[0]) + pyoffs = -15 + else: + pyoffs = 0 + price = self._price = _ba.get_account_misc_read_val( + 'price.' + str(items[0]), -1) + price_str = ba.charstr(ba.SpecialChar.TICKET) + str(price) + self._price_text = ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, + 150 + pyoffs), + size=(0, 0), + text=price_str, + h_align='center', + v_align='center', + maxwidth=self._width * 0.9, + scale=1.4, + color=(0.2, 1, 0.2)) + + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + + self._cancel_button = ba.buttonwidget( + parent=self._root_widget, + position=(50, 40), + size=(150, 60), + scale=1.0, + on_activate_call=self._cancel, + autoselect=True, + label=ba.Lstr(resource='cancelText')) + self._purchase_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - 200, 40), + size=(150, 60), + scale=1.0, + on_activate_call=self._purchase, + autoselect=True, + label=ba.Lstr(resource='store.purchaseText')) + + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button, + start_button=self._purchase_button, + selected_child=self._purchase_button) + + def _update(self) -> None: + can_die = False + + # We go away if we see that our target item is owned. + if self._items == ['pro']: + if ba.app.accounts.have_pro(): + can_die = True + else: + if _ba.get_purchased(self._items[0]): + can_die = True + + if can_die: + ba.containerwidget(edit=self._root_widget, transition='out_left') + + def _purchase(self) -> None: + from bastd.ui import getcurrency + if self._items == ['pro']: + _ba.purchase('pro') + else: + ticket_count: Optional[int] + try: + ticket_count = _ba.get_account_ticket_count() + except Exception: + ticket_count = None + if ticket_count is not None and ticket_count < self._price: + getcurrency.show_get_tickets_prompt() + ba.playsound(ba.getsound('error')) + return + + def do_it() -> None: + _ba.in_game_purchase(self._items[0], self._price) + + ba.playsound(ba.getsound('swish')) + do_it() + + def _cancel(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_right') diff --git a/dist/ba_data/python/bastd/ui/qrcode.py b/dist/ba_data/python/bastd/ui/qrcode.py new file mode 100644 index 0000000..16efd5d --- /dev/null +++ b/dist/ba_data/python/bastd/ui/qrcode.py @@ -0,0 +1,55 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides functionality for displaying QR codes.""" +from __future__ import annotations + +import ba +from bastd.ui import popup + + +class QRCodeWindow(popup.PopupWindow): + """Popup window that shows a QR code.""" + + def __init__(self, origin_widget: ba.Widget, qr_tex: ba.Texture): + + position = origin_widget.get_screen_space_center() + uiscale = ba.app.ui.uiscale + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + self._transitioning_out = False + self._width = 450 + self._height = 400 + bg_color = (0.5, 0.4, 0.6) + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=bg_color) + self._cancel_button = ba.buttonwidget( + parent=self.root_widget, + position=(50, self._height - 30), + size=(50, 50), + scale=0.5, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.imagewidget(parent=self.root_widget, + position=(self._width * 0.5 - 150, + self._height * 0.5 - 150), + size=(300, 300), + texture=qr_tex) + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/radiogroup.py b/dist/ba_data/python/bastd/ui/radiogroup.py new file mode 100644 index 0000000..20d3f9c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/radiogroup.py @@ -0,0 +1,33 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for creating radio groups of buttons.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import List, Any, Callable, Sequence + + +def make_radio_group(check_boxes: Sequence[ba.Widget], + value_names: Sequence[str], value: str, + value_change_call: Callable[[str], Any]) -> None: + """Link the provided check_boxes together into a radio group.""" + + def _radio_press(check_string: str, other_check_boxes: List[ba.Widget], + val: int) -> None: + if val == 1: + value_change_call(check_string) + for cbx in other_check_boxes: + ba.checkboxwidget(edit=cbx, value=False) + + for i, check_box in enumerate(check_boxes): + ba.checkboxwidget(edit=check_box, + value=(value == value_names[i]), + is_radio_button=True, + on_value_change_call=ba.Call( + _radio_press, value_names[i], + [c for c in check_boxes if c != check_box])) diff --git a/dist/ba_data/python/bastd/ui/report.py b/dist/ba_data/python/bastd/ui/report.py new file mode 100644 index 0000000..1c76115 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/report.py @@ -0,0 +1,92 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI related to reporting bad behavior/etc.""" + +from __future__ import annotations + +import _ba +import ba + + +class ReportPlayerWindow(ba.Window): + """Player for reporting naughty players.""" + + def __init__(self, account_id: str, origin_widget: ba.Widget): + self._width = 550 + self._height = 220 + self._account_id = account_id + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + + overlay_stack = _ba.get_special_widget('overlay_stack') + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + parent=overlay_stack, + transition='in_scale', + scale_origin_stack_offset=scale_origin, + scale=(1.8 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0))) + self._cancel_button = ba.buttonwidget(parent=self._root_widget, + scale=0.7, + position=(40, self._height - 50), + size=(50, 50), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.4, 0.4, 0.5), + icon=ba.gettexture('crossOut'), + iconscale=1.2) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.64), + size=(0, 0), + color=(1, 1, 1, 0.8), + scale=1.2, + h_align='center', + v_align='center', + text=ba.Lstr(resource='reportThisPlayerReasonText'), + maxwidth=self._width * 0.85) + ba.buttonwidget(parent=self._root_widget, + size=(235, 60), + position=(20, 30), + label=ba.Lstr(resource='reportThisPlayerLanguageText'), + on_activate_call=self._on_language_press, + autoselect=True) + ba.buttonwidget(parent=self._root_widget, + size=(235, 60), + position=(self._width - 255, 30), + label=ba.Lstr(resource='reportThisPlayerCheatingText'), + on_activate_call=self._on_cheating_press, + autoselect=True) + + def _on_language_press(self) -> None: + from urllib import parse + _ba.add_transaction({ + 'type': 'REPORT_ACCOUNT', + 'reason': 'language', + 'account': self._account_id + }) + body = ba.Lstr(resource='reportPlayerExplanationText').evaluate() + ba.open_url('mailto:support@froemling.net' + f'?subject={_ba.appnameupper()} Player Report: ' + + self._account_id + '&body=' + parse.quote(body)) + self.close() + + def _on_cheating_press(self) -> None: + from urllib import parse + _ba.add_transaction({ + 'type': 'REPORT_ACCOUNT', + 'reason': 'cheating', + 'account': self._account_id + }) + body = ba.Lstr(resource='reportPlayerExplanationText').evaluate() + ba.open_url('mailto:support@froemling.net' + f'?subject={_ba.appnameupper()} Player Report: ' + + self._account_id + '&body=' + parse.quote(body)) + self.close() + + def close(self) -> None: + """Close the window.""" + ba.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/dist/ba_data/python/bastd/ui/resourcetypeinfo.py b/dist/ba_data/python/bastd/ui/resourcetypeinfo.py new file mode 100644 index 0000000..2013f5c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/resourcetypeinfo.py @@ -0,0 +1,51 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a window which shows info about resource types.""" + +from __future__ import annotations + +import ba +from bastd.ui import popup + + +class ResourceTypeInfoWindow(popup.PopupWindow): + """Popup window providing info about resource types.""" + + def __init__(self, origin_widget: ba.Widget): + uiscale = ba.app.ui.uiscale + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + self._transitioning_out = False + self._width = 570 + self._height = 350 + bg_color = (0.5, 0.4, 0.6) + popup.PopupWindow.__init__( + self, + size=(self._width, self._height), + toolbar_visibility='inherit', + scale=scale, + bg_color=bg_color, + position=origin_widget.get_screen_space_center()) + self._cancel_button = ba.buttonwidget( + parent=self.root_widget, + position=(50, self._height - 30), + size=(50, 50), + scale=0.5, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/serverdialog.py b/dist/ba_data/python/bastd/ui/serverdialog.py new file mode 100644 index 0000000..1756503 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/serverdialog.py @@ -0,0 +1,96 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Dialog window controlled by the master server.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + + +class ServerDialogWindow(ba.Window): + """A dialog window driven by the master-server.""" + + def __init__(self, data: Dict[str, Any]): + self._dialog_id = data['dialogID'] + txt = ba.Lstr(translate=('serverResponses', data['text']), + subs=data.get('subs', [])).evaluate() + txt = txt.strip() + txt_scale = 1.5 + txt_height = (_ba.get_string_height(txt, suppress_warning=True) * + txt_scale) + self._width = 500 + self._height = 130 + min(200, txt_height) + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition='in_scale', + scale=(1.8 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0))) + self._starttime = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) + + ba.playsound(ba.getsound('swish')) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, + 70 + (self._height - 70) * 0.5), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=txt_scale, + h_align='center', + v_align='center', + text=txt, + maxwidth=self._width * 0.85, + max_height=(self._height - 110)) + show_cancel = data.get('showCancel', True) + self._cancel_button: Optional[ba.Widget] + if show_cancel: + self._cancel_button = ba.buttonwidget( + parent=self._root_widget, + position=(30, 30), + size=(160, 60), + autoselect=True, + label=ba.Lstr(resource='cancelText'), + on_activate_call=self._cancel_press) + else: + self._cancel_button = None + self._ok_button = ba.buttonwidget( + parent=self._root_widget, + position=((self._width - 182) if show_cancel else + (self._width * 0.5 - 80), 30), + size=(160, 60), + autoselect=True, + label=ba.Lstr(resource='okText'), + on_activate_call=self._ok_press) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button, + start_button=self._ok_button, + selected_child=self._ok_button) + + def _ok_press(self) -> None: + if ba.time(ba.TimeType.REAL, + ba.TimeFormat.MILLISECONDS) - self._starttime < 1000: + ba.playsound(ba.getsound('error')) + return + _ba.add_transaction({ + 'type': 'DIALOG_RESPONSE', + 'dialogID': self._dialog_id, + 'response': 1 + }) + ba.containerwidget(edit=self._root_widget, transition='out_scale') + + def _cancel_press(self) -> None: + if ba.time(ba.TimeType.REAL, + ba.TimeFormat.MILLISECONDS) - self._starttime < 1000: + ba.playsound(ba.getsound('error')) + return + _ba.add_transaction({ + 'type': 'DIALOG_RESPONSE', + 'dialogID': self._dialog_id, + 'response': 0 + }) + ba.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/dist/ba_data/python/bastd/ui/settings/__init__.py b/dist/ba_data/python/bastd/ui/settings/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..1b1ea1a Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/advanced.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/advanced.cpython-38.opt-1.pyc new file mode 100644 index 0000000..1cb9422 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/advanced.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4791915 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-38.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-38.pyc new file mode 100644 index 0000000..6c7193a Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/audio.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/audio.cpython-38.opt-1.pyc new file mode 100644 index 0000000..5a7ce65 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/audio.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/controls.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/controls.cpython-38.opt-1.pyc new file mode 100644 index 0000000..2f7415d Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/controls.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/gamepad.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/gamepad.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9ca372c Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/gamepad.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/gamepadadvanced.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/gamepadadvanced.cpython-38.opt-1.pyc new file mode 100644 index 0000000..2c0ff73 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/gamepadadvanced.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/gamepadselect.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/gamepadselect.cpython-38.opt-1.pyc new file mode 100644 index 0000000..6e2cf2e Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/gamepadselect.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/graphics.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/graphics.cpython-38.opt-1.pyc new file mode 100644 index 0000000..50e1dcd Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/graphics.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/keyboard.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/keyboard.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d139114 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/keyboard.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/nettesting.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/nettesting.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7490f78 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/nettesting.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/plugins.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/plugins.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f342ffe Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/plugins.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/ps3controller.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/ps3controller.cpython-38.opt-1.pyc new file mode 100644 index 0000000..3e21c6a Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/ps3controller.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/remoteapp.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/remoteapp.cpython-38.opt-1.pyc new file mode 100644 index 0000000..05defb5 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/remoteapp.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/testing.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/testing.cpython-38.opt-1.pyc new file mode 100644 index 0000000..015dbad Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/testing.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/touchscreen.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/touchscreen.cpython-38.opt-1.pyc new file mode 100644 index 0000000..63f74a3 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/touchscreen.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/vrtesting.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/vrtesting.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9b2abe9 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/vrtesting.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/wiimote.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/wiimote.cpython-38.opt-1.pyc new file mode 100644 index 0000000..08bbc2c Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/wiimote.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/__pycache__/xbox360controller.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/settings/__pycache__/xbox360controller.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c001dde Binary files /dev/null and b/dist/ba_data/python/bastd/ui/settings/__pycache__/xbox360controller.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/settings/advanced.py b/dist/ba_data/python/bastd/ui/settings/advanced.py new file mode 100644 index 0000000..1d78690 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/advanced.py @@ -0,0 +1,712 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for advanced settings.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui import popup as popup_ui + +if TYPE_CHECKING: + from typing import Tuple, Any, Optional, List, Dict + + +class AdvancedSettingsWindow(ba.Window): + """Window for editing advanced game settings.""" + + def __init__(self, + transition: str = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + from ba.internal import master_server_get + import threading + + # Preload some modules we use in a background thread so we won't + # have a visual hitch when the user taps them. + threading.Thread(target=self._preload_modules).start() + + app = ba.app + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + uiscale = ba.app.ui.uiscale + self._width = 870.0 if uiscale is ba.UIScale.SMALL else 670.0 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (390.0 if uiscale is ba.UIScale.SMALL else + 450.0 if uiscale is ba.UIScale.MEDIUM else 520.0) + self._spacing = 32 + self._menu_open = False + top_extra = 10 if uiscale is ba.UIScale.SMALL else 0 + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(2.06 if uiscale is ba.UIScale.SMALL else + 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -25) if uiscale is ba.UIScale.SMALL else (0, 0))) + self._prev_lang = '' + self._prev_lang_list: List[str] = [] + self._complete_langs_list: Optional[List] = None + self._complete_langs_error = False + self._language_popup: Optional[popup_ui.PopupMenu] = None + + # In vr-mode, the internal keyboard is currently the *only* option, + # so no need to show this. + self._show_always_use_internal_keyboard = (not app.vr_mode + and not app.iircade_mode) + + self._scroll_width = self._width - (100 + 2 * x_inset) + self._scroll_height = self._height - 115.0 + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 724.0 + + if self._show_always_use_internal_keyboard: + self._sub_height += 62 + + self._show_disable_gyro = app.platform in {'ios', 'android'} + if self._show_disable_gyro: + self._sub_height += 42 + + self._do_vr_test_button = app.vr_mode + self._do_net_test_button = True + self._extra_button_spacing = self._spacing * 2.5 + + if self._do_vr_test_button: + self._sub_height += self._extra_button_spacing + if self._do_net_test_button: + self._sub_height += self._extra_button_spacing + self._sub_height += self._spacing * 2.0 # plugins + + self._r = 'settingsWindowAdvanced' + + if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._do_back) + self._back_button = None + else: + self._back_button = ba.buttonwidget( + parent=self._root_widget, + position=(53 + x_inset, self._height - 60), + size=(140, 60), + scale=0.8, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._do_back) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._back_button) + + self._title_text = ba.textwidget(parent=self._root_widget, + position=(0, self._height - 52), + size=(self._width, 25), + text=ba.Lstr(resource=self._r + + '.titleText'), + color=app.ui.title_color, + h_align='center', + v_align='top') + + if self._back_button is not None: + ba.buttonwidget(edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + position=(50 + x_inset, 50), + simple_culling_v=20.0, + highlight=False, + size=(self._scroll_width, + self._scroll_height), + selection_loops_to_parent=True) + ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False, + selection_loops_to_parent=True) + + self._rebuild() + + # Rebuild periodically to pick up language changes/additions/etc. + self._rebuild_timer = ba.Timer(1.0, + ba.WeakCall(self._rebuild), + repeat=True, + timetype=ba.TimeType.REAL) + + # Fetch the list of completed languages. + master_server_get('bsLangGetCompleted', {'b': app.build_number}, + callback=ba.WeakCall(self._completed_langs_cb)) + + @staticmethod + def _preload_modules() -> None: + """Preload modules we use (called in bg thread).""" + from bastd.ui import config as _unused1 + from ba import modutils as _unused2 + from bastd.ui.settings import vrtesting as _unused3 + from bastd.ui.settings import nettesting as _unused4 + from bastd.ui import appinvite as _unused5 + from bastd.ui import account as _unused6 + from bastd.ui import promocode as _unused7 + from bastd.ui import debug as _unused8 + from bastd.ui.settings import plugins as _unused9 + + def _update_lang_status(self) -> None: + if self._complete_langs_list is not None: + up_to_date = (ba.app.lang.language in self._complete_langs_list) + ba.textwidget( + edit=self._lang_status_text, + text='' if ba.app.lang.language == 'Test' else ba.Lstr( + resource=self._r + '.translationNoUpdateNeededText') + if up_to_date else ba.Lstr(resource=self._r + + '.translationUpdateNeededText'), + color=(0.2, 1.0, 0.2, 0.8) if up_to_date else + (1.0, 0.2, 0.2, 0.8)) + else: + ba.textwidget( + edit=self._lang_status_text, + text=ba.Lstr(resource=self._r + '.translationFetchErrorText') + if self._complete_langs_error else ba.Lstr( + resource=self._r + '.translationFetchingStatusText'), + color=(1.0, 0.5, 0.2) if self._complete_langs_error else + (0.7, 0.7, 0.7)) + + def _rebuild(self) -> None: + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + from bastd.ui.config import ConfigCheckBox + from ba.modutils import show_user_scripts + + available_languages = ba.app.lang.available_languages + + # Don't rebuild if the menu is open or if our language and + # language-list hasn't changed. + # NOTE - although we now support widgets updating their own + # translations, we still change the label formatting on the language + # menu based on the language so still need this. ...however we could + # make this more limited to it only rebuilds that one menu instead + # of everything. + if self._menu_open or (self._prev_lang == _ba.app.config.get( + 'Lang', None) and self._prev_lang_list == available_languages): + return + self._prev_lang = _ba.app.config.get('Lang', None) + self._prev_lang_list = available_languages + + # Clear out our sub-container. + children = self._subcontainer.get_children() + for child in children: + child.delete() + + v = self._sub_height - 35 + + v -= self._spacing * 1.2 + + # Update our existing back button and title. + if self._back_button is not None: + ba.buttonwidget(edit=self._back_button, + label=ba.Lstr(resource='backText')) + ba.buttonwidget(edit=self._back_button, + label=ba.charstr(ba.SpecialChar.BACK)) + + ba.textwidget(edit=self._title_text, + text=ba.Lstr(resource=self._r + '.titleText')) + + this_button_width = 410 + + self._promo_code_button = ba.buttonwidget( + parent=self._subcontainer, + position=(self._sub_width / 2 - this_button_width / 2, v - 14), + size=(this_button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.enterPromoCodeText'), + text_scale=1.0, + on_activate_call=self._on_promo_code_press) + if self._back_button is not None: + ba.widget(edit=self._promo_code_button, + up_widget=self._back_button, + left_widget=self._back_button) + v -= self._extra_button_spacing * 0.8 + + ba.textwidget(parent=self._subcontainer, + position=(200, v + 10), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.languageText'), + maxwidth=150, + scale=0.95, + color=ba.app.ui.title_color, + h_align='right', + v_align='center') + + languages = _ba.app.lang.available_languages + cur_lang = _ba.app.config.get('Lang', None) + if cur_lang is None: + cur_lang = 'Auto' + + # We have a special dict of language names in that language + # so we don't have to go digging through each full language. + try: + import json + with open('ba_data/data/langdata.json') as infile: + lang_names_translated = (json.loads( + infile.read())['lang_names_translated']) + except Exception: + ba.print_exception('Error reading lang data.') + lang_names_translated = {} + + langs_translated = {} + for lang in languages: + langs_translated[lang] = lang_names_translated.get(lang, lang) + + langs_full = {} + for lang in languages: + lang_translated = ba.Lstr(translate=('languages', lang)).evaluate() + if langs_translated[lang] == lang_translated: + langs_full[lang] = lang_translated + else: + langs_full[lang] = (langs_translated[lang] + ' (' + + lang_translated + ')') + + self._language_popup = popup_ui.PopupMenu( + parent=self._subcontainer, + position=(210, v - 19), + width=150, + opening_call=ba.WeakCall(self._on_menu_open), + closing_call=ba.WeakCall(self._on_menu_close), + autoselect=False, + on_value_change_call=ba.WeakCall(self._on_menu_choice), + choices=['Auto'] + languages, + button_size=(250, 60), + choices_display=([ + ba.Lstr(value=(ba.Lstr(resource='autoText').evaluate() + ' (' + + ba.Lstr(translate=('languages', + ba.app.lang.default_language + )).evaluate() + ')')) + ] + [ba.Lstr(value=langs_full[l]) for l in languages]), + current_choice=cur_lang) + + v -= self._spacing * 1.8 + + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, v + 10), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.helpTranslateText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText'))]), + maxwidth=self._sub_width * 0.9, + max_height=55, + flatness=1.0, + scale=0.65, + color=(0.4, 0.9, 0.4, 0.8), + h_align='center', + v_align='center') + v -= self._spacing * 1.9 + this_button_width = 410 + self._translation_editor_button = ba.buttonwidget( + parent=self._subcontainer, + position=(self._sub_width / 2 - this_button_width / 2, v - 24), + size=(this_button_width, 60), + label=ba.Lstr(resource=self._r + '.translationEditorButtonText', + subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) + ]), + autoselect=True, + on_activate_call=ba.Call(ba.open_url, + 'http://bombsquadgame.com/translate')) + + self._lang_status_text = ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, + v - 40), + size=(0, 0), + text='', + flatness=1.0, + scale=0.63, + h_align='center', + v_align='center', + maxwidth=400.0) + self._update_lang_status() + v -= 40 + + lang_inform = _ba.get_account_misc_val('langInform', False) + + self._language_inform_checkbox = cbw = ba.checkboxwidget( + parent=self._subcontainer, + position=(50, v - 50), + size=(self._sub_width - 100, 30), + autoselect=True, + maxwidth=430, + textcolor=(0.8, 0.8, 0.8), + value=lang_inform, + text=ba.Lstr(resource=self._r + '.translationInformMe'), + on_value_change_call=ba.WeakCall( + self._on_lang_inform_value_change)) + + ba.widget(edit=self._translation_editor_button, + down_widget=cbw, + up_widget=self._language_popup.get_button()) + + v -= self._spacing * 3.0 + + self._kick_idle_players_check_box = ConfigCheckBox( + parent=self._subcontainer, + position=(50, v), + size=(self._sub_width - 100, 30), + configkey='Kick Idle Players', + displayname=ba.Lstr(resource=self._r + '.kickIdlePlayersText'), + scale=1.0, + maxwidth=430) + + v -= 42 + self._disable_camera_shake_check_box = ConfigCheckBox( + parent=self._subcontainer, + position=(50, v), + size=(self._sub_width - 100, 30), + configkey='Disable Camera Shake', + displayname=ba.Lstr(resource=self._r + '.disableCameraShakeText'), + scale=1.0, + maxwidth=430) + + self._disable_gyro_check_box: Optional[ConfigCheckBox] = None + if self._show_disable_gyro: + v -= 42 + self._disable_gyro_check_box = ConfigCheckBox( + parent=self._subcontainer, + position=(50, v), + size=(self._sub_width - 100, 30), + configkey='Disable Camera Gyro', + displayname=ba.Lstr(resource=self._r + + '.disableCameraGyroscopeMotionText'), + scale=1.0, + maxwidth=430) + + self._always_use_internal_keyboard_check_box: Optional[ConfigCheckBox] + if self._show_always_use_internal_keyboard: + v -= 42 + self._always_use_internal_keyboard_check_box = ConfigCheckBox( + parent=self._subcontainer, + position=(50, v), + size=(self._sub_width - 100, 30), + configkey='Always Use Internal Keyboard', + autoselect=True, + displayname=ba.Lstr(resource=self._r + + '.alwaysUseInternalKeyboardText'), + scale=1.0, + maxwidth=430) + ba.textwidget( + parent=self._subcontainer, + position=(90, v - 10), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.alwaysUseInternalKeyboardDescriptionText'), + maxwidth=400, + flatness=1.0, + scale=0.65, + color=(0.4, 0.9, 0.4, 0.8), + h_align='left', + v_align='center') + v -= 20 + else: + self._always_use_internal_keyboard_check_box = None + + v -= self._spacing * 2.1 + + this_button_width = 410 + self._show_user_mods_button = ba.buttonwidget( + parent=self._subcontainer, + position=(self._sub_width / 2 - this_button_width / 2, v - 10), + size=(this_button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.showUserModsText'), + text_scale=1.0, + on_activate_call=show_user_scripts) + if self._show_always_use_internal_keyboard: + assert self._always_use_internal_keyboard_check_box is not None + ba.widget(edit=self._always_use_internal_keyboard_check_box.widget, + down_widget=self._show_user_mods_button) + ba.widget( + edit=self._show_user_mods_button, + up_widget=self._always_use_internal_keyboard_check_box.widget) + else: + ba.widget(edit=self._show_user_mods_button, + up_widget=self._kick_idle_players_check_box.widget) + ba.widget(edit=self._kick_idle_players_check_box.widget, + down_widget=self._show_user_mods_button) + + v -= self._spacing * 2.0 + + self._modding_guide_button = ba.buttonwidget( + parent=self._subcontainer, + position=(self._sub_width / 2 - this_button_width / 2, v - 10), + size=(this_button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.moddingGuideText'), + text_scale=1.0, + on_activate_call=ba.Call( + ba.open_url, + 'http://www.froemling.net/docs/bombsquad-modding-guide')) + + v -= self._spacing * 2.0 + + self._plugins_button = ba.buttonwidget( + parent=self._subcontainer, + position=(self._sub_width / 2 - this_button_width / 2, v - 10), + size=(this_button_width, 60), + autoselect=True, + label=ba.Lstr(resource='pluginsText'), + text_scale=1.0, + on_activate_call=self._on_plugins_button_press) + + v -= self._spacing * 0.6 + + self._vr_test_button: Optional[ba.Widget] + if self._do_vr_test_button: + v -= self._extra_button_spacing + self._vr_test_button = ba.buttonwidget( + parent=self._subcontainer, + position=(self._sub_width / 2 - this_button_width / 2, v - 14), + size=(this_button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.vrTestingText'), + text_scale=1.0, + on_activate_call=self._on_vr_test_press) + else: + self._vr_test_button = None + + self._net_test_button: Optional[ba.Widget] + if self._do_net_test_button: + v -= self._extra_button_spacing + self._net_test_button = ba.buttonwidget( + parent=self._subcontainer, + position=(self._sub_width / 2 - this_button_width / 2, v - 14), + size=(this_button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.netTestingText'), + text_scale=1.0, + on_activate_call=self._on_net_test_press) + else: + self._net_test_button = None + + v -= 70 + self._benchmarks_button = ba.buttonwidget( + parent=self._subcontainer, + position=(self._sub_width / 2 - this_button_width / 2, v - 14), + size=(this_button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.benchmarksText'), + text_scale=1.0, + on_activate_call=self._on_benchmark_press) + + for child in self._subcontainer.get_children(): + ba.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20) + + if ba.app.ui.use_toolbars: + pbtn = _ba.get_special_widget('party_button') + ba.widget(edit=self._scrollwidget, right_widget=pbtn) + if self._back_button is None: + ba.widget(edit=self._scrollwidget, + left_widget=_ba.get_special_widget('back_button')) + + self._restore_state() + + def _show_restart_needed(self, value: Any) -> None: + del value # Unused. + ba.screenmessage(ba.Lstr(resource=self._r + '.mustRestartText'), + color=(1, 1, 0)) + + def _on_lang_inform_value_change(self, val: bool) -> None: + _ba.add_transaction({ + 'type': 'SET_MISC_VAL', + 'name': 'langInform', + 'value': val + }) + _ba.run_transactions() + + def _on_vr_test_press(self) -> None: + from bastd.ui.settings.vrtesting import VRTestingWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + VRTestingWindow(transition='in_right').get_root_widget()) + + def _on_net_test_press(self) -> None: + from bastd.ui.settings.nettesting import NetTestingWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + NetTestingWindow(transition='in_right').get_root_widget()) + + def _on_friend_promo_code_press(self) -> None: + from bastd.ui import appinvite + from bastd.ui import account + if _ba.get_account_state() != 'signed_in': + account.show_sign_in_prompt() + return + appinvite.handle_app_invites_press() + + def _on_plugins_button_press(self) -> None: + from bastd.ui.settings.plugins import PluginSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + PluginSettingsWindow( + origin_widget=self._plugins_button).get_root_widget()) + + def _on_promo_code_press(self) -> None: + from bastd.ui.promocode import PromoCodeWindow + from bastd.ui.account import show_sign_in_prompt + + # We have to be logged in for promo-codes to work. + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + PromoCodeWindow( + origin_widget=self._promo_code_button).get_root_widget()) + + def _on_benchmark_press(self) -> None: + from bastd.ui.debug import DebugWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + DebugWindow(transition='in_right').get_root_widget()) + + def _save_state(self) -> None: + # pylint: disable=too-many-branches + try: + sel = self._root_widget.get_selected_child() + if sel == self._scrollwidget: + sel = self._subcontainer.get_selected_child() + if sel == self._vr_test_button: + sel_name = 'VRTest' + elif sel == self._net_test_button: + sel_name = 'NetTest' + elif sel == self._promo_code_button: + sel_name = 'PromoCode' + elif sel == self._benchmarks_button: + sel_name = 'Benchmarks' + elif sel == self._kick_idle_players_check_box.widget: + sel_name = 'KickIdlePlayers' + elif sel == self._disable_camera_shake_check_box.widget: + sel_name = 'DisableCameraShake' + elif (self._always_use_internal_keyboard_check_box is not None + and sel + == self._always_use_internal_keyboard_check_box.widget): + sel_name = 'AlwaysUseInternalKeyboard' + elif (self._disable_gyro_check_box is not None + and sel == self._disable_gyro_check_box.widget): + sel_name = 'DisableGyro' + elif (self._language_popup is not None + and sel == self._language_popup.get_button()): + sel_name = 'Languages' + elif sel == self._translation_editor_button: + sel_name = 'TranslationEditor' + elif sel == self._show_user_mods_button: + sel_name = 'ShowUserMods' + elif sel == self._plugins_button: + sel_name = 'Plugins' + elif sel == self._modding_guide_button: + sel_name = 'ModdingGuide' + elif sel == self._language_inform_checkbox: + sel_name = 'LangInform' + else: + raise ValueError(f'unrecognized selection \'{sel}\'') + elif sel == self._back_button: + sel_name = 'Back' + else: + raise ValueError(f'unrecognized selection \'{sel}\'') + ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} + except Exception: + ba.print_exception(f'Error saving state for {self.__class__}') + + def _restore_state(self) -> None: + # pylint: disable=too-many-branches + try: + sel_name = ba.app.ui.window_states.get(type(self), + {}).get('sel_name') + if sel_name == 'Back': + sel = self._back_button + else: + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + if sel_name == 'VRTest': + sel = self._vr_test_button + elif sel_name == 'NetTest': + sel = self._net_test_button + elif sel_name == 'PromoCode': + sel = self._promo_code_button + elif sel_name == 'Benchmarks': + sel = self._benchmarks_button + elif sel_name == 'KickIdlePlayers': + sel = self._kick_idle_players_check_box.widget + elif sel_name == 'DisableCameraShake': + sel = self._disable_camera_shake_check_box.widget + elif (sel_name == 'AlwaysUseInternalKeyboard' + and self._always_use_internal_keyboard_check_box + is not None): + sel = self._always_use_internal_keyboard_check_box.widget + elif (sel_name == 'DisableGyro' + and self._disable_gyro_check_box is not None): + sel = self._disable_gyro_check_box.widget + elif (sel_name == 'Languages' + and self._language_popup is not None): + sel = self._language_popup.get_button() + elif sel_name == 'TranslationEditor': + sel = self._translation_editor_button + elif sel_name == 'ShowUserMods': + sel = self._show_user_mods_button + elif sel_name == 'Plugins': + sel = self._plugins_button + elif sel_name == 'ModdingGuide': + sel = self._modding_guide_button + elif sel_name == 'LangInform': + sel = self._language_inform_checkbox + else: + sel = None + if sel is not None: + ba.containerwidget(edit=self._subcontainer, + selected_child=sel, + visible_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self.__class__}') + + def _on_menu_open(self) -> None: + self._menu_open = True + + def _on_menu_close(self) -> None: + self._menu_open = False + + def _on_menu_choice(self, choice: str) -> None: + ba.app.lang.setlanguage(None if choice == 'Auto' else choice) + self._save_state() + ba.timer(0.1, ba.WeakCall(self._rebuild), timetype=ba.TimeType.REAL) + + def _completed_langs_cb(self, results: Optional[Dict[str, Any]]) -> None: + if results is not None and results['langs'] is not None: + self._complete_langs_list = results['langs'] + self._complete_langs_error = False + else: + self._complete_langs_list = None + self._complete_langs_error = True + ba.timer(0.001, + ba.WeakCall(self._update_lang_status), + timetype=ba.TimeType.REAL) + + def _do_back(self) -> None: + from bastd.ui.settings.allsettings import AllSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + AllSettingsWindow(transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/settings/allsettings.py b/dist/ba_data/python/bastd/ui/settings/allsettings.py new file mode 100644 index 0000000..ede3823 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/allsettings.py @@ -0,0 +1,283 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI for top level settings categories.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Tuple, Optional, Union + + +class AllSettingsWindow(ba.Window): + """Window for selecting a settings category.""" + + def __init__(self, + transition: str = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + import threading + + # Preload some modules we use in a background thread so we won't + # have a visual hitch when the user taps them. + threading.Thread(target=self._preload_modules).start() + + ba.set_analytics_screen('Settings Window') + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + uiscale = ba.app.ui.uiscale + width = 900 if uiscale is ba.UIScale.SMALL else 580 + x_inset = 75 if uiscale is ba.UIScale.SMALL else 0 + height = 435 + self._r = 'settingsWindow' + top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(width, height + top_extra), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(1.75 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -8) if uiscale is ba.UIScale.SMALL else (0, 0))) + + if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + self._back_button = None + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._do_back) + else: + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(40 + x_inset, height - 55), + size=(130, 60), + scale=0.8, + text_scale=1.2, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._do_back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(0, height - 44), + size=(width, 25), + text=ba.Lstr(resource=self._r + '.titleText'), + color=ba.app.ui.title_color, + h_align='center', + v_align='center', + maxwidth=130) + + if self._back_button is not None: + ba.buttonwidget(edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + v = height - 80 + v -= 145 + + basew = 280 if uiscale is ba.UIScale.SMALL else 230 + baseh = 170 + x_offs = x_inset + (105 if uiscale is ba.UIScale.SMALL else + 72) - basew # now unused + x_offs2 = x_offs + basew - 7 + x_offs3 = x_offs + 2 * (basew - 7) + x_offs4 = x_offs2 + x_offs5 = x_offs3 + + def _b_title(x: float, y: float, button: ba.Widget, + text: Union[str, ba.Lstr]) -> None: + ba.textwidget(parent=self._root_widget, + text=text, + position=(x + basew * 0.47, y + baseh * 0.22), + maxwidth=basew * 0.7, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=button, + color=(0.7, 0.9, 0.7, 1.0)) + + ctb = self._controllers_button = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x_offs2, v), + size=(basew, baseh), + button_type='square', + label='', + on_activate_call=self._do_controllers) + if ba.app.ui.use_toolbars and self._back_button is None: + bbtn = _ba.get_special_widget('back_button') + ba.widget(edit=ctb, left_widget=bbtn) + _b_title(x_offs2, v, ctb, + ba.Lstr(resource=self._r + '.controllersText')) + imgw = imgh = 130 + ba.imagewidget(parent=self._root_widget, + position=(x_offs2 + basew * 0.49 - imgw * 0.5, v + 35), + size=(imgw, imgh), + texture=ba.gettexture('controllerIcon'), + draw_controller=ctb) + + gfxb = self._graphics_button = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x_offs3, v), + size=(basew, baseh), + button_type='square', + label='', + on_activate_call=self._do_graphics) + if ba.app.ui.use_toolbars: + pbtn = _ba.get_special_widget('party_button') + ba.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn) + _b_title(x_offs3, v, gfxb, ba.Lstr(resource=self._r + '.graphicsText')) + imgw = imgh = 110 + ba.imagewidget(parent=self._root_widget, + position=(x_offs3 + basew * 0.49 - imgw * 0.5, v + 42), + size=(imgw, imgh), + texture=ba.gettexture('graphicsIcon'), + draw_controller=gfxb) + + v -= (baseh - 5) + + abtn = self._audio_button = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x_offs4, v), + size=(basew, baseh), + button_type='square', + label='', + on_activate_call=self._do_audio) + _b_title(x_offs4, v, abtn, ba.Lstr(resource=self._r + '.audioText')) + imgw = imgh = 120 + ba.imagewidget(parent=self._root_widget, + position=(x_offs4 + basew * 0.49 - imgw * 0.5 + 5, + v + 35), + size=(imgw, imgh), + color=(1, 1, 0), + texture=ba.gettexture('audioIcon'), + draw_controller=abtn) + + avb = self._advanced_button = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(x_offs5, v), + size=(basew, baseh), + button_type='square', + label='', + on_activate_call=self._do_advanced) + _b_title(x_offs5, v, avb, ba.Lstr(resource=self._r + '.advancedText')) + imgw = imgh = 120 + ba.imagewidget(parent=self._root_widget, + position=(x_offs5 + basew * 0.49 - imgw * 0.5 + 5, + v + 35), + size=(imgw, imgh), + color=(0.8, 0.95, 1), + texture=ba.gettexture('advancedIcon'), + draw_controller=avb) + self._restore_state() + + @staticmethod + def _preload_modules() -> None: + """Preload modules we use (called in bg thread).""" + import bastd.ui.mainmenu as _unused1 + import bastd.ui.settings.controls as _unused2 + import bastd.ui.settings.graphics as _unused3 + import bastd.ui.settings.audio as _unused4 + import bastd.ui.settings.advanced as _unused5 + + def _do_back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.mainmenu import MainMenuWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition='in_left').get_root_widget()) + + def _do_controllers(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.controls import ControlsSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + ControlsSettingsWindow( + origin_widget=self._controllers_button).get_root_widget()) + + def _do_graphics(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.graphics import GraphicsSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + GraphicsSettingsWindow( + origin_widget=self._graphics_button).get_root_widget()) + + def _do_audio(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.audio import AudioSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + AudioSettingsWindow( + origin_widget=self._audio_button).get_root_widget()) + + def _do_advanced(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.advanced import AdvancedSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + AdvancedSettingsWindow( + origin_widget=self._advanced_button).get_root_widget()) + + def _save_state(self) -> None: + try: + sel = self._root_widget.get_selected_child() + if sel == self._controllers_button: + sel_name = 'Controllers' + elif sel == self._graphics_button: + sel_name = 'Graphics' + elif sel == self._audio_button: + sel_name = 'Audio' + elif sel == self._advanced_button: + sel_name = 'Advanced' + elif sel == self._back_button: + sel_name = 'Back' + else: + raise ValueError(f'unrecognized selection \'{sel}\'') + ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + def _restore_state(self) -> None: + try: + sel_name = ba.app.ui.window_states.get(type(self), + {}).get('sel_name') + sel: Optional[ba.Widget] + if sel_name == 'Controllers': + sel = self._controllers_button + elif sel_name == 'Graphics': + sel = self._graphics_button + elif sel_name == 'Audio': + sel = self._audio_button + elif sel_name == 'Advanced': + sel = self._advanced_button + elif sel_name == 'Back': + sel = self._back_button + else: + sel = self._controllers_button + if sel is not None: + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') diff --git a/dist/ba_data/python/bastd/ui/settings/audio.py b/dist/ba_data/python/bastd/ui/settings/audio.py new file mode 100644 index 0000000..99f98d9 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/audio.py @@ -0,0 +1,282 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides audio settings UI.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Tuple, Optional + + +class AudioSettingsWindow(ba.Window): + """Window for editing audio settings.""" + + def __init__(self, + transition: str = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from bastd.ui.popup import PopupMenu + from bastd.ui.config import ConfigNumberEdit + + music = ba.app.music + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + self._r = 'audioSettingsWindow' + + spacing = 50.0 + width = 460.0 + height = 210.0 + + # Update: hard-coding head-relative audio to true now, + # so not showing options. + # show_vr_head_relative_audio = True if ba.app.vr_mode else False + show_vr_head_relative_audio = False + + if show_vr_head_relative_audio: + height += 70 + + show_soundtracks = False + if music.have_music_player(): + show_soundtracks = True + height += spacing * 2.0 + + uiscale = ba.app.ui.uiscale + base_scale = (2.05 if uiscale is ba.UIScale.SMALL else + 1.6 if uiscale is ba.UIScale.MEDIUM else 1.0) + popup_menu_scale = base_scale * 1.2 + + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition=transition, + scale=base_scale, + scale_origin_stack_offset=scale_origin, + stack_offset=(0, -20) if uiscale is ba.UIScale.SMALL else (0, 0))) + + self._back_button = back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(35, height - 55), + size=(120, 60), + scale=0.8, + text_scale=1.2, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._back, + autoselect=True) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + v = height - 60 + v -= spacing * 1.0 + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, height - 32), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.titleText'), + color=ba.app.ui.title_color, + maxwidth=180, + h_align='center', + v_align='center') + + ba.buttonwidget(edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + self._sound_volume_numedit = svne = ConfigNumberEdit( + parent=self._root_widget, + position=(40, v), + xoffset=10, + configkey='Sound Volume', + displayname=ba.Lstr(resource=self._r + '.soundVolumeText'), + minval=0.0, + maxval=1.0, + increment=0.1) + if ba.app.ui.use_toolbars: + ba.widget(edit=svne.plusbutton, + right_widget=_ba.get_special_widget('party_button')) + v -= spacing + self._music_volume_numedit = ConfigNumberEdit( + parent=self._root_widget, + position=(40, v), + xoffset=10, + configkey='Music Volume', + displayname=ba.Lstr(resource=self._r + '.musicVolumeText'), + minval=0.0, + maxval=1.0, + increment=0.1, + callback=music.music_volume_changed, + changesound=False) + + v -= 0.5 * spacing + + self._vr_head_relative_audio_button: Optional[ba.Widget] + if show_vr_head_relative_audio: + v -= 40 + ba.textwidget(parent=self._root_widget, + position=(40, v + 24), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.headRelativeVRAudioText'), + color=(0.8, 0.8, 0.8), + maxwidth=230, + h_align='left', + v_align='center') + + popup = PopupMenu( + parent=self._root_widget, + position=(290, v), + width=120, + button_size=(135, 50), + scale=popup_menu_scale, + choices=['Auto', 'On', 'Off'], + choices_display=[ + ba.Lstr(resource='autoText'), + ba.Lstr(resource='onText'), + ba.Lstr(resource='offText') + ], + current_choice=ba.app.config.resolve('VR Head Relative Audio'), + on_value_change_call=self._set_vr_head_relative_audio) + self._vr_head_relative_audio_button = popup.get_button() + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, v - 11), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.headRelativeVRAudioInfoText'), + scale=0.5, + color=(0.7, 0.8, 0.7), + maxwidth=400, + flatness=1.0, + h_align='center', + v_align='center') + v -= 30 + else: + self._vr_head_relative_audio_button = None + + self._soundtrack_button: Optional[ba.Widget] + if show_soundtracks: + v -= 1.2 * spacing + self._soundtrack_button = ba.buttonwidget( + parent=self._root_widget, + position=((width - 310) / 2, v), + size=(310, 50), + autoselect=True, + label=ba.Lstr(resource=self._r + '.soundtrackButtonText'), + on_activate_call=self._do_soundtracks) + v -= spacing * 0.5 + ba.textwidget(parent=self._root_widget, + position=(0, v), + size=(width, 20), + text=ba.Lstr(resource=self._r + + '.soundtrackDescriptionText'), + flatness=1.0, + h_align='center', + scale=0.5, + color=(0.7, 0.8, 0.7, 1.0), + maxwidth=400) + else: + self._soundtrack_button = None + + # Tweak a few navigation bits. + try: + ba.widget(edit=back_button, down_widget=svne.minusbutton) + except Exception: + ba.print_exception('Error wiring AudioSettingsWindow.') + + self._restore_state() + + def _set_vr_head_relative_audio(self, val: str) -> None: + cfg = ba.app.config + cfg['VR Head Relative Audio'] = val + cfg.apply_and_commit() + + def _do_soundtracks(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.soundtrack import browser as stb + + # We require disk access for soundtracks; + # if we don't have it, request it. + if not _ba.have_permission(ba.Permission.STORAGE): + ba.playsound(ba.getsound('ding')) + ba.screenmessage(ba.Lstr(resource='storagePermissionAccessText'), + color=(0.5, 1, 0.5)) + ba.timer(1.0, + ba.Call(_ba.request_permission, ba.Permission.STORAGE), + timetype=ba.TimeType.REAL) + return + + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + stb.SoundtrackBrowserWindow( + origin_widget=self._soundtrack_button).get_root_widget()) + + def _back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings import allsettings + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + allsettings.AllSettingsWindow( + transition='in_left').get_root_widget()) + + def _save_state(self) -> None: + try: + sel = self._root_widget.get_selected_child() + if sel == self._sound_volume_numedit.minusbutton: + sel_name = 'SoundMinus' + elif sel == self._sound_volume_numedit.plusbutton: + sel_name = 'SoundPlus' + elif sel == self._music_volume_numedit.minusbutton: + sel_name = 'MusicMinus' + elif sel == self._music_volume_numedit.plusbutton: + sel_name = 'MusicPlus' + elif sel == self._soundtrack_button: + sel_name = 'Soundtrack' + elif sel == self._back_button: + sel_name = 'Back' + elif sel == self._vr_head_relative_audio_button: + sel_name = 'VRHeadRelative' + else: + raise ValueError(f'unrecognized selection \'{sel}\'') + ba.app.ui.window_states[type(self)] = sel_name + except Exception: + ba.print_exception(f'Error saving state for {self.__class__}.') + + def _restore_state(self) -> None: + try: + sel_name = ba.app.ui.window_states.get(type(self)) + sel: Optional[ba.Widget] + if sel_name == 'SoundMinus': + sel = self._sound_volume_numedit.minusbutton + elif sel_name == 'SoundPlus': + sel = self._sound_volume_numedit.plusbutton + elif sel_name == 'MusicMinus': + sel = self._music_volume_numedit.minusbutton + elif sel_name == 'MusicPlus': + sel = self._music_volume_numedit.plusbutton + elif sel_name == 'VRHeadRelative': + sel = self._vr_head_relative_audio_button + elif sel_name == 'Soundtrack': + sel = self._soundtrack_button + elif sel_name == 'Back': + sel = self._back_button + else: + sel = self._back_button + if sel: + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self.__class__}.') diff --git a/dist/ba_data/python/bastd/ui/settings/controls.py b/dist/ba_data/python/bastd/ui/settings/controls.py new file mode 100644 index 0000000..ee06d27 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/controls.py @@ -0,0 +1,494 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a top level control settings window.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Tuple, Optional + + +class ControlsSettingsWindow(ba.Window): + """Top level control settings window.""" + + def __init__(self, + transition: str = 'in_right', + origin_widget: ba.Widget = None): + # FIXME: should tidy up here. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=cyclic-import + from bastd.ui import popup as popup_ui + self._have_selected_child = False + + scale_origin: Optional[Tuple[float, float]] + + # If they provided an origin-widget, scale up from that. + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + self._r = 'configControllersWindow' + app = ba.app + + # is_fire_tv = _ba.is_running_on_fire_tv() + + spacing = 50.0 + button_width = 350.0 + width = 460.0 + height = 130.0 + + space_height = spacing * 0.3 + + # FIXME: should create vis settings in platform for these, + # not hard code them here. + + show_gamepads = False + platform = app.platform + subplatform = app.subplatform + non_vr_windows = (platform == 'windows' + and (subplatform != 'oculus' or not app.vr_mode)) + if platform in ('linux', 'android', 'mac') or non_vr_windows: + show_gamepads = True + height += spacing + + show_touch = False + if _ba.have_touchscreen_input(): + show_touch = True + height += spacing + + show_space_1 = False + if show_gamepads or show_touch: + show_space_1 = True + height += space_height + + show_keyboard = False + if _ba.getinputdevice('Keyboard', '#1', doraise=False) is not None: + show_keyboard = True + height += spacing + show_keyboard_p2 = False if app.vr_mode else show_keyboard + if show_keyboard_p2: + height += spacing + + show_space_2 = False + if show_keyboard: + show_space_2 = True + height += space_height + + if bool(True): + show_remote = True + height += spacing + else: + show_remote = False + + show_ps3 = False + # if platform == 'mac': + # show_ps3 = True + # height += spacing + + show360 = False + # if platform == 'mac' or is_fire_tv: + # show360 = True + # height += spacing + + show_mac_wiimote = False + # if platform == 'mac' and _ba.is_xcode_build(): + # show_mac_wiimote = True + # height += spacing + + # On windows (outside of oculus/vr), show an option to disable xinput. + show_xinput_toggle = False + if platform == 'windows' and not app.vr_mode: + show_xinput_toggle = True + + # On mac builds, show an option to switch between generic and + # made-for-iOS/Mac systems + # (we can run into problems where devices register as one of each + # type otherwise).. + show_mac_controller_subsystem = False + if platform == 'mac' and _ba.is_xcode_build(): + show_mac_controller_subsystem = True + + if show_mac_controller_subsystem: + height += spacing * 1.5 + + if show_xinput_toggle: + height += spacing + + uiscale = ba.app.ui.uiscale + smallscale = (1.7 if show_keyboard else 2.2) + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition=transition, + scale_origin_stack_offset=scale_origin, + stack_offset=((0, -10) if uiscale is ba.UIScale.SMALL else (0, 0)), + scale=(smallscale if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0))) + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(35, height - 60), + size=(140, 65), + scale=0.8, + text_scale=1.2, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + # We need these vars to exist even if the buttons don't. + self._gamepads_button: Optional[ba.Widget] = None + self._touch_button: Optional[ba.Widget] = None + self._keyboard_button: Optional[ba.Widget] = None + self._keyboard_2_button: Optional[ba.Widget] = None + self._idevices_button: Optional[ba.Widget] = None + self._ps3_button: Optional[ba.Widget] = None + self._xbox_360_button: Optional[ba.Widget] = None + self._wiimotes_button: Optional[ba.Widget] = None + + ba.textwidget(parent=self._root_widget, + position=(0, height - 49), + size=(width, 25), + text=ba.Lstr(resource=self._r + '.titleText'), + color=ba.app.ui.title_color, + h_align='center', + v_align='top') + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + v = height - 75 + v -= spacing + + if show_touch: + self._touch_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=((width - button_width) / 2, v), + size=(button_width, 43), + autoselect=True, + label=ba.Lstr(resource=self._r + '.configureTouchText'), + on_activate_call=self._do_touchscreen) + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + if not self._have_selected_child: + ba.containerwidget(edit=self._root_widget, + selected_child=self._touch_button) + ba.widget(edit=self._back_button, + down_widget=self._touch_button) + self._have_selected_child = True + v -= spacing + + if show_gamepads: + self._gamepads_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=((width - button_width) / 2 - 7, v), + size=(button_width, 43), + autoselect=True, + label=ba.Lstr(resource=self._r + '.configureControllersText'), + on_activate_call=self._do_gamepads) + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + if not self._have_selected_child: + ba.containerwidget(edit=self._root_widget, + selected_child=self._gamepads_button) + ba.widget(edit=self._back_button, + down_widget=self._gamepads_button) + self._have_selected_child = True + v -= spacing + else: + self._gamepads_button = None + + if show_space_1: + v -= space_height + + if show_keyboard: + self._keyboard_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=((width - button_width) / 2 + 5, v), + size=(button_width, 43), + autoselect=True, + label=ba.Lstr(resource=self._r + '.configureKeyboardText'), + on_activate_call=self._config_keyboard) + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + if not self._have_selected_child: + ba.containerwidget(edit=self._root_widget, + selected_child=self._keyboard_button) + ba.widget(edit=self._back_button, + down_widget=self._keyboard_button) + self._have_selected_child = True + v -= spacing + if show_keyboard_p2: + self._keyboard_2_button = ba.buttonwidget( + parent=self._root_widget, + position=((width - button_width) / 2 - 3, v), + size=(button_width, 43), + autoselect=True, + label=ba.Lstr(resource=self._r + '.configureKeyboard2Text'), + on_activate_call=self._config_keyboard2) + v -= spacing + if show_space_2: + v -= space_height + if show_remote: + self._idevices_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=((width - button_width) / 2 - 5, v), + size=(button_width, 43), + autoselect=True, + label=ba.Lstr(resource=self._r + '.configureMobileText'), + on_activate_call=self._do_mobile_devices) + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + if not self._have_selected_child: + ba.containerwidget(edit=self._root_widget, + selected_child=self._idevices_button) + ba.widget(edit=self._back_button, + down_widget=self._idevices_button) + self._have_selected_child = True + v -= spacing + if show_ps3: + self._ps3_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=((width - button_width) / 2 + 5, v), + size=(button_width, 43), + autoselect=True, + label=ba.Lstr(resource=self._r + '.ps3Text'), + on_activate_call=self._do_ps3_controllers) + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + v -= spacing + if show360: + self._xbox_360_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=((width - button_width) / 2 - 1, v), + size=(button_width, 43), + autoselect=True, + label=ba.Lstr(resource=self._r + '.xbox360Text'), + on_activate_call=self._do_360_controllers) + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + v -= spacing + if show_mac_wiimote: + self._wiimotes_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=((width - button_width) / 2 + 5, v), + size=(button_width, 43), + autoselect=True, + label=ba.Lstr(resource=self._r + '.wiimotesText'), + on_activate_call=self._do_wiimotes) + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=_ba.get_special_widget('party_button')) + v -= spacing + + if show_xinput_toggle: + + def do_toggle(value: bool) -> None: + ba.screenmessage( + ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'), + color=(1, 1, 0)) + ba.playsound(ba.getsound('gunCocking')) + _ba.set_low_level_config_value('enablexinput', not value) + + ba.checkboxwidget( + parent=self._root_widget, + position=(100, v + 3), + size=(120, 30), + value=(not _ba.get_low_level_config_value('enablexinput', 1)), + maxwidth=200, + on_value_change_call=do_toggle, + text=ba.Lstr(resource='disableXInputText'), + autoselect=True) + ba.textwidget( + parent=self._root_widget, + position=(width * 0.5, v - 5), + size=(0, 0), + text=ba.Lstr(resource='disableXInputDescriptionText'), + scale=0.5, + h_align='center', + v_align='center', + color=ba.app.ui.infotextcolor, + maxwidth=width * 0.8) + v -= spacing + if show_mac_controller_subsystem: + popup_ui.PopupMenu( + parent=self._root_widget, + position=(260, v - 10), + width=160, + button_size=(150, 50), + scale=1.5, + choices=['Classic', 'MFi', 'Both'], + choices_display=[ + ba.Lstr(resource='macControllerSubsystemClassicText'), + ba.Lstr(resource='macControllerSubsystemMFiText'), + ba.Lstr(resource='macControllerSubsystemBothText') + ], + current_choice=ba.app.config.resolve( + 'Mac Controller Subsystem'), + on_value_change_call=self._set_mac_controller_subsystem) + ba.textwidget( + parent=self._root_widget, + position=(245, v + 13), + size=(0, 0), + text=ba.Lstr(resource='macControllerSubsystemTitleText'), + scale=1.0, + h_align='right', + v_align='center', + color=ba.app.ui.infotextcolor, + maxwidth=180) + ba.textwidget( + parent=self._root_widget, + position=(width * 0.5, v - 20), + size=(0, 0), + text=ba.Lstr(resource='macControllerSubsystemDescriptionText'), + scale=0.5, + h_align='center', + v_align='center', + color=ba.app.ui.infotextcolor, + maxwidth=width * 0.8) + v -= spacing * 1.5 + self._restore_state() + + def _set_mac_controller_subsystem(self, val: str) -> None: + cfg = ba.app.config + cfg['Mac Controller Subsystem'] = val + cfg.apply_and_commit() + + def _config_keyboard(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.keyboard import ConfigKeyboardWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + ConfigKeyboardWindow(_ba.getinputdevice('Keyboard', + '#1')).get_root_widget()) + + def _config_keyboard2(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.keyboard import ConfigKeyboardWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + ConfigKeyboardWindow(_ba.getinputdevice('Keyboard', + '#2')).get_root_widget()) + + def _do_mobile_devices(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.remoteapp import RemoteAppSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + RemoteAppSettingsWindow().get_root_widget()) + + def _do_ps3_controllers(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.ps3controller import PS3ControllerSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + PS3ControllerSettingsWindow().get_root_widget()) + + def _do_360_controllers(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.xbox360controller import ( + XBox360ControllerSettingsWindow) + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + XBox360ControllerSettingsWindow().get_root_widget()) + + def _do_wiimotes(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.wiimote import WiimoteSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + WiimoteSettingsWindow().get_root_widget()) + + def _do_gamepads(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.gamepadselect import GamepadSelectWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window(GamepadSelectWindow().get_root_widget()) + + def _do_touchscreen(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.touchscreen import TouchscreenSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + TouchscreenSettingsWindow().get_root_widget()) + + def _save_state(self) -> None: + sel = self._root_widget.get_selected_child() + if sel == self._gamepads_button: + sel_name = 'GamePads' + elif sel == self._touch_button: + sel_name = 'Touch' + elif sel == self._keyboard_button: + sel_name = 'Keyboard' + elif sel == self._keyboard_2_button: + sel_name = 'Keyboard2' + elif sel == self._idevices_button: + sel_name = 'iDevices' + elif sel == self._ps3_button: + sel_name = 'PS3' + elif sel == self._xbox_360_button: + sel_name = 'xbox360' + elif sel == self._wiimotes_button: + sel_name = 'Wiimotes' + else: + sel_name = 'Back' + ba.app.ui.window_states[type(self)] = sel_name + + def _restore_state(self) -> None: + sel_name = ba.app.ui.window_states.get(type(self)) + if sel_name == 'GamePads': + sel = self._gamepads_button + elif sel_name == 'Touch': + sel = self._touch_button + elif sel_name == 'Keyboard': + sel = self._keyboard_button + elif sel_name == 'Keyboard2': + sel = self._keyboard_2_button + elif sel_name == 'iDevices': + sel = self._idevices_button + elif sel_name == 'PS3': + sel = self._ps3_button + elif sel_name == 'xbox360': + sel = self._xbox_360_button + elif sel_name == 'Wiimotes': + sel = self._wiimotes_button + elif sel_name == 'Back': + sel = self._back_button + else: + sel = (self._gamepads_button + if self._gamepads_button is not None else self._back_button) + ba.containerwidget(edit=self._root_widget, selected_child=sel) + + def _back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.allsettings import AllSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + AllSettingsWindow(transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/settings/gamepad.py b/dist/ba_data/python/bastd/ui/settings/gamepad.py new file mode 100644 index 0000000..4c34509 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/gamepad.py @@ -0,0 +1,835 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Settings UI functionality related to gamepads.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Dict, Any, Optional, Union, Tuple, Callable + + +class GamepadSettingsWindow(ba.Window): + """Window for configuring a gamepad.""" + + def __init__(self, + gamepad: ba.InputDevice, + is_main_menu: bool = True, + transition: str = 'in_right', + transition_out: str = 'out_right', + settings: dict = None): + self._input = gamepad + + # If our input-device went away, just return an empty zombie. + if not self._input: + return + + self._name = self._input.name + + self._r = 'configGamepadWindow' + self._settings = settings + self._transition_out = transition_out + + # We're a secondary gamepad if supplied with settings. + self._is_secondary = (settings is not None) + self._ext = '_B' if self._is_secondary else '' + self._is_main_menu = is_main_menu + self._displayname = self._name + self._width = 700 if self._is_secondary else 730 + self._height = 440 if self._is_secondary else 450 + self._spacing = 40 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + scale=(1.63 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(-20, -16) if uiscale is ba.UIScale.SMALL else (0, 0), + transition=transition)) + + # Don't ask to config joysticks while we're in here. + self._rebuild_ui() + + def _rebuild_ui(self) -> None: + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + from ba.internal import get_device_value + + # Clear existing UI. + for widget in self._root_widget.get_children(): + widget.delete() + + self._textwidgets: Dict[str, ba.Widget] = {} + + # If we were supplied with settings, we're a secondary joystick and + # just operate on that. in the other (normal) case we make our own. + if not self._is_secondary: + + # Fill our temp config with present values (for our primary and + # secondary controls). + self._settings = {} + for skey in [ + 'buttonJump', + 'buttonJump_B', + 'buttonPunch', + 'buttonPunch_B', + 'buttonBomb', + 'buttonBomb_B', + 'buttonPickUp', + 'buttonPickUp_B', + 'buttonStart', + 'buttonStart_B', + 'buttonStart2', + 'buttonStart2_B', + 'buttonUp', + 'buttonUp_B', + 'buttonDown', + 'buttonDown_B', + 'buttonLeft', + 'buttonLeft_B', + 'buttonRight', + 'buttonRight_B', + 'buttonRun1', + 'buttonRun1_B', + 'buttonRun2', + 'buttonRun2_B', + 'triggerRun1', + 'triggerRun1_B', + 'triggerRun2', + 'triggerRun2_B', + 'buttonIgnored', + 'buttonIgnored_B', + 'buttonIgnored2', + 'buttonIgnored2_B', + 'buttonIgnored3', + 'buttonIgnored3_B', + 'buttonIgnored4', + 'buttonIgnored4_B', + 'buttonVRReorient', + 'buttonVRReorient_B', + 'analogStickDeadZone', + 'analogStickDeadZone_B', + 'dpad', + 'dpad_B', + 'unassignedButtonsRun', + 'unassignedButtonsRun_B', + 'startButtonActivatesDefaultWidget', + 'startButtonActivatesDefaultWidget_B', + 'uiOnly', + 'uiOnly_B', + 'ignoreCompletely', + 'ignoreCompletely_B', + 'autoRecalibrateAnalogStick', + 'autoRecalibrateAnalogStick_B', + 'analogStickLR', + 'analogStickLR_B', + 'analogStickUD', + 'analogStickUD_B', + 'enableSecondary', + ]: + val = get_device_value(self._input, skey) + if val != -1: + self._settings[skey] = val + + back_button: Optional[ba.Widget] + + if self._is_secondary: + back_button = ba.buttonwidget(parent=self._root_widget, + position=(self._width - 180, + self._height - 65), + autoselect=True, + size=(160, 60), + label=ba.Lstr(resource='doneText'), + scale=0.9, + on_activate_call=self._save) + ba.containerwidget(edit=self._root_widget, + start_button=back_button, + on_cancel_call=back_button.activate) + cancel_button = None + else: + cancel_button = ba.buttonwidget( + parent=self._root_widget, + position=(51, self._height - 65), + autoselect=True, + size=(160, 60), + label=ba.Lstr(resource='cancelText'), + scale=0.9, + on_activate_call=self._cancel) + ba.containerwidget(edit=self._root_widget, + cancel_button=cancel_button) + + save_button: Optional[ba.Widget] + if not self._is_secondary: + save_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - (165 if self._is_secondary else 195), + self._height - 65), + size=((160 if self._is_secondary else 180), 60), + autoselect=True, + label=ba.Lstr(resource='doneText') + if self._is_secondary else ba.Lstr(resource='saveText'), + scale=0.9, + on_activate_call=self._save) + ba.containerwidget(edit=self._root_widget, + start_button=save_button) + else: + save_button = None + + if not self._is_secondary: + v = self._height - 59 + ba.textwidget(parent=self._root_widget, + position=(0, v + 5), + size=(self._width, 25), + text=ba.Lstr(resource=self._r + '.titleText'), + color=ba.app.ui.title_color, + maxwidth=310, + h_align='center', + v_align='center') + v -= 48 + + ba.textwidget(parent=self._root_widget, + position=(0, v + 3), + size=(self._width, 25), + text=self._name, + color=ba.app.ui.infotextcolor, + maxwidth=self._width * 0.9, + h_align='center', + v_align='center') + v -= self._spacing * 1 + + ba.textwidget(parent=self._root_widget, + position=(50, v + 10), + size=(self._width - 100, 30), + text=ba.Lstr(resource=self._r + '.appliesToAllText'), + maxwidth=330, + scale=0.65, + color=(0.5, 0.6, 0.5, 1.0), + h_align='center', + v_align='center') + v -= 70 + self._enable_check_box = None + else: + v = self._height - 49 + ba.textwidget(parent=self._root_widget, + position=(0, v + 5), + size=(self._width, 25), + text=ba.Lstr(resource=self._r + '.secondaryText'), + color=ba.app.ui.title_color, + maxwidth=300, + h_align='center', + v_align='center') + v -= self._spacing * 1 + + ba.textwidget(parent=self._root_widget, + position=(50, v + 10), + size=(self._width - 100, 30), + text=ba.Lstr(resource=self._r + '.secondHalfText'), + maxwidth=300, + scale=0.65, + color=(0.6, 0.8, 0.6, 1.0), + h_align='center') + self._enable_check_box = ba.checkboxwidget( + parent=self._root_widget, + position=(self._width * 0.5 - 80, v - 73), + value=self.get_enable_secondary_value(), + autoselect=True, + on_value_change_call=self._enable_check_box_changed, + size=(200, 30), + text=ba.Lstr(resource=self._r + '.secondaryEnableText'), + scale=1.2) + v = self._height - 205 + + h_offs = 160 + dist = 70 + d_color = (0.4, 0.4, 0.8) + sclx = 1.2 + scly = 0.98 + dpm = ba.Lstr(resource=self._r + '.pressAnyButtonOrDpadText') + dpm2 = ba.Lstr(resource=self._r + '.ifNothingHappensTryAnalogText') + self._capture_button(pos=(h_offs, v + scly * dist), + color=d_color, + button='buttonUp' + self._ext, + texture=ba.gettexture('upButton'), + scale=1.0, + message=dpm, + message2=dpm2) + self._capture_button(pos=(h_offs - sclx * dist, v), + color=d_color, + button='buttonLeft' + self._ext, + texture=ba.gettexture('leftButton'), + scale=1.0, + message=dpm, + message2=dpm2) + self._capture_button(pos=(h_offs + sclx * dist, v), + color=d_color, + button='buttonRight' + self._ext, + texture=ba.gettexture('rightButton'), + scale=1.0, + message=dpm, + message2=dpm2) + self._capture_button(pos=(h_offs, v - scly * dist), + color=d_color, + button='buttonDown' + self._ext, + texture=ba.gettexture('downButton'), + scale=1.0, + message=dpm, + message2=dpm2) + + dpm3 = ba.Lstr(resource=self._r + '.ifNothingHappensTryDpadText') + self._capture_button(pos=(h_offs + 130, v - 125), + color=(0.4, 0.4, 0.6), + button='analogStickLR' + self._ext, + maxwidth=140, + texture=ba.gettexture('analogStick'), + scale=1.2, + message=ba.Lstr(resource=self._r + + '.pressLeftRightText'), + message2=dpm3) + + self._capture_button(pos=(self._width * 0.5, v), + color=(0.4, 0.4, 0.6), + button='buttonStart' + self._ext, + texture=ba.gettexture('startButton'), + scale=0.7) + + h_offs = self._width - 160 + + self._capture_button(pos=(h_offs, v + scly * dist), + color=(0.6, 0.4, 0.8), + button='buttonPickUp' + self._ext, + texture=ba.gettexture('buttonPickUp'), + scale=1.0) + self._capture_button(pos=(h_offs - sclx * dist, v), + color=(0.7, 0.5, 0.1), + button='buttonPunch' + self._ext, + texture=ba.gettexture('buttonPunch'), + scale=1.0) + self._capture_button(pos=(h_offs + sclx * dist, v), + color=(0.5, 0.2, 0.1), + button='buttonBomb' + self._ext, + texture=ba.gettexture('buttonBomb'), + scale=1.0) + self._capture_button(pos=(h_offs, v - scly * dist), + color=(0.2, 0.5, 0.2), + button='buttonJump' + self._ext, + texture=ba.gettexture('buttonJump'), + scale=1.0) + + self._advanced_button = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + label=ba.Lstr(resource=self._r + '.advancedText'), + text_scale=0.9, + color=(0.45, 0.4, 0.5), + textcolor=(0.65, 0.6, 0.7), + position=(self._width - 300, 30), + size=(130, 40), + on_activate_call=self._do_advanced) + + try: + if cancel_button is not None and save_button is not None: + ba.widget(edit=cancel_button, right_widget=save_button) + ba.widget(edit=save_button, left_widget=cancel_button) + except Exception: + ba.print_exception('Error wiring up gamepad config window.') + + def get_r(self) -> str: + """(internal)""" + return self._r + + def get_advanced_button(self) -> ba.Widget: + """(internal)""" + return self._advanced_button + + def get_is_secondary(self) -> bool: + """(internal)""" + return self._is_secondary + + def get_settings(self) -> Dict[str, Any]: + """(internal)""" + assert self._settings is not None + return self._settings + + def get_ext(self) -> str: + """(internal)""" + return self._ext + + def get_input(self) -> ba.InputDevice: + """(internal)""" + return self._input + + def _do_advanced(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings import gamepadadvanced + gamepadadvanced.GamepadAdvancedSettingsWindow(self) + + def _enable_check_box_changed(self, value: bool) -> None: + assert self._settings is not None + if value: + self._settings['enableSecondary'] = 1 + else: + # Just clear since this is default. + if 'enableSecondary' in self._settings: + del self._settings['enableSecondary'] + + def get_unassigned_buttons_run_value(self) -> bool: + """(internal)""" + assert self._settings is not None + return self._settings.get('unassignedButtonsRun', True) + + def set_unassigned_buttons_run_value(self, value: bool) -> None: + """(internal)""" + assert self._settings is not None + if value: + if 'unassignedButtonsRun' in self._settings: + + # Clear since this is default. + del self._settings['unassignedButtonsRun'] + return + self._settings['unassignedButtonsRun'] = False + + def get_start_button_activates_default_widget_value(self) -> bool: + """(internal)""" + assert self._settings is not None + return self._settings.get('startButtonActivatesDefaultWidget', True) + + def set_start_button_activates_default_widget_value(self, + value: bool) -> None: + """(internal)""" + assert self._settings is not None + if value: + if 'startButtonActivatesDefaultWidget' in self._settings: + + # Clear since this is default. + del self._settings['startButtonActivatesDefaultWidget'] + return + self._settings['startButtonActivatesDefaultWidget'] = False + + def get_ui_only_value(self) -> bool: + """(internal)""" + assert self._settings is not None + return self._settings.get('uiOnly', False) + + def set_ui_only_value(self, value: bool) -> None: + """(internal)""" + assert self._settings is not None + if not value: + if 'uiOnly' in self._settings: + + # Clear since this is default. + del self._settings['uiOnly'] + return + self._settings['uiOnly'] = True + + def get_ignore_completely_value(self) -> bool: + """(internal)""" + assert self._settings is not None + return self._settings.get('ignoreCompletely', False) + + def set_ignore_completely_value(self, value: bool) -> None: + """(internal)""" + assert self._settings is not None + if not value: + if 'ignoreCompletely' in self._settings: + + # Clear since this is default. + del self._settings['ignoreCompletely'] + return + self._settings['ignoreCompletely'] = True + + def get_auto_recalibrate_analog_stick_value(self) -> bool: + """(internal)""" + assert self._settings is not None + return self._settings.get('autoRecalibrateAnalogStick', False) + + def set_auto_recalibrate_analog_stick_value(self, value: bool) -> None: + """(internal)""" + assert self._settings is not None + if not value: + if 'autoRecalibrateAnalogStick' in self._settings: + + # Clear since this is default. + del self._settings['autoRecalibrateAnalogStick'] + else: + self._settings['autoRecalibrateAnalogStick'] = True + + def get_enable_secondary_value(self) -> bool: + """(internal)""" + assert self._settings is not None + if not self._is_secondary: + raise Exception('enable value only applies to secondary editor') + return self._settings.get('enableSecondary', False) + + def show_secondary_editor(self) -> None: + """(internal)""" + GamepadSettingsWindow(self._input, + is_main_menu=False, + settings=self._settings, + transition='in_scale', + transition_out='out_scale') + + def get_control_value_name(self, control: str) -> Union[str, ba.Lstr]: + """(internal)""" + # pylint: disable=too-many-return-statements + assert self._settings is not None + if control == 'analogStickLR' + self._ext: + + # This actually shows both LR and UD. + sval1 = (self._settings['analogStickLR' + + self._ext] if 'analogStickLR' + self._ext + in self._settings else 5 if self._is_secondary else 1) + sval2 = (self._settings['analogStickUD' + + self._ext] if 'analogStickUD' + self._ext + in self._settings else 6 if self._is_secondary else 2) + return self._input.get_axis_name( + sval1) + ' / ' + self._input.get_axis_name(sval2) + + # If they're looking for triggers. + if control in ['triggerRun1' + self._ext, 'triggerRun2' + self._ext]: + if control in self._settings: + return self._input.get_axis_name(self._settings[control]) + return ba.Lstr(resource=self._r + '.unsetText') + + # Dead-zone. + if control == 'analogStickDeadZone' + self._ext: + if control in self._settings: + return str(self._settings[control]) + return str(1.0) + + # For dpad buttons: show individual buttons if any are set. + # Otherwise show whichever dpad is set (defaulting to 1). + dpad_buttons = [ + 'buttonLeft' + self._ext, 'buttonRight' + self._ext, + 'buttonUp' + self._ext, 'buttonDown' + self._ext + ] + if control in dpad_buttons: + + # If *any* dpad buttons are assigned, show only button assignments. + if any(b in self._settings for b in dpad_buttons): + if control in self._settings: + return self._input.get_button_name(self._settings[control]) + return ba.Lstr(resource=self._r + '.unsetText') + + # No dpad buttons - show the dpad number for all 4. + return ba.Lstr( + value='${A} ${B}', + subs=[('${A}', ba.Lstr(resource=self._r + '.dpadText')), + ('${B}', + str(self._settings['dpad' + + self._ext] if 'dpad' + self._ext in + self._settings else 2 if self._is_secondary else 1)) + ]) + + # other buttons.. + if control in self._settings: + return self._input.get_button_name(self._settings[control]) + return ba.Lstr(resource=self._r + '.unsetText') + + def _gamepad_event(self, control: str, event: Dict[str, Any], + dialog: AwaitGamepadInputWindow) -> None: + # pylint: disable=too-many-nested-blocks + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + assert self._settings is not None + ext = self._ext + + # For our dpad-buttons we're looking for either a button-press or a + # hat-switch press. + if control in [ + 'buttonUp' + ext, 'buttonLeft' + ext, 'buttonDown' + ext, + 'buttonRight' + ext + ]: + if event['type'] in ['BUTTONDOWN', 'HATMOTION']: + + # If its a button-down. + if event['type'] == 'BUTTONDOWN': + value = event['button'] + self._settings[control] = value + + # If its a dpad. + elif event['type'] == 'HATMOTION': + # clear out any set dir-buttons + for btn in [ + 'buttonUp' + ext, 'buttonLeft' + ext, + 'buttonRight' + ext, 'buttonDown' + ext + ]: + if btn in self._settings: + del self._settings[btn] + if event['hat'] == (2 if self._is_secondary else 1): + + # Exclude value in default case. + if 'dpad' + ext in self._settings: + del self._settings['dpad' + ext] + else: + self._settings['dpad' + ext] = event['hat'] + + # Update the 4 dpad button txt widgets. + ba.textwidget(edit=self._textwidgets['buttonUp' + ext], + text=self.get_control_value_name('buttonUp' + + ext)) + ba.textwidget(edit=self._textwidgets['buttonLeft' + ext], + text=self.get_control_value_name('buttonLeft' + + ext)) + ba.textwidget(edit=self._textwidgets['buttonRight' + ext], + text=self.get_control_value_name('buttonRight' + + ext)) + ba.textwidget(edit=self._textwidgets['buttonDown' + ext], + text=self.get_control_value_name('buttonDown' + + ext)) + ba.playsound(ba.getsound('gunCocking')) + dialog.die() + + elif control == 'analogStickLR' + ext: + if event['type'] == 'AXISMOTION': + + # Ignore small values or else we might get triggered by noise. + if abs(event['value']) > 0.5: + axis = event['axis'] + if axis == (5 if self._is_secondary else 1): + + # Exclude value in default case. + if 'analogStickLR' + ext in self._settings: + del self._settings['analogStickLR' + ext] + else: + self._settings['analogStickLR' + ext] = axis + ba.textwidget( + edit=self._textwidgets['analogStickLR' + ext], + text=self.get_control_value_name('analogStickLR' + + ext)) + ba.playsound(ba.getsound('gunCocking')) + dialog.die() + + # Now launch the up/down listener. + AwaitGamepadInputWindow( + self._input, 'analogStickUD' + ext, + self._gamepad_event, + ba.Lstr(resource=self._r + '.pressUpDownText')) + + elif control == 'analogStickUD' + ext: + if event['type'] == 'AXISMOTION': + + # Ignore small values or else we might get triggered by noise. + if abs(event['value']) > 0.5: + axis = event['axis'] + + # Ignore our LR axis. + if 'analogStickLR' + ext in self._settings: + lr_axis = self._settings['analogStickLR' + ext] + else: + lr_axis = (5 if self._is_secondary else 1) + if axis != lr_axis: + if axis == (6 if self._is_secondary else 2): + + # Exclude value in default case. + if 'analogStickUD' + ext in self._settings: + del self._settings['analogStickUD' + ext] + else: + self._settings['analogStickUD' + ext] = axis + ba.textwidget( + edit=self._textwidgets['analogStickLR' + ext], + text=self.get_control_value_name('analogStickLR' + + ext)) + ba.playsound(ba.getsound('gunCocking')) + dialog.die() + else: + # For other buttons we just want a button-press. + if event['type'] == 'BUTTONDOWN': + value = event['button'] + self._settings[control] = value + + # Update the button's text widget. + ba.textwidget(edit=self._textwidgets[control], + text=self.get_control_value_name(control)) + ba.playsound(ba.getsound('gunCocking')) + dialog.die() + + def _capture_button(self, + pos: Tuple[float, float], + color: Tuple[float, float, float], + texture: ba.Texture, + button: str, + scale: float = 1.0, + message: ba.Lstr = None, + message2: ba.Lstr = None, + maxwidth: float = 80.0) -> ba.Widget: + if message is None: + message = ba.Lstr(resource=self._r + '.pressAnyButtonText') + base_size = 79 + btn = ba.buttonwidget(parent=self._root_widget, + position=(pos[0] - base_size * 0.5 * scale, + pos[1] - base_size * 0.5 * scale), + autoselect=True, + size=(base_size * scale, base_size * scale), + texture=texture, + label='', + color=color) + + # Make this in a timer so that it shows up on top of all other buttons. + + def doit() -> None: + uiscale = 0.9 * scale + txt = ba.textwidget(parent=self._root_widget, + position=(pos[0] + 0.0 * scale, + pos[1] - 58.0 * scale), + color=(1, 1, 1, 0.3), + size=(0, 0), + h_align='center', + v_align='center', + scale=uiscale, + text=self.get_control_value_name(button), + maxwidth=maxwidth) + self._textwidgets[button] = txt + ba.buttonwidget(edit=btn, + on_activate_call=ba.Call(AwaitGamepadInputWindow, + self._input, button, + self._gamepad_event, + message, message2)) + + ba.timer(0, doit, timetype=ba.TimeType.REAL) + return btn + + def _cancel(self) -> None: + from bastd.ui.settings.controls import ControlsSettingsWindow + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + if self._is_main_menu: + ba.app.ui.set_main_menu_window( + ControlsSettingsWindow(transition='in_left').get_root_widget()) + + def _save(self) -> None: + from ba.internal import (master_server_post, get_input_device_config, + get_input_map_hash, should_submit_debug_info) + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + + # If we're a secondary editor we just go away (we were editing our + # parent's settings dict). + if self._is_secondary: + return + + assert self._settings is not None + if self._input: + dst = get_input_device_config(self._input, default=True) + dst2: Dict[str, Any] = dst[0][dst[1]] + dst2.clear() + + # Store any values that aren't -1. + for key, val in list(self._settings.items()): + if val != -1: + dst2[key] = val + + # If we're allowed to phone home, send this config so we can + # generate more defaults in the future. + inputhash = get_input_map_hash(self._input) + if should_submit_debug_info(): + master_server_post( + 'controllerConfig', { + 'ua': ba.app.user_agent_string, + 'b': ba.app.build_number, + 'name': self._name, + 'inputMapHash': inputhash, + 'config': dst2, + 'v': 2 + }) + ba.app.config.apply_and_commit() + ba.playsound(ba.getsound('gunCocking')) + else: + ba.playsound(ba.getsound('error')) + + if self._is_main_menu: + from bastd.ui.settings.controls import ControlsSettingsWindow + ba.app.ui.set_main_menu_window( + ControlsSettingsWindow(transition='in_left').get_root_widget()) + + +class AwaitGamepadInputWindow(ba.Window): + """Window for capturing a gamepad button press.""" + + def __init__( + self, + gamepad: ba.InputDevice, + button: str, + callback: Callable[[str, Dict[str, Any], AwaitGamepadInputWindow], + Any], + message: ba.Lstr = None, + message2: ba.Lstr = None): + if message is None: + print('AwaitGamepadInputWindow message is None!') + # Shouldn't get here. + message = ba.Lstr(value='Press any button...') + self._callback = callback + self._input = gamepad + self._capture_button = button + width = 400 + height = 150 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + scale=(2.0 if uiscale is ba.UIScale.SMALL else + 1.9 if uiscale is ba.UIScale.MEDIUM else 1.0), + size=(width, height), + transition='in_scale'), ) + ba.textwidget(parent=self._root_widget, + position=(0, (height - 60) if message2 is None else + (height - 50)), + size=(width, 25), + text=message, + maxwidth=width * 0.9, + h_align='center', + v_align='center') + if message2 is not None: + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, height - 60), + size=(0, 0), + text=message2, + maxwidth=width * 0.9, + scale=0.47, + color=(0.7, 1.0, 0.7, 0.6), + h_align='center', + v_align='center') + self._counter = 5 + self._count_down_text = ba.textwidget(parent=self._root_widget, + h_align='center', + position=(0, height - 110), + size=(width, 25), + color=(1, 1, 1, 0.3), + text=str(self._counter)) + self._decrement_timer: Optional[ba.Timer] = ba.Timer( + 1.0, + ba.Call(self._decrement), + repeat=True, + timetype=ba.TimeType.REAL) + _ba.capture_gamepad_input(ba.WeakCall(self._event_callback)) + + def __del__(self) -> None: + pass + + def die(self) -> None: + """Kill the window.""" + + # This strong-refs us; killing it allow us to die now. + self._decrement_timer = None + _ba.release_gamepad_input() + if self._root_widget: + ba.containerwidget(edit=self._root_widget, transition='out_scale') + + def _event_callback(self, event: Dict[str, Any]) -> None: + input_device = event['input_device'] + assert isinstance(input_device, ba.InputDevice) + + # Update - we now allow *any* input device of this type. + if (self._input and input_device + and input_device.name == self._input.name): + self._callback(self._capture_button, event, self) + + def _decrement(self) -> None: + self._counter -= 1 + if self._counter >= 1: + if self._count_down_text: + ba.textwidget(edit=self._count_down_text, + text=str(self._counter)) + else: + ba.playsound(ba.getsound('error')) + self.die() diff --git a/dist/ba_data/python/bastd/ui/settings/gamepadadvanced.py b/dist/ba_data/python/bastd/ui/settings/gamepadadvanced.py new file mode 100644 index 0000000..07c1074 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/gamepadadvanced.py @@ -0,0 +1,490 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to advanced gamepad configuring.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Dict, Tuple, Optional, Any + from bastd.ui.settings import gamepad as gpsui + + +class GamepadAdvancedSettingsWindow(ba.Window): + """Window for advanced gamepad configuration.""" + + def __init__(self, parent_window: gpsui.GamepadSettingsWindow): + # pylint: disable=too-many-statements + self._parent_window = parent_window + + app = ba.app + + self._r = parent_window.get_r() + uiscale = ba.app.ui.uiscale + self._width = 900 if uiscale is ba.UIScale.SMALL else 700 + self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = 402 if uiscale is ba.UIScale.SMALL else 512 + self._textwidgets: Dict[str, ba.Widget] = {} + super().__init__(root_widget=ba.containerwidget( + transition='in_scale', + size=(self._width, self._height), + scale=1.06 * (1.85 if uiscale is ba.UIScale.SMALL else + 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -25) if uiscale is ba.UIScale.SMALL else (0, 0), + scale_origin_stack_offset=(parent_window.get_advanced_button(). + get_screen_space_center()))) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - + (40 if uiscale is ba.UIScale.SMALL else 34)), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.advancedTitleText'), + maxwidth=320, + color=ba.app.ui.title_color, + h_align='center', + v_align='center') + + back_button = btn = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(self._width - (176 + x_inset), self._height - + (60 if uiscale is ba.UIScale.SMALL else 55)), + size=(120, 48), + text_scale=0.8, + label=ba.Lstr(resource='doneText'), + on_activate_call=self._done) + ba.containerwidget(edit=self._root_widget, + start_button=btn, + on_cancel_call=btn.activate) + + self._scroll_width = self._width - (100 + 2 * x_inset) + self._scroll_height = self._height - 110 + self._sub_width = self._scroll_width - 20 + self._sub_height = (940 if self._parent_window.get_is_secondary() else + 1040) + if app.vr_mode: + self._sub_height += 50 + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + position=((self._width - self._scroll_width) * 0.5, + self._height - 65 - self._scroll_height), + size=(self._scroll_width, self._scroll_height), + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + + h = 30 + v = self._sub_height - 10 + + h2 = h + 12 + + # don't allow secondary joysticks to handle unassigned buttons + if not self._parent_window.get_is_secondary(): + v -= 40 + cb1 = ba.checkboxwidget( + parent=self._subcontainer, + position=(h + 70, v), + size=(500, 30), + text=ba.Lstr(resource=self._r + '.unassignedButtonsRunText'), + textcolor=(0.8, 0.8, 0.8), + maxwidth=330, + scale=1.0, + on_value_change_call=self._parent_window. + set_unassigned_buttons_run_value, + autoselect=True, + value=self._parent_window.get_unassigned_buttons_run_value()) + ba.widget(edit=cb1, up_widget=back_button) + v -= 60 + capb = self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.runButton1Text'), + control='buttonRun1' + self._parent_window.get_ext()) + if self._parent_window.get_is_secondary(): + for widget in capb: + ba.widget(edit=widget, up_widget=back_button) + v -= 42 + self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.runButton2Text'), + control='buttonRun2' + self._parent_window.get_ext()) + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, v - 24), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.runTriggerDescriptionText'), + color=(0.7, 1, 0.7, 0.6), + maxwidth=self._sub_width * 0.8, + scale=0.7, + h_align='center', + v_align='center') + + v -= 85 + + self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.runTrigger1Text'), + control='triggerRun1' + self._parent_window.get_ext(), + message=ba.Lstr(resource=self._r + '.pressAnyAnalogTriggerText')) + v -= 42 + self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.runTrigger2Text'), + control='triggerRun2' + self._parent_window.get_ext(), + message=ba.Lstr(resource=self._r + '.pressAnyAnalogTriggerText')) + + # in vr mode, allow assigning a reset-view button + if app.vr_mode: + v -= 50 + self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.vrReorientButtonText'), + control='buttonVRReorient' + self._parent_window.get_ext()) + + v -= 60 + self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.extraStartButtonText'), + control='buttonStart2' + self._parent_window.get_ext()) + v -= 60 + self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.ignoredButton1Text'), + control='buttonIgnored' + self._parent_window.get_ext()) + v -= 42 + self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.ignoredButton2Text'), + control='buttonIgnored2' + self._parent_window.get_ext()) + v -= 42 + self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.ignoredButton3Text'), + control='buttonIgnored3' + self._parent_window.get_ext()) + v -= 42 + self._capture_button( + pos=(h2, v), + name=ba.Lstr(resource=self._r + '.ignoredButton4Text'), + control='buttonIgnored4' + self._parent_window.get_ext()) + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, v - 14), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.ignoredButtonDescriptionText'), + color=(0.7, 1, 0.7, 0.6), + scale=0.8, + maxwidth=self._sub_width * 0.8, + h_align='center', + v_align='center') + + v -= 80 + ba.checkboxwidget(parent=self._subcontainer, + autoselect=True, + position=(h + 50, v), + size=(400, 30), + text=ba.Lstr(resource=self._r + + '.startButtonActivatesDefaultText'), + textcolor=(0.8, 0.8, 0.8), + maxwidth=450, + scale=0.9, + on_value_change_call=self._parent_window. + set_start_button_activates_default_widget_value, + value=self._parent_window. + get_start_button_activates_default_widget_value()) + ba.textwidget( + parent=self._subcontainer, + position=(self._sub_width * 0.5, v - 12), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.startButtonActivatesDefaultDescriptionText'), + color=(0.7, 1, 0.7, 0.6), + maxwidth=self._sub_width * 0.8, + scale=0.7, + h_align='center', + v_align='center') + + v -= 80 + ba.checkboxwidget( + parent=self._subcontainer, + autoselect=True, + position=(h + 50, v), + size=(400, 30), + text=ba.Lstr(resource=self._r + '.uiOnlyText'), + textcolor=(0.8, 0.8, 0.8), + maxwidth=450, + scale=0.9, + on_value_change_call=self._parent_window.set_ui_only_value, + value=self._parent_window.get_ui_only_value()) + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, v - 12), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.uiOnlyDescriptionText'), + color=(0.7, 1, 0.7, 0.6), + maxwidth=self._sub_width * 0.8, + scale=0.7, + h_align='center', + v_align='center') + + v -= 80 + ba.checkboxwidget( + parent=self._subcontainer, + autoselect=True, + position=(h + 50, v), + size=(400, 30), + text=ba.Lstr(resource=self._r + '.ignoreCompletelyText'), + textcolor=(0.8, 0.8, 0.8), + maxwidth=450, + scale=0.9, + on_value_change_call=self._parent_window. + set_ignore_completely_value, + value=self._parent_window.get_ignore_completely_value()) + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, v - 12), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.ignoreCompletelyDescriptionText'), + color=(0.7, 1, 0.7, 0.6), + maxwidth=self._sub_width * 0.8, + scale=0.7, + h_align='center', + v_align='center') + + v -= 80 + + cb1 = ba.checkboxwidget( + parent=self._subcontainer, + autoselect=True, + position=(h + 50, v), + size=(400, 30), + text=ba.Lstr(resource=self._r + '.autoRecalibrateText'), + textcolor=(0.8, 0.8, 0.8), + maxwidth=450, + scale=0.9, + on_value_change_call=self._parent_window. + set_auto_recalibrate_analog_stick_value, + value=self._parent_window.get_auto_recalibrate_analog_stick_value( + )) + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, v - 12), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.autoRecalibrateDescriptionText'), + color=(0.7, 1, 0.7, 0.6), + maxwidth=self._sub_width * 0.8, + scale=0.7, + h_align='center', + v_align='center') + v -= 80 + + buttons = self._config_value_editor( + ba.Lstr(resource=self._r + '.analogStickDeadZoneText'), + control=('analogStickDeadZone' + self._parent_window.get_ext()), + position=(h + 40, v), + min_val=0, + max_val=10.0, + increment=0.1, + x_offset=100) + ba.widget(edit=buttons[0], left_widget=cb1, up_widget=cb1) + ba.widget(edit=cb1, right_widget=buttons[0], down_widget=buttons[0]) + + ba.textwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5, v - 12), + size=(0, 0), + text=ba.Lstr(resource=self._r + + '.analogStickDeadZoneDescriptionText'), + color=(0.7, 1, 0.7, 0.6), + maxwidth=self._sub_width * 0.8, + scale=0.7, + h_align='center', + v_align='center') + v -= 100 + + # child joysticks cant have child joysticks.. that's just + # crazy talk + if not self._parent_window.get_is_secondary(): + ba.buttonwidget( + parent=self._subcontainer, + autoselect=True, + label=ba.Lstr(resource=self._r + '.twoInOneSetupText'), + position=(40, v), + size=(self._sub_width - 80, 50), + on_activate_call=self._parent_window.show_secondary_editor, + up_widget=buttons[0]) + + # set a bigger bottom show-buffer for the widgets we just made + # so we can see the text below them when navigating with + # a gamepad + for child in self._subcontainer.get_children(): + ba.widget(edit=child, show_buffer_bottom=30, show_buffer_top=30) + + def _capture_button( + self, + pos: Tuple[float, float], + name: ba.Lstr, + control: str, + message: Optional[ba.Lstr] = None) -> Tuple[ba.Widget, ba.Widget]: + if message is None: + message = ba.Lstr(resource=self._parent_window.get_r() + + '.pressAnyButtonText') + btn = ba.buttonwidget(parent=self._subcontainer, + autoselect=True, + position=(pos[0], pos[1]), + label=name, + size=(250, 60), + scale=0.7) + btn2 = ba.buttonwidget(parent=self._subcontainer, + autoselect=True, + position=(pos[0] + 400, pos[1] + 2), + left_widget=btn, + color=(0.45, 0.4, 0.5), + textcolor=(0.65, 0.6, 0.7), + label=ba.Lstr(resource=self._r + '.clearText'), + size=(110, 50), + scale=0.7, + on_activate_call=ba.Call( + self._clear_control, control)) + ba.widget(edit=btn, right_widget=btn2) + + # make this in a timer so that it shows up on top of all + # other buttons + + def doit() -> None: + from bastd.ui.settings import gamepad + txt = ba.textwidget( + parent=self._subcontainer, + position=(pos[0] + 285, pos[1] + 20), + color=(1, 1, 1, 0.3), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.7, + text=self._parent_window.get_control_value_name(control), + maxwidth=200) + self._textwidgets[control] = txt + ba.buttonwidget(edit=btn, + on_activate_call=ba.Call( + gamepad.AwaitGamepadInputWindow, + self._parent_window.get_input(), control, + self._gamepad_event, message)) + + ba.timer(0, doit, timetype=ba.TimeType.REAL) + return btn, btn2 + + def _inc(self, control: str, min_val: float, max_val: float, + inc: float) -> None: + val = self._parent_window.get_settings().get(control, 1.0) + val = min(max_val, max(min_val, val + inc)) + if abs(1.0 - val) < 0.001: + if control in self._parent_window.get_settings(): + del self._parent_window.get_settings()[control] + else: + self._parent_window.get_settings()[control] = round(val, 1) + ba.textwidget(edit=self._textwidgets[control], + text=self._parent_window.get_control_value_name(control)) + + def _config_value_editor( + self, + name: ba.Lstr, + control: str, + position: Tuple[float, float], + min_val: float = 0.0, + max_val: float = 100.0, + increment: float = 1.0, + change_sound: bool = True, + x_offset: float = 0.0, + displayname: ba.Lstr = None) -> Tuple[ba.Widget, ba.Widget]: + + if displayname is None: + displayname = name + ba.textwidget(parent=self._subcontainer, + position=position, + size=(100, 30), + text=displayname, + color=(0.8, 0.8, 0.8, 1.0), + h_align='left', + v_align='center', + scale=1.0, + maxwidth=280) + self._textwidgets[control] = ba.textwidget( + parent=self._subcontainer, + position=(246.0 + x_offset, position[1]), + size=(60, 28), + editable=False, + color=(0.3, 1.0, 0.3, 1.0), + h_align='right', + v_align='center', + text=self._parent_window.get_control_value_name(control), + padding=2) + btn = ba.buttonwidget(parent=self._subcontainer, + autoselect=True, + position=(330 + x_offset, position[1] + 4), + size=(28, 28), + label='-', + on_activate_call=ba.Call(self._inc, control, + min_val, max_val, + -increment), + repeat=True, + enable_sound=(change_sound is True)) + btn2 = ba.buttonwidget(parent=self._subcontainer, + autoselect=True, + position=(380 + x_offset, position[1] + 4), + size=(28, 28), + label='+', + on_activate_call=ba.Call( + self._inc, control, min_val, max_val, + increment), + repeat=True, + enable_sound=(change_sound is True)) + return btn, btn2 + + def _clear_control(self, control: str) -> None: + if control in self._parent_window.get_settings(): + del self._parent_window.get_settings()[control] + ba.textwidget(edit=self._textwidgets[control], + text=self._parent_window.get_control_value_name(control)) + + def _gamepad_event(self, control: str, event: Dict[str, Any], + dialog: gpsui.AwaitGamepadInputWindow) -> None: + ext = self._parent_window.get_ext() + if control in ['triggerRun1' + ext, 'triggerRun2' + ext]: + if event['type'] == 'AXISMOTION': + # ignore small values or else we might get triggered + # by noise + if abs(event['value']) > 0.5: + self._parent_window.get_settings()[control] = ( + event['axis']) + # update the button's text widget + if self._textwidgets[control]: + ba.textwidget( + edit=self._textwidgets[control], + text=self._parent_window.get_control_value_name( + control)) + ba.playsound(ba.getsound('gunCocking')) + dialog.die() + else: + if event['type'] == 'BUTTONDOWN': + value = event['button'] + self._parent_window.get_settings()[control] = value + # update the button's text widget + if self._textwidgets[control]: + ba.textwidget( + edit=self._textwidgets[control], + text=self._parent_window.get_control_value_name( + control)) + ba.playsound(ba.getsound('gunCocking')) + dialog.die() + + def _done(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/dist/ba_data/python/bastd/ui/settings/gamepadselect.py b/dist/ba_data/python/bastd/ui/settings/gamepadselect.py new file mode 100644 index 0000000..71b867c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/gamepadselect.py @@ -0,0 +1,151 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Settings UI related to gamepad functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Dict, Any + + +def gamepad_configure_callback(event: Dict[str, Any]) -> None: + """Respond to a gamepad button press during config selection.""" + from ba.internal import get_remote_app_name + from bastd.ui.settings import gamepad + + # Ignore all but button-presses. + if event['type'] not in ['BUTTONDOWN', 'HATMOTION']: + return + _ba.release_gamepad_input() + try: + ba.app.ui.clear_main_menu_window(transition='out_left') + except Exception: + ba.print_exception('Error transitioning out main_menu_window.') + ba.playsound(ba.getsound('activateBeep')) + ba.playsound(ba.getsound('swish')) + inputdevice = event['input_device'] + assert isinstance(inputdevice, ba.InputDevice) + if inputdevice.allows_configuring: + ba.app.ui.set_main_menu_window( + gamepad.GamepadSettingsWindow(inputdevice).get_root_widget()) + else: + width = 700 + height = 200 + button_width = 100 + uiscale = ba.app.ui.uiscale + dlg = (ba.containerwidget( + scale=(1.7 if uiscale is ba.UIScale.SMALL else + 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0), + size=(width, height), + transition='in_right')) + ba.app.ui.set_main_menu_window(dlg) + device_name = inputdevice.name + if device_name == 'iDevice': + msg = ba.Lstr(resource='bsRemoteConfigureInAppText', + subs=[('${REMOTE_APP_NAME}', get_remote_app_name())]) + else: + msg = ba.Lstr(resource='cantConfigureDeviceText', + subs=[('${DEVICE}', device_name)]) + ba.textwidget(parent=dlg, + position=(0, height - 80), + size=(width, 25), + text=msg, + scale=0.8, + h_align='center', + v_align='top') + + def _ok() -> None: + from bastd.ui.settings import controls + ba.containerwidget(edit=dlg, transition='out_right') + ba.app.ui.set_main_menu_window( + controls.ControlsSettingsWindow( + transition='in_left').get_root_widget()) + + ba.buttonwidget(parent=dlg, + position=((width - button_width) / 2, 20), + size=(button_width, 60), + label=ba.Lstr(resource='okText'), + on_activate_call=_ok) + + +class GamepadSelectWindow(ba.Window): + """Window for selecting a gamepad to configure.""" + + def __init__(self) -> None: + from typing import cast + width = 480 + height = 170 + spacing = 40 + self._r = 'configGamepadSelectWindow' + + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + scale=(2.3 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + size=(width, height), + transition='in_right', + )) + + btn = ba.buttonwidget(parent=self._root_widget, + position=(20, height - 60), + size=(130, 60), + label=ba.Lstr(resource='backText'), + button_type='back', + scale=0.8, + on_activate_call=self._back) + # Let's not have anything selected by default; its misleading looking + # for the controller getting configured. + ba.containerwidget(edit=self._root_widget, + cancel_button=btn, + selected_child=cast(ba.Widget, 0)) + ba.textwidget(parent=self._root_widget, + position=(20, height - 50), + size=(width, 25), + text=ba.Lstr(resource=self._r + '.titleText'), + maxwidth=250, + color=ba.app.ui.title_color, + h_align='center', + v_align='center') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + v: float = height - 60 + v -= spacing + ba.textwidget(parent=self._root_widget, + position=(15, v), + size=(width - 30, 30), + scale=0.8, + text=ba.Lstr(resource=self._r + '.pressAnyButtonText'), + maxwidth=width * 0.95, + color=ba.app.ui.infotextcolor, + h_align='center', + v_align='top') + v -= spacing * 1.24 + if ba.app.platform == 'android': + ba.textwidget(parent=self._root_widget, + position=(15, v), + size=(width - 30, 30), + scale=0.46, + text=ba.Lstr(resource=self._r + '.androidNoteText'), + maxwidth=width * 0.95, + color=(0.7, 0.9, 0.7, 0.5), + h_align='center', + v_align='top') + + _ba.capture_gamepad_input(gamepad_configure_callback) + + def _back(self) -> None: + from bastd.ui.settings import controls + _ba.release_gamepad_input() + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + controls.ControlsSettingsWindow( + transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/settings/graphics.py b/dist/ba_data/python/bastd/ui/settings/graphics.py new file mode 100644 index 0000000..df3e89c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/graphics.py @@ -0,0 +1,389 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for graphics settings.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Tuple, Optional + + +class GraphicsSettingsWindow(ba.Window): + """Window for graphics settings.""" + + def __init__(self, + transition: str = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + from bastd.ui import popup + from bastd.ui.config import ConfigCheckBox, ConfigNumberEdit + # if they provided an origin-widget, scale up from that + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + self._r = 'graphicsSettingsWindow' + app = ba.app + + spacing = 32 + self._have_selected_child = False + uiscale = app.ui.uiscale + width = 450.0 + height = 302.0 + + self._show_fullscreen = False + fullscreen_spacing_top = spacing * 0.2 + fullscreen_spacing = spacing * 1.2 + if uiscale == ba.UIScale.LARGE and app.platform != 'android': + self._show_fullscreen = True + height += fullscreen_spacing + fullscreen_spacing_top + + show_gamma = False + gamma_spacing = spacing * 1.3 + if _ba.has_gamma_control(): + show_gamma = True + height += gamma_spacing + + show_vsync = False + if app.platform == 'mac': + show_vsync = True + + show_resolution = True + if app.vr_mode: + show_resolution = (app.platform == 'android' + and app.subplatform == 'cardboard') + + uiscale = ba.app.ui.uiscale + base_scale = (2.4 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0) + popup_menu_scale = base_scale * 1.2 + v = height - 50 + v -= spacing * 1.15 + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition=transition, + scale_origin_stack_offset=scale_origin, + scale=base_scale, + stack_offset=(0, -30) if uiscale is ba.UIScale.SMALL else (0, 0))) + + btn = ba.buttonwidget(parent=self._root_widget, + position=(35, height - 50), + size=(120, 60), + scale=0.8, + text_scale=1.2, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._back) + + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(0, height - 44), + size=(width, 25), + text=ba.Lstr(resource=self._r + '.titleText'), + color=ba.app.ui.title_color, + h_align='center', + v_align='top') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + self._fullscreen_checkbox: Optional[ba.Widget] + if self._show_fullscreen: + v -= fullscreen_spacing_top + self._fullscreen_checkbox = ConfigCheckBox( + parent=self._root_widget, + position=(100, v), + maxwidth=200, + size=(300, 30), + configkey='Fullscreen', + displayname=ba.Lstr(resource=self._r + + ('.fullScreenCmdText' if app.platform == + 'mac' else '.fullScreenCtrlText'))).widget + if not self._have_selected_child: + ba.containerwidget(edit=self._root_widget, + selected_child=self._fullscreen_checkbox) + self._have_selected_child = True + v -= fullscreen_spacing + else: + self._fullscreen_checkbox = None + + self._gamma_controls: Optional[ConfigNumberEdit] + if show_gamma: + self._gamma_controls = gmc = ConfigNumberEdit( + parent=self._root_widget, + position=(90, v), + configkey='Screen Gamma', + displayname=ba.Lstr(resource=self._r + '.gammaText'), + minval=0.1, + maxval=2.0, + increment=0.1, + xoffset=-70, + textscale=0.85) + if ba.app.ui.use_toolbars: + ba.widget(edit=gmc.plusbutton, + right_widget=_ba.get_special_widget('party_button')) + if not self._have_selected_child: + ba.containerwidget(edit=self._root_widget, + selected_child=gmc.minusbutton) + self._have_selected_child = True + v -= gamma_spacing + else: + self._gamma_controls = None + + self._selected_color = (0.5, 1, 0.5, 1) + self._unselected_color = (0.7, 0.7, 0.7, 1) + + # quality + ba.textwidget(parent=self._root_widget, + position=(60, v), + size=(160, 25), + text=ba.Lstr(resource=self._r + '.visualsText'), + color=ba.app.ui.heading_color, + scale=0.65, + maxwidth=150, + h_align='center', + v_align='center') + popup.PopupMenu( + parent=self._root_widget, + position=(60, v - 50), + width=150, + scale=popup_menu_scale, + choices=['Auto', 'Higher', 'High', 'Medium', 'Low'], + choices_disabled=['Higher', 'High'] + if _ba.get_max_graphics_quality() == 'Medium' else [], + choices_display=[ + ba.Lstr(resource='autoText'), + ba.Lstr(resource=self._r + '.higherText'), + ba.Lstr(resource=self._r + '.highText'), + ba.Lstr(resource=self._r + '.mediumText'), + ba.Lstr(resource=self._r + '.lowText') + ], + current_choice=ba.app.config.resolve('Graphics Quality'), + on_value_change_call=self._set_quality) + + # texture controls + ba.textwidget(parent=self._root_widget, + position=(230, v), + size=(160, 25), + text=ba.Lstr(resource=self._r + '.texturesText'), + color=ba.app.ui.heading_color, + scale=0.65, + maxwidth=150, + h_align='center', + v_align='center') + textures_popup = popup.PopupMenu( + parent=self._root_widget, + position=(230, v - 50), + width=150, + scale=popup_menu_scale, + choices=['Auto', 'High', 'Medium', 'Low'], + choices_display=[ + ba.Lstr(resource='autoText'), + ba.Lstr(resource=self._r + '.highText'), + ba.Lstr(resource=self._r + '.mediumText'), + ba.Lstr(resource=self._r + '.lowText') + ], + current_choice=ba.app.config.resolve('Texture Quality'), + on_value_change_call=self._set_textures) + if ba.app.ui.use_toolbars: + ba.widget(edit=textures_popup.get_button(), + right_widget=_ba.get_special_widget('party_button')) + v -= 80 + + h_offs = 0 + + if show_resolution: + # resolution + ba.textwidget(parent=self._root_widget, + position=(h_offs + 60, v), + size=(160, 25), + text=ba.Lstr(resource=self._r + '.resolutionText'), + color=ba.app.ui.heading_color, + scale=0.65, + maxwidth=150, + h_align='center', + v_align='center') + + # on standard android we have 'Auto', 'Native', and a few + # HD standards + if app.platform == 'android': + # on cardboard/daydream android we have a few + # render-target-scale options + if app.subplatform == 'cardboard': + current_res_cardboard = (str(min(100, max(10, int(round( + ba.app.config.resolve('GVR Render Target Scale') + * 100.0))))) + '%') # yapf: disable + popup.PopupMenu( + parent=self._root_widget, + position=(h_offs + 60, v - 50), + width=120, + scale=popup_menu_scale, + choices=['100%', '75%', '50%', '35%'], + current_choice=current_res_cardboard, + on_value_change_call=self._set_gvr_render_target_scale) + else: + native_res = _ba.get_display_resolution() + assert native_res is not None + choices = ['Auto', 'Native'] + choices_display = [ + ba.Lstr(resource='autoText'), + ba.Lstr(resource='nativeText') + ] + for res in [1440, 1080, 960, 720, 480]: + # nav bar is 72px so lets allow for that in what + # choices we show + if native_res[1] >= res - 72: + res_str = str(res) + 'p' + choices.append(res_str) + choices_display.append(ba.Lstr(value=res_str)) + current_res_android = ba.app.config.resolve( + 'Resolution (Android)') + popup.PopupMenu(parent=self._root_widget, + position=(h_offs + 60, v - 50), + width=120, + scale=popup_menu_scale, + choices=choices, + choices_display=choices_display, + current_choice=current_res_android, + on_value_change_call=self._set_android_res) + else: + # if we're on a system that doesn't allow setting resolution, + # set pixel-scale instead + current_res = _ba.get_display_resolution() + if current_res is None: + current_res2 = (str(min(100, max(10, int(round( + ba.app.config.resolve('Screen Pixel Scale') + * 100.0))))) + '%') # yapf: disable + popup.PopupMenu( + parent=self._root_widget, + position=(h_offs + 60, v - 50), + width=120, + scale=popup_menu_scale, + choices=['100%', '88%', '75%', '63%', '50%'], + current_choice=current_res2, + on_value_change_call=self._set_pixel_scale) + else: + raise Exception('obsolete path; discrete resolutions' + ' no longer supported') + + # vsync + if show_vsync: + ba.textwidget(parent=self._root_widget, + position=(230, v), + size=(160, 25), + text=ba.Lstr(resource=self._r + '.verticalSyncText'), + color=ba.app.ui.heading_color, + scale=0.65, + maxwidth=150, + h_align='center', + v_align='center') + + popup.PopupMenu( + parent=self._root_widget, + position=(230, v - 50), + width=150, + scale=popup_menu_scale, + choices=['Auto', 'Always', 'Never'], + choices_display=[ + ba.Lstr(resource='autoText'), + ba.Lstr(resource=self._r + '.alwaysText'), + ba.Lstr(resource=self._r + '.neverText') + ], + current_choice=ba.app.config.resolve('Vertical Sync'), + on_value_change_call=self._set_vsync) + + v -= 90 + fpsc = ConfigCheckBox(parent=self._root_widget, + position=(69, v - 6), + size=(210, 30), + scale=0.86, + configkey='Show FPS', + displayname=ba.Lstr(resource=self._r + + '.showFPSText'), + maxwidth=130) + + # (tv mode doesnt apply to vr) + if not ba.app.vr_mode: + tvc = ConfigCheckBox(parent=self._root_widget, + position=(240, v - 6), + size=(210, 30), + scale=0.86, + configkey='TV Border', + displayname=ba.Lstr(resource=self._r + + '.tvBorderText'), + maxwidth=130) + # grumble.. + ba.widget(edit=fpsc.widget, right_widget=tvc.widget) + try: + pass + + except Exception: + ba.print_exception('Exception wiring up graphics settings UI:') + + v -= spacing + + # make a timer to update our controls in case the config changes + # under us + self._update_timer = ba.Timer(0.25, + ba.WeakCall(self._update_controls), + repeat=True, + timetype=ba.TimeType.REAL) + + def _back(self) -> None: + from bastd.ui.settings import allsettings + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + allsettings.AllSettingsWindow( + transition='in_left').get_root_widget()) + + def _set_quality(self, quality: str) -> None: + cfg = ba.app.config + cfg['Graphics Quality'] = quality + cfg.apply_and_commit() + + def _set_textures(self, val: str) -> None: + cfg = ba.app.config + cfg['Texture Quality'] = val + cfg.apply_and_commit() + + def _set_android_res(self, val: str) -> None: + cfg = ba.app.config + cfg['Resolution (Android)'] = val + cfg.apply_and_commit() + + def _set_pixel_scale(self, res: str) -> None: + cfg = ba.app.config + cfg['Screen Pixel Scale'] = float(res[:-1]) / 100.0 + cfg.apply_and_commit() + + def _set_gvr_render_target_scale(self, res: str) -> None: + cfg = ba.app.config + cfg['GVR Render Target Scale'] = float(res[:-1]) / 100.0 + cfg.apply_and_commit() + + def _set_vsync(self, val: str) -> None: + cfg = ba.app.config + cfg['Vertical Sync'] = val + cfg.apply_and_commit() + + def _update_controls(self) -> None: + if self._show_fullscreen: + ba.checkboxwidget(edit=self._fullscreen_checkbox, + value=ba.app.config.resolve('Fullscreen')) diff --git a/dist/ba_data/python/bastd/ui/settings/keyboard.py b/dist/ba_data/python/bastd/ui/settings/keyboard.py new file mode 100644 index 0000000..7482381 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/keyboard.py @@ -0,0 +1,308 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Keyboard settings related UI functionality.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Dict, Tuple, Any, Optional + + +class ConfigKeyboardWindow(ba.Window): + """Window for configuring keyboards.""" + + def __init__(self, c: ba.InputDevice, transition: str = 'in_right'): + self._r = 'configKeyboardWindow' + self._input = c + self._name = self._input.name + self._unique_id = self._input.unique_identifier + dname_raw = self._name + if self._unique_id != '#1': + dname_raw += ' ' + self._unique_id.replace('#', 'P') + self._displayname = ba.Lstr(translate=('inputDeviceNames', dname_raw)) + self._width = 700 + if self._unique_id != '#1': + self._height = 480 + else: + self._height = 375 + self._spacing = 40 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + scale=(1.6 if uiscale is ba.UIScale.SMALL else + 1.3 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, 5) if uiscale is ba.UIScale.SMALL else (0, 0), + transition=transition)) + + self._rebuild_ui() + + def _rebuild_ui(self) -> None: + from ba.internal import get_device_value + for widget in self._root_widget.get_children(): + widget.delete() + + # Fill our temp config with present values. + self._settings: Dict[str, int] = {} + for button in [ + 'buttonJump', 'buttonPunch', 'buttonBomb', 'buttonPickUp', + 'buttonStart', 'buttonStart2', 'buttonUp', 'buttonDown', + 'buttonLeft', 'buttonRight' + ]: + self._settings[button] = get_device_value(self._input, button) + + cancel_button = ba.buttonwidget(parent=self._root_widget, + autoselect=True, + position=(38, self._height - 85), + size=(170, 60), + label=ba.Lstr(resource='cancelText'), + scale=0.9, + on_activate_call=self._cancel) + save_button = ba.buttonwidget(parent=self._root_widget, + autoselect=True, + position=(self._width - 190, + self._height - 85), + size=(180, 60), + label=ba.Lstr(resource='saveText'), + scale=0.9, + text_scale=0.9, + on_activate_call=self._save) + ba.containerwidget(edit=self._root_widget, + cancel_button=cancel_button, + start_button=save_button) + + ba.widget(edit=cancel_button, right_widget=save_button) + ba.widget(edit=save_button, left_widget=cancel_button) + + v = self._height - 74.0 + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, v + 15), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.configuringText', + subs=[('${DEVICE}', self._displayname)]), + color=ba.app.ui.title_color, + h_align='center', + v_align='center', + maxwidth=270, + scale=0.83) + v -= 20 + + if self._unique_id != '#1': + v -= 20 + v -= self._spacing + ba.textwidget(parent=self._root_widget, + position=(0, v + 19), + size=(self._width, 50), + text=ba.Lstr(resource=self._r + + '.keyboard2NoteText'), + scale=0.7, + maxwidth=self._width * 0.75, + max_height=110, + color=ba.app.ui.infotextcolor, + h_align='center', + v_align='top') + v -= 40 + v -= 10 + v -= self._spacing * 2.2 + v += 25 + v -= 42 + h_offs = 160 + dist = 70 + d_color = (0.4, 0.4, 0.8) + self._capture_button(pos=(h_offs, v + 0.95 * dist), + color=d_color, + button='buttonUp', + texture=ba.gettexture('upButton'), + scale=1.0) + self._capture_button(pos=(h_offs - 1.2 * dist, v), + color=d_color, + button='buttonLeft', + texture=ba.gettexture('leftButton'), + scale=1.0) + self._capture_button(pos=(h_offs + 1.2 * dist, v), + color=d_color, + button='buttonRight', + texture=ba.gettexture('rightButton'), + scale=1.0) + self._capture_button(pos=(h_offs, v - 0.95 * dist), + color=d_color, + button='buttonDown', + texture=ba.gettexture('downButton'), + scale=1.0) + + if self._unique_id == '#2': + self._capture_button(pos=(self._width * 0.5, v + 0.1 * dist), + color=(0.4, 0.4, 0.6), + button='buttonStart', + texture=ba.gettexture('startButton'), + scale=0.8) + + h_offs = self._width - 160 + + self._capture_button(pos=(h_offs, v + 0.95 * dist), + color=(0.6, 0.4, 0.8), + button='buttonPickUp', + texture=ba.gettexture('buttonPickUp'), + scale=1.0) + self._capture_button(pos=(h_offs - 1.2 * dist, v), + color=(0.7, 0.5, 0.1), + button='buttonPunch', + texture=ba.gettexture('buttonPunch'), + scale=1.0) + self._capture_button(pos=(h_offs + 1.2 * dist, v), + color=(0.5, 0.2, 0.1), + button='buttonBomb', + texture=ba.gettexture('buttonBomb'), + scale=1.0) + self._capture_button(pos=(h_offs, v - 0.95 * dist), + color=(0.2, 0.5, 0.2), + button='buttonJump', + texture=ba.gettexture('buttonJump'), + scale=1.0) + + def _capture_button(self, + pos: Tuple[float, float], + color: Tuple[float, float, float], + texture: ba.Texture, + button: str, + scale: float = 1.0) -> None: + base_size = 79 + btn = ba.buttonwidget(parent=self._root_widget, + autoselect=True, + position=(pos[0] - base_size * 0.5 * scale, + pos[1] - base_size * 0.5 * scale), + size=(base_size * scale, base_size * scale), + texture=texture, + label='', + color=color) + + # Do this deferred so it shows up on top of other buttons. (ew.) + def doit() -> None: + if not self._root_widget: + return + uiscale = 0.66 * scale * 2.0 + maxwidth = 76.0 * scale + txt = ba.textwidget(parent=self._root_widget, + position=(pos[0] + 0.0 * scale, + pos[1] - (57.0 - 18.0) * scale), + color=(1, 1, 1, 0.3), + size=(0, 0), + h_align='center', + v_align='top', + scale=uiscale, + maxwidth=maxwidth, + text=self._input.get_button_name( + self._settings[button])) + ba.buttonwidget(edit=btn, + autoselect=True, + on_activate_call=ba.Call(AwaitKeyboardInputWindow, + button, txt, + self._settings)) + + ba.pushcall(doit) + + def _cancel(self) -> None: + from bastd.ui.settings.controls import ControlsSettingsWindow + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + ControlsSettingsWindow(transition='in_left').get_root_widget()) + + def _save(self) -> None: + from bastd.ui.settings.controls import ControlsSettingsWindow + from ba.internal import (get_input_device_config, + should_submit_debug_info, master_server_post) + + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.playsound(ba.getsound('gunCocking')) + + # There's a chance the device disappeared; handle that gracefully. + if not self._input: + return + + dst = get_input_device_config(self._input, default=False) + dst2: Dict[str, Any] = dst[0][dst[1]] + dst2.clear() + + # Store any values that aren't -1. + for key, val in list(self._settings.items()): + if val != -1: + dst2[key] = val + + # If we're allowed to phone home, send this config so we can generate + # more defaults in the future. + if should_submit_debug_info(): + master_server_post( + 'controllerConfig', { + 'ua': ba.app.user_agent_string, + 'name': self._name, + 'b': ba.app.build_number, + 'config': dst2, + 'v': 2 + }) + ba.app.config.apply_and_commit() + ba.app.ui.set_main_menu_window( + ControlsSettingsWindow(transition='in_left').get_root_widget()) + + +class AwaitKeyboardInputWindow(ba.Window): + """Window for capturing a keypress.""" + + def __init__(self, button: str, ui: ba.Widget, settings: dict): + + self._capture_button = button + self._capture_key_ui = ui + self._settings = settings + + width = 400 + height = 150 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition='in_right', + scale=(2.0 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0))) + ba.textwidget(parent=self._root_widget, + position=(0, height - 60), + size=(width, 25), + text=ba.Lstr(resource='pressAnyKeyText'), + h_align='center', + v_align='top') + + self._counter = 5 + self._count_down_text = ba.textwidget(parent=self._root_widget, + h_align='center', + position=(0, height - 110), + size=(width, 25), + color=(1, 1, 1, 0.3), + text=str(self._counter)) + self._decrement_timer: Optional[ba.Timer] = ba.Timer( + 1.0, self._decrement, repeat=True, timetype=ba.TimeType.REAL) + _ba.capture_keyboard_input(ba.WeakCall(self._button_callback)) + + def __del__(self) -> None: + _ba.release_keyboard_input() + + def _die(self) -> None: + # This strong-refs us; killing it allows us to die now. + self._decrement_timer = None + if self._root_widget: + ba.containerwidget(edit=self._root_widget, transition='out_left') + + def _button_callback(self, event: Dict[str, Any]) -> None: + self._settings[self._capture_button] = event['button'] + if event['type'] == 'BUTTONDOWN': + bname = event['input_device'].get_button_name(event['button']) + ba.textwidget(edit=self._capture_key_ui, text=bname) + ba.playsound(ba.getsound('gunCocking')) + self._die() + + def _decrement(self) -> None: + self._counter -= 1 + if self._counter >= 1: + ba.textwidget(edit=self._count_down_text, text=str(self._counter)) + else: + self._die() diff --git a/dist/ba_data/python/bastd/ui/settings/nettesting.py b/dist/ba_data/python/bastd/ui/settings/nettesting.py new file mode 100644 index 0000000..e8b645c --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/nettesting.py @@ -0,0 +1,40 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides ui for network related testing.""" + +from __future__ import annotations + +import ba +from bastd.ui.settings import testing + + +class NetTestingWindow(testing.TestingWindow): + """Window to test network related settings.""" + + def __init__(self, transition: str = 'in_right'): + + entries = [ + { + 'name': 'bufferTime', + 'label': 'Buffer Time', + 'increment': 1.0 + }, + { + 'name': 'delaySampling', + 'label': 'Delay Sampling', + 'increment': 1.0 + }, + { + 'name': 'dynamicsSyncTime', + 'label': 'Dynamics Sync Time', + 'increment': 10 + }, + { + 'name': 'showNetInfo', + 'label': 'Show Net Info', + 'increment': 1 + }, + ] + testing.TestingWindow.__init__( + self, ba.Lstr(resource='settingsWindowAdvanced.netTestingText'), + entries, transition) diff --git a/dist/ba_data/python/bastd/ui/settings/plugins.py b/dist/ba_data/python/bastd/ui/settings/plugins.py new file mode 100644 index 0000000..72e4480 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/plugins.py @@ -0,0 +1,156 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Plugin settings UI.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Tuple, Optional, Dict + + +class PluginSettingsWindow(ba.Window): + """Window for configuring plugins.""" + + def __init__(self, + transition: str = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-locals + app = ba.app + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + uiscale = ba.app.ui.uiscale + self._width = 870.0 if uiscale is ba.UIScale.SMALL else 670.0 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (390.0 if uiscale is ba.UIScale.SMALL else + 450.0 if uiscale is ba.UIScale.MEDIUM else 520.0) + top_extra = 10 if uiscale is ba.UIScale.SMALL else 0 + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + top_extra), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(2.06 if uiscale is ba.UIScale.SMALL else + 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -25) if uiscale is ba.UIScale.SMALL else (0, 0))) + + self._scroll_width = self._width - (100 + 2 * x_inset) + self._scroll_height = self._height - 115.0 + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 724.0 + + if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._do_back) + self._back_button = None + else: + self._back_button = ba.buttonwidget( + parent=self._root_widget, + position=(53 + x_inset, self._height - 60), + size=(140, 60), + scale=0.8, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._do_back) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._back_button) + + self._title_text = ba.textwidget(parent=self._root_widget, + position=(0, self._height - 52), + size=(self._width, 25), + text=ba.Lstr(resource='pluginsText'), + color=app.ui.title_color, + h_align='center', + v_align='top') + + if self._back_button is not None: + ba.buttonwidget(edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + position=(50 + x_inset, 50), + simple_culling_v=20.0, + highlight=False, + size=(self._scroll_width, + self._scroll_height), + selection_loops_to_parent=True) + ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + self._subcontainer = ba.columnwidget(parent=self._scrollwidget, + selection_loops_to_parent=True) + + if ba.app.meta.metascan is None: + ba.screenmessage('Still scanning plugins; please try again.', + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + pluglist = ba.app.plugins.potential_plugins + plugstates: Dict[str, Dict] = ba.app.config.setdefault('Plugins', {}) + assert isinstance(plugstates, dict) + for i, availplug in enumerate(pluglist): + active = availplug.class_path in ba.app.plugins.active_plugins + + plugstate = plugstates.setdefault(availplug.class_path, {}) + checked = plugstate.get('enabled', False) + assert isinstance(checked, bool) + check = ba.checkboxwidget( + parent=self._subcontainer, + text=availplug.display_name, + value=checked, + maxwidth=self._scroll_width - 100, + size=(self._scroll_width - 40, 50), + on_value_change_call=ba.Call(self._check_value_changed, + availplug), + textcolor=((0.8, 0.3, 0.3) if not availplug.available else + (0, 1, 0) if active else (0.6, 0.6, 0.6))) + + # Make sure we scroll all the way to the end when using + # keyboard/button nav. + ba.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40) + + # Keep last from looping to back button when down is pressed. + if i == len(pluglist) - 1: + ba.widget(edit=check, down_widget=check) + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + + self._restore_state() + + def _check_value_changed(self, plug: ba.PotentialPlugin, + value: bool) -> None: + ba.screenmessage( + ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'), + color=(1.0, 0.5, 0.0)) + plugstates: Dict[str, Dict] = ba.app.config.setdefault('Plugins', {}) + assert isinstance(plugstates, dict) + plugstate = plugstates.setdefault(plug.class_path, {}) + plugstate['enabled'] = value + ba.app.config.commit() + + def _save_state(self) -> None: + pass + + def _restore_state(self) -> None: + pass + + def _do_back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings.advanced import AdvancedSettingsWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + AdvancedSettingsWindow(transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/settings/ps3controller.py b/dist/ba_data/python/bastd/ui/settings/ps3controller.py new file mode 100644 index 0000000..6d588da --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/ps3controller.py @@ -0,0 +1,107 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Settings UI related to PS3 controllers.""" + +from __future__ import annotations + +import _ba +import ba + + +class PS3ControllerSettingsWindow(ba.Window): + """UI showing info about using PS3 controllers.""" + + def __init__(self) -> None: + width = 760 + height = 330 if _ba.is_running_on_fire_tv() else 540 + spacing = 40 + self._r = 'ps3ControllersWindow' + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition='in_right', + scale=(1.35 if uiscale is ba.UIScale.SMALL else + 1.3 if uiscale is ba.UIScale.MEDIUM else 1.0))) + + btn = ba.buttonwidget(parent=self._root_widget, + position=(37, height - 73), + size=(135, 65), + scale=0.85, + label=ba.Lstr(resource='backText'), + button_type='back', + autoselect=True, + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, height - 46), + size=(0, 0), + maxwidth=410, + text=ba.Lstr(resource=self._r + '.titleText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText'))]), + color=ba.app.ui.title_color, + h_align='center', + v_align='center') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + v = height - 90 + v -= spacing + + if _ba.is_running_on_fire_tv(): + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, height * 0.45), + size=(0, 0), + color=(0.7, 0.9, 0.7, 1.0), + maxwidth=width * 0.95, + max_height=height * 0.8, + scale=1.0, + text=ba.Lstr(resource=self._r + + '.ouyaInstructionsText'), + h_align='center', + v_align='center') + else: + txts = ba.Lstr(resource=self._r + + '.macInstructionsText').evaluate().split('\n\n\n') + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, v - 29), + size=(0, 0), + color=(0.7, 0.9, 0.7, 1.0), + maxwidth=width * 0.95, + max_height=170, + scale=1.0, + text=txts[0].strip(), + h_align='center', + v_align='center') + if txts: + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, v - 280), + size=(0, 0), + color=(0.7, 0.9, 0.7, 1.0), + maxwidth=width * 0.95, + max_height=170, + scale=1.0, + text=txts[1].strip(), + h_align='center', + v_align='center') + + ba.buttonwidget(parent=self._root_widget, + position=(225, v - 176), + size=(300, 40), + label=ba.Lstr(resource=self._r + + '.pairingTutorialText'), + autoselect=True, + on_activate_call=ba.Call( + ba.open_url, 'http://www.youtube.com/watch' + '?v=IlR_HxeOQpI&feature=related')) + + def _back(self) -> None: + from bastd.ui.settings import controls + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + controls.ControlsSettingsWindow( + transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/settings/remoteapp.py b/dist/ba_data/python/bastd/ui/settings/remoteapp.py new file mode 100644 index 0000000..dd81689 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/remoteapp.py @@ -0,0 +1,118 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Settings UI functionality related to the remote app.""" + +from __future__ import annotations + +import ba + + +class RemoteAppSettingsWindow(ba.Window): + """Window showing info/settings related to the remote app.""" + + def __init__(self) -> None: + from ba.internal import get_remote_app_name + self._r = 'connectMobileDevicesWindow' + width = 700 + height = 390 + spacing = 40 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition='in_right', + scale=(1.85 if uiscale is ba.UIScale.SMALL else + 1.3 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(-10, 0) if uiscale is ba.UIScale.SMALL else (0, 0))) + btn = ba.buttonwidget(parent=self._root_widget, + position=(40, height - 67), + size=(140, 65), + scale=0.8, + label=ba.Lstr(resource='backText'), + button_type='back', + text_scale=1.1, + autoselect=True, + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, height - 42), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.titleText'), + maxwidth=370, + color=ba.app.ui.title_color, + scale=0.8, + h_align='center', + v_align='center') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + v = height - 70.0 + v -= spacing * 1.2 + ba.textwidget(parent=self._root_widget, + position=(15, v - 26), + size=(width - 30, 30), + maxwidth=width * 0.95, + color=(0.7, 0.9, 0.7, 1.0), + scale=0.8, + text=ba.Lstr(resource=self._r + '.explanationText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText')), + ('${REMOTE_APP_NAME}', + get_remote_app_name())]), + max_height=100, + h_align='center', + v_align='center') + v -= 90 + + # hmm the itms:// version doesnt bounce through safari but is kinda + # apple-specific-ish + + # Update: now we just show link to the remote webpage. + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, v + 5), + size=(0, 0), + color=(0.7, 0.9, 0.7, 1.0), + scale=1.4, + text='bombsquadgame.com/remote', + maxwidth=width * 0.95, + max_height=60, + h_align='center', + v_align='center') + v -= 30 + + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, v - 35), + size=(0, 0), + color=(0.7, 0.9, 0.7, 0.8), + scale=0.65, + text=ba.Lstr(resource=self._r + '.bestResultsText'), + maxwidth=width * 0.95, + max_height=height * 0.19, + h_align='center', + v_align='center') + + ba.checkboxwidget( + parent=self._root_widget, + position=(width * 0.5 - 150, v - 116), + size=(300, 30), + maxwidth=300, + scale=0.8, + value=not ba.app.config.resolve('Enable Remote App'), + autoselect=True, + text=ba.Lstr(resource='disableRemoteAppConnectionsText'), + on_value_change_call=self._on_check_changed) + + def _on_check_changed(self, value: bool) -> None: + cfg = ba.app.config + cfg['Enable Remote App'] = not value + cfg.apply_and_commit() + + def _back(self) -> None: + from bastd.ui.settings import controls + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + controls.ControlsSettingsWindow( + transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/settings/testing.py b/dist/ba_data/python/bastd/ui/settings/testing.py new file mode 100644 index 0000000..a96dd0d --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/testing.py @@ -0,0 +1,179 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for test settings.""" + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Dict, List + + +class TestingWindow(ba.Window): + """Window for conveniently testing various settings.""" + + def __init__(self, + title: ba.Lstr, + entries: List[Dict[str, Any]], + transition: str = 'in_right'): + uiscale = ba.app.ui.uiscale + self._width = 600 + self._height = 324 if uiscale is ba.UIScale.SMALL else 400 + self._entries = copy.deepcopy(entries) + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=(2.5 if uiscale is ba.UIScale.SMALL else + 1.2 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -28) if uiscale is ba.UIScale.SMALL else (0, 0))) + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(65, self._height - 59), + size=(130, 60), + scale=0.8, + text_scale=1.2, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._do_back) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 35), + size=(0, 0), + color=ba.app.ui.title_color, + h_align='center', + v_align='center', + maxwidth=245, + text=title) + + ba.buttonwidget(edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 75), + size=(0, 0), + color=ba.app.ui.infotextcolor, + h_align='center', + v_align='center', + maxwidth=self._width * 0.75, + text=ba.Lstr(resource='settingsWindowAdvanced.forTestingText')) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + self._scroll_width = self._width - 130 + self._scroll_height = self._height - 140 + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + size=(self._scroll_width, self._scroll_height), + highlight=False, + position=((self._width - self._scroll_width) * 0.5, 40)) + ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + + self._spacing = 50 + + self._sub_width = self._scroll_width * 0.95 + self._sub_height = 50 + len(self._entries) * self._spacing + 60 + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False) + + h = 230 + v = self._sub_height - 48 + + for i, entry in enumerate(self._entries): + + entry_name = entry['name'] + + # If we haven't yet, record the default value for this name so + # we can reset if we want.. + if entry_name not in ba.app.value_test_defaults: + ba.app.value_test_defaults[entry_name] = ( + _ba.value_test(entry_name)) + + ba.textwidget(parent=self._subcontainer, + position=(h, v), + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=200, + text=entry['label']) + btn = ba.buttonwidget(parent=self._subcontainer, + position=(h + 20, v - 19), + size=(40, 40), + autoselect=True, + repeat=True, + left_widget=self._back_button, + button_type='square', + label='-', + on_activate_call=ba.Call( + self._on_minus_press, entry['name'])) + if i == 0: + ba.widget(edit=btn, up_widget=self._back_button) + entry['widget'] = ba.textwidget(parent=self._subcontainer, + position=(h + 100, v), + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=60, + text='%.4g' % + _ba.value_test(entry_name)) + btn = ba.buttonwidget(parent=self._subcontainer, + position=(h + 140, v - 19), + size=(40, 40), + autoselect=True, + repeat=True, + button_type='square', + label='+', + on_activate_call=ba.Call( + self._on_plus_press, entry['name'])) + if i == 0: + ba.widget(edit=btn, up_widget=self._back_button) + v -= self._spacing + v -= 35 + ba.buttonwidget( + parent=self._subcontainer, + autoselect=True, + size=(200, 50), + position=(self._sub_width * 0.5 - 100, v), + label=ba.Lstr(resource='settingsWindowAdvanced.resetText'), + right_widget=btn, + on_activate_call=self._on_reset_press) + + def _get_entry(self, name: str) -> Dict[str, Any]: + for entry in self._entries: + if entry['name'] == name: + return entry + raise ba.NotFoundError(f'Entry not found: {name}') + + def _on_reset_press(self) -> None: + for entry in self._entries: + _ba.value_test(entry['name'], + absolute=ba.app.value_test_defaults[entry['name']]) + ba.textwidget(edit=entry['widget'], + text='%.4g' % _ba.value_test(entry['name'])) + + def _on_minus_press(self, entry_name: str) -> None: + entry = self._get_entry(entry_name) + _ba.value_test(entry['name'], change=-entry['increment']) + ba.textwidget(edit=entry['widget'], + text='%.4g' % _ba.value_test(entry['name'])) + + def _on_plus_press(self, entry_name: str) -> None: + entry = self._get_entry(entry_name) + _ba.value_test(entry['name'], change=entry['increment']) + ba.textwidget(edit=entry['widget'], + text='%.4g' % _ba.value_test(entry['name'])) + + def _do_back(self) -> None: + # pylint: disable=cyclic-import + import bastd.ui.settings.advanced + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + bastd.ui.settings.advanced.AdvancedSettingsWindow( + transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/settings/touchscreen.py b/dist/ba_data/python/bastd/ui/settings/touchscreen.py new file mode 100644 index 0000000..ddf4346 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/touchscreen.py @@ -0,0 +1,236 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI settings functionality related to touchscreens.""" +from __future__ import annotations + +import _ba +import ba + + +class TouchscreenSettingsWindow(ba.Window): + """Settings window for touchscreens.""" + + def __del__(self) -> None: + # Note - this happens in 'back' too; + # we just do it here too in case the window is closed by other means. + + # FIXME: Could switch to a UI destroy callback now that those are a + # thing that exists. + _ba.set_touchscreen_editing(False) + + def __init__(self) -> None: + + self._width = 650 + self._height = 380 + self._spacing = 40 + self._r = 'configTouchscreenWindow' + + _ba.set_touchscreen_editing(True) + + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition='in_right', + scale=(1.9 if uiscale is ba.UIScale.SMALL else + 1.55 if uiscale is ba.UIScale.MEDIUM else 1.2))) + + btn = ba.buttonwidget(parent=self._root_widget, + position=(55, self._height - 60), + size=(120, 60), + label=ba.Lstr(resource='backText'), + button_type='back', + scale=0.8, + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(25, self._height - 50), + size=(self._width, 25), + text=ba.Lstr(resource=self._r + '.titleText'), + color=ba.app.ui.title_color, + maxwidth=280, + h_align='center', + v_align='center') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + self._scroll_width = self._width - 100 + self._scroll_height = self._height - 110 + self._sub_width = self._scroll_width - 20 + self._sub_height = 360 + + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + position=((self._width - self._scroll_width) * 0.5, + self._height - 65 - self._scroll_height), + size=(self._scroll_width, self._scroll_height), + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width, + self._sub_height), + background=False, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + self._build_gui() + + def _build_gui(self) -> None: + from bastd.ui.config import ConfigNumberEdit, ConfigCheckBox + from bastd.ui.radiogroup import make_radio_group + + # Clear anything already there. + children = self._subcontainer.get_children() + for child in children: + child.delete() + h = 30 + v = self._sub_height - 85 + clr = (0.8, 0.8, 0.8, 1.0) + clr2 = (0.8, 0.8, 0.8) + ba.textwidget(parent=self._subcontainer, + position=(-10, v + 43), + size=(self._sub_width, 25), + text=ba.Lstr(resource=self._r + '.swipeInfoText'), + flatness=1.0, + color=(0, 0.9, 0.1, 0.7), + maxwidth=self._sub_width * 0.9, + scale=0.55, + h_align='center', + v_align='center') + cur_val = ba.app.config.get('Touch Movement Control Type', 'swipe') + ba.textwidget(parent=self._subcontainer, + position=(h, v - 2), + size=(0, 30), + text=ba.Lstr(resource=self._r + '.movementText'), + maxwidth=190, + color=clr, + v_align='center') + cb1 = ba.checkboxwidget(parent=self._subcontainer, + position=(h + 220, v), + size=(170, 30), + text=ba.Lstr(resource=self._r + + '.joystickText'), + maxwidth=100, + textcolor=clr2, + scale=0.9) + cb2 = ba.checkboxwidget(parent=self._subcontainer, + position=(h + 357, v), + size=(170, 30), + text=ba.Lstr(resource=self._r + '.swipeText'), + maxwidth=100, + textcolor=clr2, + value=False, + scale=0.9) + make_radio_group((cb1, cb2), ('joystick', 'swipe'), cur_val, + self._movement_changed) + v -= 50 + ConfigNumberEdit(parent=self._subcontainer, + position=(h, v), + xoffset=65, + configkey='Touch Controls Scale Movement', + displayname=ba.Lstr(resource=self._r + + '.movementControlScaleText'), + changesound=False, + minval=0.1, + maxval=4.0, + increment=0.1) + v -= 50 + cur_val = ba.app.config.get('Touch Action Control Type', 'buttons') + ba.textwidget(parent=self._subcontainer, + position=(h, v - 2), + size=(0, 30), + text=ba.Lstr(resource=self._r + '.actionsText'), + maxwidth=190, + color=clr, + v_align='center') + cb1 = ba.checkboxwidget(parent=self._subcontainer, + position=(h + 220, v), + size=(170, 30), + text=ba.Lstr(resource=self._r + + '.buttonsText'), + maxwidth=100, + textcolor=clr2, + scale=0.9) + cb2 = ba.checkboxwidget(parent=self._subcontainer, + position=(h + 357, v), + size=(170, 30), + text=ba.Lstr(resource=self._r + '.swipeText'), + maxwidth=100, + textcolor=clr2, + scale=0.9) + make_radio_group((cb1, cb2), ('buttons', 'swipe'), cur_val, + self._actions_changed) + v -= 50 + ConfigNumberEdit(parent=self._subcontainer, + position=(h, v), + xoffset=65, + configkey='Touch Controls Scale Actions', + displayname=ba.Lstr(resource=self._r + + '.actionControlScaleText'), + changesound=False, + minval=0.1, + maxval=4.0, + increment=0.1) + + v -= 50 + ConfigCheckBox(parent=self._subcontainer, + position=(h, v), + size=(400, 30), + maxwidth=400, + configkey='Touch Controls Swipe Hidden', + displayname=ba.Lstr(resource=self._r + + '.swipeControlsHiddenText')) + v -= 65 + + ba.buttonwidget(parent=self._subcontainer, + position=(self._sub_width * 0.5 - 70, v), + size=(170, 60), + label=ba.Lstr(resource=self._r + '.resetText'), + scale=0.75, + on_activate_call=self._reset) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, 38), + size=(0, 0), + h_align='center', + text=ba.Lstr(resource=self._r + '.dragControlsText'), + maxwidth=self._width * 0.8, + scale=0.65, + color=(1, 1, 1, 0.4)) + + def _actions_changed(self, v: str) -> None: + cfg = ba.app.config + cfg['Touch Action Control Type'] = v + cfg.apply_and_commit() + + def _movement_changed(self, v: str) -> None: + cfg = ba.app.config + cfg['Touch Movement Control Type'] = v + cfg.apply_and_commit() + + def _reset(self) -> None: + cfg = ba.app.config + cfgkeys = [ + 'Touch Movement Control Type', 'Touch Action Control Type', + 'Touch Controls Scale', 'Touch Controls Scale Movement', + 'Touch Controls Scale Actions', 'Touch Controls Swipe Hidden', + 'Touch DPad X', 'Touch DPad Y', 'Touch Buttons X', + 'Touch Buttons Y' + ] + for cfgkey in cfgkeys: + if cfgkey in cfg: + del cfg[cfgkey] + cfg.apply_and_commit() + ba.timer(0, self._build_gui, timetype=ba.TimeType.REAL) + + def _back(self) -> None: + from bastd.ui.settings import controls + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + controls.ControlsSettingsWindow( + transition='in_left').get_root_widget()) + _ba.set_touchscreen_editing(False) diff --git a/dist/ba_data/python/bastd/ui/settings/vrtesting.py b/dist/ba_data/python/bastd/ui/settings/vrtesting.py new file mode 100644 index 0000000..7065d46 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/vrtesting.py @@ -0,0 +1,90 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for testing vr settings.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.ui.settings import testing + +if TYPE_CHECKING: + from typing import Any, Dict, List + + +class VRTestingWindow(testing.TestingWindow): + """Window for testing vr settings.""" + + def __init__(self, transition: str = 'in_right'): + + entries: List[Dict[str, Any]] = [] + app = ba.app + # these are gear-vr only + if app.platform == 'android' and app.subplatform == 'oculus': + entries += [ + { + 'name': 'timeWarpDebug', + 'label': 'Time Warp Debug', + 'increment': 1.0 + }, + { + 'name': 'chromaticAberrationCorrection', + 'label': 'Chromatic Aberration Correction', + 'increment': 1.0 + }, + { + 'name': 'vrMinimumVSyncs', + 'label': 'Minimum Vsyncs', + 'increment': 1.0 + }, + # {'name':'eyeOffsX','label':'Eye IPD','increment':0.001} + ] + # cardboard/gearvr get eye offset controls.. + # if app.platform == 'android': + # entries += [ + # {'name':'eyeOffsY','label':'Eye Offset Y','increment':0.01}, + # {'name':'eyeOffsZ','label':'Eye Offset Z','increment':0.005}] + # everyone gets head-scale + entries += [{ + 'name': 'headScale', + 'label': 'Head Scale', + 'increment': 1.0 + }] + # and everyone gets all these.. + entries += [ + { + 'name': 'vrCamOffsetY', + 'label': 'In-Game Cam Offset Y', + 'increment': 0.1 + }, + { + 'name': 'vrCamOffsetZ', + 'label': 'In-Game Cam Offset Z', + 'increment': 0.1 + }, + { + 'name': 'vrOverlayScale', + 'label': 'Overlay Scale', + 'increment': 0.025 + }, + { + 'name': 'allowCameraMovement', + 'label': 'Allow Camera Movement', + 'increment': 1.0 + }, + { + 'name': 'cameraPanSpeedScale', + 'label': 'Camera Movement Speed', + 'increment': 0.1 + }, + { + 'name': 'showOverlayBounds', + 'label': 'Show Overlay Bounds', + 'increment': 1 + }, + ] + + super().__init__( + ba.Lstr(resource='settingsWindowAdvanced.vrTestingText'), entries, + transition) diff --git a/dist/ba_data/python/bastd/ui/settings/wiimote.py b/dist/ba_data/python/bastd/ui/settings/wiimote.py new file mode 100644 index 0000000..89dbadc --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/wiimote.py @@ -0,0 +1,251 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Settings UI functionality related to wiimote support.""" +from __future__ import annotations + +import _ba +import ba + + +class WiimoteSettingsWindow(ba.Window): + """Window for setting up Wiimotes.""" + + def __init__(self) -> None: + self._r = 'wiimoteSetupWindow' + width = 600 + height = 480 + spacing = 40 + super().__init__(root_widget=ba.containerwidget(size=(width, height), + transition='in_right')) + + btn = ba.buttonwidget(parent=self._root_widget, + position=(55, height - 50), + size=(120, 60), + scale=0.8, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._back) + + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, height - 28), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.titleText'), + maxwidth=270, + color=ba.app.ui.title_color, + h_align='center', + v_align='center') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + v = height - 60.0 + v -= spacing + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, v - 80), + size=(0, 0), + color=(0.7, 0.9, 0.7, 1.0), + scale=0.75, + text=ba.Lstr(resource=self._r + '.macInstructionsText'), + maxwidth=width * 0.95, + max_height=height * 0.5, + h_align='center', + v_align='center') + v -= 230 + button_width = 200 + v -= 30 + btn = ba.buttonwidget(parent=self._root_widget, + position=(width / 2 - button_width / 2, v + 1), + autoselect=True, + size=(button_width, 50), + label=ba.Lstr(resource=self._r + '.listenText'), + on_activate_call=WiimoteListenWindow) + ba.containerwidget(edit=self._root_widget, start_button=btn) + v -= spacing * 1.1 + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, v), + size=(0, 0), + color=(0.7, 0.9, 0.7, 1.0), + scale=0.8, + maxwidth=width * 0.95, + text=ba.Lstr(resource=self._r + '.thanksText'), + h_align='center', + v_align='center') + v -= 30 + this_button_width = 200 + ba.buttonwidget(parent=self._root_widget, + position=(width / 2 - this_button_width / 2, v - 14), + color=(0.45, 0.4, 0.5), + autoselect=True, + size=(this_button_width, 15), + label=ba.Lstr(resource=self._r + '.copyrightText'), + textcolor=(0.55, 0.5, 0.6), + text_scale=0.6, + on_activate_call=WiimoteLicenseWindow) + + def _back(self) -> None: + from bastd.ui.settings import controls + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + controls.ControlsSettingsWindow( + transition='in_left').get_root_widget()) + + +class WiimoteListenWindow(ba.Window): + """Window shown while listening for a wiimote connection.""" + + def __init__(self) -> None: + self._r = 'wiimoteListenWindow' + width = 650 + height = 210 + super().__init__(root_widget=ba.containerwidget(size=(width, height), + transition='in_right')) + btn = ba.buttonwidget(parent=self._root_widget, + position=(35, height - 60), + size=(140, 60), + autoselect=True, + label=ba.Lstr(resource='cancelText'), + scale=0.8, + on_activate_call=self._dismiss) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + _ba.start_listening_for_wii_remotes() + self._wiimote_connect_counter = 15 + ba.app.ui.dismiss_wii_remotes_window_call = ba.WeakCall(self._dismiss) + ba.textwidget(parent=self._root_widget, + position=(15, height - 55), + size=(width - 30, 30), + text=ba.Lstr(resource=self._r + '.listeningText'), + color=ba.app.ui.title_color, + maxwidth=320, + h_align='center', + v_align='center') + ba.textwidget(parent=self._root_widget, + position=(15, height - 110), + size=(width - 30, 30), + scale=1.0, + text=ba.Lstr(resource=self._r + '.pressText'), + maxwidth=width * 0.9, + color=(0.7, 0.9, 0.7, 1.0), + h_align='center', + v_align='center') + ba.textwidget(parent=self._root_widget, + position=(15, height - 140), + size=(width - 30, 30), + color=(0.7, 0.9, 0.7, 1.0), + scale=0.55, + text=ba.Lstr(resource=self._r + '.pressText2'), + maxwidth=width * 0.95, + h_align='center', + v_align='center') + self._counter_text = ba.textwidget(parent=self._root_widget, + position=(15, 23), + size=(width - 30, 30), + scale=1.2, + text='15', + h_align='center', + v_align='top') + for i in range(1, 15): + ba.timer(1.0 * i, + ba.WeakCall(self._decrement), + timetype=ba.TimeType.REAL) + ba.timer(15.0, ba.WeakCall(self._dismiss), timetype=ba.TimeType.REAL) + + def _decrement(self) -> None: + self._wiimote_connect_counter -= 1 + ba.textwidget(edit=self._counter_text, + text=str(self._wiimote_connect_counter)) + + def _dismiss(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_left') + _ba.stop_listening_for_wii_remotes() + + +class WiimoteLicenseWindow(ba.Window): + """Window displaying the Darwiinremote software license.""" + + def __init__(self) -> None: + self._r = 'wiimoteLicenseWindow' + width = 750 + height = 550 + super().__init__(root_widget=ba.containerwidget(size=(width, height), + transition='in_right')) + btn = ba.buttonwidget(parent=self._root_widget, + position=(65, height - 50), + size=(120, 60), + scale=0.8, + autoselect=True, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._close) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + ba.textwidget(parent=self._root_widget, + position=(0, height - 48), + size=(width, 30), + text=ba.Lstr(resource=self._r + '.titleText'), + h_align='center', + color=ba.app.ui.title_color, + v_align='center') + license_text = ( + 'Copyright (c) 2007, DarwiinRemote Team\n' + 'All rights reserved.\n' + '\n' + ' Redistribution and use in source and binary forms, with or ' + 'without modification,\n' + ' are permitted provided that' + ' the following conditions are met:\n' + '\n' + '1. Redistributions of source code must retain the above copyright' + ' notice, this\n' + ' list of conditions and the following disclaimer.\n' + '2. Redistributions in binary form must reproduce the above' + ' copyright notice, this\n' + ' list of conditions and the following disclaimer in the' + ' documentation and/or other\n' + ' materials provided with the distribution.\n' + '3. Neither the name of this project nor the names of its' + ' contributors may be used to\n' + ' endorse or promote products derived from this software' + ' without specific prior\n' + ' written permission.\n' + '\n' + 'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND' + ' CONTRIBUTORS "AS IS"\n' + 'AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT' + ' LIMITED TO, THE\n' + 'IMPLIED WARRANTIES OF MERCHANTABILITY' + ' AND FITNESS FOR A PARTICULAR' + ' PURPOSE\n' + 'ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR' + ' CONTRIBUTORS BE\n' + 'LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,' + ' OR\n' + 'CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT' + ' OF\n' + ' SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;' + ' OR BUSINESS\n' + 'INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,' + ' WHETHER IN\n' + 'CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR' + ' OTHERWISE)\n' + 'ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF' + ' ADVISED OF THE\n' + 'POSSIBILITY OF SUCH DAMAGE.\n') + license_text_scale = 0.62 + ba.textwidget(parent=self._root_widget, + position=(100, height * 0.45), + size=(0, 0), + h_align='left', + v_align='center', + padding=4, + color=(0.7, 0.9, 0.7, 1.0), + scale=license_text_scale, + maxwidth=width * 0.9 - 100, + max_height=height * 0.85, + text=license_text) + + def _close(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_right') diff --git a/dist/ba_data/python/bastd/ui/settings/xbox360controller.py b/dist/ba_data/python/bastd/ui/settings/xbox360controller.py new file mode 100644 index 0000000..0e145a7 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/settings/xbox360controller.py @@ -0,0 +1,115 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to using xbox360 controllers.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + pass + + +class XBox360ControllerSettingsWindow(ba.Window): + """UI showing info about xbox 360 controllers.""" + + def __init__(self) -> None: + self._r = 'xbox360ControllersWindow' + width = 700 + height = 300 if _ba.is_running_on_fire_tv() else 485 + spacing = 40 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(width, height), + transition='in_right', + scale=(1.4 if uiscale is ba.UIScale.SMALL else + 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0))) + + btn = ba.buttonwidget(parent=self._root_widget, + position=(35, height - 65), + size=(120, 60), + scale=0.84, + label=ba.Lstr(resource='backText'), + button_type='back', + autoselect=True, + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, height - 42), + size=(0, 0), + scale=0.85, + text=ba.Lstr(resource=self._r + '.titleText', + subs=[('${APP_NAME}', + ba.Lstr(resource='titleText'))]), + color=ba.app.ui.title_color, + maxwidth=400, + h_align='center', + v_align='center') + + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + v = height - 70 + v -= spacing + + if _ba.is_running_on_fire_tv(): + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, height * 0.47), + size=(0, 0), + color=(0.7, 0.9, 0.7, 1.0), + maxwidth=width * 0.95, + max_height=height * 0.75, + scale=0.7, + text=ba.Lstr(resource=self._r + + '.ouyaInstructionsText'), + h_align='center', + v_align='center') + else: + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, v - 1), + size=(0, 0), + color=(0.7, 0.9, 0.7, 1.0), + maxwidth=width * 0.95, + max_height=height * 0.22, + text=ba.Lstr(resource=self._r + + '.macInstructionsText'), + scale=0.7, + h_align='center', + v_align='center') + v -= 90 + b_width = 300 + btn = ba.buttonwidget( + parent=self._root_widget, + position=((width - b_width) * 0.5, v - 10), + size=(b_width, 50), + label=ba.Lstr(resource=self._r + '.getDriverText'), + autoselect=True, + on_activate_call=ba.Call( + ba.open_url, + 'https://github.com/360Controller/360Controller/releases')) + ba.containerwidget(edit=self._root_widget, start_button=btn) + v -= 60 + ba.textwidget(parent=self._root_widget, + position=(width * 0.5, v - 85), + size=(0, 0), + color=(0.7, 0.9, 0.7, 1.0), + maxwidth=width * 0.95, + max_height=height * 0.46, + scale=0.7, + text=ba.Lstr(resource=self._r + + '.macInstructions2Text'), + h_align='center', + v_align='center') + + def _back(self) -> None: + from bastd.ui.settings import controls + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + controls.ControlsSettingsWindow( + transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/bastd/ui/soundtrack/__init__.py b/dist/ba_data/python/bastd/ui/soundtrack/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/soundtrack/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4cc0aee Binary files /dev/null and b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-38.opt-1.pyc new file mode 100644 index 0000000..90e38dc Binary files /dev/null and b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-38.opt-1.pyc new file mode 100644 index 0000000..bc09470 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-38.opt-1.pyc new file mode 100644 index 0000000..2e77d9e Binary files /dev/null and b/dist/ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/soundtrack/browser.py b/dist/ba_data/python/bastd/ui/soundtrack/browser.py new file mode 100644 index 0000000..7989d32 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/soundtrack/browser.py @@ -0,0 +1,487 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for browsing soundtracks.""" + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Optional, List, Tuple, Dict + + +class SoundtrackBrowserWindow(ba.Window): + """Window for browsing soundtracks.""" + + def __init__(self, + transition: str = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + + # If they provided an origin-widget, scale up from that. + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + self._r = 'editSoundtrackWindow' + uiscale = ba.app.ui.uiscale + self._width = 800 if uiscale is ba.UIScale.SMALL else 600 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (340 if uiscale is ba.UIScale.SMALL else + 370 if uiscale is ba.UIScale.MEDIUM else 440) + spacing = 40.0 + v = self._height - 40.0 + v -= spacing * 1.0 + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(2.3 if uiscale is ba.UIScale.SMALL else + 1.6 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -18) if uiscale is ba.UIScale.SMALL else (0, 0))) + + if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + self._back_button = None + else: + self._back_button = ba.buttonwidget( + parent=self._root_widget, + position=(45 + x_inset, self._height - 60), + size=(120, 60), + scale=0.8, + label=ba.Lstr(resource='backText'), + button_type='back', + autoselect=True) + ba.buttonwidget(edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 35), + size=(0, 0), + maxwidth=300, + text=ba.Lstr(resource=self._r + '.titleText'), + color=ba.app.ui.title_color, + h_align='center', + v_align='center') + + h = 43 + x_inset + v = self._height - 60 + b_color = (0.6, 0.53, 0.63) + b_textcolor = (0.75, 0.7, 0.8) + lock_tex = ba.gettexture('lock') + self._lock_images: List[ba.Widget] = [] + + scl = (1.0 if uiscale is ba.UIScale.SMALL else + 1.13 if uiscale is ba.UIScale.MEDIUM else 1.4) + v -= 60.0 * scl + self._new_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(100, 55.0 * scl), + on_activate_call=self._new_soundtrack, + color=b_color, + button_type='square', + autoselect=True, + textcolor=b_textcolor, + text_scale=0.7, + label=ba.Lstr(resource=self._r + '.newText')) + self._lock_images.append( + ba.imagewidget(parent=self._root_widget, + size=(30, 30), + draw_controller=btn, + position=(h - 10, v + 55.0 * scl - 28), + texture=lock_tex)) + + if self._back_button is None: + ba.widget(edit=btn, + left_widget=_ba.get_special_widget('back_button')) + v -= 60.0 * scl + + self._edit_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(100, 55.0 * scl), + on_activate_call=self._edit_soundtrack, + color=b_color, + button_type='square', + autoselect=True, + textcolor=b_textcolor, + text_scale=0.7, + label=ba.Lstr(resource=self._r + '.editText')) + self._lock_images.append( + ba.imagewidget(parent=self._root_widget, + size=(30, 30), + draw_controller=btn, + position=(h - 10, v + 55.0 * scl - 28), + texture=lock_tex)) + if self._back_button is None: + ba.widget(edit=btn, + left_widget=_ba.get_special_widget('back_button')) + v -= 60.0 * scl + + self._duplicate_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(100, 55.0 * scl), + on_activate_call=self._duplicate_soundtrack, + button_type='square', + autoselect=True, + color=b_color, + textcolor=b_textcolor, + text_scale=0.7, + label=ba.Lstr(resource=self._r + '.duplicateText')) + self._lock_images.append( + ba.imagewidget(parent=self._root_widget, + size=(30, 30), + draw_controller=btn, + position=(h - 10, v + 55.0 * scl - 28), + texture=lock_tex)) + if self._back_button is None: + ba.widget(edit=btn, + left_widget=_ba.get_special_widget('back_button')) + v -= 60.0 * scl + + self._delete_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(h, v), + size=(100, 55.0 * scl), + on_activate_call=self._delete_soundtrack, + color=b_color, + button_type='square', + autoselect=True, + textcolor=b_textcolor, + text_scale=0.7, + label=ba.Lstr(resource=self._r + '.deleteText')) + self._lock_images.append( + ba.imagewidget(parent=self._root_widget, + size=(30, 30), + draw_controller=btn, + position=(h - 10, v + 55.0 * scl - 28), + texture=lock_tex)) + if self._back_button is None: + ba.widget(edit=btn, + left_widget=_ba.get_special_widget('back_button')) + + # Keep our lock images up to date/etc. + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + self._update() + + v = self._height - 65 + scroll_height = self._height - 105 + v -= scroll_height + self._scrollwidget = scrollwidget = ba.scrollwidget( + parent=self._root_widget, + position=(152 + x_inset, v), + highlight=False, + size=(self._width - (205 + 2 * x_inset), scroll_height)) + ba.widget(edit=self._scrollwidget, + left_widget=self._new_button, + right_widget=_ba.get_special_widget('party_button') + if ba.app.ui.use_toolbars else self._scrollwidget) + self._col = ba.columnwidget(parent=scrollwidget, border=2, margin=0) + + self._soundtracks: Optional[Dict[str, Any]] = None + self._selected_soundtrack: Optional[str] = None + self._selected_soundtrack_index: Optional[int] = None + self._soundtrack_widgets: List[ba.Widget] = [] + self._allow_changing_soundtracks = False + self._refresh() + if self._back_button is not None: + ba.buttonwidget(edit=self._back_button, + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._back_button) + else: + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._back) + + def _update(self) -> None: + have = ba.app.accounts.have_pro_options() + for lock in self._lock_images: + ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0) + + def _do_delete_soundtrack(self) -> None: + cfg = ba.app.config + soundtracks = cfg.setdefault('Soundtracks', {}) + if self._selected_soundtrack in soundtracks: + del soundtracks[self._selected_soundtrack] + cfg.commit() + ba.playsound(ba.getsound('shieldDown')) + assert self._selected_soundtrack_index is not None + assert self._soundtracks is not None + if self._selected_soundtrack_index >= len(self._soundtracks): + self._selected_soundtrack_index = len(self._soundtracks) + self._refresh() + + def _delete_soundtrack(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.purchase import PurchaseWindow + from bastd.ui.confirm import ConfirmWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + if self._selected_soundtrack is None: + return + if self._selected_soundtrack == '__default__': + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource=self._r + + '.cantDeleteDefaultText'), + color=(1, 0, 0)) + else: + ConfirmWindow( + ba.Lstr(resource=self._r + '.deleteConfirmText', + subs=[('${NAME}', self._selected_soundtrack)]), + self._do_delete_soundtrack, 450, 150) + + def _duplicate_soundtrack(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.purchase import PurchaseWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + cfg = ba.app.config + cfg.setdefault('Soundtracks', {}) + + if self._selected_soundtrack is None: + return + sdtk: Dict[str, Any] + if self._selected_soundtrack == '__default__': + sdtk = {} + else: + sdtk = cfg['Soundtracks'][self._selected_soundtrack] + + # Find a valid dup name that doesn't exist. + test_index = 1 + copy_text = ba.Lstr(resource='copyOfText').evaluate() + # Get just 'Copy' or whatnot. + copy_word = copy_text.replace('${NAME}', '').strip() + base_name = self._get_soundtrack_display_name( + self._selected_soundtrack).evaluate() + assert isinstance(base_name, str) + + # If it looks like a copy, strip digits and spaces off the end. + if copy_word in base_name: + while base_name[-1].isdigit() or base_name[-1] == ' ': + base_name = base_name[:-1] + while True: + if copy_word in base_name: + test_name = base_name + else: + test_name = copy_text.replace('${NAME}', base_name) + if test_index > 1: + test_name += ' ' + str(test_index) + if test_name not in cfg['Soundtracks']: + break + test_index += 1 + + cfg['Soundtracks'][test_name] = copy.deepcopy(sdtk) + cfg.commit() + self._refresh(select_soundtrack=test_name) + + def _select(self, name: str, index: int) -> None: + music = ba.app.music + self._selected_soundtrack_index = index + self._selected_soundtrack = name + cfg = ba.app.config + current_soundtrack = cfg.setdefault('Soundtrack', '__default__') + + # If it varies from current, commit and play. + if current_soundtrack != name and self._allow_changing_soundtracks: + ba.playsound(ba.getsound('gunCocking')) + cfg['Soundtrack'] = self._selected_soundtrack + cfg.commit() + + # Just play whats already playing.. this'll grab it from the + # new soundtrack. + music.do_play_music(music.music_types[ba.MusicPlayMode.REGULAR]) + + def _back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.settings import audio + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + audio.AudioSettingsWindow(transition='in_left').get_root_widget()) + + def _edit_soundtrack_with_sound(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.purchase import PurchaseWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + ba.playsound(ba.getsound('swish')) + self._edit_soundtrack() + + def _edit_soundtrack(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.purchase import PurchaseWindow + from bastd.ui.soundtrack.edit import SoundtrackEditWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + if self._selected_soundtrack is None: + return + if self._selected_soundtrack == '__default__': + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource=self._r + + '.cantEditDefaultText'), + color=(1, 0, 0)) + return + + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window( + SoundtrackEditWindow(existing_soundtrack=self._selected_soundtrack + ).get_root_widget()) + + def _get_soundtrack_display_name(self, soundtrack: str) -> ba.Lstr: + if soundtrack == '__default__': + return ba.Lstr(resource=self._r + '.defaultSoundtrackNameText') + return ba.Lstr(value=soundtrack) + + def _refresh(self, select_soundtrack: str = None) -> None: + from efro.util import asserttype + self._allow_changing_soundtracks = False + old_selection = self._selected_soundtrack + + # If there was no prev selection, look in prefs. + if old_selection is None: + old_selection = ba.app.config.get('Soundtrack') + old_selection_index = self._selected_soundtrack_index + + # Delete old. + while self._soundtrack_widgets: + self._soundtrack_widgets.pop().delete() + + self._soundtracks = ba.app.config.get('Soundtracks', {}) + assert self._soundtracks is not None + items = list(self._soundtracks.items()) + items.sort(key=lambda x: asserttype(x[0], str).lower()) + items = [('__default__', None)] + items # default is always first + index = 0 + for pname, _pval in items: + assert pname is not None + txtw = ba.textwidget( + parent=self._col, + size=(self._width - 40, 24), + text=self._get_soundtrack_display_name(pname), + h_align='left', + v_align='center', + maxwidth=self._width - 110, + always_highlight=True, + on_select_call=ba.WeakCall(self._select, pname, index), + on_activate_call=self._edit_soundtrack_with_sound, + selectable=True) + if index == 0: + ba.widget(edit=txtw, up_widget=self._back_button) + self._soundtrack_widgets.append(txtw) + + # Select this one if the user requested it + if select_soundtrack is not None: + if pname == select_soundtrack: + ba.columnwidget(edit=self._col, + selected_child=txtw, + visible_child=txtw) + else: + # Select this one if it was previously selected. + # Go by index if there's one. + if old_selection_index is not None: + if index == old_selection_index: + ba.columnwidget(edit=self._col, + selected_child=txtw, + visible_child=txtw) + else: # Otherwise look by name. + if pname == old_selection: + ba.columnwidget(edit=self._col, + selected_child=txtw, + visible_child=txtw) + index += 1 + + # Explicitly run select callback on current one and re-enable + # callbacks. + + # Eww need to run this in a timer so it happens after our select + # callbacks. With a small-enough time sometimes it happens before + # anyway. Ew. need a way to just schedule a callable i guess. + ba.timer(0.1, + ba.WeakCall(self._set_allow_changing), + timetype=ba.TimeType.REAL) + + def _set_allow_changing(self) -> None: + self._allow_changing_soundtracks = True + assert self._selected_soundtrack is not None + assert self._selected_soundtrack_index is not None + self._select(self._selected_soundtrack, + self._selected_soundtrack_index) + + def _new_soundtrack(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.purchase import PurchaseWindow + from bastd.ui.soundtrack.edit import SoundtrackEditWindow + if not ba.app.accounts.have_pro_options(): + PurchaseWindow(items=['pro']) + return + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + SoundtrackEditWindow(existing_soundtrack=None) + + def _create_done(self, new_soundtrack: str) -> None: + if new_soundtrack is not None: + ba.playsound(ba.getsound('gunCocking')) + self._refresh(select_soundtrack=new_soundtrack) + + def _save_state(self) -> None: + try: + sel = self._root_widget.get_selected_child() + if sel == self._scrollwidget: + sel_name = 'Scroll' + elif sel == self._new_button: + sel_name = 'New' + elif sel == self._edit_button: + sel_name = 'Edit' + elif sel == self._duplicate_button: + sel_name = 'Duplicate' + elif sel == self._delete_button: + sel_name = 'Delete' + elif sel == self._back_button: + sel_name = 'Back' + else: + raise ValueError(f'unrecognized selection \'{sel}\'') + ba.app.ui.window_states[type(self)] = sel_name + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + def _restore_state(self) -> None: + try: + sel_name = ba.app.ui.window_states.get(type(self)) + if sel_name == 'Scroll': + sel = self._scrollwidget + elif sel_name == 'New': + sel = self._new_button + elif sel_name == 'Edit': + sel = self._edit_button + elif sel_name == 'Duplicate': + sel = self._duplicate_button + elif sel_name == 'Delete': + sel = self._delete_button + else: + sel = self._scrollwidget + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') diff --git a/dist/ba_data/python/bastd/ui/soundtrack/edit.py b/dist/ba_data/python/bastd/ui/soundtrack/edit.py new file mode 100644 index 0000000..e264d11 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/soundtrack/edit.py @@ -0,0 +1,403 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for editing a soundtrack.""" + +from __future__ import annotations + +import copy +import os +from typing import TYPE_CHECKING, cast + +import ba + +if TYPE_CHECKING: + from typing import Any, Dict, Union, Optional + + +class SoundtrackEditWindow(ba.Window): + """Window for editing a soundtrack.""" + + def __init__(self, + existing_soundtrack: Optional[Union[str, Dict[str, Any]]], + transition: str = 'in_right'): + # pylint: disable=too-many-statements + appconfig = ba.app.config + self._r = 'editSoundtrackWindow' + self._folder_tex = ba.gettexture('folder') + self._file_tex = ba.gettexture('file') + uiscale = ba.app.ui.uiscale + self._width = 848 if uiscale is ba.UIScale.SMALL else 648 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (395 if uiscale is ba.UIScale.SMALL else + 450 if uiscale is ba.UIScale.MEDIUM else 560) + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=(2.08 if uiscale is ba.UIScale.SMALL else + 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -48) if uiscale is ba.UIScale.SMALL else ( + 0, 15) if uiscale is ba.UIScale.MEDIUM else (0, 0))) + cancel_button = ba.buttonwidget(parent=self._root_widget, + position=(38 + x_inset, + self._height - 60), + size=(160, 60), + autoselect=True, + label=ba.Lstr(resource='cancelText'), + scale=0.8) + save_button = ba.buttonwidget(parent=self._root_widget, + position=(self._width - (168 + x_inset), + self._height - 60), + autoselect=True, + size=(160, 60), + label=ba.Lstr(resource='saveText'), + scale=0.8) + ba.widget(edit=save_button, left_widget=cancel_button) + ba.widget(edit=cancel_button, right_widget=save_button) + ba.textwidget( + parent=self._root_widget, + position=(0, self._height - 50), + size=(self._width, 25), + text=ba.Lstr(resource=self._r + + ('.editSoundtrackText' if existing_soundtrack + is not None else '.newSoundtrackText')), + color=ba.app.ui.title_color, + h_align='center', + v_align='center', + maxwidth=280) + v = self._height - 110 + if 'Soundtracks' not in appconfig: + appconfig['Soundtracks'] = {} + + self._soundtrack_name: Optional[str] + self._existing_soundtrack_name: Optional[str] + if existing_soundtrack is not None: + # if they passed just a name, pull info from that soundtrack + if isinstance(existing_soundtrack, str): + self._soundtrack = copy.deepcopy( + appconfig['Soundtracks'][existing_soundtrack]) + self._soundtrack_name = existing_soundtrack + self._existing_soundtrack_name = existing_soundtrack + self._last_edited_song_type = None + else: + # otherwise they can pass info on an in-progress edit + self._soundtrack = existing_soundtrack['soundtrack'] + self._soundtrack_name = existing_soundtrack['name'] + self._existing_soundtrack_name = ( + existing_soundtrack['existing_name']) + self._last_edited_song_type = ( + existing_soundtrack['last_edited_song_type']) + else: + self._soundtrack_name = None + self._existing_soundtrack_name = None + self._soundtrack = {} + self._last_edited_song_type = None + + ba.textwidget(parent=self._root_widget, + text=ba.Lstr(resource=self._r + '.nameText'), + maxwidth=80, + scale=0.8, + position=(105 + x_inset, v + 19), + color=(0.8, 0.8, 0.8, 0.5), + size=(0, 0), + h_align='right', + v_align='center') + + # if there's no initial value, find a good initial unused name + if existing_soundtrack is None: + i = 1 + st_name_text = ba.Lstr(resource=self._r + + '.newSoundtrackNameText').evaluate() + if '${COUNT}' not in st_name_text: + # make sure we insert number *somewhere* + st_name_text = st_name_text + ' ${COUNT}' + while True: + self._soundtrack_name = st_name_text.replace( + '${COUNT}', str(i)) + if self._soundtrack_name not in appconfig['Soundtracks']: + break + i += 1 + + self._text_field = ba.textwidget( + parent=self._root_widget, + position=(120 + x_inset, v - 5), + size=(self._width - (160 + 2 * x_inset), 43), + text=self._soundtrack_name, + h_align='left', + v_align='center', + max_chars=32, + autoselect=True, + description=ba.Lstr(resource=self._r + '.nameText'), + editable=True, + padding=4, + on_return_press_call=self._do_it_with_sound) + + scroll_height = self._height - 180 + self._scrollwidget = scrollwidget = ba.scrollwidget( + parent=self._root_widget, + highlight=False, + position=(40 + x_inset, v - (scroll_height + 10)), + size=(self._width - (80 + 2 * x_inset), scroll_height), + simple_culling_v=10, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + ba.widget(edit=self._text_field, down_widget=self._scrollwidget) + self._col = ba.columnwidget(parent=scrollwidget, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + + self._song_type_buttons: Dict[str, ba.Widget] = {} + self._refresh() + ba.buttonwidget(edit=cancel_button, on_activate_call=self._cancel) + ba.containerwidget(edit=self._root_widget, cancel_button=cancel_button) + ba.buttonwidget(edit=save_button, on_activate_call=self._do_it) + ba.containerwidget(edit=self._root_widget, start_button=save_button) + ba.widget(edit=self._text_field, up_widget=cancel_button) + ba.widget(edit=cancel_button, down_widget=self._text_field) + + def _refresh(self) -> None: + for widget in self._col.get_children(): + widget.delete() + + types = [ + 'Menu', + 'CharSelect', + 'ToTheDeath', + 'Onslaught', + 'Keep Away', + 'Race', + 'Epic Race', + 'ForwardMarch', + 'FlagCatcher', + 'Survival', + 'Epic', + 'Hockey', + 'Football', + 'Flying', + 'Scary', + 'Marching', + 'GrandRomp', + 'Chosen One', + 'Scores', + 'Victory', + ] + + # FIXME: We should probably convert this to use translations. + type_names_translated = ba.app.lang.get_resource('soundtrackTypeNames') + prev_type_button: Optional[ba.Widget] = None + prev_test_button: Optional[ba.Widget] = None + + for index, song_type in enumerate(types): + row = ba.rowwidget(parent=self._col, + size=(self._width - 40, 40), + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + type_name = type_names_translated.get(song_type, song_type) + ba.textwidget(parent=row, + size=(230, 25), + always_highlight=True, + text=type_name, + scale=0.7, + h_align='left', + v_align='center', + maxwidth=190) + + if song_type in self._soundtrack: + entry = self._soundtrack[song_type] + else: + entry = None + + if entry is not None: + # Make sure they don't muck with this after it gets to us. + entry = copy.deepcopy(entry) + + icon_type = self._get_entry_button_display_icon_type(entry) + self._song_type_buttons[song_type] = btn = ba.buttonwidget( + parent=row, + size=(230, 32), + label=self._get_entry_button_display_name(entry), + text_scale=0.6, + on_activate_call=ba.Call(self._get_entry, song_type, entry, + type_name), + icon=(self._file_tex if icon_type == 'file' else + self._folder_tex if icon_type == 'folder' else None), + icon_color=(1.1, 0.8, 0.2) if icon_type == 'folder' else + (1, 1, 1), + left_widget=self._text_field, + iconscale=0.7, + autoselect=True, + up_widget=prev_type_button) + if index == 0: + ba.widget(edit=btn, up_widget=self._text_field) + ba.widget(edit=btn, down_widget=btn) + + if (self._last_edited_song_type is not None + and song_type == self._last_edited_song_type): + ba.containerwidget(edit=row, + selected_child=btn, + visible_child=btn) + ba.containerwidget(edit=self._col, + selected_child=row, + visible_child=row) + ba.containerwidget(edit=self._scrollwidget, + selected_child=self._col, + visible_child=self._col) + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget, + visible_child=self._scrollwidget) + + if prev_type_button is not None: + ba.widget(edit=prev_type_button, down_widget=btn) + prev_type_button = btn + ba.textwidget(parent=row, size=(10, 32), text='') # spacing + btn = ba.buttonwidget( + parent=row, + size=(50, 32), + label=ba.Lstr(resource=self._r + '.testText'), + text_scale=0.6, + on_activate_call=ba.Call(self._test, ba.MusicType(song_type)), + up_widget=prev_test_button + if prev_test_button is not None else self._text_field) + if prev_test_button is not None: + ba.widget(edit=prev_test_button, down_widget=btn) + ba.widget(edit=btn, down_widget=btn, right_widget=btn) + prev_test_button = btn + + @classmethod + def _restore_editor(cls, state: Dict[str, Any], musictype: str, + entry: Any) -> None: + music = ba.app.music + + # Apply the change and recreate the window. + soundtrack = state['soundtrack'] + existing_entry = (None if musictype not in soundtrack else + soundtrack[musictype]) + if existing_entry != entry: + ba.playsound(ba.getsound('gunCocking')) + + # Make sure this doesn't get mucked with after we get it. + if entry is not None: + entry = copy.deepcopy(entry) + + entry_type = music.get_soundtrack_entry_type(entry) + if entry_type == 'default': + # For 'default' entries simply exclude them from the list. + if musictype in soundtrack: + del soundtrack[musictype] + else: + soundtrack[musictype] = entry + + ba.app.ui.set_main_menu_window( + cls(state, transition='in_left').get_root_widget()) + + def _get_entry(self, song_type: str, entry: Any, + selection_target_name: str) -> None: + music = ba.app.music + if selection_target_name != '': + selection_target_name = "'" + selection_target_name + "'" + state = { + 'name': self._soundtrack_name, + 'existing_name': self._existing_soundtrack_name, + 'soundtrack': self._soundtrack, + 'last_edited_song_type': song_type + } + ba.containerwidget(edit=self._root_widget, transition='out_left') + ba.app.ui.set_main_menu_window(music.get_music_player().select_entry( + ba.Call(self._restore_editor, state, song_type), entry, + selection_target_name).get_root_widget()) + + def _test(self, song_type: ba.MusicType) -> None: + music = ba.app.music + + # Warn if volume is zero. + if ba.app.config.resolve('Music Volume') < 0.01: + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource=self._r + + '.musicVolumeZeroWarning'), + color=(1, 0.5, 0)) + music.set_music_play_mode(ba.MusicPlayMode.TEST) + music.do_play_music(song_type, + mode=ba.MusicPlayMode.TEST, + testsoundtrack=self._soundtrack) + + def _get_entry_button_display_name(self, + entry: Any) -> Union[str, ba.Lstr]: + music = ba.app.music + etype = music.get_soundtrack_entry_type(entry) + ename: Union[str, ba.Lstr] + if etype == 'default': + ename = ba.Lstr(resource=self._r + '.defaultGameMusicText') + elif etype in ('musicFile', 'musicFolder'): + ename = os.path.basename(music.get_soundtrack_entry_name(entry)) + else: + ename = music.get_soundtrack_entry_name(entry) + return ename + + def _get_entry_button_display_icon_type(self, entry: Any) -> Optional[str]: + music = ba.app.music + etype = music.get_soundtrack_entry_type(entry) + if etype == 'musicFile': + return 'file' + if etype == 'musicFolder': + return 'folder' + return None + + def _cancel(self) -> None: + from bastd.ui.soundtrack import browser as stb + music = ba.app.music + + # Resets music back to normal. + music.set_music_play_mode(ba.MusicPlayMode.REGULAR) + ba.containerwidget(edit=self._root_widget, transition='out_right') + ba.app.ui.set_main_menu_window( + stb.SoundtrackBrowserWindow( + transition='in_left').get_root_widget()) + + def _do_it(self) -> None: + from bastd.ui.soundtrack import browser as stb + music = ba.app.music + cfg = ba.app.config + new_name = cast(str, ba.textwidget(query=self._text_field)) + if (new_name != self._soundtrack_name + and new_name in cfg['Soundtracks']): + ba.screenmessage( + ba.Lstr(resource=self._r + '.cantSaveAlreadyExistsText')) + ba.playsound(ba.getsound('error')) + return + if not new_name: + ba.playsound(ba.getsound('error')) + return + if new_name == ba.Lstr(resource=self._r + + '.defaultSoundtrackNameText').evaluate(): + ba.screenmessage( + ba.Lstr(resource=self._r + '.cantOverwriteDefaultText')) + ba.playsound(ba.getsound('error')) + return + + # Make sure config exists. + if 'Soundtracks' not in cfg: + cfg['Soundtracks'] = {} + + # If we had an old one, delete it. + if (self._existing_soundtrack_name is not None + and self._existing_soundtrack_name in cfg['Soundtracks']): + del cfg['Soundtracks'][self._existing_soundtrack_name] + cfg['Soundtracks'][new_name] = self._soundtrack + cfg['Soundtrack'] = new_name + + cfg.commit() + ba.playsound(ba.getsound('gunCocking')) + ba.containerwidget(edit=self._root_widget, transition='out_right') + + # Resets music back to normal. + music.set_music_play_mode(ba.MusicPlayMode.REGULAR, force_restart=True) + + ba.app.ui.set_main_menu_window( + stb.SoundtrackBrowserWindow( + transition='in_left').get_root_widget()) + + def _do_it_with_sound(self) -> None: + ba.playsound(ba.getsound('swish')) + self._do_it() diff --git a/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py b/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py new file mode 100644 index 0000000..142a6bc --- /dev/null +++ b/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py @@ -0,0 +1,202 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI for selecting soundtrack entry types.""" +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + +class SoundtrackEntryTypeSelectWindow(ba.Window): + """Window for selecting a soundtrack entry type.""" + + def __init__(self, + callback: Callable[[Any], Any], + current_entry: Any, + selection_target_name: str, + transition: str = 'in_right'): + music = ba.app.music + self._r = 'editSoundtrackWindow' + + self._callback = callback + self._current_entry = copy.deepcopy(current_entry) + + self._width = 580 + self._height = 220 + spacing = 80 + + # FIXME: Generalize this so new custom soundtrack types can add + # themselves here. + do_default = True + do_mac_music_app_playlist = music.supports_soundtrack_entry_type( + 'iTunesPlaylist') + do_music_file = music.supports_soundtrack_entry_type('musicFile') + do_music_folder = music.supports_soundtrack_entry_type('musicFolder') + + if do_mac_music_app_playlist: + self._height += spacing + if do_music_file: + self._height += spacing + if do_music_folder: + self._height += spacing + + uiscale = ba.app.ui.uiscale + + # NOTE: When something is selected, we close our UI and kick off + # another window which then calls us back when its done, so the + # standard UI-cleanup-check complains that something is holding on + # to our instance after its ui is gone. Should restructure in a + # cleaner way, but just disabling that check for now. + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=(1.7 if uiscale is ba.UIScale.SMALL else + 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0)), + cleanupcheck=False) + btn = ba.buttonwidget(parent=self._root_widget, + position=(35, self._height - 65), + size=(160, 60), + scale=0.8, + text_scale=1.2, + label=ba.Lstr(resource='cancelText'), + on_activate_call=self._on_cancel_press) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 32), + size=(0, 0), + text=ba.Lstr(resource=self._r + '.selectASourceText'), + color=ba.app.ui.title_color, + maxwidth=230, + h_align='center', + v_align='center') + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 56), + size=(0, 0), + text=selection_target_name, + color=ba.app.ui.infotextcolor, + scale=0.7, + maxwidth=230, + h_align='center', + v_align='center') + + v = self._height - 155 + + current_entry_type = music.get_soundtrack_entry_type(current_entry) + + if do_default: + btn = ba.buttonwidget(parent=self._root_widget, + size=(self._width - 100, 60), + position=(50, v), + label=ba.Lstr(resource=self._r + + '.useDefaultGameMusicText'), + on_activate_call=self._on_default_press) + if current_entry_type == 'default': + ba.containerwidget(edit=self._root_widget, selected_child=btn) + v -= spacing + + if do_mac_music_app_playlist: + btn = ba.buttonwidget( + parent=self._root_widget, + size=(self._width - 100, 60), + position=(50, v), + label=ba.Lstr(resource=self._r + '.useITunesPlaylistText'), + on_activate_call=self._on_mac_music_app_playlist_press, + icon=None) + if current_entry_type == 'iTunesPlaylist': + ba.containerwidget(edit=self._root_widget, selected_child=btn) + v -= spacing + + if do_music_file: + btn = ba.buttonwidget(parent=self._root_widget, + size=(self._width - 100, 60), + position=(50, v), + label=ba.Lstr(resource=self._r + + '.useMusicFileText'), + on_activate_call=self._on_music_file_press, + icon=ba.gettexture('file')) + if current_entry_type == 'musicFile': + ba.containerwidget(edit=self._root_widget, selected_child=btn) + v -= spacing + + if do_music_folder: + btn = ba.buttonwidget(parent=self._root_widget, + size=(self._width - 100, 60), + position=(50, v), + label=ba.Lstr(resource=self._r + + '.useMusicFolderText'), + on_activate_call=self._on_music_folder_press, + icon=ba.gettexture('folder'), + icon_color=(1.1, 0.8, 0.2)) + if current_entry_type == 'musicFolder': + ba.containerwidget(edit=self._root_widget, selected_child=btn) + v -= spacing + + def _on_mac_music_app_playlist_press(self) -> None: + music = ba.app.music + from bastd.ui.soundtrack.macmusicapp import ( + MacMusicAppPlaylistSelectWindow) + ba.containerwidget(edit=self._root_widget, transition='out_left') + + current_playlist_entry: Optional[str] + if (music.get_soundtrack_entry_type( + self._current_entry) == 'iTunesPlaylist'): + current_playlist_entry = music.get_soundtrack_entry_name( + self._current_entry) + else: + current_playlist_entry = None + ba.app.ui.set_main_menu_window( + MacMusicAppPlaylistSelectWindow( + self._callback, current_playlist_entry, + self._current_entry).get_root_widget()) + + def _on_music_file_press(self) -> None: + from ba.osmusic import OSMusicPlayer + from bastd.ui.fileselector import FileSelectorWindow + ba.containerwidget(edit=self._root_widget, transition='out_left') + base_path = _ba.android_get_external_storage_path() + ba.app.ui.set_main_menu_window( + FileSelectorWindow( + base_path, + callback=self._music_file_selector_cb, + show_base_path=False, + valid_file_extensions=( + OSMusicPlayer.get_valid_music_file_extensions()), + allow_folders=False).get_root_widget()) + + def _on_music_folder_press(self) -> None: + from bastd.ui.fileselector import FileSelectorWindow + ba.containerwidget(edit=self._root_widget, transition='out_left') + base_path = _ba.android_get_external_storage_path() + ba.app.ui.set_main_menu_window( + FileSelectorWindow(base_path, + callback=self._music_folder_selector_cb, + show_base_path=False, + valid_file_extensions=[], + allow_folders=True).get_root_widget()) + + def _music_file_selector_cb(self, result: Optional[str]) -> None: + if result is None: + self._callback(self._current_entry) + else: + self._callback({'type': 'musicFile', 'name': result}) + + def _music_folder_selector_cb(self, result: Optional[str]) -> None: + if result is None: + self._callback(self._current_entry) + else: + self._callback({'type': 'musicFolder', 'name': result}) + + def _on_default_press(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_right') + self._callback(None) + + def _on_cancel_press(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_right') + self._callback(self._current_entry) diff --git a/dist/ba_data/python/bastd/ui/soundtrack/macmusicapp.py b/dist/ba_data/python/bastd/ui/soundtrack/macmusicapp.py new file mode 100644 index 0000000..1f149b0 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/soundtrack/macmusicapp.py @@ -0,0 +1,98 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to using the macOS Music app for soundtracks.""" + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +import ba + +if TYPE_CHECKING: + from typing import Any, List, Optional, Callable + + +class MacMusicAppPlaylistSelectWindow(ba.Window): + """Window for selecting an iTunes playlist.""" + + def __init__(self, callback: Callable[[Any], Any], + existing_playlist: Optional[str], existing_entry: Any): + from ba.macmusicapp import MacMusicAppMusicPlayer + self._r = 'editSoundtrackWindow' + self._callback = callback + self._existing_playlist = existing_playlist + self._existing_entry = copy.deepcopy(existing_entry) + self._width = 520.0 + self._height = 520.0 + self._spacing = 45.0 + v = self._height - 90.0 + v -= self._spacing * 1.0 + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), transition='in_right')) + btn = ba.buttonwidget(parent=self._root_widget, + position=(35, self._height - 65), + size=(130, 50), + label=ba.Lstr(resource='cancelText'), + on_activate_call=self._back, + autoselect=True) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + ba.textwidget(parent=self._root_widget, + position=(20, self._height - 54), + size=(self._width, 25), + text=ba.Lstr(resource=self._r + '.selectAPlaylistText'), + color=ba.app.ui.title_color, + h_align='center', + v_align='center', + maxwidth=200) + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + position=(40, v - 340), + size=(self._width - 80, 400), + claims_tab=True, + selection_loops_to_parent=True) + ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + self._column = ba.columnwidget(parent=self._scrollwidget, + claims_tab=True, + selection_loops_to_parent=True) + + ba.textwidget(parent=self._column, + size=(self._width - 80, 22), + text=ba.Lstr(resource=self._r + '.fetchingITunesText'), + color=(0.6, 0.9, 0.6, 1.0), + scale=0.8) + musicplayer = ba.app.music.get_music_player() + assert isinstance(musicplayer, MacMusicAppMusicPlayer) + musicplayer.get_playlists(self._playlists_cb) + ba.containerwidget(edit=self._root_widget, + selected_child=self._scrollwidget) + + def _playlists_cb(self, playlists: List[str]) -> None: + if self._column: + for widget in self._column.get_children(): + widget.delete() + for i, playlist in enumerate(playlists): + txt = ba.textwidget(parent=self._column, + size=(self._width - 80, 30), + text=playlist, + v_align='center', + maxwidth=self._width - 110, + selectable=True, + on_activate_call=ba.Call( + self._sel, playlist), + click_activate=True) + ba.widget(edit=txt, show_buffer_top=40, show_buffer_bottom=40) + if playlist == self._existing_playlist: + ba.columnwidget(edit=self._column, + selected_child=txt, + visible_child=txt) + if i == len(playlists) - 1: + ba.widget(edit=txt, down_widget=txt) + + def _sel(self, selection: str) -> None: + if self._root_widget: + ba.containerwidget(edit=self._root_widget, transition='out_right') + self._callback({'type': 'iTunesPlaylist', 'name': selection}) + + def _back(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_right') + self._callback(self._existing_entry) diff --git a/dist/ba_data/python/bastd/ui/specialoffer.py b/dist/ba_data/python/bastd/ui/specialoffer.py new file mode 100644 index 0000000..3e3a542 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/specialoffer.py @@ -0,0 +1,465 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI for presenting sales/etc.""" + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + +class SpecialOfferWindow(ba.Window): + """Window for presenting sales/etc.""" + + def __init__(self, offer: Dict[str, Any], transition: str = 'in_right'): + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + from ba.internal import (get_store_item_display_size, get_clean_price) + from ba import SpecialChar + from bastd.ui.store import item as storeitemui + self._cancel_delay = offer.get('cancelDelay', 0) + + # First thing: if we're offering pro or an IAP, see if we have a + # price for it. + # If not, abort and go into zombie mode (the user should never see + # us that way). + + real_price: Optional[str] + + # Misnomer: 'pro' actually means offer 'pro_sale'. + if offer['item'] in ['pro', 'pro_fullprice']: + real_price = _ba.get_price('pro' if offer['item'] == + 'pro_fullprice' else 'pro_sale') + if real_price is None and ba.app.debug_build: + print('NOTE: Faking prices for debug build.') + real_price = '$1.23' + zombie = real_price is None + elif isinstance(offer['price'], str): + # (a string price implies IAP id) + real_price = _ba.get_price(offer['price']) + if real_price is None and ba.app.debug_build: + print('NOTE: Faking price for debug build.') + real_price = '$1.23' + zombie = real_price is None + else: + real_price = None + zombie = False + if real_price is None: + real_price = '?' + + if offer['item'] in ['pro', 'pro_fullprice']: + self._offer_item = 'pro' + else: + self._offer_item = offer['item'] + + # If we wanted a real price but didn't find one, go zombie. + if zombie: + return + + # This can pop up suddenly, so lets block input for 1 second. + _ba.lock_all_input() + ba.timer(1.0, _ba.unlock_all_input, timetype=ba.TimeType.REAL) + ba.playsound(ba.getsound('ding')) + ba.timer(0.3, + lambda: ba.playsound(ba.getsound('ooh')), + timetype=ba.TimeType.REAL) + self._offer = copy.deepcopy(offer) + self._width = 580 + self._height = 590 + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition=transition, + scale=(1.2 if uiscale is ba.UIScale.SMALL else + 1.15 if uiscale is ba.UIScale.MEDIUM else 1.0), + stack_offset=(0, -15) if uiscale is ba.UIScale.SMALL else (0, 0))) + self._is_bundle_sale = False + try: + if offer['item'] in ['pro', 'pro_fullprice']: + original_price_str = _ba.get_price('pro') + if original_price_str is None: + original_price_str = '?' + new_price_str = _ba.get_price('pro_sale') + if new_price_str is None: + new_price_str = '?' + percent_off_text = '' + else: + # If the offer includes bonus tickets, it's a bundle-sale. + if ('bonusTickets' in offer + and offer['bonusTickets'] is not None): + self._is_bundle_sale = True + original_price = _ba.get_account_misc_read_val( + 'price.' + self._offer_item, 9999) + + # For pure ticket prices we can show a percent-off. + if isinstance(offer['price'], int): + new_price = offer['price'] + tchar = ba.charstr(SpecialChar.TICKET) + original_price_str = tchar + str(original_price) + new_price_str = tchar + str(new_price) + percent_off = int( + round(100.0 - + (float(new_price) / original_price) * 100.0)) + percent_off_text = ' ' + ba.Lstr( + resource='store.salePercentText').evaluate().replace( + '${PERCENT}', str(percent_off)) + else: + original_price_str = new_price_str = '?' + percent_off_text = '' + + except Exception: + print(f'Offer: {offer}') + ba.print_exception('Error setting up special-offer') + original_price_str = new_price_str = '?' + percent_off_text = '' + + # If its a bundle sale, change the title. + if self._is_bundle_sale: + sale_text = ba.Lstr(resource='store.saleBundleText', + fallback_resource='store.saleText').evaluate() + else: + # For full pro we say 'Upgrade?' since its not really a sale. + if offer['item'] == 'pro_fullprice': + sale_text = ba.Lstr( + resource='store.upgradeQuestionText', + fallback_resource='store.saleExclaimText').evaluate() + else: + sale_text = ba.Lstr( + resource='store.saleExclaimText', + fallback_resource='store.saleText').evaluate() + + self._title_text = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 40), + size=(0, 0), + text=sale_text + + ((' ' + ba.Lstr(resource='store.oneTimeOnlyText').evaluate()) + if self._offer['oneTimeOnly'] else '') + percent_off_text, + h_align='center', + v_align='center', + maxwidth=self._width * 0.9 - 220, + scale=1.4, + color=(0.3, 1, 0.3)) + + self._flash_on = False + self._flashing_timer: Optional[ba.Timer] = ba.Timer( + 0.05, + ba.WeakCall(self._flash_cycle), + repeat=True, + timetype=ba.TimeType.REAL) + ba.timer(0.6, + ba.WeakCall(self._stop_flashing), + timetype=ba.TimeType.REAL) + + size = get_store_item_display_size(self._offer_item) + display: Dict[str, Any] = {} + storeitemui.instantiate_store_item_display( + self._offer_item, + display, + parent_widget=self._root_widget, + b_pos=(self._width * 0.5 - size[0] * 0.5 + 10 - + ((size[0] * 0.5 + 30) if self._is_bundle_sale else 0), + self._height * 0.5 - size[1] * 0.5 + 20 + + (20 if self._is_bundle_sale else 0)), + b_width=size[0], + b_height=size[1], + button=not self._is_bundle_sale) + + # Wire up the parts we need. + if self._is_bundle_sale: + self._plus_text = ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, + self._height * 0.5 + 50), + size=(0, 0), + text='+', + h_align='center', + v_align='center', + maxwidth=self._width * 0.9, + scale=1.4, + color=(0.5, 0.5, 0.5)) + self._plus_tickets = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5 + 120, self._height * 0.5 + 50), + size=(0, 0), + text=ba.charstr(SpecialChar.TICKET_BACKING) + + str(offer['bonusTickets']), + h_align='center', + v_align='center', + maxwidth=self._width * 0.9, + scale=2.5, + color=(0.2, 1, 0.2)) + self._price_text = ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, 150), + size=(0, 0), + text=real_price, + h_align='center', + v_align='center', + maxwidth=self._width * 0.9, + scale=1.4, + color=(0.2, 1, 0.2)) + # Total-value if they supplied it. + total_worth_item = offer.get('valueItem', None) + if total_worth_item is not None: + price = _ba.get_price(total_worth_item) + total_worth_price = (get_clean_price(price) + if price is not None else None) + if total_worth_price is not None: + total_worth_text = ba.Lstr(resource='store.totalWorthText', + subs=[('${TOTAL_WORTH}', + total_worth_price)]) + self._total_worth_text = ba.textwidget( + parent=self._root_widget, + text=total_worth_text, + position=(self._width * 0.5, 210), + scale=0.9, + maxwidth=self._width * 0.7, + size=(0, 0), + h_align='center', + v_align='center', + shadow=1.0, + flatness=1.0, + color=(0.3, 1, 1)) + + elif offer['item'] == 'pro_fullprice': + # for full-price pro we simply show full price + ba.textwidget(edit=display['price_widget'], text=real_price) + ba.buttonwidget(edit=display['button'], + on_activate_call=self._purchase) + else: + # Show old/new prices otherwise (for pro sale). + ba.buttonwidget(edit=display['button'], + on_activate_call=self._purchase) + ba.imagewidget(edit=display['price_slash_widget'], opacity=1.0) + ba.textwidget(edit=display['price_widget_left'], + text=original_price_str) + ba.textwidget(edit=display['price_widget_right'], + text=new_price_str) + + # Add ticket button only if this is ticket-purchasable. + if isinstance(offer.get('price'), int): + self._get_tickets_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - 125, self._height - 68), + size=(90, 55), + scale=1.0, + button_type='square', + color=(0.7, 0.5, 0.85), + textcolor=(0.2, 1, 0.2), + autoselect=True, + label=ba.Lstr(resource='getTicketsWindow.titleText'), + on_activate_call=self._on_get_more_tickets_press) + + self._ticket_text_update_timer = ba.Timer( + 1.0, + ba.WeakCall(self._update_tickets_text), + timetype=ba.TimeType.REAL, + repeat=True) + self._update_tickets_text() + + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + timetype=ba.TimeType.REAL, + repeat=True) + + self._cancel_button = ba.buttonwidget( + parent=self._root_widget, + position=(50, 40) if self._is_bundle_sale else + (self._width * 0.5 - 75, 40), + size=(150, 60), + scale=1.0, + on_activate_call=self._cancel, + autoselect=True, + label=ba.Lstr(resource='noThanksText')) + self._cancel_countdown_text = ba.textwidget( + parent=self._root_widget, + text='', + position=(50 + 150 + 20, 40 + 27) if self._is_bundle_sale else + (self._width * 0.5 - 75 + 150 + 20, 40 + 27), + scale=1.1, + size=(0, 0), + h_align='left', + v_align='center', + shadow=1.0, + flatness=1.0, + color=(0.6, 0.5, 0.5)) + self._update_cancel_button_graphics() + + if self._is_bundle_sale: + self._purchase_button = ba.buttonwidget( + parent=self._root_widget, + position=(self._width - 200, 40), + size=(150, 60), + scale=1.0, + on_activate_call=self._purchase, + autoselect=True, + label=ba.Lstr(resource='store.purchaseText')) + + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button, + start_button=self._purchase_button + if self._is_bundle_sale else None, + selected_child=self._purchase_button + if self._is_bundle_sale else display['button']) + + def _stop_flashing(self) -> None: + self._flashing_timer = None + ba.textwidget(edit=self._title_text, color=(0.3, 1, 0.3)) + + def _flash_cycle(self) -> None: + if not self._root_widget: + return + self._flash_on = not self._flash_on + ba.textwidget(edit=self._title_text, + color=(0.3, 1, 0.3) if self._flash_on else (1, 0.5, 0)) + + def _update_cancel_button_graphics(self) -> None: + ba.buttonwidget(edit=self._cancel_button, + color=(0.5, 0.5, 0.5) if self._cancel_delay > 0 else + (0.7, 0.4, 0.34), + textcolor=(0.5, 0.5, + 0.5) if self._cancel_delay > 0 else + (0.9, 0.9, 1.0)) + ba.textwidget( + edit=self._cancel_countdown_text, + text=str(self._cancel_delay) if self._cancel_delay > 0 else '') + + def _update(self) -> None: + + # If we've got seconds left on our countdown, update it. + if self._cancel_delay > 0: + self._cancel_delay = max(0, self._cancel_delay - 1) + self._update_cancel_button_graphics() + + can_die = False + + # We go away if we see that our target item is owned. + if self._offer_item == 'pro': + if _ba.app.accounts.have_pro(): + can_die = True + else: + if _ba.get_purchased(self._offer_item): + can_die = True + + if can_die: + self._transition_out('out_left') + + def _transition_out(self, transition: str = 'out_left') -> None: + # Also clear any pending-special-offer we've stored at this point. + cfg = ba.app.config + if 'pendingSpecialOffer' in cfg: + del cfg['pendingSpecialOffer'] + cfg.commit() + + ba.containerwidget(edit=self._root_widget, transition=transition) + + def _update_tickets_text(self) -> None: + from ba import SpecialChar + if not self._root_widget: + return + sval: Union[str, ba.Lstr] + if _ba.get_account_state() == 'signed_in': + sval = (ba.charstr(SpecialChar.TICKET) + + str(_ba.get_account_ticket_count())) + else: + sval = ba.Lstr(resource='getTicketsWindow.titleText') + ba.buttonwidget(edit=self._get_tickets_button, label=sval) + + def _on_get_more_tickets_press(self) -> None: + from bastd.ui import account + from bastd.ui import getcurrency + if _ba.get_account_state() != 'signed_in': + account.show_sign_in_prompt() + return + getcurrency.GetCurrencyWindow(modal=True).get_root_widget() + + def _purchase(self) -> None: + from ba.internal import get_store_item_name_translated + from bastd.ui import getcurrency + from bastd.ui import confirm + if self._offer['item'] == 'pro': + _ba.purchase('pro_sale') + elif self._offer['item'] == 'pro_fullprice': + _ba.purchase('pro') + elif self._is_bundle_sale: + # With bundle sales, the price is the name of the IAP. + _ba.purchase(self._offer['price']) + else: + ticket_count: Optional[int] + try: + ticket_count = _ba.get_account_ticket_count() + except Exception: + ticket_count = None + if (ticket_count is not None + and ticket_count < self._offer['price']): + getcurrency.show_get_tickets_prompt() + ba.playsound(ba.getsound('error')) + return + + def do_it() -> None: + _ba.in_game_purchase('offer:' + str(self._offer['id']), + self._offer['price']) + + ba.playsound(ba.getsound('swish')) + confirm.ConfirmWindow(ba.Lstr( + resource='store.purchaseConfirmText', + subs=[('${ITEM}', + get_store_item_name_translated(self._offer['item']))]), + width=400, + height=120, + action=do_it, + ok_text=ba.Lstr( + resource='store.purchaseText', + fallback_resource='okText')) + + def _cancel(self) -> None: + if self._cancel_delay > 0: + ba.playsound(ba.getsound('error')) + return + self._transition_out('out_right') + + +def show_offer() -> bool: + """(internal)""" + try: + from bastd.ui import feedback + app = ba.app + + # Space things out a bit so we don't hit the poor user with an ad and + # then an in-game offer. + has_been_long_enough_since_ad = True + if (app.ads.last_ad_completion_time is not None and + (ba.time(ba.TimeType.REAL) - app.ads.last_ad_completion_time < + 30.0)): + has_been_long_enough_since_ad = False + + if app.special_offer is not None and has_been_long_enough_since_ad: + + # Special case: for pro offers, store this in our prefs so we + # can re-show it if the user kills us (set phasers to 'NAG'!!!). + if app.special_offer.get('item') == 'pro_fullprice': + cfg = app.config + cfg['pendingSpecialOffer'] = { + 'a': _ba.get_public_login_id(), + 'o': app.special_offer + } + cfg.commit() + + with ba.Context('ui'): + if app.special_offer['item'] == 'rating': + feedback.ask_for_rating() + else: + SpecialOfferWindow(app.special_offer) + + app.special_offer = None + return True + except Exception: + ba.print_exception('Error showing offer.') + + return False diff --git a/dist/ba_data/python/bastd/ui/store/__init__.py b/dist/ba_data/python/bastd/ui/store/__init__.py new file mode 100644 index 0000000..867b171 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/store/__init__.py @@ -0,0 +1 @@ +# Released under the MIT License. See LICENSE for details. diff --git a/dist/ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c5da340 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..70f7e5d Binary files /dev/null and b/dist/ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/store/__pycache__/browser.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/store/__pycache__/browser.cpython-38.opt-1.pyc new file mode 100644 index 0000000..10daf73 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/store/__pycache__/browser.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/store/__pycache__/browser.cpython-38.pyc b/dist/ba_data/python/bastd/ui/store/__pycache__/browser.cpython-38.pyc new file mode 100644 index 0000000..1a45d01 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/store/__pycache__/browser.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/store/__pycache__/button.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/store/__pycache__/button.cpython-38.opt-1.pyc new file mode 100644 index 0000000..ae1fdb0 Binary files /dev/null and b/dist/ba_data/python/bastd/ui/store/__pycache__/button.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/store/__pycache__/button.cpython-38.pyc b/dist/ba_data/python/bastd/ui/store/__pycache__/button.cpython-38.pyc new file mode 100644 index 0000000..ed8784d Binary files /dev/null and b/dist/ba_data/python/bastd/ui/store/__pycache__/button.cpython-38.pyc differ diff --git a/dist/ba_data/python/bastd/ui/store/__pycache__/item.cpython-38.opt-1.pyc b/dist/ba_data/python/bastd/ui/store/__pycache__/item.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d12c53d Binary files /dev/null and b/dist/ba_data/python/bastd/ui/store/__pycache__/item.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/bastd/ui/store/browser.py b/dist/ba_data/python/bastd/ui/store/browser.py new file mode 100644 index 0000000..990f406 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/store/browser.py @@ -0,0 +1,1092 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI for browsing the store.""" +# pylint: disable=too-many-lines +from __future__ import annotations + +import copy +import math +import weakref +from enum import Enum +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import (Any, Callable, Optional, Tuple, Dict, Union, Sequence, + List) + + +class StoreBrowserWindow(ba.Window): + """Window for browsing the store.""" + + class TabID(Enum): + """Our available tab types.""" + EXTRAS = 'extras' + MAPS = 'maps' + MINIGAMES = 'minigames' + CHARACTERS = 'characters' + ICONS = 'icons' + + def __init__(self, + transition: str = 'in_right', + modal: bool = False, + show_tab: StoreBrowserWindow.TabID = None, + on_close_call: Callable[[], Any] = None, + back_location: str = None, + origin_widget: ba.Widget = None): + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + from bastd.ui.tabs import TabRow + from ba import SpecialChar + + app = ba.app + uiscale = app.ui.uiscale + + ba.set_analytics_screen('Store Window') + + scale_origin: Optional[Tuple[float, float]] + + # If they provided an origin-widget, scale up from that. + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + + self.button_infos: Optional[Dict[str, Dict[str, Any]]] = None + self.update_buttons_timer: Optional[ba.Timer] = None + self._status_textwidget_update_timer = None + + self._back_location = back_location + self._on_close_call = on_close_call + self._show_tab = show_tab + self._modal = modal + self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 + self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (578 if uiscale is ba.UIScale.SMALL else + 645 if uiscale is ba.UIScale.MEDIUM else 800) + self._current_tab: Optional[StoreBrowserWindow.TabID] = None + extra_top = 30 if uiscale is ba.UIScale.SMALL else 0 + + self._request: Any = None + self._r = 'store' + self._last_buy_time: Optional[float] = None + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + extra_top), + transition=transition, + toolbar_visibility='menu_full', + scale=(1.3 if uiscale is ba.UIScale.SMALL else + 0.9 if uiscale is ba.UIScale.MEDIUM else 0.8), + scale_origin_stack_offset=scale_origin, + stack_offset=((0, -5) if uiscale is ba.UIScale.SMALL else ( + 0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0)))) + + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + position=(70 + x_inset, self._height - 74), + size=(140, 60), + scale=1.1, + autoselect=True, + label=ba.Lstr(resource='doneText' if self._modal else 'backText'), + button_type=None if self._modal else 'back', + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + + self._ticket_count_text: Optional[ba.Widget] = None + self._get_tickets_button: Optional[ba.Widget] = None + + if ba.app.allow_ticket_purchases: + self._get_tickets_button = ba.buttonwidget( + parent=self._root_widget, + size=(210, 65), + on_activate_call=self._on_get_more_tickets_press, + autoselect=True, + scale=0.9, + text_scale=1.4, + left_widget=self._back_button, + color=(0.7, 0.5, 0.85), + textcolor=(0.2, 1.0, 0.2), + label=ba.Lstr(resource='getTicketsWindow.titleText')) + else: + self._ticket_count_text = ba.textwidget(parent=self._root_widget, + size=(210, 64), + color=(0.2, 1.0, 0.2), + h_align='center', + v_align='center') + + # Move this dynamically to keep it out of the way of the party icon. + self._update_get_tickets_button_pos() + self._get_ticket_pos_update_timer = ba.Timer( + 1.0, + ba.WeakCall(self._update_get_tickets_button_pos), + repeat=True, + timetype=ba.TimeType.REAL) + if self._get_tickets_button: + ba.widget(edit=self._back_button, + right_widget=self._get_tickets_button) + self._ticket_text_update_timer = ba.Timer( + 1.0, + ba.WeakCall(self._update_tickets_text), + timetype=ba.TimeType.REAL, + repeat=True) + self._update_tickets_text() + + app = ba.app + if app.platform in ['mac', 'ios'] and app.subplatform == 'appstore': + ba.buttonwidget( + parent=self._root_widget, + position=(self._width * 0.5 - 70, 16), + size=(230, 50), + scale=0.65, + on_activate_call=ba.WeakCall(self._restore_purchases), + color=(0.35, 0.3, 0.4), + selectable=False, + textcolor=(0.55, 0.5, 0.6), + label=ba.Lstr( + resource='getTicketsWindow.restorePurchasesText')) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 44), + size=(0, 0), + color=app.ui.title_color, + scale=1.5, + h_align='center', + v_align='center', + text=ba.Lstr(resource='storeText'), + maxwidth=420) + + if not self._modal: + ba.buttonwidget(edit=self._back_button, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(SpecialChar.BACK)) + + scroll_buffer_h = 130 + 2 * x_inset + tab_buffer_h = 250 + 2 * x_inset + + tabs_def = [ + (self.TabID.EXTRAS, ba.Lstr(resource=self._r + '.extrasText')), + (self.TabID.MAPS, ba.Lstr(resource=self._r + '.mapsText')), + (self.TabID.MINIGAMES, + ba.Lstr(resource=self._r + '.miniGamesText')), + (self.TabID.CHARACTERS, + ba.Lstr(resource=self._r + '.charactersText')), + (self.TabID.ICONS, ba.Lstr(resource=self._r + '.iconsText')), + ] + + self._tab_row = TabRow(self._root_widget, + tabs_def, + pos=(tab_buffer_h * 0.5, self._height - 130), + size=(self._width - tab_buffer_h, 50), + on_select_call=self._set_tab) + + self._purchasable_count_widgets: Dict[StoreBrowserWindow.TabID, + Dict[str, Any]] = {} + + # Create our purchasable-items tags and have them update over time. + for tab_id, tab in self._tab_row.tabs.items(): + pos = tab.position + size = tab.size + button = tab.button + rad = 10 + center = (pos[0] + 0.1 * size[0], pos[1] + 0.9 * size[1]) + img = ba.imagewidget(parent=self._root_widget, + position=(center[0] - rad * 1.04, + center[1] - rad * 1.15), + size=(rad * 2.2, rad * 2.2), + texture=ba.gettexture('circleShadow'), + color=(1, 0, 0)) + txt = ba.textwidget(parent=self._root_widget, + position=center, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=1.4 * rad, + scale=0.6, + shadow=1.0, + flatness=1.0) + rad = 20 + sale_img = ba.imagewidget(parent=self._root_widget, + position=(center[0] - rad, + center[1] - rad), + size=(rad * 2, rad * 2), + draw_controller=button, + texture=ba.gettexture('circleZigZag'), + color=(0.5, 0, 1.0)) + sale_title_text = ba.textwidget(parent=self._root_widget, + position=(center[0], + center[1] + 0.24 * rad), + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=button, + maxwidth=1.4 * rad, + scale=0.6, + shadow=0.0, + flatness=1.0, + color=(0, 1, 0)) + sale_time_text = ba.textwidget(parent=self._root_widget, + position=(center[0], + center[1] - 0.29 * rad), + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=button, + maxwidth=1.4 * rad, + scale=0.4, + shadow=0.0, + flatness=1.0, + color=(0, 1, 0)) + self._purchasable_count_widgets[tab_id] = { + 'img': img, + 'text': txt, + 'sale_img': sale_img, + 'sale_title_text': sale_title_text, + 'sale_time_text': sale_time_text + } + self._tab_update_timer = ba.Timer(1.0, + ba.WeakCall(self._update_tabs), + timetype=ba.TimeType.REAL, + repeat=True) + self._update_tabs() + + if self._get_tickets_button: + last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button + ba.widget(edit=self._get_tickets_button, + down_widget=last_tab_button) + ba.widget(edit=last_tab_button, + up_widget=self._get_tickets_button, + right_widget=self._get_tickets_button) + + self._scroll_width = self._width - scroll_buffer_h + self._scroll_height = self._height - 180 + + self._scrollwidget: Optional[ba.Widget] = None + self._status_textwidget: Optional[ba.Widget] = None + self._restore_state() + + def _update_get_tickets_button_pos(self) -> None: + uiscale = ba.app.ui.uiscale + pos = (self._width - 252 - (self._x_inset + + (47 if uiscale is ba.UIScale.SMALL + and _ba.is_party_icon_visible() else 0)), + self._height - 70) + if self._get_tickets_button: + ba.buttonwidget(edit=self._get_tickets_button, position=pos) + if self._ticket_count_text: + ba.textwidget(edit=self._ticket_count_text, position=pos) + + def _restore_purchases(self) -> None: + from bastd.ui import account + if _ba.get_account_state() != 'signed_in': + account.show_sign_in_prompt() + else: + _ba.restore_purchases() + + def _update_tabs(self) -> None: + from ba.internal import (get_available_sale_time, + get_available_purchase_count) + if not self._root_widget: + return + for tab_id, tab_data in list(self._purchasable_count_widgets.items()): + sale_time = get_available_sale_time(tab_id.value) + + if sale_time is not None: + ba.textwidget(edit=tab_data['sale_title_text'], + text=ba.Lstr(resource='store.saleText')) + ba.textwidget(edit=tab_data['sale_time_text'], + text=ba.timestring( + sale_time, + centi=False, + timeformat=ba.TimeFormat.MILLISECONDS)) + ba.imagewidget(edit=tab_data['sale_img'], opacity=1.0) + count = 0 + else: + ba.textwidget(edit=tab_data['sale_title_text'], text='') + ba.textwidget(edit=tab_data['sale_time_text'], text='') + ba.imagewidget(edit=tab_data['sale_img'], opacity=0.0) + count = get_available_purchase_count(tab_id.value) + + if count > 0: + ba.textwidget(edit=tab_data['text'], text=str(count)) + ba.imagewidget(edit=tab_data['img'], opacity=1.0) + else: + ba.textwidget(edit=tab_data['text'], text='') + ba.imagewidget(edit=tab_data['img'], opacity=0.0) + + def _update_tickets_text(self) -> None: + from ba import SpecialChar + if not self._root_widget: + return + sval: Union[str, ba.Lstr] + if _ba.get_account_state() == 'signed_in': + sval = ba.charstr(SpecialChar.TICKET) + str( + _ba.get_account_ticket_count()) + else: + sval = ba.Lstr(resource='getTicketsWindow.titleText') + if self._get_tickets_button: + ba.buttonwidget(edit=self._get_tickets_button, label=sval) + if self._ticket_count_text: + ba.textwidget(edit=self._ticket_count_text, text=sval) + + def _set_tab(self, tab_id: TabID) -> None: + if self._current_tab is tab_id: + return + self._current_tab = tab_id + + # We wanna preserve our current tab between runs. + cfg = ba.app.config + cfg['Store Tab'] = tab_id.value + cfg.commit() + + # Update tab colors based on which is selected. + self._tab_row.update_appearance(tab_id) + + # (Re)create scroll widget. + if self._scrollwidget: + self._scrollwidget.delete() + + self._scrollwidget = ba.scrollwidget( + parent=self._root_widget, + highlight=False, + position=((self._width - self._scroll_width) * 0.5, + self._height - self._scroll_height - 79 - 48), + size=(self._scroll_width, self._scroll_height), + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + + # NOTE: this stuff is modified by the _Store class. + # Should maybe clean that up. + self.button_infos = {} + self.update_buttons_timer = None + + # Show status over top. + if self._status_textwidget: + self._status_textwidget.delete() + self._status_textwidget = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + color=(1, 0.7, 1, 0.5), + h_align='center', + v_align='center', + text=ba.Lstr(resource=self._r + '.loadingText'), + maxwidth=self._scroll_width * 0.9) + + class _Request: + + def __init__(self, window: StoreBrowserWindow): + self._window = weakref.ref(window) + data = {'tab': tab_id.value} + ba.timer(0.1, + ba.WeakCall(self._on_response, data), + timetype=ba.TimeType.REAL) + + def _on_response(self, data: Optional[Dict[str, Any]]) -> None: + # FIXME: clean this up. + # pylint: disable=protected-access + window = self._window() + if window is not None and (window._request is self): + window._request = None + # noinspection PyProtectedMember + window._on_response(data) + + # Kick off a server request. + self._request = _Request(self) + + # Actually start the purchase locally. + def _purchase_check_result(self, item: str, is_ticket_purchase: bool, + result: Optional[Dict[str, Any]]) -> None: + if result is None: + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource='internal.unavailableNoConnectionText'), + color=(1, 0, 0)) + else: + if is_ticket_purchase: + if result['allow']: + price = _ba.get_account_misc_read_val( + 'price.' + item, None) + if (price is None or not isinstance(price, int) + or price <= 0): + print('Error; got invalid local price of', price, + 'for item', item) + ba.playsound(ba.getsound('error')) + else: + ba.playsound(ba.getsound('click01')) + _ba.in_game_purchase(item, price) + else: + if result['reason'] == 'versionTooOld': + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr( + resource='getTicketsWindow.versionTooOldText'), + color=(1, 0, 0)) + else: + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr( + resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + # Real in-app purchase. + else: + if result['allow']: + _ba.purchase(item) + else: + if result['reason'] == 'versionTooOld': + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr( + resource='getTicketsWindow.versionTooOldText'), + color=(1, 0, 0)) + else: + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr( + resource='getTicketsWindow.unavailableText'), + color=(1, 0, 0)) + + def _do_purchase_check(self, + item: str, + is_ticket_purchase: bool = False) -> None: + from ba.internal import master_server_get + + # Here we ping the server to ask if it's valid for us to + # purchase this. Better to fail now than after we've + # paid locally. + app = ba.app + master_server_get( + 'bsAccountPurchaseCheck', + { + 'item': item, + 'platform': app.platform, + 'subplatform': app.subplatform, + 'version': app.version, + 'buildNumber': app.build_number, + 'purchaseType': 'ticket' if is_ticket_purchase else 'real' + }, + callback=ba.WeakCall(self._purchase_check_result, item, + is_ticket_purchase), + ) + + def buy(self, item: str) -> None: + """Attempt to purchase the provided item.""" + from ba.internal import (get_available_sale_time, + get_store_item_name_translated) + from bastd.ui import account + from bastd.ui.confirm import ConfirmWindow + from bastd.ui import getcurrency + + # Prevent pressing buy within a few seconds of the last press + # (gives the buttons time to disable themselves and whatnot). + curtime = ba.time(ba.TimeType.REAL) + if self._last_buy_time is not None and (curtime - + self._last_buy_time) < 2.0: + ba.playsound(ba.getsound('error')) + else: + if _ba.get_account_state() != 'signed_in': + account.show_sign_in_prompt() + else: + self._last_buy_time = curtime + + # Pro is an actual IAP; the rest are ticket purchases. + if item == 'pro': + ba.playsound(ba.getsound('click01')) + + # Purchase either pro or pro_sale depending on whether + # there is a sale going on. + self._do_purchase_check('pro' if get_available_sale_time( + 'extras') is None else 'pro_sale') + else: + price = _ba.get_account_misc_read_val( + 'price.' + item, None) + our_tickets = _ba.get_account_ticket_count() + if price is not None and our_tickets < price: + ba.playsound(ba.getsound('error')) + getcurrency.show_get_tickets_prompt() + else: + + def do_it() -> None: + self._do_purchase_check(item, + is_ticket_purchase=True) + + ba.playsound(ba.getsound('swish')) + ConfirmWindow( + ba.Lstr(resource='store.purchaseConfirmText', + subs=[ + ('${ITEM}', + get_store_item_name_translated(item)) + ]), + width=400, + height=120, + action=do_it, + ok_text=ba.Lstr(resource='store.purchaseText', + fallback_resource='okText')) + + def _print_already_own(self, charname: str) -> None: + ba.screenmessage(ba.Lstr(resource=self._r + '.alreadyOwnText', + subs=[('${NAME}', charname)]), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + + def update_buttons(self) -> None: + """Update our buttons.""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + from ba.internal import get_available_sale_time + from ba import SpecialChar + if not self._root_widget: + return + import datetime + sales_raw = _ba.get_account_misc_read_val('sales', {}) + sales = {} + try: + # Look at the current set of sales; filter any with time remaining. + for sale_item, sale_info in list(sales_raw.items()): + to_end = (datetime.datetime.utcfromtimestamp(sale_info['e']) - + datetime.datetime.utcnow()).total_seconds() + if to_end > 0: + sales[sale_item] = { + 'to_end': to_end, + 'original_price': sale_info['op'] + } + except Exception: + ba.print_exception('Error parsing sales.') + + assert self.button_infos is not None + for b_type, b_info in self.button_infos.items(): + + if b_type in ['upgrades.pro', 'pro']: + purchased = _ba.app.accounts.have_pro() + else: + purchased = _ba.get_purchased(b_type) + + sale_opacity = 0.0 + sale_title_text: Union[str, ba.Lstr] = '' + sale_time_text: Union[str, ba.Lstr] = '' + + if purchased: + title_color = (0.8, 0.7, 0.9, 1.0) + color = (0.63, 0.55, 0.78) + extra_image_opacity = 0.5 + call = ba.WeakCall(self._print_already_own, b_info['name']) + price_text = '' + price_text_left = '' + price_text_right = '' + show_purchase_check = True + description_color: Sequence[float] = (0.4, 1.0, 0.4, 0.4) + description_color2: Sequence[float] = (0.0, 0.0, 0.0, 0.0) + price_color = (0.5, 1, 0.5, 0.3) + else: + title_color = (0.7, 0.9, 0.7, 1.0) + color = (0.4, 0.8, 0.1) + extra_image_opacity = 1.0 + call = b_info['call'] if 'call' in b_info else None + if b_type in ['upgrades.pro', 'pro']: + sale_time = get_available_sale_time('extras') + if sale_time is not None: + priceraw = _ba.get_price('pro') + price_text_left = (priceraw + if priceraw is not None else '?') + priceraw = _ba.get_price('pro_sale') + price_text_right = (priceraw + if priceraw is not None else '?') + sale_opacity = 1.0 + price_text = '' + sale_title_text = ba.Lstr(resource='store.saleText') + sale_time_text = ba.timestring( + sale_time, + centi=False, + timeformat=ba.TimeFormat.MILLISECONDS) + else: + priceraw = _ba.get_price('pro') + price_text = priceraw if priceraw is not None else '?' + price_text_left = '' + price_text_right = '' + else: + price = _ba.get_account_misc_read_val('price.' + b_type, 0) + + # Color the button differently if we cant afford this. + if _ba.get_account_state() == 'signed_in': + if _ba.get_account_ticket_count() < price: + color = (0.6, 0.61, 0.6) + price_text = ba.charstr(ba.SpecialChar.TICKET) + str( + _ba.get_account_misc_read_val('price.' + b_type, '?')) + price_text_left = '' + price_text_right = '' + + # TESTING: + if b_type in sales: + sale_opacity = 1.0 + price_text_left = ba.charstr(SpecialChar.TICKET) + str( + sales[b_type]['original_price']) + price_text_right = price_text + price_text = '' + sale_title_text = ba.Lstr(resource='store.saleText') + sale_time_text = ba.timestring( + int(sales[b_type]['to_end'] * 1000), + centi=False, + timeformat=ba.TimeFormat.MILLISECONDS) + + description_color = (0.5, 1.0, 0.5) + description_color2 = (0.3, 1.0, 1.0) + price_color = (0.2, 1, 0.2, 1.0) + show_purchase_check = False + + if 'title_text' in b_info: + ba.textwidget(edit=b_info['title_text'], color=title_color) + if 'purchase_check' in b_info: + ba.imagewidget(edit=b_info['purchase_check'], + opacity=1.0 if show_purchase_check else 0.0) + if 'price_widget' in b_info: + ba.textwidget(edit=b_info['price_widget'], + text=price_text, + color=price_color) + if 'price_widget_left' in b_info: + ba.textwidget(edit=b_info['price_widget_left'], + text=price_text_left) + if 'price_widget_right' in b_info: + ba.textwidget(edit=b_info['price_widget_right'], + text=price_text_right) + if 'price_slash_widget' in b_info: + ba.imagewidget(edit=b_info['price_slash_widget'], + opacity=sale_opacity) + if 'sale_bg_widget' in b_info: + ba.imagewidget(edit=b_info['sale_bg_widget'], + opacity=sale_opacity) + if 'sale_title_widget' in b_info: + ba.textwidget(edit=b_info['sale_title_widget'], + text=sale_title_text) + if 'sale_time_widget' in b_info: + ba.textwidget(edit=b_info['sale_time_widget'], + text=sale_time_text) + if 'button' in b_info: + ba.buttonwidget(edit=b_info['button'], + color=color, + on_activate_call=call) + if 'extra_backings' in b_info: + for bck in b_info['extra_backings']: + ba.imagewidget(edit=bck, + color=color, + opacity=extra_image_opacity) + if 'extra_images' in b_info: + for img in b_info['extra_images']: + ba.imagewidget(edit=img, opacity=extra_image_opacity) + if 'extra_texts' in b_info: + for etxt in b_info['extra_texts']: + ba.textwidget(edit=etxt, color=description_color) + if 'extra_texts_2' in b_info: + for etxt in b_info['extra_texts_2']: + ba.textwidget(edit=etxt, color=description_color2) + if 'descriptionText' in b_info: + ba.textwidget(edit=b_info['descriptionText'], + color=description_color) + + def _on_response(self, data: Optional[Dict[str, Any]]) -> None: + # pylint: disable=too-many-statements + + # clear status text.. + if self._status_textwidget: + self._status_textwidget.delete() + self._status_textwidget_update_timer = None + + if data is None: + self._status_textwidget = ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.5), + size=(0, 0), + scale=1.3, + transition_delay=0.1, + color=(1, 0.3, 0.3, 1.0), + h_align='center', + v_align='center', + text=ba.Lstr(resource=self._r + '.loadErrorText'), + maxwidth=self._scroll_width * 0.9) + else: + + class _Store: + + def __init__(self, store_window: StoreBrowserWindow, + sdata: Dict[str, Any], width: float): + from ba.internal import (get_store_item_display_size, + get_store_layout) + self._store_window = store_window + self._width = width + store_data = get_store_layout() + self._tab = sdata['tab'] + self._sections = copy.deepcopy(store_data[sdata['tab']]) + self._height: Optional[float] = None + + uiscale = ba.app.ui.uiscale + + # Pre-calc a few things and add them to store-data. + for section in self._sections: + if self._tab == 'characters': + dummy_name = 'characters.foo' + elif self._tab == 'extras': + dummy_name = 'pro' + elif self._tab == 'maps': + dummy_name = 'maps.foo' + elif self._tab == 'icons': + dummy_name = 'icons.foo' + else: + dummy_name = '' + section['button_size'] = get_store_item_display_size( + dummy_name) + section['v_spacing'] = (-17 if self._tab + == 'characters' else 0) + if 'title' not in section: + section['title'] = '' + section['x_offs'] = (130 if self._tab == 'extras' else + 270 if self._tab == 'maps' else 0) + section['y_offs'] = ( + 55 if (self._tab == 'extras' + and uiscale is ba.UIScale.SMALL) else + -20 if self._tab == 'icons' else 0) + + def instantiate(self, scrollwidget: ba.Widget, + tab_button: ba.Widget) -> None: + """Create the store.""" + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + from bastd.ui.store import item as storeitemui + title_spacing = 40 + button_border = 20 + button_spacing = 4 + boffs_h = 40 + self._height = 80.0 + + # Calc total height. + for i, section in enumerate(self._sections): + if section['title'] != '': + assert self._height is not None + self._height += title_spacing + b_width, b_height = section['button_size'] + b_column_count = int( + math.floor((self._width - boffs_h - 20) / + (b_width + button_spacing))) + b_row_count = int( + math.ceil( + float(len(section['items'])) / b_column_count)) + b_height_total = ( + 2 * button_border + b_row_count * b_height + + (b_row_count - 1) * section['v_spacing']) + self._height += b_height_total + + assert self._height is not None + cnt2 = ba.containerwidget(parent=scrollwidget, + scale=1.0, + size=(self._width, self._height), + background=False, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + v = self._height - 20 + + if self._tab == 'characters': + txt = ba.Lstr( + resource='store.howToSwitchCharactersText', + subs=[ + ('${SETTINGS}', + ba.Lstr( + resource='accountSettingsWindow.titleText' + )), + ('${PLAYER_PROFILES}', + ba.Lstr( + resource='playerProfilesWindow.titleText') + ) + ]) + ba.textwidget(parent=cnt2, + text=txt, + size=(0, 0), + position=(self._width * 0.5, + self._height - 28), + h_align='center', + v_align='center', + color=(0.7, 1, 0.7, 0.4), + scale=0.7, + shadow=0, + flatness=1.0, + maxwidth=700, + transition_delay=0.4) + elif self._tab == 'icons': + txt = ba.Lstr( + resource='store.howToUseIconsText', + subs=[ + ('${SETTINGS}', + ba.Lstr(resource='mainMenu.settingsText')), + ('${PLAYER_PROFILES}', + ba.Lstr( + resource='playerProfilesWindow.titleText') + ) + ]) + ba.textwidget(parent=cnt2, + text=txt, + size=(0, 0), + position=(self._width * 0.5, + self._height - 28), + h_align='center', + v_align='center', + color=(0.7, 1, 0.7, 0.4), + scale=0.7, + shadow=0, + flatness=1.0, + maxwidth=700, + transition_delay=0.4) + elif self._tab == 'maps': + assert self._width is not None + assert self._height is not None + txt = ba.Lstr(resource='store.howToUseMapsText') + ba.textwidget(parent=cnt2, + text=txt, + size=(0, 0), + position=(self._width * 0.5, + self._height - 28), + h_align='center', + v_align='center', + color=(0.7, 1, 0.7, 0.4), + scale=0.7, + shadow=0, + flatness=1.0, + maxwidth=700, + transition_delay=0.4) + + prev_row_buttons: Optional[List] = None + this_row_buttons = [] + + delay = 0.3 + for section in self._sections: + if section['title'] != '': + ba.textwidget( + parent=cnt2, + position=(60, v - title_spacing * 0.8), + size=(0, 0), + scale=1.0, + transition_delay=delay, + color=(0.7, 0.9, 0.7, 1), + h_align='left', + v_align='center', + text=ba.Lstr(resource=section['title']), + maxwidth=self._width * 0.7) + v -= title_spacing + delay = max(0.100, delay - 0.100) + v -= button_border + b_width, b_height = section['button_size'] + b_count = len(section['items']) + b_column_count = int( + math.floor((self._width - boffs_h - 20) / + (b_width + button_spacing))) + col = 0 + item: Dict[str, Any] + assert self._store_window.button_infos is not None + for i, item_name in enumerate(section['items']): + item = self._store_window.button_infos[ + item_name] = {} + item['call'] = ba.WeakCall(self._store_window.buy, + item_name) + if 'x_offs' in section: + boffs_h2 = section['x_offs'] + else: + boffs_h2 = 0 + + if 'y_offs' in section: + boffs_v2 = section['y_offs'] + else: + boffs_v2 = 0 + b_pos = (boffs_h + boffs_h2 + + (b_width + button_spacing) * col, + v - b_height + boffs_v2) + storeitemui.instantiate_store_item_display( + item_name, + item, + parent_widget=cnt2, + b_pos=b_pos, + boffs_h=boffs_h, + b_width=b_width, + b_height=b_height, + boffs_h2=boffs_h2, + boffs_v2=boffs_v2, + delay=delay) + btn = item['button'] + delay = max(0.1, delay - 0.1) + this_row_buttons.append(btn) + + # Wire this button to the equivalent in the + # previous row. + if prev_row_buttons is not None: + # pylint: disable=unsubscriptable-object + if len(prev_row_buttons) > col: + ba.widget(edit=btn, + up_widget=prev_row_buttons[col]) + ba.widget(edit=prev_row_buttons[col], + down_widget=btn) + + # If we're the last button in our row, + # wire any in the previous row past + # our position to go to us if down is + # pressed. + if (col + 1 == b_column_count + or i == b_count - 1): + for b_prev in prev_row_buttons[col + + 1:]: + ba.widget(edit=b_prev, + down_widget=btn) + else: + ba.widget(edit=btn, + up_widget=prev_row_buttons[-1]) + else: + ba.widget(edit=btn, up_widget=tab_button) + + col += 1 + if col == b_column_count or i == b_count - 1: + prev_row_buttons = this_row_buttons + this_row_buttons = [] + col = 0 + v -= b_height + if i < b_count - 1: + v -= section['v_spacing'] + + v -= button_border + + # Set a timer to update these buttons periodically as long + # as we're alive (so if we buy one it will grey out, etc). + self._store_window.update_buttons_timer = ba.Timer( + 0.5, + ba.WeakCall(self._store_window.update_buttons), + repeat=True, + timetype=ba.TimeType.REAL) + + # Also update them immediately. + self._store_window.update_buttons() + + if self._current_tab in (self.TabID.EXTRAS, self.TabID.MINIGAMES, + self.TabID.CHARACTERS, self.TabID.MAPS, + self.TabID.ICONS): + store = _Store(self, data, self._scroll_width) + assert self._scrollwidget is not None + store.instantiate( + scrollwidget=self._scrollwidget, + tab_button=self._tab_row.tabs[self._current_tab].button) + else: + cnt = ba.containerwidget(parent=self._scrollwidget, + scale=1.0, + size=(self._scroll_width, + self._scroll_height * 0.95), + background=False, + claims_left_right=True, + claims_tab=True, + selection_loops_to_parent=True) + self._status_textwidget = ba.textwidget( + parent=cnt, + position=(self._scroll_width * 0.5, + self._scroll_height * 0.5), + size=(0, 0), + scale=1.3, + transition_delay=0.1, + color=(1, 1, 0.3, 1.0), + h_align='center', + v_align='center', + text=ba.Lstr(resource=self._r + '.comingSoonText'), + maxwidth=self._scroll_width * 0.9) + + def _save_state(self) -> None: + try: + sel = self._root_widget.get_selected_child() + selected_tab_ids = [ + tab_id for tab_id, tab in self._tab_row.tabs.items() + if sel == tab.button + ] + if sel == self._get_tickets_button: + sel_name = 'GetTickets' + elif sel == self._scrollwidget: + sel_name = 'Scroll' + elif sel == self._back_button: + sel_name = 'Back' + elif selected_tab_ids: + assert len(selected_tab_ids) == 1 + sel_name = f'Tab:{selected_tab_ids[0].value}' + else: + raise ValueError(f'unrecognized selection \'{sel}\'') + ba.app.ui.window_states[type(self)] = { + 'sel_name': sel_name, + } + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + def _restore_state(self) -> None: + from efro.util import enum_by_value + try: + sel: Optional[ba.Widget] + sel_name = ba.app.ui.window_states.get(type(self), + {}).get('sel_name') + assert isinstance(sel_name, (str, type(None))) + + try: + current_tab = enum_by_value(self.TabID, + ba.app.config.get('Store Tab')) + except ValueError: + current_tab = self.TabID.CHARACTERS + + if self._show_tab is not None: + current_tab = self._show_tab + if sel_name == 'GetTickets' and self._get_tickets_button: + sel = self._get_tickets_button + elif sel_name == 'Back': + sel = self._back_button + elif sel_name == 'Scroll': + sel = self._scrollwidget + elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): + try: + sel_tab_id = enum_by_value(self.TabID, + sel_name.split(':')[-1]) + except ValueError: + sel_tab_id = self.TabID.CHARACTERS + sel = self._tab_row.tabs[sel_tab_id].button + else: + sel = self._tab_row.tabs[current_tab].button + + # If we were requested to show a tab, select it too.. + if (self._show_tab is not None + and self._show_tab in self._tab_row.tabs): + sel = self._tab_row.tabs[self._show_tab].button + self._set_tab(current_tab) + if sel is not None: + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') + + def _on_get_more_tickets_press(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.getcurrency import GetCurrencyWindow + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + self._save_state() + ba.containerwidget(edit=self._root_widget, transition='out_left') + window = GetCurrencyWindow( + from_modal_store=self._modal, + store_back_location=self._back_location).get_root_widget() + if not self._modal: + ba.app.ui.set_main_menu_window(window) + + def _back(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.coop.browser import CoopBrowserWindow + from bastd.ui.mainmenu import MainMenuWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + if not self._modal: + if self._back_location == 'CoopBrowserWindow': + ba.app.ui.set_main_menu_window( + CoopBrowserWindow(transition='in_left').get_root_widget()) + else: + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition='in_left').get_root_widget()) + if self._on_close_call is not None: + self._on_close_call() diff --git a/dist/ba_data/python/bastd/ui/store/button.py b/dist/ba_data/python/bastd/ui/store/button.py new file mode 100644 index 0000000..5aec050 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/store/button.py @@ -0,0 +1,274 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for a button leading to the store.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Sequence, Callable, Optional + + +class StoreButton: + """A button leading to the store.""" + + def __init__(self, + parent: ba.Widget, + position: Sequence[float], + size: Sequence[float], + scale: float, + on_activate_call: Callable[[], Any] = None, + transition_delay: float = None, + color: Sequence[float] = None, + textcolor: Sequence[float] = None, + show_tickets: bool = False, + button_type: str = None, + sale_scale: float = 1.0): + self._position = position + self._size = size + self._scale = scale + + if on_activate_call is None: + on_activate_call = ba.WeakCall(self._default_on_activate_call) + self._on_activate_call = on_activate_call + + self._button = ba.buttonwidget( + parent=parent, + size=size, + label='' if show_tickets else ba.Lstr(resource='storeText'), + scale=scale, + autoselect=True, + on_activate_call=self._on_activate, + transition_delay=transition_delay, + color=color, + button_type=button_type) + + self._title_text: Optional[ba.Widget] + self._ticket_text: Optional[ba.Widget] + + if show_tickets: + self._title_text = ba.textwidget( + parent=parent, + position=(position[0] + size[0] * 0.5 * scale, + position[1] + size[1] * 0.65 * scale), + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=size[0] * scale * 0.65, + text=ba.Lstr(resource='storeText'), + draw_controller=self._button, + scale=scale, + transition_delay=transition_delay, + color=textcolor) + self._ticket_text = ba.textwidget( + parent=parent, + size=(0, 0), + h_align='center', + v_align='center', + maxwidth=size[0] * scale * 0.85, + text='', + color=(0.2, 1.0, 0.2), + flatness=1.0, + shadow=0.0, + scale=scale * 0.6, + transition_delay=transition_delay) + else: + self._title_text = None + self._ticket_text = None + + self._circle_rad = 12 * scale + self._circle_center = (0.0, 0.0) + self._sale_circle_center = (0.0, 0.0) + + self._available_purchase_backing = ba.imagewidget( + parent=parent, + color=(1, 0, 0), + draw_controller=self._button, + size=(2.2 * self._circle_rad, 2.2 * self._circle_rad), + texture=ba.gettexture('circleShadow'), + transition_delay=transition_delay) + self._available_purchase_text = ba.textwidget( + parent=parent, + size=(0, 0), + h_align='center', + v_align='center', + text='', + draw_controller=self._button, + color=(1, 1, 1), + flatness=1.0, + shadow=1.0, + scale=0.6 * scale, + maxwidth=self._circle_rad * 1.4, + transition_delay=transition_delay) + + self._sale_circle_rad = 18 * scale * sale_scale + self._sale_backing = ba.imagewidget( + parent=parent, + color=(0.5, 0, 1.0), + draw_controller=self._button, + size=(2 * self._sale_circle_rad, 2 * self._sale_circle_rad), + texture=ba.gettexture('circleZigZag'), + transition_delay=transition_delay) + self._sale_title_text = ba.textwidget( + parent=parent, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=self._button, + color=(0, 1, 0), + flatness=1.0, + shadow=0.0, + scale=0.5 * scale * sale_scale, + maxwidth=self._sale_circle_rad * 1.5, + transition_delay=transition_delay) + self._sale_time_text = ba.textwidget(parent=parent, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=self._button, + color=(0, 1, 0), + flatness=1.0, + shadow=0.0, + scale=0.4 * scale * sale_scale, + maxwidth=self._sale_circle_rad * + 1.5, + transition_delay=transition_delay) + + self.set_position(position) + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + repeat=True, + timetype=ba.TimeType.REAL) + self._update() + + def _on_activate(self) -> None: + _ba.increment_analytics_count('Store button press') + self._on_activate_call() + + def set_position(self, position: Sequence[float]) -> None: + """Set the button position.""" + self._position = position + self._circle_center = (position[0] + 0.1 * self._size[0] * self._scale, + position[1] + self._size[1] * self._scale * 0.8) + self._sale_circle_center = (position[0] + + 0.07 * self._size[0] * self._scale, + position[1] + + self._size[1] * self._scale * 0.8) + + if not self._button: + return + ba.buttonwidget(edit=self._button, position=self._position) + if self._title_text is not None: + ba.textwidget(edit=self._title_text, + position=(self._position[0] + + self._size[0] * 0.5 * self._scale, + self._position[1] + + self._size[1] * 0.65 * self._scale)) + if self._ticket_text is not None: + ba.textwidget( + edit=self._ticket_text, + position=(position[0] + self._size[0] * 0.5 * self._scale, + position[1] + self._size[1] * 0.28 * self._scale), + size=(0, 0)) + ba.imagewidget( + edit=self._available_purchase_backing, + position=(self._circle_center[0] - self._circle_rad * 1.02, + self._circle_center[1] - self._circle_rad * 1.13)) + ba.textwidget(edit=self._available_purchase_text, + position=self._circle_center) + + ba.imagewidget( + edit=self._sale_backing, + position=(self._sale_circle_center[0] - self._sale_circle_rad, + self._sale_circle_center[1] - self._sale_circle_rad)) + ba.textwidget(edit=self._sale_title_text, + position=(self._sale_circle_center[0], + self._sale_circle_center[1] + + self._sale_circle_rad * 0.3)) + ba.textwidget(edit=self._sale_time_text, + position=(self._sale_circle_center[0], + self._sale_circle_center[1] - + self._sale_circle_rad * 0.3)) + + def _default_on_activate_call(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.store.browser import StoreBrowserWindow + if _ba.get_account_state() != 'signed_in': + show_sign_in_prompt() + return + StoreBrowserWindow(modal=True, origin_widget=self._button) + + def get_button(self) -> ba.Widget: + """Return the underlying button widget.""" + return self._button + + def _update(self) -> None: + # pylint: disable=too-many-branches + # pylint: disable=cyclic-import + from ba import SpecialChar, TimeFormat + from ba.internal import (get_available_sale_time, + get_available_purchase_count) + if not self._button: + return # Our instance may outlive our UI objects. + + if self._ticket_text is not None: + if _ba.get_account_state() == 'signed_in': + sval = ba.charstr(SpecialChar.TICKET) + str( + _ba.get_account_ticket_count()) + else: + sval = '-' + ba.textwidget(edit=self._ticket_text, text=sval) + available_purchases = get_available_purchase_count() + + # Old pro sale stuff.. + sale_time = get_available_sale_time('extras') + + # ..also look for new style sales. + if sale_time is None: + import datetime + sales_raw = _ba.get_account_misc_read_val('sales', {}) + sale_times = [] + try: + # Look at the current set of sales; filter any with time + # remaining that we don't own. + for sale_item, sale_info in list(sales_raw.items()): + if not _ba.get_purchased(sale_item): + to_end = (datetime.datetime.utcfromtimestamp( + sale_info['e']) - + datetime.datetime.utcnow()).total_seconds() + if to_end > 0: + sale_times.append(to_end) + except Exception: + ba.print_exception('Error parsing sales.') + if sale_times: + sale_time = int(min(sale_times) * 1000) + + if sale_time is not None: + ba.textwidget(edit=self._sale_title_text, + text=ba.Lstr(resource='store.saleText')) + ba.textwidget(edit=self._sale_time_text, + text=ba.timestring( + sale_time, + centi=False, + timeformat=TimeFormat.MILLISECONDS)) + ba.imagewidget(edit=self._sale_backing, opacity=1.0) + ba.imagewidget(edit=self._available_purchase_backing, opacity=1.0) + ba.textwidget(edit=self._available_purchase_text, text='') + ba.imagewidget(edit=self._available_purchase_backing, opacity=0.0) + else: + ba.imagewidget(edit=self._sale_backing, opacity=0.0) + ba.textwidget(edit=self._sale_time_text, text='') + ba.textwidget(edit=self._sale_title_text, text='') + if available_purchases > 0: + ba.textwidget(edit=self._available_purchase_text, + text=str(available_purchases)) + ba.imagewidget(edit=self._available_purchase_backing, + opacity=1.0) + else: + ba.textwidget(edit=self._available_purchase_text, text='') + ba.imagewidget(edit=self._available_purchase_backing, + opacity=0.0) diff --git a/dist/ba_data/python/bastd/ui/store/item.py b/dist/ba_data/python/bastd/ui/store/item.py new file mode 100644 index 0000000..841ae1f --- /dev/null +++ b/dist/ba_data/python/bastd/ui/store/item.py @@ -0,0 +1,547 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to UI items.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Tuple, Dict, Optional + + +def instantiate_store_item_display(item_name: str, + item: Dict[str, Any], + parent_widget: ba.Widget, + b_pos: Tuple[float, float], + b_width: float, + b_height: float, + boffs_h: float = 0.0, + boffs_h2: float = 0.0, + boffs_v2: float = 0, + delay: float = 0.0, + button: bool = True) -> None: + """(internal)""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + from ba.internal import (get_store_item, get_store_item_name_translated, + get_clean_price) + del boffs_h # unused arg + del boffs_h2 # unused arg + del boffs_v2 # unused arg + item_info = get_store_item(item_name) + title_v = 0.24 + price_v = 0.145 + base_text_scale = 1.0 + + item['name'] = title = get_store_item_name_translated(item_name) + + btn: Optional[ba.Widget] + if button: + item['button'] = btn = ba.buttonwidget(parent=parent_widget, + position=b_pos, + transition_delay=delay, + show_buffer_top=76.0, + enable_sound=False, + button_type='square', + size=(b_width, b_height), + autoselect=True, + label='') + ba.widget(edit=btn, show_buffer_bottom=76.0) + else: + btn = None + + b_offs_x = -0.015 * b_width + check_pos = 0.76 + + icon_tex = None + tint_tex = None + tint_color = None + tint2_color = None + tex_name: Optional[str] = None + desc: Optional[str] = None + modes: Optional[ba.Lstr] = None + + if item_name.startswith('characters.'): + character = ba.app.spaz_appearances[item_info['character']] + tint_color = ( + item_info['color'] if 'color' in item_info else + character.default_color if character.default_color is not None else + (1, 1, 1)) + tint2_color = (item_info['highlight'] if 'highlight' in item_info else + character.default_highlight if + character.default_highlight is not None else (1, 1, 1)) + icon_tex = character.icon_texture + tint_tex = character.icon_mask_texture + title_v = 0.255 + price_v = 0.145 + elif item_name in ['upgrades.pro', 'pro']: + base_text_scale = 0.6 + title_v = 0.85 + price_v = 0.15 + elif item_name.startswith('maps.'): + map_type = item_info['map_type'] + tex_name = map_type.get_preview_texture_name() + title_v = 0.312 + price_v = 0.17 + + elif item_name.startswith('games.'): + gametype = item_info['gametype'] + modes_l = [] + if gametype.supports_session_type(ba.CoopSession): + modes_l.append(ba.Lstr(resource='playModes.coopText')) + if gametype.supports_session_type(ba.DualTeamSession): + modes_l.append(ba.Lstr(resource='playModes.teamsText')) + if gametype.supports_session_type(ba.FreeForAllSession): + modes_l.append(ba.Lstr(resource='playModes.freeForAllText')) + + if len(modes_l) == 3: + modes = ba.Lstr(value='${A}, ${B}, ${C}', + subs=[('${A}', modes_l[0]), ('${B}', modes_l[1]), + ('${C}', modes_l[2])]) + elif len(modes_l) == 2: + modes = ba.Lstr(value='${A}, ${B}', + subs=[('${A}', modes_l[0]), ('${B}', modes_l[1])]) + elif len(modes_l) == 1: + modes = modes_l[0] + else: + raise Exception() + desc = gametype.get_description_display_string(ba.CoopSession) + tex_name = item_info['previewTex'] + base_text_scale = 0.8 + title_v = 0.48 + price_v = 0.17 + + elif item_name.startswith('icons.'): + base_text_scale = 1.5 + price_v = 0.2 + check_pos = 0.6 + + if item_name.startswith('characters.'): + frame_size = b_width * 0.7 + im_dim = frame_size * (100.0 / 113.0) + im_pos = (b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.57 - im_dim * 0.5) + mask_texture = ba.gettexture('characterIconMask') + assert icon_tex is not None + assert tint_tex is not None + ba.imagewidget(parent=parent_widget, + position=im_pos, + size=(im_dim, im_dim), + color=(1, 1, 1), + transition_delay=delay, + mask_texture=mask_texture, + draw_controller=btn, + texture=ba.gettexture(icon_tex), + tint_texture=ba.gettexture(tint_tex), + tint_color=tint_color, + tint2_color=tint2_color) + + if item_name in ['pro', 'upgrades.pro']: + frame_size = b_width * 0.5 + im_dim = frame_size * (100.0 / 113.0) + im_pos = (b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.5 - im_dim * 0.5) + ba.imagewidget(parent=parent_widget, + position=im_pos, + size=(im_dim, im_dim), + transition_delay=delay, + draw_controller=btn, + color=(0.3, 0.0, 0.3), + opacity=0.3, + texture=ba.gettexture('logo')) + txt = ba.Lstr(resource='store.bombSquadProNewDescriptionText') + + # t = 'foo\nfoo\nfoo\nfoo\nfoo\nfoo' + item['descriptionText'] = ba.textwidget( + parent=parent_widget, + text=txt, + position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.69), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * 0.75, + maxwidth=b_width * 0.75, + max_height=b_height * 0.2, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.3, 1, 0.3)) + + extra_backings = item['extra_backings'] = [] + extra_images = item['extra_images'] = [] + extra_texts = item['extra_texts'] = [] + extra_texts_2 = item['extra_texts_2'] = [] + + backing_color = (0.5, 0.8, 0.3) if button else (0.6, 0.5, 0.65) + b_square_texture = ba.gettexture('buttonSquare') + char_mask_texture = ba.gettexture('characterIconMask') + + pos = (0.17, 0.43) + tile_size = (b_width * 0.16 * 1.2, b_width * 0.2 * 1.2) + tile_pos = (b_pos[0] + b_width * pos[0], b_pos[1] + b_height * pos[1]) + extra_backings.append( + ba.imagewidget(parent=parent_widget, + position=(tile_pos[0] - tile_size[0] * 0.5, + tile_pos[1] - tile_size[1] * 0.5), + size=tile_size, + transition_delay=delay, + draw_controller=btn, + color=backing_color, + texture=b_square_texture)) + im_size = tile_size[0] * 0.8 + extra_images.append( + ba.imagewidget(parent=parent_widget, + position=(tile_pos[0] - im_size * 0.5, + tile_pos[1] - im_size * 0.4), + size=(im_size, im_size), + transition_delay=delay, + draw_controller=btn, + color=(1, 1, 1), + texture=ba.gettexture('ticketsMore'))) + bonus_tickets = str( + _ba.get_account_misc_read_val('proBonusTickets', 100)) + extra_texts.append( + ba.textwidget(parent=parent_widget, + draw_controller=btn, + position=(tile_pos[0] - tile_size[0] * 0.03, + tile_pos[1] - tile_size[1] * 0.25), + size=(0, 0), + color=(0.6, 1, 0.6), + transition_delay=delay, + h_align='center', + v_align='center', + maxwidth=tile_size[0] * 0.7, + scale=0.55, + text=ba.Lstr(resource='getTicketsWindow.ticketsText', + subs=[('${COUNT}', bonus_tickets)]), + flatness=1.0, + shadow=0.0)) + + for charname, pos in [('Kronk', (0.32, 0.45)), ('Zoe', (0.425, 0.4)), + ('Jack Morgan', (0.555, 0.45)), + ('Mel', (0.645, 0.4))]: + tile_size = (b_width * 0.16 * 0.9, b_width * 0.2 * 0.9) + tile_pos = (b_pos[0] + b_width * pos[0], + b_pos[1] + b_height * pos[1]) + character = ba.app.spaz_appearances[charname] + extra_backings.append( + ba.imagewidget(parent=parent_widget, + position=(tile_pos[0] - tile_size[0] * 0.5, + tile_pos[1] - tile_size[1] * 0.5), + size=tile_size, + transition_delay=delay, + draw_controller=btn, + color=backing_color, + texture=b_square_texture)) + im_size = tile_size[0] * 0.7 + extra_images.append( + ba.imagewidget(parent=parent_widget, + position=(tile_pos[0] - im_size * 0.53, + tile_pos[1] - im_size * 0.35), + size=(im_size, im_size), + transition_delay=delay, + draw_controller=btn, + color=(1, 1, 1), + texture=ba.gettexture(character.icon_texture), + tint_texture=ba.gettexture( + character.icon_mask_texture), + tint_color=character.default_color, + tint2_color=character.default_highlight, + mask_texture=char_mask_texture)) + extra_texts.append( + ba.textwidget(parent=parent_widget, + draw_controller=btn, + position=(tile_pos[0] - im_size * 0.03, + tile_pos[1] - im_size * 0.51), + size=(0, 0), + color=(0.6, 1, 0.6), + transition_delay=delay, + h_align='center', + v_align='center', + maxwidth=tile_size[0] * 0.7, + scale=0.55, + text=ba.Lstr(translate=('characterNames', + charname)), + flatness=1.0, + shadow=0.0)) + + # If we have a 'total-worth' item-id for this id, show that price so + # the user knows how much this is worth. + total_worth_item = _ba.get_account_misc_read_val('twrths', + {}).get(item_name) + total_worth_price: Optional[str] + if total_worth_item is not None: + price = _ba.get_price(total_worth_item) + total_worth_price = (get_clean_price(price) + if price is not None else '??') + else: + total_worth_price = None + + if total_worth_price is not None: + total_worth_text = ba.Lstr(resource='store.totalWorthText', + subs=[('${TOTAL_WORTH}', + total_worth_price)]) + extra_texts_2.append( + ba.textwidget(parent=parent_widget, + text=total_worth_text, + position=(b_pos[0] + b_width * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.25), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * + 0.45, + maxwidth=b_width * 0.5, + size=(0, 0), + h_align='center', + v_align='center', + shadow=1.0, + flatness=1.0, + draw_controller=btn, + color=(0.3, 1, 1))) + + model_opaque = ba.getmodel('level_select_button_opaque') + model_transparent = ba.getmodel('level_select_button_transparent') + mask_tex = ba.gettexture('mapPreviewMask') + for levelname, preview_tex_name, pos in [ + ('Infinite Onslaught', 'doomShroomPreview', (0.80, 0.48)), + ('Infinite Runaround', 'towerDPreview', (0.80, 0.32)) + ]: + tile_size = (b_width * 0.2, b_width * 0.13) + tile_pos = (b_pos[0] + b_width * pos[0], + b_pos[1] + b_height * pos[1]) + im_size = tile_size[0] * 0.8 + extra_backings.append( + ba.imagewidget(parent=parent_widget, + position=(tile_pos[0] - tile_size[0] * 0.5, + tile_pos[1] - tile_size[1] * 0.5), + size=tile_size, + transition_delay=delay, + draw_controller=btn, + color=backing_color, + texture=b_square_texture)) + + # Hack - gotta draw two transparent versions to avoid z issues. + for mod in model_opaque, model_transparent: + extra_images.append( + ba.imagewidget(parent=parent_widget, + position=(tile_pos[0] - im_size * 0.52, + tile_pos[1] - im_size * 0.2), + size=(im_size, im_size * 0.5), + transition_delay=delay, + model_transparent=mod, + mask_texture=mask_tex, + draw_controller=btn, + texture=ba.gettexture(preview_tex_name))) + + extra_texts.append( + ba.textwidget(parent=parent_widget, + draw_controller=btn, + position=(tile_pos[0] - im_size * 0.03, + tile_pos[1] - im_size * 0.2), + size=(0, 0), + color=(0.6, 1, 0.6), + transition_delay=delay, + h_align='center', + v_align='center', + maxwidth=tile_size[0] * 0.7, + scale=0.55, + text=ba.Lstr(translate=('coopLevelNames', + levelname)), + flatness=1.0, + shadow=0.0)) + + if item_name.startswith('icons.'): + item['icon_text'] = ba.textwidget( + parent=parent_widget, + text=item_info['icon'], + position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.5), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * 2.0, + maxwidth=b_width * 0.9, + max_height=b_height * 0.9, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn) + + if item_name.startswith('maps.'): + frame_size = b_width * 0.9 + im_dim = frame_size * (100.0 / 113.0) + im_pos = (b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.62 - im_dim * 0.25) + model_opaque = ba.getmodel('level_select_button_opaque') + model_transparent = ba.getmodel('level_select_button_transparent') + mask_tex = ba.gettexture('mapPreviewMask') + assert tex_name is not None + ba.imagewidget(parent=parent_widget, + position=im_pos, + size=(im_dim, im_dim * 0.5), + transition_delay=delay, + model_opaque=model_opaque, + model_transparent=model_transparent, + mask_texture=mask_tex, + draw_controller=btn, + texture=ba.gettexture(tex_name)) + + if item_name.startswith('games.'): + frame_size = b_width * 0.8 + im_dim = frame_size * (100.0 / 113.0) + im_pos = (b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, + b_pos[1] + b_height * 0.72 - im_dim * 0.25) + model_opaque = ba.getmodel('level_select_button_opaque') + model_transparent = ba.getmodel('level_select_button_transparent') + mask_tex = ba.gettexture('mapPreviewMask') + assert tex_name is not None + ba.imagewidget(parent=parent_widget, + position=im_pos, + size=(im_dim, im_dim * 0.5), + transition_delay=delay, + model_opaque=model_opaque, + model_transparent=model_transparent, + mask_texture=mask_tex, + draw_controller=btn, + texture=ba.gettexture(tex_name)) + item['descriptionText'] = ba.textwidget( + parent=parent_widget, + text=desc, + position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.36), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * 0.78, + maxwidth=b_width * 0.8, + max_height=b_height * 0.14, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + flatness=1.0, + shadow=0.0, + color=(0.6, 1, 0.6)) + item['gameModesText'] = ba.textwidget( + parent=parent_widget, + text=modes, + position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.26), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale * 0.65, + maxwidth=b_width * 0.8, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + shadow=0, + flatness=1.0, + color=(0.6, 0.8, 0.6)) + + if not item_name.startswith('icons.'): + item['title_text'] = ba.textwidget( + parent=parent_widget, + text=title, + position=(b_pos[0] + b_width * 0.5 + b_offs_x, + b_pos[1] + b_height * title_v), + transition_delay=delay, + scale=b_width * (1.0 / 230.0) * base_text_scale, + maxwidth=b_width * 0.8, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.7, 0.9, 0.7, 1.0)) + + item['purchase_check'] = ba.imagewidget( + parent=parent_widget, + position=(b_pos[0] + b_width * check_pos, b_pos[1] + b_height * 0.05), + transition_delay=delay, + model_transparent=ba.getmodel('checkTransparent'), + opacity=0.0, + size=(60, 60), + color=(0.6, 0.5, 0.8), + draw_controller=btn, + texture=ba.gettexture('uiAtlas')) + item['price_widget'] = ba.textwidget( + parent=parent_widget, + text='', + position=(b_pos[0] + b_width * 0.5 + b_offs_x, + b_pos[1] + b_height * price_v), + transition_delay=delay, + scale=b_width * (1.0 / 300.0) * base_text_scale, + maxwidth=b_width * 0.9, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.2, 1, 0.2, 1.0)) + item['price_widget_left'] = ba.textwidget( + parent=parent_widget, + text='', + position=(b_pos[0] + b_width * 0.33 + b_offs_x, + b_pos[1] + b_height * price_v), + transition_delay=delay, + scale=b_width * (1.0 / 300.0) * base_text_scale, + maxwidth=b_width * 0.3, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.2, 1, 0.2, 0.5)) + item['price_widget_right'] = ba.textwidget( + parent=parent_widget, + text='', + position=(b_pos[0] + b_width * 0.66 + b_offs_x, + b_pos[1] + b_height * price_v), + transition_delay=delay, + scale=1.1 * b_width * (1.0 / 300.0) * base_text_scale, + maxwidth=b_width * 0.3, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + color=(0.2, 1, 0.2, 1.0)) + item['price_slash_widget'] = ba.imagewidget( + parent=parent_widget, + position=(b_pos[0] + b_width * 0.33 + b_offs_x - 36, + b_pos[1] + b_height * price_v - 35), + transition_delay=delay, + texture=ba.gettexture('slash'), + opacity=0.0, + size=(70, 70), + draw_controller=btn, + color=(1, 0, 0)) + badge_rad = 44 + badge_center = (b_pos[0] + b_width * 0.1 + b_offs_x, + b_pos[1] + b_height * 0.87) + item['sale_bg_widget'] = ba.imagewidget( + parent=parent_widget, + position=(badge_center[0] - badge_rad, badge_center[1] - badge_rad), + opacity=0.0, + transition_delay=delay, + texture=ba.gettexture('circleZigZag'), + draw_controller=btn, + size=(badge_rad * 2, badge_rad * 2), + color=(0.5, 0, 1)) + item['sale_title_widget'] = ba.textwidget(parent=parent_widget, + position=(badge_center[0], + badge_center[1] + 12), + transition_delay=delay, + scale=1.0, + maxwidth=badge_rad * 1.6, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + shadow=0.0, + flatness=1.0, + color=(0, 1, 0)) + item['sale_time_widget'] = ba.textwidget(parent=parent_widget, + position=(badge_center[0], + badge_center[1] - 12), + transition_delay=delay, + scale=0.7, + maxwidth=badge_rad * 1.6, + size=(0, 0), + h_align='center', + v_align='center', + draw_controller=btn, + shadow=0.0, + flatness=1.0, + color=(0.0, 1, 0.0, 1)) diff --git a/dist/ba_data/python/bastd/ui/tabs.py b/dist/ba_data/python/bastd/ui/tabs.py new file mode 100644 index 0000000..62e8a2f --- /dev/null +++ b/dist/ba_data/python/bastd/ui/tabs.py @@ -0,0 +1,77 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for creating tab style buttons.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypeVar, Generic + +import ba + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Tuple, List, Sequence, Optional + + +@dataclass +class Tab: + """Info for an individual tab in a TabRow""" + button: ba.Widget + position: Tuple[float, float] + size: Tuple[float, float] + + +T = TypeVar('T') + + +class TabRow(Generic[T]): + """Encapsulates a row of tab-styled buttons. + + Tabs are indexed by id which is an arbitrary user-provided type. + """ + + def __init__(self, + parent: ba.Widget, + tabdefs: List[Tuple[T, ba.Lstr]], + pos: Tuple[float, float], + size: Tuple[float, float], + on_select_call: Callable[[T], None] = None) -> None: + if not tabdefs: + raise ValueError('At least one tab def is required') + self.tabs: Dict[T, Tab] = {} + tab_pos_v = pos[1] + tab_button_width = float(size[0]) / len(tabdefs) + tab_spacing = (250.0 - tab_button_width) * 0.06 + h = pos[0] + for tab_id, tab_label in tabdefs: + pos = (h + tab_spacing * 0.5, tab_pos_v) + size = (tab_button_width - tab_spacing, 50.0) + btn = ba.buttonwidget(parent=parent, + position=pos, + autoselect=True, + button_type='tab', + size=size, + label=tab_label, + enable_sound=False, + on_activate_call=ba.Call( + self._tick_and_call, on_select_call, + tab_id)) + h += tab_button_width + self.tabs[tab_id] = Tab(button=btn, position=pos, size=size) + + def update_appearance(self, selected_tab_id: T) -> None: + """Update appearances to make the provided tab appear selected.""" + for tab_id, tab in self.tabs.items(): + if tab_id == selected_tab_id: + ba.buttonwidget(edit=tab.button, + color=(0.5, 0.4, 0.93), + textcolor=(0.85, 0.75, 0.95)) # lit + else: + ba.buttonwidget(edit=tab.button, + color=(0.52, 0.48, 0.63), + textcolor=(0.65, 0.6, 0.7)) # unlit + + def _tick_and_call(self, call: Optional[Callable], arg: Any) -> None: + ba.playsound(ba.getsound('click01')) + if call is not None: + call(arg) diff --git a/dist/ba_data/python/bastd/ui/teamnamescolors.py b/dist/ba_data/python/bastd/ui/teamnamescolors.py new file mode 100644 index 0000000..9f8450e --- /dev/null +++ b/dist/ba_data/python/bastd/ui/teamnamescolors.py @@ -0,0 +1,190 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a window to customize team names and colors.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + from typing import Tuple, List, Sequence + from bastd.ui.colorpicker import ColorPicker + + +class TeamNamesColorsWindow(popup.PopupWindow): + """A popup window for customizing team names and colors.""" + + def __init__(self, scale_origin: Tuple[float, float]): + from ba.internal import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES + self._width = 500 + self._height = 330 + self._transitioning_out = False + self._max_name_length = 16 + + # Creates our _root_widget. + uiscale = ba.app.ui.uiscale + scale = (1.69 if uiscale is ba.UIScale.SMALL else + 1.1 if uiscale is ba.UIScale.MEDIUM else 0.85) + super().__init__(position=scale_origin, + size=(self._width, self._height), + scale=scale) + + appconfig = ba.app.config + self._names = list( + appconfig.get('Custom Team Names', DEFAULT_TEAM_NAMES)) + + # We need to flatten the translation since it will be an + # editable string. + self._names = [ + ba.Lstr(translate=('teamNames', n)).evaluate() for n in self._names + ] + self._colors = list( + appconfig.get('Custom Team Colors', DEFAULT_TEAM_COLORS)) + + self._color_buttons: List[ba.Widget] = [] + self._color_text_fields: List[ba.Widget] = [] + + resetbtn = ba.buttonwidget( + parent=self.root_widget, + label=ba.Lstr(resource='settingsWindowAdvanced.resetText'), + autoselect=True, + scale=0.7, + on_activate_call=self._reset, + size=(120, 50), + position=(self._width * 0.5 - 60 * 0.7, self._height - 60)) + + for i in range(2): + self._color_buttons.append( + ba.buttonwidget(parent=self.root_widget, + autoselect=True, + position=(50, 0 + 195 - 90 * i), + on_activate_call=ba.Call(self._color_click, i), + size=(70, 70), + color=self._colors[i], + label='', + button_type='square')) + self._color_text_fields.append( + ba.textwidget(parent=self.root_widget, + position=(135, 0 + 201 - 90 * i), + size=(280, 46), + text=self._names[i], + h_align='left', + v_align='center', + max_chars=self._max_name_length, + color=self._colors[i], + description=ba.Lstr(resource='nameText'), + editable=True, + padding=4)) + ba.widget(edit=self._color_text_fields[0], + down_widget=self._color_text_fields[1]) + ba.widget(edit=self._color_text_fields[1], + up_widget=self._color_text_fields[0]) + ba.widget(edit=self._color_text_fields[0], up_widget=resetbtn) + + cancelbtn = ba.buttonwidget(parent=self.root_widget, + label=ba.Lstr(resource='cancelText'), + autoselect=True, + on_activate_call=self._on_cancel_press, + size=(150, 50), + position=(self._width * 0.5 - 200, 20)) + okbtn = ba.buttonwidget(parent=self.root_widget, + label=ba.Lstr(resource='okText'), + autoselect=True, + on_activate_call=self._ok, + size=(150, 50), + position=(self._width * 0.5 + 50, 20)) + ba.containerwidget(edit=self.root_widget, + selected_child=self._color_buttons[0]) + ba.widget(edit=okbtn, left_widget=cancelbtn) + self._update() + + def _color_click(self, i: int) -> None: + from bastd.ui.colorpicker import ColorPicker + ColorPicker(parent=self.root_widget, + position=self._color_buttons[i].get_screen_space_center(), + offset=(270.0, 0), + initial_color=self._colors[i], + delegate=self, + tag=i) + + def color_picker_closing(self, picker: ColorPicker) -> None: + """Called when the color picker is closing.""" + + def color_picker_selected_color(self, picker: ColorPicker, + color: Sequence[float]) -> None: + """Called when a color is selected in the color picker.""" + self._colors[picker.get_tag()] = color + self._update() + + def _reset(self) -> None: + from ba.internal import DEFAULT_TEAM_NAMES, DEFAULT_TEAM_COLORS + for i in range(2): + self._colors[i] = DEFAULT_TEAM_COLORS[i] + name = ba.Lstr(translate=('teamNames', + DEFAULT_TEAM_NAMES[i])).evaluate() + if len(name) > self._max_name_length: + print('GOT DEFAULT TEAM NAME LONGER THAN MAX LENGTH') + ba.textwidget(edit=self._color_text_fields[i], text=name) + self._update() + + def _update(self) -> None: + for i in range(2): + ba.buttonwidget(edit=self._color_buttons[i], color=self._colors[i]) + ba.textwidget(edit=self._color_text_fields[i], + color=self._colors[i]) + + def _ok(self) -> None: + from ba.internal import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES + cfg = ba.app.config + + # First, determine whether the values here are defaults, in which case + # we can clear any values from prefs. Currently if the string matches + # either the default raw value or its translation we consider it + # default. (the fact that team names get translated makes this + # situation a bit sloppy) + new_names: List[str] = [] + is_default = True + for i in range(2): + name = cast(str, ba.textwidget(query=self._color_text_fields[i])) + if not name: + ba.screenmessage(ba.Lstr(resource='nameNotEmptyText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + new_names.append(name) + + for i in range(2): + if self._colors[i] != DEFAULT_TEAM_COLORS[i]: + is_default = False + default_team_name = DEFAULT_TEAM_NAMES[i] + default_team_name_translated = ba.Lstr( + translate=('teamNames', default_team_name)).evaluate() + if ((new_names[i] != default_team_name + and new_names[i] != default_team_name_translated)): + is_default = False + + if is_default: + for key in ('Custom Team Names', 'Custom Team Colors'): + if key in cfg: + del cfg[key] + else: + cfg['Custom Team Names'] = list(new_names) + cfg['Custom Team Colors'] = list(self._colors) + + cfg.commit() + self._transition_out() + + def _transition_out(self, transition: str = 'out_scale') -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition=transition) + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() + + def _on_cancel_press(self) -> None: + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/telnet.py b/dist/ba_data/python/bastd/ui/telnet.py new file mode 100644 index 0000000..8ea63e4 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/telnet.py @@ -0,0 +1,53 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality for telnet access.""" + +from __future__ import annotations + +import _ba +import ba + + +class TelnetAccessRequestWindow(ba.Window): + """Window asking the user whether to allow a telnet connection.""" + + def __init__(self) -> None: + width = 400 + height = 100 + text = ba.Lstr(resource='telnetAccessText') + + uiscale = ba.app.ui.uiscale + super().__init__(root_widget=ba.containerwidget( + size=(width, height + 40), + transition='in_right', + scale=(1.7 if uiscale is ba.UIScale.SMALL else + 1.3 if uiscale is ba.UIScale.MEDIUM else 1.0))) + padding = 20 + ba.textwidget(parent=self._root_widget, + position=(padding, padding + 33), + size=(width - 2 * padding, height - 2 * padding), + h_align='center', + v_align='top', + text=text) + btn = ba.buttonwidget(parent=self._root_widget, + position=(20, 20), + size=(140, 50), + label=ba.Lstr(resource='denyText'), + on_activate_call=self._cancel) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + ba.containerwidget(edit=self._root_widget, selected_child=btn) + + ba.buttonwidget(parent=self._root_widget, + position=(width - 155, 20), + size=(140, 50), + label=ba.Lstr(resource='allowText'), + on_activate_call=self._ok) + + def _cancel(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_right') + _ba.set_telnet_access_enabled(False) + + def _ok(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_left') + _ba.set_telnet_access_enabled(True) + ba.screenmessage(ba.Lstr(resource='telnetAccessGrantedText')) diff --git a/dist/ba_data/python/bastd/ui/tournamententry.py b/dist/ba_data/python/bastd/ui/tournamententry.py new file mode 100644 index 0000000..12ed8b3 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/tournamententry.py @@ -0,0 +1,634 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines a popup window for entering tournaments.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + from typing import Any, Tuple, Callable, Optional, Dict + + +class TournamentEntryWindow(popup.PopupWindow): + """Popup window for entering tournaments.""" + + def __init__(self, + tournament_id: str, + tournament_activity: ba.Activity = None, + position: Tuple[float, float] = (0.0, 0.0), + delegate: Any = None, + scale: float = None, + offset: Tuple[float, float] = (0.0, 0.0), + on_close_call: Callable[[], Any] = None): + # Needs some tidying. + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + + ba.set_analytics_screen('Tournament Entry Window') + + self._tournament_id = tournament_id + self._tournament_info = ( + ba.app.accounts.tournament_info[self._tournament_id]) + + # Set a few vars depending on the tourney fee. + self._fee = self._tournament_info['fee'] + self._allow_ads = self._tournament_info['allowAds'] + if self._fee == 4: + self._purchase_name = 'tournament_entry_4' + self._purchase_price_name = 'price.tournament_entry_4' + elif self._fee == 3: + self._purchase_name = 'tournament_entry_3' + self._purchase_price_name = 'price.tournament_entry_3' + elif self._fee == 2: + self._purchase_name = 'tournament_entry_2' + self._purchase_price_name = 'price.tournament_entry_2' + elif self._fee == 1: + self._purchase_name = 'tournament_entry_1' + self._purchase_price_name = 'price.tournament_entry_1' + else: + if self._fee != 0: + raise ValueError('invalid fee: ' + str(self._fee)) + self._purchase_name = 'tournament_entry_0' + self._purchase_price_name = 'price.tournament_entry_0' + + self._purchase_price: Optional[int] = None + + self._on_close_call = on_close_call + if scale is None: + uiscale = ba.app.ui.uiscale + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + self._delegate = delegate + self._transitioning_out = False + + self._tournament_activity = tournament_activity + + self._width = 340 + self._height = 225 + + bg_color = (0.5, 0.4, 0.6) + + # Creates our root_widget. + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=bg_color, + offset=offset, + toolbar_visibility='menu_currency') + + self._last_ad_press_time = -9999.0 + self._last_ticket_press_time = -9999.0 + self._entering = False + self._launched = False + + # Show the ad button only if we support ads *and* it has a level 1 fee. + self._do_ad_btn = (_ba.has_video_ads() and self._allow_ads) + + x_offs = 0 if self._do_ad_btn else 85 + + self._cancel_button = ba.buttonwidget(parent=self.root_widget, + position=(20, self._height - 34), + size=(60, 60), + scale=0.5, + label='', + color=bg_color, + on_activate_call=self._on_cancel, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + + self._title_text = ba.textwidget( + parent=self.root_widget, + position=(self._width * 0.5, self._height - 20), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.6, + text=ba.Lstr(resource='tournamentEntryText'), + maxwidth=180, + color=(1, 1, 1, 0.4)) + + btn = self._pay_with_tickets_button = ba.buttonwidget( + parent=self.root_widget, + position=(30 + x_offs, 60), + autoselect=True, + button_type='square', + size=(120, 120), + label='', + on_activate_call=self._on_pay_with_tickets_press) + self._ticket_img_pos = (50 + x_offs, 94) + self._ticket_img_pos_free = (50 + x_offs, 80) + self._ticket_img = ba.imagewidget(parent=self.root_widget, + draw_controller=btn, + size=(80, 80), + position=self._ticket_img_pos, + texture=ba.gettexture('tickets')) + self._ticket_cost_text_position = (87 + x_offs, 88) + self._ticket_cost_text_position_free = (87 + x_offs, 120) + self._ticket_cost_text = ba.textwidget( + parent=self.root_widget, + draw_controller=btn, + position=self._ticket_cost_text_position, + size=(0, 0), + h_align='center', + v_align='center', + scale=0.6, + text='', + maxwidth=95, + color=(0, 1, 0)) + self._free_plays_remaining_text = ba.textwidget( + parent=self.root_widget, + draw_controller=btn, + position=(87 + x_offs, 78), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.33, + text='', + maxwidth=95, + color=(0, 0.8, 0)) + self._pay_with_ad_btn: Optional[ba.Widget] + if self._do_ad_btn: + btn = self._pay_with_ad_btn = ba.buttonwidget( + parent=self.root_widget, + position=(190, 60), + autoselect=True, + button_type='square', + size=(120, 120), + label='', + on_activate_call=self._on_pay_with_ad_press) + self._pay_with_ad_img = ba.imagewidget(parent=self.root_widget, + draw_controller=btn, + size=(80, 80), + position=(210, 94), + texture=ba.gettexture('tv')) + + self._ad_text_position = (251, 88) + self._ad_text_position_remaining = (251, 92) + have_ad_tries_remaining = ( + self._tournament_info['adTriesRemaining'] is not None) + self._ad_text = ba.textwidget( + parent=self.root_widget, + draw_controller=btn, + position=self._ad_text_position_remaining + if have_ad_tries_remaining else self._ad_text_position, + size=(0, 0), + h_align='center', + v_align='center', + scale=0.6, + text=ba.Lstr(resource='watchAVideoText', + fallback_resource='watchAnAdText'), + maxwidth=95, + color=(0, 1, 0)) + ad_plays_remaining_text = ( + '' if not have_ad_tries_remaining else '' + + str(self._tournament_info['adTriesRemaining'])) + self._ad_plays_remaining_text = ba.textwidget( + parent=self.root_widget, + draw_controller=btn, + position=(251, 78), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.33, + text=ad_plays_remaining_text, + maxwidth=95, + color=(0, 0.8, 0)) + + ba.textwidget(parent=self.root_widget, + position=(self._width * 0.5, 120), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.6, + text=ba.Lstr(resource='orText', + subs=[('${A}', ''), ('${B}', '')]), + maxwidth=35, + color=(1, 1, 1, 0.5)) + else: + self._pay_with_ad_btn = None + + self._get_tickets_button: Optional[ba.Widget] = None + self._ticket_count_text: Optional[ba.Widget] = None + if not ba.app.ui.use_toolbars: + if ba.app.allow_ticket_purchases: + self._get_tickets_button = ba.buttonwidget( + parent=self.root_widget, + position=(self._width - 190 + 125, self._height - 34), + autoselect=True, + scale=0.5, + size=(120, 60), + textcolor=(0.2, 1, 0.2), + label=ba.charstr(ba.SpecialChar.TICKET), + color=(0.65, 0.5, 0.8), + on_activate_call=self._on_get_tickets_press) + else: + self._ticket_count_text = ba.textwidget( + parent=self.root_widget, + scale=0.5, + position=(self._width - 190 + 125, self._height - 34), + color=(0.2, 1, 0.2), + h_align='center', + v_align='center') + + self._seconds_remaining = None + + ba.containerwidget(edit=self.root_widget, + cancel_button=self._cancel_button) + + # Let's also ask the server for info about this tournament + # (time remaining, etc) so we can show the user time remaining, + # disallow entry if time has run out, etc. + # xoffs = 104 if ba.app.ui.use_toolbars else 0 + self._time_remaining_text = ba.textwidget( + parent=self.root_widget, + position=(self._width / 2, 28), + size=(0, 0), + h_align='center', + v_align='center', + text='-', + scale=0.65, + maxwidth=100, + flatness=1.0, + color=(0.7, 0.7, 0.7), + ) + self._time_remaining_label_text = ba.textwidget( + parent=self.root_widget, + position=(self._width / 2, 45), + size=(0, 0), + h_align='center', + v_align='center', + text=ba.Lstr(resource='coopSelectWindow.timeRemainingText'), + scale=0.45, + flatness=1.0, + maxwidth=100, + color=(0.7, 0.7, 0.7)) + + self._last_query_time: Optional[float] = None + + # If there seems to be a relatively-recent valid cached info for this + # tournament, use it. Otherwise we'll kick off a query ourselves. + if (self._tournament_id in ba.app.accounts.tournament_info and + ba.app.accounts.tournament_info[self._tournament_id]['valid'] + and (ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) - + ba.app.accounts.tournament_info[self._tournament_id] + ['timeReceived'] < 1000 * 60 * 5)): + try: + info = ba.app.accounts.tournament_info[self._tournament_id] + self._seconds_remaining = max( + 0, info['timeRemaining'] - int( + (ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) + - info['timeReceived']) / 1000)) + self._have_valid_data = True + self._last_query_time = ba.time(ba.TimeType.REAL) + except Exception: + ba.print_exception('error using valid tourney data') + self._have_valid_data = False + else: + self._have_valid_data = False + + self._fg_state = ba.app.fg_state + self._running_query = False + self._update_timer = ba.Timer(1.0, + ba.WeakCall(self._update), + repeat=True, + timetype=ba.TimeType.REAL) + self._update() + self._restore_state() + + def _on_tournament_query_response(self, data: Optional[Dict[str, + Any]]) -> None: + accounts = ba.app.accounts + self._running_query = False + if data is not None: + data = data['t'] # This used to be the whole payload. + accounts.cache_tournament_info(data) + self._seconds_remaining = accounts.tournament_info[ + self._tournament_id]['timeRemaining'] + self._have_valid_data = True + + def _save_state(self) -> None: + if not self.root_widget: + return + sel = self.root_widget.get_selected_child() + if sel == self._pay_with_ad_btn: + sel_name = 'Ad' + else: + sel_name = 'Tickets' + cfg = ba.app.config + cfg['Tournament Pay Selection'] = sel_name + cfg.commit() + + def _restore_state(self) -> None: + sel_name = ba.app.config.get('Tournament Pay Selection', 'Tickets') + if sel_name == 'Ad' and self._pay_with_ad_btn is not None: + sel = self._pay_with_ad_btn + else: + sel = self._pay_with_tickets_button + ba.containerwidget(edit=self.root_widget, selected_child=sel) + + def _update(self) -> None: + # We may outlive our widgets. + if not self.root_widget: + return + + # If we've been foregrounded/backgrounded we need to re-grab data. + if self._fg_state != ba.app.fg_state: + self._fg_state = ba.app.fg_state + self._have_valid_data = False + + # If we need to run another tournament query, do so. + if not self._running_query and ( + (self._last_query_time is None) or (not self._have_valid_data) or + (ba.time(ba.TimeType.REAL) - self._last_query_time > 30.0)): + _ba.tournament_query(args={ + 'source': + 'entry window' if self._tournament_activity is None else + 'retry entry window' + }, + callback=ba.WeakCall( + self._on_tournament_query_response)) + self._last_query_time = ba.time(ba.TimeType.REAL) + self._running_query = True + + # Grab the latest info on our tourney. + self._tournament_info = ba.app.accounts.tournament_info[ + self._tournament_id] + + # If we don't have valid data always show a '-' for time. + if not self._have_valid_data: + ba.textwidget(edit=self._time_remaining_text, text='-') + else: + if self._seconds_remaining is not None: + self._seconds_remaining = max(0, self._seconds_remaining - 1) + ba.textwidget(edit=self._time_remaining_text, + text=ba.timestring( + self._seconds_remaining * 1000, + centi=False, + timeformat=ba.TimeFormat.MILLISECONDS)) + + # Keep price up-to-date and update the button with it. + self._purchase_price = _ba.get_account_misc_read_val( + self._purchase_price_name, None) + + ba.textwidget( + edit=self._ticket_cost_text, + text=(ba.Lstr(resource='getTicketsWindow.freeText') + if self._purchase_price == 0 else ba.Lstr( + resource='getTicketsWindow.ticketsText', + subs=[('${COUNT}', str(self._purchase_price) + if self._purchase_price is not None else '?')])), + position=self._ticket_cost_text_position_free + if self._purchase_price == 0 else self._ticket_cost_text_position, + scale=1.0 if self._purchase_price == 0 else 0.6) + + ba.textwidget( + edit=self._free_plays_remaining_text, + text='' if + (self._tournament_info['freeTriesRemaining'] in [None, 0] + or self._purchase_price != 0) else '' + + str(self._tournament_info['freeTriesRemaining'])) + + ba.imagewidget(edit=self._ticket_img, + opacity=0.2 if self._purchase_price == 0 else 1.0, + position=self._ticket_img_pos_free + if self._purchase_price == 0 else self._ticket_img_pos) + + if self._do_ad_btn: + enabled = _ba.have_incentivized_ad() + have_ad_tries_remaining = ( + self._tournament_info['adTriesRemaining'] is not None + and self._tournament_info['adTriesRemaining'] > 0) + ba.textwidget(edit=self._ad_text, + position=self._ad_text_position_remaining if + have_ad_tries_remaining else self._ad_text_position, + color=(0, 1, 0) if enabled else (0.5, 0.5, 0.5)) + ba.imagewidget(edit=self._pay_with_ad_img, + opacity=1.0 if enabled else 0.2) + ba.buttonwidget(edit=self._pay_with_ad_btn, + color=(0.5, 0.7, 0.2) if enabled else + (0.5, 0.5, 0.5)) + ad_plays_remaining_text = ( + '' if not have_ad_tries_remaining else '' + + str(self._tournament_info['adTriesRemaining'])) + ba.textwidget(edit=self._ad_plays_remaining_text, + text=ad_plays_remaining_text, + color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4)) + + try: + t_str = str(_ba.get_account_ticket_count()) + except Exception: + t_str = '?' + if self._get_tickets_button: + ba.buttonwidget(edit=self._get_tickets_button, + label=ba.charstr(ba.SpecialChar.TICKET) + t_str) + if self._ticket_count_text: + ba.textwidget(edit=self._ticket_count_text, + text=ba.charstr(ba.SpecialChar.TICKET) + t_str) + + def _launch(self) -> None: + if self._launched: + return + self._launched = True + launched = False + + # If they gave us an existing activity, just restart it. + if self._tournament_activity is not None: + try: + ba.timer(0.1, + lambda: ba.playsound(ba.getsound('cashRegister')), + timetype=ba.TimeType.REAL) + with ba.Context(self._tournament_activity): + self._tournament_activity.end({'outcome': 'restart'}, + force=True) + ba.timer(0.3, self._transition_out, timetype=ba.TimeType.REAL) + launched = True + ba.screenmessage(ba.Lstr(translate=('serverResponses', + 'Entering tournament...')), + color=(0, 1, 0)) + + # We can hit exceptions here if _tournament_activity ends before + # our restart attempt happens. + # In this case we'll fall back to launching a new session. + # This is not ideal since players will have to rejoin, etc., + # but it works for now. + except Exception: + ba.print_exception('Error restarting tournament activity.') + + # If we had no existing activity (or were unable to restart it) + # launch a new session. + if not launched: + ba.timer(0.1, + lambda: ba.playsound(ba.getsound('cashRegister')), + timetype=ba.TimeType.REAL) + ba.timer( + 1.0, + lambda: ba.app.launch_coop_game( + self._tournament_info['game'], + args={ + 'min_players': self._tournament_info['minPlayers'], + 'max_players': self._tournament_info['maxPlayers'], + 'tournament_id': self._tournament_id + }), + timetype=ba.TimeType.REAL) + ba.timer(0.7, self._transition_out, timetype=ba.TimeType.REAL) + ba.screenmessage(ba.Lstr(translate=('serverResponses', + 'Entering tournament...')), + color=(0, 1, 0)) + + def _on_pay_with_tickets_press(self) -> None: + from bastd.ui import getcurrency + + # If we're already entering, ignore. + if self._entering: + return + + if not self._have_valid_data: + ba.screenmessage(ba.Lstr(resource='tournamentCheckingStateText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + # If we don't have a price. + if self._purchase_price is None: + ba.screenmessage(ba.Lstr(resource='tournamentCheckingStateText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + # Deny if it looks like the tourney has ended. + if self._seconds_remaining == 0: + ba.screenmessage(ba.Lstr(resource='tournamentEndedText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + # Deny if we don't have enough tickets. + ticket_count: Optional[int] + try: + ticket_count = _ba.get_account_ticket_count() + except Exception: + # FIXME: should add a ba.NotSignedInError we can use here. + ticket_count = None + ticket_cost = self._purchase_price + if ticket_count is not None and ticket_count < ticket_cost: + getcurrency.show_get_tickets_prompt() + ba.playsound(ba.getsound('error')) + return + + cur_time = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) + self._last_ticket_press_time = cur_time + assert isinstance(ticket_cost, int) + _ba.in_game_purchase(self._purchase_name, ticket_cost) + + self._entering = True + _ba.add_transaction({ + 'type': 'ENTER_TOURNAMENT', + 'fee': self._fee, + 'tournamentID': self._tournament_id + }) + _ba.run_transactions() + self._launch() + + def _on_pay_with_ad_press(self) -> None: + + # If we're already entering, ignore. + if self._entering: + return + + if not self._have_valid_data: + ba.screenmessage(ba.Lstr(resource='tournamentCheckingStateText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + # Deny if it looks like the tourney has ended. + if self._seconds_remaining == 0: + ba.screenmessage(ba.Lstr(resource='tournamentEndedText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + cur_time = ba.time(ba.TimeType.REAL) + if cur_time - self._last_ad_press_time > 5.0: + self._last_ad_press_time = cur_time + _ba.app.ads.show_ad_2('tournament_entry', + on_completion_call=ba.WeakCall( + self._on_ad_complete)) + + def _on_ad_complete(self, actually_showed: bool) -> None: + + # Make sure any transactions the ad added got locally applied + # (rewards added, etc.). + _ba.run_transactions() + + # If we're already entering the tourney, ignore. + if self._entering: + return + + if not actually_showed: + return + + # This should have awarded us the tournament_entry_ad purchase; + # make sure that's present. + # (otherwise the server will ignore our tournament entry anyway) + if not _ba.get_purchased('tournament_entry_ad'): + print('no tournament_entry_ad purchase present in _on_ad_complete') + ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + self._entering = True + _ba.add_transaction({ + 'type': 'ENTER_TOURNAMENT', + 'fee': 'ad', + 'tournamentID': self._tournament_id + }) + _ba.run_transactions() + self._launch() + + def _on_get_tickets_press(self) -> None: + from bastd.ui import getcurrency + + # If we're already entering, ignore presses. + if self._entering: + return + + # Bring up get-tickets window and then kill ourself (we're on the + # overlay layer so we'd show up above it). + getcurrency.GetCurrencyWindow(modal=True, + origin_widget=self._get_tickets_button) + self._transition_out() + + def _on_cancel(self) -> None: + + # Don't allow canceling for several seconds after poking an enter + # button if it looks like we're waiting on a purchase or entering + # the tournament. + if ((ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) - + self._last_ticket_press_time < 6000) and + (_ba.have_outstanding_transactions() + or _ba.get_purchased(self._purchase_name) or self._entering)): + ba.playsound(ba.getsound('error')) + return + self._transition_out() + + def _transition_out(self) -> None: + if not self.root_widget: + return + if not self._transitioning_out: + self._transitioning_out = True + self._save_state() + ba.containerwidget(edit=self.root_widget, transition='out_scale') + if self._on_close_call is not None: + self._on_close_call() + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._on_cancel() diff --git a/dist/ba_data/python/bastd/ui/tournamentscores.py b/dist/ba_data/python/bastd/ui/tournamentscores.py new file mode 100644 index 0000000..e162b47 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/tournamentscores.py @@ -0,0 +1,209 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a popup for viewing tournament scores.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba +from bastd.ui import popup as popup_ui + +if TYPE_CHECKING: + from typing import Any, Tuple, Sequence, Callable, Dict, Optional, List + + +class TournamentScoresWindow(popup_ui.PopupWindow): + """Window for viewing tournament scores.""" + + def __init__(self, + tournament_id: str, + tournament_activity: ba.GameActivity = None, + position: Tuple[float, float] = (0.0, 0.0), + scale: float = None, + offset: Tuple[float, float] = (0.0, 0.0), + tint_color: Sequence[float] = (1.0, 1.0, 1.0), + tint2_color: Sequence[float] = (1.0, 1.0, 1.0), + selected_character: str = None, + on_close_call: Callable[[], Any] = None): + + del tournament_activity # unused arg + del tint_color # unused arg + del tint2_color # unused arg + del selected_character # unused arg + self._tournament_id = tournament_id + self._subcontainer: Optional[ba.Widget] = None + self._on_close_call = on_close_call + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + self._transitioning_out = False + + self._width = 400 + self._height = (300 if uiscale is ba.UIScale.SMALL else + 370 if uiscale is ba.UIScale.MEDIUM else 450) + + bg_color = (0.5, 0.4, 0.6) + + # creates our _root_widget + super().__init__(position=position, + size=(self._width, self._height), + scale=scale, + bg_color=bg_color, + offset=offset) + + # app = ba.app + + self._cancel_button = ba.buttonwidget( + parent=self.root_widget, + position=(50, self._height - 30), + size=(50, 50), + scale=0.5, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + + self._title_text = ba.textwidget( + parent=self.root_widget, + position=(self._width * 0.5, self._height - 20), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.6, + text=ba.Lstr(resource='tournamentStandingsText'), + maxwidth=200, + color=(1, 1, 1, 0.4)) + + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + size=(self._width - 60, + self._height - 70), + position=(30, 30), + highlight=False, + simple_culling_v=10) + ba.widget(edit=self._scrollwidget, autoselect=True) + + self._loading_text = ba.textwidget( + parent=self._scrollwidget, + scale=0.5, + text=ba.Lstr(value='${A}...', + subs=[('${A}', ba.Lstr(resource='loadingText'))]), + size=(self._width - 60, 100), + h_align='center', + v_align='center') + + ba.containerwidget(edit=self.root_widget, + cancel_button=self._cancel_button) + + _ba.tournament_query(args={ + 'tournamentIDs': [tournament_id], + 'numScores': 50, + 'source': 'scores window' + }, + callback=ba.WeakCall( + self._on_tournament_query_response)) + + def _on_tournament_query_response(self, data: Optional[Dict[str, + Any]]) -> None: + if data is not None: + # this used to be the whole payload + data_t: List[Dict[str, Any]] = data['t'] + # kill our loading text if we've got scores.. otherwise just + # replace it with 'no scores yet' + if data_t[0]['scores']: + self._loading_text.delete() + else: + ba.textwidget(edit=self._loading_text, + text=ba.Lstr(resource='noScoresYetText')) + incr = 30 + sub_width = self._width - 90 + sub_height = 30 + len(data_t[0]['scores']) * incr + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(sub_width, + sub_height), + background=False) + for i, entry in enumerate(data_t[0]['scores']): + + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 0.1 - 5, + sub_height - 20 - incr * i), + maxwidth=20, + scale=0.5, + color=(0.6, 0.6, 0.7), + flatness=1.0, + shadow=0.0, + text=str(i + 1), + size=(0, 0), + h_align='right', + v_align='center') + + ba.textwidget( + parent=self._subcontainer, + position=(sub_width * 0.25 - 2, + sub_height - 20 - incr * i), + maxwidth=sub_width * 0.24, + color=(0.9, 1.0, 0.9), + flatness=1.0, + shadow=0.0, + scale=0.6, + text=(ba.timestring(entry[0] * 10, + centi=True, + timeformat=ba.TimeFormat.MILLISECONDS) + if data_t[0]['scoreType'] == 'time' else str( + entry[0])), + size=(0, 0), + h_align='center', + v_align='center') + + txt = ba.textwidget( + parent=self._subcontainer, + position=(sub_width * 0.25, + sub_height - 20 - incr * i - (0.5 / 0.7) * incr), + maxwidth=sub_width * 0.6, + scale=0.7, + flatness=1.0, + shadow=0.0, + text=ba.Lstr(value=entry[1]), + selectable=True, + click_activate=True, + autoselect=True, + extra_touch_border_scale=0.0, + size=((sub_width * 0.6) / 0.7, incr / 0.7), + h_align='left', + v_align='center') + + ba.textwidget(edit=txt, + on_activate_call=ba.Call(self._show_player_info, + entry, txt)) + if i == 0: + ba.widget(edit=txt, up_widget=self._cancel_button) + + def _show_player_info(self, entry: Any, textwidget: ba.Widget) -> None: + from bastd.ui.account.viewer import AccountViewerWindow + # for the moment we only work if a single player-info is present.. + if len(entry[2]) != 1: + ba.playsound(ba.getsound('error')) + return + ba.playsound(ba.getsound('swish')) + AccountViewerWindow(account_id=entry[2][0].get('a', None), + profile_id=entry[2][0].get('p', None), + position=textwidget.get_screen_space_center()) + self._transition_out() + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + if self._on_close_call is not None: + self._on_close_call() + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/trophies.py b/dist/ba_data/python/bastd/ui/trophies.py new file mode 100644 index 0000000..4763172 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/trophies.py @@ -0,0 +1,187 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides a popup window for viewing trophies.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.ui import popup + +if TYPE_CHECKING: + from typing import Any, Tuple, Dict, List + + +class TrophiesWindow(popup.PopupWindow): + """Popup window for viewing trophies.""" + + def __init__(self, + position: Tuple[float, float], + data: Dict[str, Any], + scale: float = None): + self._data = data + uiscale = ba.app.ui.uiscale + if scale is None: + scale = (2.3 if uiscale is ba.UIScale.SMALL else + 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23) + self._transitioning_out = False + self._width = 300 + self._height = 300 + bg_color = (0.5, 0.4, 0.6) + + popup.PopupWindow.__init__(self, + position=position, + size=(self._width, self._height), + scale=scale, + bg_color=bg_color) + + self._cancel_button = ba.buttonwidget( + parent=self.root_widget, + position=(50, self._height - 30), + size=(50, 50), + scale=0.5, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + + self._title_text = ba.textwidget(parent=self.root_widget, + position=(self._width * 0.5, + self._height - 20), + size=(0, 0), + h_align='center', + v_align='center', + scale=0.6, + text=ba.Lstr(resource='trophiesText'), + maxwidth=200, + color=(1, 1, 1, 0.4)) + + self._scrollwidget = ba.scrollwidget(parent=self.root_widget, + size=(self._width - 60, + self._height - 70), + position=(30, 30), + capture_arrows=True) + ba.widget(edit=self._scrollwidget, autoselect=True) + + ba.containerwidget(edit=self.root_widget, + cancel_button=self._cancel_button) + + incr = 31 + sub_width = self._width - 90 + + trophy_types = [['0a'], ['0b'], ['1'], ['2'], ['3'], ['4']] + sub_height = 40 + len(trophy_types) * incr + + eq_text = ba.Lstr( + resource='coopSelectWindow.powerRankingPointsEqualsText').evaluate( + ) + + self._subcontainer = ba.containerwidget(parent=self._scrollwidget, + size=(sub_width, sub_height), + background=False) + + total_pts = 0 + + multi_txt = ba.Lstr( + resource='coopSelectWindow.powerRankingPointsMultText').evaluate() + + total_pts += self._create_trophy_type_widgets(eq_text, incr, multi_txt, + sub_height, sub_width, + trophy_types) + + ba.textwidget( + parent=self._subcontainer, + position=(sub_width * 1.0, + sub_height - 20 - incr * len(trophy_types)), + maxwidth=sub_width * 0.5, + scale=0.7, + color=(0.7, 0.8, 1.0), + flatness=1.0, + shadow=0.0, + text=ba.Lstr(resource='coopSelectWindow.totalText').evaluate() + + ' ' + eq_text.replace('${NUMBER}', str(total_pts)), + size=(0, 0), + h_align='right', + v_align='center') + + def _create_trophy_type_widgets(self, eq_text: str, incr: int, + multi_txt: str, sub_height: int, + sub_width: int, + trophy_types: List[List[str]]) -> int: + from ba.internal import get_trophy_string + pts = 0 + for i, trophy_type in enumerate(trophy_types): + t_count = self._data['t' + trophy_type[0]] + t_mult = self._data['t' + trophy_type[0] + 'm'] + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 0.15, + sub_height - 20 - incr * i), + scale=0.7, + flatness=1.0, + shadow=0.7, + color=(1, 1, 1), + text=get_trophy_string(trophy_type[0]), + size=(0, 0), + h_align='center', + v_align='center') + + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 0.31, + sub_height - 20 - incr * i), + maxwidth=sub_width * 0.2, + scale=0.8, + flatness=1.0, + shadow=0.0, + color=(0, 1, 0) if (t_count > 0) else + (0.6, 0.6, 0.6, 0.5), + text=str(t_count), + size=(0, 0), + h_align='center', + v_align='center') + + txt = multi_txt.replace('${NUMBER}', str(t_mult)) + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 0.57, + sub_height - 20 - incr * i), + maxwidth=sub_width * 0.3, + scale=0.4, + flatness=1.0, + shadow=0.0, + color=(0.63, 0.6, 0.75) if (t_count > 0) else + (0.6, 0.6, 0.6, 0.4), + text=txt, + size=(0, 0), + h_align='center', + v_align='center') + + pts = t_count * t_mult + ba.textwidget(parent=self._subcontainer, + position=(sub_width * 0.88, + sub_height - 20 - incr * i), + maxwidth=sub_width * 0.3, + color=(0.7, 0.8, 1.0) if (t_count > 0) else + (0.9, 0.9, 1.0, 0.3), + flatness=1.0, + shadow=0.0, + scale=0.5, + text=eq_text.replace('${NUMBER}', str(pts)), + size=(0, 0), + h_align='center', + v_align='center') + pts += pts + return pts + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + ba.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_popup_cancel(self) -> None: + ba.playsound(ba.getsound('swish')) + self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/url.py b/dist/ba_data/python/bastd/ui/url.py new file mode 100644 index 0000000..00f3b30 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/url.py @@ -0,0 +1,89 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to URLs.""" + +from __future__ import annotations + +import _ba +import ba + + +class ShowURLWindow(ba.Window): + """A window presenting a URL to the user visually.""" + + def __init__(self, address: str): + + # in some cases we might want to show it as a qr code + # (for long URLs especially) + app = ba.app + uiscale = app.ui.uiscale + if app.platform == 'android' and app.subplatform == 'alibaba': + self._width = 500 + self._height = 500 + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height), + transition='in_right', + scale=(1.25 if uiscale is ba.UIScale.SMALL else + 1.25 if uiscale is ba.UIScale.MEDIUM else 1.25))) + self._cancel_button = ba.buttonwidget( + parent=self._root_widget, + position=(50, self._height - 30), + size=(50, 50), + scale=0.6, + label='', + color=(0.6, 0.5, 0.6), + on_activate_call=self._done, + autoselect=True, + icon=ba.gettexture('crossOut'), + iconscale=1.2) + qr_size = 400 + ba.imagewidget(parent=self._root_widget, + position=(self._width * 0.5 - qr_size * 0.5, + self._height * 0.5 - qr_size * 0.5), + size=(qr_size, qr_size), + texture=_ba.get_qrcode_texture(address)) + ba.containerwidget(edit=self._root_widget, + cancel_button=self._cancel_button) + else: + # show it as a simple string... + self._width = 800 + self._height = 200 + self._root_widget = ba.containerwidget( + size=(self._width, self._height + 40), + transition='in_right', + scale=(1.25 if uiscale is ba.UIScale.SMALL else + 1.25 if uiscale is ba.UIScale.MEDIUM else 1.25)) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 10), + size=(0, 0), + color=ba.app.ui.title_color, + h_align='center', + v_align='center', + text=ba.Lstr(resource='directBrowserToURLText'), + maxwidth=self._width * 0.95) + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, + self._height * 0.5 + 29), + size=(0, 0), + scale=1.3, + color=ba.app.ui.infotextcolor, + h_align='center', + v_align='center', + text=address, + maxwidth=self._width * 0.95) + button_width = 200 + btn = ba.buttonwidget(parent=self._root_widget, + position=(self._width * 0.5 - + button_width * 0.5, 20), + size=(button_width, 65), + label=ba.Lstr(resource='doneText'), + on_activate_call=self._done) + # we have no 'cancel' button but still want to be able to + # hit back/escape/etc to leave.. + ba.containerwidget(edit=self._root_widget, + selected_child=btn, + start_button=btn, + on_cancel_call=btn.activate) + + def _done(self) -> None: + ba.containerwidget(edit=self._root_widget, transition='out_left') diff --git a/dist/ba_data/python/bastd/ui/watch.py b/dist/ba_data/python/bastd/ui/watch.py new file mode 100644 index 0000000..b27a381 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/watch.py @@ -0,0 +1,543 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides UI functionality for watching replays.""" + +from __future__ import annotations + +import os +from enum import Enum +from typing import TYPE_CHECKING, cast + +import _ba +import ba + +if TYPE_CHECKING: + from typing import Any, Optional, Tuple, Dict + + +class WatchWindow(ba.Window): + """Window for watching replays.""" + + class TabID(Enum): + """Our available tab types.""" + MY_REPLAYS = 'my_replays' + TEST_TAB = 'test_tab' + + def __init__(self, + transition: Optional[str] = 'in_right', + origin_widget: ba.Widget = None): + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + from bastd.ui.tabs import TabRow + ba.set_analytics_screen('Watch Window') + scale_origin: Optional[Tuple[float, float]] + if origin_widget is not None: + self._transition_out = 'out_scale' + scale_origin = origin_widget.get_screen_space_center() + transition = 'in_scale' + else: + self._transition_out = 'out_right' + scale_origin = None + ba.app.ui.set_main_menu_location('Watch') + self._tab_data: Dict[str, Any] = {} + self._my_replays_scroll_width: Optional[float] = None + self._my_replays_watch_replay_button: Optional[ba.Widget] = None + self._scrollwidget: Optional[ba.Widget] = None + self._columnwidget: Optional[ba.Widget] = None + self._my_replay_selected: Optional[str] = None + self._my_replays_rename_window: Optional[ba.Widget] = None + self._my_replay_rename_text: Optional[ba.Widget] = None + self._r = 'watchWindow' + uiscale = ba.app.ui.uiscale + self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (578 if uiscale is ba.UIScale.SMALL else + 670 if uiscale is ba.UIScale.MEDIUM else 800) + self._current_tab: Optional[WatchWindow.TabID] = None + extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 + + super().__init__(root_widget=ba.containerwidget( + size=(self._width, self._height + extra_top), + transition=transition, + toolbar_visibility='menu_minimal', + scale_origin_stack_offset=scale_origin, + scale=(1.3 if uiscale is ba.UIScale.SMALL else + 0.97 if uiscale is ba.UIScale.MEDIUM else 0.8), + stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( + 0, 15) if uiscale is ba.UIScale.MEDIUM else (0, 0))) + + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + ba.containerwidget(edit=self._root_widget, + on_cancel_call=self._back) + self._back_button = None + else: + self._back_button = btn = ba.buttonwidget( + parent=self._root_widget, + autoselect=True, + position=(70 + x_inset, self._height - 74), + size=(140, 60), + scale=1.1, + label=ba.Lstr(resource='backText'), + button_type='back', + on_activate_call=self._back) + ba.containerwidget(edit=self._root_widget, cancel_button=btn) + ba.buttonwidget(edit=btn, + button_type='backSmall', + size=(60, 60), + label=ba.charstr(ba.SpecialChar.BACK)) + + ba.textwidget(parent=self._root_widget, + position=(self._width * 0.5, self._height - 38), + size=(0, 0), + color=ba.app.ui.title_color, + scale=1.5, + h_align='center', + v_align='center', + text=ba.Lstr(resource=self._r + '.titleText'), + maxwidth=400) + + tabdefs = [ + (self.TabID.MY_REPLAYS, + ba.Lstr(resource=self._r + '.myReplaysText')), + # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')), + ] + + scroll_buffer_h = 130 + 2 * x_inset + tab_buffer_h = 750 + 2 * x_inset + + self._tab_row = TabRow(self._root_widget, + tabdefs, + pos=(tab_buffer_h * 0.5, self._height - 130), + size=(self._width - tab_buffer_h, 50), + on_select_call=self._set_tab) + + if ba.app.ui.use_toolbars: + first_tab = self._tab_row.tabs[tabdefs[0][0]] + last_tab = self._tab_row.tabs[tabdefs[-1][0]] + ba.widget(edit=last_tab.button, + right_widget=_ba.get_special_widget('party_button')) + if uiscale is ba.UIScale.SMALL: + bbtn = _ba.get_special_widget('back_button') + ba.widget(edit=first_tab.button, + up_widget=bbtn, + left_widget=bbtn) + + self._scroll_width = self._width - scroll_buffer_h + self._scroll_height = self._height - 180 + + # Not actually using a scroll widget anymore; just an image. + scroll_left = (self._width - self._scroll_width) * 0.5 + scroll_bottom = self._height - self._scroll_height - 79 - 48 + buffer_h = 10 + buffer_v = 4 + ba.imagewidget(parent=self._root_widget, + position=(scroll_left - buffer_h, + scroll_bottom - buffer_v), + size=(self._scroll_width + 2 * buffer_h, + self._scroll_height + 2 * buffer_v), + texture=ba.gettexture('scrollWidget'), + model_transparent=ba.getmodel('softEdgeOutside')) + self._tab_container: Optional[ba.Widget] = None + + self._restore_state() + + def _set_tab(self, tab_id: TabID) -> None: + # pylint: disable=too-many-locals + + if self._current_tab == tab_id: + return + self._current_tab = tab_id + + # Preserve our current tab between runs. + cfg = ba.app.config + cfg['Watch Tab'] = tab_id.value + cfg.commit() + + # Update tab colors based on which is selected. + # tabs.update_tab_button_colors(self._tab_buttons, tab) + self._tab_row.update_appearance(tab_id) + + if self._tab_container: + self._tab_container.delete() + scroll_left = (self._width - self._scroll_width) * 0.5 + scroll_bottom = self._height - self._scroll_height - 79 - 48 + + # A place where tabs can store data to get cleared when + # switching to a different tab + self._tab_data = {} + + uiscale = ba.app.ui.uiscale + if tab_id is self.TabID.MY_REPLAYS: + c_width = self._scroll_width + c_height = self._scroll_height - 20 + sub_scroll_height = c_height - 63 + self._my_replays_scroll_width = sub_scroll_width = ( + 680 if uiscale is ba.UIScale.SMALL else 640) + + self._tab_container = cnt = ba.containerwidget( + parent=self._root_widget, + position=(scroll_left, scroll_bottom + + (self._scroll_height - c_height) * 0.5), + size=(c_width, c_height), + background=False, + selection_loops_to_parent=True) + + v = c_height - 30 + ba.textwidget(parent=cnt, + position=(c_width * 0.5, v), + color=(0.6, 1.0, 0.6), + scale=0.7, + size=(0, 0), + maxwidth=c_width * 0.9, + h_align='center', + v_align='center', + text=ba.Lstr( + resource='replayRenameWarningText', + subs=[('${REPLAY}', + ba.Lstr(resource='replayNameDefaultText')) + ])) + + b_width = 140 if uiscale is ba.UIScale.SMALL else 178 + b_height = (107 if uiscale is ba.UIScale.SMALL else + 142 if uiscale is ba.UIScale.MEDIUM else 190) + b_space_extra = (0 if uiscale is ba.UIScale.SMALL else + -2 if uiscale is ba.UIScale.MEDIUM else -5) + + b_color = (0.6, 0.53, 0.63) + b_textcolor = (0.75, 0.7, 0.8) + btnv = (c_height - (48 if uiscale is ba.UIScale.SMALL else + 45 if uiscale is ba.UIScale.MEDIUM else 40) - + b_height) + btnh = 40 if uiscale is ba.UIScale.SMALL else 40 + smlh = 190 if uiscale is ba.UIScale.SMALL else 225 + tscl = 1.0 if uiscale is ba.UIScale.SMALL else 1.2 + self._my_replays_watch_replay_button = btn1 = ba.buttonwidget( + parent=cnt, + size=(b_width, b_height), + position=(btnh, btnv), + button_type='square', + color=b_color, + textcolor=b_textcolor, + on_activate_call=self._on_my_replay_play_press, + text_scale=tscl, + label=ba.Lstr(resource=self._r + '.watchReplayButtonText'), + autoselect=True) + ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button) + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + ba.widget(edit=btn1, + left_widget=_ba.get_special_widget('back_button')) + btnv -= b_height + b_space_extra + ba.buttonwidget(parent=cnt, + size=(b_width, b_height), + position=(btnh, btnv), + button_type='square', + color=b_color, + textcolor=b_textcolor, + on_activate_call=self._on_my_replay_rename_press, + text_scale=tscl, + label=ba.Lstr(resource=self._r + + '.renameReplayButtonText'), + autoselect=True) + btnv -= b_height + b_space_extra + ba.buttonwidget(parent=cnt, + size=(b_width, b_height), + position=(btnh, btnv), + button_type='square', + color=b_color, + textcolor=b_textcolor, + on_activate_call=self._on_my_replay_delete_press, + text_scale=tscl, + label=ba.Lstr(resource=self._r + + '.deleteReplayButtonText'), + autoselect=True) + + v -= sub_scroll_height + 23 + self._scrollwidget = scrlw = ba.scrollwidget( + parent=cnt, + position=(smlh, v), + size=(sub_scroll_width, sub_scroll_height)) + ba.containerwidget(edit=cnt, selected_child=scrlw) + self._columnwidget = ba.columnwidget(parent=scrlw, + left_border=10, + border=2, + margin=0) + + ba.widget(edit=scrlw, + autoselect=True, + left_widget=btn1, + up_widget=self._tab_row.tabs[tab_id].button) + ba.widget(edit=self._tab_row.tabs[tab_id].button, + down_widget=scrlw) + + self._my_replay_selected = None + self._refresh_my_replays() + + def _no_replay_selected_error(self) -> None: + ba.screenmessage(ba.Lstr(resource=self._r + + '.noReplaySelectedErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + + def _on_my_replay_play_press(self) -> None: + if self._my_replay_selected is None: + self._no_replay_selected_error() + return + _ba.increment_analytics_count('Replay watch') + + def do_it() -> None: + try: + # Reset to normal speed. + _ba.set_replay_speed_exponent(0) + _ba.fade_screen(True) + assert self._my_replay_selected is not None + _ba.new_replay_session(_ba.get_replays_dir() + '/' + + self._my_replay_selected) + except Exception: + ba.print_exception('Error running replay session.') + + # Drop back into a fresh main menu session + # in case we half-launched or something. + from bastd import mainmenu + _ba.new_host_session(mainmenu.MainMenuSession) + + _ba.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it)) + ba.containerwidget(edit=self._root_widget, transition='out_left') + + def _on_my_replay_rename_press(self) -> None: + if self._my_replay_selected is None: + self._no_replay_selected_error() + return + c_width = 600 + c_height = 250 + uiscale = ba.app.ui.uiscale + self._my_replays_rename_window = cnt = ba.containerwidget( + scale=(1.8 if uiscale is ba.UIScale.SMALL else + 1.55 if uiscale is ba.UIScale.MEDIUM else 1.0), + size=(c_width, c_height), + transition='in_scale') + dname = self._get_replay_display_name(self._my_replay_selected) + ba.textwidget(parent=cnt, + size=(0, 0), + h_align='center', + v_align='center', + text=ba.Lstr(resource=self._r + '.renameReplayText', + subs=[('${REPLAY}', dname)]), + maxwidth=c_width * 0.8, + position=(c_width * 0.5, c_height - 60)) + self._my_replay_rename_text = txt = ba.textwidget( + parent=cnt, + size=(c_width * 0.8, 40), + h_align='left', + v_align='center', + text=dname, + editable=True, + description=ba.Lstr(resource=self._r + '.replayNameText'), + position=(c_width * 0.1, c_height - 140), + autoselect=True, + maxwidth=c_width * 0.7, + max_chars=200) + cbtn = ba.buttonwidget( + parent=cnt, + label=ba.Lstr(resource='cancelText'), + on_activate_call=ba.Call( + lambda c: ba.containerwidget(edit=c, transition='out_scale'), + cnt), + size=(180, 60), + position=(30, 30), + autoselect=True) + okb = ba.buttonwidget(parent=cnt, + label=ba.Lstr(resource=self._r + '.renameText'), + size=(180, 60), + position=(c_width - 230, 30), + on_activate_call=ba.Call( + self._rename_my_replay, + self._my_replay_selected), + autoselect=True) + ba.widget(edit=cbtn, right_widget=okb) + ba.widget(edit=okb, left_widget=cbtn) + ba.textwidget(edit=txt, on_return_press_call=okb.activate) + ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) + + def _rename_my_replay(self, replay: str) -> None: + new_name = None + try: + if not self._my_replay_rename_text: + return + new_name_raw = cast( + str, ba.textwidget(query=self._my_replay_rename_text)) + new_name = new_name_raw + '.brp' + + # Ignore attempts to change it to what it already is + # (or what it looks like to the user). + if (replay != new_name + and self._get_replay_display_name(replay) != new_name_raw): + old_name_full = (_ba.get_replays_dir() + '/' + + replay).encode('utf-8') + new_name_full = (_ba.get_replays_dir() + '/' + + new_name).encode('utf-8') + # False alarm; ba.textwidget can return non-None val. + # pylint: disable=unsupported-membership-test + if os.path.exists(new_name_full): + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource=self._r + + '.replayRenameErrorAlreadyExistsText'), + color=(1, 0, 0)) + elif any(char in new_name_raw for char in ['/', '\\', ':']): + ba.playsound(ba.getsound('error')) + ba.screenmessage(ba.Lstr(resource=self._r + + '.replayRenameErrorInvalidName'), + color=(1, 0, 0)) + else: + _ba.increment_analytics_count('Replay rename') + os.rename(old_name_full, new_name_full) + self._refresh_my_replays() + ba.playsound(ba.getsound('gunCocking')) + except Exception: + ba.print_exception( + f"Error renaming replay '{replay}' to '{new_name}'.") + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource=self._r + '.replayRenameErrorText'), + color=(1, 0, 0), + ) + + ba.containerwidget(edit=self._my_replays_rename_window, + transition='out_scale') + + def _on_my_replay_delete_press(self) -> None: + from bastd.ui import confirm + if self._my_replay_selected is None: + self._no_replay_selected_error() + return + confirm.ConfirmWindow( + ba.Lstr(resource=self._r + '.deleteConfirmText', + subs=[('${REPLAY}', + self._get_replay_display_name( + self._my_replay_selected))]), + ba.Call(self._delete_replay, self._my_replay_selected), 450, 150) + + def _get_replay_display_name(self, replay: str) -> str: + if replay.endswith('.brp'): + replay = replay[:-4] + if replay == '__lastReplay': + return ba.Lstr(resource='replayNameDefaultText').evaluate() + return replay + + def _delete_replay(self, replay: str) -> None: + try: + _ba.increment_analytics_count('Replay delete') + os.remove((_ba.get_replays_dir() + '/' + replay).encode('utf-8')) + self._refresh_my_replays() + ba.playsound(ba.getsound('shieldDown')) + if replay == self._my_replay_selected: + self._my_replay_selected = None + except Exception: + ba.print_exception(f"Error deleting replay '{replay}'.") + ba.playsound(ba.getsound('error')) + ba.screenmessage( + ba.Lstr(resource=self._r + '.replayDeleteErrorText'), + color=(1, 0, 0), + ) + + def _on_my_replay_select(self, replay: str) -> None: + self._my_replay_selected = replay + + def _refresh_my_replays(self) -> None: + assert self._columnwidget is not None + for child in self._columnwidget.get_children(): + child.delete() + t_scale = 1.6 + try: + names = os.listdir(_ba.get_replays_dir()) + + # Ignore random other files in there. + names = [n for n in names if n.endswith('.brp')] + names.sort(key=lambda x: x.lower()) + except Exception: + ba.print_exception('Error listing replays dir.') + names = [] + + assert self._my_replays_scroll_width is not None + assert self._my_replays_watch_replay_button is not None + for i, name in enumerate(names): + txt = ba.textwidget( + parent=self._columnwidget, + size=(self._my_replays_scroll_width / t_scale, 30), + selectable=True, + color=(1.0, 1, 0.4) if name == '__lastReplay.brp' else + (1, 1, 1), + always_highlight=True, + on_select_call=ba.Call(self._on_my_replay_select, name), + on_activate_call=self._my_replays_watch_replay_button.activate, + text=self._get_replay_display_name(name), + h_align='left', + v_align='center', + corner_scale=t_scale, + maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93) + if i == 0: + ba.widget( + edit=txt, + up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button) + + def _save_state(self) -> None: + try: + sel = self._root_widget.get_selected_child() + selected_tab_ids = [ + tab_id for tab_id, tab in self._tab_row.tabs.items() + if sel == tab.button + ] + if sel == self._back_button: + sel_name = 'Back' + elif selected_tab_ids: + assert len(selected_tab_ids) == 1 + sel_name = f'Tab:{selected_tab_ids[0].value}' + elif sel == self._tab_container: + sel_name = 'TabContainer' + else: + raise ValueError(f'unrecognized selection {sel}') + ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} + except Exception: + ba.print_exception(f'Error saving state for {self}.') + + def _restore_state(self) -> None: + from efro.util import enum_by_value + try: + sel: Optional[ba.Widget] + sel_name = ba.app.ui.window_states.get(type(self), + {}).get('sel_name') + assert isinstance(sel_name, (str, type(None))) + try: + current_tab = enum_by_value(self.TabID, + ba.app.config.get('Watch Tab')) + except ValueError: + current_tab = self.TabID.MY_REPLAYS + self._set_tab(current_tab) + + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'TabContainer': + sel = self._tab_container + elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): + try: + sel_tab_id = enum_by_value(self.TabID, + sel_name.split(':')[-1]) + except ValueError: + sel_tab_id = self.TabID.MY_REPLAYS + sel = self._tab_row.tabs[sel_tab_id].button + else: + if self._tab_container is not None: + sel = self._tab_container + else: + sel = self._tab_row.tabs[current_tab].button + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') + + def _back(self) -> None: + from bastd.ui.mainmenu import MainMenuWindow + self._save_state() + ba.containerwidget(edit=self._root_widget, + transition=self._transition_out) + ba.app.ui.set_main_menu_window( + MainMenuWindow(transition='in_left').get_root_widget()) diff --git a/dist/ba_data/python/efro/__init__.py b/dist/ba_data/python/efro/__init__.py new file mode 100644 index 0000000..1f50d61 --- /dev/null +++ b/dist/ba_data/python/efro/__init__.py @@ -0,0 +1,7 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Common bits of functionality shared between all efro projects. + +Things in here should be hardened, highly type-safe, and well-covered by unit +tests since they are widely used in live client and server code. +""" diff --git a/dist/ba_data/python/efro/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7a637d4 Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/__init__.cpython-38.pyc b/dist/ba_data/python/efro/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..9fd2721 Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/__init__.cpython-38.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/call.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/__pycache__/call.cpython-38.opt-1.pyc new file mode 100644 index 0000000..8b7f08a Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/call.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/dataclasses.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/__pycache__/dataclasses.cpython-38.opt-1.pyc new file mode 100644 index 0000000..236b9c0 Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/dataclasses.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/dataclasses.cpython-38.pyc b/dist/ba_data/python/efro/__pycache__/dataclasses.cpython-38.pyc new file mode 100644 index 0000000..87fc11e Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/dataclasses.cpython-38.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/error.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/__pycache__/error.cpython-38.opt-1.pyc new file mode 100644 index 0000000..95d2650 Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/error.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/error.cpython-38.pyc b/dist/ba_data/python/efro/__pycache__/error.cpython-38.pyc new file mode 100644 index 0000000..e178660 Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/error.cpython-38.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/json.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/__pycache__/json.cpython-38.opt-1.pyc new file mode 100644 index 0000000..1ac9e15 Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/json.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/terminal.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/__pycache__/terminal.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a0bde9a Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/terminal.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/terminal.cpython-38.pyc b/dist/ba_data/python/efro/__pycache__/terminal.cpython-38.pyc new file mode 100644 index 0000000..99a9d50 Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/terminal.cpython-38.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/util.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/__pycache__/util.cpython-38.opt-1.pyc new file mode 100644 index 0000000..52074fa Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/util.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/__pycache__/util.cpython-38.pyc b/dist/ba_data/python/efro/__pycache__/util.cpython-38.pyc new file mode 100644 index 0000000..769f1da Binary files /dev/null and b/dist/ba_data/python/efro/__pycache__/util.cpython-38.pyc differ diff --git a/dist/ba_data/python/efro/call.py b/dist/ba_data/python/efro/call.py new file mode 100644 index 0000000..e83f22a --- /dev/null +++ b/dist/ba_data/python/efro/call.py @@ -0,0 +1,268 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Call related functionality shared between all efro components.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar, Generic, Callable, cast + +if TYPE_CHECKING: + from typing import Any, overload + +CT = TypeVar('CT', bound=Callable) + + +class _CallbackCall(Generic[CT]): + """Descriptor for exposing a call with a type defined by a TypeVar.""" + + def __get__(self, obj: Any, type_in: Any = None) -> CT: + return cast(CT, None) + + +class CallbackSet(Generic[CT]): + """Wrangles callbacks for a particular event in a type-safe manner.""" + + # In the type-checker's eyes, our 'run' attr is a CallbackCall which + # returns a callable with the type we were created with. This lets us + # type-check our run calls. (Is there another way to expose a function + # with a signature defined by a generic?..) + # At runtime, run() simply passes its args verbatim to its registered + # callbacks; no types are checked. + if TYPE_CHECKING: + run: _CallbackCall[CT] = _CallbackCall() + else: + + def run(self, *args, **keywds): + """Run all callbacks.""" + print('HELLO FROM RUN', *args, **keywds) + + def __init__(self) -> None: + print('CallbackSet()') + + def __del__(self) -> None: + print('~CallbackSet()') + + def add(self, call: CT) -> None: + """Add a callback to be run.""" + print('Would add call', call) + + +# Define Call() which can be used in type-checking call-wrappers that behave +# similarly to functools.partial (in that they take a callable and some +# positional arguments to be passed to it). + +# In type-checking land, We define several different _CallXArg classes +# corresponding to different argument counts and define Call() as an +# overloaded function which returns one of them based on how many args are +# passed. + +# To use this, simply assign your call type to this Call for type checking: +# Example: +# class _MyCallWrapper: +# +# if TYPE_CHECKING: +# MyCallWrapper = efro.call.Call +# else: +# MyCallWrapper = _MyCallWrapper + +# Note that this setup currently only works with positional arguments; if you +# would like to pass args via keyword you can wrap a lambda or local function +# which takes keyword args and converts to a call containing keywords. + +if TYPE_CHECKING: + In1T = TypeVar('In1T') + In2T = TypeVar('In2T') + In3T = TypeVar('In3T') + In4T = TypeVar('In4T') + In5T = TypeVar('In5T') + In6T = TypeVar('In6T') + In7T = TypeVar('In7T') + OutT = TypeVar('OutT') + + class _CallNoArgs(Generic[OutT]): + """Single argument variant of call wrapper.""" + + def __init__(self, _call: Callable[[], OutT]): + ... + + def __call__(self) -> OutT: + ... + + class _Call1Arg(Generic[In1T, OutT]): + """Single argument variant of call wrapper.""" + + def __init__(self, _call: Callable[[In1T], OutT]): + ... + + def __call__(self, _arg1: In1T) -> OutT: + ... + + class _Call2Args(Generic[In1T, In2T, OutT]): + """Two argument variant of call wrapper""" + + def __init__(self, _call: Callable[[In1T, In2T], OutT]): + ... + + def __call__(self, _arg1: In1T, _arg2: In2T) -> OutT: + ... + + class _Call3Args(Generic[In1T, In2T, In3T, OutT]): + """Three argument variant of call wrapper""" + + def __init__(self, _call: Callable[[In1T, In2T, In3T], OutT]): + ... + + def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T) -> OutT: + ... + + class _Call4Args(Generic[In1T, In2T, In3T, In4T, OutT]): + """Four argument variant of call wrapper""" + + def __init__(self, _call: Callable[[In1T, In2T, In3T, In4T], OutT]): + ... + + def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T, + _arg4: In4T) -> OutT: + ... + + class _Call5Args(Generic[In1T, In2T, In3T, In4T, In5T, OutT]): + """Five argument variant of call wrapper""" + + def __init__(self, _call: Callable[[In1T, In2T, In3T, In4T, In5T], + OutT]): + ... + + def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T, _arg4: In4T, + _arg5: In5T) -> OutT: + ... + + class _Call6Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, OutT]): + """Six argument variant of call wrapper""" + + def __init__(self, + _call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T], + OutT]): + ... + + def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T, _arg4: In4T, + _arg5: In5T, _arg6: In6T) -> OutT: + ... + + class _Call7Args(Generic[In1T, In2T, In3T, In4T, In5T, In6T, In7T, OutT]): + """Seven argument variant of call wrapper""" + + def __init__( + self, + _call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T, In7T], + OutT]): + ... + + def __call__(self, _arg1: In1T, _arg2: In2T, _arg3: In3T, _arg4: In4T, + _arg5: In5T, _arg6: In6T, _arg7: In7T) -> OutT: + ... + + # No arg call; no args bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[], OutT]) -> _CallNoArgs[OutT]: + ... + + # 1 arg call; 1 arg bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T], OutT], arg1: In1T) -> _CallNoArgs[OutT]: + ... + + # 1 arg call; no args bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T], OutT]) -> _Call1Arg[In1T, OutT]: + ... + + # 2 arg call; 2 args bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T, In2T], OutT], arg1: In1T, + arg2: In2T) -> _CallNoArgs[OutT]: + ... + + # 2 arg call; 1 arg bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T, In2T], OutT], + arg1: In1T) -> _Call1Arg[In2T, OutT]: + ... + + # 2 arg call; no args bundled. + # noinspection PyPep8Naming + @overload + def Call( + call: Callable[[In1T, In2T], + OutT]) -> _Call2Args[In1T, In2T, OutT]: + ... + + # 3 arg call; 3 args bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T, arg2: In2T, + arg3: In3T) -> _CallNoArgs[OutT]: + ... + + # 3 arg call; 2 args bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T, In2T, In3T], OutT], arg1: In1T, + arg2: In2T) -> _Call1Arg[In3T, OutT]: + ... + + # 3 arg call; 1 arg bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T, In2T, In3T], OutT], + arg1: In1T) -> _Call2Args[In2T, In3T, OutT]: + ... + + # 3 arg call; no args bundled. + # noinspection PyPep8Naming + @overload + def Call( + call: Callable[[In1T, In2T, In3T], OutT] + ) -> _Call3Args[In1T, In2T, In3T, OutT]: + ... + + # 4 arg call; 4 args bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T, In2T, In3T, In4T], OutT], arg1: In1T, + arg2: In2T, arg3: In3T, arg4: In4T) -> _CallNoArgs[OutT]: + ... + + # 5 arg call; 5 args bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T, In2T, In3T, In4T, In5T], + OutT], arg1: In1T, arg2: In2T, arg3: In3T, + arg4: In4T, arg5: In5T) -> _CallNoArgs[OutT]: + ... + + # 6 arg call; 6 args bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T], + OutT], arg1: In1T, arg2: In2T, arg3: In3T, + arg4: In4T, arg5: In5T, arg6: In6T) -> _CallNoArgs[OutT]: + ... + + # 7 arg call; 7 args bundled. + # noinspection PyPep8Naming + @overload + def Call(call: Callable[[In1T, In2T, In3T, In4T, In5T, In6T, In7T], OutT], + arg1: In1T, arg2: In2T, arg3: In3T, arg4: In4T, arg5: In5T, + arg6: In6T, arg7: In7T) -> _CallNoArgs[OutT]: + ... + + # noinspection PyPep8Naming + def Call(*_args: Any, **_keywds: Any) -> Any: + ... + + Call = Call diff --git a/dist/ba_data/python/efro/dataclasses.py b/dist/ba_data/python/efro/dataclasses.py new file mode 100644 index 0000000..7c29fbd --- /dev/null +++ b/dist/ba_data/python/efro/dataclasses.py @@ -0,0 +1,295 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Custom functionality for dealing with dataclasses.""" +# Note: We do lots of comparing of exact types here which is normally +# frowned upon (stuff like isinstance() is usually encouraged). +# pylint: disable=unidiomatic-typecheck + +from __future__ import annotations + +import dataclasses +import inspect +from enum import Enum +from typing import TYPE_CHECKING, TypeVar, Generic + +from efro.util import enum_by_value + +if TYPE_CHECKING: + from typing import Any, Dict, Type, Tuple, Optional + +T = TypeVar('T') + +SIMPLE_NAMES_TO_TYPES: Dict[str, Type] = { + 'int': int, + 'bool': bool, + 'str': str, + 'float': float, +} +SIMPLE_TYPES_TO_NAMES = {tp: nm for nm, tp in SIMPLE_NAMES_TO_TYPES.items()} + + +def dataclass_to_dict(obj: Any, coerce_to_float: bool = True) -> dict: + """Given a dataclass object, emit a json-friendly dict. + + All values will be checked to ensure they match the types specified + on fields. Note that only a limited set of types is supported. + + If coerce_to_float is True, integer values present on float typed fields + will be converted to floats in the dict output. If False, a TypeError + will be triggered. + """ + + out = _Outputter(obj, create=True, coerce_to_float=coerce_to_float).run() + assert isinstance(out, dict) + return out + + +def dataclass_from_dict(cls: Type[T], + values: dict, + coerce_to_float: bool = True) -> T: + """Given a dict, instantiates a dataclass of the given type. + + The dict must be in the json-friendly format as emitted from + dataclass_to_dict. This means that sequence values such as tuples or + sets should be passed as lists, enums should be passed as their + associated values, and nested dataclasses should be passed as dicts. + + If coerce_to_float is True, int values passed for float typed fields + will be converted to float values. Otherwise a TypeError is raised. + """ + return _Inputter(cls, coerce_to_float=coerce_to_float).run(values) + + +def dataclass_validate(obj: Any, coerce_to_float: bool = True) -> None: + """Ensure that current values in a dataclass are the correct types.""" + _Outputter(obj, create=False, coerce_to_float=coerce_to_float).run() + + +def _field_type_str(cls: Type, field: dataclasses.Field) -> str: + # We expect to be operating under 'from __future__ import annotations' + # so field types should always be strings for us; not actual types. + # (Can pull this check out once we get to Python 3.10) + typestr: str = field.type # type: ignore + + if not isinstance(typestr, str): + raise RuntimeError( + f'Dataclass {cls.__name__} seems to have' + f' been created without "from __future__ import annotations";' + f' those dataclasses are unsupported here.') + return typestr + + +def _raise_type_error(fieldpath: str, valuetype: Type, + expected: Tuple[Type, ...]) -> None: + """Raise an error when a field value's type does not match expected.""" + assert isinstance(expected, tuple) + assert all(isinstance(e, type) for e in expected) + if len(expected) == 1: + expected_str = expected[0].__name__ + else: + names = ', '.join(t.__name__ for t in expected) + expected_str = f'Union[{names}]' + raise TypeError(f'Invalid value type for "{fieldpath}";' + f' expected "{expected_str}", got' + f' "{valuetype.__name__}".') + + +class _Outputter: + + def __init__(self, obj: Any, create: bool, coerce_to_float: bool) -> None: + self._obj = obj + self._create = create + self._coerce_to_float = coerce_to_float + + def run(self) -> Any: + """Do the thing.""" + return self._dataclass_to_output(self._obj, '') + + def _value_to_output(self, fieldpath: str, typestr: str, + value: Any) -> Any: + # pylint: disable=too-many-return-statements + # pylint: disable=too-many-branches + + # For simple flat types, look for exact matches: + simpletype = SIMPLE_NAMES_TO_TYPES.get(typestr) + if simpletype is not None: + if type(value) is not simpletype: + # Special case: if they want to coerce ints to floats, do so. + if (self._coerce_to_float and simpletype is float + and type(value) is int): + return float(value) if self._create else None + _raise_type_error(fieldpath, type(value), (simpletype, )) + return value + + if typestr.startswith('Optional[') and typestr.endswith(']'): + subtypestr = typestr[9:-1] + # Handle the 'None' case special and do the default otherwise. + if value is None: + return None + return self._value_to_output(fieldpath, subtypestr, value) + + if typestr.startswith('List[') and typestr.endswith(']'): + subtypestr = typestr[5:-1] + if not isinstance(value, list): + raise TypeError(f'Expected a list for {fieldpath};' + f' found a {type(value)}') + if self._create: + return [ + self._value_to_output(fieldpath, subtypestr, x) + for x in value + ] + for x in value: + self._value_to_output(fieldpath, subtypestr, x) + return None + + if typestr.startswith('Set[') and typestr.endswith(']'): + subtypestr = typestr[4:-1] + if not isinstance(value, set): + raise TypeError(f'Expected a set for {fieldpath};' + f' found a {type(value)}') + if self._create: + # Note: we output json-friendly values so this becomes a list. + return [ + self._value_to_output(fieldpath, subtypestr, x) + for x in value + ] + for x in value: + self._value_to_output(fieldpath, subtypestr, x) + return None + + if dataclasses.is_dataclass(value): + return self._dataclass_to_output(value, fieldpath) + + if isinstance(value, Enum): + enumvalue = value.value + if type(enumvalue) not in SIMPLE_TYPES_TO_NAMES: + raise TypeError(f'Invalid enum value type {type(enumvalue)}' + f' for "{fieldpath}".') + return enumvalue + + raise TypeError( + f"Field '{fieldpath}' of type '{typestr}' is unsupported here.") + + def _dataclass_to_output(self, obj: Any, fieldpath: str) -> Any: + if not dataclasses.is_dataclass(obj): + raise TypeError(f'Passed obj {obj} is not a dataclass.') + fields = dataclasses.fields(obj) + out: Optional[Dict[str, Any]] = {} if self._create else None + + for field in fields: + fieldname = field.name + + if fieldpath: + subfieldpath = f'{fieldpath}.{fieldname}' + else: + subfieldpath = fieldname + typestr = _field_type_str(type(obj), field) + value = getattr(obj, fieldname) + outvalue = self._value_to_output(subfieldpath, typestr, value) + if self._create: + assert out is not None + out[fieldname] = outvalue + + return out + + +class _Inputter(Generic[T]): + + def __init__(self, cls: Type[T], coerce_to_float: bool): + self._cls = cls + self._coerce_to_float = coerce_to_float + + def run(self, values: dict) -> T: + """Do the thing.""" + return self._dataclass_from_input( # type: ignore + self._cls, '', values) + + def _value_from_input(self, cls: Type, fieldpath: str, typestr: str, + value: Any) -> Any: + """Convert an assigned value to what a dataclass field expects.""" + # pylint: disable=too-many-return-statements + + simpletype = SIMPLE_NAMES_TO_TYPES.get(typestr) + if simpletype is not None: + if type(value) is not simpletype: + # Special case: if they want to coerce ints to floats, do so. + if (self._coerce_to_float and simpletype is float + and type(value) is int): + return float(value) + _raise_type_error(fieldpath, type(value), (simpletype, )) + return value + if typestr.startswith('List[') and typestr.endswith(']'): + return self._sequence_from_input(cls, fieldpath, typestr, value, + 'List', list) + if typestr.startswith('Set[') and typestr.endswith(']'): + return self._sequence_from_input(cls, fieldpath, typestr, value, + 'Set', set) + if typestr.startswith('Optional[') and typestr.endswith(']'): + subtypestr = typestr[9:-1] + # Handle the 'None' case special and do the default + # thing otherwise. + if value is None: + return None + return self._value_from_input(cls, fieldpath, subtypestr, value) + + # Ok, its not a builtin type. It might be an enum or nested dataclass. + cls2 = getattr(inspect.getmodule(cls), typestr, None) + if cls2 is None: + raise RuntimeError(f"Unable to resolve '{typestr}'" + f" used by class '{cls.__name__}';" + f' make sure all nested types are declared' + f' in the global namespace of the module where' + f" '{cls.__name__} is defined.") + + if dataclasses.is_dataclass(cls2): + return self._dataclass_from_input(cls2, fieldpath, value) + + if issubclass(cls2, Enum): + return enum_by_value(cls2, value) + + raise TypeError( + f"Field '{fieldpath}' of type '{typestr}' is unsupported here.") + + def _dataclass_from_input(self, cls: Type, fieldpath: str, + values: dict) -> Any: + """Given a dict, instantiates a dataclass of the given type. + + The dict must be in the json-friendly format as emitted from + dataclass_to_dict. This means that sequence values such as tuples or + sets should be passed as lists, enums should be passed as their + associated values, and nested dataclasses should be passed as dicts. + """ + if not dataclasses.is_dataclass(cls): + raise TypeError(f'Passed class {cls} is not a dataclass.') + if not isinstance(values, dict): + raise TypeError("Expected a dict for 'values' arg.") + + # noinspection PyDataclass + fields = dataclasses.fields(cls) + fields_by_name = {f.name: f for f in fields} + args: Dict[str, Any] = {} + for key, value in values.items(): + field = fields_by_name.get(key) + if field is None: + raise AttributeError(f"'{cls.__name__}' has no '{key}' field.") + + typestr = _field_type_str(cls, field) + + subfieldpath = (f'{fieldpath}.{field.name}' + if fieldpath else field.name) + args[key] = self._value_from_input(cls, subfieldpath, typestr, + value) + + return cls(**args) + + def _sequence_from_input(self, cls: Type, fieldpath: str, typestr: str, + value: Any, seqtypestr: str, + seqtype: Type) -> Any: + # Because we are json-centric, we expect a list for all sequences. + if type(value) is not list: + raise TypeError(f'Invalid input value for "{fieldpath}";' + f' expected a list, got a {type(value).__name__}') + subtypestr = typestr[len(seqtypestr) + 1:-1] + return seqtype( + self._value_from_input(cls, fieldpath, subtypestr, i) + for i in value) diff --git a/dist/ba_data/python/efro/entity/__init__.py b/dist/ba_data/python/efro/entity/__init__.py new file mode 100644 index 0000000..33aea85 --- /dev/null +++ b/dist/ba_data/python/efro/entity/__init__.py @@ -0,0 +1,36 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Entity functionality. + +A system for defining structured data, supporting both static and runtime +type safety, serialization, efficient/sparse storage, per-field value +limits, etc. This is a heavyweight option in comparison to things such as +dataclasses, but the increased features can make the overhead worth it for +certain use cases. + +Advantages compared to nested dataclasses: +- Field names separated from their data representation so can get more + concise json data, change variable names while preserving back-compat, etc. +- Can wrap and preserve unmapped data (so fields can be added to new versions + of something without breaking old versions' ability to read the data) +- Incorrectly typed data is caught at runtime (for dataclasses we rely on + type-checking and explicit validation calls) + +Disadvantages compared to nested dataclasses: +- More complex to use +- Significantly more heavyweight (roughly 10 times slower in quick tests) +- Can't currently be initialized in constructors (this would probably require + a Mypy plugin to do in a type-safe way) +""" +# pylint: disable=unused-import + +from efro.entity._entity import EntityMixin, Entity +from efro.entity._field import (Field, CompoundField, ListField, DictField, + CompoundListField, CompoundDictField) +from efro.entity._value import ( + EnumValue, OptionalEnumValue, IntValue, OptionalIntValue, StringValue, + OptionalStringValue, BoolValue, OptionalBoolValue, FloatValue, + OptionalFloatValue, DateTimeValue, OptionalDateTimeValue, Float3Value, + CompoundValue) + +from efro.entity._support import FieldInspector diff --git a/dist/ba_data/python/efro/entity/__pycache__/__init__.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/entity/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f52d394 Binary files /dev/null and b/dist/ba_data/python/efro/entity/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/entity/__pycache__/_base.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/entity/__pycache__/_base.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7275817 Binary files /dev/null and b/dist/ba_data/python/efro/entity/__pycache__/_base.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/entity/__pycache__/_entity.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/entity/__pycache__/_entity.cpython-38.opt-1.pyc new file mode 100644 index 0000000..0a604e6 Binary files /dev/null and b/dist/ba_data/python/efro/entity/__pycache__/_entity.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/entity/__pycache__/_field.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/entity/__pycache__/_field.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c573aaf Binary files /dev/null and b/dist/ba_data/python/efro/entity/__pycache__/_field.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/entity/__pycache__/_support.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/entity/__pycache__/_support.cpython-38.opt-1.pyc new file mode 100644 index 0000000..2c0cd1e Binary files /dev/null and b/dist/ba_data/python/efro/entity/__pycache__/_support.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/entity/__pycache__/_value.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/entity/__pycache__/_value.cpython-38.opt-1.pyc new file mode 100644 index 0000000..3d7e945 Binary files /dev/null and b/dist/ba_data/python/efro/entity/__pycache__/_value.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/entity/__pycache__/util.cpython-38.opt-1.pyc b/dist/ba_data/python/efro/entity/__pycache__/util.cpython-38.opt-1.pyc new file mode 100644 index 0000000..3ae29ce Binary files /dev/null and b/dist/ba_data/python/efro/entity/__pycache__/util.cpython-38.opt-1.pyc differ diff --git a/dist/ba_data/python/efro/entity/_base.py b/dist/ba_data/python/efro/entity/_base.py new file mode 100644 index 0000000..9c6c7bd --- /dev/null +++ b/dist/ba_data/python/efro/entity/_base.py @@ -0,0 +1,121 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Base classes for the entity system.""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Type + + +def dict_key_to_raw(key: Any, keytype: Type) -> Any: + """Given a key value from the world, filter to stored key.""" + if not isinstance(key, keytype): + raise TypeError( + f'Invalid key type; expected {keytype}, got {type(key)}.') + if issubclass(keytype, Enum): + return key.value + return key + + +def dict_key_from_raw(key: Any, keytype: Type) -> Any: + """Given internal key, filter to world visible type.""" + if issubclass(keytype, Enum): + return keytype(key) + return key + + +class DataHandler: + """Base class for anything that can wrangle entity data. + + This contains common functionality shared by Fields and Values. + """ + + def get_default_data(self) -> Any: + """Return the default internal data value for this object. + + This will be inserted when initing nonexistent entity data. + """ + raise RuntimeError(f'get_default_data() unimplemented for {self}') + + def filter_input(self, data: Any, error: bool) -> Any: + """Given arbitrary input data, return valid internal data. + + If error is True, exceptions should be thrown for any non-trivial + mismatch (more than just int vs float/etc.). Otherwise the invalid + data should be replaced with valid defaults and the problem noted + via the logging module. + The passed-in data can be modified in-place or returned as-is, or + completely new data can be returned. Compound types are responsible + for setting defaults and/or calling this recursively for their + children. Data that is not used by the field (such as orphaned values + in a dict field) can be left alone. + + Supported types for internal data are: + - anything that works with json (lists, dicts, bools, floats, ints, + strings, None) - no tuples! + - datetime.datetime objects + """ + del error # unused + return data + + def filter_output(self, data: Any) -> Any: + """Given valid internal data, return user-facing data. + + Note that entity data is expected to be filtered to correctness on + input, so if internal and extra entity data are the same type + Value types such as Vec3 may store data internally as simple float + tuples but return Vec3 objects to the user/etc. this is the mechanism + by which they do so. + """ + return data + + def prune_data(self, data: Any) -> bool: + """Prune internal data to strip out default values/etc. + + Should return a bool indicating whether root data itself can be pruned. + The object is responsible for pruning any sub-fields before returning. + """ + + +class BaseField(DataHandler): + """Base class for all field types.""" + + def __init__(self, d_key: str = None) -> None: + + # Key for this field's data in parent dict/list (when applicable; + # some fields such as the child field under a list field represent + # more than a single field entry so this is unused) + self.d_key = d_key + + # IMPORTANT: this method should only be overridden in the eyes of the + # type-checker (to specify exact return types). Subclasses should instead + # override get_with_data() for doing the actual work, since that method + # may sometimes be called explicitly instead of through __get__ + def __get__(self, obj: Any, type_in: Any = None) -> Any: + if obj is None: + # when called on the type, we return the field + return self + return self.get_with_data(obj.d_data) + + # IMPORTANT: same deal as __get__() (see note above) + def __set__(self, obj: Any, value: Any) -> None: + assert obj is not None + self.set_with_data(obj.d_data, value, error=True) + + def get_with_data(self, data: Any) -> Any: + """Get the field value given an explicit data source.""" + assert self.d_key is not None + return self.filter_output(data[self.d_key]) + + def set_with_data(self, data: Any, value: Any, error: bool) -> Any: + """Set the field value given an explicit data target. + + If error is True, exceptions should be thrown for invalid data; + otherwise the problem should be logged but corrected. + """ + assert self.d_key is not None + data[self.d_key] = self.filter_input(value, error=error) diff --git a/dist/ba_data/python/efro/entity/_entity.py b/dist/ba_data/python/efro/entity/_entity.py new file mode 100644 index 0000000..6a3d394 --- /dev/null +++ b/dist/ba_data/python/efro/entity/_entity.py @@ -0,0 +1,219 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality for the actual Entity types.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, TypeVar + +from efro.entity._support import FieldInspector, BoundCompoundValue +from efro.entity._value import CompoundValue +from efro.json import ExtendedJSONEncoder, ExtendedJSONDecoder + +if TYPE_CHECKING: + from typing import Dict, Any, Type, Union, Optional + +T = TypeVar('T', bound='EntityMixin') + + +class EntityMixin: + """Mixin class to add data-storage to CompoundValue, forming an Entity. + + Distinct Entity types should inherit from this first and a CompoundValue + (sub)type second. This order ensures that constructor arguments for this + class are accessible on the new type. + """ + + def __init__(self, + d_data: Dict[str, Any] = None, + error: bool = True) -> None: + super().__init__() + if not isinstance(self, CompoundValue): + raise RuntimeError('EntityMixin class must be combined' + ' with a CompoundValue class.') + + # Underlying data for this entity; fields simply operate on this. + self.d_data: Dict[str, Any] = {} + assert isinstance(self, EntityMixin) + self.set_data(d_data if d_data is not None else {}, error=error) + + def reset(self) -> None: + """Resets data to default.""" + self.set_data({}, error=True) + + def set_data(self, data: Dict, error: bool = True) -> None: + """Set the data for this entity and apply all value filters to it. + + Note that it is more efficient to pass data to an Entity's constructor + than it is to create a default Entity and then call this on it. + """ + self.d_data = data + assert isinstance(self, CompoundValue) + self.apply_fields_to_data(self.d_data, error=error) + + def copy_data(self, target: Union[CompoundValue, + BoundCompoundValue]) -> None: + """Copy data from a target Entity or compound-value. + + This first verifies that the target has a matching set of fields + and then copies its data into ourself. To copy data into a nested + compound field, the assignment operator can be used. + """ + import copy + from efro.entity.util import have_matching_fields + tvalue: CompoundValue + if isinstance(target, CompoundValue): + tvalue = target + elif isinstance(target, BoundCompoundValue): + tvalue = target.d_value + else: + raise TypeError( + 'Target must be a CompoundValue or BoundCompoundValue') + target_data = getattr(target, 'd_data', None) + if target_data is None: + raise ValueError('Target is not bound to data.') + assert isinstance(self, CompoundValue) + if not have_matching_fields(self, tvalue): + raise ValueError( + f'Fields for target {type(tvalue)} do not match ours' + f" ({type(self)}); can't copy data.") + self.d_data = copy.deepcopy(target_data) + + def steal_data(self, target: EntityMixin) -> None: + """Steal data from another entity. + + This is more efficient than copy_data, as data is moved instead + of copied. However this leaves the target object in an invalid + state, and it must no longer be used after this call. + This can be convenient for entities to use to update themselves + with the result of a database transaction (which generally return + fresh entities). + """ + from efro.entity.util import have_matching_fields + if not isinstance(target, EntityMixin): + raise TypeError('EntityMixin is required.') + assert isinstance(target, CompoundValue) + assert isinstance(self, CompoundValue) + if not have_matching_fields(self, target): + raise ValueError( + f'Fields for target {type(target)} do not match ours' + f" ({type(self)}); can't steal data.") + assert target.d_data is not None + self.d_data = target.d_data + + # Make sure target blows up if someone tries to use it. + # noinspection PyTypeHints + target.d_data = None # type: ignore + + def pruned_data(self) -> Dict[str, Any]: + """Return a pruned version of this instance's data. + + This varies from d_data in that values may be stripped out if + they are equal to defaults (for fields with that option enabled). + """ + import copy + data = copy.deepcopy(self.d_data) + assert isinstance(self, CompoundValue) + self.prune_fields_data(data) + return data + + def to_json_str(self, + prune: bool = True, + pretty: bool = False, + sort_keys_override: Optional[bool] = None) -> str: + """Convert the entity to a json string. + + This uses efro.jsontools.ExtendedJSONEncoder/Decoder + to support data types not natively storable in json. + Be sure to use the corresponding loading functions here for + this same reason. + By default, keys are sorted when pretty-printing and not otherwise, + but this can be overridden by passing a bool as sort_keys_override. + """ + if prune: + data = self.pruned_data() + else: + data = self.d_data + if pretty: + return json.dumps( + data, + indent=2, + sort_keys=(sort_keys_override + if sort_keys_override is not None else True), + cls=ExtendedJSONEncoder) + + # When not doing pretty, go for quick and compact. + return json.dumps(data, + separators=(',', ':'), + sort_keys=(sort_keys_override if sort_keys_override + is not None else False), + cls=ExtendedJSONEncoder) + + @staticmethod + def json_loads(s: str) -> Any: + """Load a json string using our special extended decoder. + + Note that this simply returns loaded json data; no + Entities are involved. + """ + return json.loads(s, cls=ExtendedJSONDecoder) + + def load_from_json_str(self, s: str, error: bool = True) -> None: + """Set the entity's data in-place from a json string. + + The 'error' argument determines whether Exceptions will be raised + for invalid data values. Values will be reset/conformed to valid ones + if error is False. Note that Exceptions will always be raised + in the case of invalid formatted json. + """ + data = self.json_loads(s) + self.set_data(data, error=error) + + @classmethod + def from_json_str(cls: Type[T], s: str, error: bool = True) -> T: + """Instantiate a new instance with provided json string. + + The 'error' argument determines whether exceptions will be raised + on invalid data values. Values will be reset/conformed to valid ones + if error is False. Note that exceptions will always be raised + in the case of invalid formatted json. + """ + obj = cls(d_data=cls.json_loads(s), error=error) + return obj + + # Note: though d_fields actually returns a FieldInspector, + # in type-checking-land we currently just say it returns self. + # This allows the type-checker to at least validate subfield access, + # though the types will be incorrect (values instead of inspectors). + # This means that anything taking FieldInspectors needs to take 'Any' + # at the moment. Hopefully we can make this cleaner via a mypy + # plugin at some point. + if TYPE_CHECKING: + + @property + def d_fields(self: T) -> T: + """For accessing entity field objects (as opposed to values).""" + ... + else: + + @property + def d_fields(self): + """For accessing entity field objects (as opposed to values).""" + return FieldInspector(self, self, [], []) + + +class Entity(EntityMixin, CompoundValue): + """A data class consisting of Fields and their underlying data. + + Fields and Values simply define a data layout; Entities are concrete + objects using those layouts. + + Inherit from this class and add Fields to define a simple Entity type. + Alternately, combine an EntityMixin with any CompoundValue child class + to accomplish the same. The latter allows sharing CompoundValue + layouts between different concrete Entity types. For example, a + 'Weapon' CompoundValue could be embedded as part of a 'Character' + Entity but also exist as a distinct 'WeaponEntity' in an armory + database. + """ diff --git a/dist/ba_data/python/efro/entity/_field.py b/dist/ba_data/python/efro/entity/_field.py new file mode 100644 index 0000000..33c26ff --- /dev/null +++ b/dist/ba_data/python/efro/entity/_field.py @@ -0,0 +1,588 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Field types for the entity system.""" + +from __future__ import annotations + +import copy +import logging +from enum import Enum +from typing import TYPE_CHECKING, Generic, TypeVar, overload + +from efro.util import enum_by_value +from efro.entity._base import BaseField +from efro.entity._support import (BoundCompoundValue, BoundListField, + BoundDictField, BoundCompoundListField, + BoundCompoundDictField) +from efro.entity.util import have_matching_fields + +if TYPE_CHECKING: + from typing import Dict, Type, List, Any + from efro.entity._value import TypedValue, CompoundValue + +T = TypeVar('T') +TK = TypeVar('TK') +TC = TypeVar('TC', bound='CompoundValue') + + +class Field(BaseField, Generic[T]): + """Field consisting of a single value.""" + + def __init__(self, + d_key: str, + value: TypedValue[T], + store_default: bool = True) -> None: + super().__init__(d_key) + self.d_value = value + self._store_default = store_default + + def __repr__(self) -> str: + return f'' + + def get_default_data(self) -> Any: + return self.d_value.get_default_data() + + def filter_input(self, data: Any, error: bool) -> Any: + return self.d_value.filter_input(data, error) + + def filter_output(self, data: Any) -> Any: + return self.d_value.filter_output(data) + + def prune_data(self, data: Any) -> bool: + return self.d_value.prune_data(data) + + if TYPE_CHECKING: + # Use default runtime get/set but let type-checker know our types. + # Note: we actually return a bound-field when accessed on + # a type instead of an instance, but we don't reflect that here yet + # (would need to write a mypy plugin so sub-field access works first) + + @overload + def __get__(self, obj: None, cls: Any = None) -> Field[T]: + ... + + @overload + def __get__(self, obj: Any, cls: Any = None) -> T: + ... + + def __get__(self, obj: Any, cls: Any = None) -> Any: + ... + + def __set__(self, obj: Any, value: T) -> None: + ... + + +class CompoundField(BaseField, Generic[TC]): + """Field consisting of a single compound value.""" + + def __init__(self, + d_key: str, + value: TC, + store_default: bool = True) -> None: + super().__init__(d_key) + if __debug__: + from efro.entity._value import CompoundValue + assert isinstance(value, CompoundValue) + assert not hasattr(value, 'd_data') + self.d_value = value + self._store_default = store_default + + def get_default_data(self) -> dict: + return self.d_value.get_default_data() + + def filter_input(self, data: Any, error: bool) -> dict: + return self.d_value.filter_input(data, error) + + def prune_data(self, data: Any) -> bool: + return self.d_value.prune_data(data) + + # Note: + # Currently, to the type-checker we just return a simple instance + # of our CompoundValue so it can properly type-check access to its + # attrs. However at runtime we return a FieldInspector or + # BoundCompoundField which both use magic to provide the same attrs + # dynamically (but which the type-checker doesn't understand). + # Perhaps at some point we can write a mypy plugin to correct this. + if TYPE_CHECKING: + + def __get__(self, obj: Any, cls: Any = None) -> TC: + ... + + # Theoretically this type-checking may be too tight; + # we can support assigning a parent class to a child class if + # their fields match. Not sure if that'll ever come up though; + # gonna leave this for now as I prefer to have *some* checking. + # Also once we get BoundCompoundValues working with mypy we'll + # need to accept those too. + def __set__(self: CompoundField[TC], obj: Any, value: TC) -> None: + ... + + def get_with_data(self, data: Any) -> Any: + assert self.d_key in data + return BoundCompoundValue(self.d_value, data[self.d_key]) + + def set_with_data(self, data: Any, value: Any, error: bool) -> Any: + from efro.entity._value import CompoundValue + + # Ok here's the deal: our type checking above allows any subtype + # of our CompoundValue in here, but we want to be more picky than + # that. Let's check fields for equality. This way we'll allow + # assigning something like a Carentity to a Car field + # (where the data is the same), but won't allow assigning a Car + # to a Vehicle field (as Car probably adds more fields). + value1: CompoundValue + if isinstance(value, BoundCompoundValue): + value1 = value.d_value + elif isinstance(value, CompoundValue): + value1 = value + else: + raise ValueError(f"Can't assign from object type {type(value)}") + dataval = getattr(value, 'd_data', None) + if dataval is None: + raise ValueError(f"Can't assign from unbound object {value}") + if self.d_value.get_fields() != value1.get_fields(): + raise ValueError(f"Can't assign to {self.d_value} from" + f' incompatible type {value.d_value}; ' + f'sub-fields do not match.') + + # If we're allowing this to go through, we can simply copy the + # data from the passed in value. The fields match so it should + # be in a valid state already. + data[self.d_key] = copy.deepcopy(dataval) + + +class ListField(BaseField, Generic[T]): + """Field consisting of repeated values.""" + + def __init__(self, + d_key: str, + value: TypedValue[T], + store_default: bool = True) -> None: + super().__init__(d_key) + self.d_value = value + self._store_default = store_default + + def get_default_data(self) -> list: + return [] + + def filter_input(self, data: Any, error: bool) -> Any: + + # If we were passed a BoundListField, operate on its raw values + if isinstance(data, BoundListField): + data = data.d_data + + if not isinstance(data, list): + if error: + raise TypeError(f'list value expected; got {type(data)}') + logging.error('Ignoring non-list data for %s: %s', self, data) + data = [] + for i, entry in enumerate(data): + data[i] = self.d_value.filter_input(entry, error=error) + return data + + def prune_data(self, data: Any) -> bool: + # We never prune individual values since that would fundamentally + # change the list, but we can prune completely if empty (and allowed). + return not data and not self._store_default + + # When accessed on a FieldInspector we return a sub-field FieldInspector. + # When accessed on an instance we return a BoundListField. + + if TYPE_CHECKING: + + # Access via type gives our field; via an instance gives a bound field. + @overload + def __get__(self, obj: None, cls: Any = None) -> ListField[T]: + ... + + @overload + def __get__(self, obj: Any, cls: Any = None) -> BoundListField[T]: + ... + + def __get__(self, obj: Any, cls: Any = None) -> Any: + ... + + # Allow setting via a raw value list or a bound list field + @overload + def __set__(self, obj: Any, value: List[T]) -> None: + ... + + @overload + def __set__(self, obj: Any, value: BoundListField[T]) -> None: + ... + + def __set__(self, obj: Any, value: Any) -> None: + ... + + def get_with_data(self, data: Any) -> Any: + return BoundListField(self, data[self.d_key]) + + +class DictField(BaseField, Generic[TK, T]): + """A field of values in a dict with a specified index type.""" + + def __init__(self, + d_key: str, + keytype: Type[TK], + field: TypedValue[T], + store_default: bool = True) -> None: + super().__init__(d_key) + self.d_value = field + self._store_default = store_default + self._keytype = keytype + + def get_default_data(self) -> dict: + return {} + + def filter_input(self, data: Any, error: bool) -> Any: + + # If we were passed a BoundDictField, operate on its raw values + if isinstance(data, BoundDictField): + data = data.d_data + + if not isinstance(data, dict): + if error: + raise TypeError('dict value expected') + logging.error('Ignoring non-dict data for %s: %s', self, data) + data = {} + data_out = {} + for key, val in data.items(): + + # For enum keys, make sure its a valid enum. + if issubclass(self._keytype, Enum): + try: + _enumval = enum_by_value(self._keytype, key) + except Exception as exc: + if error: + raise ValueError(f'No enum of type {self._keytype}' + f' exists with value {key}') from exc + logging.error('Ignoring invalid key type for %s: %s', self, + data) + continue + + # For all other keys we can check for exact types. + elif not isinstance(key, self._keytype): + if error: + raise TypeError( + f'Invalid key type; expected {self._keytype},' + f' got {type(key)}.') + logging.error('Ignoring invalid key type for %s: %s', self, + data) + continue + + data_out[key] = self.d_value.filter_input(val, error=error) + return data_out + + def prune_data(self, data: Any) -> bool: + # We never prune individual values since that would fundamentally + # change the dict, but we can prune completely if empty (and allowed) + return not data and not self._store_default + + if TYPE_CHECKING: + + # Return our field if accessed via type and bound-dict-field + # if via instance. + @overload + def __get__(self, obj: None, cls: Any = None) -> DictField[TK, T]: + ... + + @overload + def __get__(self, obj: Any, cls: Any = None) -> BoundDictField[TK, T]: + ... + + def __get__(self, obj: Any, cls: Any = None) -> Any: + ... + + # Allow setting via matching dict values or BoundDictFields + @overload + def __set__(self, obj: Any, value: Dict[TK, T]) -> None: + ... + + @overload + def __set__(self, obj: Any, value: BoundDictField[TK, T]) -> None: + ... + + def __set__(self, obj: Any, value: Any) -> None: + ... + + def get_with_data(self, data: Any) -> Any: + return BoundDictField(self._keytype, self, data[self.d_key]) + + +class CompoundListField(BaseField, Generic[TC]): + """A field consisting of repeated instances of a compound-value. + + Element access returns the sub-field, allowing nested field access. + ie: mylist[10].fieldattr = 'foo' + """ + + def __init__(self, + d_key: str, + valuetype: TC, + store_default: bool = True) -> None: + super().__init__(d_key) + self.d_value = valuetype + + # This doesnt actually exist for us, but want the type-checker + # to think it does (see TYPE_CHECKING note below). + self.d_data: Any + self._store_default = store_default + + def filter_input(self, data: Any, error: bool) -> list: + + if not isinstance(data, list): + if error: + raise TypeError('list value expected') + logging.error('Ignoring non-list data for %s: %s', self, data) + data = [] + assert isinstance(data, list) + + # Ok we've got a list; now run everything in it through validation. + for i, subdata in enumerate(data): + data[i] = self.d_value.filter_input(subdata, error=error) + return data + + def get_default_data(self) -> list: + return [] + + def prune_data(self, data: Any) -> bool: + # Run pruning on all individual entries' data through out child field. + # However we don't *completely* prune values from the list since that + # would change it. + for subdata in data: + self.d_value.prune_fields_data(subdata) + + # We can also optionally prune the whole list if empty and allowed. + return not data and not self._store_default + + if TYPE_CHECKING: + + @overload + def __get__(self, obj: None, cls: Any = None) -> CompoundListField[TC]: + ... + + @overload + def __get__(self, + obj: Any, + cls: Any = None) -> BoundCompoundListField[TC]: + ... + + def __get__(self, obj: Any, cls: Any = None) -> Any: + ... + + # Note: + # When setting the list, we tell the type-checker that we also accept + # a raw list of CompoundValue objects, but at runtime we actually + # always deal with BoundCompoundValue objects (see note in + # BoundCompoundListField for why we accept CompoundValue objs) + @overload + def __set__(self, obj: Any, value: List[TC]) -> None: + ... + + @overload + def __set__(self, obj: Any, value: BoundCompoundListField[TC]) -> None: + ... + + def __set__(self, obj: Any, value: Any) -> None: + ... + + def get_with_data(self, data: Any) -> Any: + assert self.d_key in data + return BoundCompoundListField(self, data[self.d_key]) + + def set_with_data(self, data: Any, value: Any, error: bool) -> Any: + + # If we were passed a BoundCompoundListField, + # simply convert it to a flat list of BoundCompoundValue objects which + # is what we work with natively here. + if isinstance(value, BoundCompoundListField): + value = list(value) + + if not isinstance(value, list): + raise TypeError(f'CompoundListField expected list value on set;' + f' got {type(value)}.') + + # Allow assigning only from a sequence of our existing children. + # (could look into expanding this to other children if we can + # be sure the underlying data will line up; for example two + # CompoundListFields with different child_field values should not + # be inter-assignable. + if not all(isinstance(i, BoundCompoundValue) for i in value): + raise ValueError('CompoundListField assignment must be a ' + 'list containing only BoundCompoundValue objs.') + + # Make sure the data all has the same CompoundValue type and + # compare that type against ours once to make sure its fields match. + # (this will not allow passing CompoundValues from multiple sources + # but I don't know if that would ever come up..) + for i, val in enumerate(value): + if i == 0: + # Do the full field comparison on the first value only.. + if not have_matching_fields(val.d_value, self.d_value): + raise ValueError( + 'CompoundListField assignment must be a ' + 'list containing matching CompoundValues.') + else: + # For all remaining values, just ensure they match the first. + if val.d_value is not value[0].d_value: + raise ValueError( + 'CompoundListField assignment cannot contain ' + 'multiple CompoundValue types as sources.') + + data[self.d_key] = self.filter_input([i.d_data for i in value], + error=error) + + +class CompoundDictField(BaseField, Generic[TK, TC]): + """A field consisting of key-indexed instances of a compound-value. + + Element access returns the sub-field, allowing nested field access. + ie: mylist[10].fieldattr = 'foo' + """ + + def __init__(self, + d_key: str, + keytype: Type[TK], + valuetype: TC, + store_default: bool = True) -> None: + super().__init__(d_key) + self.d_value = valuetype + + # This doesnt actually exist for us, but want the type-checker + # to think it does (see TYPE_CHECKING note below). + self.d_data: Any + + self.d_keytype = keytype + self._store_default = store_default + + def filter_input(self, data: Any, error: bool) -> dict: + if not isinstance(data, dict): + if error: + raise TypeError('dict value expected') + logging.error('Ignoring non-dict data for %s: %s', self, data) + data = {} + data_out = {} + for key, val in data.items(): + + # For enum keys, make sure its a valid enum. + if issubclass(self.d_keytype, Enum): + try: + _enumval = enum_by_value(self.d_keytype, key) + except Exception as exc: + if error: + raise ValueError(f'No enum of type {self.d_keytype}' + f' exists with value {key}') from exc + logging.error('Ignoring invalid key type for %s: %s', self, + data) + continue + + # For all other keys we can check for exact types. + elif not isinstance(key, self.d_keytype): + if error: + raise TypeError( + f'Invalid key type; expected {self.d_keytype},' + f' got {type(key)}.') + logging.error('Ignoring invalid key type for %s: %s', self, + data) + continue + + data_out[key] = self.d_value.filter_input(val, error=error) + return data_out + + def get_default_data(self) -> dict: + return {} + + def prune_data(self, data: Any) -> bool: + # Run pruning on all individual entries' data through our child field. + # However we don't *completely* prune values from the list since that + # would change it. + for subdata in data.values(): + self.d_value.prune_fields_data(subdata) + + # We can also optionally prune the whole list if empty and allowed. + return not data and not self._store_default + + # ONLY overriding these in type-checker land to clarify types. + # (see note in BaseField) + if TYPE_CHECKING: + + @overload + def __get__(self, + obj: None, + cls: Any = None) -> CompoundDictField[TK, TC]: + ... + + @overload + def __get__(self, + obj: Any, + cls: Any = None) -> BoundCompoundDictField[TK, TC]: + ... + + def __get__(self, obj: Any, cls: Any = None) -> Any: + ... + + # Note: + # When setting the dict, we tell the type-checker that we also accept + # a raw dict of CompoundValue objects, but at runtime we actually + # always deal with BoundCompoundValue objects (see note in + # BoundCompoundDictField for why we accept CompoundValue objs) + @overload + def __set__(self, obj: Any, value: Dict[TK, TC]) -> None: + ... + + @overload + def __set__(self, obj: Any, value: BoundCompoundDictField[TK, + TC]) -> None: + ... + + def __set__(self, obj: Any, value: Any) -> None: + ... + + def get_with_data(self, data: Any) -> Any: + assert self.d_key in data + return BoundCompoundDictField(self, data[self.d_key]) + + def set_with_data(self, data: Any, value: Any, error: bool) -> Any: + + # If we were passed a BoundCompoundDictField, + # simply convert it to a flat dict of BoundCompoundValue objects which + # is what we work with natively here. + if isinstance(value, BoundCompoundDictField): + value = dict(value.items()) + + if not isinstance(value, dict): + raise TypeError('CompoundDictField expected dict value on set.') + + # Allow assigning only from a sequence of our existing children. + # (could look into expanding this to other children if we can + # be sure the underlying data will line up; for example two + # CompoundListFields with different child_field values should not + # be inter-assignable. + if (not all(isinstance(i, BoundCompoundValue) + for i in value.values())): + raise ValueError('CompoundDictField assignment must be a ' + 'dict containing only BoundCompoundValues.') + + # Make sure the data all has the same CompoundValue type and + # compare that type against ours once to make sure its fields match. + # (this will not allow passing CompoundValues from multiple sources + # but I don't know if that would ever come up..) + first_value: Any = None + for i, val in enumerate(value.values()): + if i == 0: + first_value = val.d_value + # Do the full field comparison on the first value only.. + if not have_matching_fields(val.d_value, self.d_value): + raise ValueError( + 'CompoundListField assignment must be a ' + 'list containing matching CompoundValues.') + else: + # For all remaining values, just ensure they match the first. + if val.d_value is not first_value: + raise ValueError( + 'CompoundListField assignment cannot contain ' + 'multiple CompoundValue types as sources.') + + data[self.d_key] = self.filter_input( + {key: val.d_data + for key, val in value.items()}, error=error) diff --git a/dist/ba_data/python/efro/entity/_support.py b/dist/ba_data/python/efro/entity/_support.py new file mode 100644 index 0000000..99ceede --- /dev/null +++ b/dist/ba_data/python/efro/entity/_support.py @@ -0,0 +1,468 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Various support classes for accessing data and info on fields and values.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar, Generic, overload + +from efro.entity._base import (BaseField, dict_key_to_raw, dict_key_from_raw) + +if TYPE_CHECKING: + from typing import (Optional, Tuple, Type, Any, Dict, List, Union) + from efro.entity._value import CompoundValue + from efro.entity._field import (ListField, DictField, CompoundListField, + CompoundDictField) + +T = TypeVar('T') +TKey = TypeVar('TKey') +TCompound = TypeVar('TCompound', bound='CompoundValue') +TBoundList = TypeVar('TBoundList', bound='BoundCompoundListField') + + +class BoundCompoundValue: + """Wraps a CompoundValue object and its entity data. + + Allows access to its values through our own equivalent attributes. + """ + + def __init__(self, value: CompoundValue, d_data: Union[List[Any], + Dict[str, Any]]): + self.d_value: CompoundValue + self.d_data: Union[List[Any], Dict[str, Any]] + + # Need to use base setters to avoid triggering our own overrides. + object.__setattr__(self, 'd_value', value) + object.__setattr__(self, 'd_data', d_data) + + def __eq__(self, other: Any) -> Any: + # Allow comparing to compound and bound-compound objects. + from efro.entity.util import compound_eq + return compound_eq(self, other) + + def __getattr__(self, name: str, default: Any = None) -> Any: + # If this attribute corresponds to a field on our compound value's + # unbound type, ask it to give us a value using our data + d_value = type(object.__getattribute__(self, 'd_value')) + field = getattr(d_value, name, None) + if isinstance(field, BaseField): + return field.get_with_data(self.d_data) + raise AttributeError + + def __setattr__(self, name: str, value: Any) -> None: + # Same deal as __getattr__ basically. + field = getattr(type(object.__getattribute__(self, 'd_value')), name, + None) + if isinstance(field, BaseField): + field.set_with_data(self.d_data, value, error=True) + return + super().__setattr__(name, value) + + def reset(self) -> None: + """Reset this field's data to defaults.""" + value = object.__getattribute__(self, 'd_value') + data = object.__getattribute__(self, 'd_data') + assert isinstance(data, dict) + + # Need to clear our dict in-place since we have no + # access to our parent which we'd need to assign an empty one. + data.clear() + + # Now fill in default data. + value.apply_fields_to_data(data, error=True) + + def __repr__(self) -> str: + fstrs: List[str] = [] + for field in self.d_value.get_fields(): + try: + fstrs.append(str(field) + '=' + repr(getattr(self, field))) + except Exception: + fstrs.append('FAIL' + str(field) + ' ' + str(type(self))) + return type(self.d_value).__name__ + '(' + ', '.join(fstrs) + ')' + + +class FieldInspector: + """Used for inspecting fields.""" + + def __init__(self, root: Any, obj: Any, path: List[str], + dbpath: List[str]) -> None: + self._root = root + self._obj = obj + self._path = path + self._dbpath = dbpath + + def __repr__(self) -> str: + path = '.'.join(self._path) + typename = type(self._root).__name__ + if path == '': + return f'' + return f'' + + def __getattr__(self, name: str, default: Any = None) -> Any: + # pylint: disable=cyclic-import + from efro.entity._field import CompoundField + + # If this attribute corresponds to a field on our obj's + # unbound type, return a new inspector for it. + if isinstance(self._obj, CompoundField): + target = self._obj.d_value + else: + target = self._obj + field = getattr(type(target), name, None) + if isinstance(field, BaseField): + newpath = list(self._path) + newpath.append(name) + newdbpath = list(self._dbpath) + assert field.d_key is not None + newdbpath.append(field.d_key) + return FieldInspector(self._root, field, newpath, newdbpath) + raise AttributeError + + def get_root(self) -> Any: + """Return the root object this inspector is targeting.""" + return self._root + + def get_path(self) -> List[str]: + """Return the python path components of this inspector.""" + return self._path + + def get_db_path(self) -> List[str]: + """Return the database path components of this inspector.""" + return self._dbpath + + +class BoundListField(Generic[T]): + """ListField bound to data; used for accessing field values.""" + + def __init__(self, field: ListField[T], d_data: List[Any]): + self.d_field = field + assert isinstance(d_data, list) + self.d_data = d_data + self._i = 0 + + def __eq__(self, other: Any) -> Any: + # Just convert us into a regular list and run a compare with that. + flattened = [ + self.d_field.d_value.filter_output(value) for value in self.d_data + ] + return flattened == other + + def __repr__(self) -> str: + return '[' + ', '.join( + repr(self.d_field.d_value.filter_output(i)) + for i in self.d_data) + ']' + + def __len__(self) -> int: + return len(self.d_data) + + def __iter__(self) -> Any: + self._i = 0 + return self + + def append(self, val: T) -> None: + """Append the provided value to the list.""" + self.d_data.append(self.d_field.d_value.filter_input(val, error=True)) + + def __next__(self) -> T: + if self._i < len(self.d_data): + self._i += 1 + val: T = self.d_field.d_value.filter_output(self.d_data[self._i - + 1]) + return val + raise StopIteration + + @overload + def __getitem__(self, key: int) -> T: + ... + + @overload + def __getitem__(self, key: slice) -> List[T]: + ... + + def __getitem__(self, key: Any) -> Any: + if isinstance(key, slice): + dofilter = self.d_field.d_value.filter_output + return [ + dofilter(self.d_data[i]) + for i in range(*key.indices(len(self))) + ] + assert isinstance(key, int) + return self.d_field.d_value.filter_output(self.d_data[key]) + + def __setitem__(self, key: int, value: T) -> None: + if not isinstance(key, int): + raise TypeError('Expected int index.') + self.d_data[key] = self.d_field.d_value.filter_input(value, error=True) + + +class BoundDictField(Generic[TKey, T]): + """DictField bound to its data; used for accessing its values.""" + + def __init__(self, keytype: Type[TKey], field: DictField[TKey, T], + d_data: Dict[TKey, T]): + self._keytype = keytype + self.d_field = field + assert isinstance(d_data, dict) + self.d_data = d_data + + def __eq__(self, other: Any) -> Any: + # Just convert us into a regular dict and run a compare with that. + flattened = { + key: self.d_field.d_value.filter_output(value) + for key, value in self.d_data.items() + } + return flattened == other + + def __repr__(self) -> str: + return '{' + ', '.join( + repr(dict_key_from_raw(key, self._keytype)) + ': ' + + repr(self.d_field.d_value.filter_output(val)) + for key, val in self.d_data.items()) + '}' + + def __len__(self) -> int: + return len(self.d_data) + + def __getitem__(self, key: TKey) -> T: + keyfilt = dict_key_to_raw(key, self._keytype) + typedval: T = self.d_field.d_value.filter_output(self.d_data[keyfilt]) + return typedval + + def get(self, key: TKey, default: Optional[T] = None) -> Optional[T]: + """Get a value if present, or a default otherwise.""" + keyfilt = dict_key_to_raw(key, self._keytype) + if keyfilt not in self.d_data: + return default + typedval: T = self.d_field.d_value.filter_output(self.d_data[keyfilt]) + return typedval + + def __setitem__(self, key: TKey, value: T) -> None: + keyfilt = dict_key_to_raw(key, self._keytype) + self.d_data[keyfilt] = self.d_field.d_value.filter_input(value, + error=True) + + def __contains__(self, key: TKey) -> bool: + keyfilt = dict_key_to_raw(key, self._keytype) + return keyfilt in self.d_data + + def __delitem__(self, key: TKey) -> None: + keyfilt = dict_key_to_raw(key, self._keytype) + del self.d_data[keyfilt] + + def keys(self) -> List[TKey]: + """Return a list of our keys.""" + return [ + dict_key_from_raw(k, self._keytype) for k in self.d_data.keys() + ] + + def values(self) -> List[T]: + """Return a list of our values.""" + return [ + self.d_field.d_value.filter_output(value) + for value in self.d_data.values() + ] + + def items(self) -> List[Tuple[TKey, T]]: + """Return a list of item/value pairs.""" + return [(dict_key_from_raw(key, self._keytype), + self.d_field.d_value.filter_output(value)) + for key, value in self.d_data.items()] + + +class BoundCompoundListField(Generic[TCompound]): + """A CompoundListField bound to its entity sub-data.""" + + def __init__(self, field: CompoundListField[TCompound], d_data: List[Any]): + self.d_field = field + self.d_data = d_data + self._i = 0 + + def __eq__(self, other: Any) -> Any: + from efro.entity.util import have_matching_fields + + # We can only be compared to other bound-compound-fields + if not isinstance(other, BoundCompoundListField): + return NotImplemented + + # If our compound values have differing fields, we're unequal. + if not have_matching_fields(self.d_field.d_value, + other.d_field.d_value): + return False + + # Ok our data schemas match; now just compare our data.. + return self.d_data == other.d_data + + def __len__(self) -> int: + return len(self.d_data) + + def __repr__(self) -> str: + return '[' + ', '.join( + repr(BoundCompoundValue(self.d_field.d_value, i)) + for i in self.d_data) + ']' + + # Note: to the type checker our gets/sets simply deal with CompoundValue + # objects so the type-checker can cleanly handle their sub-fields. + # However at runtime we deal in BoundCompoundValue objects which use magic + # to tie the CompoundValue object to its data but which the type checker + # can't understand. + if TYPE_CHECKING: + + @overload + def __getitem__(self, key: int) -> TCompound: + ... + + @overload + def __getitem__(self, key: slice) -> List[TCompound]: + ... + + def __getitem__(self, key: Any) -> Any: + ... + + def __next__(self) -> TCompound: + ... + + def append(self) -> TCompound: + """Append and return a new field entry to the array.""" + ... + else: + + def __getitem__(self, key: Any) -> Any: + if isinstance(key, slice): + return [ + BoundCompoundValue(self.d_field.d_value, self.d_data[i]) + for i in range(*key.indices(len(self))) + ] + assert isinstance(key, int) + return BoundCompoundValue(self.d_field.d_value, self.d_data[key]) + + def __next__(self): + if self._i < len(self.d_data): + self._i += 1 + return BoundCompoundValue(self.d_field.d_value, + self.d_data[self._i - 1]) + raise StopIteration + + def append(self) -> Any: + """Append and return a new field entry to the array.""" + # push the entity default into data and then let it fill in + # any children/etc. + self.d_data.append( + self.d_field.d_value.filter_input( + self.d_field.d_value.get_default_data(), error=True)) + return BoundCompoundValue(self.d_field.d_value, self.d_data[-1]) + + def __iter__(self: TBoundList) -> TBoundList: + self._i = 0 + return self + + +class BoundCompoundDictField(Generic[TKey, TCompound]): + """A CompoundDictField bound to its entity sub-data.""" + + def __init__(self, field: CompoundDictField[TKey, TCompound], + d_data: Dict[Any, Any]): + self.d_field = field + self.d_data = d_data + + def __eq__(self, other: Any) -> Any: + from efro.entity.util import have_matching_fields + + # We can only be compared to other bound-compound-fields + if not isinstance(other, BoundCompoundDictField): + return NotImplemented + + # If our compound values have differing fields, we're unequal. + if not have_matching_fields(self.d_field.d_value, + other.d_field.d_value): + return False + + # Ok our data schemas match; now just compare our data.. + return self.d_data == other.d_data + + def __repr__(self) -> str: + return '{' + ', '.join( + repr(key) + ': ' + + repr(BoundCompoundValue(self.d_field.d_value, value)) + for key, value in self.d_data.items()) + '}' + + # In the typechecker's eyes, gets/sets on us simply deal in + # CompoundValue object. This allows type-checking to work nicely + # for its sub-fields. + # However in real-life we return BoundCompoundValues which use magic + # to tie the CompoundValue to its data (but which the typechecker + # would not be able to make sense of) + if TYPE_CHECKING: + + def get(self, key: TKey) -> Optional[TCompound]: + """Return a value if present; otherwise None.""" + + def __getitem__(self, key: TKey) -> TCompound: + ... + + def values(self) -> List[TCompound]: + """Return a list of our values.""" + + def items(self) -> List[Tuple[TKey, TCompound]]: + """Return key/value pairs for all dict entries.""" + + def add(self, key: TKey) -> TCompound: + """Add an entry into the dict, returning it. + + Any existing value is replaced.""" + + else: + + def get(self, key): + """return a value if present; otherwise None.""" + keyfilt = dict_key_to_raw(key, self.d_field.d_keytype) + data = self.d_data.get(keyfilt) + if data is not None: + return BoundCompoundValue(self.d_field.d_value, data) + return None + + def __getitem__(self, key): + keyfilt = dict_key_to_raw(key, self.d_field.d_keytype) + return BoundCompoundValue(self.d_field.d_value, + self.d_data[keyfilt]) + + def values(self): + """Return a list of our values.""" + return list( + BoundCompoundValue(self.d_field.d_value, i) + for i in self.d_data.values()) + + def items(self): + """Return key/value pairs for all dict entries.""" + return [(dict_key_from_raw(key, self.d_field.d_keytype), + BoundCompoundValue(self.d_field.d_value, value)) + for key, value in self.d_data.items()] + + def add(self, key: TKey) -> TCompound: + """Add an entry into the dict, returning it. + + Any existing value is replaced.""" + keyfilt = dict_key_to_raw(key, self.d_field.d_keytype) + + # Push the entity default into data and then let it fill in + # any children/etc. + self.d_data[keyfilt] = (self.d_field.d_value.filter_input( + self.d_field.d_value.get_default_data(), error=True)) + return BoundCompoundValue(self.d_field.d_value, + self.d_data[keyfilt]) + + def __len__(self) -> int: + return len(self.d_data) + + def __contains__(self, key: TKey) -> bool: + keyfilt = dict_key_to_raw(key, self.d_field.d_keytype) + return keyfilt in self.d_data + + def __delitem__(self, key: TKey) -> None: + keyfilt = dict_key_to_raw(key, self.d_field.d_keytype) + del self.d_data[keyfilt] + + def keys(self) -> List[TKey]: + """Return a list of our keys.""" + return [ + dict_key_from_raw(k, self.d_field.d_keytype) + for k in self.d_data.keys() + ] diff --git a/dist/ba_data/python/efro/entity/_value.py b/dist/ba_data/python/efro/entity/_value.py new file mode 100644 index 0000000..5798179 --- /dev/null +++ b/dist/ba_data/python/efro/entity/_value.py @@ -0,0 +1,537 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Value types for the entity system.""" + +from __future__ import annotations + +import datetime +import inspect +import logging +from collections import abc +from enum import Enum +from typing import TYPE_CHECKING, TypeVar, Generic +# Our Pylint class_generics_filter gives us a false-positive unused-import. +from typing import Tuple, Optional # pylint: disable=W0611 + +from efro.entity._base import DataHandler, BaseField +from efro.entity.util import compound_eq + +if TYPE_CHECKING: + from typing import Optional, Set, List, Dict, Any, Type + +T = TypeVar('T') +TE = TypeVar('TE', bound=Enum) + +_sanity_tested_types: Set[Type] = set() +_type_field_cache: Dict[Type, Dict[str, BaseField]] = {} + + +class TypedValue(DataHandler, Generic[T]): + """Base class for all value types dealing with a single data type.""" + + +class SimpleValue(TypedValue[T]): + """Standard base class for simple single-value types. + + This class provides enough functionality to handle most simple + types such as int/float/etc without too many subclass overrides. + """ + + def __init__(self, + default: T, + store_default: bool, + target_type: Type = None, + convert_source_types: Tuple[Type, ...] = (), + allow_none: bool = False) -> None: + """Init the value field. + + If store_default is False, the field value will not be included + in final entity data if it is a default value. Be sure to set + this to True for any fields that will be used for server-side + queries so they are included in indexing. + target_type and convert_source_types are used in the default + filter_input implementation; if passed in data's type is present + in convert_source_types, a target_type will be instantiated + using it. (allows for simple conversions to bool, int, etc) + Data will also be allowed through untouched if it matches target_type. + (types needing further introspection should override filter_input). + Lastly, the value of allow_none is also used in filter_input for + whether values of None should be allowed. + """ + super().__init__() + + self._store_default = store_default + self._target_type = target_type + self._convert_source_types = convert_source_types + self._allow_none = allow_none + + # We store _default_data in our internal data format so need + # to run user-facing value through our input filter. + # Make sure we do this last since filter_input depends on above vals. + self._default_data: T = self.filter_input(default, error=True) + + def __repr__(self) -> str: + if self._target_type is not None: + return f'' + return '' + + def get_default_data(self) -> Any: + return self._default_data + + def prune_data(self, data: Any) -> bool: + return not self._store_default and data == self._default_data + + def filter_input(self, data: Any, error: bool) -> Any: + + # Let data pass through untouched if its already our target type + if self._target_type is not None: + if isinstance(data, self._target_type): + return data + + # ...and also if its None and we're into that sort of thing. + if self._allow_none and data is None: + return data + + # If its one of our convertible types, convert. + if (self._convert_source_types + and isinstance(data, self._convert_source_types)): + assert self._target_type is not None + return self._target_type(data) + if error: + errmsg = (f'value of type {self._target_type} or None expected' + if self._allow_none else + f'value of type {self._target_type} expected') + errmsg += f'; got {type(data)}' + raise TypeError(errmsg) + errmsg = f'Ignoring incompatible data for {self};' + errmsg += (f' expected {self._target_type} or None;' + if self._allow_none else f'expected {self._target_type};') + errmsg += f' got {type(data)}' + logging.error(errmsg) + return self.get_default_data() + + +class StringValue(SimpleValue[str]): + """Value consisting of a single string.""" + + def __init__(self, default: str = '', store_default: bool = True) -> None: + super().__init__(default, store_default, str) + + +class OptionalStringValue(SimpleValue[Optional[str]]): + """Value consisting of a single string or None.""" + + def __init__(self, + default: Optional[str] = None, + store_default: bool = True) -> None: + super().__init__(default, store_default, str, allow_none=True) + + +class BoolValue(SimpleValue[bool]): + """Value consisting of a single bool.""" + + def __init__(self, + default: bool = False, + store_default: bool = True) -> None: + super().__init__(default, store_default, bool, (int, float)) + + +class OptionalBoolValue(SimpleValue[Optional[bool]]): + """Value consisting of a single bool or None.""" + + def __init__(self, + default: Optional[bool] = None, + store_default: bool = True) -> None: + super().__init__(default, + store_default, + bool, (int, float), + allow_none=True) + + +def verify_time_input(data: Any, error: bool, allow_none: bool) -> Any: + """Checks input data for time values.""" + pytz_utc: Any + + # We don't *require* pytz since it must be installed through pip + # but it is used by firestore client for its date values + # (in which case it should be installed as a dependency anyway). + try: + import pytz + pytz_utc = pytz.utc + except ModuleNotFoundError: + pytz_utc = None + + # Filter unallowed None values. + if not allow_none and data is None: + if error: + raise ValueError('datetime value cannot be None') + logging.error('ignoring datetime value of None') + data = (None if allow_none else datetime.datetime.now( + datetime.timezone.utc)) + + # Parent filter_input does what we need, but let's just make + # sure we *only* accept datetime values that know they're UTC. + elif (isinstance(data, datetime.datetime) + and data.tzinfo is not datetime.timezone.utc + and (pytz_utc is None or data.tzinfo is not pytz_utc)): + if error: + raise ValueError( + 'datetime values must have timezone set as timezone.utc') + logging.error( + 'ignoring datetime value without timezone.utc set: %s %s', + type(datetime.timezone.utc), type(data.tzinfo)) + data = (None if allow_none else datetime.datetime.now( + datetime.timezone.utc)) + return data + + +class DateTimeValue(SimpleValue[datetime.datetime]): + """Value consisting of a datetime.datetime object. + + The default value for this is always the current time in UTC. + """ + + def __init__(self, store_default: bool = True) -> None: + # Pass dummy datetime value as default just to satisfy constructor; + # we override get_default_data though so this doesn't get used. + dummy_default = datetime.datetime.now(datetime.timezone.utc) + super().__init__(dummy_default, store_default, datetime.datetime) + + def get_default_data(self) -> Any: + # For this class we don't use a static default value; + # default is always now. + return datetime.datetime.now(datetime.timezone.utc) + + def filter_input(self, data: Any, error: bool) -> Any: + data = verify_time_input(data, error, allow_none=False) + return super().filter_input(data, error) + + +class OptionalDateTimeValue(SimpleValue[Optional[datetime.datetime]]): + """Value consisting of a datetime.datetime object or None.""" + + def __init__(self, store_default: bool = True) -> None: + super().__init__(None, + store_default, + datetime.datetime, + allow_none=True) + + def filter_input(self, data: Any, error: bool) -> Any: + data = verify_time_input(data, error, allow_none=True) + return super().filter_input(data, error) + + +class IntValue(SimpleValue[int]): + """Value consisting of a single int.""" + + def __init__(self, default: int = 0, store_default: bool = True) -> None: + super().__init__(default, store_default, int, (bool, float)) + + +class OptionalIntValue(SimpleValue[Optional[int]]): + """Value consisting of a single int or None""" + + def __init__(self, + default: int = None, + store_default: bool = True) -> None: + super().__init__(default, + store_default, + int, (bool, float), + allow_none=True) + + +class FloatValue(SimpleValue[float]): + """Value consisting of a single float.""" + + def __init__(self, + default: float = 0.0, + store_default: bool = True) -> None: + super().__init__(default, store_default, float, (bool, int)) + + +class OptionalFloatValue(SimpleValue[Optional[float]]): + """Value consisting of a single float or None.""" + + def __init__(self, + default: float = None, + store_default: bool = True) -> None: + super().__init__(default, + store_default, + float, (bool, int), + allow_none=True) + + +class Float3Value(SimpleValue[Tuple[float, float, float]]): + """Value consisting of 3 floats.""" + + def __init__(self, + default: Tuple[float, float, float] = (0.0, 0.0, 0.0), + store_default: bool = True) -> None: + super().__init__(default, store_default) + + def __repr__(self) -> str: + return '' + + def filter_input(self, data: Any, error: bool) -> Any: + if (not isinstance(data, abc.Sequence) or len(data) != 3 + or any(not isinstance(i, (int, float)) for i in data)): + if error: + raise TypeError('Sequence of 3 float values expected.') + logging.error('Ignoring non-3-float-sequence data for %s: %s', + self, data) + data = self.get_default_data() + + # Actually store as list. + return [float(data[0]), float(data[1]), float(data[2])] + + def filter_output(self, data: Any) -> Any: + """Override.""" + assert len(data) == 3 + return tuple(data) + + +class BaseEnumValue(TypedValue[T]): + """Value class for storing Python Enums. + + Internally enums are stored as their corresponding int/str/etc. values. + """ + + def __init__(self, + enumtype: Type[T], + default: Optional[T] = None, + store_default: bool = True, + allow_none: bool = False) -> None: + super().__init__() + assert issubclass(enumtype, Enum) + + vals: List[T] = list(enumtype) + + # Bit of sanity checking: make sure this enum has at least + # one value and that its underlying values are all of simple + # json-friendly types. + if not vals: + raise TypeError(f'enum {enumtype} has no values') + for val in vals: + assert isinstance(val, Enum) + if not isinstance(val.value, (int, bool, float, str)): + raise TypeError(f'enum value {val} has an invalid' + f' value type {type(val.value)}') + self._enumtype: Type[Enum] = enumtype + self._store_default: bool = store_default + self._allow_none: bool = allow_none + + # We store default data is internal format so need to run + # user-provided value through input filter. + # Make sure to set this last since it could depend on other + # stuff we set here. + if default is None and not self._allow_none: + # Special case: we allow passing None as default even if + # we don't support None as a value; in that case we sub + # in the first enum value. + default = vals[0] + self._default_data: Enum = self.filter_input(default, error=True) + + def get_default_data(self) -> Any: + return self._default_data + + def prune_data(self, data: Any) -> bool: + return not self._store_default and data == self._default_data + + def filter_input(self, data: Any, error: bool) -> Any: + + # Allow passing in enum objects directly of course. + if isinstance(data, self._enumtype): + data = data.value + elif self._allow_none and data is None: + pass + else: + # At this point we assume its an enum value + try: + self._enumtype(data) + except ValueError: + if error: + raise ValueError( + f'Invalid value for {self._enumtype}: {data}' + ) from None + logging.error('Ignoring invalid value for %s: %s', + self._enumtype, data) + data = self._default_data + return data + + def filter_output(self, data: Any) -> Any: + if self._allow_none and data is None: + return None + return self._enumtype(data) + + +class EnumValue(BaseEnumValue[TE]): + """Value class for storing Python Enums. + + Internally enums are stored as their corresponding int/str/etc. values. + """ + + def __init__(self, + enumtype: Type[TE], + default: TE = None, + store_default: bool = True) -> None: + super().__init__(enumtype, default, store_default, allow_none=False) + + +class OptionalEnumValue(BaseEnumValue[Optional[TE]]): + """Value class for storing Python Enums (or None). + + Internally enums are stored as their corresponding int/str/etc. values. + """ + + def __init__(self, + enumtype: Type[TE], + default: TE = None, + store_default: bool = True) -> None: + super().__init__(enumtype, default, store_default, allow_none=True) + + +class CompoundValue(DataHandler): + """A value containing one or more named child fields of its own. + + Custom classes can be defined that inherit from this and include + any number of Field instances within themself. + """ + + def __init__(self, store_default: bool = True) -> None: + super().__init__() + self._store_default = store_default + + # Run sanity checks on this type if we haven't. + self.run_type_sanity_checks() + + def __eq__(self, other: Any) -> Any: + # Allow comparing to compound and bound-compound objects. + return compound_eq(self, other) + + def get_default_data(self) -> dict: + return {} + + # NOTE: once we've got bound-compound-fields working in mypy + # we should get rid of this here. + # For now it needs to be here though since bound-compound fields + # come across as these in type-land. + def reset(self) -> None: + """Resets data to default.""" + raise ValueError('Unbound CompoundValue cannot be reset.') + + def filter_input(self, data: Any, error: bool) -> dict: + if not isinstance(data, dict): + if error: + raise TypeError('dict value expected') + logging.error('Ignoring non-dict data for %s: %s', self, data) + data = {} + assert isinstance(data, dict) + self.apply_fields_to_data(data, error=error) + return data + + def prune_data(self, data: Any) -> bool: + # Let all of our sub-fields prune themselves.. + self.prune_fields_data(data) + + # Now we can optionally prune ourself completely if there's + # nothing left in our data dict... + return not data and not self._store_default + + def prune_fields_data(self, d_data: Dict[str, Any]) -> None: + """Given a CompoundValue and data, prune any unnecessary data. + will include those set to default values with store_default False. + """ + + # Allow all fields to take a pruning pass. + assert isinstance(d_data, dict) + for field in self.get_fields().values(): + assert isinstance(field.d_key, str) + + # This is supposed to be valid data so there should be *something* + # there for all fields. + if field.d_key not in d_data: + raise RuntimeError(f'expected to find {field.d_key} in data' + f' for {self}; got data {d_data}') + + # Now ask the field if this data is necessary. If not, prune it. + if field.prune_data(d_data[field.d_key]): + del d_data[field.d_key] + + def apply_fields_to_data(self, d_data: Dict[str, Any], + error: bool) -> None: + """Apply all of our fields to target data. + + If error is True, exceptions will be raised for invalid data; + otherwise it will be overwritten (with logging notices emitted). + """ + assert isinstance(d_data, dict) + for field in self.get_fields().values(): + assert isinstance(field.d_key, str) + + # First off, make sure *something* is there for this field. + if field.d_key not in d_data: + d_data[field.d_key] = field.get_default_data() + + # Now let the field tweak the data as needed so its valid. + d_data[field.d_key] = field.filter_input(d_data[field.d_key], + error=error) + + def __repr__(self) -> str: + if not hasattr(self, 'd_data'): + return f'' + fstrs: List[str] = [] + assert isinstance(self, CompoundValue) + for field in self.get_fields(): + fstrs.append(str(field) + '=' + repr(getattr(self, field))) + return type(self).__name__ + '(' + ', '.join(fstrs) + ')' + + @classmethod + def get_fields(cls) -> Dict[str, BaseField]: + """Return all field instances for this type.""" + assert issubclass(cls, CompoundValue) + + # If we haven't yet, calculate and cache a complete list of fields + # for this exact type. + if cls not in _type_field_cache: + fields: Dict[str, BaseField] = {} + for icls in inspect.getmro(cls): + for name, field in icls.__dict__.items(): + if isinstance(field, BaseField): + fields[name] = field + _type_field_cache[cls] = fields + retval: Dict[str, BaseField] = _type_field_cache[cls] + assert isinstance(retval, dict) + return retval + + @classmethod + def run_type_sanity_checks(cls) -> None: + """Given a type, run one-time sanity checks on it. + + These tests ensure child fields are using valid + non-repeating names/etc. + """ + if cls not in _sanity_tested_types: + _sanity_tested_types.add(cls) + + # Make sure all embedded fields have a key set and there are no + # duplicates. + field_keys: Set[str] = set() + for field in cls.get_fields().values(): + assert isinstance(field.d_key, str) + if field.d_key is None: + raise RuntimeError(f'Child field {field} under {cls}' + 'has d_key None') + if field.d_key == '': + raise RuntimeError(f'Child field {field} under {cls}' + 'has empty d_key') + + # Allow alphanumeric and underscore only. + if not field.d_key.replace('_', '').isalnum(): + raise RuntimeError( + f'Child field "{field.d_key}" under {cls}' + f' contains invalid characters; only alphanumeric' + f' and underscore allowed.') + if field.d_key in field_keys: + raise RuntimeError('Multiple child fields with key' + f' "{field.d_key}" found in {cls}') + field_keys.add(field.d_key) diff --git a/dist/ba_data/python/efro/entity/util.py b/dist/ba_data/python/efro/entity/util.py new file mode 100644 index 0000000..ff1898f --- /dev/null +++ b/dist/ba_data/python/efro/entity/util.py @@ -0,0 +1,131 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Misc utility functionality related to the entity system.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Union, Tuple, List + from efro.entity._value import CompoundValue + from efro.entity._support import BoundCompoundValue + + +def diff_compound_values( + obj1: Union[BoundCompoundValue, CompoundValue], + obj2: Union[BoundCompoundValue, CompoundValue]) -> str: + """Generate a string showing differences between two compound values. + + Both must be associated with data and have the same set of fields. + """ + + # Ensure fields match and both are attached to data... + value1, data1 = get_compound_value_and_data(obj1) + if data1 is None: + raise ValueError(f'Invalid unbound compound value: {obj1}') + value2, data2 = get_compound_value_and_data(obj2) + if data2 is None: + raise ValueError(f'Invalid unbound compound value: {obj2}') + if not have_matching_fields(value1, value2): + raise ValueError( + f"Can't diff objs with non-matching fields: {value1} and {value2}") + + # Ok; let 'er rip... + diff = _diff(obj1, obj2, 2) + return ' ' if diff == '' else diff + + +class CompoundValueDiff: + """Wraps diff_compound_values() in an object for efficiency. + + It is preferable to pass this to logging calls instead of the + final diff string since the diff will never be generated if + the associated logging level is not being emitted. + """ + + def __init__(self, obj1: Union[BoundCompoundValue, CompoundValue], + obj2: Union[BoundCompoundValue, CompoundValue]): + self._obj1 = obj1 + self._obj2 = obj2 + + def __repr__(self) -> str: + return diff_compound_values(self._obj1, self._obj2) + + +def _diff(obj1: Union[BoundCompoundValue, CompoundValue], + obj2: Union[BoundCompoundValue, CompoundValue], indent: int) -> str: + from efro.entity._support import BoundCompoundValue + bits: List[str] = [] + indentstr = ' ' * indent + vobj1, _data1 = get_compound_value_and_data(obj1) + fields = sorted(vobj1.get_fields().keys()) + for field in fields: + val1 = getattr(obj1, field) + val2 = getattr(obj2, field) + # for nested compounds, dive in and do nice piecewise compares + if isinstance(val1, BoundCompoundValue): + assert isinstance(val2, BoundCompoundValue) + diff = _diff(val1, val2, indent + 2) + if diff != '': + bits.append(f'{indentstr}{field}:') + bits.append(diff) + # for all else just do a single line + # (perhaps we could improve on this for other complex types) + else: + if val1 != val2: + bits.append(f'{indentstr}{field}: {val1} -> {val2}') + return '\n'.join(bits) + + +def have_matching_fields(val1: CompoundValue, val2: CompoundValue) -> bool: + """Return whether two compound-values have matching sets of fields. + + Note this just refers to the field configuration; not data. + """ + # Quick-out: matching types will always have identical fields. + if type(val1) is type(val2): + return True + + # Otherwise do a full comparison. + return val1.get_fields() == val2.get_fields() + + +def get_compound_value_and_data( + obj: Union[BoundCompoundValue, + CompoundValue]) -> Tuple[CompoundValue, Any]: + """Return value and data for bound or unbound compound values.""" + # pylint: disable=cyclic-import + from efro.entity._support import BoundCompoundValue + from efro.entity._value import CompoundValue + if isinstance(obj, BoundCompoundValue): + value = obj.d_value + data = obj.d_data + elif isinstance(obj, CompoundValue): + value = obj + data = getattr(obj, 'd_data', None) # may not exist + else: + raise TypeError( + f'Expected a BoundCompoundValue or CompoundValue; got {type(obj)}') + return value, data + + +def compound_eq(obj1: Union[BoundCompoundValue, CompoundValue], + obj2: Union[BoundCompoundValue, CompoundValue]) -> Any: + """Compare two compound value/bound-value objects for equality.""" + + # Criteria for comparison: both need to be a compound value + # and both must have data (which implies they are either a entity + # or bound to a subfield in an entity). + value1, data1 = get_compound_value_and_data(obj1) + if data1 is None: + return NotImplemented + value2, data2 = get_compound_value_and_data(obj2) + if data2 is None: + return NotImplemented + + # Ok we can compare them. To consider them equal we look for + # matching sets of fields and matching data. Note that there + # could be unbound data causing inequality despite their field + # values all matching; not sure if that's what we want. + return have_matching_fields(value1, value2) and data1 == data2 diff --git a/dist/ba_data/python/efro/error.py b/dist/ba_data/python/efro/error.py new file mode 100644 index 0000000..80bd3ce --- /dev/null +++ b/dist/ba_data/python/efro/error.py @@ -0,0 +1,35 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality for dealing with errors.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + pass + + +class CleanError(Exception): + """An error that should be presented to the user as a simple message. + + These errors should be completely self-explanatory, to the point where + a traceback or other context would not be useful. + + A CleanError with no message can be used to inform a script to fail + without printing any message. + + This should generally be limited to errors that will *always* be + presented to the user (such as those in high level tool code). + Exceptions that may be caught and handled by other code should use + more descriptive exception types. + """ + + def pretty_print(self, flush: bool = False) -> None: + """Print the error to stdout, using red colored output if available. + + If the error has an empty message, prints nothing (not even a newline). + """ + from efro.terminal import Clr + errstr = str(self) + if errstr: + print(f'{Clr.SRED}{errstr}{Clr.RST}', flush=flush) diff --git a/dist/ba_data/python/efro/json.py b/dist/ba_data/python/efro/json.py new file mode 100644 index 0000000..f777558 --- /dev/null +++ b/dist/ba_data/python/efro/json.py @@ -0,0 +1,72 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Custom json compressor/decompressor with support for more data times/etc.""" + +from __future__ import annotations + +import datetime +import json +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + +# Special attr we included for our extended type information +# (extended-json-type) +TYPE_TAG = '_xjtp' + +_pytz_utc: Any + +# We don't *require* pytz since it must be installed through pip +# but it is used by firestore client for its utc tzinfos. +# (in which case it should be installed as a dependency anyway) +try: + import pytz + _pytz_utc = pytz.utc +except ModuleNotFoundError: + _pytz_utc = None # pylint: disable=invalid-name + + +class ExtendedJSONEncoder(json.JSONEncoder): + """Custom json encoder supporting additional types.""" + + def default(self, obj: Any) -> Any: # pylint: disable=W0221 + if isinstance(obj, datetime.datetime): + + # We only support timezone-aware utc times. + if (obj.tzinfo is not datetime.timezone.utc + and (_pytz_utc is None or obj.tzinfo is not _pytz_utc)): + raise ValueError( + 'datetime values must have timezone set as timezone.utc') + return { + TYPE_TAG: + 'dt', + 'v': [ + obj.year, obj.month, obj.day, obj.hour, obj.minute, + obj.second, obj.microsecond + ], + } + return super().default(obj) + + +class ExtendedJSONDecoder(json.JSONDecoder): + """Custom json decoder supporting extended types.""" + + def __init__(self, *args: Any, **kwargs: Any): + json.JSONDecoder.__init__(self, + object_hook=self.object_hook, + *args, + **kwargs) + + def object_hook(self, obj: Any) -> Any: # pylint: disable=E0202 + """Custom hook.""" + if TYPE_TAG not in obj: + return obj + objtype = obj[TYPE_TAG] + if objtype == 'dt': + vals = obj.get('v', []) + if len(vals) != 7: + raise ValueError('malformed datetime value') + return datetime.datetime( # type: ignore + *vals, tzinfo=datetime.timezone.utc) + return obj diff --git a/dist/ba_data/python/efro/terminal.py b/dist/ba_data/python/efro/terminal.py new file mode 100644 index 0000000..aafa34f --- /dev/null +++ b/dist/ba_data/python/efro/terminal.py @@ -0,0 +1,305 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to terminal IO.""" +from __future__ import annotations + +import sys +import os +from enum import Enum, unique +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, ClassVar, Type + + +@unique +class TerminalColor(Enum): + """Color codes for printing to terminals. + + Generally the Clr class should be used when incorporating color into + terminal output, as it handles non-color-supporting terminals/etc. + """ + + # Styles + RESET = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + INVERSE = '\033[7m' + + # Normal foreground colors + BLACK = '\033[30m' + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + BLUE = '\033[34m' + MAGENTA = '\033[35m' + CYAN = '\033[36m' + WHITE = '\033[37m' + + # Normal background colors. + BG_BLACK = '\033[40m' + BG_RED = '\033[41m' + BG_GREEN = '\033[42m' + BG_YELLOW = '\033[43m' + BG_BLUE = '\033[44m' + BG_MAGENTA = '\033[45m' + BG_CYAN = '\033[46m' + BG_WHITE = '\033[47m' + + # Strong foreground colors + STRONG_BLACK = '\033[90m' + STRONG_RED = '\033[91m' + STRONG_GREEN = '\033[92m' + STRONG_YELLOW = '\033[93m' + STRONG_BLUE = '\033[94m' + STRONG_MAGENTA = '\033[95m' + STRONG_CYAN = '\033[96m' + STRONG_WHITE = '\033[97m' + + # Strong background colors. + STRONG_BG_BLACK = '\033[100m' + STRONG_BG_RED = '\033[101m' + STRONG_BG_GREEN = '\033[102m' + STRONG_BG_YELLOW = '\033[103m' + STRONG_BG_BLUE = '\033[104m' + STRONG_BG_MAGENTA = '\033[105m' + STRONG_BG_CYAN = '\033[106m' + STRONG_BG_WHITE = '\033[107m' + + +def _default_color_enabled() -> bool: + """Return whether we should enable ANSI color codes by default.""" + import platform + + # If we're not attached to a terminal, go with no-color. + if not sys.__stdout__.isatty(): + return False + + # On windows, try to enable ANSI color mode. + if platform.system() == 'Windows': + return _windows_enable_color() + + # We seem to be a terminal with color support; let's do it! + return True + + +# noinspection PyPep8Naming +def _windows_enable_color() -> bool: + """Attempt to enable ANSI color on windows terminal; return success.""" + # pylint: disable=invalid-name, import-error, undefined-variable + # Pulled from: https://bugs.python.org/issue30075 + import msvcrt + import ctypes + from ctypes import wintypes + kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) # type: ignore + + ERROR_INVALID_PARAMETER = 0x0057 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + + def _check_bool(result: Any, _func: Any, args: Any) -> Any: + if not result: + raise ctypes.WinError(ctypes.get_last_error()) # type: ignore + return args + + LPDWORD = ctypes.POINTER(wintypes.DWORD) + kernel32.GetConsoleMode.errcheck = _check_bool + kernel32.GetConsoleMode.argtypes = (wintypes.HANDLE, LPDWORD) + kernel32.SetConsoleMode.errcheck = _check_bool + kernel32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD) + + def set_conout_mode(new_mode: int, mask: int = 0xffffffff) -> int: + # don't assume StandardOutput is a console. + # open CONOUT$ instead + fdout = os.open('CONOUT$', os.O_RDWR) + try: + hout = msvcrt.get_osfhandle(fdout) # type: ignore + old_mode = wintypes.DWORD() + kernel32.GetConsoleMode(hout, ctypes.byref(old_mode)) + mode = (new_mode & mask) | (old_mode.value & ~mask) + kernel32.SetConsoleMode(hout, mode) + return old_mode.value + finally: + os.close(fdout) + + def enable_vt_mode() -> int: + mode = mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING + try: + return set_conout_mode(mode, mask) + except WindowsError as exc: + if exc.winerror == ERROR_INVALID_PARAMETER: + raise NotImplementedError from exc + raise + + try: + enable_vt_mode() + return True + except NotImplementedError: + return False + + +class ClrBase: + """Base class for color convenience class.""" + RST: ClassVar[str] + BLD: ClassVar[str] + UND: ClassVar[str] + INV: ClassVar[str] + + # Normal foreground colors + BLK: ClassVar[str] + RED: ClassVar[str] + GRN: ClassVar[str] + YLW: ClassVar[str] + BLU: ClassVar[str] + MAG: ClassVar[str] + CYN: ClassVar[str] + WHT: ClassVar[str] + + # Normal background colors. + BBLK: ClassVar[str] + BRED: ClassVar[str] + BGRN: ClassVar[str] + BYLW: ClassVar[str] + BBLU: ClassVar[str] + BMAG: ClassVar[str] + BCYN: ClassVar[str] + BWHT: ClassVar[str] + + # Strong foreground colors + SBLK: ClassVar[str] + SRED: ClassVar[str] + SGRN: ClassVar[str] + SYLW: ClassVar[str] + SBLU: ClassVar[str] + SMAG: ClassVar[str] + SCYN: ClassVar[str] + SWHT: ClassVar[str] + + # Strong background colors. + SBBLK: ClassVar[str] + SBRED: ClassVar[str] + SBGRN: ClassVar[str] + SBYLW: ClassVar[str] + SBBLU: ClassVar[str] + SBMAG: ClassVar[str] + SBCYN: ClassVar[str] + SBWHT: ClassVar[str] + + +class ClrAlways(ClrBase): + """Convenience class for color terminal output. + + This version has colors always enabled. Generally you should use Clr which + points to the correct enabled/disabled class depending on the environment. + """ + color_enabled = True + + # Styles + RST = TerminalColor.RESET.value + BLD = TerminalColor.BOLD.value + UND = TerminalColor.UNDERLINE.value + INV = TerminalColor.INVERSE.value + + # Normal foreground colors + BLK = TerminalColor.BLACK.value + RED = TerminalColor.RED.value + GRN = TerminalColor.GREEN.value + YLW = TerminalColor.YELLOW.value + BLU = TerminalColor.BLUE.value + MAG = TerminalColor.MAGENTA.value + CYN = TerminalColor.CYAN.value + WHT = TerminalColor.WHITE.value + + # Normal background colors. + BBLK = TerminalColor.BG_BLACK.value + BRED = TerminalColor.BG_RED.value + BGRN = TerminalColor.BG_GREEN.value + BYLW = TerminalColor.BG_YELLOW.value + BBLU = TerminalColor.BG_BLUE.value + BMAG = TerminalColor.BG_MAGENTA.value + BCYN = TerminalColor.BG_CYAN.value + BWHT = TerminalColor.BG_WHITE.value + + # Strong foreground colors + SBLK = TerminalColor.STRONG_BLACK.value + SRED = TerminalColor.STRONG_RED.value + SGRN = TerminalColor.STRONG_GREEN.value + SYLW = TerminalColor.STRONG_YELLOW.value + SBLU = TerminalColor.STRONG_BLUE.value + SMAG = TerminalColor.STRONG_MAGENTA.value + SCYN = TerminalColor.STRONG_CYAN.value + SWHT = TerminalColor.STRONG_WHITE.value + + # Strong background colors. + SBBLK = TerminalColor.STRONG_BG_BLACK.value + SBRED = TerminalColor.STRONG_BG_RED.value + SBGRN = TerminalColor.STRONG_BG_GREEN.value + SBYLW = TerminalColor.STRONG_BG_YELLOW.value + SBBLU = TerminalColor.STRONG_BG_BLUE.value + SBMAG = TerminalColor.STRONG_BG_MAGENTA.value + SBCYN = TerminalColor.STRONG_BG_CYAN.value + SBWHT = TerminalColor.STRONG_BG_WHITE.value + + +class ClrNever(ClrBase): + """Convenience class for color terminal output. + + This version has colors disabled. Generally you should use Clr which + points to the correct enabled/disabled class depending on the environment. + """ + color_enabled = False + + # Styles + RST = '' + BLD = '' + UND = '' + INV = '' + + # Normal foreground colors + BLK = '' + RED = '' + GRN = '' + YLW = '' + BLU = '' + MAG = '' + CYN = '' + WHT = '' + + # Normal background colors. + BBLK = '' + BRED = '' + BGRN = '' + BYLW = '' + BBLU = '' + BMAG = '' + BCYN = '' + BWHT = '' + + # Strong foreground colors + SBLK = '' + SRED = '' + SGRN = '' + SYLW = '' + SBLU = '' + SMAG = '' + SCYN = '' + SWHT = '' + + # Strong background colors. + SBBLK = '' + SBRED = '' + SBGRN = '' + SBYLW = '' + SBBLU = '' + SBMAG = '' + SBCYN = '' + SBWHT = '' + + +_envval = os.environ.get('EFRO_TERMCOLORS') +_color_enabled: bool = (True if _envval == '1' else + False if _envval == '0' else _default_color_enabled()) +Clr: Type[ClrBase] +if _color_enabled: + Clr = ClrAlways +else: + Clr = ClrNever diff --git a/dist/ba_data/python/efro/util.py b/dist/ba_data/python/efro/util.py new file mode 100644 index 0000000..9a0a8c4 --- /dev/null +++ b/dist/ba_data/python/efro/util.py @@ -0,0 +1,478 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Small handy bits of functionality.""" + +from __future__ import annotations + +import datetime +import time +import weakref +import functools +from enum import Enum +from typing import TYPE_CHECKING, cast, TypeVar, Generic + +if TYPE_CHECKING: + import asyncio + from efro.call import Call as Call # 'as Call' so we re-export. + from weakref import ReferenceType + from typing import Any, Dict, Callable, Optional, Type + +T = TypeVar('T') +TVAL = TypeVar('TVAL') +TARG = TypeVar('TARG') +TSELF = TypeVar('TSELF') +TRET = TypeVar('TRET') +TENUM = TypeVar('TENUM', bound=Enum) + + +class _EmptyObj: + pass + + +if TYPE_CHECKING: + Call = Call +else: + Call = functools.partial + + +def enum_by_value(cls: Type[TENUM], value: Any) -> TENUM: + """Create an enum from a value. + + This is basically the same as doing 'obj = EnumType(value)' except + that it works around an issue where a reference loop is created + if an exception is thrown due to an invalid value. Since we disable + the cyclic garbage collector for most of the time, such loops can lead + to our objects sticking around longer than we want. + This issue has been submitted to Python as a bug so hopefully we can + remove this eventually if it gets fixed: https://bugs.python.org/issue42248 + """ + + # Note: we don't recreate *ALL* the functionality of the Enum constructor + # such as the _missing_ hook; but this should cover our basic needs. + value2member_map = getattr(cls, '_value2member_map_') + assert value2member_map is not None + try: + out = value2member_map[value] + assert isinstance(out, cls) + return out + except KeyError: + raise ValueError('%r is not a valid %s' % + (value, cls.__name__)) from None + + +def utc_now() -> datetime.datetime: + """Get offset-aware current utc time. + + This should be used for all datetimes getting sent over the network, + used with the entity system, etc. + (datetime.utcnow() gives a utc time value, but it is not timezone-aware + which makes it less safe to use) + """ + return datetime.datetime.now(datetime.timezone.utc) + + +def empty_weakref(objtype: Type[T]) -> ReferenceType[T]: + """Return an invalidated weak-reference for the specified type.""" + # At runtime, all weakrefs are the same; our type arg is just + # for the static type checker. + del objtype # Unused. + # Just create an object and let it die. Is there a cleaner way to do this? + return weakref.ref(_EmptyObj()) # type: ignore + + +def data_size_str(bytecount: int) -> str: + """Given a size in bytes, returns a short human readable string. + + This should be 6 or fewer chars for most all sane file sizes. + """ + # pylint: disable=too-many-return-statements + if bytecount <= 999: + return f'{bytecount} B' + kbytecount = bytecount / 1024 + if round(kbytecount, 1) < 10.0: + return f'{kbytecount:.1f} KB' + if round(kbytecount, 0) < 999: + return f'{kbytecount:.0f} KB' + mbytecount = bytecount / (1024 * 1024) + if round(mbytecount, 1) < 10.0: + return f'{mbytecount:.1f} MB' + if round(mbytecount, 0) < 999: + return f'{mbytecount:.0f} MB' + gbytecount = bytecount / (1024 * 1024 * 1024) + if round(gbytecount, 1) < 10.0: + return f'{mbytecount:.1f} GB' + return f'{gbytecount:.0f} GB' + + +class DirtyBit: + """Manages whether a thing is dirty and regulates attempts to clean it. + + To use, simply set the 'dirty' value on this object to True when some + action is needed, and then check the 'should_update' value to regulate + when attempts to clean it should be made. Set 'dirty' back to False after + a successful update. + If 'use_lock' is True, an asyncio Lock will be created and incorporated + into update attempts to prevent simultaneous updates (should_update will + only return True when the lock is unlocked). Note that It is up to the user + to lock/unlock the lock during the actual update attempt. + If a value is passed for 'auto_dirty_seconds', the dirtybit will flip + itself back to dirty after being clean for the given amount of time. + 'min_update_interval' can be used to enforce a minimum update + interval even when updates are successful (retry_interval only applies + when updates fail) + """ + + def __init__(self, + dirty: bool = False, + retry_interval: float = 5.0, + use_lock: bool = False, + auto_dirty_seconds: float = None, + min_update_interval: Optional[float] = None): + curtime = time.time() + self._retry_interval = retry_interval + self._auto_dirty_seconds = auto_dirty_seconds + self._min_update_interval = min_update_interval + self._dirty = dirty + self._next_update_time: Optional[float] = (curtime if dirty else None) + self._last_update_time: Optional[float] = None + self._next_auto_dirty_time: Optional[float] = ( + (curtime + self._auto_dirty_seconds) if + (not dirty and self._auto_dirty_seconds is not None) else None) + self._use_lock = use_lock + self.lock: asyncio.Lock + if self._use_lock: + import asyncio + self.lock = asyncio.Lock() + + @property + def dirty(self) -> bool: + """Whether the target is currently dirty. + + This should be set to False once an update is successful. + """ + return self._dirty + + @dirty.setter + def dirty(self, value: bool) -> None: + + # If we're freshly clean, set our next auto-dirty time (if we have + # one). + if self._dirty and not value and self._auto_dirty_seconds is not None: + self._next_auto_dirty_time = time.time() + self._auto_dirty_seconds + + # If we're freshly dirty, schedule an immediate update. + if not self._dirty and value: + self._next_update_time = time.time() + + # If they want to enforce a minimum update interval, + # push out the next update time if it hasn't been long enough. + if (self._min_update_interval is not None + and self._last_update_time is not None): + self._next_update_time = max( + self._next_update_time, + self._last_update_time + self._min_update_interval) + + self._dirty = value + + @property + def should_update(self) -> bool: + """Whether an attempt should be made to clean the target now. + + Always returns False if the target is not dirty. + Takes into account the amount of time passed since the target + was marked dirty or since should_update last returned True. + """ + curtime = time.time() + + # Auto-dirty ourself if we're into that. + if (self._next_auto_dirty_time is not None + and curtime > self._next_auto_dirty_time): + self.dirty = True + self._next_auto_dirty_time = None + if not self._dirty: + return False + if self._use_lock and self.lock.locked(): + return False + assert self._next_update_time is not None + if curtime > self._next_update_time: + self._next_update_time = curtime + self._retry_interval + self._last_update_time = curtime + return True + return False + + +class DispatchMethodWrapper(Generic[TARG, TRET]): + """Type-aware standin for the dispatch func returned by dispatchmethod.""" + + def __call__(self, arg: TARG) -> TRET: + pass + + @staticmethod + def register(func: Callable[[Any, Any], TRET]) -> Callable: + """Register a new dispatch handler for this dispatch-method.""" + + registry: Dict[Any, Callable] + + +# noinspection PyProtectedMember,PyTypeHints +def dispatchmethod( + func: Callable[[Any, TARG], + TRET]) -> DispatchMethodWrapper[TARG, TRET]: + """A variation of functools.singledispatch for methods. + + Note: as of Python 3.9 there is now functools.singledispatchmethod, + but it currently (as of Jan 2021) is not type-aware (at least in mypy), + which gives us a reason to keep this one around for now. + """ + from functools import singledispatch, update_wrapper + origwrapper: Any = singledispatch(func) + + # Pull this out so hopefully origwrapper can die, + # otherwise we reference origwrapper in our wrapper. + dispatch = origwrapper.dispatch + + # All we do here is recreate the end of functools.singledispatch + # where it returns a wrapper except instead of the wrapper using the + # first arg to the function ours uses the second (to skip 'self'). + # This was made against Python 3.7; we should probably check up on + # this in later versions in case anything has changed. + # (or hopefully they'll add this functionality to their version) + # NOTE: sounds like we can use functools singledispatchmethod in 3.8 + def wrapper(*args: Any, **kw: Any) -> Any: + if not args or len(args) < 2: + raise TypeError(f'{funcname} requires at least ' + '2 positional arguments') + + return dispatch(args[1].__class__)(*args, **kw) + + funcname = getattr(func, '__name__', 'dispatchmethod method') + wrapper.register = origwrapper.register # type: ignore + wrapper.dispatch = dispatch # type: ignore + wrapper.registry = origwrapper.registry # type: ignore + # pylint: disable=protected-access + wrapper._clear_cache = origwrapper._clear_cache # type: ignore + update_wrapper(wrapper, func) + # pylint: enable=protected-access + return cast(DispatchMethodWrapper, wrapper) + + +def valuedispatch(call: Callable[[TVAL], TRET]) -> ValueDispatcher[TVAL, TRET]: + """Decorator for functions to allow dispatching based on a value. + + This differs from functools.singledispatch in that it dispatches based + on the value of an argument, not based on its type. + The 'register' method of a value-dispatch function can be used + to assign new functions to handle particular values. + Unhandled values wind up in the original dispatch function.""" + return ValueDispatcher(call) + + +class ValueDispatcher(Generic[TVAL, TRET]): + """Used by the valuedispatch decorator""" + + def __init__(self, call: Callable[[TVAL], TRET]) -> None: + self._base_call = call + self._handlers: Dict[TVAL, Callable[[], TRET]] = {} + + def __call__(self, value: TVAL) -> TRET: + handler = self._handlers.get(value) + if handler is not None: + return handler() + return self._base_call(value) + + def _add_handler(self, value: TVAL, call: Callable[[], TRET]) -> None: + if value in self._handlers: + raise RuntimeError(f'Duplicate handlers added for {value}') + self._handlers[value] = call + + def register(self, value: TVAL) -> Callable[[Callable[[], TRET]], None]: + """Add a handler to the dispatcher.""" + from functools import partial + return partial(self._add_handler, value) + + +def valuedispatch1arg( + call: Callable[[TVAL, TARG], + TRET]) -> ValueDispatcher1Arg[TVAL, TARG, TRET]: + """Like valuedispatch but for functions taking an extra argument.""" + return ValueDispatcher1Arg(call) + + +class ValueDispatcher1Arg(Generic[TVAL, TARG, TRET]): + """Used by the valuedispatch1arg decorator""" + + def __init__(self, call: Callable[[TVAL, TARG], TRET]) -> None: + self._base_call = call + self._handlers: Dict[TVAL, Callable[[TARG], TRET]] = {} + + def __call__(self, value: TVAL, arg: TARG) -> TRET: + handler = self._handlers.get(value) + if handler is not None: + return handler(arg) + return self._base_call(value, arg) + + def _add_handler(self, value: TVAL, call: Callable[[TARG], TRET]) -> None: + if value in self._handlers: + raise RuntimeError(f'Duplicate handlers added for {value}') + self._handlers[value] = call + + def register(self, + value: TVAL) -> Callable[[Callable[[TARG], TRET]], None]: + """Add a handler to the dispatcher.""" + from functools import partial + return partial(self._add_handler, value) + + +if TYPE_CHECKING: + + class ValueDispatcherMethod(Generic[TVAL, TRET]): + """Used by the valuedispatchmethod decorator.""" + + def __call__(self, value: TVAL) -> TRET: + ... + + def register(self, + value: TVAL) -> Callable[[Callable[[TSELF], TRET]], None]: + """Add a handler to the dispatcher.""" + ... + + +def valuedispatchmethod( + call: Callable[[TSELF, TVAL], + TRET]) -> ValueDispatcherMethod[TVAL, TRET]: + """Like valuedispatch but works with methods instead of functions.""" + + # NOTE: It seems that to wrap a method with a decorator and have self + # dispatching do the right thing, we must return a function and not + # an executable object. So for this version we store our data here + # in the function call dict and simply return a call. + + _base_call = call + _handlers: Dict[TVAL, Callable[[TSELF], TRET]] = {} + + def _add_handler(value: TVAL, addcall: Callable[[TSELF], TRET]) -> None: + if value in _handlers: + raise RuntimeError(f'Duplicate handlers added for {value}') + _handlers[value] = addcall + + def _register(value: TVAL) -> Callable[[Callable[[TSELF], TRET]], None]: + from functools import partial + return partial(_add_handler, value) + + def _call_wrapper(self: TSELF, value: TVAL) -> TRET: + handler = _handlers.get(value) + if handler is not None: + return handler(self) + return _base_call(self, value) + + # We still want to use our returned object to register handlers, but we're + # actually just returning a function. So manually stuff the call onto it. + setattr(_call_wrapper, 'register', _register) + + # To the type checker's eyes we return a ValueDispatchMethod instance; + # this lets it know about our register func and type-check its usage. + # In reality we just return a raw function call (for reasons listed above). + if TYPE_CHECKING: # pylint: disable=no-else-return + return ValueDispatcherMethod[TVAL, TRET]() + else: + return _call_wrapper + + +def make_hash(obj: Any) -> int: + """Makes a hash from a dictionary, list, tuple or set to any level, + that contains only other hashable types (including any lists, tuples, + sets, and dictionaries). + + Note that this uses Python's hash() function internally so collisions/etc. + may be more common than with fancy cryptographic hashes. + + Also be aware that Python's hash() output varies across processes, so + this should only be used for values that will remain in a single process. + """ + import copy + + if isinstance(obj, (set, tuple, list)): + return hash(tuple(make_hash(e) for e in obj)) + if not isinstance(obj, dict): + return hash(obj) + + new_obj = copy.deepcopy(obj) + for k, v in new_obj.items(): + new_obj[k] = make_hash(v) + + # NOTE: there is sorted works correctly because it compares only + # unique first values (i.e. dict keys) + return hash(tuple(frozenset(sorted(new_obj.items())))) + + +def asserttype(obj: Any, typ: Type[T]) -> T: + """Return an object typed as a given type. + + Assert is used to check its actual type, so only use this when + failures are not expected. Otherwise use checktype. + """ + assert isinstance(obj, typ) + return obj + + +def checktype(obj: Any, typ: Type[T]) -> T: + """Return an object typed as a given type. + + Always checks the type at runtime with isinstance and throws a TypeError + on failure. Use asserttype for more efficient (but less safe) equivalent. + """ + if not isinstance(obj, typ): + raise TypeError(f'Expected a {typ}; got a {type(obj)}.') + return obj + + +def warntype(obj: Any, typ: Type[T]) -> T: + """Return an object typed as a given type. + + Always checks the type at runtime and simply logs a warning if it is + not what is expected. + """ + if not isinstance(obj, typ): + import logging + logging.warning('warntype: expected a %s, got a %s', typ, type(obj)) + return obj # type: ignore + + +def assert_non_optional(obj: Optional[T]) -> T: + """Return an object with Optional typing removed. + + Assert is used to check its actual type, so only use this when + failures are not expected. Use check_non_optional otherwise. + """ + assert obj is not None + return obj + + +def check_non_optional(obj: Optional[T]) -> T: + """Return an object with Optional typing removed. + + Always checks the actual type and throws a TypeError on failure. + Use assert_non_optional for a more efficient (but less safe) equivalent. + """ + if obj is None: + raise TypeError('Got None value in check_non_optional.') + return obj + + +def smoothstep(edge0: float, edge1: float, x: float) -> float: + """A smooth transition function. + + Returns a value that smoothly moves from 0 to 1 as we go between edges. + Values outside of the range return 0 or 1. + """ + y = min(1.0, max(0.0, (x - edge0) / (edge1 - edge0))) + return y * y * (3.0 - 2.0 * y) + + +def linearstep(edge0: float, edge1: float, x: float) -> float: + """A linear transition function. + + Returns a value that linearly moves from 0 to 1 as we go between edges. + Values outside of the range return 0 or 1. + """ + return max(0.0, min(1.0, (x - edge0) / (edge1 - edge0))) diff --git a/dist/ba_root/config.json b/dist/ba_root/config.json new file mode 100644 index 0000000..b099ee4 --- /dev/null +++ b/dist/ba_root/config.json @@ -0,0 +1,167 @@ +{ + "Achievements": { + "Boom Goes the Dynamite": { + "Complete": false + }, + "Boxer": { + "Complete": false + }, + "Dual Wielding": { + "Complete": false + }, + "Flawless Victory": { + "Complete": false + }, + "Free Loader": { + "Complete": true + }, + "Gold Miner": { + "Complete": false + }, + "Got the Moves": { + "Complete": false + }, + "In Control": { + "Complete": false + }, + "Last Stand God": { + "Complete": false + }, + "Last Stand Master": { + "Complete": false + }, + "Last Stand Wizard": { + "Complete": false + }, + "Mine Games": { + "Complete": false + }, + "Off You Go Then": { + "Complete": false + }, + "Onslaught God": { + "Complete": false + }, + "Onslaught Master": { + "Complete": false + }, + "Onslaught Training Victory": { + "Complete": false + }, + "Onslaught Wizard": { + "Complete": false + }, + "Precision Bombing": { + "Complete": false + }, + "Pro Boxer": { + "Complete": false + }, + "Pro Football Shutout": { + "Complete": false + }, + "Pro Football Victory": { + "Complete": false + }, + "Pro Onslaught Victory": { + "Complete": false + }, + "Pro Runaround Victory": { + "Complete": false + }, + "Rookie Football Shutout": { + "Complete": false + }, + "Rookie Football Victory": { + "Complete": false + }, + "Rookie Onslaught Victory": { + "Complete": false + }, + "Runaround God": { + "Complete": false + }, + "Runaround Master": { + "Complete": false + }, + "Runaround Wizard": { + "Complete": false + }, + "Sharing is Caring": { + "Complete": false + }, + "Stayin' Alive": { + "Complete": false + }, + "Super Mega Punch": { + "Complete": false + }, + "Super Punch": { + "Complete": false + }, + "TNT Terror": { + "Complete": false + }, + "Team Player": { + "Complete": false + }, + "The Great Wall": { + "Complete": false + }, + "The Wall": { + "Complete": false + }, + "Uber Football Shutout": { + "Complete": false + }, + "Uber Football Victory": { + "Complete": false + }, + "Uber Onslaught Victory": { + "Complete": false + }, + "Uber Runaround Victory": { + "Complete": false + } + }, + "Auto Account State": "Server", + "Auto Balance Teams": true, + "Campaigns": {}, + "Default Player Profiles": { + "Client Input Device #1": "__account__", + "Client Input Device #2": "__account__", + "Client Input Device #3": "__account__" + }, + "Free-for-All Playlist Randomize": true, + "Free-for-All Playlist Selection": "__default__", + "Free-for-All Playlists": {}, + "Idle Exit Minutes": null, + "Local Account Name": "Server137026", + "Player Profiles": { + "__account__": { + "character": "Spaz", + "color": [ + 0.5, + 0.25, + 1.0 + ], + "highlight": [ + 0.5, + 0.25, + 1.0 + ] + } + }, + "Plugins": { + "privateserver.private": { + "enabled": true + } + }, + "Port": 43210, + "Show Tutorial": false, + "Signed In Last Session": false, + "Team Tournament Playlists": {}, + "launchCount": 24, + "lc14173": 1, + "lc14292": 1 +} \ No newline at end of file diff --git a/dist/ba_root/config.json.prev b/dist/ba_root/config.json.prev new file mode 100644 index 0000000..0844937 --- /dev/null +++ b/dist/ba_root/config.json.prev @@ -0,0 +1 @@ +{"Achievements": {"Boom Goes the Dynamite": {"Complete": false}, "Boxer": {"Complete": false}, "Dual Wielding": {"Complete": false}, "Flawless Victory": {"Complete": false}, "Free Loader": {"Complete": true}, "Gold Miner": {"Complete": false}, "Got the Moves": {"Complete": false}, "In Control": {"Complete": false}, "Last Stand God": {"Complete": false}, "Last Stand Master": {"Complete": false}, "Last Stand Wizard": {"Complete": false}, "Mine Games": {"Complete": false}, "Off You Go Then": {"Complete": false}, "Onslaught God": {"Complete": false}, "Onslaught Master": {"Complete": false}, "Onslaught Training Victory": {"Complete": false}, "Onslaught Wizard": {"Complete": false}, "Precision Bombing": {"Complete": false}, "Pro Boxer": {"Complete": false}, "Pro Football Shutout": {"Complete": false}, "Pro Football Victory": {"Complete": false}, "Pro Onslaught Victory": {"Complete": false}, "Pro Runaround Victory": {"Complete": false}, "Rookie Football Shutout": {"Complete": false}, "Rookie Football Victory": {"Complete": false}, "Rookie Onslaught Victory": {"Complete": false}, "Runaround God": {"Complete": false}, "Runaround Master": {"Complete": false}, "Runaround Wizard": {"Complete": false}, "Sharing is Caring": {"Complete": false}, "Stayin' Alive": {"Complete": false}, "Super Mega Punch": {"Complete": false}, "Super Punch": {"Complete": false}, "TNT Terror": {"Complete": false}, "Team Player": {"Complete": false}, "The Great Wall": {"Complete": false}, "The Wall": {"Complete": false}, "Uber Football Shutout": {"Complete": false}, "Uber Football Victory": {"Complete": false}, "Uber Onslaught Victory": {"Complete": false}, "Uber Runaround Victory": {"Complete": false}}, "Auto Account State": "Server", "Auto Balance Teams": true, "Campaigns": {}, "Default Player Profiles": {"Client Input Device #1": "__account__", "Client Input Device #2": "__account__", "Client Input Device #3": "__account__"}, "Free-for-All Playlist Randomize": true, "Free-for-All Playlist Selection": "__default__", "Free-for-All Playlists": {}, "Idle Exit Minutes": null, "Local Account Name": "Server137026", "Player Profiles": {"__account__": {"character": "Spaz", "color": [0.5, 0.25, 1.0], "highlight": [0.5, 0.25, 1.0]}}, "Plugins": {"privateserver.private": {"enabled": true}}, "Port": 43210, "Show Tutorial": false, "Signed In Last Session": false, "Team Tournament Playlists": {}, "launchCount": 23, "lc14173": 1, "lc14292": 1} \ No newline at end of file diff --git a/dist/ba_root/mods/privateserver.py b/dist/ba_root/mods/privateserver.py new file mode 100644 index 0000000..cd167a7 --- /dev/null +++ b/dist/ba_root/mods/privateserver.py @@ -0,0 +1,152 @@ +"""Define a simple example plugin.""" + +# ba_meta require api 6 + +# Private Server whitelist by Mr.Smoothy +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba,json,_ba,time + +if TYPE_CHECKING: + pass +import datetime +from ba._enums import TimeType + + +whitelist_on=True # change it by chat commands for by editing here +spectators=False # ,, ,, ,, ,, False means spectating not allowed +whitelist={} # dont change +lobbychecktime=3 # time in seconds, to check lobby players ... increase time ,for more time unwanted players can watch match + # decrease time , kick them fast , but can also give some lagg to the server , adjust yourself acrd. to cpu power + +admins=['pb-JiNJARBaXEFBVF9HFkNXXF1EF0ZaRlZE'] # dirty admin system , for now , until we get good working chat commands + + +def inWhiteList(id): + global whitelist + if id in whitelist: + return True + else: + return False +def addToWhitelist(id,displaystr): + global whitelist + if id not in whitelist: + whitelist[id]=[displaystr] + else: + whitelist[id].append(displaystr) + f=open("whitelist.json","w") + json.dump(whitelist,f,indent=4) + f.close() + +def handlechat(msg,clientid): + gg=_ba.get_game_roster() + acc_id="LOL" + if msg.startswith("/"): + for clt in gg: + if clt['client_id'] ==clientid: + acc_id=clt['account_id'] + global admins + if acc_id in admins: + commands(acc_id ,msg) +def handlerequest(player): + if whitelist_on: + if inWhiteList(player.get_account_id()): + pass + else: + for clt in _ba.get_game_roster(): + if clt['account_id']==player.get_account_id(): + + f=open("loggs.txt",'a+') + f.write("kicked for joining"+clt['account_id']+"\n") + f.close() + _ba.disconnect_client(clt['client_id']) + + + + +def commands(acc_id,msg): + global whitelist + global whitelist_on + global spectators + cmnd=msg.split(" ")[0] + + args=msg.split(" ")[1:] + if cmnd=='/add' and args!=[]: + + gg=_ba.get_game_roster() + for clt in gg: + if clt['client_id']==int(args[0]): + + addToWhitelist(clt['account_id'],clt['display_string']) + f=open("loggs.txt",'a+') + f.write(acc_id+" added "+clt['account_id']+"\n") + f.close() + ba.screenmessage(clt['display_string']+" whitelisted") + if cmnd=='/whitelist': + whitelist_on=whitelist_on==False + if whitelist_on: + ba.screenmessage("WhiteList turned on") + else: + ba.screenmessage("whitelist turned off") + if cmnd=='/spectators': + spectators=spectators==False + if spectators: + ba.screenmessage("Spectators can watch now") + else: + ba.screenmessage("Spectators will be kicked") + + + +def dstrinWhiteList(dstr): + global whitelist + return any(dstr in chici for chici in whitelist.values()) + + +# ba_meta export plugin +class private(ba.Plugin): + """My first ballistica plugin!""" + + def __init__(self): + global whitelist + global whitelist_on + global spectators + global lobbychecktime + + try: + f=open("whitelist.json") + dat=json.loads(f.read()) + whitelist=dat + f.close() + except: + print("no whitelist detected , creating one") + self.li={} + self.li['pb-JiNJARBaXEFBVF9HFkNXXF1EF0ZaRlZE']=['smoothyki-id','mr.smoothy'] + f=open("whitelist.json",'w') + json.dump(self.li,f,indent=4) + f.close() + if whitelist_on and not spectators: + self.timerr=ba.Timer(lobbychecktime,self.checklobby,repeat=True,timetype=TimeType.REAL) + def checklobby(self): + global whitelist_on + global whitelist + global spectators + + try: + gg=_ba.get_game_roster() + for clt in gg: + if clt['account_id'] in whitelist and clt['account_id']!='': + pass + else: + f=open("loggs.txt","a+") + f.write("Kicked from lobby"+clt['account_id']+" "+clt['spec_string']+"\n") + _ba.disconnect_client(clt['client_id']) + except: + pass + + + + + + diff --git a/dist/ballisticacore b/dist/ballisticacore new file mode 100644 index 0000000..2950192 Binary files /dev/null and b/dist/ballisticacore differ diff --git a/dist/prefablib/libballisticacore_internal.a b/dist/prefablib/libballisticacore_internal.a new file mode 100644 index 0000000..29b3d76 Binary files /dev/null and b/dist/prefablib/libballisticacore_internal.a differ