diff --git a/.gitignore b/.gitignore index 532acbca..00713148 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,28 @@ +# compiled Python files *.pyc -db.sqlite3 -TestLogs -.idea \ No newline at end of file + +# dev database +/db.sqlite3 + +# test data +/TestLogs + +# soloraidar output +/Output + +# IntelliJ IDEA +.idea + +# iPython +.ipynb_checkpoints +*.ipynb + +# Local settings, possibly with sensitive info +# (copy from `gw2raidar/settings_local.py.example`) +/gw2raidar/settings_local.py + +# result of `python3 manage.py collectstatic --noinput` +/static + +# secret stuff +/credentials diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 00000000..2d2f8eb2 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,134 @@ +How to pull off the WSGI+PostgreSQL deploy + +``` +apt install python3-dev python3-pip apache2 apache2-dev git postgresql-9.6 +``` + +Create the postgres user + +``` +sudo -u postgres psql +CREATE ROLE [user] LOGIN PASSWORD '[password]'; +CREATE DATABASE [database] WITH OWNER = [user]; +``` + +Download `mod_wsgi` source + +``` +sudo su - +wget [mod_wsgi_url] +tar xzf [mod_wsgi].tar.gz +cd [mod_wsgi_dir] +./configure --with-python=`which python3` +make +make install +``` + + +create `/etc/apache2/mods-available/wsgi.load`: + +``` +LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.so +WSGIApplicationGroup %{GLOBAL} +``` + +Then + +``` +a2enmod wsgi +certbot --apache +``` + +Assume the app will be called `gw2r-test`. +Put the following into `/etc/apache2/sites-enabled/000-default-le-ssl.conf`, somewhere inside `VirtualHost`: + +``` +Alias /gw2r-test/static/ /var/www/apps/gw2r-test/static/ +WSGIDaemonProcess gw2r-test processes=2 threads=15 display-name=%{GROUP} python-path=/var/www/apps/gw2r-test +WSGIProcessGroup gw2r-test +WSGIScriptAlias /gw2r-test /var/www/apps/gw2r-test/gw2raidar/wsgi.py process-group=gw2r-test + + + Require all granted + + +``` + +Get GW2Raidar (assuming `develop` branch): + +``` +mkdir /var/www/apps +cd /var/www/apps +git clone git@github.com:merforga/gw2raidar.git -b develop gw2r-test +cd gw2r-test +cp gw2raidar/settings_local.py.example gw2raidar/settings_local.py +``` + +Edit `gw2raidar/settings_local.py` to prepare for WSGI: + +``` +DEBUG = False + +# When DEBUG is False (must for production), +# this needs to be set for the app's host, like so: +ALLOWED_HOSTS = ['139.162.68.156'] + +# This needs to be secret and can't be committed to the repository +SECRET_KEY = '[secretkey]' + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'gw2r_test', + 'USER': 'gw2raidar', + 'PASSWORD': '[password]', + 'HOST': '127.0.0.1', + 'PORT': '5432', + } +} + +EMAIL_SUBJECT_PREFIX = '[gw2raidar] ' + +# normal mail (like password reset), to users +DEFAULT_FROM_EMAIL = 'gw2raidar@example.com' + +# error emails, to admins +SERVER_EMAIL = 'gw2raidar@example.com' # + +# the abovementioned admins +# ADMINS = [ +# ('Admin1', 'admin1@example.com'), +# ('Admin2', 'admin2@example.com'), +# ] + + +# SMTP server +# You can use a fake SMTPD that will print any "sent" emails to console: +# +# python -m smtpd -n -c DebuggingServer localhost:1025 +# +# This would require 'EMAIL_PORT = 1025`. +# Obviously, in production, point it to a real SMTP server, where the default +# `EMAIL_PORT = 25` should be okay. +EMAIL_HOST = 'localhost' +# EMAIL_PORT = 1025 + +STATIC_URL = '/gw2r-test/static/' +STATIC_ROOT = '/var/www/apps/gw2r-test/static/' +``` + + +# Adding a user + +``` +useradd -m amadan +passwd amadan +mkdir ~amadan/.ssh +cd ~amadan/.ssh +cat > authorized_hosts +chown amadan:amadan . authorized_hosts +chmod og-rwx . authorized_hosts +usermod -aG sudo amadan + diff --git a/README.md b/README.md index 8e64acae..162da0e9 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,13 @@ GW2 Raidar Quickstart ---------- -* Install Python 3 -* `pip3 install django` +* Install Python 3 and pip3 +* `pip3 install django pandas requests django-taggit psycopg2 google-api-python-client` * `python3 manage.py migrate` * `python3 manage.py createsuperuser` * `python3 manage.py runserver` * Browse to http://localhost:8000/ + +To generate statistics, use `python3 manage.py restat [-f] [-v{0,1,2,3}]`. + +To process new uploads, use `python3 manage.py process_uploads [-v{0,1,2,3}]`. diff --git a/analyser/analyser.py b/analyser/analyser.py index e083e704..44d26380 100644 --- a/analyser/analyser.py +++ b/analyser/analyser.py @@ -1,137 +1,605 @@ - -from enum import Enum +from enum import IntEnum from evtcparser import * +import pandas as pd +import numpy as np +from functools import reduce +from .collector import * +from .buffs import * +from .splits import * +from .bossmetrics import * +from .bosses import * -class BasicMetric: - def __init__(self, data): - self.data = data - - def __iter__(self): - return iter(self.data.items()) - -class StructuredMetric: - def __iter__(self): - return filter(lambda a: a[0][0] != '_', vars(self).items()) - -class SkillDamageMetric(BasicMetric): - def add_damage(self, skill_name, damage): - self.data[skill_name] = self.data.get(skill_name, 0) + damage - -class TeamDPSMetric(StructuredMetric): - def __init__(self, player_dps): - self.player_dps = player_dps - self.total_damage = sum(map(lambda a: a.total_damage, player_dps.values())) - self.total_condi = sum(map(lambda a: a.total_condi, player_dps.values())) - self.total_power = sum(map(lambda a: a.total_power, player_dps.values())) - self.dps = sum(map(lambda a: a.dps, player_dps.values())) - self.dps_condi = sum(map(lambda a: a.dps_condi, player_dps.values())) - self.dps_power = sum(map(lambda a: a.dps_power, player_dps.values())) - -class PlayerDPSMetric(StructuredMetric): - def __init__(self): - self.total_damage = 0 - self.total_condi = 0 - self.total_power = 0 - self.total_skill_damage = SkillDamageMetric({}) - - self._hits = 0 - self._crits = 0 - - self.dps = None - self.dps_condi = None - self.dps_power = None - self.crit_rate = None - - def value(self): - return self.dps - - def add_damage(self, skill_name, target_inst_id, damage, is_condi, is_crit): - self.total_damage += damage - self.total_skill_damage.add_damage(skill_name, damage) - if is_condi: - self.total_condi += damage - else: - self.total_power += damage - self._hits += 1 - if is_crit: - self._crits += 1 - - def end(self, time): - self.dps = self.total_damage / time - self.dps_condi = self.total_condi / time - self.dps_power = self.total_power / time - self.dps = self.total_damage / time - - if self._hits > 0: - self.crit_rate = self._crits / self._hits +# DEBUG +from sys import exit +import timeit -class LogType(Enum): +class LogType(IntEnum): UNKNOWN = 0 POWER = 1 CONDI = 2 - BUFF = 3 + APPLY = 3 + ACTIVATION = 4 + STATUSREMOVE = 5 + +class Archetype(IntEnum): + POWER = 1 + CONDI = 2 + TANK = 3 HEAL = 4 + SUPPORT = 5 +class Elite(IntEnum): + CORE = 0 + HEART_OF_THORNS = 1 + PATH_OF_FIRE = 2 - @staticmethod - def of(event): - if event.buff: - if event.buff_dmg > 0: - return LogType.CONDI - else: - return LogType.BUFF - elif event.state_change == parser.StateChange.NORMAL: - return LogType.POWER +class Specialization(IntEnum): + NONE = 0 + DRUID = 5 + DAREDEVIL = 7 + BERSERKER = 18 + DRAGONHUNTER = 27 + REAPER = 34 + CHRONOMANCER = 40 + SCRAPPER = 43 + TEMPEST = 48 + HERALD = 52 + SOULBEAST = 55 + WEAVER = 56 + HOLOSMITH = 57 + DEADEYE = 58 + MIRAGE = 59 + SCOURGE = 60 + SPELLBREAKER = 61 + FIREBRAND = 62 + RENEGADE = 63 + +def per_second(f): + return portion_of(f, ContextType.DURATION) + +def percentage_per_second(f): + return portion_of(percentage, ContextType.DURATION) + +def percentage_per_second_per_dst(f): + return portion_of2(percentage, ContextType.DESTINATIONS, ContextType.DURATION) + +def per_second_per_dst(f): + return portion_of2(f, ContextType.DESTINATIONS, ContextType.DURATION) + +def assign_event_types(events): + events['type'] = np.where( + events['is_activation'] != parser.Activation.NONE, LogType.ACTIVATION, + # non-activation events + np.where(events['is_buffremove'] != 0, LogType.STATUSREMOVE, + + # non-statusremove events + np.where(events['buff'] == 0, LogType.POWER, + + # buff events + np.where(events['buff_dmg'] != 0, LogType.CONDI, + LogType.APPLY)))) - return LogType.UNKNOWN + #print(events.groupby('type').count()) + return events +class EvtcAnalysisException(BaseException): + pass + +def only_entry(frame): + return frame.iloc[0] if not frame.empty else None + +def unique_names(dictionary): + unique = dict() + existing_names = set() + for key in dictionary: + base_name = dictionary[key] + name = base_name + index = 1 + while name in existing_names: + index += 1 + name = "{0}-{1}".format(base_name, index) + unique[key] = name + existing_names.add(name) + return unique + +def create_mapping(df, column): + return unique_names(df.to_dict()[column]) + +def filter_damage_events(events): + damage_events = events[(events.type == LogType.POWER) |(events.type == LogType.CONDI)] + damage_events = damage_events.assign(damage = + np.where(damage_events.type == LogType.POWER, + damage_events['value'], + damage_events['buff_dmg'])) + return damage_events[damage_events.damage > 0] + +def print_frame(df, *mods): + dfc = df.copy() + for name,new_name,func in mods: + dfc[new_name] = (dfc.index if name == 'index' else dfc[name]).apply(func) + with pd.option_context('display.max_rows', 9999999, 'display.max_columns', 500, 'display.height', 100000, 'display.width', 100000): + print(dfc) class Analyser: + def preprocess_agents(self, agents, collector, events): + #Add hit count column + agents_that_get_hit_a_lot = events[(events.type == LogType.POWER) + & (events.value > 0)][ + ['dst_instid']].groupby('dst_instid').size().rename('hit_count') + agents = agents.join(agents_that_get_hit_a_lot) + agents.hit_count.fillna(0, inplace=True) + + #identify specific ones we care about + players = agents[(agents.prof >= 1) & (agents.prof <= 9)] + + if len(players) < 1: + raise EvtcAnalysisException("No players found in this log") + elif len(players) > 50: + raise EvtcAnalysisException("Too many players found in this log: {0}".format(len(agents))) + + if not players[players.party == 0].empty: + for player in players.index.values: + agents.loc[player, 'party'] = 1 + players = agents[(agents.prof >= 1) & (agents.prof <= 9)] + + bosses = agents[(agents.prof.isin(self.boss_info.boss_ids)) | + (self.boss_info.has_structure_boss + & (agents.prof < 0) + & (agents.hit_count >= 100))] + final_bosses = agents[agents.prof == self.boss_info.boss_ids[-1]] + + #set up important preprocessed data + self.subgroups = dict([(number, subgroup.index.values) for number, subgroup in players.groupby("party")]) + + self.player_instids = players.index.values + self.boss_instids = bosses.index.values + + print(self.boss_instids) + self.final_boss_instids = final_bosses.index.values + collector.set_context_value(ContextType.AGENT_NAME, create_mapping(agents, 'name')) + return players, bosses, final_bosses + + def preprocess_events(self, events): + #experimental phase calculations + events['ult_src_instid'] = events.src_master_instid.where( + events.src_master_instid != 0, events.src_instid) + + player_src_events = events[events.ult_src_instid.isin(self.player_instids)].sort_values(by='time') + + player_dst_events = events[events.dst_instid.isin(self.player_instids)].sort_values(by='time') + from_boss_events = events[events.src_instid.isin(self.boss_instids)] + to_boss_events = events[events.dst_instid.isin(self.boss_instids)] + from_final_boss_events = from_boss_events[from_boss_events.src_instid.isin(self.final_boss_instids)] + + #construct frame of all power damage to boss, including deltas since last hit. + boss_power_events = to_boss_events[(to_boss_events.type == LogType.POWER) & (to_boss_events.value > 0)] + deltas = boss_power_events.time - boss_power_events.time.shift(1) + boss_power_events = boss_power_events.assign(delta = deltas) + #print_frame(boss_power_events[boss_power_events.delta >= 3000]) + #construct frame of all health updates from the boss + health_updates = from_boss_events[(from_boss_events.state_change == parser.StateChange.HEALTH_UPDATE) + & (from_boss_events.dst_agent > 0)] + #print_frame(health_updates) + + #construct frame of all boss skill activations + boss_skill_activations = from_boss_events[from_boss_events.is_activation != parser.Activation.NONE] + def process_end_condition(end_condition, phase_end): + pass + + #Determine phases... + self.start_time = events.time.min() + self.end_time = events.time.max() + current_time = self.start_time + phase_starts = [] + phase_ends = [] + phase_names = [] + for phase in self.boss_info.phases: + phase_names.append(phase.name) + phase_starts.append(current_time) + phase_end = phase.find_end_time(current_time, + boss_power_events, + health_updates, + boss_skill_activations) + if phase_end is None: + break + phase_ends.append(phase_end) + current_time = phase_end + phase_ends.append( self.end_time) + + def print_phase(phase): + print("{0}: {1} - {2} ({3})".format(phase[0], + phase[1] - self.start_time, + phase[2] - self.start_time, + phase[2] - phase[1])) + + all_phases = list(zip(phase_names, phase_starts, phase_ends)) + print("Autodetected phases:") + list(map(print_phase, all_phases)) + self.phases = [a for (a,i) in zip(all_phases, self.boss_info.phases) if i.important] + print("Important phases:") + list(map(print_phase, self.phases)) + + return player_src_events, player_dst_events, from_boss_events, from_final_boss_events, health_updates + + def preprocess_skills(self, skills, collector): + collector.set_context_value(ContextType.SKILL_NAME, create_mapping(skills, 'name')) + def __init__(self, encounter): - self.encounter = encounter - start_time = self.encounter.events[0].time - end_time = self.encounter.events[-1].time - self.time = (end_time - start_time)/1000 - - self.events = dict((t,[]) for t in list(LogType)) - self.agents = dict((agent.inst_id,agent) for agent in self.encounter.agents) - self.players = filter(lambda a: a.prof.is_player(), self.encounter.agents) - self.key_target_ids = {encounter.area_id} - self.skill_names = dict((skill.id,skill.name) for skill in self.encounter.skills) - - for event in self.encounter.events: - self.events[LogType.of(event)].append(event) - - def get_player_source(self, event): - agent = self.agents.get(event.src_instid) - if agent and agent.prof.is_player(): - return agent - agent = self.agents.get(event.src_master_instid) - if agent and agent.prof.is_player(): - return agent - return None - - def compute_dps_metrics(self): - player_dps = dict((agent.name, PlayerDPSMetric()) for agent in self.players) - for event in self.events[LogType.CONDI]: - player_source = self.get_player_source(event) - if player_source != None: - skill_name = self.skill_names[event.skill_id] - player_dps[player_source.name].add_damage( - skill_name, event.dst_instid, event.buff_dmg, True, False) - - for event in self.events[LogType.POWER]: - player_source = self.get_player_source(event) - if player_source != None: - skill_name = self.skill_names[event.skill_id] - player_dps[player_source.name].add_damage( - skill_name, event.dst_instid, event.value, False, event.result == parser.Result.CRIT) - - for name in player_dps: - player_dps[name].end(self.time) - - return TeamDPSMetric(player_dps) - - def compute_all_metrics(self): - dps_metrics = self.compute_dps_metrics() - return BasicMetric({"DPS": dps_metrics}) + self.debug = False + self.boss_info = BOSSES[encounter.area_id] + collector = Collector.root([Group.CATEGORY, + Group.PHASE, + Group.PLAYER, + Group.SUBGROUP, + Group.METRICS, + Group.SOURCE, + Group.DESTINATION, + Group.SKILL, + Group.BUFF, + + ]) + + #@merforga youll want to disable logs with a build stamp prior to today's + #or, if the system supports it, game build >= 82356 requires arc from sep22 2017 + + #print_frame(encounter.duplicate_id_agents) + + #set up data structures + events = assign_event_types(encounter.events) + if (encounter.version < '20170923' + and not events[(events.state_change == parser.StateChange.GW_BUILD) + & (events.src_agent >= 82356)].empty): + raise EvtcAnalysisException("This log's arc version and GW2 build are not fully compatible. Update arcdps!") + + agents = encounter.agents + skills = encounter.skills + players, bosses, final_bosses = self.preprocess_agents(agents, collector, events) + + self.preprocess_skills(skills, collector) + self.players = players + player_src_events, player_dst_events, boss_events, final_boss_events, health_updates = self.preprocess_events(events) + player_only_events = player_src_events[player_src_events.src_instid.isin(self.player_instids)] + + #time constraints + start_event = events[events.state_change == parser.StateChange.LOG_START] + start_timestamp = start_event['value'].iloc[0] + start_time = start_event['time'].iloc[0] + encounter_end = events.time.max() + state_events = self.assemble_state_data(player_only_events, players, encounter_end) + self.state_events = state_events + + BossMetricAnalyser(agents, self.subgroups, self.players, bosses, self.phases, encounter_end).gather_boss_specific_stats(events, collector) + buff_data = BuffPreprocessor().process_events(start_time, encounter_end, skills, players, player_src_events) + + collector.with_key(Group.CATEGORY, "boss").run(self.collect_boss_key_events, events) + collector.with_key(Group.CATEGORY, "status").run(self.collect_player_status, players) + collector.with_key(Group.CATEGORY, "status").run(self.collect_player_key_events, player_src_events) + collector.with_key(Group.CATEGORY, "combat").with_key(Group.METRICS, "damage").run(self.collect_outgoing_damage, player_src_events) + collector.with_key(Group.CATEGORY, "combat").with_key(Group.METRICS, "damage").run(self.collect_incoming_damage, player_dst_events) + collector.with_key(Group.CATEGORY, "combat").with_key(Group.METRICS, "shielded").run(self.collect_incoming_damage, player_dst_events[player_dst_events.is_shields != 0]) + collector.with_key(Group.CATEGORY, "combat").with_key(Group.METRICS, "buffs").run(self.collect_incoming_buffs, buff_data) + collector.with_key(Group.CATEGORY, "combat").with_key(Group.METRICS, "buffs").run(self.collect_outgoing_buffs, buff_data) + collector.with_key(Group.CATEGORY, "combat").with_key(Group.METRICS, "events").run(self.collect_player_combat_events, player_only_events) + collector.with_key(Group.CATEGORY, "combat").with_key(Group.METRICS, "events").run(self.collect_player_state_duration, state_events) + + + + encounter_collector = collector.with_key(Group.CATEGORY, "encounter") + encounter_collector.add_data('evtc_version', encounter.version) + encounter_collector.add_data('start', start_timestamp, int) + encounter_collector.add_data('start_tick', start_time, int) + encounter_collector.add_data('end_tick', encounter_end, int) + encounter_collector.add_data('duration', (encounter_end - start_time) / 1000, float) + encounter_collector.add_data('cm', self.boss_info.cm_detector(events, self.boss_instids)) + + encounter_collector.add_data('phase_order', [name for name,start,end in self.phases]) + for phase in self.phases: + phase_collector = encounter_collector.with_key(Group.PHASE, phase[0]) + phase_collector.add_data('start_tick', phase[1], int) + phase_collector.add_data('end_tick', phase[2], int) + phase_collector.add_data('duration', (phase[2] - phase[1]) / 1000, float) + + success = self.determine_success(events, final_boss_events, player_src_events, encounter, health_updates) + encounter_collector.add_data('success', success, bool) + + # saved as a JSON dump + self.data = collector.all_data + + def assemble_state_data(self, events, players, encounter_end): + # Get Up/Down/Death events + down_events = events[(events['state_change'] == parser.StateChange.CHANGE_DOWN) + |(events['state_change'] == parser.StateChange.CHANGE_DEAD) + |(events['state_change'] == parser.StateChange.CHANGE_UP) + |(events['state_change'] == parser.StateChange.DESPAWN) + |(events['state_change'] == parser.StateChange.SPAWN)].sort_values(by='time') + + # Produce down state + raw_data = np.array([np.arange(0, dtype=int)] * 5, dtype=int).T + + for player in list(players.index): + data = np.array([np.arange(0)] * 4).T + relevent_events = down_events[down_events['src_instid'] == player] + + state = parser.StateChange.CHANGE_UP + start_time = 0 + for event in relevent_events.itertuples(): + + if state == parser.StateChange.CHANGE_DOWN: + data = np.append(data, [[start_time, parser.StateChange.CHANGE_DOWN, event.time - start_time, (event.state_change == parser.StateChange.CHANGE_UP)]], axis=0) + elif state == parser.StateChange.CHANGE_DEAD: + data = np.append(data, [[start_time, parser.StateChange.CHANGE_DEAD, event.time - start_time, 0]], axis=0) + elif state == parser.StateChange.DESPAWN: + data = np.append(data, [[start_time, parser.StateChange.DESPAWN, event.time - start_time, 0]], axis=0) + + if event.state_change != parser.StateChange.SPAWN: + state = event.state_change; + start_time = event.time + + if state != parser.StateChange.CHANGE_UP: + data = np.append(data, [[start_time, state, encounter_end - start_time, 1]], axis=0) + + data = np.c_[[player] * data.shape[0], data] + raw_data = np.r_[raw_data, data] + + return pd.DataFrame(columns = ['player', 'time', 'state', 'duration', 'recovered'], data = raw_data) + + # Note: While this is just broken into areas with comments for now, we may want + # a more concrete split in future + + # section: Agent stats (player/boss + # subsection: player events + def collect_player_state_duration(self, collector, events): + split_by_player_groups(collector, self.collect_player_state_duration_by_phase, events, 'player', self.subgroups, self.players) + + def collect_player_state_duration_by_phase(self, collector, events): + split_duration_event_by_phase(collector, self.collect_state_duration, events, self.phases) + + def collect_state_duration(self, collector, events): + collector.add_data('down_time', events[events['state'] == parser.StateChange.CHANGE_DOWN]['duration'].sum()) + collector.add_data('dead_time', events[events['state'] == parser.StateChange.CHANGE_DEAD]['duration'].sum()) + collector.add_data('disconnect_time', events[events['state'] == parser.StateChange.DESPAWN]['duration'].sum()) + + def collect_player_combat_events(self, collector, events): + split_by_player_groups(collector, self.collect_combat_events_by_phase, events, 'src_instid', self.subgroups, self.players) + + def collect_combat_events_by_phase(self, collector, events): + split_by_phase(collector, self.collect_combat_events, events, self.phases) + + def collect_combat_events(self, collector, events): + death_events = len(events[events['state_change'] == parser.StateChange.CHANGE_DEAD]) + down_events = len(events[events['state_change'] == parser.StateChange.CHANGE_DOWN]) + disconnect_events = len(events[events['state_change'] == parser.StateChange.DESPAWN]) + collector.add_data('deaths', death_events, int) + collector.add_data('downs', down_events, int) + collector.add_data('disconnects', disconnect_events, int) + + # subsection: boss stats + def collect_individual_boss_key_events(self, collector, events): + enter_combat_time = only_entry(events[events.state_change == parser.StateChange.ENTER_COMBAT].time) + death_time = only_entry(events[events.state_change == parser.StateChange.CHANGE_DEAD].time) + collector.add_data("EnterCombat", enter_combat_time, int) + collector.add_data("Death", death_time, int) + + def collect_boss_key_events(self, collector, events): + boss_events = events[events.ult_src_instid.isin(self.boss_instids)] + split_by_boss(collector, + self.collect_individual_boss_key_events, + boss_events, + 'ult_src_instid', + Group.BOSS) + + #subsection: player stats + def collect_player_status(self, collector, players): + # player archetypes + players = players.assign(archetype=Archetype.POWER) + players.loc[players.condition >= 5, 'archetype'] = Archetype.CONDI + players.loc[(players.toughness >= 5) | (players.healing >= 5), 'archetype'] = Archetype.SUPPORT + collector.group(self.collect_individual_player_status, players, ('name', Group.PLAYER)) + + def collect_individual_player_status(self, collector, player): + only_entry = player.iloc[0] + collector.add_data('profession', only_entry['prof'], parser.AgentType) + if only_entry['elite'] == 0: + collector.add_data('elite', Elite.CORE) + elif only_entry['elite'] < 55: + collector.add_data('elite', Elite.HEART_OF_THORNS) + else: + collector.add_data('elite', Elite.PATH_OF_FIRE) + collector.add_data('toughness', only_entry['toughness'], int) + collector.add_data('healing', only_entry['healing'], int) + collector.add_data('condition', only_entry['condition'], int) + collector.add_data('archetype', only_entry['archetype'], Archetype) + collector.add_data('party', only_entry['party'], int) + collector.add_data('account', only_entry['account'], str) + + def collect_player_key_events(self, collector, events): + # player archetypes + player_only_events = events[events.src_instid.isin(self.player_instids)] + split_by_player(collector, self.collect_individual_player_key_events, player_only_events, 'src_instid', self.players) + + def collect_individual_player_key_events(self, collector, events): + # collector.add_data('profession_name', parser.AgentType(only_entry['prof']).name, str) + enter_combat_time = only_entry(events[events.state_change == parser.StateChange.ENTER_COMBAT].time) + death_time = only_entry(events[events.state_change == parser.StateChange.CHANGE_DEAD].time) + collector.add_data("EnterCombat", enter_combat_time, int) + collector.add_data("Death", death_time, int) + + #section: Outgoing damage stats filtering + def collect_outgoing_damage(self, collector, player_events): + damage_events = filter_damage_events(player_events) + split_by_phase(collector, self.collect_phase_damage, damage_events, self.phases) + + def collect_phase_damage(self, collector, damage_events): + collector.with_key(Group.DESTINATION, "*All").run(self.collect_skill_data, damage_events) + split_by_agent(collector, + self.collect_destination_damage, + damage_events, + Group.DESTINATION, + 'dst_instid', self.boss_instids, self.player_instids) + + + + def collect_destination_damage(self, collector, damage_events): + collector.set_context_value(ContextType.TOTAL_DAMAGE_TO_DESTINATION, + damage_events['damage'].sum()) + collector.set_context_value(ContextType.TOTAL_DAMAGE_FROM_SOURCE_TO_DESTINATION, + damage_events['damage'].sum()) + split_by_player_groups(collector, + self.aggregate_overall_damage_stats, + damage_events, + 'ult_src_instid', self.subgroups, self.players) + + def collect_skill_data(self, collector, damage_events): + split_by_player(collector, + self.collect_player_skill_damage, + damage_events, + 'ult_src_instid', self.players) + + def collect_player_skill_damage(self, collector, events): + power_events = events[events.type == LogType.POWER] + collector.set_context_value(ContextType.TOTAL_DAMAGE_FROM_SOURCE_TO_DESTINATION, + events['damage'].sum()) + split_by_skill(collector, self.aggregate_power_damage_stats, power_events) + split_by_skill(collector, self.aggregate_basic_damage_stats, events) + + #subsection incoming damage stat filtering + def collect_incoming_damage(self, collector, player_events): + damage_events = filter_damage_events(player_events) + split_by_phase(collector, self.collect_phase_incoming_damage, damage_events, self.phases) + + def collect_phase_incoming_damage(self, collector, damage_events): + collector.set_context_value(ContextType.TOTAL_DAMAGE_FROM_SOURCE_TO_DESTINATION, + damage_events['damage'].sum()) + source_collector = collector.with_key(Group.SOURCE, "*All") + split_by_player_groups(source_collector, self.aggregate_basic_damage_stats, damage_events, 'dst_instid', self.subgroups, self.players) + split_by_player_groups(source_collector, self.collect_player_incoming_skill_damage, damage_events, 'dst_instid', self.subgroups, self.players) + + def collect_player_incoming_skill_damage(self, collector, events): + collector.set_context_value(ContextType.TOTAL_DAMAGE_FROM_SOURCE_TO_DESTINATION, + events['damage'].sum()) + split_by_skill(collector, self.aggregate_basic_damage_stats, events) + + #subsection: Aggregating damage + def aggregate_overall_damage_stats(self, collector, events): + power_events = events[events.type == LogType.POWER] + condi_events = events[events.type == LogType.CONDI] + self.aggregate_power_damage_stats(collector, power_events) + self.aggregate_basic_damage_stats(collector, events) + collector.add_data('power', power_events['damage'].sum(), int) + collector.add_data('condi', condi_events['damage'].sum(), int) + collector.add_data('power_dps', power_events['damage'].sum(), per_second(int)) + collector.add_data('condi_dps', condi_events['damage'].sum(), per_second(int)) + + def aggregate_power_damage_stats(self, collector, events): + collector.add_data('fifty', events['is_fifty'].mean(), percentage) + collector.add_data('scholar', events['is_ninety'].mean(), percentage) + collector.add_data('seaweed', events['is_moving'].mean(), percentage) + collector.add_data('flanking', events['is_flanking'].mean(), percentage) + collector.add_data('crit', (events.result == parser.Result.CRIT).mean(), percentage) + + def aggregate_basic_damage_stats(self, collector, events): + collector.add_data('total', events['damage'].sum(), int) + collector.add_data('dps', events['damage'].sum(), per_second(int)) + collector.add_data('percentage', events['damage'].sum(), + percentage_of(ContextType.TOTAL_DAMAGE_FROM_SOURCE_TO_DESTINATION)) + + #Section: buff stats + + def collect_outgoing_buffs(self, collector, buff_data): + destination_collector = collector.with_key(Group.DESTINATION, "*All"); + phase_data = self._split_buff_by_phase(buff_data, self.start_time, self.end_time) + destination_collector.set_context_value(ContextType.DURATION, self.end_time - self.start_time) + destination_collector.with_key(Group.PHASE, "All").run(self.collect_buffs_by_source, phase_data) + + for i in range(0, len(self.phases)): + phase = self.phases[i] + phase_data = self._split_buff_by_phase(buff_data, phase[1], phase[2]) + destination_collector.set_context_value(ContextType.DURATION, phase[2] - phase[1]) + destination_collector.with_key(Group.PHASE, "{0}".format(phase[0])).run(self.collect_buffs_by_source, phase_data) + + def collect_incoming_buffs(self, collector, buff_data): + source_collector = collector.with_key(Group.SOURCE, "*All"); + phase_data = self._split_buff_by_phase(buff_data, self.start_time, self.end_time) + source_collector.set_context_value(ContextType.DURATION, self.end_time - self.start_time) + source_collector.with_key(Group.PHASE, "All").run(self.collect_buffs_by_target, phase_data) + + for i in range(0, len(self.phases)): + phase = self.phases[i] + phase_data = self._split_buff_by_phase(buff_data, phase[1], phase[2]) + source_collector.set_context_value(ContextType.DURATION, phase[2] - phase[1]) + source_collector.with_key(Group.PHASE, "{0}".format(phase[0])).run(self.collect_buffs_by_target, phase_data) + + def collect_buffs_by_target(self, collector, buff_data): + split_by_player_groups(collector, self.collect_buffs_by_type, buff_data, 'dst_instid', self.subgroups, self.players) + + def collect_buffs_by_source(self, collector, buff_data): + split_by_player_groups(collector, self.collect_buffs_by_type, buff_data, 'src_instid', self.subgroups, self.players) + def collect_buffs_by_type(self, collector, buff_data): + #collector.with_key(Group.PHASE, "All").run(self.collect_buffs_by_target, buff_data); + if len(buff_data) > 0: + for buff_type in BUFF_TYPES: + collector.set_context_value(ContextType.BUFF_TYPE, buff_type) + buff_specific_data = buff_data[buff_data['buff'] == buff_type.code] + collector.with_key(Group.BUFF, buff_type.code).run(self.collect_buff, buff_specific_data) + + def _split_buff_by_phase(self, diff_data, start, end): + across_phase = diff_data[(diff_data['time'] < start) & (diff_data['time'] + diff_data['duration'] > end)] + + #HACK: review why copy? + before_phase = diff_data[(diff_data['time'] < start) & (diff_data['time'] + diff_data['duration'] > start) & (diff_data['time'] + diff_data['duration'] <= end)].copy() + main_phase = diff_data[(diff_data['time'] >= start) & (diff_data['time'] + diff_data['duration'] <= end)] + after_phase = diff_data[(diff_data['time'] >= start) & (diff_data['time'] < end) & (diff_data['time'] + diff_data['duration'] > end)] + + across_phase = across_phase.assign(time = start, duration = end - start, stripped = 0) + + before_phase.loc[:, 'duration'] = before_phase['duration'] + before_phase['time'] - start + before_phase = before_phase.assign(time = start, stripped = 0) + + after_phase = after_phase.assign(duration = end) + after_phase.loc[:, 'duration'] = after_phase['duration'] - after_phase['time'] + return across_phase.append(before_phase).append(main_phase).append(after_phase) + + def collect_buff(self, collector, diff_data): + if diff_data.empty: + collector.add_data(None, 0.0) + else: + mean = (diff_data['duration'] * diff_data['stacks']).sum() + buff_type = collector.context_values[ContextType.BUFF_TYPE] + if buff_type.stacking == StackType.INTENSITY: + collector.add_data(None, mean, per_second_per_dst(float)) + else: + collector.add_data(None, mean, percentage_per_second_per_dst(float)) + + def determine_success(self, events, final_boss_events, player_src_events, encounter, health_updates): + success = (not self.boss_info.despawns_instead_of_dying) and (not final_boss_events[(final_boss_events.state_change == parser.StateChange.CHANGE_DEAD)].empty) + print("Death detected: {0}".format(success)) + #If we completed all phases, and the key npcs survived, and at least one player survived... assume we succeeded + if self.boss_info.despawns_instead_of_dying and len(self.phases) == len(list(filter(lambda a: a.important, self.boss_info.phases))): + end_state_changes = [parser.StateChange.CHANGE_DEAD, parser.StateChange.DESPAWN] + key_npc_events = events[events.src_instid.isin(self.boss_info.key_npc_ids)] + if key_npc_events[(key_npc_events.state_change == parser.StateChange.CHANGE_DEAD)].empty: + print("No key NPCs died...") + dead_players = player_src_events[(player_src_events.src_instid.isin(self.player_instids)) & + (player_src_events.state_change.isin(end_state_changes))].src_instid.unique() + print("These players died: {0}".format(dead_players)) + surviving_players = list(filter(lambda a: a not in dead_players, self.player_instids)) + print("These players survived: {0}".format(surviving_players)) + if surviving_players: + success = True + print("Probable death of despawn-only boss detected: {0}".format(success)) + + if (self.boss_info.success_health_limit is not None and + health_updates[health_updates.dst_agent <= (self.boss_info.success_health_limit * 100)].empty): + success = False + print("Success changed due to health still being too high: {0}".format(success)) + + print_frame(events[events.state_change == parser.StateChange.REWARD][['value', 'src_agent', 'dst_agent']]) + + if self.boss_info.kind == Kind.RAID and encounter.version >= '20170905': + success_types = [55821, 60685] + success = not events[(events.state_change == parser.StateChange.REWARD) + & events.value.isin(success_types)].empty + + print("Success overridden by reward chest logging: {0}".format(success)) + + return success diff --git a/analyser/bosses.py b/analyser/bosses.py new file mode 100644 index 00000000..2935c3ad --- /dev/null +++ b/analyser/bosses.py @@ -0,0 +1,319 @@ +from enum import IntEnum + +class DesiredValue(IntEnum): + LOW = -1 + NONE = 0 + HIGH = 1 + +class MetricType(IntEnum): + TIME = 0 + COUNT = 1 + +class Kind(IntEnum): + RAID = 1 + EASY = 2 + DUMMY = 3 + FRACTAL = 4 + +def no_cm(events, boss_instids): + return False + +def yes_cm(events, boss_instids): + return True + +def cairn_cm_detector(events, boss_instids): + return len(events[events.skillid == 38098]) > 0 + +def samarog_cm_detector(events, boss_instids): + return len(events[(events.skillid == 37966)&(events.time - events.time.min() < 10000)]) > 0 + +def mo_cm_detector(events, boss_instids): + return len(events[(events.state_change == 12) & (events.dst_agent == 30000000) & (events.src_instid.isin(boss_instids))]) > 0 + +def deimos_cm_detector(events, boss_instids): + return len(events[(events.state_change == 12) & (events.dst_agent == 42000000) & (events.src_instid.isin(boss_instids))]) > 0 + +class Metric: + def __init__(self, name, short_name, data_type, split_by_player = True, split_by_phase = False, desired = DesiredValue.LOW): + self.name = name + self.short_name = short_name + self.long_name = name # TODO + self.data_type = data_type + self.desired = desired + self.split_by_player = split_by_player + self.split_by_phase = split_by_phase + + def __repr__(self): + return "%s (%s, %s)" % (self.name, self.data_type, self.desired) + +class Boss: + def __init__(self, name, kind, boss_ids, metrics=None, sub_boss_ids=None, key_npc_ids = None, phases=None, despawns_instead_of_dying = False, has_structure_boss = False, success_health_limit = None, cm_detector = no_cm): + self.name = name + self.kind = kind + self.boss_ids = boss_ids + self.metrics = [] if metrics is None else metrics + self.sub_boss_ids = [] if sub_boss_ids is None else sub_boss_ids + self.phases = [] if phases is None else phases + self.key_npc_ids = [] if key_npc_ids is None else key_npc_ids + self.despawns_instead_of_dying = despawns_instead_of_dying + self.has_structure_boss = has_structure_boss + self.success_health_limit = success_health_limit + self.cm_detector = cm_detector + +class Phase: + def __init__(self, name, important, + phase_end_damage_stop=None, + phase_end_damage_start=None, + phase_end_health=None): + self.name = name + self.important = important + self.phase_end_damage_stop = phase_end_damage_stop + self.phase_end_damage_start = phase_end_damage_start + self.phase_end_health = phase_end_health + + def find_end_time(self, + current_time, + damage_gaps, + health_updates, + skill_activations): + end_time = None + if self.phase_end_health is not None: + relevant_health_updates = health_updates[(health_updates.time >= current_time) & + (health_updates.dst_agent >= self.phase_end_health * 100)] + if relevant_health_updates.empty or health_updates['dst_agent'].min() > (self.phase_end_health + 2) * 100: + return None + end_time = current_time = int(relevant_health_updates['time'].iloc[-1]) + print("{0}: Detected health below {1} at time {2}".format(self.name, self.phase_end_health, current_time)) + + if self.phase_end_damage_stop is not None: + relevant_gaps = damage_gaps[(damage_gaps.time - damage_gaps.delta >= current_time) & + (damage_gaps.delta > self.phase_end_damage_stop)] + if not relevant_gaps.empty: + end_time = current_time = int(relevant_gaps['time'].iloc[0] - relevant_gaps['delta'].iloc[0]) + elif len(damage_gaps.time) > 0 and int(damage_gaps.time.iloc[-1]) >= current_time: + end_time = current_time = int(damage_gaps.time.iloc[-1]) + else: + return None + + print("{0}: Detected gap of at least {1} at time {2}".format(self.name, self.phase_end_damage_stop, current_time)) + + if self.phase_end_damage_start is not None: + relevant_gaps = damage_gaps[(damage_gaps.time >= current_time) & + (damage_gaps.delta > self.phase_end_damage_start)] + if relevant_gaps.empty: + return None + end_time = current_time = int(relevant_gaps['time'].iloc[0]) + print("{0}: Detected gap of at least {1} ending at time {2}".format(self.name, self.phase_end_damage_start, current_time)) + return end_time + +BOSS_ARRAY = [ + Boss('Vale Guardian', Kind.RAID, [0x3C4E], phases = [ + Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 10000), + Phase("First split", False, phase_end_damage_start = 10000), + Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 10000), + Phase("Second split", False, phase_end_damage_start = 10000), + Phase("Phase 3", True) + ], metrics = [ + Metric('Blue Guardian Invulnerability Time', 'Blue Invuln', MetricType.TIME, False), + Metric('Bullets Eaten', 'Bulleted', MetricType.COUNT), + Metric('Teleports', 'Teleported', MetricType.COUNT) + ]), + Boss('Gorseval', Kind.RAID, [0x3C45], phases = [ + Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 10000), + Phase("First souls", False, phase_end_damage_start = 10000), + Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 10000), + Phase("Second souls", False, phase_end_damage_start = 10000), + Phase("Phase 3", True) + ], metrics = [ + Metric('Unmitigated Spectral Impacts', 'Slammed', MetricType.COUNT, True, True), + Metric('Ghastly Imprisonments', 'Imprisoned', MetricType.COUNT), + Metric('Spectral Darkness', 'Tainted', MetricType.TIME) + ]), + Boss('Sabetha', Kind.RAID, [0x3C0F], phases = [ + Phase("Phase 1", True, phase_end_health = 75, phase_end_damage_stop = 10000), + Phase("Kernan", False, phase_end_damage_start = 10000), + Phase("Phase 2", True, phase_end_health = 50, phase_end_damage_stop = 10000), + Phase("Knuckles", False, phase_end_damage_start = 10000), + Phase("Phase 3", True, phase_end_health = 25, phase_end_damage_stop = 10000), + Phase("Karde", False, phase_end_damage_start = 10000), + Phase("Phase 4", True) + ], metrics = [ + Metric('Heavy Bombs Undefused', 'Heavy Bombs', MetricType.COUNT, False) + ]), + Boss('Slothasor', Kind.RAID, [0x3EFB], phases = [ + Phase("Phase 1", True, phase_end_health = 80, phase_end_damage_stop = 1000), + Phase("Break 1", False, phase_end_damage_start = 1000), + Phase("Phase 2", True, phase_end_health = 60, phase_end_damage_stop = 1000), + Phase("Break 2", False, phase_end_damage_start = 1000), + Phase("Phase 3", True, phase_end_health = 40, phase_end_damage_stop = 1000), + Phase("Break 3", False, phase_end_damage_start = 1000), + Phase("Phase 4", True, phase_end_health = 20, phase_end_damage_stop = 1000), + Phase("Break 4", False, phase_end_damage_start = 1000), + Phase("Phase 5", True, phase_end_health = 10, phase_end_damage_stop = 1000), + Phase("Break 5", False, phase_end_damage_start = 1000), + Phase("Phase 6", True) + ], metrics = [ + Metric('Tantrum Knockdowns', 'Tantrumed', MetricType.COUNT), + Metric('Spores Received', 'Spored', MetricType.COUNT), + Metric('Spores Blocked', 'Spore Blocks', MetricType.COUNT, True, False, DesiredValue.HIGH), + Metric('Volatile Poison Carrier', 'Poisoned', MetricType.COUNT, True, False, DesiredValue.NONE), + Metric('Toxic Cloud Breathed', 'Green Goo', MetricType.COUNT, True, False) + ]), + Boss('Bandit Trio', Kind.EASY, [0x3ED8, 0x3F09, 0x3EFD], phases = [ + #Needs to be a little bit more robust, but it's trio - not the most important fight. + #Phase("Clear 1", False, phase_end_health = 99), + Phase("Berg", True, phase_end_damage_stop = 10000), + Phase("Clear 2", False, phase_end_damage_start= 10000), + Phase("Zane", True, phase_end_damage_stop = 10000), + Phase("Clear 3", False, phase_end_damage_start = 10000), + Phase("Narella", True, phase_end_damage_stop = 10000) + ]), + Boss('Matthias', Kind.RAID, [0x3EF3], phases = [ + #Will currently detect phases slightly early - but probably not a big deal? + Phase("Ice", True, phase_end_health = 80), + Phase("Fire", True, phase_end_health = 60), + Phase("Rain", True, phase_end_health = 40), + Phase("Abomination", True) + ], metrics = [ + Metric('Moved While Unbalanced', 'Slipped', MetricType.COUNT), + Metric('Surrender', 'Surrendered', MetricType.COUNT), + Metric('Burning Stacks Received', 'Burned', MetricType.COUNT, True, True), + Metric('Corrupted', 'Corrupted', MetricType.COUNT, True, False, DesiredValue.NONE), + Metric('Matthias Shards Returned', 'Reflected', MetricType.COUNT, False), + Metric('Shards Absorbed', 'Absorbed', MetricType.COUNT, True, False, DesiredValue.NONE), + Metric('Sacrificed', 'Sacrificed', MetricType.COUNT, True, False, DesiredValue.NONE), + Metric('Well of the Profane Carrier', 'Welled', MetricType.COUNT, True, False, DesiredValue.NONE) + ]), + Boss('Keep Construct', Kind.RAID, [0x3F6B], phases = [ + # Needs more robust sub-phase mechanisms, but this should be on par with raid-heroes. + Phase("Pre-burn 1", True, phase_end_damage_stop = 15000), + Phase("Split 1", False, phase_end_damage_start = 15000), + Phase("Burn 1", True, phase_end_health = 66, phase_end_damage_stop = 15000), + Phase("Pacman 1", False, phase_end_damage_start = 15000), + Phase("Pre-burn 2", True, phase_end_damage_stop = 15000), + Phase("Split 2", False, phase_end_damage_start = 15000), + Phase("Burn 2", True, phase_end_health = 33, phase_end_damage_stop = 15000), + Phase("Pacman 2", False, phase_end_damage_start = 15000), + Phase("Pre-burn 3", True, phase_end_damage_stop = 18000), + Phase("Split 3", False, phase_end_damage_start = 18000), + Phase("Burn 3", True) + ], metrics = [ + Metric('Correct Orb', 'Correct Orbs', MetricType.COUNT), + Metric('Wrong Orb', 'Wrong Orbs', MetricType.COUNT), + Metric('Rifts Hit', 'Rifts Hit', MetricType.COUNT, False, False, DesiredValue.HIGH), + Metric('Gaining Power', 'Power Gained', MetricType.COUNT, False, False), + Metric('Magic Blast Intensity', 'Orbs Missed', MetricType.COUNT, False, False) + ]), + Boss('Xera', Kind.RAID, [0x3F76, 0x3F9E], despawns_instead_of_dying = True, success_health_limit = 3, phases = [ + Phase("Phase 1", True, phase_end_health = 51, phase_end_damage_stop = 30000), + Phase("Leyline", False, phase_end_damage_start = 30000), + Phase("Phase 2", True), + ], metrics = [ + Metric('Derangement', 'Deranged', MetricType.COUNT) + ]), + Boss('Cairn', Kind.RAID, [0x432A], metrics = [ + Metric('Displacement', 'Teleported', MetricType.COUNT), + Metric('Meteor Swarm', 'Shard Hits', MetricType.COUNT), + Metric('Spatial Manipulation', 'Circles', MetricType.COUNT), + Metric('Shared Agony', 'Agony', MetricType.COUNT) + ], cm_detector = cairn_cm_detector), + Boss('Mursaat Overseer', Kind.RAID, [0x4314], metrics = [ + Metric('Protect', 'Protector', MetricType.COUNT), + Metric('Claim', 'Claimer', MetricType.COUNT), + Metric('Dispel', 'Dispeller', MetricType.COUNT), + Metric('Soldiers', 'Soldiers', MetricType.COUNT, False), + Metric('Soldier\'s Aura', 'Soldier AOE', MetricType.COUNT), + Metric('Enemy Tile', 'Enemy Tile', MetricType.COUNT) + ], cm_detector = mo_cm_detector), + Boss('Samarog', Kind.RAID, [0x4324], phases = [ + Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 10000), + Phase("First split", False, phase_end_damage_start = 10000), + Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 10000), + Phase("Second split", False, phase_end_damage_start = 10000), + Phase("Phase 3", True, phase_end_health=1) + ], metrics = [ + Metric('Claw', 'Claw', MetricType.COUNT, True, True), + Metric('Shockwave', 'Shockwave', MetricType.COUNT, True, True), + Metric('Prisoner Sweep', 'Sweep', MetricType.COUNT, True, True), + Metric('Charge', 'Charge', MetricType.COUNT, True, False), + Metric('Anguished Bolt', 'Guldhem Stun', MetricType.COUNT, True, False), + Metric('Inevitable Betrayal', 'Chose Poorly', MetricType.COUNT, True, False), + Metric('Bludgeon', 'Bludgeon', MetricType.COUNT, True, False), + Metric('Fixate', 'Fixate', MetricType.COUNT, True, True), + Metric('Small Friend', 'Small Friend', MetricType.COUNT, True, True), + Metric('Big Friend', 'Big Friend', MetricType.COUNT, True, True), + Metric('Spear Impact', 'Spear Impacts', MetricType.COUNT, True, True) + ], cm_detector = samarog_cm_detector), + Boss('Deimos', Kind.RAID, [0x4302], key_npc_ids=[17126], despawns_instead_of_dying = True, has_structure_boss = True, phases = [ + Phase("Phase 1", True, phase_end_health = 10, phase_end_damage_stop = 20000), + Phase("Phase 2", True) + ], metrics = [ + Metric('Annihilate', 'Slammed', MetricType.COUNT, True, False), + Metric('Soul Feast', 'Hand Touches', MetricType.COUNT, True, False), + Metric('Mind Crush', 'Mind Crush', MetricType.COUNT, True, False), + Metric('Rapid Decay', 'Black', MetricType.COUNT, True, False), + Metric('Demonic Shockwave', 'Shockwave', MetricType.COUNT, True, False), + Metric('Teleports', 'Teleports', MetricType.COUNT, True, False), + Metric('Tear Consumed', 'Tears Consumed', MetricType.COUNT, True, False) + ], cm_detector = deimos_cm_detector), + Boss('Standard Kitty Golem', Kind.DUMMY, [16199]), + Boss('Average Kitty Golem', Kind.DUMMY, [16177]), + Boss('Vital Kitty Golem', Kind.DUMMY, [16198]), + Boss('Massive Standard Kitty Golem', Kind.DUMMY, [16178]), + Boss('Massive Average Kitty Golem', Kind.DUMMY, [16202]), + Boss('Massive Vital Kitty Golem', Kind.DUMMY, [16169]), + Boss('Resistant Kitty Golem', Kind.DUMMY, [16176]), + Boss('Tough Kitty Golem', Kind.DUMMY, [16174]), + Boss('Skorvald the Shattered (CM)', Kind.FRACTAL,[0x44E0], phases = [ + Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 15000), + Phase("First split", False, phase_end_damage_start = 15000), + Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 15000), + Phase("Second split", False, phase_end_damage_start = 15000), + Phase("Phase 3", True, phase_end_health=1) + ], cm_detector = yes_cm), + Boss('Artsariiv (CM)', Kind.FRACTAL, [0x461d], despawns_instead_of_dying = True, success_health_limit = 3, phases = [ + Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 10000), + Phase("First split", False, phase_end_damage_start = 10000), + Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 10000), + Phase("Second split", False, phase_end_damage_start = 10000), + Phase("Phase 3", True, phase_end_health=1) + ], cm_detector = yes_cm), + Boss('Arkk (CM)', Kind.FRACTAL,[0x455f], despawns_instead_of_dying = True, success_health_limit = 3, phases =[ + Phase("100-80", True, phase_end_health = 80, phase_end_damage_stop = 10000), + Phase("First orb", False, phase_end_damage_start = 10000), + Phase("80-70", True, phase_end_health = 70, phase_end_damage_stop = 10000), + Phase("Archdiviner", False, phase_end_damage_start = 10000), + Phase("70-50", True, phase_end_health = 50, phase_end_damage_stop = 10000), + Phase("Second orb", False, phase_end_damage_start = 10000), + Phase("50-40", True, phase_end_health = 40, phase_end_damage_stop = 10000), + Phase("Gladiator", False, phase_end_damage_start = 10000), + Phase("40-30", True, phase_end_health = 30, phase_end_damage_stop = 10000), + Phase("Third orb", False, phase_end_damage_start = 10000), + Phase("30-0", True, phase_end_health = 1, phase_end_damage_stop = 10000) + ], cm_detector = yes_cm), + Boss('MAMA (CM)', Kind.FRACTAL, [0x427d], phases = [ + Phase("Phase 1", True, phase_end_health = 75, phase_end_damage_stop = 3000), + Phase("First split", False, phase_end_damage_start = 3000), + Phase("Phase 2", True, phase_end_health = 50, phase_end_damage_stop = 3000), + Phase("Second split", False, phase_end_damage_start = 3000), + Phase("Phase 3", True, phase_end_health = 25, phase_end_damage_stop = 3000), + Phase("Second split", False, phase_end_damage_start = 3000), + Phase("Phase 4", True, phase_end_health=1) + ], cm_detector = yes_cm), + Boss('Siax (CM)', Kind.FRACTAL,[0x4284], phases = [ + Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 15000), + Phase("First split", False, phase_end_damage_start = 15000), + Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 15000), + Phase("Second split", False, phase_end_damage_start = 15000), + Phase("Phase 3", True, phase_end_health=1) + ], cm_detector = yes_cm), + Boss('Ensolyss (CM)', Kind.FRACTAL,[0x4234], phases = [ + Phase("Phase 1", True, phase_end_health = 66, phase_end_damage_stop = 15000), + Phase("First split", False, phase_end_damage_start = 15000), + Phase("Phase 2", True, phase_end_health = 33, phase_end_damage_stop = 15000), + Phase("Second split", False, phase_end_damage_start = 15000), + Phase("Phase 3", True, phase_end_health=15), + Phase("Phase 4", True, phase_end_health=1) + ], cm_detector = yes_cm) +] +BOSSES = {boss.boss_ids[0]: boss for boss in BOSS_ARRAY} diff --git a/analyser/bossmetrics.py b/analyser/bossmetrics.py new file mode 100644 index 00000000..dc9169a2 --- /dev/null +++ b/analyser/bossmetrics.py @@ -0,0 +1,379 @@ +from enum import IntEnum +from evtcparser import * +import pandas as pd +import numpy as np +from functools import reduce +from .collector import * +from .splits import * + +class Skills: + BLUE_PYLON_POWER = 31413 + BULLET_STORM = 31793 + UNSTABLE_MAGIC_SPIKE = 31392 + SPECTRAL_IMPACT = 31875 + GHASTLY_PRISON = 31623 + SPECTRAL_DARKNESS = 31498 + HEAVY_BOMB_EXPLODE = 31596 + TANTRUM = 34479 + TOXIC_CLOUD = 34565 + BLEEDING = 736 + BURNING = 737 + VOLATILE_POISON = 34387 + UNBALANCED = 34367 + CORRUPTION = 34416 + SURRENDER = 34413 + BLOOD_FUELED = 34422 + SACRIFICE = 34442 + UNSTABLE_BLOOD_MAGIC = 34450 + DERANGEMENT = 34965 + DISPLACEMENT = 38113 + METEOR_SWARM = 38313 + SHARED_AGONY = 38049 + SPATIAL_MANIPULATION = {37611, 37642, 37673, 38074, 38302} + PROTECT = 37813 + CLAIM = 37779 + DISPEL = 37697 + SOLDIERS_AURA = 37677 + ENEMY_TILE = 38184 + SAMAROG_CLAW = 37843 + SHOCKWAVE = 37996 + PRISONER_SWEEP = 38168 + CHARGE = 37797 + ANGUISHED_BOLT = 38314 + INEVITABLE_BETRAYL = 38260 + #Spear of Revulsion + EFFIGY_PULSE = 37901 + BLUDGEON = 38305 + SAMAROG_FIXATE = 37868 + SMALL_FRIEND = 38247 + BIG_FRIEND = 37966 + ANNIHILATE = 38208 + SOUL_FEAST = 37805 + MIND_CRUSH = 37613 + RAPID_DECAY = 37716 + DEMONIC_SHOCKWAVE = 38046 + DEIMOS_PRIMARY_AGGRO = 34500 + DEIMOS_TELEPORT = 37838 + TEAR_CONSUMED = 37733 + RED_ORB = 34972 + WHITE_ORB = 34914 + RED_ORB_ATTUNEMENT = 35091 + WHITE_ORB_ATTUNEMENT = 35109 + COMPROMISED = 35096 + GAINING_POWER = 35075 + MAGIC_BLAST_INTENSITY = 35119 + SPEAR_IMPACT = 37816 + + +class BossMetricAnalyser: + def __init__(self, agents, subgroups, players, bosses, phases, encounter_end): + self.agents = agents + self.subgroups = subgroups + self.players = players + self.bosses = bosses + self.phases = phases + self.encounter_end = encounter_end + + def standard_count(events): + return len(events); + + def combine_by_time_range_and_instid(self, events, time_range, inst_id = 'dst_instid'): + events = events.sort_values(by=[inst_id, 'time']) + deltas = abs(events.time - events.time.shift(1)) + (abs(events[inst_id] - events[inst_id].shift(1)) * 10000000) + deltas.fillna(10000000, inplace=True) + events = events.assign(deltas = deltas) + events = events[events.deltas > time_range] + return events + + def generate_player_buff_times(self, events, players, skillid): + events = events[(events.skillid == skillid) & (events.buff == 1)].sort_values(by='time') + + raw_data = np.array([np.arange(0, dtype=int)] * 3, dtype=int).T + for player in list(players.index): + data = np.array([np.arange(0)] * 2).T + player_events = events[((events['dst_instid'] == player)&(events.is_buffremove == 0))| + ((events['src_instid'] == player)&(events.is_buffremove == 1))] + + active = False + start_time = 0 + for event in player_events.itertuples(): + if event.is_buffremove == 0 and active == False: + active = True + start_time = event.time + elif event.is_buffremove == 1 and active == True: + active = False + data = np.append(data, [[start_time, event.time - start_time]], axis=0) + + if active == True: + data = np.append(data, [[start_time, self.encounter_end - start_time]], axis=0) + + data = np.c_[[player] * data.shape[0], data] + raw_data = np.r_[raw_data, data] + + return pd.DataFrame(columns = ['player', 'time', 'duration'], data = raw_data) + + def gather_count_stat(self, name, collector, by_player, by_phase, events, calculation = standard_count): + def count_by_phase(collector, events, func): + split_by_phase(collector, func, events, self.phases) + def count_by_player(collector, events): + split_by_player_groups(collector, count, events, 'dst_instid', self.subgroups, self.players) + def count(collector, events): + collector.add_data(name, calculation(events), int) + + if by_phase and by_player: + count_by_phase(collector, events, count_by_player) + elif by_phase: + collector = collector.with_key(Group.SUBGROUP, "*All") + count_by_phase(collector, events, count) + elif by_player: + collector = collector.with_key(Group.PHASE, "All") + count_by_player(collector, events) + else: + collector = collector.with_key(Group.PHASE, "All").with_key(Group.SUBGROUP, "*All") + count(collector, events) + + def gather_boss_specific_stats(self, events, collector): + self.end_time = events.time.max() + self.start_time = events.time.min() + metric_collector = collector.with_key(Group.CATEGORY, "combat").with_key(Group.METRICS, "mechanics") + if len(self.bosses[self.bosses.name == 'Vale Guardian']) != 0: + self.gather_vg_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Gorseval the Multifarious']) != 0: + self.gather_gorse_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Sabetha the Saboteur']) != 0: + self.gather_sab_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Slothasor']) != 0: + self.gather_sloth_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Matthias Gabrel']) != 0: + self.gather_matt_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Keep Construct']) != 0: + self.gather_kc_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Xera']) != 0: + self.gather_xera_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Cairn the Indomitable']) != 0: + self.gather_cairn_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Mursaat Overseer']) != 0: + self.gather_mursaat_overseer_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Samarog']) != 0: + self.gather_samarog_stats(events, metric_collector) + elif len(self.bosses[self.bosses.name == 'Deimos']) != 0: + self.gather_deimos_stats(events, metric_collector) + + def gather_vg_stats(self, events, collector): + teleport_events = events[(events.skillid == Skills.UNSTABLE_MAGIC_SPIKE) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + teleport_events = self.combine_by_time_range_and_instid(teleport_events, 1000) + bullet_storm_events = events[(events.skillid == Skills.BULLET_STORM) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + self.gather_count_stat('Teleports', collector, True, False, teleport_events) + self.gather_count_stat('Bullets Eaten', collector, True, False, bullet_storm_events) + self.vg_blue_guardian_invul(events, collector) + + def vg_blue_guardian_invul(self, events, collector): + relevent_events = events[(events.skillid == Skills.BLUE_PYLON_POWER) & ((events.is_buffremove == 1) | (events.is_buffremove == 0))] + time = 0 + start_time = 0 + buff_up = False + for event in relevent_events.itertuples(): + if buff_up == False & (event.is_buffremove == 0): + buff_up = True + start_time = event.time + elif buff_up == True & (event.is_buffremove == 1): + buff_up = False + time += event.time - start_time + + collector.with_key(Group.PHASE, "All").with_key(Group.SUBGROUP, "*All").add_data('Blue Guardian Invulnerability Time', time, int) + + def gather_gorse_stats(self, events, collector): + spectral_impact_events = events[(events.skillid == Skills.SPECTRAL_IMPACT) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + imprisonment_events = events[(events.skillid == Skills.GHASTLY_PRISON) & events.dst_instid.isin(self.players.index) & (events.is_buffremove == 0)] + imprisonment_events = self.combine_by_time_range_and_instid(imprisonment_events, 1000) + self.gather_count_stat('Unmitigated Spectral Impacts', collector, True, True, spectral_impact_events) + self.gather_count_stat('Ghastly Imprisonments', collector, True, False, imprisonment_events) + self.gorse_spectral_darkness_time('Spectral Darkness', collector, events) + + def gorse_spectral_darkness_time(self, name, collector, events): + times = self.generate_player_buff_times(events, self.players, Skills.SPECTRAL_DARKNESS) + collector = collector.with_key(Group.PHASE, "All") + def count(collector, times): + collector.add_data(name, times['duration'].sum(), int) + split_by_player_groups(collector, count, times, 'player', self.subgroups, self.players) + + def gather_sab_stats(self, events, collector): + bomb_explosion_events = events[(events.skillid == Skills.HEAVY_BOMB_EXPLODE) & (events.is_buffremove == 1)] + self.gather_count_stat('Heavy Bombs Undefused', collector, False, False, bomb_explosion_events) + + def gather_sloth_stats(self, events, collector): + tantrum_hits = events[(events.skillid == Skills.TANTRUM) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + spores_received = events[(events.skillid == Skills.BLEEDING) & events.dst_instid.isin(self.players.index) & (events.value > 0) & (events.is_buffremove == 0)] + spores_blocked = events[(events.skillid == Skills.BLEEDING) & events.dst_instid.isin(self.players.index) & (events.value == 0) & (events.is_buffremove == 0)] + volatile_poison = events[(events.skillid == Skills.VOLATILE_POISON) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 0)] + toxic_cloud = events[(events.skillid == Skills.TOXIC_CLOUD) & events.dst_instid.isin(self.players.index) & (events.value == 0)] + self.gather_count_stat('Tantrum Knockdowns', collector, True, False, tantrum_hits) + self.gather_count_stat('Spores Received', collector, True, False, spores_received, lambda e: len(e) / 5) + self.gather_count_stat('Spores Blocked', collector, True, False, spores_blocked, lambda e: len(e) / 5) + self.gather_count_stat('Volatile Poison Carrier', collector, True, False, volatile_poison) + self.gather_count_stat('Toxic Cloud Breathed', collector, True, False, toxic_cloud) + + def gather_matt_stats(self, events, collector): + unbalanced_events = events[(events.skillid == Skills.UNBALANCED) & events.dst_instid.isin(self.players.index) & (events.buff == 0)] + surrender_events = events[(events.skillid == Skills.SURRENDER) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + burning_events = events[(events.skillid == Skills.BURNING) & events.dst_instid.isin(self.players.index) & events.src_instid.isin(self.bosses.index) & (events.value > 0) & (events.buff == 1) & (events.is_buffremove == 0)] + corrupted_events = events[(events.skillid == Skills.CORRUPTION) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 0)] + blood_fueled_events = events[(events.skillid == Skills.BLOOD_FUELED) & (events.buff == 1) & (events.is_buffremove == 0)] + sacrifice_events = events[(events.skillid == Skills.SACRIFICE) & (events.buff == 1) & (events.is_buffremove == 0)] + profane_events = events[(events.skillid == Skills.UNSTABLE_BLOOD_MAGIC) & (events.buff == 1) & (events.is_buffremove == 0)] + + self.gather_count_stat('Moved While Unbalanced', collector, True, False, unbalanced_events) + self.gather_count_stat('Surrender', collector, True, False, surrender_events) + self.gather_count_stat('Burning Stacks Received', collector, True, True, burning_events) + self.gather_count_stat('Corrupted', collector, True, False, corrupted_events) + self.gather_count_stat('Matthias Shards Returned', collector, False, False, + blood_fueled_events[blood_fueled_events.dst_instid.isin(self.bosses.index)]) + self.gather_count_stat('Shards Absorbed', collector, True, False, + blood_fueled_events[blood_fueled_events.dst_instid.isin(self.players.index)]) + self.gather_count_stat('Sacrificed', collector, True, False, sacrifice_events) + self.gather_count_stat('Well of the Profane Carrier', collector, True, False, profane_events) + + def gather_kc_stats(self, events, collector): + orb_events = events[events.dst_instid.isin(self.players.index) & events.skillid.isin({Skills.RED_ORB_ATTUNEMENT, Skills.WHITE_ORB_ATTUNEMENT, Skills.RED_ORB, Skills.WHITE_ORB}) & (events.is_buffremove == 0)] + + orb_catch_events = self.generate_kc_orb_catch_events(self.players, orb_events) + + compromised_events = events[(events.skillid == Skills.COMPROMISED) & (events.is_buffremove == 0)] + gaining_power_events = events[(events.skillid == Skills.GAINING_POWER) & (events.is_buffremove == 0)] + magic_blast_intensity_events = events[(events.skillid == Skills.MAGIC_BLAST_INTENSITY) & (events.is_buffremove == 0)] + + self.gather_count_stat('Correct Orb', collector, True, False, orb_catch_events[orb_catch_events.correct == 1]) + self.gather_count_stat('Wrong Orb', collector, True, False, orb_catch_events[orb_catch_events.correct == 0]) + self.gather_count_stat('Rifts Hit', collector, False, False, compromised_events) + self.gather_count_stat('Gaining Power', collector, False, False, gaining_power_events) + self.gather_count_stat('Magic Blast Intensity', collector, False, False, magic_blast_intensity_events) + + def generate_kc_orb_catch_events(self, players, events): + raw_data = np.array([np.arange(0, dtype=int)] * 3, dtype=int).T + for player in list(players.index): + data = np.array([np.arange(0)] * 2).T + player_events = events[(events['dst_instid'] == player)] + + red_attuned = False + for event in player_events.itertuples(): + if event.skillid == Skills.RED_ORB_ATTUNEMENT: + red_attuned = True + elif event.skillid == Skills.WHITE_ORB_ATTUNEMENT: + red_attuned = False + elif event.skillid == Skills.RED_ORB: + data = np.append(data, [[event.time, red_attuned]], axis=0) + elif event.skillid == Skills.WHITE_ORB: + data = np.append(data, [[event.time, not red_attuned]], axis=0) + + data = np.c_[[player] * data.shape[0], data] + raw_data = np.r_[raw_data, data] + + return pd.DataFrame(columns = ['dst_instid', 'time', 'correct'], data = raw_data) + + def gather_xera_stats(self, events, collector): + derangement_events = events[(events.skillid == Skills.DERANGEMENT) & (events.buff == 1) & ((events.dst_instid.isin(self.players.index) & (events.is_buffremove == 0))|(events.src_instid.isin(self.players.index) & (events.is_buffremove == 1)))] + self.gather_count_stat('Derangement', collector, True, False, derangement_events[derangement_events.is_buffremove == 0]) + #self.xera_derangement_max_stacks('Peak Derangement', collector, derangement_events) + + def xera_derangement_max_stacks(self, name, collector, events): + events = events.sort_values(by='time') + + raw_data = np.array([np.arange(0, dtype=int)] * 2, dtype=int).T + for player in list(self.players.index): + player_events = events[((events['dst_instid'] == player)&(events.is_buffremove == 0))| + ((events['src_instid'] == player)&(events.is_buffremove == 1))] + + max_stacks = 0 + stacks = 0 + for event in player_events.itertuples(): + if event.is_buffremove == 0: + stacks = stacks + 1 + elif event.is_buffremove == 1: + print(str(event.time - self.start_time) + " - " + str(stacks)) + if stacks > max_stacks: + max_stacks = stacks + stacks = max(stacks - 8, 0) + + print(stacks) + if stacks > max_stacks: + max_stacks = stacks + raw_data = np.append(raw_data, [[player, max_stacks]], axis=0) + + data = pd.DataFrame(columns = ['player', 'max_stacks'], data = raw_data) + + collector = collector.with_key(Group.PHASE, "All") + def max_stacks(collector, data): + collector.add_data(name, data['max_stacks'].max(), int) + split_by_player_groups(collector, max_stacks, data, 'player', self.subgroups, self.players) + + def gather_cairn_stats(self, events, collector): + displacement_events = events[(events.skillid == Skills.DISPLACEMENT) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + meteor_swarm_events = events[(events.skillid == Skills.METEOR_SWARM) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + meteor_swarm_events = self.combine_by_time_range_and_instid(meteor_swarm_events, 1000, 'dst_instid') + + spatial_manipulation_events = events[(events.skillid.isin(Skills.SPATIAL_MANIPULATION)) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + shared_agony_events = events[(events.skillid == Skills.SHARED_AGONY) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 0)] + self.gather_count_stat('Displacement', collector, True, False, displacement_events) + self.gather_count_stat('Meteor Swarm', collector, True, False, meteor_swarm_events) + self.gather_count_stat('Spatial Manipulation', collector, True, False, spatial_manipulation_events) + self.gather_count_stat('Shared Agony', collector, True, False, shared_agony_events) + + + def gather_mursaat_overseer_stats(self, events, collector): + protect_events = events[(events.skillid == Skills.PROTECT) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 1)] + claim_events = events[(events.skillid == Skills.CLAIM) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 1)] + dispel_events = events[(events.skillid == Skills.DISPEL) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 1)] + soldiers_aura_events = events[(events.skillid == Skills.SOLDIERS_AURA) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + soldiers = events[(events.skillid == Skills.SOLDIERS_AURA)].groupby('src_instid').first() + enemy_tile_events = events[(events.skillid == Skills.ENEMY_TILE) & events.dst_instid.isin(self.players.index)] + + self.gather_count_stat('Protect', collector, True, False, protect_events) + self.gather_count_stat('Claim', collector, True, False, claim_events) + self.gather_count_stat('Dispel', collector, True, False, dispel_events) + self.gather_count_stat('Soldiers', collector, False, False, soldiers) + self.gather_count_stat('Soldier\'s Aura', collector, True, False, soldiers_aura_events) + self.gather_count_stat('Enemy Tile', collector, True, False, enemy_tile_events) + + def gather_samarog_stats(self, events, collector): + claw_events = events[(events.skillid == Skills.SAMAROG_CLAW) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + shockwave_events = events[(events.skillid == Skills.SHOCKWAVE) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + sweep_events = events[(events.skillid == Skills.PRISONER_SWEEP) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + charge_events = events[(events.skillid == Skills.CHARGE) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + guldhem_stun_events = events[(events.skillid == Skills.ANGUISHED_BOLT) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + inevitable_betrayl_events = events[(events.skillid == Skills.INEVITABLE_BETRAYL) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + bludgeon_events = events[(events.skillid == Skills.BLUDGEON) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + fixate_events = events[(events.skillid == Skills.SAMAROG_FIXATE) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 0)] + small_friend_events = events[(events.skillid == Skills.SMALL_FRIEND) & events.dst_instid.isin(self.players.index) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + big_friend_events = events[(events.skillid == Skills.BIG_FRIEND) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 0)] + spear_impact_events = events[(events.skillid == Skills.SPEAR_IMPACT) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + + self.gather_count_stat('Claw', collector, True, True, claw_events) + self.gather_count_stat('Shockwave', collector, True, True, shockwave_events) + self.gather_count_stat('Prisoner Sweep', collector, True, True, sweep_events) + self.gather_count_stat('Charge', collector, True, False, charge_events) + self.gather_count_stat('Anguished Bolt', collector, True, False, guldhem_stun_events) + self.gather_count_stat('Inevitable Betrayl', collector, True, False, inevitable_betrayl_events) + self.gather_count_stat('Bludgeon', collector, True, False, bludgeon_events) + self.gather_count_stat('Fixate', collector, True, True, fixate_events) + self.gather_count_stat('Small Friend', collector, True, True, small_friend_events) + self.gather_count_stat('Big Friend', collector, True, True, big_friend_events) + self.gather_count_stat('Spear Impact', collector, True, True, spear_impact_events) + + def gather_deimos_stats(self, events, collector): + annihilate_events = events[(events.skillid == Skills.ANNIHILATE) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + soul_feast_events = events[(events.skillid == Skills.SOUL_FEAST) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + mind_crush_events = events[(events.skillid == Skills.MIND_CRUSH) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + rapid_decay_events = events[(events.skillid == Skills.RAPID_DECAY) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + shockwave_events = events[(events.skillid == Skills.DEMONIC_SHOCKWAVE) & events.dst_instid.isin(self.players.index) & (events.value > 0)] + teleport_events = events[(events.skillid == Skills.DEIMOS_TELEPORT) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 0)] + tear_consumed_events = events[(events.skillid == Skills.TEAR_CONSUMED) & events.dst_instid.isin(self.players.index) & (events.buff == 1) & (events.is_buffremove == 0)] + + self.gather_count_stat('Annihilate', collector, True, False, annihilate_events) + self.gather_count_stat('Soul Feast', collector, True, False, soul_feast_events) + self.gather_count_stat('Mind Crush', collector, True, False, mind_crush_events) + self.gather_count_stat('Rapid Decay', collector, True, False, rapid_decay_events) + self.gather_count_stat('Demonic Shockwave', collector, True, False, shockwave_events) + self.gather_count_stat('Teleports', collector, True, False, teleport_events) + self.gather_count_stat('Tear Consumed', collector, True, False, tear_consumed_events) + + \ No newline at end of file diff --git a/analyser/buffs.py b/analyser/buffs.py new file mode 100644 index 00000000..c1fdf093 --- /dev/null +++ b/analyser/buffs.py @@ -0,0 +1,278 @@ +from enum import IntEnum +from evtcparser import * +import pandas as pd +import numpy as np + +class StackType(IntEnum): + INTENSITY = 0 + DURATION = 1 + +class BuffType: + def __init__(self, name, code, skillid, stacking, capacity): + self.name = name + self.code = code + self.skillid = skillid + self.stacking = stacking + self.capacity = capacity + +BUFF_TYPES = [ + # General Boons + BuffType('Might', 'might', 740, StackType.INTENSITY, 25), + BuffType('Quickness', 'quickness', 1187, StackType.DURATION, 5), + BuffType('Fury', 'fury', 725, StackType.DURATION, 9), + BuffType('Protection', 'protection', 717, StackType.DURATION, 5), + BuffType('Alacrity', 'alacrity', 30328, StackType.DURATION, 9), + BuffType('Retaliation', 'retaliation', 873, StackType.DURATION, 5), + BuffType('Regeneration', 'regen', 718, StackType.DURATION, 5), + + # Ranger + BuffType('Spotter', 'spotter', 14055, StackType.DURATION, 1), + BuffType('Spirit of Frost', 'spirit_of_frost', 12544, StackType.DURATION, 1), + BuffType('Sun Spirit', 'sun_spirit', 12540, StackType.DURATION, 1), + BuffType('Stone Spirit', 'stone_spirit', 12547, StackType.DURATION, 1), + BuffType('Storm Spirit', 'storm_spirit', 12549, StackType.DURATION, 1), + BuffType('Glyph of Empowerment', 'glyph_of_empowerment', 31803, StackType.DURATION, 1), + BuffType('Grace of the Land', 'gotl', 34062, StackType.INTENSITY, 5), + + # Warrior + BuffType('Empower Allies', 'empower_allies', 14222, StackType.DURATION, 1), + BuffType('Banner of Strength', 'banner_strength', 14417, StackType.DURATION, 1), + BuffType('Banner of Discipline', 'banner_discipline', 14449, StackType.DURATION, 1), + BuffType('Banner of Tactics', 'banner_tactics', 14450, StackType.DURATION, 1), + BuffType('Banner of Defence', 'banner_defence', 14543, StackType.DURATION, 1), + + # Revenant + BuffType('Assassin''s Presence', 'assassins_presence', 26854, StackType.DURATION, 1), + BuffType('Naturalistic Resonance', 'naturalistic_resonance', 29379, StackType.DURATION, 1), + + # Engineer + BuffType('Pinpoint Distribution', 'pinpoint_distribution', 38333, StackType.DURATION, 1), + + # Elementalist + BuffType('Soothing Mist', 'soothing_mist', 5587, StackType.DURATION, 1), + + # Necro + BuffType('Vampiric Presence', 'vampiric_presence', 30285, StackType.DURATION, 1), + + # Thief + BuffType('Lotus Training', 'lotus_training', 32200, StackType.DURATION, 1), + BuffType('Lead Attacks', 'lead_attacks', 34659, StackType.INTENSITY, 15) + + #Future boon ids + #Aegis - 743 + ] + +BUFFS = { buff.name: buff for buff in BUFF_TYPES } + +class BuffTrackIntensity: + def __init__(self, buff_type, dst_instid, src_instids, encounter_start, encounter_end): + self.buff_type = buff_type + self.dst_instid = dst_instid + self.stack_durations = [] + self.current_time = encounter_start + + self.src_trackers = {} + for src in src_instids: + self.src_trackers[src] = [src, 0, 0] + + self.data = [] + + def apply_change(self, time, new_count, src_instid): + tracker = self.src_trackers[src_instid] + if tracker[1] > 0: + duration = time - tracker[2] + if duration > 0: + self.data.append([tracker[2], duration, self.buff_type.code, src_instid, self.dst_instid, tracker[1]]) + tracker[1] = new_count + tracker[2] = time + + def add_event(self, event): + if event.time != self.current_time: + self.simulate_to_time(event.time) + + if event.is_buffremove: + self.clear(event.time) + elif len(self.stack_durations) < self.buff_type.capacity: + end_time = event.time + event.value; + self.stack_durations.append([end_time, event.ult_src_instid]) + self.stack_durations.sort() + self.apply_change(event.time, self.src_trackers[event.ult_src_instid][1] + 1, event.ult_src_instid) + elif self.stack_durations[0][0] < event.time + event.value: + old_src = self.stack_durations[0][1] + if old_src != event.ult_src_instid: + self.apply_change(event.time, self.src_trackers[old_src][1] - 1, old_src) + self.apply_change(event.time, self.src_trackers[event.ult_src_instid][1] + 1, event.ult_src_instid) + end_time = event.time + event.value; + self.stack_durations[0] = [end_time, event.ult_src_instid] + self.stack_durations.sort() + + def clear(self, time): + if len(self.stack_durations) > 0: + self.stack_durations = [] + for x in self.src_trackers: + self.apply_change(time, 0, x) + + def simulate_to_time(self, new_time): + while (len(self.stack_durations) > 0) and (self.stack_durations[0][0] <= new_time): + self.apply_change(self.stack_durations[0][0], self.src_trackers[self.stack_durations[0][1]][1] - 1, self.stack_durations[0][1]) + del self.stack_durations[0] + self.current_time = new_time + + def end_track(self, time): + end_time = int(time) + self.simulate_to_time(end_time) + self.clear(time) + +class BuffTrackDuration: + def __init__(self, buff_type, dst_instid, encounter_start, encounter_end): + self.buff_type = buff_type + self.dst_instid = dst_instid + self.stack_durations = [] + self.data = [] + self.current_time = encounter_start + self.current_src = -1 + self.stack_start = encounter_start + + def apply_change(self, time): + duration = time - self.stack_start + if duration > 0: + self.data.append([self.stack_start, duration, self.buff_type.code, self.current_src, self.dst_instid, 1]) + + def add_event(self, event): + if event.time != self.current_time: + self.simulate(event.time - self.current_time) + + if event.is_buffremove: + if len(self.stack_durations) > 0: + self.stack_durations = [] + self.apply_change(event.time) + self.current_src = -1 + elif len(self.stack_durations) < self.buff_type.capacity: + self.stack_durations.append([event.value, event.ult_src_instid]) + if len(self.stack_durations) == 1: + self.stack_start = event.time + self.current_src = event.ult_src_instid + else: + self.stack_durations.sort() + if self.stack_durations[0][1] != self.current_src: + self.apply_change(event.time) + self.current_src = self.stack_durations[0][1] + self.stack_start = event.time + elif self.stack_durations[0][0] < event.value: + self.stack_durations[0] = [event.value, event.ult_src_instid] + self.stack_durations.sort() + if self.stack_durations[0][1] != self.current_src: + self.apply_change(event.time) + self.current_src = self.stack_durations[0][1] + self.stack_start = event.time + + def simulate(self, delta_time): + remaining_delta = delta_time + while len(self.stack_durations) > 0 and self.stack_durations[0][0] <= remaining_delta: + self.current_time += self.stack_durations[0][0] + remaining_delta -= self.stack_durations[0][0] + del self.stack_durations[0] + if len(self.stack_durations) == 0 or self.stack_durations[0][1] != self.current_src: + self.apply_change(self.current_time) + if len(self.stack_durations) == 0: + self.current_src = -1 + else: + self.current_src = self.stack_durations[0][1] + self.stack_start = self.current_time + + self.current_time += remaining_delta + if len(self.stack_durations) > 0: + self.stack_durations[0][0] -= remaining_delta + + def end_track(self, time): + end_time = int(time) + self.simulate(end_time - self.current_time) + if len(self.stack_durations) > 0: + self.apply_change(end_time) + +class BuffPreprocessor: + + def process_events(self, start_time, end_time, skills, players, player_events): + def process_buff_events(buff_type, buff_events, raw_buff_data): + for player in list(players.index): + relevent_events = buff_events[buff_events['dst_instid'] == player] + + agent_start_time = self.get_time(player_events[player_events['src_instid'] == player], parser.StateChange.SPAWN, start_time) + agent_end_time = self.get_time(player_events[player_events['src_instid'] == player], parser.StateChange.DESPAWN, end_time) + if len(relevent_events) > 0: + if relevent_events.time.min() < agent_start_time: + agent_start_time = start_time + if relevent_events.time.max() > agent_end_time: + agent_end_time = end_time + + if (buff_type.stacking == StackType.INTENSITY): + bufftrack = BuffTrackIntensity(BUFFS[buff_type.name], player, relevent_events['ult_src_instid'].drop_duplicates().tolist(), agent_start_time, agent_end_time) + else: + bufftrack = BuffTrackDuration(BUFFS[buff_type.name], player, agent_start_time, agent_end_time) + + for event in relevent_events.itertuples(): + bufftrack.add_event(event) + bufftrack.end_track(agent_end_time) + + raw_buff_data = raw_buff_data + bufftrack.data + return raw_buff_data + + # Filter out state change and cancellation events + not_cancel_events = player_events[(player_events.state_change == parser.StateChange.NORMAL) + & (player_events.is_activation < parser.Activation.CANCEL_FIRE) + & player_events.dst_instid.isin(players.index)] + + # Extract out the buff events + status_remove_groups = not_cancel_events.groupby('is_buffremove') + if 0 in status_remove_groups.indices: + not_statusremove_events = status_remove_groups.get_group(0) + else: + not_statusremove_events = not_cancel_events[not_cancel_events.is_buffremove == 0] + + #not_statusremove_events = not_cancel_events[not_cancel_events.is_buffremove == 0] + apply_events = not_statusremove_events[(not_statusremove_events.buff != 0) + & (not_statusremove_events.value != 0)] + buff_events = apply_events[['skillid', 'time', 'value', 'overstack_value', 'is_buffremove', 'dst_instid', 'ult_src_instid']] + + # Extract out buff removal events + if 1 in status_remove_groups.indices: + statusremove_events = status_remove_groups.get_group(1) + else: + statusremove_events = not_cancel_events[not_cancel_events.is_buffremove == 1] + #statusremove_events = not_cancel_events[not_cancel_events.is_buffremove == 1] + buffremove_events = statusremove_events[['skillid', 'time', 'value', 'overstack_value', 'is_buffremove', 'dst_instid', 'ult_src_instid']] + + # Combine buff application and removal events + buff_update_events = pd.concat([buff_events, buffremove_events]).sort_values('time') + + # Add in skill ids for ease of processing + buff_update_events[['time', 'value']] = buff_update_events[['time', 'value']].apply(pd.to_numeric) + + raw_buff_data = [] + + groups = buff_update_events.groupby('skillid') + + remaining_buff_types = list(BUFF_TYPES) + for skillid, buff_events in groups: + + relevant_buff_types = list(filter(lambda a: a.skillid == skillid, remaining_buff_types)) + if not relevant_buff_types: + continue + + buff_type = relevant_buff_types[0] + remaining_buff_types.remove(buff_type) + raw_buff_data = process_buff_events(buff_type, buff_events, raw_buff_data) + + buff_data = pd.DataFrame(columns = ['time', 'duration', 'buff', 'src_instid', 'dst_instid', 'stacks'], data = raw_buff_data) + buff_data.fillna(0, inplace=True) + buff_data[['time', 'duration', 'src_instid', 'dst_instid', 'stacks']] = buff_data[['time', 'duration', 'src_instid', 'dst_instid', 'stacks']].apply(pd.to_numeric) + return buff_data; + + #format: time, duration, buff_type, src, dst, stacks + + def get_time(self, player_events, state, start_time): + event = player_events[player_events['state_change'] == state] + if len(event) > 0: + return event.iloc[0]['time'] + return start_time + diff --git a/analyser/collector.py b/analyser/collector.py new file mode 100644 index 00000000..1da50553 --- /dev/null +++ b/analyser/collector.py @@ -0,0 +1,111 @@ +import numpy as np + +class Filter: + def __init__(self, conversion_function, context_function): + self.conversion_function = conversion_function + self.context_function = context_function + + def apply(self, value, context): + with_context = self.context_function(value, context) + if not self.conversion_function: + return with_context + return self.conversion_function(with_context) + +def percentage(n): + return round(n * 100, 1) + +def portion_of(f, name): + return Filter(f, lambda value,context: 0 + if float(context[name]) < 0.001 + else float(value)/float(context[name])) +def portion_of2(f, name1, name2): + return Filter(f, lambda value,context: 0 + if float(context[name1]) < 0.001 and float(context[name2]) < 0.001 + else float(value)/float(context[name1])/float(context[name2])) + +def percentage_of(name): + return portion_of(percentage, name) + +def mapped_to(name): + return Filter(None, lambda value,context: context.get(name).get(value)) + +all_data_types = set() + +#NOTE: May want to add "range" style data to context levels, such as time or total damage? +class Collector: + """ Used for collecting data and automatically structuring it for output. """ + + def __init__(self, ordering, registrations, context, all_data, context_values): + self.ordering = ordering + self.registrations = registrations + self.context = context + self.all_data = all_data + self.context_values = context_values + + @classmethod + def root(cls, ordering): + return cls(ordering, [], {}, {}, {}) + + def group(self, function, data, *group_mappings): + if not group_mappings: + self.run(function, data) + return + group_mapping = list(group_mappings[0]) + group_from,group_to = group_mapping[0:2] + group_filters = group_mapping[2:] + remaining_group_mappings = group_mappings[1:] + groups = data.groupby(group_from) + + for name, group in groups: + for filter in group_filters: + name = filter.apply(name, self.context_values) + self.with_key(group_to, name).group(function, group, *remaining_group_mappings) + + def run(self, function, data): + function(self, data) + + def add_data(self, name, value, type_function = None): + if isinstance(value, np.float) and np.isnan(value): + value = 0 + + if value != None and type_function: + try: + value = type_function(value) + except: + value = type_function.apply(value, self.context_values) + + output_block = self.all_data + sorted_context = [key for key in self.ordering if key in self.context] + sorted([ + key for key in self.context if key not in self.ordering]) + + + + if name == None: + name = self.context[sorted_context.pop()] + + for path_key in sorted_context: + output_block = Collector._navigate(output_block, path_key) + output_block = Collector._navigate(output_block, self.context[path_key]) + if name in output_block: + print("Clash for {0}:{1}".format(self.context, name)) + output_block[name] = value + + def with_key(self, key, value): + new_context = dict(self.context) + new_context[key] = value + return Collector(self.ordering, + self.registrations, + new_context, + self.all_data, + dict(self.context_values)) + + def set_context_value(self, key, value): + self.context_values[key] = value + + @staticmethod + def _navigate(dictionary, key): + if key not in dictionary: + new_node = {} + dictionary[key] = new_node + return new_node + return dictionary[key] diff --git a/analyser/splits.py b/analyser/splits.py new file mode 100644 index 00000000..28264407 --- /dev/null +++ b/analyser/splits.py @@ -0,0 +1,127 @@ +from enum import IntEnum +from evtcparser import * +import pandas as pd +import numpy as np +from functools import reduce +from .collector import * + +class Group: + CATEGORY = "Category" + PLAYER = "Player" + BOSS = "Boss" + PHASE = "Phase" + DESTINATION = "To" + SOURCE = "From" + SKILL = "Skill" + SUBGROUP = "Subgroup" + BUFF = "Buff" + METRICS = "Metrics" + +class ContextType: + DURATION = "Duration" + TOTAL_DAMAGE_FROM_SOURCE_TO_DESTINATION = "Total Damage" + TOTAL_DAMAGE_TO_DESTINATION = "Target Damage" + SKILL_NAME = "Skill Name" + AGENT_NAME = "Agent Name" + PROFESSION_NAME = "Profession Name" + BUFF_TYPE = "Buff" + DESTINATIONS = "Destinations" + +def split_duration_event_by_phase(collector, method, events, phases): + def collect_phase(name, phase_events): + duration = float(phase_events['time'].max() - phase_events['time'].min())/1000.0 + if not duration > 0.001: + duration = 0 + collector.set_context_value(ContextType.DURATION, duration) + collector.with_key(Group.PHASE, name).run(method, phase_events) + + collect_phase("All", events) + + #Yes, this lists each phase individually even if there is only one + #That's for consistency for things like: + #Some things happen outside a phase. + #Some fights have multiple phases, but you only get to phase one + #Still want to list it as phase 1 + for i in range(0,len(phases)): + phase = phases[i] + start = phase[1] + end = phase[2] + + if len(events) > 0: + across_phase = events[(events['time'] < start) & (events['time'] + events['duration'] > end)] + + #HACK: review why copy? + before_phase = events[(events['time'] < start) & (events['time'] + events['duration'] > start) & (events['time'] + events['duration'] <= end)].copy() + main_phase = events[(events['time'] >= start) & (events['time'] + events['duration'] <= end)] + after_phase = events[(events['time'] >= start) & (events['time'] < end) & (events['time'] + events['duration'] > end)] + + across_phase = across_phase.assign(time = start, duration = end - start) + + before_phase.loc[:, 'duration'] = before_phase['duration'] + before_phase['time'] - start + before_phase = before_phase.assign(time = start) + + after_phase = after_phase.assign(duration = end) + after_phase.loc[:, 'duration'] = after_phase['duration'] - after_phase['time'] + + collect_phase(phase[0], across_phase.append(before_phase).append(main_phase).append(after_phase)) + else: + collect_phase(phase[0], events) + +def split_by_phase(collector, method, events, phases): + def collect_phase(name, phase_events, duration): + #duration = float(phase_events['time'].max() - phase_events['time'].min())/1000.0 + if not duration > 0.001: + duration = 0 + collector.set_context_value(ContextType.DURATION, duration) + collector.with_key(Group.PHASE, name).run(method, phase_events) + + collect_phase("All", events, float(events['time'].max() - events['time'].min()) / 1000.0) + + #Yes, this lists each phase individually even if there is only one + #That's for consistency for things like: + #Some things happen outside a phase. + #Some fights have multiple phases, but you only get to phase one + #Still want to list it as phase 1 + for i in range(0,len(phases)): + phase = phases[i] + phase_events = events[(events.time >= phase[1]) & (events.time <= phase[2])] + collect_phase(phase[0], phase_events, (phase[2] - phase[1]) / 1000.0) + +def split_by_player_groups(collector, method, events, player_column, subgroups, players): + collector.set_context_value(ContextType.DESTINATIONS, len(players)) + collector.with_key(Group.SUBGROUP, "*All").run(method, events) + for subgroup in subgroups: + subgroup_players = subgroups[subgroup] + subgroup_events = events[events[player_column].isin(subgroup_players)] + collector.set_context_value(ContextType.DESTINATIONS, len(subgroup_players)) + collector.with_key(Group.SUBGROUP, "{0}".format(subgroup)).run( + method, subgroup_events) + split_by_player(collector, method, events, player_column, players) + +def split_by_player(collector, method, events, player_column, players): + for character in players.groupby('name').groups.items(): + characters = players[players['name'] == character[0]] + collector.set_context_value(ContextType.DESTINATIONS, 1) + collector.with_key(Group.PLAYER, character[0]).run(method,events[events[player_column].isin(characters.index)]) + +def split_by_agent(collector, method, events, group, enemy_column, bosses, players): + boss_events = events[events[enemy_column].isin(bosses)] + player_events = events[events[enemy_column].isin(players)] + + non_add_instids = bosses + add_events = events[events[enemy_column].isin(non_add_instids) != True] + + collector.with_key(group, "*All").run(method, events) + collector.with_key(group, "*Boss").run(method, boss_events) + collector.with_key(group, "*Players").run(method, player_events) + collector.with_key(group, "*Adds").run(method, add_events) + if len(bosses) > 1: + split_by_boss(collector, method, boss_events, enemy_column, group) + +def split_by_boss(collector, method, events, enemy_column, group): + collector.group(method, events, + (enemy_column, group, mapped_to(ContextType.AGENT_NAME))) + +def split_by_skill(collector, method, events): + collector.group(method, events, + ('skillid', Group.SKILL, mapped_to(ContextType.SKILL_NAME))) diff --git a/bin/regen_db.sh b/bin/regen_db.sh index 86b68dda..af3ec02e 100755 --- a/bin/regen_db.sh +++ b/bin/regen_db.sh @@ -1,9 +1,30 @@ #!/bin/bash +email="$1" +name="$2" +password="$3" + cd $(dirname "$0")/.. -rm db.sqlite3 +rm -f db.sqlite3 +dbengine=$(python3 -c "from gw2raidar.settings import DATABASES; print(DATABASES['default']['ENGINE'])") +if [[ "$dbengine" == "django.db.backends.postgresql" ]]; then + dbname=$(python3 -c "from gw2raidar.settings import DATABASES; print(DATABASES['default']['NAME'])") + dbuser=$(python3 -c "from gw2raidar.settings import DATABASES; print(DATABASES['default']['USER'])") + dropdb $dbname + createdb $dbname -O $dbuser +elif [[ "$dbengine" == "django.db.backends.sqlite3" ]]; then + dbname=$(python3 -c "from gw2raidar.settings import DATABASES; print(DATABASES['default']['NAME'])") + rm $dbname +fi python3 manage.py migrate -python3 manage.py createsuperuser +if [[ "$email" == "-n" ]]; then + echo Skipping superuser +elif [[ -z "$password" ]]; then + echo Superuser: + python3 manage.py createsuperuser +else + echo "from django.contrib.auth.models import User; User.objects.create_superuser('$name', '$email', '$password')" | python3 manage.py shell > /dev/null +fi echo "After this, you may want to" echo "python3 manage.py runserver" diff --git a/evtcparser/parser.py b/evtcparser/parser.py index 85315924..83968859 100644 --- a/evtcparser/parser.py +++ b/evtcparser/parser.py @@ -1,17 +1,20 @@ -import struct import re -from enum import Enum +from enum import IntEnum +import struct +import numpy as np +import pandas as pd +from io import UnsupportedOperation ENCODING = "utf8" -class Activation(Enum): +class Activation(IntEnum): NONE = 0 NORMAL = 1 QUICKNESS = 2 CANCEL_FIRE = 3 CANCEL_CANCEL = 4 -class StateChange(Enum): +class StateChange(IntEnum): NORMAL = 0 ENTER_COMBAT = 1 EXIT_COMBAT = 2 @@ -23,9 +26,16 @@ class StateChange(Enum): HEALTH_UPDATE = 8 LOG_START = 9 LOG_END = 10 + WEAPON_SWAP = 11 + MAX_HEALTH_UPDATE = 12 + POINT_OF_VIEW = 13 + LANGUAGE = 14 + GW_BUILD = 15 + SHARD_ID = 16 + REWARD = 17 #Change to another data type - it's not really an enum? -class AgentType(Enum): +class AgentType(IntEnum): NO_ID = -1 UNKNOWN = 0 GUARDIAN = 1 @@ -37,23 +47,22 @@ class AgentType(Enum): MESMER = 7 NECROMANCER = 8 REVENANT = 9 - MURSAAT_OVERSEER = 17172 def is_player(self): - return AgentType.GUARDIAN.value <= self.value <= AgentType.REVENANT.value + return AgentType.GUARDIAN <= self <= AgentType.REVENANT -class CustomSkill(Enum): +class CustomSkill(IntEnum): RESURRECT = 1066 BANDAGE = 1175 DODGE = 65001 -class IFF(Enum): +class IFF(IntEnum): FRIEND = 0 FOE = 1 UNKNOWN = 2 -class Result(Enum): +class Result(IntEnum): NORMAL=0 CRIT=1 GLANCE=2 @@ -64,7 +73,7 @@ class Result(Enum): BLIND=7 KILLING_BLOW=8 -class Boon(Enum): +class Boon(IntEnum): MIGHT = 1 QUICKNESS = 2 FURY = 3 @@ -81,74 +90,149 @@ class Boon(Enum): SOOTHING_MIST = 14 -class FileFormatException(BaseException): +class EvtcParseException(BaseException): pass -class Agent: - def whitelistName(self, name): - new_name = re.sub("[^\w \\.\\-]","?", name) - if new_name != name: - print("Unexpected name: {0}", name.__repr__()) - return new_name - - def __init__(self, data): - self.addr, prof, elite, self.toughness, self.healing, self.condition, name_account = struct.unpack(" 0 - self.name, self.account = name_account.decode(ENCODING).split("\0")[0:2] - if self.account: - self.account = self.whitelistName(self.account[1:]) - self.name = self.whitelistName(self.name) - - self.inst_id = None - - def __str__(self): - return "{0} ({1}) - {2} (elite: {3}) - id {4}".format(self.name, self.account, self.prof, self.elite, self.addr) - - def set_inst_id(self, id): - if id == 0: - return - if self.inst_id is None: - self.inst_id = id - else: - if self.inst_id != id: - raise Exception("Multiples ids for agent {0}: {1},{2}", self.name, self.inst_id, id) - -class Skill: - def __init__(self, data): - self.id, name = struct.unpack(" 1: + self.agents['name'] = split[0].str.decode(ENCODING) + self.agents['account'] = split[1].str.decode(ENCODING) + self.agents['party'] = split[2].fillna(0).astype(np.uint8) + else: + self.agents['account'] = None + self.agents['party'] = 0 + def _read_skills(self, file): num_skills, = struct.unpack("{url_id}", url_id=obj.url_id) + url_id_link.short_description = "Link" + + search_fields = ('=url_id', '=filename', '^area__name', '=characters__name', '^characters__account__name', '=characters__account__user__username', '=tags__name', '=category__name') + list_display = ('filename', 'url_id_link', 'area', 'success', 'category', 'started_at', 'duration', 'uploaded_at', 'uploaded_by') + list_select_related = ('category', 'uploaded_by', 'area') inlines = (ParticipationInline,) + readonly_fields = ('url_id', 'started_at', 'duration', 'uploaded_at', 'uploaded_by', 'area', 'filename') # hack, but... ugly otherwise class Media: css = { 'all' : ('raidar/hide_admin_original.css',) } +class CharacterAdmin(QuotedSearchModelAdmin): + search_fields = ('=name', '^account__name', '=account__user__username') + list_display = ('name', 'profession', 'account') + readonly_fields = ('account', 'name', 'profession') + class CharacterInline(admin.TabularInline): model = Character + readonly_fields = ('name', 'profession') extra = 1 -class AccountAdmin(admin.ModelAdmin): +class AccountAdmin(QuotedSearchModelAdmin): + search_fields = ('^name', '=user__username') + list_display = ('name', 'user') inlines = (CharacterInline,) + readonly_fields = ('name',) # hack, but... ugly otherwise class Media: css = { 'all' : ('raidar/hide_admin_original.css',) } -admin.site.register(Area) + +admin.site.register(Area, AreaAdmin) +admin.site.register(Era, EraAdmin) +admin.site.register(Category, CategoryAdmin) admin.site.register(Account, AccountAdmin) -admin.site.register(Character) +admin.site.register(Character, CharacterAdmin) admin.site.register(Encounter, EncounterAdmin) +admin.site.register(UserProfile) diff --git a/raidar/backends.py b/raidar/backends.py new file mode 100644 index 00000000..38ce7fda --- /dev/null +++ b/raidar/backends.py @@ -0,0 +1,32 @@ +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.models import User + +class EmailAuthBackend(ModelBackend): + """ + Email Authentication Backend + + Allows a user to sign in using an email/password pair, then check + a username/password pair if email failed + """ + + # https://stackoverflow.com/questions/27953859/authenticate-users-with-both-username-and-email + def authenticate(self, username=None, password=None): + """ Authenticate a user based on email address as the user name. """ + try: + user = User.objects.get(email=username) + if user.check_password(password): + return user + except User.DoesNotExist: + try: + user = User.objects.get(username=username) + if user.check_password(password): + return user + except User.DoesNotExist: + return None + + def get_user(self, user_id): + """ Get a User object from the user_id. """ + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None diff --git a/raidar/management/__init__.py b/raidar/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/raidar/management/commands/__init__.py b/raidar/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/raidar/management/commands/_qsetiter.py b/raidar/management/commands/_qsetiter.py new file mode 100644 index 00000000..8f8dd2a7 --- /dev/null +++ b/raidar/management/commands/_qsetiter.py @@ -0,0 +1,27 @@ +# https://djangosnippets.org/snippets/1949/ +import gc + +def queryset_iterator(queryset, chunksize=1000): + ''''' + Iterate over a Django Queryset ordered by the primary key + + This method loads a maximum of chunksize (default: 1000) rows in it's + memory at the same time while django normally would load all rows in it's + memory. Using the iterator() method only causes it to not preload all the + classes. + + Note that the implementation of the iterator does not support ordered query sets. + ''' + pk = 0 + last_pk_query = queryset.order_by('-pk') + try: + last_pk = last_pk_query[0].pk + except IndexError: + return + + queryset = queryset.order_by('pk') + while pk < last_pk: + for row in queryset.filter(pk__gt=pk)[:chunksize]: + pk = row.pk + yield row + gc.collect() diff --git a/raidar/management/commands/process_uploads.py b/raidar/management/commands/process_uploads.py new file mode 100644 index 00000000..82ce5cc8 --- /dev/null +++ b/raidar/management/commands/process_uploads.py @@ -0,0 +1,365 @@ +from analyser.analyser import Analyser, Group, Archetype, EvtcAnalysisException +from multiprocessing import Queue, Process, log_to_stderr +from contextlib import contextmanager +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction +from django.db.utils import IntegrityError +from evtcparser.parser import Encounter as EvtcEncounter, EvtcParseException +from gw2raidar import settings +from raidar.models import * +from sys import exit, stderr +from time import time +from zipfile import ZipFile, BadZipFile +from queue import Empty +import os +import os.path +import logging +import signal +from traceback import format_exc + + +logger = log_to_stderr() +logger.setLevel(logging.INFO) + +# inspired by https://stackoverflow.com/a/31464349/240443 +class GracefulKiller: + def __init__(self, queue): + signal.signal(signal.SIGINT, self.exit_gracefully) + signal.signal(signal.SIGTERM, self.exit_gracefully) + self.queue = queue + + def exit_gracefully(self, signum, frame): + logger.info('clearing the queue') + try: + while True: + self.queue.get_nowait() + except Empty: + pass + + + +# Google Drive +# pip install --upgrade google-api-python-client + + +def get_gdrive_service(): + return None + +gdrive_service = None +if hasattr(settings, 'GOOGLE_CREDENTIAL_FILE'): + try: + from oauth2client.service_account import ServiceAccountCredentials + from httplib2 import Http + from apiclient import discovery + from googleapiclient.http import MediaFileUpload + from googleapiclient.errors import HttpError + + def get_gdrive_service(): + scopes = ['https://www.googleapis.com/auth/drive.file'] + credentials = ServiceAccountCredentials.from_json_keyfile_name( + settings.GOOGLE_CREDENTIAL_FILE, scopes=scopes) + http_auth = credentials.authorize(Http()) + gdrive_service = discovery.build('drive', 'v3', http=http_auth) + return gdrive_service + + gdrive_service = get_gdrive_service() + + try: + gdrive_folder = Variable.get('gdrive_folder') + except Variable.DoesNotExist: + metadata = { + 'name' : 'GW2 Raidar Files', + 'mimeType' : 'application/vnd.google-apps.folder' + } + folder = gdrive_service.files().create( + body=metadata, fields='id').execute() + gdrive_folder = folder.get('id') + + permission = { + 'role': 'reader', + 'type': 'anyone', + 'allowFileDiscovery': False + } + result = gdrive_service.permissions().create( + fileId=gdrive_folder, + body=permission, + fields='id', + ).execute() + + Variable.set('gdrive_folder', gdrive_folder) + except ImportError: + pass + + +if hasattr(settings, 'UPLOAD_DIR'): + upload_dir = settings.UPLOAD_DIR +else: + upload_dir = 'uploads' + + +@contextmanager +def single_process(name): + try: + pid = os.getpid() + pid_var = Variable.objects.create(key='%s_pid' % name, val=os.getpid()) + except IntegrityError: + # already running + exit() + + try: + yield pid + finally: + pid_var.delete() + + +class Command(BaseCommand): + help = 'Processes the uploads' + + def add_arguments(self, parser): + parser.add_argument('-p', '--processes', + type=int, + dest='processes', + default=1, + help='Number of parallel processes') + parser.add_argument('-l', '--limit', + type=int, + dest='limit', + help='Limit of uploads to process') + + def handle(self, *args, **options): + with single_process('process_uploads'): + start = time() + self.analyse_uploads(*args, **options) + self.clean_up(*args, **options) + end = time() + + if options['verbosity'] >= 3: + print() + print("Completed in %ss" % (end - start)) + + def analyse_uploads(self, *args, **options): + new_uploads = Upload.objects.order_by('-filename') + if 'limit' in options: + new_uploads = new_uploads[:options['limit']] + + queue = Queue(len(new_uploads)) + killer = GracefulKiller(queue) + + for upload in new_uploads: + queue.put(upload) + + from django import db + db.connections.close_all() + + if options['processes'] > 1: + process_pool = [] + for i in range(options['processes']): + process = Process(target=self.analyse_upload_worker, args=(queue,)) + process_pool.append(process) + process.start() + + for process in process_pool: + process.join() + else: + self.analyse_upload_worker(queue, False) + + def analyse_upload_worker(self, queue, multi=True): + if multi: + from Crypto import Random + Random.atfork() + self.gdrive_service = get_gdrive_service() + try: + while True: + upload = queue.get_nowait() + logger.info(upload.filename) + self.analyse_upload(upload) + except Empty: + logger.info("done") + + def analyse_upload(self, upload): + diskname = upload.diskname() + zipfile = None + file = None + + try: + if upload.filename.endswith('.evtc.zip'): + zipfile = ZipFile(diskname) + contents = zipfile.infolist() + if len(contents) == 1: + try: + file = zipfile.open(contents[0].filename) + except RuntimeError as e: + raise EvtcAnalysisException(e) + else: + raise EvtcParseException('Only single-file ZIP archives are allowed') + else: + file = open(diskname, 'rb') + + evtc_encounter = EvtcEncounter(file) + + analyser = Analyser(evtc_encounter) + + dump = analyser.data + uploader = upload.uploaded_by + + started_at = dump['Category']['encounter']['start'] + duration = dump['Category']['encounter']['duration'] + success = dump['Category']['encounter']['success'] + if duration < 60: + raise EvtcAnalysisException('Encounter shorter than 60s') + + era = Era.by_time(started_at) + area, _ = Area.objects.get_or_create(id=evtc_encounter.area_id, + defaults={ "name": analyser.boss_info.name }) + + status_for = {name: player for name, player in dump[Group.CATEGORY]['status']['Player'].items() if 'account' in player} + account_names = [player['account'] for player in status_for.values()] + + with transaction.atomic(): + # heuristics to see if the encounter is a re-upload: + # a character can only be in one raid at a time + # account_names are being hashed, and the triplet + # (area, account_hash, started_at) is being checked for + # uniqueness (along with some fuzzing to started_at) + started_at_full, started_at_half = Encounter.calculate_start_guards(started_at) + account_hash = Encounter.calculate_account_hash(account_names) + try: + encounter = Encounter.objects.get( + Q(started_at_full=started_at_full) | Q(started_at_half=started_at_half), + area=area, account_hash=account_hash + ) + encounter.era = era + encounter.filename = upload.filename + encounter.uploaded_at = upload.uploaded_at + encounter.uploaded_by = upload.uploaded_by + encounter.duration = duration + encounter.success = success + encounter.val = dump + encounter.started_at = started_at + encounter.started_at_full = started_at_full + encounter.started_at_half = started_at_half + if not zipfile: + encounter.filename += ".zip" + encounter.save() + except Encounter.DoesNotExist: + encounter = Encounter.objects.create( + filename=upload.filename, + uploaded_at=upload.uploaded_at, uploaded_by=upload.uploaded_by, + duration=duration, success=success, val=dump, + area=area, era=era, started_at=started_at, + started_at_full=started_at_full, started_at_half=started_at_half, + account_hash=account_hash + ) + + file.close() + file = None + new_diskname = encounter.diskname() + os.makedirs(os.path.dirname(new_diskname), exist_ok=True) + if zipfile: + zipfile.close() + zipfile = None + os.rename(diskname, new_diskname) + else: + with ZipFile(new_diskname, 'w') as zipfile_out: + zipfile_out.write(diskname) + + for name, player in status_for.items(): + account, _ = Account.objects.get_or_create( + name=player['account']) + character, _ = Character.objects.get_or_create( + name=name, account=account, + defaults={ + 'profession': player['profession'] + } + ) + participation, _ = Participation.objects.update_or_create( + character=character, encounter=encounter, + defaults={ + 'archetype': player['archetype'], + 'party': player['party'], + 'elite': player['elite'] + } + ) + if account.user: + Notification.objects.create(user=account.user, val={ + "type": "upload", + "upload_id": upload.id, + "uploaded_by": upload.uploaded_by.username, + "filename": upload.filename, + "encounter_id": encounter.id, + "encounter_url_id": encounter.url_id, + "encounter": participation.data(), + }) + if uploader and account.user_id == uploader.id: + uploader = None + + if uploader: + Notification.objects.create(user=uploader, val={ + "type": "upload", + "upload_id": upload.id, + "uploaded_by": upload.uploaded_by.username, + "filename": upload.filename, + "encounter_id": encounter.id, + "encounter_url_id": encounter.url_id, + }) + + if self.gdrive_service: + media = MediaFileUpload(new_diskname, mimetype='application/prs.evtc') + try: + if encounter.gdrive_id: + result = self.gdrive_service.files().update( + fileId=encounter.gdrive_id, + media_body=media, + ).execute() + else: + metadata = { + 'name': upload.filename, + 'parents': [gdrive_folder], + } + gdrive_file = self.gdrive_service.files().create( + body=metadata, media_body=media, + fields='id, webContentLink', + ).execute() + encounter.gdrive_id = gdrive_file['id'] + encounter.gdrive_url = gdrive_file['webContentLink'] + encounter.save() + except HttpError as e: + logger.error(e) + pass + + except (EvtcParseException, EvtcAnalysisException, BadZipFile) as e: + Notification.objects.create(user=upload.uploaded_by, val={ + "type": "upload_error", + "upload_id": upload.id, + "error": str(e), + }) + + # for diagnostics and catching new exceptions + except Exception as e: + exc = format_exc() + path = os.path.join(upload_dir, 'errors') + os.makedirs(path, exist_ok=True) + path = os.path.join(path, os.path.basename(diskname)) + os.rename(diskname, path) + with open(path + '.error', 'w') as f: + f.write("%s (%s)\n" % (upload.filename, upload.uploaded_by.username)) + f.write(exc) + logger.error(exc) + Notification.objects.create(user=upload.uploaded_by, val={ + "type": "upload_error", + "upload_id": upload.id, + "error": "An unexpected error has occured, and your file has been stored for inspection by the developers.", + }) + + finally: + if file: + file.close() + + if zipfile: + zipfile.close() + + upload.delete() + + def clean_up(self, *args, **options): + # delete Notifications older than 15s (assuming poll is every 10s) + Notification.objects.filter(created_at__lt=time() - 15).delete() diff --git a/raidar/management/commands/restat.py b/raidar/management/commands/restat.py new file mode 100644 index 00000000..3ae2d957 --- /dev/null +++ b/raidar/management/commands/restat.py @@ -0,0 +1,418 @@ +from ._qsetiter import queryset_iterator +from collections import defaultdict +from contextlib import contextmanager +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction +from django.db.utils import IntegrityError +from gw2raidar import settings +from analyser.bosses import BOSSES, Kind +from os.path import join as path_join +from raidar.models import * +from sys import exit +from time import time +import os +import csv +from evtcparser.parser import AgentType +import pandas as pd +import numpy as np +import base64 +# XXX DEBUG +# import logging +# l = logging.getLogger('django.db.backends') +# l.setLevel(logging.DEBUG) +# l.addHandler(logging.StreamHandler()) + + + +@contextmanager +def single_process(name): + try: + pid = os.getpid() + #Uncomment to remove pid in case of having cancelled restat with Ctrl+C... + #Variable.objects.get(key='%s_pid' % name).delete() + pid_var = Variable.objects.create(key='%s_pid' % name, val=os.getpid()) + except IntegrityError: + # already running + exit() + + try: + yield pid + finally: + pid_var.delete() + +@contextmanager +def necessary(force=False): + try: + last_run = Variable.get('restat_last') + except Variable.DoesNotExist: + last_run = 0 + + start = time() + + new_encounters = Encounter.objects.filter(uploaded_at__gte=last_run).count() + if not (new_encounters or force): + exit() + + yield last_run + + # only if successful: + Variable.set('restat_last', start) + + +class RestatException(Exception): + pass + +def name_for(id): + if id in BOSSES: + return BOSSES[id].name + return AgentType(id).name + + +def navigate(node, *names): + new_node = node + for name in names: + if name not in new_node: + new_node[name] = dict() + new_node = new_node[name] + return new_node + +#Automated statistics style: +def count(output): + current = output.get('count', 0) + output['count'] = current+ 1 + +def bound_stats(output, name, value): + maxprop = 'max_' + name + if maxprop not in output or value > output[maxprop]: + output[maxprop] = value + + minprop = 'min_' + name + if minprop not in output or value < output[minprop]: + output[minprop] = value + +def advanced_stats(maximum_percentile_samples): + return lambda a,b,c: advanced_stats_internal(maximum_percentile_samples, a, b, c) + +def advanced_stats_internal(maximum_percentile_samples, output, name, value): + bound_stats(output, name, value) + average_stats(output, name, value) + l = output.get('values|' + name, []) + if(len(l) < maximum_percentile_samples): + l.append(value) + output['values|' + name] = l + +def all_stats(output, name, value): + bound_stats(output, name, value) + average_stats(output, name, value) + +def average_stats(output, name, value): + output['avgsum|' + name] = output.get('avgsum|' + name, 0) + value + output['avgnum|' + name] = output.get('avgnum|' + name, 0) + 1 + +def finalise_stats(node): + try: + for key in list(node): + sections = str(key).split('|') + if sections[0] == 'avgsum': + node['avg_' + sections[1]] = node[key]/node['avgnum|' + sections[1]] + del node['avgsum|' + sections[1]] + del node['avgnum|' + sections[1]] + if sections[0] == 'values': + values = node[key] + """def percentile(n): + i, p = divmod((len(values)-1) * n, 100) + n = values[i] + if(p > 0): + n = ((n * (100-p)) + (values[i+1] * p))/100 + return n""" + #node['n_' + sections[1]] = len(values) + b = np.percentile(values, q = range(0,100)).astype(np.float32).tobytes() + node['per_' + sections[1]] = base64.b64encode(b).decode('utf-8') + #node['per_a_' + sections[1]] = np.frombuffer(b, np.float32).tolist() + del node['values|' + sections[1]] + elif key in node: + finalise_stats(node[key]) + except TypeError: + pass + +def _safe_get(func, default=0): + try: + return func() + except (KeyError, TypeError): + return default + +#subprocesses +def calculate(l, f, *args): + for t in l: + f(t, *args) + +def calculate_standard_stats(f, stats, main_stat_targets, incoming_buff_targets, outgoing_buff_targets): + stats_in_phase_to_all = _safe_get(lambda: stats['Metrics']['damage']['To']['*All']) + stats_in_phase_to_boss = _safe_get(lambda: stats['Metrics']['damage']['To']['*Boss']) + stats_in_phase_from_all = _safe_get(lambda: stats['Metrics']['damage']['From']['*All']) + shielded_in_phase_from_all = _safe_get(lambda: stats['Metrics']['shielded']['From']['*All']) + outgoing_buff_stats = _safe_get(lambda: stats['Metrics']['buffs']['To']['*All'], {}) + incoming_buff_stats = _safe_get(lambda: stats['Metrics']['buffs']['From']['*All'], {}) + + for stat in ['dps','crit','seaweed','scholar','flanking']: + calculate(main_stat_targets, f, stat, _safe_get(lambda: stats_in_phase_to_all[stat])) + calculate(main_stat_targets, f, 'dps_boss', _safe_get(lambda: stats_in_phase_to_boss['dps'])) + calculate(main_stat_targets, f, 'dps_received', _safe_get(lambda: stats_in_phase_from_all['dps'])) + calculate(main_stat_targets, f, 'total_received', _safe_get(lambda: stats_in_phase_from_all['total'])) + calculate(main_stat_targets, f, 'total_shielded', _safe_get(lambda: shielded_in_phase_from_all['total'])) + + for buff, value in incoming_buff_stats.items(): + calculate(incoming_buff_targets, f, buff, value) + + for buff, value in outgoing_buff_stats.items(): + calculate(outgoing_buff_targets, f, buff, value) + +def navigate_to_profile_outputs(totals, participation, encounter, boss): + class ProfileOutputs: + def __init__(self, breakdown, all, encounter_stats): + self.breakdown = breakdown + self.all = all + self.encounter_stats = encounter_stats + + def categorise(split_encounter, split_archetype, split_profession): + return navigate(totals_for_player, + 'encounter', encounter.area_id if split_encounter else 'All %s bosses' % boss.kind.name.lower(), + 'archetype', participation.archetype if split_archetype else 'All', + 'profession', participation.character.profession if split_profession else 'All', + 'elite', participation.elite if split_profession else 'All') + + user_id = participation.character.account.user_id + + if not user_id: + return ProfileOutputs([],[],[]) + # TODO if user_id: # (otherwise we're ignoring the person) + totals_for_player = navigate(totals['user'], user_id) + #TODO: Remove if the one in player_summary is enough! + count(totals_for_player) + #profile categorisations + player_summary = navigate(totals_for_player, 'summary') + player_this_encounter = categorise(True, False, False) + player_this_archetype = categorise(False, True, False) + player_this_profession = categorise(False, False, True) + player_this_build = categorise(False, True, True) + player_archetype_encounter = categorise(True, True, False) + player_build_encounter = categorise(True, True, True) + player_profession_encounter = categorise(True, False, True) + player_all = categorise(False, False, False) + + breakdown = [player_this_build, + player_this_archetype, + player_this_profession, + player_archetype_encounter, + player_build_encounter, + player_profession_encounter] + all = breakdown + [player_summary, player_this_encounter, player_all] + encounter_stats = [player_this_encounter, + player_archetype_encounter, + player_build_encounter, + player_profession_encounter, + player_all] + return ProfileOutputs(breakdown, all, encounter_stats) + +class Command(BaseCommand): + help = 'Recalculates the stats' + + def add_arguments(self, parser): + parser.add_argument('-f', '--force', + action='store_true', + dest='force', + default=False, + help='Force calculation even if no new Encounters') + + parser.add_argument('-p', '--percentile_samples', + action='store', + dest='percentile_samples', + type=int, + default=1000, + help='Indicates the maximum number of samples to store for percentile sampling') + + def handle(self, *args, **options): + with single_process('restat'), necessary(options['force']) as last_run: + start = time() + self.calculate_stats(*args, **options) + end = time() + + if options['verbosity'] >= 3: + print() + print("Completed in %ss" % (end - start)) + + + + def calculate_stats(self, *args, **options): + for era in Era.objects.all(): + # TODO: don't recalculate eras with no uploads + totals = { + "area": {}, + "user": {} + } + era_queryset = era.encounters.all().order_by('?') + totals_in_era = {} + for encounter in queryset_iterator(era_queryset): + boss = BOSSES[encounter.area_id] + try: + data = encounter.val + duration = data['Category']['encounter']['duration'] * 1000 + phases = data['Category']['combat']['Phase'] + totals_in_area = navigate(totals['area'], encounter.area_id) + + for phase, stats_in_phase in phases.items(): + squad_stats = stats_in_phase['Subgroup']['*All'] + phase_duration = data['Category']['encounter']['duration'] if phase == 'All' else _safe_get(lambda: data['Category']['encounter']['Phase'][phase]['duration']) + group_totals = navigate(totals_in_area, phase, 'group') + buffs_by_party = navigate(group_totals, 'buffs') + buffs_out_by_party = navigate(group_totals, 'buffs_out') + + group_totals_era = navigate(totals_in_era, phase, 'group') + buffs_by_party_era = navigate(group_totals_era, 'buffs') + buffs_out_by_party_era = navigate(group_totals_era, 'buffs_out') + + + if(encounter.success): + calculate([group_totals, group_totals_era], + advanced_stats(options['percentile_samples']), + 'duration', + phase_duration) + calculate([group_totals, group_totals_era], count) + calculate_standard_stats( + advanced_stats(options['percentile_samples']), + squad_stats, + [group_totals, group_totals_era], + [buffs_by_party, buffs_by_party_era], + [buffs_out_by_party, buffs_out_by_party_era]) + + individual_totals = navigate(totals_in_area, phase, 'individual') + individual_totals_era = navigate(totals_in_era, phase, 'individual') + participations = encounter.participations.select_related('character', 'character__account').all() + for participation in participations: + # XXX in case player did not actually participate (hopefully fix in analyser) + if (participation.character.name not in stats_in_phase['Player']): + continue + player_stats = stats_in_phase['Player'][participation.character.name] + + prof = participation.character.profession + arch = participation.archetype + elite = participation.elite + totals_by_build = navigate(totals_in_area, phase, 'build', prof, elite, arch) + totals_by_archetype = navigate(totals_in_area, phase, 'build', 'All', 'All', arch) + totals_by_spec = navigate(totals_in_area, phase, 'build', prof, elite, 'All') + buffs_by_build = navigate(totals_by_build, 'buffs') + buffs_out_by_build = navigate(totals_by_build, 'buffs_out') + + #todo: add these only if in phase "all" + totals_by_build_era = navigate(totals_in_era, phase, 'build', prof, elite, arch) + totals_by_archetype_era = navigate(totals_in_era, phase, 'build', 'All', 'All', arch) + totals_by_spec_era = navigate(totals_in_era, phase, 'build', prof, elite, 'All') + buffs_by_build_era = navigate(totals_by_build_era, 'buffs') + buffs_out_by_build_era = navigate(totals_by_build_era, 'buffs_out') + + if(encounter.success): + + calculate([totals_by_build, totals_by_archetype, totals_by_spec, individual_totals, + totals_by_build_era, totals_by_archetype_era, totals_by_spec_era, individual_totals_era], count) + calculate_standard_stats( + advanced_stats(options['percentile_samples']), + player_stats, + [totals_by_build, totals_by_archetype, totals_by_spec, individual_totals, + totals_by_build_era, totals_by_archetype_era, totals_by_spec_era, individual_totals_era], + [buffs_by_build, buffs_by_build_era], + [buffs_out_by_build, buffs_out_by_build_era]) + + if phase == 'All': + profile_output = navigate_to_profile_outputs(totals, participation, encounter, boss) + stats_in_phase_events = _safe_get(lambda: player_stats['Metrics']['events'], None) + if stats_in_phase_events is not None: + + + calculate(profile_output.all, count) + calculate(profile_output.encounter_stats, average_stats, 'success_percentage', 100 if encounter.success else 0) + + if(encounter.success): + calculate_standard_stats( + all_stats, + player_stats, + profile_output.breakdown, + [], + map(lambda a: navigate(a, 'outgoing'), profile_output.breakdown)) + + dead_percentage = 100 * _safe_get(lambda: stats_in_phase_events['dead_time']) / duration + down_percentage = 100 * _safe_get(lambda: stats_in_phase_events['down_time']) / duration + disconnect_percentage = 100 * _safe_get(lambda: stats_in_phase_events['disconnect_time']) / duration + + calculate(profile_output.all, average_stats, 'dead_percentage', dead_percentage) + calculate(profile_output.all, average_stats, 'down_percentage', down_percentage) + calculate(profile_output.all, average_stats, 'disconnect_percentage', disconnect_percentage) + except: + raise RestatException("Error in %s" % encounter) + + #postprocessing + for area_id, totals_in_area in totals['area'].items(): + finalise_stats(totals_in_area) + EraAreaStore.objects.update_or_create( + era=era, area_id=area_id, defaults={ "val": totals_in_area }) + + for user_id, totals_for_player in totals['user'].items(): + finalise_stats(totals_for_player) + EraUserStore.objects.update_or_create( + era=era, user_id=user_id, defaults={ "val": totals_for_player }) + + finalise_stats(totals_in_era) + era.val=totals_in_era + #Era.objects.update_or_create(era) + era.save() + + if options['verbosity'] >= 2: + flattened = flatten(totals) + for key in sorted(flattened.keys()): + print_node(key, flattened[key]) + + if options['verbosity'] >= 3: + import pprint + pp = pprint.PrettyPrinter(indent=2) + print(era) + pp.pprint(totals) + + + +def is_basic_value(node): + try: + dict(node) + return False + except: + return True + +def flatten(root): + nodes = dict((str(key), node) for key,node in root.items()) + stack = list(nodes.keys()) + for node_name in stack: + node = nodes[node_name] + try: + for child_name, child in node.items(): + try: + full_child_name = "{0}-{1}".format(node_name, child_name) + nodes[full_child_name] = dict(child) + stack.append(full_child_name) + except TypeError: + pass + except ValueError: + pass + except AttributeError: + pass + return nodes + +def format_value(value): + return value + +def print_node(key, node, f=None): + try: + basic_values = list(filter(lambda key:is_basic_value(key[1]), node.items())) + if basic_values: + output_string = "{0}: {1}".format(key, ", ".join( + ["{0}:{1}".format(name, format_value(value)) for name,value in basic_values])) + print(output_string, file=f) + except AttributeError: + pass diff --git a/raidar/management/commands/reupload.py b/raidar/management/commands/reupload.py new file mode 100644 index 00000000..7fa4f774 --- /dev/null +++ b/raidar/management/commands/reupload.py @@ -0,0 +1,63 @@ +#from analyser.analyser import Analyser, Group, Archetype, EvtcAnalysisException +#from Crypto import Random +#from multiprocessing import Queue, Process, log_to_stderr +#from contextlib import contextmanager +from django.core.management.base import BaseCommand, CommandError +#from django.db import transaction +#from django.db.utils import IntegrityError +#from evtcparser.parser import Encounter as EvtcEncounter, EvtcParseException +#from gw2raidar import settings +#from json import loads as json_loads, dumps as json_dumps +from raidar.models import * +#from sys import exit, stderr +from time import time +#from zipfile import ZipFile, BadZipFile +#from queue import Empty +import os +import os.path +import logging +import re +#import signal +#from traceback import format_exc + + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +class Command(BaseCommand): + help = 'Reupload EVTC files' + + def add_arguments(self, parser): + parser.add_argument('files', + nargs='+', + help='EVTC files to reupload') + + def handle(self, *args, **options): + start = time() + self.reupload(*args, **options) + end = time() + + if options['verbosity'] >= 3: + print() + print("Completed in %ss" % (end - start)) + + def reupload(self, *args, **options): + files = set(re.sub(r'\.error', '', file) for file in options['files']) + for filename in files: + try: + with open(filename + '.error', 'r') as f: + match = re.match(r"(.*) \((.*?)\)", f.readline().rstrip()) + orig_name = match.group(1) + username = match.group(2) + except FileNotFoundError: + continue + + user = User.objects.get(username=username) + upload_time = os.path.getmtime(filename) + upload, _ = Upload.objects.update_or_create( + filename=orig_name, uploaded_by=user, + defaults={ "uploaded_at": upload_time }) + diskname = upload.diskname() + os.makedirs(os.path.dirname(diskname), exist_ok=True) + os.rename(filename, diskname) + os.remove(filename + '.error') diff --git a/raidar/migrations/0001_initial.py b/raidar/migrations/0001_initial.py index b62596bd..26e92b98 100644 --- a/raidar/migrations/0001_initial.py +++ b/raidar/migrations/0001_initial.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-02-20 06:20 +# Generated by Django 1.10.5 on 2017-05-25 02:26 from __future__ import unicode_literals +import analyser.analyser from django.conf import settings import django.core.validators from django.db import migrations, models @@ -24,7 +25,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=64, unique=True, validators=[django.core.validators.RegexValidator(re.compile('\\S+\\.\\d{4}', 32))])), ('api_key', models.CharField(blank=True, max_length=72, validators=[django.core.validators.RegexValidator(re.compile('[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{20}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$', 34))], verbose_name='API key')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accounts', to=settings.AUTH_USER_MODEL)), ], options={ 'ordering': ('name',), @@ -35,6 +36,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.IntegerField(primary_key=True, serialize=False)), ('name', models.CharField(max_length=64, unique=True)), + ('stats', models.TextField(default='{}', editable=False)), ], options={ 'ordering': ('name',), @@ -57,7 +59,15 @@ class Migration(migrations.Migration): name='Encounter', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('started_at', models.DateTimeField(db_index=True)), + ('started_at', models.IntegerField(db_index=True)), + ('duration', models.FloatField()), + ('success', models.BooleanField()), + ('filename', models.CharField(max_length=255)), + ('uploaded_at', models.IntegerField(db_index=True)), + ('dump', models.TextField(editable=False)), + ('account_hash', models.CharField(editable=False, max_length=32)), + ('started_at_full', models.IntegerField(editable=False)), + ('started_at_half', models.IntegerField(editable=False)), ('area', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='encounters', to='raidar.Area')), ], options={ @@ -68,6 +78,9 @@ class Migration(migrations.Migration): name='Participation', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('archetype', models.PositiveSmallIntegerField(choices=[(analyser.analyser.Archetype(1), 'Power'), (analyser.analyser.Archetype(2), 'Condi'), (analyser.analyser.Archetype(3), 'Tank'), (analyser.analyser.Archetype(4), 'Heal')], db_index=True)), + ('elite', models.PositiveSmallIntegerField(choices=[(analyser.analyser.Elite(0), 'Core'), (analyser.analyser.Elite(1), 'Heart of Thorns')], db_index=True)), + ('party', models.PositiveSmallIntegerField(db_index=True)), ('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='raidar.Character')), ('encounter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='raidar.Encounter')), ], @@ -77,10 +90,19 @@ class Migration(migrations.Migration): name='characters', field=models.ManyToManyField(related_name='encounters', through='raidar.Participation', to='raidar.Character'), ), + migrations.AddField( + model_name='encounter', + name='uploaded_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uploaded_encounters', to=settings.AUTH_USER_MODEL), + ), migrations.AlterUniqueTogether( name='participation', unique_together=set([('encounter', 'character')]), ), + migrations.AlterUniqueTogether( + name='encounter', + unique_together=set([('area', 'account_hash', 'started_at_half'), ('area', 'account_hash', 'started_at_full')]), + ), migrations.AlterIndexTogether( name='encounter', index_together=set([('area', 'started_at')]), diff --git a/raidar/migrations/0003_change_archetype_field.py b/raidar/migrations/0003_change_archetype_field.py new file mode 100644 index 00000000..078ae4d9 --- /dev/null +++ b/raidar/migrations/0003_change_archetype_field.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-12 01:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0002_populate_areas'), + ] + + operations = [ + migrations.AlterField( + model_name='participation', + name='archetype', + field=models.PositiveSmallIntegerField(choices=[(1, 'Power'), (2, 'Condi'), (3, 'Tank'), (4, 'Heal'), (5, 'Support')], db_index=True), + ), + ] diff --git a/raidar/migrations/0004_userprofile.py b/raidar/migrations/0004_userprofile.py new file mode 100644 index 00000000..14aeb753 --- /dev/null +++ b/raidar/migrations/0004_userprofile.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-12 01:48 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('raidar', '0003_change_archetype_field'), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('portrait_url', models.URLField(null=True)), + ('stats', models.TextField(default='{}', editable=False)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_profile', to=settings.AUTH_USER_MODEL)), + ('last_notified_at', models.IntegerField(db_index=True, default=0, editable=False)), + ], + ), + ] diff --git a/raidar/migrations/0005_era.py b/raidar/migrations/0005_era.py new file mode 100644 index 00000000..ef546ca8 --- /dev/null +++ b/raidar/migrations/0005_era.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-12 02:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0004_userprofile'), + ] + + operations = [ + migrations.CreateModel( + name='Era', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('started_at', models.IntegerField(db_index=True)), + ('name', models.CharField(max_length=255, null=True)), + ('description', models.TextField(null=True)), + ], + ), + ] diff --git a/raidar/migrations/0006_add_first_era.py b/raidar/migrations/0006_add_first_era.py new file mode 100644 index 00000000..8965ba7f --- /dev/null +++ b/raidar/migrations/0006_add_first_era.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-12 02:28 +from __future__ import unicode_literals + +from django.db import migrations +from time import time + + + +def _forwards_func(apps, schema_editor): + Era = apps.get_model('raidar', 'Era') + Era.objects.update_or_create(id=1, defaults = { + "name": "Prehistoric", + "started_at": 0 + }) + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0005_era'), + ] + + operations = [ + migrations.RunPython(_forwards_func, reverse_code=migrations.RunPython.noop), + ] diff --git a/raidar/migrations/0007_encounter_era.py b/raidar/migrations/0007_encounter_era.py new file mode 100644 index 00000000..7f125deb --- /dev/null +++ b/raidar/migrations/0007_encounter_era.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-12 02:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0006_add_first_era'), + ] + + operations = [ + migrations.AddField( + model_name='encounter', + name='era', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='encounters', to='raidar.Era'), + preserve_default=False, + ), + ] diff --git a/raidar/migrations/0008_add_encounter_gdrive_fields.py b/raidar/migrations/0008_add_encounter_gdrive_fields.py new file mode 100644 index 00000000..5000feec --- /dev/null +++ b/raidar/migrations/0008_add_encounter_gdrive_fields.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-12 03:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0007_encounter_era'), + ] + + operations = [ + migrations.AddField( + model_name='encounter', + name='gdrive_id', + field=models.CharField(editable=False, max_length=255, null=True), + ), + migrations.AddField( + model_name='encounter', + name='gdrive_url', + field=models.CharField(editable=False, max_length=255, null=True), + ), + ] diff --git a/raidar/migrations/0009_add_user_profiles.py b/raidar/migrations/0009_add_user_profiles.py new file mode 100644 index 00000000..41b93373 --- /dev/null +++ b/raidar/migrations/0009_add_user_profiles.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-12 07:56 +from __future__ import unicode_literals + +from django.db import migrations + +def gen_user_profile(apps, schema_editor): + User = apps.get_model('auth', 'User') + UserProfile = apps.get_model('raidar', 'UserProfile') + for user in User.objects.all(): + UserProfile.objects.update_or_create(user=user) + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0008_add_encounter_gdrive_fields'), + ] + + operations = [ + migrations.RunPython(gen_user_profile, reverse_code=migrations.RunPython.noop), + ] diff --git a/raidar/migrations/0010_variable.py b/raidar/migrations/0010_variable.py new file mode 100644 index 00000000..ce297844 --- /dev/null +++ b/raidar/migrations/0010_variable.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-19 06:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0009_add_user_profiles'), + ] + + operations = [ + migrations.CreateModel( + name='Variable', + fields=[ + ('key', models.CharField(max_length=255, primary_key=True, serialize=False)), + ('value', models.TextField(null=True)), + ], + ), + ] diff --git a/raidar/migrations/0011_upload.py b/raidar/migrations/0011_upload.py new file mode 100644 index 00000000..6a20db8b --- /dev/null +++ b/raidar/migrations/0011_upload.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-19 06:06 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('raidar', '0010_variable'), + ] + + operations = [ + migrations.CreateModel( + name='Upload', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('filename', models.CharField(max_length=255)), + ('uploaded_at', models.IntegerField(db_index=True)), + ('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unprocessed_uploads', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/raidar/migrations/0012_notification.py b/raidar/migrations/0012_notification.py new file mode 100644 index 00000000..9ac0de74 --- /dev/null +++ b/raidar/migrations/0012_notification.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-19 06:34 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('raidar', '0011_upload'), + ] + + operations = [ + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.TextField(default='{}')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/raidar/migrations/0013_upload_uniqueness.py b/raidar/migrations/0013_upload_uniqueness.py new file mode 100644 index 00000000..a367f96b --- /dev/null +++ b/raidar/migrations/0013_upload_uniqueness.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-27 01:39 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('raidar', '0012_notification'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='upload', + unique_together=set([('filename', 'uploaded_by')]), + ), + ] diff --git a/raidar/migrations/0014_encounter_url_id.py b/raidar/migrations/0014_encounter_url_id.py new file mode 100644 index 00000000..64864755 --- /dev/null +++ b/raidar/migrations/0014_encounter_url_id.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-27 07:54 +from __future__ import unicode_literals + +from django.db import migrations, models + +import raidar + +def gen_url_id(apps, schema_editor): + Encounter = apps.get_model('raidar', 'Encounter') + default_func = raidar.models._generate_url_id + for encounter in Encounter.objects.all().iterator(): + encounter.url_id = default_func() + encounter.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0013_upload_uniqueness'), + ] + + operations = [ + migrations.AddField( + model_name='encounter', + name='url_id', + field=models.TextField(editable=False, max_length=255, null=True), + ), + migrations.RunPython(gen_url_id, migrations.RunPython.noop), + migrations.AlterField( + model_name='encounter', + name='url_id', + field=models.TextField(default=raidar.models._generate_url_id, editable=False, max_length=255, unique=True), + ), + ] diff --git a/raidar/migrations/0015_notification_created_at.py b/raidar/migrations/0015_notification_created_at.py new file mode 100644 index 00000000..442a6c8c --- /dev/null +++ b/raidar/migrations/0015_notification_created_at.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-28 07:11 +from __future__ import unicode_literals + +from django.db import migrations, models +import time + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0014_encounter_url_id'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='created_at', + field=models.IntegerField(db_index=True, default=time.time), + ), + ] diff --git a/raidar/migrations/0016_era_area_store.py b/raidar/migrations/0016_era_area_store.py new file mode 100644 index 00000000..4f597fb2 --- /dev/null +++ b/raidar/migrations/0016_era_area_store.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-08-02 07:10 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import raidar.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0015_notification_created_at'), + ] + + operations = [ + migrations.CreateModel( + name='EraAreaStore', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.TextField(default='{}')), + ], + ), + migrations.RemoveField( + model_name='area', + name='stats', + ), + migrations.AlterField( + model_name='encounter', + name='url_id', + field=models.TextField(default=raidar.models._generate_url_id, editable=False, max_length=255, unique=True, verbose_name='URL ID'), + ), + migrations.AddField( + model_name='eraareastore', + name='area', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='era_area_stores', to='raidar.Area'), + ), + migrations.AddField( + model_name='eraareastore', + name='era', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='era_area_stores', to='raidar.Era'), + ), + ] diff --git a/raidar/migrations/0017_era_user_store.py b/raidar/migrations/0017_era_user_store.py new file mode 100644 index 00000000..4c452719 --- /dev/null +++ b/raidar/migrations/0017_era_user_store.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-08-02 08:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('raidar', '0016_era_area_store'), + ] + + operations = [ + migrations.CreateModel( + name='EraUserStore', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.TextField(default='{}')), + ('era', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='era_user_stores', to='raidar.Era')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='era_user_stores', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.RemoveField( + model_name='userprofile', + name='stats', + ), + ] diff --git a/raidar/migrations/0018_path_of_fire.py b/raidar/migrations/0018_path_of_fire.py new file mode 100644 index 00000000..f2ed9f68 --- /dev/null +++ b/raidar/migrations/0018_path_of_fire.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-08-02 08:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0017_era_user_store'), + ] + + operations = [ + migrations.AlterField( + model_name='participation', + name='elite', + field=models.PositiveSmallIntegerField(choices=[(0, 'Core'), (1, 'Heart of Thorns'), (2, 'Path of Fire')], db_index=True), + ), + ] diff --git a/raidar/migrations/0019_user_email_set_unique.py b/raidar/migrations/0019_user_email_set_unique.py new file mode 100644 index 00000000..adfcba0c --- /dev/null +++ b/raidar/migrations/0019_user_email_set_unique.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-09-04 02:15 +from __future__ import unicode_literals + +from django.db import migrations, models +from types import MethodType + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ('raidar', '0018_path_of_fire'), + ] + + op = migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address'), + ) + + + # XXX HACK warning + def state_forwards(self, app_label, state): + self.state_forwards_backup('auth', state) + op.state_forwards_backup = op.state_forwards + op.state_forwards = MethodType(state_forwards, op) + def database_forwards(self, app_label, schema_editor, from_state, to_state): + self.database_forwards_backup('auth', schema_editor, from_state, to_state) + op.database_forwards_backup = op.database_forwards + op.database_forwards = MethodType(database_forwards, op) + + + operations = [ + op + ] diff --git a/raidar/migrations/0020_userprofile_private.py b/raidar/migrations/0020_userprofile_private.py new file mode 100644 index 00000000..34eee5c6 --- /dev/null +++ b/raidar/migrations/0020_userprofile_private.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-09-13 04:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0019_user_email_set_unique'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='private', + field=models.BooleanField(default=False, editable=False), + ), + ] diff --git a/raidar/migrations/0021_make_privacy_trivalent.py b/raidar/migrations/0021_make_privacy_trivalent.py new file mode 100644 index 00000000..3f2b7b50 --- /dev/null +++ b/raidar/migrations/0021_make_privacy_trivalent.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-09-13 05:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0020_userprofile_private'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='private', + ), + migrations.AddField( + model_name='userprofile', + name='privacy', + field=models.PositiveSmallIntegerField(choices=[(1, 'Private'), (2, 'Squad'), (3, 'Public')], default=3, editable=False), + ), + ] diff --git a/raidar/migrations/0022_encounter_tags.py b/raidar/migrations/0022_encounter_tags.py new file mode 100644 index 00000000..88dc90bc --- /dev/null +++ b/raidar/migrations/0022_encounter_tags.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-09-19 02:29 +from __future__ import unicode_literals + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0002_auto_20150616_2121'), + ('raidar', '0021_make_privacy_trivalent'), + ] + + operations = [ + migrations.AddField( + model_name='encounter', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + ] diff --git a/raidar/migrations/0023_category.py b/raidar/migrations/0023_category.py new file mode 100644 index 00000000..1d93c476 --- /dev/null +++ b/raidar/migrations/0023_category.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-09-22 06:53 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0022_encounter_tags'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + migrations.AddField( + model_name='encounter', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='encounters', to='raidar.Category'), + ), + ] diff --git a/raidar/migrations/0024_blank_tags.py b/raidar/migrations/0024_blank_tags.py new file mode 100644 index 00000000..22fdc03a --- /dev/null +++ b/raidar/migrations/0024_blank_tags.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-09-23 09:54 +from __future__ import unicode_literals + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0023_category'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'verbose_name_plural': 'categories'}, + ), + migrations.AlterField( + model_name='encounter', + name='tags', + field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + ] diff --git a/raidar/migrations/0025_era_value.py b/raidar/migrations/0025_era_value.py new file mode 100644 index 00000000..957ab262 --- /dev/null +++ b/raidar/migrations/0025_era_value.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-10-06 00:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0024_blank_tags'), + ] + + operations = [ + migrations.AddField( + model_name='era', + name='value', + field=models.TextField(default='{}'), + ), + ] diff --git a/raidar/migrations/0026_rename_encounter_dump.py b/raidar/migrations/0026_rename_encounter_dump.py new file mode 100644 index 00000000..e86fb27b --- /dev/null +++ b/raidar/migrations/0026_rename_encounter_dump.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-10-06 01:58 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0025_era_value'), + ] + + operations = [ + migrations.RenameField('Encounter', 'dump', 'value'), + ] diff --git a/raidar/migrations/0027_era_default_sort.py b/raidar/migrations/0027_era_default_sort.py new file mode 100644 index 00000000..935e53e3 --- /dev/null +++ b/raidar/migrations/0027_era_default_sort.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-10-23 02:22 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('raidar', '0026_rename_encounter_dump'), + ] + + operations = [ + migrations.AlterModelOptions( + name='era', + options={'ordering': ('-started_at',)}, + ), + ] diff --git a/raidar/models.py b/raidar/models.py index b8b01d79..d51c608d 100644 --- a/raidar/models.py +++ b/raidar/models.py @@ -1,9 +1,71 @@ from django.db import models +from django.db.models.signals import post_save, post_delete +from django.db.models import Q from django.contrib.auth.models import User from django.core.validators import RegexValidator +from hashlib import md5 +from analyser.analyser import Archetype, Elite +from json import loads as json_loads, dumps as json_dumps +from gw2raidar import settings +from os.path import join as path_join +from functools import lru_cache +from time import time +from taggit.managers import TaggableManager +import random +import os import re +# unique to 30-60s precision +START_RESOLUTION = 60 + + + +# XXX TODO Move to a separate module, does not really belong here +gdrive_service = None +if hasattr(settings, 'GOOGLE_CREDENTIAL_FILE'): + try: + from oauth2client.service_account import ServiceAccountCredentials + from httplib2 import Http + from apiclient import discovery + from googleapiclient.http import MediaFileUpload + + scopes = ['https://www.googleapis.com/auth/drive.file'] + credentials = ServiceAccountCredentials.from_json_keyfile_name( + settings.GOOGLE_CREDENTIAL_FILE, scopes=scopes) + http_auth = credentials.authorize(Http()) + gdrive_service = discovery.build('drive', 'v3', http=http_auth) + except ImportError: + # No Google Drive support + pass + + + +User._meta.get_field('email')._unique = True + +class UserProfile(models.Model): + PRIVATE = 1 + SQUAD = 2 + PUBLIC = 3 + + PRIVACY_CHOICES = ( + (PRIVATE, 'Private'), + (SQUAD, 'Squad'), + (PUBLIC, 'Public') + ) + portrait_url = models.URLField(null=True) # XXX not using... delete? + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="user_profile") + last_notified_at = models.IntegerField(db_index=True, default=0, editable=False) + privacy = models.PositiveSmallIntegerField(editable=False, choices=PRIVACY_CHOICES, default=PUBLIC) + + def __str__(self): + return self.user.username + +def _create_user_profile(sender, instance, created, **kwargs): + if created: + UserProfile.objects.create(user=instance) + +post_save.connect(_create_user_profile, sender=User) class Area(models.Model): @@ -23,7 +85,7 @@ class Account(models.Model): r'-'.join(r'[0-9A-F]{%d}' % n for n in (8, 4, 4, 4, 20, 4, 4, 4, 12)) + r'$', re.IGNORECASE) - user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE, related_name='accounts') + user = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL, related_name='accounts') name = models.CharField(max_length=64, unique=True, validators=[RegexValidator(ACCOUNT_NAME_RE)]) api_key = models.CharField('API key', max_length=72, blank=True, validators=[RegexValidator(API_KEY_RE)]) @@ -57,10 +119,33 @@ class Character(models.Model): (REVENANT, 'Revenant'), ) + SPECIALISATIONS = { (id, 0): name for id, name in PROFESSION_CHOICES } + SPECIALISATIONS.update({ + (GUARDIAN, 1): 'Dragonhunter', + (WARRIOR, 1): 'Berserker', + (ENGINEER, 1): 'Scrapper', + (RANGER, 1): 'Druid', + (THIEF, 1): 'Daredevil', + (ELEMENTALIST, 1): 'Tempest', + (MESMER, 1): 'Chronomancer', + (NECROMANCER, 1): 'Reaper', + (REVENANT, 1): 'Herald', + + (GUARDIAN, 2): 'Firebrand', + (WARRIOR, 2): 'Spellbreaker', + (ENGINEER, 2): 'Holosmith', + (RANGER, 2): 'Soulbeast', + (THIEF, 2): 'Deadeye', + (ELEMENTALIST, 2): 'Weaver', + (MESMER, 2): 'Mirage', + (NECROMANCER, 2): 'Scourge', + (REVENANT, 2): 'Renegade', + }) + account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='characters') name = models.CharField(max_length=64, db_index=True) profession = models.PositiveSmallIntegerField(choices=PROFESSION_CHOICES, db_index=True) - verified_at = models.DateTimeField(auto_now_add=True) + verified_at = models.DateTimeField(auto_now_add=True) # XXX don't remember this... delete? def __str__(self): return self.name @@ -71,25 +156,268 @@ class Meta: ordering = ('name',) +class Era(models.Model): + started_at = models.IntegerField(db_index=True) + name = models.CharField(max_length=255, null=True) + description = models.TextField(null=True) + value = models.TextField(default="{}") + + @property + def val(self): + return json_loads(self.value) + + @val.setter + def val(self, value): + self.value = json_dumps(value) + + def __str__(self): + return "%s (#%d)" % (self.name or "", self.id) + + @staticmethod + def by_time(started_at): + return Era.objects.filter(started_at__lte=started_at).latest('started_at') + + class Meta: + ordering = ('-started_at',) + + +class Category(models.Model): + name = models.CharField(max_length=255) + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = "categories" + + +class Upload(models.Model): + filename = models.CharField(max_length=255) + uploaded_at = models.IntegerField(db_index=True) + uploaded_by = models.ForeignKey(User, related_name='unprocessed_uploads') + + def __str__(self): + return '%s (%s)' % (self.filename, self.uploaded_by.username) + + def diskname(self): + if hasattr(settings, 'UPLOAD_DIR'): + upload_dir = settings.UPLOAD_DIR + else: + upload_dir = 'uploads' + ext = '.' + '.'.join(self.filename.split('.')[1:]) + return path_join(upload_dir, str(self.id) + ext) + + class Meta: + unique_together = ('filename', 'uploaded_by') + +def _delete_upload_file(sender, instance, using, **kwargs): + try: + os.remove(instance.diskname()) + except FileNotFoundError: + pass + +post_delete.connect(_delete_upload_file, sender=Upload) + + +class Notification(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications') + value = models.TextField(default="{}") + created_at = models.IntegerField(db_index=True, default=time) + + @property + def val(self): + return json_loads(self.value) + + @val.setter + def val(self, value): + self.value = json_dumps(value) + + +class Variable(models.Model): + key = models.CharField(max_length=255, primary_key=True) + value = models.TextField(null=True) + + def __str__(self): + return '%s=%s' % (self.key, self.val) + + @property + def val(self): + return json_loads(self.value) + + @val.setter + def val(self, value): + self.value = json_dumps(value) + + def get(name): + return Variable.objects.get(key=name).val + + def set(name, value): + Variable.objects.update_or_create(key=name, defaults={'val': value}) + + +@lru_cache(maxsize=1) +def _dictionary(): + location = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + with open(os.path.join(location, "words.txt")) as f: + return [l.strip() for l in f.readlines()] + +def _generate_url_id(size=5): + return ''.join(w.capitalize() for w in random.sample(_dictionary(), size)) + class Encounter(models.Model): - started_at = models.DateTimeField(db_index=True) + url_id = models.TextField(max_length=255, editable=False, unique=True, default=_generate_url_id, verbose_name="URL ID") + started_at = models.IntegerField(db_index=True) + duration = models.FloatField() + success = models.BooleanField() + filename = models.CharField(max_length=255) + uploaded_at = models.IntegerField(db_index=True) + uploaded_by = models.ForeignKey(User, related_name='uploaded_encounters') area = models.ForeignKey(Area, on_delete=models.PROTECT, related_name='encounters') + era = models.ForeignKey(Era, on_delete=models.PROTECT, related_name='encounters') + category = models.ForeignKey(Category, on_delete=models.SET_NULL, related_name='encounters', null=True) characters = models.ManyToManyField(Character, through='Participation', related_name='encounters') + value = models.TextField(editable=False) + # hack to try to ensure uniqueness + account_hash = models.CharField(max_length=32, editable=False) + started_at_full = models.IntegerField(editable=False) + started_at_half = models.IntegerField(editable=False) + # Google Drive + gdrive_id = models.CharField(max_length=255, editable=False, null=True) + gdrive_url = models.CharField(max_length=255, editable=False, null=True) + tags = TaggableManager(blank=True) + + @property + def val(self): + return json_loads(self.value) + + @val.setter + def val(self, value): + self.value = json_dumps(value) def __str__(self): - return '%s (%s)' % (self.area.name, self.started_at) + return '%s (%s, %s, #%s)' % (self.area.name, self.filename, self.uploaded_by.username, self.id) + + def save(self, *args, **kwargs): + self.started_at_full, self.started_at_half = Encounter.calculate_start_guards(self.started_at) + super(Encounter, self).save(*args, **kwargs) + + def diskname(self): + if hasattr(settings, 'UPLOAD_DIR'): + upload_dir = settings.UPLOAD_DIR + else: + upload_dir = 'uploads' + return path_join(upload_dir, 'encounters', self.uploaded_by.username, self.filename) + + @property + def tagstring(self): + return ','.join(self.tags.names()) + + @tagstring.setter + def tagstring(self, value): + self.tags.set(*value.split(',')) + + @staticmethod + def calculate_account_hash(account_names): + conc = ':'.join(sorted(account_names)) + hash_object = md5(conc.encode()) + return hash_object.hexdigest() + + @staticmethod + def calculate_start_guards(started_at): + started_at_full = round(started_at / START_RESOLUTION) * START_RESOLUTION + started_at_half = round((started_at + START_RESOLUTION / 2) / START_RESOLUTION) * START_RESOLUTION + return (started_at_full, started_at_half) + class Meta: index_together = ('area', 'started_at') ordering = ('started_at',) + unique_together = ( + ('area', 'account_hash', 'started_at_full'), + ('area', 'account_hash', 'started_at_half'), + ) + +def _delete_encounter_file(sender, instance, using, **kwargs): + if gdrive_service and instance.gdrive_id: + gdrive_service.files().delete( + fileId=instance.gdrive_id).execute() + try: + os.remove(instance.diskname()) + except FileNotFoundError: + pass + +post_delete.connect(_delete_encounter_file, sender=Encounter) class Participation(models.Model): + ARCHETYPE_CHOICES = ( + (int(Archetype.POWER), "Power"), + (int(Archetype.CONDI), "Condi"), + (int(Archetype.TANK), "Tank"), + (int(Archetype.HEAL), "Heal"), + (int(Archetype.SUPPORT), "Support"), + ) + + ELITE_CHOICES = ( + (int(Elite.CORE), "Core"), + (int(Elite.HEART_OF_THORNS), "Heart of Thorns"), + (int(Elite.PATH_OF_FIRE), "Path of Fire"), + ) + encounter = models.ForeignKey(Encounter, on_delete=models.CASCADE, related_name='participations') character = models.ForeignKey(Character, on_delete=models.CASCADE, related_name='participations') + archetype = models.PositiveSmallIntegerField(choices=ARCHETYPE_CHOICES, db_index=True) + elite = models.PositiveSmallIntegerField(choices=ELITE_CHOICES, db_index=True) + party = models.PositiveSmallIntegerField(db_index=True) def __str__(self): return '%s in %s' % (self.character, self.encounter) + def data(self): + return { + 'id': self.encounter.id, + 'url_id': self.encounter.url_id, + 'area': self.encounter.area.name, + 'started_at': self.encounter.started_at, + 'duration': self.encounter.duration, + 'character': self.character.name, + 'account': self.character.account.name, + 'profession': self.character.profession, + 'archetype': self.archetype, + 'elite': self.elite, + 'uploaded_at': self.encounter.uploaded_at, + 'success': self.encounter.success, + 'category': self.encounter.category_id, + 'tags': list(self.encounter.tags.names()), + } + class Meta: unique_together = ('encounter', 'character') + + +class EraAreaStore(models.Model): + era = models.ForeignKey(Era, on_delete=models.CASCADE, related_name="era_area_stores") + area = models.ForeignKey(Area, on_delete=models.CASCADE, related_name="era_area_stores") + value = models.TextField(default="{}") + + @property + def val(self): + return json_loads(self.value) + + @val.setter + def val(self, value): + self.value = json_dumps(value) + + +class EraUserStore(models.Model): + era = models.ForeignKey(Era, on_delete=models.CASCADE, related_name="era_user_stores") + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="era_user_stores") + value = models.TextField(default="{}") + + @property + def val(self): + return json_loads(self.value) + + @val.setter + def val(self, value): + self.value = json_dumps(value) diff --git a/raidar/static/raidar/css/uikit.min.css b/raidar/static/raidar/css/uikit.min.css new file mode 100644 index 00000000..bd0c794e --- /dev/null +++ b/raidar/static/raidar/css/uikit.min.css @@ -0,0 +1 @@ +html{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:16px;font-weight:400;line-height:1.5;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;background:#fff;color:#666}body{margin:0}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline:0}.uk-link,a{color:#1e87f0;text-decoration:none;cursor:pointer}.uk-link:hover,a:hover{color:#0f6ecd;text-decoration:underline}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}:not(pre)>code,:not(pre)>kbd,:not(pre)>samp{font-size:.875rem;font-family:Consolas,monaco,monospace;color:#f0506e;white-space:nowrap;padding:2px 6px;background:#f8f8f8}em{color:#f0506e}ins{background:#ffd;color:#666;text-decoration:none}mark{background:#ffd;color:#666}q{font-style:italic}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}audio,canvas,iframe,img,svg,video{vertical-align:middle}audio,canvas,img,video{max-width:100%;height:auto;box-sizing:border-box}img{border-style:none}svg:not(:root){overflow:hidden}address,dl,fieldset,figure,ol,p,pre,ul{margin:0 0 20px 0}*+address,*+dl,*+fieldset,*+figure,*+ol,*+p,*+pre,*+ul{margin-top:20px}.uk-h1,.uk-h2,.uk-h3,.uk-h4,.uk-h5,.uk-h6,h1,h2,h3,h4,h5,h6{margin:0 0 20px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-weight:400;color:#333;text-transform:none}*+.uk-h1,*+.uk-h2,*+.uk-h3,*+.uk-h4,*+.uk-h5,*+.uk-h6,*+h1,*+h2,*+h3,*+h4,*+h5,*+h6{margin-top:40px}.uk-h1,h1{font-size:2.625rem;line-height:1.2}.uk-h2,h2{font-size:2rem;line-height:1.3}.uk-h3,h3{font-size:1.5rem;line-height:1.4}.uk-h4,h4{font-size:1.25rem;line-height:1.4}.uk-h5,h5{font-size:16px;line-height:1.4}.uk-h6,h6{font-size:.875rem;line-height:1.4}ol,ul{padding-left:30px}ol>li>ol,ol>li>ul,ul>li>ol,ul>li>ul{margin:0}dt{font-weight:700}dd{margin-left:0}.uk-hr,hr{box-sizing:content-box;height:0;overflow:visible;margin:0 0 20px 0;border:0;border-top:1px solid #e5e5e5}*+.uk-hr,*+hr{margin-top:20px}address{font-style:normal}blockquote{margin:0 0 20px 0;font-size:1.25rem;line-height:1.5;font-style:italic;color:#333}*+blockquote{margin-top:20px}blockquote p:last-of-type{margin-bottom:0}blockquote footer{margin-top:10px;font-size:.875rem;line-height:1.5;color:#666}blockquote footer::before{content:"— "}pre{font:.875rem/1.5 Consolas,monaco,monospace;color:#666;-moz-tab-size:4;tab-size:4;overflow:auto;padding:10px;border:1px solid #e5e5e5;border-radius:3px;background:#fff}pre code{font-family:Consolas,monaco,monospace}::-moz-selection{background:#39f;color:#fff;text-shadow:none}::selection{background:#39f;color:#fff;text-shadow:none}article,aside,details,figcaption,figure,footer,header,main,nav,section,summary{display:block}progress{vertical-align:baseline}[hidden],template{display:none}iframe{border:0}a,area,button,input,label,select,summary,textarea{touch-action:manipulation}.var-media-s:before{content:'640px'}.var-media-m:before{content:'960px'}.var-media-l:before{content:'1200px'}.var-media-xl:before{content:'1600px'}.uk-link-muted a,a.uk-link-muted{color:#999}.uk-link-muted a:hover,a.uk-link-muted:hover{color:#666}.uk-link-reset a,.uk-link-reset a:focus,.uk-link-reset a:hover,a.uk-link-reset,a.uk-link-reset:focus,a.uk-link-reset:hover{color:inherit!important;text-decoration:none!important}.uk-heading-primary{font-size:2.625rem;line-height:1.2}@media (min-width:960px){.uk-heading-primary{font-size:3.75rem;line-height:1.1}}.uk-heading-hero{font-size:4rem;line-height:1.1}@media (min-width:640px){.uk-heading-hero{font-size:6rem;line-height:1}}@media (min-width:960px){.uk-heading-hero{font-size:8rem;line-height:1}}.uk-heading-divider{padding-bottom:10px;border-bottom:1px solid #e5e5e5}.uk-heading-bullet{position:relative}.uk-heading-bullet::before{content:"";display:inline-block;position:relative;top:calc(-.1 * 1em);vertical-align:middle;height:.9em;margin-right:10px;border-left:5px solid #e5e5e5}.uk-heading-line{overflow:hidden}.uk-heading-line>*{display:inline-block;position:relative}.uk-heading-line>:after,.uk-heading-line>:before{content:"";position:absolute;top:calc(50% - (1px / 2));width:2000px;border-bottom:1px solid #e5e5e5}.uk-heading-line>:before{right:100%;margin-right:.6em}.uk-heading-line>:after{left:100%;margin-left:.6em}[class*=uk-divider]{border:none;margin-bottom:20px}*+[class*=uk-divider]{margin-top:20px}.uk-divider-icon{position:relative;height:20px;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22none%22%20stroke%3D%22%23e5e5e5%22%20stroke-width%3D%222%22%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%227%22%3E%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A");background-repeat:no-repeat;background-position:50% 50%}.uk-divider-icon::after,.uk-divider-icon::before{content:"";position:absolute;top:50%;max-width:calc(50% - (50px / 2));border-bottom:1px solid #e5e5e5}.uk-divider-icon::before{right:calc(50% + (50px / 2));width:100%}.uk-divider-icon::after{left:calc(50% + (50px / 2));width:100%}.uk-divider-small{line-height:0}.uk-divider-small::after{content:"";display:inline-block;width:100px;max-width:100%;border-top:1px solid #e5e5e5;vertical-align:top}.uk-list{padding:0;list-style:none}.uk-list>li::after,.uk-list>li::before{content:"";display:table}.uk-list>li::after{clear:both}.uk-list>li>:last-child{margin-bottom:0}.uk-list ul{margin:0;padding-left:30px;list-style:none}.uk-list>li:nth-child(n+2),.uk-list>li>ul{margin-top:10px}.uk-list-divider>li:nth-child(n+2){margin-top:10px;padding-top:10px;border-top:1px solid #e5e5e5}.uk-list-striped>li{padding:10px 10px;border-bottom:1px solid #e5e5e5}.uk-list-striped>li:nth-of-type(odd){background:#f8f8f8}.uk-list-striped>li:nth-child(n+2){margin-top:0}.uk-list-bullet>li{position:relative;padding-left:calc(1.5em + 10px)}.uk-list-bullet>li::before{content:"";position:absolute;top:0;left:0;width:1.5em;height:1.5em;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22%23666%22%20cx%3D%223%22%20cy%3D%223%22%20r%3D%223%22%3E%3C%2Fcircle%3E%0A%3C%2Fsvg%3E");background-repeat:no-repeat;background-position:50% 50%;float:left}.uk-list-large>li:nth-child(n+2),.uk-list-large>li>ul{margin-top:20px}.uk-list-large.uk-list-divider>li:nth-child(n+2){margin-top:20px;padding-top:20px}.uk-list-large.uk-list-striped>li{padding:20px 10px;border-bottom:1px solid #e5e5e5}.uk-list-large.uk-list-striped>li:nth-child(n+2){margin-top:0}.uk-list-striped>li:first-child{border-top:1px solid #e5e5e5}.uk-description-list>dt{color:#333;font-size:.875rem;font-weight:400;text-transform:uppercase}.uk-description-list>dt:nth-child(n+2){margin-top:20px}.uk-description-list-divider>dt:nth-child(n+2){margin-top:20px;padding-top:20px;border-top:1px solid #e5e5e5}.uk-table{border-collapse:collapse;border-spacing:0;width:100%;margin-bottom:20px}*+.uk-table{margin-top:20px}.uk-table th{padding:16px 12px;text-align:left;vertical-align:bottom;font-size:.875rem;font-weight:400;color:#999;border-bottom:1px solid #e5e5e5;text-transform:uppercase}.uk-table td{padding:16px 12px;vertical-align:top}.uk-table tfoot{font-size:.875rem;border-top:1px solid #e5e5e5}.uk-table caption{font-size:.875rem;text-align:left;color:#999}.uk-table tbody tr.uk-active,.uk-table>tr.uk-active{background:#ffd}.uk-table-middle,.uk-table-middle td{vertical-align:middle!important}.uk-table-striped tbody tr:nth-of-type(odd),.uk-table-striped>tr:nth-of-type(odd){background:#f8f8f8}.uk-table-hover tbody tr:hover,.uk-table-hover>tr:hover{background:#ffd}.uk-table-small td,.uk-table-small th{padding:10px 12px}.uk-table-shrink{width:1px}.uk-table-expand{min-width:300px}.uk-table-link{padding:0!important}.uk-table-link>a{display:block;padding:16px 12px}.uk-table-small .uk-table-link>a{padding:10px 12px}.uk-table :not(:last-child)>td{border-bottom:1px solid #e5e5e5}.uk-table tbody tr{-webkit-transition:background-color .1s linear;transition:background-color .1s linear}.uk-icon{display:inline-block;fill:currentcolor;line-height:0}.uk-icon [fill*='#']:not(.uk-preserve){fill:currentcolor}.uk-icon [stroke*='#']:not(.uk-preserve){stroke:currentcolor}.uk-icon>*{transform:translate(0,0)}.uk-icon-image{width:20px;height:20px;background-position:50% 50%;background-repeat:no-repeat;background-size:contain;vertical-align:middle}.uk-icon-link{color:#999}.uk-icon-link:focus,.uk-icon-link:hover{color:#666;outline:0}.uk-active>.uk-icon-link,.uk-icon-link:active{color:#595959}.uk-icon-button{box-sizing:border-box;width:36px;height:36px;border-radius:500px;background:#f8f8f8;color:#999;vertical-align:middle;display:-ms-inline-flexbox;display:-webkit-inline-flex;display:inline-flex;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;-webkit-transition:.1s ease-in-out;transition:.1s ease-in-out;-webkit-transition-property:color,background-color;transition-property:color,background-color}.uk-icon-button:focus,.uk-icon-button:hover{background-color:#ebebeb;color:#666;outline:0}.uk-active>.uk-icon-button,.uk-icon-button:active{background-color:#dfdfdf;color:#666}.uk-checkbox,.uk-input,.uk-radio,.uk-select,.uk-textarea{box-sizing:border-box;margin:0;border-radius:0;font:inherit}.uk-input{overflow:visible}.uk-select{text-transform:none}.uk-select optgroup{font:inherit;font-weight:700}.uk-textarea{overflow:auto}.uk-input[type=search]::-webkit-search-cancel-button,.uk-input[type=search]::-webkit-search-decoration{-webkit-appearance:none}.uk-input[type=number]::-webkit-inner-spin-button,.uk-input[type=number]::-webkit-outer-spin-button{height:auto}.uk-input::-moz-placeholder,.uk-textarea::-moz-placeholder{opacity:1}.uk-checkbox,.uk-radio{padding:0}.uk-checkbox:not(:disabled),.uk-radio:not(:disabled){cursor:pointer}.uk-fieldset{border:none;margin:0;padding:0}.uk-input,.uk-textarea{-webkit-appearance:none}.uk-input,.uk-select,.uk-textarea{max-width:100%;width:100%;border:0 none;padding:0 6px;background:#fff;color:#666;border:1px solid #e5e5e5;-webkit-transition:.2s ease-in-out;transition:.2s ease-in-out;-webkit-transition-property:color,background-color,border;transition-property:color,background-color,border}.uk-input,.uk-select:not([multiple]):not([size]){height:40px;vertical-align:middle;display:inline-block;line-height:38px}.uk-select[multiple],.uk-select[size],.uk-textarea{padding-top:4px;padding-bottom:4px;vertical-align:top}.uk-input:focus,.uk-select:focus,.uk-textarea:focus{outline:0;background-color:#fff;color:#666;border-color:#1e87f0}.uk-input:disabled,.uk-select:disabled,.uk-textarea:disabled{background-color:#f8f8f8;color:#999;border-color:#e5e5e5}.uk-input:-ms-input-placeholder{color:#999!important}.uk-input::-moz-placeholder{color:#999}.uk-input::-webkit-input-placeholder{color:#999}.uk-textarea:-ms-input-placeholder{color:#999!important}.uk-textarea::-moz-placeholder{color:#999}.uk-textarea::-webkit-input-placeholder{color:#999}.uk-form-small{font-size:.875rem}.uk-form-small:not(textarea):not([multiple]):not([size]){height:30px;line-height:28px}.uk-form-large{font-size:1.25rem}.uk-form-large:not(textarea):not([multiple]):not([size]){height:55px;line-height:53px}.uk-form-danger,.uk-form-danger:focus{color:#f0506e;border-color:#f0506e}.uk-form-success,.uk-form-success:focus{color:#32d296;border-color:#32d296}.uk-form-blank{background:0 0;border-color:transparent}.uk-form-blank:focus{border-color:#e5e5e5;border-style:dashed}input.uk-form-width-xsmall{width:40px}select.uk-form-width-xsmall{width:65px}.uk-form-width-small{width:130px}.uk-form-width-medium{width:200px}.uk-form-width-large{width:500px}.uk-select:not([multiple]):not([size]){-webkit-appearance:none;-moz-appearance:none;padding-right:20px;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23666%22%20points%3D%224%201%201%206%207%206%22%3E%3C%2Fpolygon%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23666%22%20points%3D%224%2013%201%208%207%208%22%3E%3C%2Fpolygon%3E%0A%3C%2Fsvg%3E");background-repeat:no-repeat;background-position:100% 50%}.uk-select:not([multiple]):not([size])::-ms-expand{display:none}.uk-select:not([multiple]):not([size]):disabled{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23999%22%20points%3D%224%201%201%206%207%206%22%3E%3C%2Fpolygon%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23999%22%20points%3D%224%2013%201%208%207%208%22%3E%3C%2Fpolygon%3E%0A%3C%2Fsvg%3E")}.uk-checkbox,.uk-radio{display:inline-block;height:16px;width:16px;overflow:hidden;margin-top:-4px;vertical-align:middle;-webkit-appearance:none;background-color:transparent;background-repeat:no-repeat;background-position:50% 50%;border:1px solid #ccc;-webkit-transition:.2s ease-in-out;transition:.2s ease-in-out;-webkit-transition-property:background-color,border;transition-property:background-color,border}.uk-radio{border-radius:50%}.uk-checkbox:focus,.uk-radio:focus{outline:0;border-color:#1e87f0}.uk-checkbox:checked,.uk-checkbox:indeterminate,.uk-radio:checked{background-color:#1e87f0;border-color:transparent}.uk-checkbox:checked:focus,.uk-checkbox:indeterminate:focus,.uk-radio:checked:focus{background-color:#0e6dcd}.uk-radio:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22%23fff%22%20cx%3D%228%22%20cy%3D%228%22%20r%3D%222%22%3E%3C%2Fcircle%3E%0A%3C%2Fsvg%3E")}.uk-checkbox:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2211%22%20viewBox%3D%220%200%2014%2011%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23fff%22%20points%3D%2212%201%205%207.5%202%205%201%205.5%205%2010%2013%201.5%22%2F%3E%0A%3C%2Fsvg%3E")}.uk-checkbox:indeterminate{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23fff%22%20x%3D%223%22%20y%3D%228%22%20width%3D%2210%22%20height%3D%221%22%3E%3C%2Frect%3E%0A%3C%2Fsvg%3E")}.uk-checkbox:disabled,.uk-radio:disabled{background-color:#f8f8f8;border-color:#e5e5e5}.uk-radio:disabled:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22%23999%22%20cx%3D%228%22%20cy%3D%228%22%20r%3D%222%22%3E%3C%2Fcircle%3E%0A%3C%2Fsvg%3E")}.uk-checkbox:disabled:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2211%22%20viewBox%3D%220%200%2014%2011%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23999%22%20points%3D%2212%201%205%207.5%202%205%201%205.5%205%2010%2013%201.5%22%2F%3E%0A%3C%2Fsvg%3E")}.uk-checkbox:disabled:indeterminate{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23999%22%20x%3D%223%22%20y%3D%228%22%20width%3D%2210%22%20height%3D%221%22%3E%3C%2Frect%3E%0A%3C%2Fsvg%3E")}.uk-legend{width:100%;color:inherit;padding:0;font-size:1.5rem;line-height:1.4}.uk-form-custom{display:inline-block;position:relative;max-width:100%;vertical-align:middle}.uk-form-custom input[type=file],.uk-form-custom select{position:absolute;top:0;z-index:1;width:100%;height:100%;left:0;-webkit-appearance:none;opacity:0;cursor:pointer}.uk-form-custom input[type=file]{font-size:500px;overflow:hidden}.uk-form-label{color:#333;font-size:.875rem}.uk-form-stacked .uk-form-label{display:block;margin-bottom:5px}@media (max-width:959px){.uk-form-horizontal .uk-form-label{display:block;margin-bottom:5px}}@media (min-width:960px){.uk-form-horizontal .uk-form-label{width:200px;margin-top:7px;float:left}.uk-form-horizontal .uk-form-controls{margin-left:215px}.uk-form-horizontal .uk-form-controls-text{padding-top:7px}}.uk-form-icon{position:absolute;top:0;bottom:0;left:0;width:30px;display:-ms-inline-flexbox;display:-webkit-inline-flex;display:inline-flex;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;color:#999}.uk-form-icon:hover{color:#666}.uk-form-icon:not(a):not(button):not(input){pointer-events:none}.uk-form-icon:not(.uk-form-icon-flip)+.uk-input{padding-left:30px}.uk-form-icon-flip{right:0;left:auto}.uk-form-icon-flip+.uk-input{padding-right:30px}.uk-button{margin:0;border:none;overflow:visible;font:inherit;color:inherit;text-transform:none;display:inline-block;box-sizing:border-box;padding:0 30px;vertical-align:middle;font-size:.875rem;line-height:38px;text-align:center;text-decoration:none;text-transform:uppercase;-webkit-transition:.1s ease-in-out;transition:.1s ease-in-out;-webkit-transition-property:color,background-color,border-color;transition-property:color,background-color,border-color}.uk-button:not(:disabled){cursor:pointer}.uk-button::-moz-focus-inner{border:0;padding:0}.uk-button:hover{text-decoration:none}.uk-button:focus{outline:0}.uk-button-default{background-color:transparent;color:#333;border:1px solid #e5e5e5}.uk-button-default:focus,.uk-button-default:hover{background-color:transparent;color:#333;border-color:#b2b2b2}.uk-button-default.uk-active,.uk-button-default:active{background-color:transparent;color:#333;border-color:#999}.uk-button-primary{background-color:#1e87f0;color:#fff;border:1px solid transparent}.uk-button-primary:focus,.uk-button-primary:hover{background-color:#0f7ae5;color:#fff}.uk-button-primary.uk-active,.uk-button-primary:active{background-color:#0e6dcd;color:#fff}.uk-button-secondary{background-color:#222;color:#fff;border:1px solid transparent}.uk-button-secondary:focus,.uk-button-secondary:hover{background-color:#151515;color:#fff}.uk-button-secondary.uk-active,.uk-button-secondary:active{background-color:#080808;color:#fff}.uk-button-danger{background-color:#f0506e;color:#fff;border:1px solid transparent}.uk-button-danger:focus,.uk-button-danger:hover{background-color:#ee395b;color:#fff}.uk-button-danger.uk-active,.uk-button-danger:active{background-color:#ec2147;color:#fff}.uk-button-danger:disabled,.uk-button-default:disabled,.uk-button-primary:disabled,.uk-button-secondary:disabled{background-color:transparent;color:#999;border-color:#e5e5e5}.uk-button-small{padding:0 15px;line-height:28px;font-size:.875rem}.uk-button-large{padding:0 40px;line-height:53px;font-size:.875rem}.uk-button-text{padding:0;line-height:1.5;background:0 0;color:#333;position:relative}.uk-button-text::before{content:"";position:absolute;bottom:0;left:0;right:100%;border-bottom:1px solid #333;-webkit-transition:right .3s ease-out;transition:right .3s ease-out}.uk-button-text:focus,.uk-button-text:hover{color:#333}.uk-button-text:focus::before,.uk-button-text:hover::before{right:0}.uk-button-text:disabled{color:#999}.uk-button-text:disabled::before{display:none}.uk-button-link{padding:0;line-height:1.5;background:0 0;color:#1e87f0}.uk-button-link:focus,.uk-button-link:hover{color:#0f6ecd;text-decoration:underline}.uk-button-link:disabled{color:#999;text-decoration:none}.uk-button-group{display:-ms-inline-flexbox;display:-webkit-inline-flex;display:inline-flex;vertical-align:middle;position:relative}.uk-button-group>.uk-button:nth-child(n+2),.uk-button-group>div:nth-child(n+2) .uk-button{margin-left:-1px}.uk-button-group .uk-button.uk-active,.uk-button-group .uk-button:active,.uk-button-group .uk-button:focus,.uk-button-group .uk-button:hover{position:relative;z-index:1}.uk-section{box-sizing:border-box;padding-top:40px;padding-bottom:40px}@media (min-width:960px){.uk-section{padding-top:70px;padding-bottom:70px}}.uk-section::after,.uk-section::before{content:"";display:table}.uk-section::after{clear:both}.uk-section>:last-child{margin-bottom:0}.uk-section-xsmall{padding-top:20px;padding-bottom:20px}.uk-section-small{padding-top:40px;padding-bottom:40px}.uk-section-large{padding-top:70px;padding-bottom:70px}@media (min-width:960px){.uk-section-large{padding-top:140px;padding-bottom:140px}}.uk-section-xlarge{padding-top:140px;padding-bottom:140px}@media (min-width:960px){.uk-section-xlarge{padding-top:210px;padding-bottom:210px}}.uk-section-default{background:#fff}.uk-section-muted{background:#f8f8f8}.uk-section-primary{background:#1e87f0}.uk-section-secondary{background:#222}.uk-container{box-sizing:content-box;max-width:1200px;margin-left:auto;margin-right:auto;padding-left:15px;padding-right:15px}@media (min-width:640px){.uk-container{padding-left:30px;padding-right:30px}}@media (min-width:960px){.uk-container{padding-left:40px;padding-right:40px}}.uk-container::after,.uk-container::before{content:"";display:table}.uk-container::after{clear:both}.uk-container>:last-child{margin-bottom:0}.uk-container .uk-container{padding-left:0;padding-right:0}.uk-container-small{max-width:900px}.uk-container-large{max-width:1600px}.uk-container-expand{max-width:none}.uk-grid{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:0;padding:0;list-style:none}.uk-grid>*{margin:0}.uk-grid>*>:last-child{margin-bottom:0}.uk-grid{margin-left:-30px}.uk-grid>*{padding-left:30px}*+.uk-grid-margin,.uk-grid+.uk-grid,.uk-grid>.uk-grid-margin{margin-top:30px}@media (min-width:1200px){.uk-grid{margin-left:-40px}.uk-grid>*{padding-left:40px}*+.uk-grid-margin,.uk-grid+.uk-grid,.uk-grid>.uk-grid-margin{margin-top:40px}}.uk-grid-small{margin-left:-15px}.uk-grid-small>*{padding-left:15px}*+.uk-grid-margin-small,.uk-grid+.uk-grid-small,.uk-grid-small>.uk-grid-margin{margin-top:15px}.uk-grid-medium{margin-left:-30px}.uk-grid-medium>*{padding-left:30px}*+.uk-grid-margin-medium,.uk-grid+.uk-grid-medium,.uk-grid-medium>.uk-grid-margin{margin-top:30px}.uk-grid-large{margin-left:-40px}.uk-grid-large>*{padding-left:40px}*+.uk-grid-margin-large,.uk-grid+.uk-grid-large,.uk-grid-large>.uk-grid-margin{margin-top:40px}@media (min-width:1200px){.uk-grid-large{margin-left:-70px}.uk-grid-large>*{padding-left:70px}*+.uk-grid-margin-large,.uk-grid+.uk-grid-large,.uk-grid-large>.uk-grid-margin{margin-top:70px}}.uk-grid-collapse{margin-left:0}.uk-grid-collapse>*{padding-left:0}.uk-grid+.uk-grid-collapse,.uk-grid-collapse>.uk-grid-margin{margin-top:0}.uk-grid-divider>*{position:relative}.uk-grid-divider>:not(.uk-first-column)::before{content:"";position:absolute;top:0;bottom:0;border-left:1px solid #e5e5e5}.uk-grid-divider.uk-grid-stack>.uk-grid-margin::before{content:"";position:absolute;left:0;right:0;border-top:1px solid #e5e5e5}.uk-grid-divider{margin-left:-60px}.uk-grid-divider>*{padding-left:60px}.uk-grid-divider>:not(.uk-first-column)::before{left:30px}.uk-grid-divider.uk-grid-stack>.uk-grid-margin{margin-top:60px}.uk-grid-divider.uk-grid-stack>.uk-grid-margin::before{top:-30px;left:60px}@media (min-width:1200px){.uk-grid-divider{margin-left:-80px}.uk-grid-divider>*{padding-left:80px}.uk-grid-divider>:not(.uk-first-column)::before{left:40px}.uk-grid-divider.uk-grid-stack>.uk-grid-margin{margin-top:80px}.uk-grid-divider.uk-grid-stack>.uk-grid-margin::before{top:-40px;left:80px}}.uk-grid-divider.uk-grid-small{margin-left:-30px}.uk-grid-divider.uk-grid-small>*{padding-left:30px}.uk-grid-divider.uk-grid-small>:not(.uk-first-column)::before{left:15px}.uk-grid-divider.uk-grid-small.uk-grid-stack>.uk-grid-margin{margin-top:30px}.uk-grid-divider.uk-grid-small.uk-grid-stack>.uk-grid-margin::before{top:-15px;left:30px}.uk-grid-divider.uk-grid-medium{margin-left:-60px}.uk-grid-divider.uk-grid-medium>*{padding-left:60px}.uk-grid-divider.uk-grid-medium>:not(.uk-first-column)::before{left:30px}.uk-grid-divider.uk-grid-medium.uk-grid-stack>.uk-grid-margin{margin-top:60px}.uk-grid-divider.uk-grid-medium.uk-grid-stack>.uk-grid-margin::before{top:-30px;left:60px}.uk-grid-divider.uk-grid-large{margin-left:-80px}.uk-grid-divider.uk-grid-large>*{padding-left:80px}.uk-grid-divider.uk-grid-large>:not(.uk-first-column)::before{left:40px}.uk-grid-divider.uk-grid-large.uk-grid-stack>.uk-grid-margin{margin-top:80px}.uk-grid-divider.uk-grid-large.uk-grid-stack>.uk-grid-margin::before{top:-40px;left:80px}@media (min-width:1200px){.uk-grid-divider.uk-grid-large{margin-left:-140px}.uk-grid-divider.uk-grid-large>*{padding-left:140px}.uk-grid-divider.uk-grid-large>:not(.uk-first-column)::before{left:70px}.uk-grid-divider.uk-grid-large.uk-grid-stack>.uk-grid-margin{margin-top:140px}.uk-grid-divider.uk-grid-large.uk-grid-stack>.uk-grid-margin::before{top:-70px;left:140px}}.uk-grid-match>*{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap}.uk-grid-match>*>:not([class*=uk-width]){box-sizing:border-box;width:100%;-ms-flex:auto;-webkit-flex:auto;flex:auto}.uk-card{position:relative;box-sizing:border-box;-webkit-transition:box-shadow .1s ease-in-out;transition:box-shadow .1s ease-in-out}.uk-card-body{padding:30px 30px}.uk-card-header{padding:15px 30px}.uk-card-footer{padding:15px 30px}@media (min-width:1200px){.uk-card-body{padding:40px 40px}.uk-card-header{padding:20px 40px}.uk-card-footer{padding:20px 40px}}.uk-card-body::after,.uk-card-body::before,.uk-card-footer::after,.uk-card-footer::before,.uk-card-header::after,.uk-card-header::before{content:"";display:table}.uk-card-body::after,.uk-card-footer::after,.uk-card-header::after{clear:both}.uk-card-body>:last-child,.uk-card-footer>:last-child,.uk-card-header>:last-child{margin-bottom:0}.uk-card-title{font-size:1.5rem;line-height:1.4}.uk-card-badge{position:absolute;top:30px;right:30px;z-index:1}.uk-card-badge:first-child+*{margin-top:0}.uk-card-hover:not(.uk-card-default):not(.uk-card-primary):not(.uk-card-secondary):hover{background:#fff;box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-card-default{background:#fff;color:#666;box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-card-default .uk-card-title{color:#333}.uk-card-default.uk-card-hover:hover{background-color:#fff;box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-card-default .uk-card-header{border-bottom:1px solid #e5e5e5}.uk-card-default .uk-card-footer{border-top:1px solid #e5e5e5}.uk-card-primary{background:#1e87f0;color:#fff;box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-card-primary .uk-card-title{color:#fff}.uk-card-primary.uk-card-hover:hover{background-color:#1e87f0;box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-card-secondary{background:#222;color:#fff;box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-card-secondary .uk-card-title{color:#fff}.uk-card-secondary.uk-card-hover:hover{background-color:#222;box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-card-small .uk-card-body,.uk-card-small.uk-card-body{padding:20px 20px}.uk-card-small .uk-card-header{padding:13px 20px}.uk-card-small .uk-card-footer{padding:13px 20px}@media (min-width:1200px){.uk-card-large .uk-card-body,.uk-card-large.uk-card-body{padding:70px 70px}.uk-card-large .uk-card-header{padding:35px 70px}.uk-card-large .uk-card-footer{padding:35px 70px}}.uk-card-body .uk-nav-default{margin:-15px -30px}.uk-card-title+.uk-nav-default{margin-top:0}.uk-card-body .uk-nav-default .uk-nav-divider,.uk-card-body .uk-nav-default .uk-nav-header,.uk-card-body .uk-nav-default>li>a{padding-left:30px;padding-right:30px}.uk-card-body .uk-nav-default .uk-nav-sub{padding-left:45px}@media (min-width:1200px){.uk-card-body .uk-nav-default{margin:-25px -40px}.uk-card-title+.uk-nav-default{margin-top:0}.uk-card-body .uk-nav-default .uk-nav-divider,.uk-card-body .uk-nav-default .uk-nav-header,.uk-card-body .uk-nav-default>li>a{padding-left:40px;padding-right:40px}.uk-card-body .uk-nav-default .uk-nav-sub{padding-left:55px}}.uk-card-small .uk-nav-default{margin:-5px -20px}.uk-card-small .uk-card-title+.uk-nav-default{margin-top:0}.uk-card-small .uk-nav-default .uk-nav-divider,.uk-card-small .uk-nav-default .uk-nav-header,.uk-card-small .uk-nav-default>li>a{padding-left:20px;padding-right:20px}.uk-card-small .uk-nav-default .uk-nav-sub{padding-left:35px}@media (min-width:1200px){.uk-card-large .uk-nav-default{margin:-55px -70px}.uk-card-large .uk-card-title+.uk-nav-default{margin-top:0}}.uk-close{margin:0;border:none;overflow:visible;font:inherit;color:#999;text-transform:none;padding:0;background-color:transparent;cursor:pointer;line-height:0;-webkit-transition:.1s ease-in-out;transition:.1s ease-in-out;-webkit-transition-property:color,opacity;transition-property:color,opacity}.uk-close::-moz-focus-inner{border:0;padding:0}.uk-close:focus,.uk-close:hover{color:#666;outline:0}.uk-spinner>*{-webkit-animation:uk-spinner-rotate 1.4s linear infinite;animation:uk-spinner-rotate 1.4s linear infinite}@-webkit-keyframes uk-spinner-rotate{0%{-webkit-transform:rotate(0)}100%{-webkit-transform:rotate(270deg)}}@keyframes uk-spinner-rotate{0%{transform:rotate(0)}100%{transform:rotate(270deg)}}.uk-spinner>*>*{stroke-dasharray:88px;stroke-dashoffset:0;transform-origin:center;-webkit-animation:uk-spinner-dash 1.4s ease-in-out infinite;animation:uk-spinner-dash 1.4s ease-in-out infinite;stroke-width:1;stroke-linecap:round}@-webkit-keyframes uk-spinner-dash{0%{stroke-dashoffset:88px}50%{stroke-dashoffset:22px;-webkit-transform:rotate(135deg)}100%{stroke-dashoffset:88px;-webkit-transform:rotate(450deg)}}@keyframes uk-spinner-dash{0%{stroke-dashoffset:88px}50%{stroke-dashoffset:22px;transform:rotate(135deg)}100%{stroke-dashoffset:88px;transform:rotate(450deg)}}.uk-totop{padding:5px;color:#999;-webkit-transition:color .1s ease-in-out;transition:color .1s ease-in-out}.uk-totop:focus,.uk-totop:hover{color:#666;outline:0}.uk-totop:active{color:#333}.uk-alert{position:relative;margin-bottom:20px;padding:15px 29px 15px 15px;background:#f8f8f8;color:#666}*+.uk-alert{margin-top:20px}.uk-alert>:last-child{margin-bottom:0}.uk-alert-close{position:absolute;top:20px;right:15px;color:inherit;opacity:.4}.uk-alert-close:first-child+*{margin-top:0}.uk-alert-close:focus,.uk-alert-close:hover{color:inherit;opacity:.8}.uk-alert-primary{background:#d8eafc;color:#1e87f0}.uk-alert-success{background:#edfbf6;color:#32d296}.uk-alert-warning{background:#fff6ee;color:#faa05a}.uk-alert-danger{background:#fef4f6;color:#f0506e}.uk-alert h1,.uk-alert h2,.uk-alert h3,.uk-alert h4,.uk-alert h5,.uk-alert h6{color:inherit}.uk-alert a:not([class]){color:inherit;text-decoration:underline}.uk-alert a:not([class]):hover{color:inherit;text-decoration:underline}.uk-badge{box-sizing:border-box;min-width:22px;height:22px;line-height:22px;padding:0 5px;border-radius:500px;vertical-align:middle;background:#1e87f0;color:#fff;font-size:.875rem;display:-ms-inline-flexbox;display:-webkit-inline-flex;display:inline-flex;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center}.uk-badge:focus,.uk-badge:hover{color:#fff;text-decoration:none;outline:0}.uk-label{display:inline-block;padding:0 10px;background:#1e87f0;line-height:1.5;font-size:.875rem;color:#fff;vertical-align:middle;white-space:nowrap;border-radius:2px;text-transform:uppercase}.uk-label-success{background-color:#32d296;color:#fff}.uk-label-warning{background-color:#faa05a;color:#fff}.uk-label-danger{background-color:#f0506e;color:#fff}.uk-overlay{padding:30px 30px}.uk-overlay>:last-child{margin-bottom:0}.uk-overlay-default{background:rgba(255,255,255,.8)}.uk-overlay-primary{background:rgba(34,34,34,.8)}.uk-article::after,.uk-article::before{content:"";display:table}.uk-article::after{clear:both}.uk-article>:last-child{margin-bottom:0}.uk-article+.uk-article{margin-top:70px}.uk-article-title{font-size:2.625rem;line-height:1.2}.uk-article-meta{font-size:.875rem;line-height:1.4;color:#999}.uk-article-meta a{color:#999}.uk-article-meta a:hover{color:#666;text-decoration:none}.uk-comment-header{margin-bottom:20px}.uk-comment-body::after,.uk-comment-body::before,.uk-comment-header::after,.uk-comment-header::before{content:"";display:table}.uk-comment-body::after,.uk-comment-header::after{clear:both}.uk-comment-body>:last-child,.uk-comment-header>:last-child{margin-bottom:0}.uk-comment-title{font-size:1.25rem;line-height:1.4}.uk-comment-meta{font-size:.875rem;line-height:1.4;color:#999}.uk-comment-list{padding:0;list-style:none}.uk-comment-list>:nth-child(n+2){margin-top:70px}.uk-comment-list .uk-comment~ul{margin:70px 0 0 0;padding-left:30px;list-style:none}@media (min-width:960px){.uk-comment-list .uk-comment~ul{padding-left:100px}}.uk-comment-list .uk-comment~ul>:nth-child(n+2){margin-top:70px}.uk-comment-primary{padding:30px;background-color:#f8f8f8}.uk-search{display:inline-block;position:relative;max-width:100%;margin:0}.uk-search-input::-webkit-search-cancel-button,.uk-search-input::-webkit-search-decoration{-webkit-appearance:none}.uk-search-input::-moz-placeholder{opacity:1}.uk-search-input{box-sizing:border-box;margin:0;border-radius:0;font:inherit;overflow:visible;-webkit-appearance:none;vertical-align:middle;width:100%;border:none;color:#666}.uk-search-input:focus{outline:0}.uk-search-input:-ms-input-placeholder{color:#999!important}.uk-search-input::-moz-placeholder{color:#999}.uk-search-input::-webkit-input-placeholder{color:#999}.uk-search-icon{margin:0;border:none;overflow:visible;font:inherit;color:inherit;text-transform:none;padding:0;background-color:transparent;cursor:pointer}.uk-search-icon::-moz-focus-inner{border:0;padding:0}.uk-search-icon:focus{outline:0}.uk-search .uk-search-icon{position:absolute;top:0;bottom:0;left:0;display:-ms-inline-flexbox;display:-webkit-inline-flex;display:inline-flex;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;color:#999}.uk-search .uk-search-icon:hover{color:#999}.uk-search .uk-search-icon:not(a):not(button):not(input){pointer-events:none}.uk-search .uk-search-icon-flip{right:0;left:auto}.uk-search-default{width:180px}.uk-search-default .uk-search-input{height:40px;padding-left:6px;padding-right:6px;background:0 0;border:1px solid #e5e5e5}.uk-search-default .uk-search-input:focus{background-color:transparent}.uk-search-default .uk-search-icon{width:40px}.uk-search-default .uk-search-icon:not(.uk-search-icon-flip)+.uk-search-input{padding-left:40px}.uk-search-default .uk-search-icon-flip+.uk-search-input{padding-right:40px}.uk-search-navbar{width:400px}.uk-search-navbar .uk-search-input{height:40px;background:0 0;font-size:1.5rem}.uk-search-navbar .uk-search-icon{width:40px}.uk-search-navbar .uk-search-icon:not(.uk-search-icon-flip)+.uk-search-input{padding-left:40px}.uk-search-navbar .uk-search-icon-flip+.uk-search-input{padding-right:40px}.uk-search-large{width:500px}.uk-search-large .uk-search-input{height:80px;background:0 0;font-size:2.625rem}.uk-search-large .uk-search-icon{width:80px}.uk-search-large .uk-search-icon:not(.uk-search-icon-flip)+.uk-search-input{padding-left:80px}.uk-search-large .uk-search-icon-flip+.uk-search-input{padding-right:80px}.uk-search-toggle{color:#999}.uk-search-toggle:focus,.uk-search-toggle:hover{color:#666}.uk-nav,.uk-nav ul{margin:0;padding:0;list-style:none}.uk-nav li>a{display:block;text-decoration:none}.uk-nav li>a:focus{outline:0}.uk-nav>li>a{padding:5px 0}ul.uk-nav-sub{padding:5px 0 5px 15px}.uk-nav-sub ul{padding-left:15px}.uk-nav-sub a{padding:2px 0}.uk-nav-parent-icon>.uk-parent>a::after{content:"";width:1.5em;height:1.5em;float:right;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22%23666%22%20stroke-width%3D%221.1%22%20points%3D%2210%201%204%207%2010%2013%22%3E%3C%2Fpolyline%3E%0A%3C%2Fsvg%3E");background-repeat:no-repeat;background-position:50% 50%}.uk-nav-parent-icon>.uk-parent.uk-open>a::after{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22%23666%22%20stroke-width%3D%221.1%22%20points%3D%221%204%207%2010%2013%204%22%3E%3C%2Fpolyline%3E%0A%3C%2Fsvg%3E")}.uk-nav-header{padding:5px 0;text-transform:uppercase;font-size:.875rem}.uk-nav-header:not(:first-child){margin-top:20px}.uk-nav-divider{margin:5px 0}.uk-nav-default{font-size:.875rem}.uk-nav-default>li>a{color:#999}.uk-nav-default>li>a:focus,.uk-nav-default>li>a:hover{color:#666}.uk-nav-default>li.uk-active>a{color:#333}.uk-nav-default .uk-nav-header{color:#333}.uk-nav-default .uk-nav-divider{border-top:1px solid #e5e5e5}.uk-nav-default .uk-nav-sub a{color:#999}.uk-nav-default .uk-nav-sub a:focus,.uk-nav-default .uk-nav-sub a:hover{color:#666}.uk-nav-primary>li>a{font-size:1.5rem;color:#999}.uk-nav-primary>li>a:focus,.uk-nav-primary>li>a:hover{color:#666}.uk-nav-primary>li.uk-active>a{color:#333}.uk-nav-primary .uk-nav-header{color:#333}.uk-nav-primary .uk-nav-divider{border-top:1px solid #e5e5e5}.uk-nav-primary .uk-nav-sub a{color:#999}.uk-nav-primary .uk-nav-sub a:focus,.uk-nav-primary .uk-nav-sub a:hover{color:#666}.uk-nav-center{text-align:center}.uk-nav-center .uk-nav-sub,.uk-nav-center .uk-nav-sub ul{padding-left:0}.uk-nav-center.uk-nav-parent-icon>.uk-parent>a::after{position:absolute}.uk-navbar{display:-ms-flexbox;display:-webkit-flex;display:flex;position:relative}.uk-navbar-container:not(.uk-navbar-transparent){background:#f8f8f8}.uk-navbar-container>::after,.uk-navbar-container>::before{display:none!important}.uk-navbar-left,.uk-navbar-right,[class*=uk-navbar-center]{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-align:center;-webkit-align-items:center;align-items:center}.uk-navbar-right{margin-left:auto}.uk-navbar-center:only-child{margin-left:auto;margin-right:auto;position:relative}.uk-navbar-center:not(:only-child){position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);z-index:990}.uk-navbar-center:not(:only-child) .uk-navbar-item,.uk-navbar-center:not(:only-child) .uk-navbar-nav>li>a,.uk-navbar-center:not(:only-child) .uk-navbar-toggle{white-space:nowrap}.uk-navbar-center-left,.uk-navbar-center-right{position:absolute;top:0}.uk-navbar-center-left{right:100%}.uk-navbar-center-right{left:100%}[class*=uk-navbar-center-] .uk-navbar-item,[class*=uk-navbar-center-] .uk-navbar-nav>li>a,[class*=uk-navbar-center-] .uk-navbar-toggle{white-space:nowrap}.uk-navbar-nav{display:-ms-flexbox;display:-webkit-flex;display:flex;margin:0;padding:0;list-style:none}.uk-navbar-center:only-child,.uk-navbar-left,.uk-navbar-right{-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap}.uk-navbar-item,.uk-navbar-nav>li>a,.uk-navbar-toggle{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;box-sizing:border-box;height:80px;padding:0 15px;font-size:.875rem;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;text-decoration:none}.uk-navbar-nav>li>a{color:#999;text-transform:uppercase;-webkit-transition:.1s ease-in-out;transition:.1s ease-in-out;-webkit-transition-property:color,background-color;transition-property:color,background-color}.uk-navbar-nav>li:hover>a,.uk-navbar-nav>li>a.uk-open,.uk-navbar-nav>li>a:focus{color:#666;outline:0}.uk-navbar-nav>li>a:active{color:#333}.uk-navbar-nav>li.uk-active>a{color:#333}.uk-navbar-item{color:#666}.uk-navbar-toggle{color:#999}.uk-navbar-toggle.uk-open,.uk-navbar-toggle:focus,.uk-navbar-toggle:hover{color:#666;outline:0;text-decoration:none}.uk-navbar-subtitle{font-size:.875rem}.uk-navbar-dropdown{display:none;position:absolute;z-index:1020;box-sizing:border-box;width:200px;padding:25px;background:#fff;color:#666;box-shadow:0 5px 12px rgba(0,0,0,.15)}.uk-navbar-dropdown.uk-open{display:block}[class*=uk-navbar-dropdown-top]{margin-top:-15px}[class*=uk-navbar-dropdown-bottom]{margin-top:15px}[class*=uk-navbar-dropdown-left]{margin-left:-15px}[class*=uk-navbar-dropdown-right]{margin-left:15px}.uk-navbar-dropdown-grid{margin-left:-50px}.uk-navbar-dropdown-grid>*{padding-left:50px}.uk-navbar-dropdown-grid>.uk-grid-margin{margin-top:50px}.uk-navbar-dropdown-stack .uk-navbar-dropdown-grid>*{width:100%!important}.uk-navbar-dropdown-width-2:not(.uk-navbar-dropdown-stack){width:400px}.uk-navbar-dropdown-width-3:not(.uk-navbar-dropdown-stack){width:600px}.uk-navbar-dropdown-width-4:not(.uk-navbar-dropdown-stack){width:800px}.uk-navbar-dropdown-width-5:not(.uk-navbar-dropdown-stack){width:1000px}.uk-navbar-dropdown-dropbar{margin-bottom:30px;box-shadow:none}.uk-navbar-dropdown-nav{font-size:.875rem}.uk-navbar-dropdown-nav>li>a{color:#999}.uk-navbar-dropdown-nav>li>a:focus,.uk-navbar-dropdown-nav>li>a:hover{color:#666}.uk-navbar-dropdown-nav>li.uk-active>a{color:#333}.uk-navbar-dropdown-nav .uk-nav-header{color:#333}.uk-navbar-dropdown-nav .uk-nav-divider{border-top:1px solid #e5e5e5}.uk-navbar-dropdown-nav .uk-nav-sub a{color:#999}.uk-navbar-dropdown-nav .uk-nav-sub a:focus,.uk-navbar-dropdown-nav .uk-nav-sub a:hover{color:#666}.uk-navbar-dropbar{position:relative;background:#fff;overflow:hidden}.uk-navbar-dropbar-slide{position:absolute;z-index:1020;left:0;right:0}.uk-navbar-container>.uk-container .uk-navbar-left{margin-left:-15px;margin-right:-15px}.uk-navbar-container>.uk-container .uk-navbar-right{margin-right:-15px}.uk-navbar-dropdown-grid>*{position:relative}.uk-navbar-dropdown-grid>:not(.uk-first-column)::before{content:"";position:absolute;top:0;bottom:0;left:25px;border-left:1px solid #e5e5e5}.uk-navbar-dropdown-grid.uk-grid-stack>.uk-grid-margin::before{content:"";position:absolute;top:-25px;left:50px;right:0;border-top:1px solid #e5e5e5}.uk-subnav{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin-left:-20px;padding:0;list-style:none}.uk-subnav>*{-ms-flex:none;-webkit-flex:none;flex:none;padding-left:20px;position:relative}.uk-subnav>*>:first-child{display:block;color:#999;font-size:.875rem;text-transform:uppercase;-webkit-transition:.1s ease-in-out;transition:.1s ease-in-out;-webkit-transition-property:color,background-color;transition-property:color,background-color}.uk-subnav>*>a:focus,.uk-subnav>*>a:hover{color:#666;text-decoration:none;outline:0}.uk-subnav>.uk-active>a{color:#333}.uk-subnav-divider>*{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-align:center;-webkit-align-items:center;align-items:center}.uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before{content:"";height:1.5em;margin-left:0;margin-right:20px;border-left:1px solid #e5e5e5}.uk-subnav-pill>*>:first-child{padding:5px 10px;background:0 0;color:#999}.uk-subnav-pill>*>a:focus,.uk-subnav-pill>*>a:hover{background-color:#f8f8f8;color:#666}.uk-subnav-pill>*>a:active{background-color:#f8f8f8;color:#666}.uk-subnav-pill>.uk-active>a{background-color:#1e87f0;color:#fff}.uk-subnav>.uk-disabled>a{color:#999}.uk-breadcrumb{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap;padding:0;list-style:none}.uk-breadcrumb>*{-ms-flex:none;-webkit-flex:none;flex:none}.uk-breadcrumb>*>*{display:inline-block;font-size:.875rem;color:#999}.uk-breadcrumb>*>:focus,.uk-breadcrumb>*>:hover{color:#666;text-decoration:none}.uk-breadcrumb>:last-child>*{color:#666}.uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before{content:"/";display:inline-block;margin:0 20px;color:#999}.uk-pagination{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin-left:-20px;padding:0;list-style:none}.uk-pagination>*{-ms-flex:none;-webkit-flex:none;flex:none;padding-left:20px;position:relative}.uk-pagination>*>*{display:block;color:#999;-webkit-transition:color .1s ease-in-out;transition:color .1s ease-in-out}.uk-pagination>*>:focus,.uk-pagination>*>:hover{color:#666;text-decoration:none}.uk-pagination>.uk-active>*{color:#666}.uk-pagination>.uk-disabled>*{color:#999}.uk-tab{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin-left:-20px;padding:0;list-style:none;position:relative}.uk-tab::before{content:"";position:absolute;bottom:0;left:20px;right:0;border-bottom:1px solid #e5e5e5}.uk-tab>*{-ms-flex:none;-webkit-flex:none;flex:none;padding-left:20px;position:relative}.uk-tab>*>a{display:block;text-align:center;padding:5px 10px;color:#999;border-bottom:1px solid transparent;font-size:.875rem;text-transform:uppercase;-webkit-transition:color .1s ease-in-out;transition:color .1s ease-in-out}.uk-tab>*>a:focus,.uk-tab>*>a:hover{color:#666;text-decoration:none}.uk-tab>.uk-active>a{color:#333;border-color:#1e87f0}.uk-tab>.uk-disabled>a{color:#999}.uk-tab-bottom::before{top:0;bottom:auto}.uk-tab-bottom>*>a{border-top:1px solid transparent;border-bottom:none}.uk-tab-left,.uk-tab-right{-ms-flex-direction:column;-webkit-flex-direction:column;flex-direction:column;margin-left:0}.uk-tab-left>*,.uk-tab-right>*{padding-left:0}.uk-tab-left::before{top:0;bottom:0;left:auto;right:0;border-left:1px solid #e5e5e5;border-bottom:none}.uk-tab-right::before{top:0;bottom:0;left:0;right:auto;border-left:1px solid #e5e5e5;border-bottom:none}.uk-tab-left>*>a{text-align:right;border-right:1px solid transparent;border-bottom:none}.uk-tab-right>*>a{text-align:left;border-left:1px solid transparent;border-bottom:none}.uk-tab .uk-dropdown{margin-left:30px}.uk-slidenav{padding:5px;color:rgba(102,102,102,.6);-webkit-transition:color .1s ease-in-out;transition:color .1s ease-in-out}.uk-slidenav:focus,.uk-slidenav:hover{color:rgba(102,102,102,.8);outline:0}.uk-slidenav:active{color:rgba(102,102,102,.9)}.uk-slidenav-container{display:-ms-flexbox;display:-webkit-flex;display:flex}.uk-dotnav{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:0;padding:0;list-style:none;margin-left:-15px}.uk-dotnav>*{-ms-flex:none;-webkit-flex:none;flex:none;padding-left:15px}.uk-dotnav>*>*{display:block;box-sizing:content-box;width:16px;height:16px;border-radius:50%;background:rgba(102,102,102,.1);text-indent:100%;overflow:hidden;white-space:nowrap;-webkit-transition:background-color .2s ease-in-out;transition:background-color .2s ease-in-out}.uk-dotnav>*>:focus,.uk-dotnav>*>:hover{background-color:rgba(102,102,102,.4);outline:0}.uk-dotnav>*>:active{background-color:rgba(102,102,102,.6)}.uk-dotnav>.uk-active>*{background-color:rgba(102,102,102,.4)}.uk-dotnav-vertical{-ms-flex-direction:column;-webkit-flex-direction:column;flex-direction:column;margin-left:0;margin-top:-15px}.uk-dotnav-vertical>*{padding-left:0;padding-top:15px}.uk-accordion{padding:0;list-style:none}.uk-accordion>:nth-child(n+2){margin-top:20px}.uk-accordion-title{margin:0;font-size:1.25rem;line-height:1.4;cursor:pointer;overflow:hidden}.uk-accordion-title::after{content:"";width:1.4em;height:1.4em;float:right;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23666%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%3E%3C%2Frect%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23666%22%20width%3D%221%22%20height%3D%2213%22%20x%3D%226%22%20y%3D%220%22%3E%3C%2Frect%3E%0A%3C%2Fsvg%3E");background-repeat:no-repeat;background-position:50% 50%}.uk-open>.uk-accordion-title::after{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23666%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%3E%3C%2Frect%3E%0A%3C%2Fsvg%3E")}.uk-accordion-content{margin-top:20px}.uk-accordion-content:after,.uk-accordion-content:before{content:"";display:table}.uk-accordion-content:after{clear:both}.uk-accordion-content>:last-child{margin-bottom:0}.uk-drop{display:none;position:absolute;z-index:1020;box-sizing:border-box;width:300px}.uk-drop.uk-open{display:block}[class*=uk-drop-top]{margin-top:-20px}[class*=uk-drop-bottom]{margin-top:20px}[class*=uk-drop-left]{margin-left:-20px}[class*=uk-drop-right]{margin-left:20px}.uk-drop-stack .uk-drop-grid>*{width:100%!important}.uk-dropdown{display:none;position:absolute;z-index:1020;box-sizing:border-box;min-width:200px;padding:25px;background:#fff;color:#666;box-shadow:0 5px 12px rgba(0,0,0,.15)}.uk-dropdown.uk-open{display:block}.uk-dropdown-nav{white-space:nowrap;font-size:.875rem}.uk-dropdown-nav>li>a{color:#999}.uk-dropdown-nav>li.uk-active>a,.uk-dropdown-nav>li>a:focus,.uk-dropdown-nav>li>a:hover{color:#666}.uk-dropdown-nav .uk-nav-header{color:#333}.uk-dropdown-nav .uk-nav-divider{border-top:1px solid #e5e5e5}.uk-dropdown-nav .uk-nav-sub a{color:#999}.uk-dropdown-nav .uk-nav-sub a:focus,.uk-dropdown-nav .uk-nav-sub a:hover{color:#666}[class*=uk-dropdown-top]{margin-top:-10px}[class*=uk-dropdown-bottom]{margin-top:10px}[class*=uk-dropdown-left]{margin-left:-10px}[class*=uk-dropdown-right]{margin-left:10px}.uk-dropdown-stack .uk-dropdown-grid>*{width:100%!important}.uk-modal{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1010;overflow-y:auto;-webkit-overflow-scrolling:touch;padding-left:15px;padding-right:15px;background:rgba(0,0,0,.6);opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}@media (min-width:640px){.uk-modal{padding-left:30px;padding-right:30px}}@media (min-width:960px){.uk-modal{padding-left:40px;padding-right:40px}}.uk-modal.uk-open{opacity:1}.uk-modal-page{overflow:hidden}.uk-modal-dialog{position:relative;box-sizing:border-box;margin:50px auto;width:600px;max-width:100%;background:#fff;opacity:0;-webkit-transform:translateY(-100px);transform:translateY(-100px);-webkit-transition:opacity .3s linear,-webkit-transform .3s ease-out;transition:opacity .3s linear,transform .3s ease-out}@media (max-width:639px){.uk-modal-dialog{margin-top:15px;margin-bottom:15px}}.uk-open>.uk-modal-dialog{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}.uk-modal-container .uk-modal-dialog{width:1200px}.uk-modal-full{padding:0;background:0 0}.uk-modal-full .uk-modal-dialog{margin:0;width:100%;max-width:100%;-webkit-transform:translateY(0);transform:translateY(0)}.uk-modal-lightbox{background:rgba(0,0,0,.9)}.uk-modal-lightbox .uk-modal-dialog{margin-left:15px;margin-right:15px}.uk-modal-body{padding:30px 30px}.uk-modal-header{padding:15px 30px;background:#fff;border-bottom:1px solid #e5e5e5}.uk-modal-footer{padding:15px 30px;background:#fff;border-top:1px solid #e5e5e5}.uk-modal-body::after,.uk-modal-body::before,.uk-modal-footer::after,.uk-modal-footer::before,.uk-modal-header::after,.uk-modal-header::before{content:"";display:table}.uk-modal-body::after,.uk-modal-footer::after,.uk-modal-header::after{clear:both}.uk-modal-body>:last-child,.uk-modal-footer>:last-child,.uk-modal-header>:last-child{margin-bottom:0}.uk-modal-title{font-size:2rem;line-height:1.3}[class*=uk-modal-close-]{position:absolute;top:10px;right:10px;padding:5px}[class*=uk-modal-close-]:first-child+*{margin-top:0}.uk-modal-close-outside{top:0;right:0;-webkit-transform:translate(100%,-100%);transform:translate(100%,-100%);color:#fff}.uk-modal-close-outside:hover{color:#fff}.uk-modal-close-full{top:0;right:0;padding:20px;background:#fff}.uk-modal-caption{position:absolute;left:0;right:0;top:100%;margin-top:20px;color:#fff;text-align:center}.uk-sticky-fixed{z-index:980;box-sizing:border-box;margin:0!important;-webkit-backface-visibility:hidden;backface-visibility:hidden}.uk-sticky[class*=uk-animation-]{-webkit-animation-duration:.2s;animation-duration:.2s}.uk-sticky.uk-animation-reverse{-webkit-animation-duration:.2s;animation-duration:.2s}.uk-offcanvas{display:none;position:fixed;top:0;bottom:0;left:0;z-index:1000}.uk-offcanvas-flip .uk-offcanvas{right:0;left:auto}.uk-offcanvas-page{-webkit-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;overflow-x:hidden}.uk-offcanvas-page-animation{margin-left:270px}.uk-offcanvas-flip.uk-offcanvas-page-animation{margin-left:-270px}.uk-offcanvas-page-overlay{overflow:hidden}.uk-offcanvas-overlay{width:100vw}.uk-offcanvas-overlay::before{content:"";position:absolute;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,.1);opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.uk-offcanvas-overlay.uk-open::before{opacity:1}.uk-offcanvas-bar{position:absolute;top:0;bottom:0;left:0;box-sizing:border-box;width:270px;padding:20px 20px;background:#222;overflow-y:auto;-webkit-overflow-scrolling:touch;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.uk-offcanvas-flip .uk-offcanvas-bar{left:auto;right:0;-webkit-transform:translateX(100%);transform:translateX(100%)}.uk-open>.uk-offcanvas-bar{-webkit-transform:translateX(0);transform:translateX(0)}.uk-offcanvas-bar-animation{-webkit-transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out}.uk-offcanvas-reveal{-webkit-transform:translateX(0);transform:translateX(0);clip:rect(0,0,100vh,0);-webkit-transition:clip .3s ease-in-out;transition:clip .3s ease-in-out}.uk-offcanvas-page:not(.uk-offcanvas-flip) .uk-offcanvas-reveal{-webkit-backface-visibility:hidden;backface-visibility:hidden}.uk-open>.uk-offcanvas-reveal{clip:rect(0,270px,100vh,0)}.uk-offcanvas-flip .uk-offcanvas-reveal{-webkit-transform:translateX(0);transform:translateX(0);clip:rect(0,270px,100vh,270px)}.uk-offcanvas-flip .uk-open>.uk-offcanvas-reveal{clip:rect(0,270px,100vh,0)}.uk-switcher{margin:0;padding:0;list-style:none}.uk-switcher>:not(.uk-active){display:none}.uk-switcher>*>:last-child{margin-bottom:0}.uk-iconnav{display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:0;padding:0;list-style:none;margin-left:-10px}.uk-iconnav>*{-ms-flex:none;-webkit-flex:none;flex:none;padding-left:10px}.uk-iconnav>*>*{display:block;color:#999}.uk-iconnav>*>:focus,.uk-iconnav>*>:hover{color:#666;outline:0}.uk-iconnav>.uk-active>*{color:#666}.uk-iconnav-vertical{-ms-flex-direction:column;-webkit-flex-direction:column;flex-direction:column;margin-left:0;margin-top:-10px}.uk-iconnav-vertical>*{padding-left:0;padding-top:10px}.uk-notification{position:fixed;top:10px;left:10px;z-index:1040;box-sizing:border-box;width:350px}.uk-notification-bottom-right,.uk-notification-top-right{left:auto;right:10px}.uk-notification-bottom-center,.uk-notification-top-center{left:50%;margin-left:-175px}.uk-notification-bottom-center,.uk-notification-bottom-left,.uk-notification-bottom-right{top:auto;bottom:10px}@media (max-width:639px){.uk-notification{left:10px;right:10px;width:auto;margin:0}}.uk-notification-message{position:relative;margin-bottom:10px;padding:15px;background:#f8f8f8;color:#666;font-size:1.25rem;line-height:1.4;cursor:pointer}.uk-notification-close{display:none;position:absolute;top:20px;right:15px}.uk-notification-message:hover .uk-notification-close{display:block}.uk-notification-message-primary{color:#1e87f0}.uk-notification-message-success{color:#32d296}.uk-notification-message-warning{color:#faa05a}.uk-notification-message-danger{color:#f0506e}.uk-tooltip{display:none;position:absolute;z-index:1030;box-sizing:border-box;max-width:200px;padding:3px 6px;background:#666;border-radius:2px;color:#fff;font-size:12px}.uk-tooltip.uk-active{display:block}[class*=uk-tooltip-top]{margin-top:-10px}[class*=uk-tooltip-bottom]{margin-top:10px}[class*=uk-tooltip-left]{margin-left:-10px}[class*=uk-tooltip-right]{margin-left:10px}.uk-placeholder{margin-bottom:20px;padding:30px 30px;background:0 0;border:1px dashed #e5e5e5}*+.uk-placeholder{margin-top:20px}.uk-placeholder>:last-child{margin-bottom:0}.uk-progress{-webkit-appearance:none;-moz-appearance:none;display:block;width:100%;border:0;background-color:#f8f8f8;margin-bottom:20px;height:15px;border-radius:500px;overflow:hidden}*+.uk-progress{margin-top:20px}.uk-progress:indeterminate{color:transparent}.uk-progress::-webkit-progress-bar{background-color:#f8f8f8;border-radius:500px;overflow:hidden}.uk-progress:indeterminate::-moz-progress-bar{width:0}.uk-progress::-webkit-progress-value{background-color:#1e87f0;transition:width .6s ease}.uk-progress::-moz-progress-bar{background-color:#1e87f0}.uk-progress::-ms-fill{background-color:#1e87f0;transition:width .6s ease;border:0}.uk-sortable{position:relative}.uk-sortable>*{touch-action:none}.uk-sortable svg{pointer-events:none}.uk-sortable>:last-child{margin-bottom:0}.uk-sortable-drag{position:absolute!important;z-index:1050!important;pointer-events:none}.uk-sortable-placeholder{opacity:0}.uk-sortable-empty{min-height:50px}.uk-sortable-handle:hover{cursor:move}[class*=uk-animation-]{-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.uk-animation-reverse{-webkit-animation-direction:reverse;animation-direction:reverse;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}.uk-animation-fade{-webkit-animation-name:uk-fade;animation-name:uk-fade;-webkit-animation-duration:.8s;animation-duration:.8s;-webkit-animation-timing-function:linear;animation-timing-function:linear}.uk-animation-scale-up{-webkit-animation-name:uk-fade-scale-02;animation-name:uk-fade-scale-02}.uk-animation-scale-down{-webkit-animation-name:uk-fade-scale-18;animation-name:uk-fade-scale-18}.uk-animation-slide-top{-webkit-animation-name:uk-fade-top;animation-name:uk-fade-top}.uk-animation-slide-bottom{-webkit-animation-name:uk-fade-bottom;animation-name:uk-fade-bottom}.uk-animation-slide-left{-webkit-animation-name:uk-fade-left;animation-name:uk-fade-left}.uk-animation-slide-right{-webkit-animation-name:uk-fade-right;animation-name:uk-fade-right}.uk-animation-slide-top-small{-webkit-animation-name:uk-fade-top-small;animation-name:uk-fade-top-small}.uk-animation-slide-bottom-small{-webkit-animation-name:uk-fade-bottom-small;animation-name:uk-fade-bottom-small}.uk-animation-slide-left-small{-webkit-animation-name:uk-fade-left-small;animation-name:uk-fade-left-small}.uk-animation-slide-right-small{-webkit-animation-name:uk-fade-right-small;animation-name:uk-fade-right-small}.uk-animation-slide-top-medium{-webkit-animation-name:uk-fade-top-medium;animation-name:uk-fade-top-medium}.uk-animation-slide-bottom-medium{-webkit-animation-name:uk-fade-bottom-medium;animation-name:uk-fade-bottom-medium}.uk-animation-slide-left-medium{-webkit-animation-name:uk-fade-left-medium;animation-name:uk-fade-left-medium}.uk-animation-slide-right-medium{-webkit-animation-name:uk-fade-right-medium;animation-name:uk-fade-right-medium}.uk-animation-kenburns{-webkit-animation-name:uk-scale-kenburns;animation-name:uk-scale-kenburns;-webkit-animation-duration:15s;animation-duration:15s}.uk-animation-shake{-webkit-animation-name:uk-shake;animation-name:uk-shake}.uk-animation-fast{-webkit-animation-duration:.1s;animation-duration:.1s}.uk-animation-toggle:not(:hover):not(.uk-hover) [class*=uk-animation-]{-webkit-animation-name:none;animation-name:none}@-webkit-keyframes uk-fade{0%{opacity:0}100%{opacity:1}}@keyframes uk-fade{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes uk-fade-top{0%{opacity:0;-webkit-transform:translateY(-100%)}100%{opacity:1;-webkit-transform:translateY(0)}}@keyframes uk-fade-top{0%{opacity:0;transform:translateY(-100%)}100%{opacity:1;transform:translateY(0)}}@-webkit-keyframes uk-fade-bottom{0%{opacity:0;-webkit-transform:translateY(100%)}100%{opacity:1;-webkit-transform:translateY(0)}}@keyframes uk-fade-bottom{0%{opacity:0;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}@-webkit-keyframes uk-fade-left{0%{opacity:0;-webkit-transform:translateX(-100%)}100%{opacity:1;-webkit-transform:translateX(0)}}@keyframes uk-fade-left{0%{opacity:0;transform:translateX(-100%)}100%{opacity:1;transform:translateX(0)}}@-webkit-keyframes uk-fade-right{0%{opacity:0;-webkit-transform:translateX(100%)}100%{opacity:1;-webkit-transform:translateX(0)}}@keyframes uk-fade-right{0%{opacity:0;transform:translateX(100%)}100%{opacity:1;transform:translateX(0)}}@-webkit-keyframes uk-fade-top-small{0%{opacity:0;-webkit-transform:translateY(-10px)}100%{opacity:1;-webkit-transform:translateY(0)}}@keyframes uk-fade-top-small{0%{opacity:0;transform:translateY(-10px)}100%{opacity:1;transform:translateY(0)}}@-webkit-keyframes uk-fade-bottom-small{0%{opacity:0;-webkit-transform:translateY(10px)}100%{opacity:1;-webkit-transform:translateY(0)}}@keyframes uk-fade-bottom-small{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:translateY(0)}}@-webkit-keyframes uk-fade-left-small{0%{opacity:0;-webkit-transform:translateX(-10px)}100%{opacity:1;-webkit-transform:translateX(0)}}@keyframes uk-fade-left-small{0%{opacity:0;transform:translateX(-10px)}100%{opacity:1;transform:translateX(0)}}@-webkit-keyframes uk-fade-right-small{0%{opacity:0;-webkit-transform:translateX(10px)}100%{opacity:1;-webkit-transform:translateX(0)}}@keyframes uk-fade-right-small{0%{opacity:0;transform:translateX(10px)}100%{opacity:1;transform:translateX(0)}}@-webkit-keyframes uk-fade-top-medium{0%{opacity:0;-webkit-transform:translateY(-50px)}100%{opacity:1;-webkit-transform:translateY(0)}}@keyframes uk-fade-top-medium{0%{opacity:0;transform:translateY(-50px)}100%{opacity:1;transform:translateY(0)}}@-webkit-keyframes uk-fade-bottom-medium{0%{opacity:0;-webkit-transform:translateY(50px)}100%{opacity:1;-webkit-transform:translateY(0)}}@keyframes uk-fade-bottom-medium{0%{opacity:0;transform:translateY(50px)}100%{opacity:1;transform:translateY(0)}}@-webkit-keyframes uk-fade-left-medium{0%{opacity:0;-webkit-transform:translateX(-50px)}100%{opacity:1;-webkit-transform:translateX(0)}}@keyframes uk-fade-left-medium{0%{opacity:0;transform:translateX(-50px)}100%{opacity:1;transform:translateX(0)}}@-webkit-keyframes uk-fade-right-medium{0%{opacity:0;-webkit-transform:translateX(50px)}100%{opacity:1;-webkit-transform:translateX(0)}}@keyframes uk-fade-right-medium{0%{opacity:0;transform:translateX(50px)}100%{opacity:1;transform:translateX(0)}}@-webkit-keyframes uk-fade-scale-02{0%{opacity:0;-webkit-transform:scale(.2)}100%{opacity:1;-webkit-transform:scale(1)}}@keyframes uk-fade-scale-02{0%{opacity:0;transform:scale(.2)}100%{opacity:1;transform:scale(1)}}@-webkit-keyframes uk-fade-scale-18{0%{opacity:0;-webkit-transform:scale(1.8)}100%{opacity:1;-webkit-transform:scale(1)}}@keyframes uk-fade-scale-18{0%{opacity:0;transform:scale(1.8)}100%{opacity:1;transform:scale(1)}}@-webkit-keyframes uk-scale-kenburns{0%{-webkit-transform:scale(1)}100%{-webkit-transform:scale(1.2)}}@keyframes uk-scale-kenburns{0%{transform:scale(1)}100%{transform:scale(1.2)}}@-webkit-keyframes uk-shake{0%,100%{-webkit-transform:translateX(0)}10%{-webkit-transform:translateX(-9px)}20%{-webkit-transform:translateX(8px)}30%{-webkit-transform:translateX(-7px)}40%{-webkit-transform:translateX(6px)}50%{-webkit-transform:translateX(-5px)}60%{-webkit-transform:translateX(4px)}70%{-webkit-transform:translateX(-3px)}80%{-webkit-transform:translateX(2px)}90%{-webkit-transform:translateX(-1px)}}@keyframes uk-shake{0%,100%{transform:translateX(0)}10%{transform:translateX(-9px)}20%{transform:translateX(8px)}30%{transform:translateX(-7px)}40%{transform:translateX(6px)}50%{transform:translateX(-5px)}60%{transform:translateX(4px)}70%{transform:translateX(-3px)}80%{transform:translateX(2px)}90%{transform:translateX(-1px)}}[class*=uk-child-width]>*{box-sizing:border-box;width:100%}.uk-child-width-1-2>*{width:50%}.uk-child-width-1-3>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4>*{width:25%}.uk-child-width-1-5>*{width:20%}.uk-child-width-1-6>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto>*{width:auto}.uk-child-width-expand>*{width:1px}.uk-child-width-expand>:not([class*=uk-width]){-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}@media (min-width:640px){.uk-child-width-1-1\@s>*{width:100%}.uk-child-width-1-2\@s>*{width:50%}.uk-child-width-1-3\@s>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4\@s>*{width:25%}.uk-child-width-1-5\@s>*{width:20%}.uk-child-width-1-6\@s>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto\@s>*{width:auto}.uk-child-width-expand\@s>*{width:1px}.uk-child-width-expand\@s>:not([class*=uk-width]){-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}}@media (min-width:960px){.uk-child-width-1-1\@m>*{width:100%}.uk-child-width-1-2\@m>*{width:50%}.uk-child-width-1-3\@m>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4\@m>*{width:25%}.uk-child-width-1-5\@m>*{width:20%}.uk-child-width-1-6\@m>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto\@m>*{width:auto}.uk-child-width-expand\@m>*{width:1px}.uk-child-width-expand\@m>:not([class*=uk-width]){-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}}@media (min-width:1200px){.uk-child-width-1-1\@l>*{width:100%}.uk-child-width-1-2\@l>*{width:50%}.uk-child-width-1-3\@l>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4\@l>*{width:25%}.uk-child-width-1-5\@l>*{width:20%}.uk-child-width-1-6\@l>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto\@l>*{width:auto}.uk-child-width-expand\@l>*{width:1px}.uk-child-width-expand\@l>:not([class*=uk-width]){-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}}@media (min-width:1600px){.uk-child-width-1-1\@xl>*{width:100%}.uk-child-width-1-2\@xl>*{width:50%}.uk-child-width-1-3\@xl>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4\@xl>*{width:25%}.uk-child-width-1-5\@xl>*{width:20%}.uk-child-width-1-6\@xl>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto\@xl>*{width:auto}.uk-child-width-expand\@xl>*{width:1px}.uk-child-width-expand\@xl>:not([class*=uk-width]){-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}}[class*=uk-width]{box-sizing:border-box;width:100%;max-width:100%}.uk-width-1-2{width:50%}.uk-width-1-3{width:calc(100% * 1 / 3.001)}.uk-width-2-3{width:calc(100% * 2 / 3.001)}.uk-width-1-4{width:25%}.uk-width-3-4{width:75%}.uk-width-1-5{width:20%}.uk-width-2-5{width:40%}.uk-width-3-5{width:60%}.uk-width-4-5{width:80%}.uk-width-1-6{width:calc(100% * 1 / 6.001)}.uk-width-5-6{width:calc(100% * 5 / 6.001)}.uk-width-small{width:150px}.uk-width-medium{width:300px}.uk-width-large{width:450px}.uk-width-xlarge{width:600px}.uk-width-xxlarge{width:750px}.uk-width-auto{width:auto}.uk-width-expand{width:1px;-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}@media (min-width:640px){.uk-width-1-1\@s{width:100%}.uk-width-1-2\@s{width:50%}.uk-width-1-3\@s{width:calc(100% * 1 / 3.001)}.uk-width-2-3\@s{width:calc(100% * 2 / 3.001)}.uk-width-1-4\@s{width:25%}.uk-width-3-4\@s{width:75%}.uk-width-1-5\@s{width:20%}.uk-width-2-5\@s{width:40%}.uk-width-3-5\@s{width:60%}.uk-width-4-5\@s{width:80%}.uk-width-1-6\@s{width:calc(100% * 1 / 6.001)}.uk-width-5-6\@s{width:calc(100% * 5 / 6.001)}.uk-width-small\@s{width:150px}.uk-width-medium\@s{width:300px}.uk-width-large\@s{width:450px}.uk-width-xlarge\@s{width:600px}.uk-width-xxlarge\@s{width:750px}.uk-width-auto\@s{width:auto}.uk-width-expand\@s{width:1px;-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}}@media (min-width:960px){.uk-width-1-1\@m{width:100%}.uk-width-1-2\@m{width:50%}.uk-width-1-3\@m{width:calc(100% * 1 / 3.001)}.uk-width-2-3\@m{width:calc(100% * 2 / 3.001)}.uk-width-1-4\@m{width:25%}.uk-width-3-4\@m{width:75%}.uk-width-1-5\@m{width:20%}.uk-width-2-5\@m{width:40%}.uk-width-3-5\@m{width:60%}.uk-width-4-5\@m{width:80%}.uk-width-1-6\@m{width:calc(100% * 1 / 6.001)}.uk-width-5-6\@m{width:calc(100% * 5 / 6.001)}.uk-width-small\@m{width:150px}.uk-width-medium\@m{width:300px}.uk-width-large\@m{width:450px}.uk-width-xlarge\@m{width:600px}.uk-width-xxlarge\@m{width:750px}.uk-width-auto\@m{width:auto}.uk-width-expand\@m{width:1px;-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}}@media (min-width:1200px){.uk-width-1-1\@l{width:100%}.uk-width-1-2\@l{width:50%}.uk-width-1-3\@l{width:calc(100% * 1 / 3.001)}.uk-width-2-3\@l{width:calc(100% * 2 / 3.001)}.uk-width-1-4\@l{width:25%}.uk-width-3-4\@l{width:75%}.uk-width-1-5\@l{width:20%}.uk-width-2-5\@l{width:40%}.uk-width-3-5\@l{width:60%}.uk-width-4-5\@l{width:80%}.uk-width-1-6\@l{width:calc(100% * 1 / 6.001)}.uk-width-5-6\@l{width:calc(100% * 5 / 6.001)}.uk-width-small\@l{width:150px}.uk-width-medium\@l{width:300px}.uk-width-large\@l{width:450px}.uk-width-xlarge\@l{width:600px}.uk-width-xxlarge\@l{width:750px}.uk-width-auto\@l{width:auto}.uk-width-expand\@l{width:1px;-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}}@media (min-width:1600px){.uk-width-1-1\@xl{width:100%}.uk-width-1-2\@xl{width:50%}.uk-width-1-3\@xl{width:calc(100% * 1 / 3.001)}.uk-width-2-3\@xl{width:calc(100% * 2 / 3.001)}.uk-width-1-4\@xl{width:25%}.uk-width-3-4\@xl{width:75%}.uk-width-1-5\@xl{width:20%}.uk-width-2-5\@xl{width:40%}.uk-width-3-5\@xl{width:60%}.uk-width-4-5\@xl{width:80%}.uk-width-1-6\@xl{width:calc(100% * 1 / 6.001)}.uk-width-5-6\@xl{width:calc(100% * 5 / 6.001)}.uk-width-small\@xl{width:150px}.uk-width-medium\@xl{width:300px}.uk-width-large\@xl{width:450px}.uk-width-xlarge\@xl{width:600px}.uk-width-xxlarge\@xl{width:750px}.uk-width-auto\@xl{width:auto}.uk-width-expand\@xl{width:1px;-ms-flex:1;-webkit-flex:1;flex:1;min-width:0;flex-basis:1px}}.uk-text-lead{font-size:1.5rem;line-height:1.5;color:#333}.uk-text-meta{font-size:.875rem;line-height:1.4;color:#999}.uk-text-meta a{color:#999}.uk-text-meta a:hover{color:#666;text-decoration:none}.uk-text-small{font-size:.875rem;line-height:1.5}.uk-text-large{font-size:1.5rem;line-height:1.5}.uk-text-bold{font-weight:bolder}.uk-text-uppercase{text-transform:uppercase!important}.uk-text-capitalize{text-transform:capitalize!important}.uk-text-lowercase{text-transform:lowercase!important}.uk-text-muted{color:#999!important}.uk-text-primary{color:#1e87f0!important}.uk-text-success{color:#32d296!important}.uk-text-warning{color:#faa05a!important}.uk-text-danger{color:#f0506e!important}.uk-text-background{-webkit-background-clip:text;-webkit-text-fill-color:transparent;display:inline-block;color:#1e87f0!important}.uk-text-left{text-align:left!important}.uk-text-right{text-align:right!important}.uk-text-center{text-align:center!important}.uk-text-justify{text-align:justify!important}@media (min-width:640px){.uk-text-left\@s{text-align:left!important}.uk-text-right\@s{text-align:right!important}.uk-text-center\@s{text-align:center!important}}@media (min-width:960px){.uk-text-left\@m{text-align:left!important}.uk-text-right\@m{text-align:right!important}.uk-text-center\@m{text-align:center!important}}@media (min-width:1200px){.uk-text-left\@l{text-align:left!important}.uk-text-right\@l{text-align:right!important}.uk-text-center\@l{text-align:center!important}}@media (min-width:1600px){.uk-text-left\@xl{text-align:left!important}.uk-text-right\@xl{text-align:right!important}.uk-text-center\@xl{text-align:center!important}}.uk-text-top{vertical-align:top!important}.uk-text-middle{vertical-align:middle!important}.uk-text-bottom{vertical-align:bottom!important}.uk-text-baseline{vertical-align:baseline!important}.uk-text-nowrap{white-space:nowrap}.uk-text-truncate{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}td.uk-text-truncate,th.uk-text-truncate{max-width:0}.uk-text-break{overflow-wrap:break-word;word-wrap:break-word;-webkit-hyphens:auto;-ms-hyphens:auto;-moz-hyphens:auto;hyphens:auto}td.uk-text-break,th.uk-text-break{word-break:break-all}[class*=uk-column-]{-webkit-column-gap:30px;-moz-column-gap:30px;column-gap:30px}@media (min-width:1200px){[class*=uk-column-]{-webkit-column-gap:40px;-moz-column-gap:40px;column-gap:40px}}[class*=uk-column-] img{transform:translate3d(0,0,0)}.uk-column-divider{-webkit-column-rule:1px solid #e5e5e5;-moz-column-rule:1px solid #e5e5e5;column-rule:1px solid #e5e5e5;-webkit-column-gap:60px;-moz-column-gap:60px;column-gap:60px}@media (min-width:1200px){.uk-column-divider{-webkit-column-gap:80px;-moz-column-gap:80px;column-gap:80px}}.uk-column-1-2{-webkit-column-count:2;-moz-column-count:2;column-count:2}.uk-column-1-3{-webkit-column-count:3;-moz-column-count:3;column-count:3}.uk-column-1-4{-webkit-column-count:4;-moz-column-count:4;column-count:4}.uk-column-1-5{-webkit-column-count:5;-moz-column-count:5;column-count:5}.uk-column-1-6{-webkit-column-count:6;-moz-column-count:6;column-count:6}@media (min-width:640px){.uk-column-1-2\@s{-webkit-column-count:2;-moz-column-count:2;column-count:2}.uk-column-1-3\@s{-webkit-column-count:3;-moz-column-count:3;column-count:3}.uk-column-1-4\@s{-webkit-column-count:4;-moz-column-count:4;column-count:4}.uk-column-1-5\@s{-webkit-column-count:5;-moz-column-count:5;column-count:5}.uk-column-1-6\@s{-webkit-column-count:6;-moz-column-count:6;column-count:6}}@media (min-width:960px){.uk-column-1-2\@m{-webkit-column-count:2;-moz-column-count:2;column-count:2}.uk-column-1-3\@m{-webkit-column-count:3;-moz-column-count:3;column-count:3}.uk-column-1-4\@m{-webkit-column-count:4;-moz-column-count:4;column-count:4}.uk-column-1-5\@m{-webkit-column-count:5;-moz-column-count:5;column-count:5}.uk-column-1-6\@m{-webkit-column-count:6;-moz-column-count:6;column-count:6}}@media (min-width:1200px){.uk-column-1-2\@l{-webkit-column-count:2;-moz-column-count:2;column-count:2}.uk-column-1-3\@l{-webkit-column-count:3;-moz-column-count:3;column-count:3}.uk-column-1-4\@l{-webkit-column-count:4;-moz-column-count:4;column-count:4}.uk-column-1-5\@l{-webkit-column-count:5;-moz-column-count:5;column-count:5}.uk-column-1-6\@l{-webkit-column-count:6;-moz-column-count:6;column-count:6}}@media (min-width:1600px){.uk-column-1-2\@xl{-webkit-column-count:2;-moz-column-count:2;column-count:2}.uk-column-1-3\@xl{-webkit-column-count:3;-moz-column-count:3;column-count:3}.uk-column-1-4\@xl{-webkit-column-count:4;-moz-column-count:4;column-count:4}.uk-column-1-5\@xl{-webkit-column-count:5;-moz-column-count:5;column-count:5}.uk-column-1-6\@xl{-webkit-column-count:6;-moz-column-count:6;column-count:6}}.uk-column-span{-webkit-column-span:all;-moz-column-span:all;column-span:all}.uk-cover{max-width:none;position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.uk-cover-container{overflow:hidden;position:relative}.uk-background{background-color:#fff}.uk-background-muted{background-color:#f8f8f8}.uk-background-primary{background-color:#1e87f0}.uk-background-secondary{background-color:#222}.uk-background-contain,.uk-background-cover{background-position:50% 50%;background-repeat:no-repeat}.uk-background-cover{background-size:cover}.uk-background-contain{background-size:contain}.uk-background-top-left{background-position:0 0}.uk-background-top-center{background-position:50% 0}.uk-background-top-right{background-position:100% 0}.uk-background-center-left{background-position:0 50%}.uk-background-center-center{background-position:50% 50%}.uk-background-center-right{background-position:100% 50%}.uk-background-bottom-left{background-position:0 100%}.uk-background-bottom-center{background-position:50% 100%}.uk-background-bottom-right{background-position:100% 100%}.uk-background-norepeat{background-repeat:no-repeat}.uk-background-fixed{background-attachment:fixed}@media (pointer:coarse){.uk-background-fixed{background-attachment:scroll}}@media (max-width:639px){.uk-background-image\@s{background-image:none!important}}@media (max-width:959px){.uk-background-image\@m{background-image:none!important}}@media (max-width:1199px){.uk-background-image\@l{background-image:none!important}}@media (max-width:1599px){.uk-background-image\@xl{background-image:none!important}}.uk-background-blend-multiply{background-blend-mode:multiply}.uk-background-blend-screen{background-blend-mode:screen}.uk-background-blend-overlay{background-blend-mode:overlay}.uk-background-blend-darken{background-blend-mode:darken}.uk-background-blend-lighten{background-blend-mode:lighten}.uk-background-blend-color-dodge{background-blend-mode:color-dodge}.uk-background-blend-color-burn{background-blend-mode:color-burn}.uk-background-blend-hard-light{background-blend-mode:hard-light}.uk-background-blend-soft-light{background-blend-mode:soft-light}.uk-background-blend-difference{background-blend-mode:difference}.uk-background-blend-exclusion{background-blend-mode:exclusion}.uk-background-blend-hue{background-blend-mode:hue}.uk-background-blend-saturation{background-blend-mode:saturation}.uk-background-blend-color{background-blend-mode:color}.uk-background-blend-luminosity{background-blend-mode:luminosity}[class*=uk-align]{display:block;margin-bottom:30px}*+[class*=uk-align]{margin-top:30px}.uk-align-center{margin-left:auto;margin-right:auto}.uk-align-left{margin-top:0;margin-right:30px;float:left}.uk-align-right{margin-top:0;margin-left:30px;float:right}@media (min-width:640px){.uk-align-left\@s{margin-top:0;margin-right:30px;float:left}.uk-align-right\@s{margin-top:0;margin-left:30px;float:right}}@media (min-width:960px){.uk-align-left\@m{margin-top:0;margin-right:30px;float:left}.uk-align-right\@m{margin-top:0;margin-left:30px;float:right}}@media (min-width:1200px){.uk-align-left\@l{margin-top:0;float:left}.uk-align-right\@l{margin-top:0;float:right}.uk-align-left,.uk-align-left\@l,.uk-align-left\@m,.uk-align-left\@s{margin-right:40px}.uk-align-right,.uk-align-right\@l,.uk-align-right\@m,.uk-align-right\@s{margin-left:40px}}@media (min-width:1600px){.uk-align-left\@xl{margin-top:0;margin-right:40px;float:left}.uk-align-right\@xl{margin-top:0;margin-left:40px;float:right}}.uk-panel{position:relative;box-sizing:border-box}.uk-panel::after,.uk-panel::before{content:"";display:table}.uk-panel::after{clear:both}.uk-panel>:last-child{margin-bottom:0}.uk-panel-scrollable{height:170px;padding:10px;border:1px solid #e5e5e5;overflow:auto;-webkit-overflow-scrolling:touch;resize:both}.uk-clearfix::before{content:"";display:table-cell}.uk-clearfix::after{content:"";display:table;clear:both}.uk-float-left{float:left}.uk-float-right{float:right}[class*=uk-float-]{max-width:100%}.uk-overflow-hidden{overflow:hidden}.uk-overflow-auto{overflow:auto;-webkit-overflow-scrolling:touch}.uk-overflow-auto>:last-child{margin-bottom:0}.uk-resize{resize:both}.uk-resize-vertical{resize:vertical}.uk-display-block{display:block!important}.uk-display-inline{display:inline!important}.uk-display-inline-block{display:inline-block!important}[class*=uk-inline]{display:inline-block;position:relative;max-width:100%;vertical-align:middle}.uk-inline-clip{overflow:hidden}[class*=uk-height]{box-sizing:border-box}.uk-height-1-1{height:100%}.uk-height-viewport{min-height:100vh}.uk-height-small{height:150px}.uk-height-medium{height:300px}.uk-height-large{height:450px}.uk-height-max-small{max-height:150px}.uk-height-max-medium{max-height:300px}.uk-height-max-large{max-height:450px}.uk-preserve-width,.uk-preserve-width audio,.uk-preserve-width canvas,.uk-preserve-width img,.uk-preserve-width svg,.uk-preserve-width video{max-width:none}.uk-responsive-height,.uk-responsive-width{box-sizing:border-box}.uk-responsive-width{max-width:100%!important;height:auto}.uk-responsive-height{max-height:100%;width:auto;max-width:none}.uk-border-circle{border-radius:50%}.uk-border-rounded{border-radius:5px}.uk-box-shadow-small{box-shadow:0 2px 8px rgba(0,0,0,.08)}.uk-box-shadow-medium{box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-box-shadow-large{box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-box-shadow-xlarge{box-shadow:0 28px 50px rgba(0,0,0,.16)}[class*=uk-box-shadow-hover]{-webkit-transition:box-shadow .1s ease-in-out;transition:box-shadow .1s ease-in-out}.uk-box-shadow-hover-small:hover{box-shadow:0 2px 8px rgba(0,0,0,.08)}.uk-box-shadow-hover-medium:hover{box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-box-shadow-hover-large:hover{box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-box-shadow-hover-xlarge:hover{box-shadow:0 28px 50px rgba(0,0,0,.16)}.uk-dropcap::first-letter,.uk-dropcap>p:first-of-type::first-letter{display:block;margin-right:10px;float:left;font-size:4.5em;line-height:1;margin-bottom:-2px}.uk-logo{font-size:1.5rem;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#666;text-decoration:none}.uk-logo:focus,.uk-logo:hover{color:#666;outline:0;text-decoration:none}.uk-logo-inverse{display:none}.uk-svg,.uk-svg:not(.uk-preserve) [fill*='#']:not(.uk-preserve){fill:currentcolor}.uk-svg:not(.uk-preserve) [stroke*='#']:not(.uk-preserve){stroke:currentcolor}.uk-svg{transform:translate(0,0)}.uk-disabled{pointer-events:none}.uk-drag,.uk-drag *{cursor:move}.uk-drag iframe{pointer-events:none}.uk-dragover{box-shadow:0 0 20px rgba(100,100,100,.3)}.uk-blend-multiply{mix-blend-mode:multiply}.uk-blend-screen{mix-blend-mode:screen}.uk-blend-overlay{mix-blend-mode:overlay}.uk-blend-darken{mix-blend-mode:darken}.uk-blend-lighten{mix-blend-mode:lighten}.uk-blend-color-dodge{mix-blend-mode:color-dodge}.uk-blend-color-burn{mix-blend-mode:color-burn}.uk-blend-hard-light{mix-blend-mode:hard-light}.uk-blend-soft-light{mix-blend-mode:soft-light}.uk-blend-difference{mix-blend-mode:difference}.uk-blend-exclusion{mix-blend-mode:exclusion}.uk-blend-hue{mix-blend-mode:hue}.uk-blend-saturation{mix-blend-mode:saturation}.uk-blend-color{mix-blend-mode:color}.uk-blend-luminosity{mix-blend-mode:luminosity}.uk-transform-origin-top-left{-webkit-transform-origin:0 0;transform-origin:0 0}.uk-transform-origin-top-center{-webkit-transform-origin:50% 0;transform-origin:50% 0}.uk-transform-origin-top-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.uk-transform-origin-center-left{-webkit-transform-origin:0 50%;transform-origin:0 50%}.uk-transform-origin-center-right{-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.uk-transform-origin-bottom-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.uk-transform-origin-bottom-center{-webkit-transform-origin:50% 100%;transform-origin:50% 100%}.uk-transform-origin-bottom-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.uk-flex{display:-ms-flexbox;display:-webkit-flex;display:flex}.uk-flex-inline{display:-ms-inline-flexbox;display:-webkit-inline-flex;display:inline-flex}.uk-flex-inline::after,.uk-flex-inline::before,.uk-flex::after,.uk-flex::before{display:none}.uk-flex-left{-ms-flex-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.uk-flex-center{-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center}.uk-flex-right{-ms-flex-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.uk-flex-between{-ms-flex-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.uk-flex-around{-ms-flex-pack:distribute;-webkit-justify-content:space-around;justify-content:space-around}.uk-flex-stretch{-ms-flex-align:stretch;-webkit-align-items:stretch;align-items:stretch}.uk-flex-top{-ms-flex-align:start;-webkit-align-items:flex-start;align-items:flex-start}.uk-flex-middle{-ms-flex-align:center;-webkit-align-items:center;align-items:center}.uk-flex-bottom{-ms-flex-align:end;-webkit-align-items:flex-end;align-items:flex-end}.uk-flex-row{-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row}.uk-flex-row-reverse{-ms-flex-direction:row-reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse}.uk-flex-column{-ms-flex-direction:column;-webkit-flex-direction:column;flex-direction:column}.uk-flex-column-reverse{-ms-flex-direction:column-reverse;-webkit-flex-direction:column-reverse;flex-direction:column-reverse}.uk-flex-nowrap{-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap}.uk-flex-wrap{-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap}.uk-flex-wrap-reverse{-ms-flex-wrap:wrap-reverse;-webkit-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse}.uk-flex-wrap-stretch{-ms-flex-line-pack:stretch;-webkit-align-content:stretch;align-content:stretch}.uk-flex-wrap-top{-ms-flex-line-pack:start;-webkit-align-content:flex-start;align-content:flex-start}.uk-flex-wrap-middle{-ms-flex-line-pack:center;-webkit-align-content:center;align-content:center}.uk-flex-wrap-bottom{-ms-flex-line-pack:end;-webkit-align-content:flex-end;align-content:flex-end}.uk-flex-wrap-between{-ms-flex-line-pack:justify;-webkit-align-content:space-between;align-content:space-between}.uk-flex-wrap-around{-ms-flex-line-pack:distribute;-webkit-align-content:space-around;align-content:space-around}.uk-flex-first{-ms-flex-order:-1;-webkit-order:-1;order:-1}.uk-flex-last{-ms-flex-order:99;-webkit-order:99;order:99}@media (min-width:640px){.uk-flex-first\@s{-ms-flex-order:-1;-webkit-order:-1;order:-1}.uk-flex-last\@s{-ms-flex-order:99;-webkit-order:99;order:99}}@media (min-width:960px){.uk-flex-first\@m{-ms-flex-order:-1;-webkit-order:-1;order:-1}.uk-flex-last\@m{-ms-flex-order:99;-webkit-order:99;order:99}}@media (min-width:1200px){.uk-flex-first\@l{-ms-flex-order:-1;-webkit-order:-1;order:-1}.uk-flex-last\@l{-ms-flex-order:99;-webkit-order:99;order:99}}@media (min-width:1600px){.uk-flex-first\@xl{-ms-flex-order:-1;-webkit-order:-1;order:-1}.uk-flex-last\@xl{-ms-flex-order:99;-webkit-order:99;order:99}}.uk-flex-none{-ms-flex:none;-webkit-flex:none;flex:none}.uk-flex-auto{-ms-flex:auto;-webkit-flex:auto;flex:auto}.uk-flex-1{-ms-flex:1;-webkit-flex:1;flex:1}.uk-margin{margin-bottom:20px}*+.uk-margin{margin-top:20px!important}.uk-margin-top{margin-top:20px!important}.uk-margin-bottom{margin-bottom:20px!important}.uk-margin-left{margin-left:20px!important}.uk-margin-right{margin-right:20px!important}.uk-margin-small{margin-bottom:10px}*+.uk-margin-small{margin-top:10px!important}.uk-margin-small-top{margin-top:10px!important}.uk-margin-small-bottom{margin-bottom:10px!important}.uk-margin-small-left{margin-left:10px!important}.uk-margin-small-right{margin-right:10px!important}.uk-margin-medium{margin-bottom:40px}*+.uk-margin-medium{margin-top:40px!important}.uk-margin-medium-top{margin-top:40px!important}.uk-margin-medium-bottom{margin-bottom:40px!important}.uk-margin-medium-left{margin-left:40px!important}.uk-margin-medium-right{margin-right:40px!important}.uk-margin-large{margin-bottom:40px}*+.uk-margin-large{margin-top:40px!important}.uk-margin-large-top{margin-top:40px!important}.uk-margin-large-bottom{margin-bottom:40px!important}.uk-margin-large-left{margin-left:40px!important}.uk-margin-large-right{margin-right:40px!important}@media (min-width:1200px){.uk-margin-large{margin-bottom:70px}*+.uk-margin-large{margin-top:70px!important}.uk-margin-large-top{margin-top:70px!important}.uk-margin-large-bottom{margin-bottom:70px!important}.uk-margin-large-left{margin-left:70px!important}.uk-margin-large-right{margin-right:70px!important}}.uk-margin-xlarge{margin-bottom:70px}*+.uk-margin-xlarge{margin-top:70px!important}.uk-margin-xlarge-top{margin-top:70px!important}.uk-margin-xlarge-bottom{margin-bottom:70px!important}.uk-margin-xlarge-left{margin-left:70px!important}.uk-margin-xlarge-right{margin-right:70px!important}@media (min-width:1200px){.uk-margin-xlarge{margin-bottom:140px}*+.uk-margin-xlarge{margin-top:140px!important}.uk-margin-xlarge-top{margin-top:140px!important}.uk-margin-xlarge-bottom{margin-bottom:140px!important}.uk-margin-xlarge-left{margin-left:140px!important}.uk-margin-xlarge-right{margin-right:140px!important}}.uk-margin-remove{margin:0!important}.uk-margin-remove-top{margin-top:0!important}.uk-margin-remove-bottom{margin-bottom:0!important}.uk-margin-remove-left{margin-left:0!important}.uk-margin-remove-right{margin-right:0!important}.uk-margin-remove-vertical{margin-top:0!important;margin-bottom:0!important}.uk-margin-remove-adjacent+*{margin-top:0!important}.uk-margin-auto{margin-left:auto!important;margin-right:auto!important}.uk-margin-auto-left{margin-left:auto!important}.uk-margin-auto-right{margin-right:auto!important}.uk-margin-auto-vertical{margin-top:auto!important;margin-bottom:auto!important}.uk-padding{padding:30px}@media (min-width:1200px){.uk-padding{padding:40px}}.uk-padding-small{padding:15px}.uk-padding-large{padding:30px}@media (min-width:1200px){.uk-padding-large{padding:70px}}.uk-padding-remove{padding:0!important}.uk-padding-remove-top{padding-top:0!important}.uk-padding-remove-bottom{padding-bottom:0!important}.uk-padding-remove-vertical{padding-top:0!important;padding-bottom:0!important}.uk-padding-remove-horizontal{padding-left:0!important;padding-right:0!important}[class*=uk-position-bottom],[class*=uk-position-center],[class*=uk-position-left],[class*=uk-position-right],[class*=uk-position-top]{position:absolute!important}.uk-position-top{top:0;left:0;right:0}.uk-position-bottom{bottom:0;left:0;right:0}.uk-position-left{top:0;bottom:0;left:0}.uk-position-right{top:0;bottom:0;right:0}.uk-position-top-left{top:0;left:0}.uk-position-top-right{top:0;right:0}.uk-position-bottom-left{bottom:0;left:0}.uk-position-bottom-right{bottom:0;right:0}.uk-position-center{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);display:table}.uk-position-center-left,.uk-position-center-right{top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.uk-position-center-left{left:0}.uk-position-center-right{right:0}.uk-position-bottom-center,.uk-position-top-center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);display:table}.uk-position-top-center{top:0}.uk-position-bottom-center{bottom:0}.uk-position-cover{position:absolute;top:0;bottom:0;left:0;right:0}.uk-position-relative{position:relative!important}.uk-position-absolute{position:absolute!important}.uk-position-fixed{position:fixed!important}.uk-position-z-index{z-index:1}.uk-position-small{margin:15px}.uk-position-small.uk-position-center{-webkit-transform:translate(calc(-50% - 15px),calc(-50% - 15px));transform:translate(calc(-50% - 15px),calc(-50% - 15px))}.uk-position-small.uk-position-center-left,.uk-position-small.uk-position-center-right{-webkit-transform:translateY(calc(-50% - 15px));transform:translateY(calc(-50% - 15px))}.uk-position-small.uk-position-bottom-center,.uk-position-small.uk-position-top-center{-webkit-transform:translateX(calc(-50% - 15px));transform:translateX(calc(-50% - 15px))}.uk-position-medium{margin:30px}.uk-position-medium.uk-position-center{-webkit-transform:translate(calc(-50% - 30px),calc(-50% - 30px));transform:translate(calc(-50% - 30px),calc(-50% - 30px))}.uk-position-medium.uk-position-center-left,.uk-position-medium.uk-position-center-right{-webkit-transform:translateY(calc(-50% - 30px));transform:translateY(calc(-50% - 30px))}.uk-position-medium.uk-position-bottom-center,.uk-position-medium.uk-position-top-center{-webkit-transform:translateX(calc(-50% - 30px));transform:translateX(calc(-50% - 30px))}.uk-transition-fade,[class*=uk-transition-scale],[class*=uk-transition-slide]{transition-duration:.3s;transition-timing-function:ease-out;transition-property:opacity,transform,filter}.uk-transition-fade{opacity:0}.uk-transition-toggle.uk-hover [class*=uk-transition-fade],.uk-transition-toggle:hover [class*=uk-transition-fade]{opacity:1}[class*=uk-transition-scale]{opacity:0}.uk-transition-scale-up{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}.uk-transition-toggle.uk-hover .uk-transition-scale-up,.uk-transition-toggle:hover .uk-transition-scale-up{opacity:1;-webkit-transform:scale3d(1.1,1.1,1);transform:scale3d(1.1,1.1,1)}.uk-transition-scale-down{-webkit-transform:scale3d(1.1,1.1,1);transform:scale3d(1.1,1.1,1)}.uk-transition-toggle.uk-hover .uk-transition-scale-down,.uk-transition-toggle:hover .uk-transition-scale-down{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}[class*=uk-transition-slide]{opacity:0}.uk-transition-slide-top{-webkit-transform:translateY(-100%);transform:translateY(-100%)}.uk-transition-slide-bottom{-webkit-transform:translateY(100%);transform:translateY(100%)}.uk-transition-slide-left{-webkit-transform:translateX(-100%);transform:translateX(-100%)}.uk-transition-slide-right{-webkit-transform:translateX(100%);transform:translateX(100%)}.uk-transition-slide-top-small{-webkit-transform:translateY(-10px);transform:translateY(-10px)}.uk-transition-slide-bottom-small{-webkit-transform:translateY(10px);transform:translateY(10px)}.uk-transition-slide-left-small{-webkit-transform:translateX(-10px);transform:translateX(-10px)}.uk-transition-slide-right-small{-webkit-transform:translateX(10px);transform:translateX(10px)}.uk-transition-slide-top-medium{-webkit-transform:translateY(-50px);transform:translateY(-50px)}.uk-transition-slide-bottom-medium{-webkit-transform:translateY(50px);transform:translateY(50px)}.uk-transition-slide-left-medium{-webkit-transform:translateX(-50px);transform:translateX(-50px)}.uk-transition-slide-right-medium{-webkit-transform:translateX(50px);transform:translateX(50px)}.uk-transition-toggle.uk-hover [class*=uk-transition-slide],.uk-transition-toggle:hover [class*=uk-transition-slide]{opacity:1;-webkit-transform:translateX(0) translateY(0);transform:translateX(0) translateY(0)}.uk-transition-opaque{opacity:1}.uk-transition-slow{transition-duration:.7s}.uk-hidden,[hidden]{display:none!important}@media (min-width:640px){.uk-hidden\@s{display:none!important}}@media (min-width:960px){.uk-hidden\@m{display:none!important}}@media (min-width:1200px){.uk-hidden\@l{display:none!important}}@media (min-width:1600px){.uk-hidden\@xl{display:none!important}}@media (max-width:639px){.uk-visible\@s{display:none!important}}@media (max-width:959px){.uk-visible\@m{display:none!important}}@media (max-width:1199px){.uk-visible\@l{display:none!important}}@media (max-width:1599px){.uk-visible\@xl{display:none!important}}.uk-invisible{visibility:hidden!important}.uk-visible-toggle:not(:hover):not(.uk-hover) .uk-hidden-hover{display:none!important}.uk-visible-toggle:not(:hover):not(.uk-hover) .uk-invisible-hover{visibility:hidden!important}.uk-card-primary.uk-card-body,.uk-card-primary>:not([class*=uk-card-media]),.uk-card-secondary.uk-card-body,.uk-card-secondary>:not([class*=uk-card-media]),.uk-light,.uk-offcanvas-bar,.uk-overlay-primary,.uk-section-primary:not(.uk-preserve-color),.uk-section-secondary:not(.uk-preserve-color){color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-link,.uk-card-primary.uk-card-body a,.uk-card-primary>:not([class*=uk-card-media]) .uk-link,.uk-card-primary>:not([class*=uk-card-media]) a,.uk-card-secondary.uk-card-body .uk-link,.uk-card-secondary.uk-card-body a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link,.uk-card-secondary>:not([class*=uk-card-media]) a,.uk-light .uk-link,.uk-light a,.uk-offcanvas-bar .uk-link,.uk-offcanvas-bar a,.uk-overlay-primary .uk-link,.uk-overlay-primary a,.uk-section-primary:not(.uk-preserve-color) .uk-link,.uk-section-primary:not(.uk-preserve-color) a,.uk-section-secondary:not(.uk-preserve-color) .uk-link,.uk-section-secondary:not(.uk-preserve-color) a{color:#fff}.uk-card-primary.uk-card-body .uk-link:hover,.uk-card-primary.uk-card-body a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-link:hover,.uk-card-primary>:not([class*=uk-card-media]) a:hover,.uk-card-secondary.uk-card-body .uk-link:hover,.uk-card-secondary.uk-card-body a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link:hover,.uk-card-secondary>:not([class*=uk-card-media]) a:hover,.uk-light .uk-link:hover,.uk-light a:hover,.uk-offcanvas-bar .uk-link:hover,.uk-offcanvas-bar a:hover,.uk-overlay-primary .uk-link:hover,.uk-overlay-primary a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-link:hover,.uk-section-primary:not(.uk-preserve-color) a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-link:hover,.uk-section-secondary:not(.uk-preserve-color) a:hover{color:#fff}.uk-card-primary.uk-card-body :not(pre)>code,.uk-card-primary.uk-card-body :not(pre)>kbd,.uk-card-primary.uk-card-body :not(pre)>samp,.uk-card-primary>:not([class*=uk-card-media]) :not(pre)>code,.uk-card-primary>:not([class*=uk-card-media]) :not(pre)>kbd,.uk-card-primary>:not([class*=uk-card-media]) :not(pre)>samp,.uk-card-secondary.uk-card-body :not(pre)>code,.uk-card-secondary.uk-card-body :not(pre)>kbd,.uk-card-secondary.uk-card-body :not(pre)>samp,.uk-card-secondary>:not([class*=uk-card-media]) :not(pre)>code,.uk-card-secondary>:not([class*=uk-card-media]) :not(pre)>kbd,.uk-card-secondary>:not([class*=uk-card-media]) :not(pre)>samp,.uk-light :not(pre)>code,.uk-light :not(pre)>kbd,.uk-light :not(pre)>samp,.uk-offcanvas-bar :not(pre)>code,.uk-offcanvas-bar :not(pre)>kbd,.uk-offcanvas-bar :not(pre)>samp,.uk-overlay-primary :not(pre)>code,.uk-overlay-primary :not(pre)>kbd,.uk-overlay-primary :not(pre)>samp,.uk-section-primary:not(.uk-preserve-color) :not(pre)>code,.uk-section-primary:not(.uk-preserve-color) :not(pre)>kbd,.uk-section-primary:not(.uk-preserve-color) :not(pre)>samp,.uk-section-secondary:not(.uk-preserve-color) :not(pre)>code,.uk-section-secondary:not(.uk-preserve-color) :not(pre)>kbd,.uk-section-secondary:not(.uk-preserve-color) :not(pre)>samp{color:rgba(255,255,255,.7);background:rgba(255,255,255,.1)}.uk-card-primary.uk-card-body em,.uk-card-primary>:not([class*=uk-card-media]) em,.uk-card-secondary.uk-card-body em,.uk-card-secondary>:not([class*=uk-card-media]) em,.uk-light em,.uk-offcanvas-bar em,.uk-overlay-primary em,.uk-section-primary:not(.uk-preserve-color) em,.uk-section-secondary:not(.uk-preserve-color) em{color:#fff}.uk-card-primary.uk-card-body .uk-h1,.uk-card-primary.uk-card-body .uk-h2,.uk-card-primary.uk-card-body .uk-h3,.uk-card-primary.uk-card-body .uk-h4,.uk-card-primary.uk-card-body .uk-h5,.uk-card-primary.uk-card-body .uk-h6,.uk-card-primary.uk-card-body h1,.uk-card-primary.uk-card-body h2,.uk-card-primary.uk-card-body h3,.uk-card-primary.uk-card-body h4,.uk-card-primary.uk-card-body h5,.uk-card-primary.uk-card-body h6,.uk-card-primary>:not([class*=uk-card-media]) .uk-h1,.uk-card-primary>:not([class*=uk-card-media]) .uk-h2,.uk-card-primary>:not([class*=uk-card-media]) .uk-h3,.uk-card-primary>:not([class*=uk-card-media]) .uk-h4,.uk-card-primary>:not([class*=uk-card-media]) .uk-h5,.uk-card-primary>:not([class*=uk-card-media]) .uk-h6,.uk-card-primary>:not([class*=uk-card-media]) h1,.uk-card-primary>:not([class*=uk-card-media]) h2,.uk-card-primary>:not([class*=uk-card-media]) h3,.uk-card-primary>:not([class*=uk-card-media]) h4,.uk-card-primary>:not([class*=uk-card-media]) h5,.uk-card-primary>:not([class*=uk-card-media]) h6,.uk-card-secondary.uk-card-body .uk-h1,.uk-card-secondary.uk-card-body .uk-h2,.uk-card-secondary.uk-card-body .uk-h3,.uk-card-secondary.uk-card-body .uk-h4,.uk-card-secondary.uk-card-body .uk-h5,.uk-card-secondary.uk-card-body .uk-h6,.uk-card-secondary.uk-card-body h1,.uk-card-secondary.uk-card-body h2,.uk-card-secondary.uk-card-body h3,.uk-card-secondary.uk-card-body h4,.uk-card-secondary.uk-card-body h5,.uk-card-secondary.uk-card-body h6,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h1,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h2,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h3,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h4,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h5,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h6,.uk-card-secondary>:not([class*=uk-card-media]) h1,.uk-card-secondary>:not([class*=uk-card-media]) h2,.uk-card-secondary>:not([class*=uk-card-media]) h3,.uk-card-secondary>:not([class*=uk-card-media]) h4,.uk-card-secondary>:not([class*=uk-card-media]) h5,.uk-card-secondary>:not([class*=uk-card-media]) h6,.uk-light .uk-h1,.uk-light .uk-h2,.uk-light .uk-h3,.uk-light .uk-h4,.uk-light .uk-h5,.uk-light .uk-h6,.uk-light h1,.uk-light h2,.uk-light h3,.uk-light h4,.uk-light h5,.uk-light h6,.uk-offcanvas-bar .uk-h1,.uk-offcanvas-bar .uk-h2,.uk-offcanvas-bar .uk-h3,.uk-offcanvas-bar .uk-h4,.uk-offcanvas-bar .uk-h5,.uk-offcanvas-bar .uk-h6,.uk-offcanvas-bar h1,.uk-offcanvas-bar h2,.uk-offcanvas-bar h3,.uk-offcanvas-bar h4,.uk-offcanvas-bar h5,.uk-offcanvas-bar h6,.uk-overlay-primary .uk-h1,.uk-overlay-primary .uk-h2,.uk-overlay-primary .uk-h3,.uk-overlay-primary .uk-h4,.uk-overlay-primary .uk-h5,.uk-overlay-primary .uk-h6,.uk-overlay-primary h1,.uk-overlay-primary h2,.uk-overlay-primary h3,.uk-overlay-primary h4,.uk-overlay-primary h5,.uk-overlay-primary h6,.uk-section-primary:not(.uk-preserve-color) .uk-h1,.uk-section-primary:not(.uk-preserve-color) .uk-h2,.uk-section-primary:not(.uk-preserve-color) .uk-h3,.uk-section-primary:not(.uk-preserve-color) .uk-h4,.uk-section-primary:not(.uk-preserve-color) .uk-h5,.uk-section-primary:not(.uk-preserve-color) .uk-h6,.uk-section-primary:not(.uk-preserve-color) h1,.uk-section-primary:not(.uk-preserve-color) h2,.uk-section-primary:not(.uk-preserve-color) h3,.uk-section-primary:not(.uk-preserve-color) h4,.uk-section-primary:not(.uk-preserve-color) h5,.uk-section-primary:not(.uk-preserve-color) h6,.uk-section-secondary:not(.uk-preserve-color) .uk-h1,.uk-section-secondary:not(.uk-preserve-color) .uk-h2,.uk-section-secondary:not(.uk-preserve-color) .uk-h3,.uk-section-secondary:not(.uk-preserve-color) .uk-h4,.uk-section-secondary:not(.uk-preserve-color) .uk-h5,.uk-section-secondary:not(.uk-preserve-color) .uk-h6,.uk-section-secondary:not(.uk-preserve-color) h1,.uk-section-secondary:not(.uk-preserve-color) h2,.uk-section-secondary:not(.uk-preserve-color) h3,.uk-section-secondary:not(.uk-preserve-color) h4,.uk-section-secondary:not(.uk-preserve-color) h5,.uk-section-secondary:not(.uk-preserve-color) h6{color:#fff}.uk-card-primary.uk-card-body blockquote,.uk-card-primary>:not([class*=uk-card-media]) blockquote,.uk-card-secondary.uk-card-body blockquote,.uk-card-secondary>:not([class*=uk-card-media]) blockquote,.uk-light blockquote,.uk-offcanvas-bar blockquote,.uk-overlay-primary blockquote,.uk-section-primary:not(.uk-preserve-color) blockquote,.uk-section-secondary:not(.uk-preserve-color) blockquote{color:#fff}.uk-card-primary.uk-card-body blockquote footer,.uk-card-primary>:not([class*=uk-card-media]) blockquote footer,.uk-card-secondary.uk-card-body blockquote footer,.uk-card-secondary>:not([class*=uk-card-media]) blockquote footer,.uk-light blockquote footer,.uk-offcanvas-bar blockquote footer,.uk-overlay-primary blockquote footer,.uk-section-primary:not(.uk-preserve-color) blockquote footer,.uk-section-secondary:not(.uk-preserve-color) blockquote footer{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body hr,.uk-card-primary>:not([class*=uk-card-media]) hr,.uk-card-secondary.uk-card-body hr,.uk-card-secondary>:not([class*=uk-card-media]) hr,.uk-light hr,.uk-offcanvas-bar hr,.uk-overlay-primary hr,.uk-section-primary:not(.uk-preserve-color) hr,.uk-section-secondary:not(.uk-preserve-color) hr{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-link-muted a,.uk-card-primary.uk-card-body a.uk-link-muted,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-muted a,.uk-card-primary>:not([class*=uk-card-media]) a.uk-link-muted,.uk-card-secondary.uk-card-body .uk-link-muted a,.uk-card-secondary.uk-card-body a.uk-link-muted,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-muted a,.uk-card-secondary>:not([class*=uk-card-media]) a.uk-link-muted,.uk-light .uk-link-muted a,.uk-light a.uk-link-muted,.uk-offcanvas-bar .uk-link-muted a,.uk-offcanvas-bar a.uk-link-muted,.uk-overlay-primary .uk-link-muted a,.uk-overlay-primary a.uk-link-muted,.uk-section-primary:not(.uk-preserve-color) .uk-link-muted a,.uk-section-primary:not(.uk-preserve-color) a.uk-link-muted,.uk-section-secondary:not(.uk-preserve-color) .uk-link-muted a,.uk-section-secondary:not(.uk-preserve-color) a.uk-link-muted{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-link-muted a:hover,.uk-card-primary.uk-card-body a.uk-link-muted:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-muted a:hover,.uk-card-primary>:not([class*=uk-card-media]) a.uk-link-muted:hover,.uk-card-secondary.uk-card-body .uk-link-muted a:hover,.uk-card-secondary.uk-card-body a.uk-link-muted:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-muted a:hover,.uk-card-secondary>:not([class*=uk-card-media]) a.uk-link-muted:hover,.uk-light .uk-link-muted a:hover,.uk-light a.uk-link-muted:hover,.uk-offcanvas-bar .uk-link-muted a:hover,.uk-offcanvas-bar a.uk-link-muted:hover,.uk-overlay-primary .uk-link-muted a:hover,.uk-overlay-primary a.uk-link-muted:hover,.uk-section-primary:not(.uk-preserve-color) .uk-link-muted a:hover,.uk-section-primary:not(.uk-preserve-color) a.uk-link-muted:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-link-muted a:hover,.uk-section-secondary:not(.uk-preserve-color) a.uk-link-muted:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-heading-divider,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-divider,.uk-card-secondary.uk-card-body .uk-heading-divider,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-divider,.uk-light .uk-heading-divider,.uk-offcanvas-bar .uk-heading-divider,.uk-overlay-primary .uk-heading-divider,.uk-section-primary:not(.uk-preserve-color) .uk-heading-divider,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-divider{border-bottom-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-heading-bullet::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-bullet::before,.uk-card-secondary.uk-card-body .uk-heading-bullet::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-bullet::before,.uk-light .uk-heading-bullet::before,.uk-offcanvas-bar .uk-heading-bullet::before,.uk-overlay-primary .uk-heading-bullet::before,.uk-section-primary:not(.uk-preserve-color) .uk-heading-bullet::before,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-bullet::before{border-left-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-heading-line>:after,.uk-card-primary.uk-card-body .uk-heading-line>:before,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-line>:after,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-line>:before,.uk-card-secondary.uk-card-body .uk-heading-line>:after,.uk-card-secondary.uk-card-body .uk-heading-line>:before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-line>:after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-line>:before,.uk-light .uk-heading-line>:after,.uk-light .uk-heading-line>:before,.uk-offcanvas-bar .uk-heading-line>:after,.uk-offcanvas-bar .uk-heading-line>:before,.uk-overlay-primary .uk-heading-line>:after,.uk-overlay-primary .uk-heading-line>:before,.uk-section-primary:not(.uk-preserve-color) .uk-heading-line>:after,.uk-section-primary:not(.uk-preserve-color) .uk-heading-line>:before,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-line>:after,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-line>:before{border-bottom-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-divider-icon,.uk-card-primary>:not([class*=uk-card-media]) .uk-divider-icon,.uk-card-secondary.uk-card-body .uk-divider-icon,.uk-card-secondary>:not([class*=uk-card-media]) .uk-divider-icon,.uk-light .uk-divider-icon,.uk-offcanvas-bar .uk-divider-icon,.uk-overlay-primary .uk-divider-icon,.uk-section-primary:not(.uk-preserve-color) .uk-divider-icon,.uk-section-secondary:not(.uk-preserve-color) .uk-divider-icon{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22none%22%20stroke%3D%22rgba%28255,%20255,%20255,%200.2%29%22%20stroke-width%3D%222%22%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%227%22%3E%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A")}.uk-card-primary.uk-card-body .uk-divider-icon::after,.uk-card-primary.uk-card-body .uk-divider-icon::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-divider-icon::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-divider-icon::before,.uk-card-secondary.uk-card-body .uk-divider-icon::after,.uk-card-secondary.uk-card-body .uk-divider-icon::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-divider-icon::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-divider-icon::before,.uk-light .uk-divider-icon::after,.uk-light .uk-divider-icon::before,.uk-offcanvas-bar .uk-divider-icon::after,.uk-offcanvas-bar .uk-divider-icon::before,.uk-overlay-primary .uk-divider-icon::after,.uk-overlay-primary .uk-divider-icon::before,.uk-section-primary:not(.uk-preserve-color) .uk-divider-icon::after,.uk-section-primary:not(.uk-preserve-color) .uk-divider-icon::before,.uk-section-secondary:not(.uk-preserve-color) .uk-divider-icon::after,.uk-section-secondary:not(.uk-preserve-color) .uk-divider-icon::before{border-bottom-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-divider-small::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-divider-small::after,.uk-card-secondary.uk-card-body .uk-divider-small::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-divider-small::after,.uk-light .uk-divider-small::after,.uk-offcanvas-bar .uk-divider-small::after,.uk-overlay-primary .uk-divider-small::after,.uk-section-primary:not(.uk-preserve-color) .uk-divider-small::after,.uk-section-secondary:not(.uk-preserve-color) .uk-divider-small::after{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-list-divider>li:nth-child(n+2),.uk-card-primary>:not([class*=uk-card-media]) .uk-list-divider>li:nth-child(n+2),.uk-card-secondary.uk-card-body .uk-list-divider>li:nth-child(n+2),.uk-card-secondary>:not([class*=uk-card-media]) .uk-list-divider>li:nth-child(n+2),.uk-light .uk-list-divider>li:nth-child(n+2),.uk-offcanvas-bar .uk-list-divider>li:nth-child(n+2),.uk-overlay-primary .uk-list-divider>li:nth-child(n+2),.uk-section-primary:not(.uk-preserve-color) .uk-list-divider>li:nth-child(n+2),.uk-section-secondary:not(.uk-preserve-color) .uk-list-divider>li:nth-child(n+2){border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-list-striped>li,.uk-card-primary>:not([class*=uk-card-media]) .uk-list-striped>li,.uk-card-secondary.uk-card-body .uk-list-striped>li,.uk-card-secondary>:not([class*=uk-card-media]) .uk-list-striped>li,.uk-light .uk-list-striped>li,.uk-offcanvas-bar .uk-list-striped>li,.uk-overlay-primary .uk-list-striped>li,.uk-section-primary:not(.uk-preserve-color) .uk-list-striped>li,.uk-section-secondary:not(.uk-preserve-color) .uk-list-striped>li{border-bottom-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-list-striped>li:first-child,.uk-card-primary>:not([class*=uk-card-media]) .uk-list-striped>li:first-child,.uk-card-secondary.uk-card-body .uk-list-striped>li:first-child,.uk-card-secondary>:not([class*=uk-card-media]) .uk-list-striped>li:first-child,.uk-light .uk-list-striped>li:first-child,.uk-offcanvas-bar .uk-list-striped>li:first-child,.uk-overlay-primary .uk-list-striped>li:first-child,.uk-section-primary:not(.uk-preserve-color) .uk-list-striped>li:first-child,.uk-section-secondary:not(.uk-preserve-color) .uk-list-striped>li:first-child{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-list-striped>li:nth-of-type(odd),.uk-card-primary>:not([class*=uk-card-media]) .uk-list-striped>li:nth-of-type(odd),.uk-card-secondary.uk-card-body .uk-list-striped>li:nth-of-type(odd),.uk-card-secondary>:not([class*=uk-card-media]) .uk-list-striped>li:nth-of-type(odd),.uk-light .uk-list-striped>li:nth-of-type(odd),.uk-offcanvas-bar .uk-list-striped>li:nth-of-type(odd),.uk-overlay-primary .uk-list-striped>li:nth-of-type(odd),.uk-section-primary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd),.uk-section-secondary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd){background-color:rgba(255,255,255,.1)}.uk-card-primary.uk-card-body .uk-list-bullet>li::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-list-bullet>li::before,.uk-card-secondary.uk-card-body .uk-list-bullet>li::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-list-bullet>li::before,.uk-light .uk-list-bullet>li::before,.uk-offcanvas-bar .uk-list-bullet>li::before,.uk-overlay-primary .uk-list-bullet>li::before,.uk-section-primary:not(.uk-preserve-color) .uk-list-bullet>li::before,.uk-section-secondary:not(.uk-preserve-color) .uk-list-bullet>li::before{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20cx%3D%223%22%20cy%3D%223%22%20r%3D%223%22%3E%3C%2Fcircle%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-icon-link,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-link,.uk-card-secondary.uk-card-body .uk-icon-link,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-link,.uk-light .uk-icon-link,.uk-offcanvas-bar .uk-icon-link,.uk-overlay-primary .uk-icon-link,.uk-section-primary:not(.uk-preserve-color) .uk-icon-link,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-link{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-icon-link:focus,.uk-card-primary.uk-card-body .uk-icon-link:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-link:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-link:hover,.uk-card-secondary.uk-card-body .uk-icon-link:focus,.uk-card-secondary.uk-card-body .uk-icon-link:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-link:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-link:hover,.uk-light .uk-icon-link:focus,.uk-light .uk-icon-link:hover,.uk-offcanvas-bar .uk-icon-link:focus,.uk-offcanvas-bar .uk-icon-link:hover,.uk-overlay-primary .uk-icon-link:focus,.uk-overlay-primary .uk-icon-link:hover,.uk-section-primary:not(.uk-preserve-color) .uk-icon-link:focus,.uk-section-primary:not(.uk-preserve-color) .uk-icon-link:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-link:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-link:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-active>.uk-icon-link,.uk-card-primary.uk-card-body .uk-icon-link:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-active>.uk-icon-link,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-link:active,.uk-card-secondary.uk-card-body .uk-active>.uk-icon-link,.uk-card-secondary.uk-card-body .uk-icon-link:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-active>.uk-icon-link,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-link:active,.uk-light .uk-active>.uk-icon-link,.uk-light .uk-icon-link:active,.uk-offcanvas-bar .uk-active>.uk-icon-link,.uk-offcanvas-bar .uk-icon-link:active,.uk-overlay-primary .uk-active>.uk-icon-link,.uk-overlay-primary .uk-icon-link:active,.uk-section-primary:not(.uk-preserve-color) .uk-active>.uk-icon-link,.uk-section-primary:not(.uk-preserve-color) .uk-icon-link:active,.uk-section-secondary:not(.uk-preserve-color) .uk-active>.uk-icon-link,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-link:active{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-icon-button,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-button,.uk-card-secondary.uk-card-body .uk-icon-button,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-button,.uk-light .uk-icon-button,.uk-offcanvas-bar .uk-icon-button,.uk-overlay-primary .uk-icon-button,.uk-section-primary:not(.uk-preserve-color) .uk-icon-button,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-button{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-icon-button:focus,.uk-card-primary.uk-card-body .uk-icon-button:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-button:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-button:hover,.uk-card-secondary.uk-card-body .uk-icon-button:focus,.uk-card-secondary.uk-card-body .uk-icon-button:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-button:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-button:hover,.uk-light .uk-icon-button:focus,.uk-light .uk-icon-button:hover,.uk-offcanvas-bar .uk-icon-button:focus,.uk-offcanvas-bar .uk-icon-button:hover,.uk-overlay-primary .uk-icon-button:focus,.uk-overlay-primary .uk-icon-button:hover,.uk-section-primary:not(.uk-preserve-color) .uk-icon-button:focus,.uk-section-primary:not(.uk-preserve-color) .uk-icon-button:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-button:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-button:hover{background-color:rgba(242,242,242,.1);color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-icon-button:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-button:active,.uk-card-secondary.uk-card-body .uk-icon-button:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-button:active,.uk-light .uk-icon-button:active,.uk-offcanvas-bar .uk-icon-button:active,.uk-overlay-primary .uk-icon-button:active,.uk-section-primary:not(.uk-preserve-color) .uk-icon-button:active,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-button:active{background-color:rgba(230,230,230,.1);color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-input,.uk-card-primary.uk-card-body .uk-select,.uk-card-primary.uk-card-body .uk-textarea,.uk-card-primary>:not([class*=uk-card-media]) .uk-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-select,.uk-card-primary>:not([class*=uk-card-media]) .uk-textarea,.uk-card-secondary.uk-card-body .uk-input,.uk-card-secondary.uk-card-body .uk-select,.uk-card-secondary.uk-card-body .uk-textarea,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-select,.uk-card-secondary>:not([class*=uk-card-media]) .uk-textarea,.uk-light .uk-input,.uk-light .uk-select,.uk-light .uk-textarea,.uk-offcanvas-bar .uk-input,.uk-offcanvas-bar .uk-select,.uk-offcanvas-bar .uk-textarea,.uk-overlay-primary .uk-input,.uk-overlay-primary .uk-select,.uk-overlay-primary .uk-textarea,.uk-section-primary:not(.uk-preserve-color) .uk-input,.uk-section-primary:not(.uk-preserve-color) .uk-select,.uk-section-primary:not(.uk-preserve-color) .uk-textarea,.uk-section-secondary:not(.uk-preserve-color) .uk-input,.uk-section-secondary:not(.uk-preserve-color) .uk-select,.uk-section-secondary:not(.uk-preserve-color) .uk-textarea{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.7);background-clip:padding-box;border-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-input:focus,.uk-card-primary.uk-card-body .uk-select:focus,.uk-card-primary.uk-card-body .uk-textarea:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-input:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-select:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-textarea:focus,.uk-card-secondary.uk-card-body .uk-input:focus,.uk-card-secondary.uk-card-body .uk-select:focus,.uk-card-secondary.uk-card-body .uk-textarea:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-select:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-textarea:focus,.uk-light .uk-input:focus,.uk-light .uk-select:focus,.uk-light .uk-textarea:focus,.uk-offcanvas-bar .uk-input:focus,.uk-offcanvas-bar .uk-select:focus,.uk-offcanvas-bar .uk-textarea:focus,.uk-overlay-primary .uk-input:focus,.uk-overlay-primary .uk-select:focus,.uk-overlay-primary .uk-textarea:focus,.uk-section-primary:not(.uk-preserve-color) .uk-input:focus,.uk-section-primary:not(.uk-preserve-color) .uk-select:focus,.uk-section-primary:not(.uk-preserve-color) .uk-textarea:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-input:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-select:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-textarea:focus{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.7);border-color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-input:-ms-input-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-input:-ms-input-placeholder,.uk-card-secondary.uk-card-body .uk-input:-ms-input-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input:-ms-input-placeholder,.uk-light .uk-input:-ms-input-placeholder,.uk-offcanvas-bar .uk-input:-ms-input-placeholder,.uk-overlay-primary .uk-input:-ms-input-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-input:-ms-input-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-input:-ms-input-placeholder{color:rgba(255,255,255,.5)!important}.uk-card-primary.uk-card-body .uk-input::-moz-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-input::-moz-placeholder,.uk-card-secondary.uk-card-body .uk-input::-moz-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input::-moz-placeholder,.uk-light .uk-input::-moz-placeholder,.uk-offcanvas-bar .uk-input::-moz-placeholder,.uk-overlay-primary .uk-input::-moz-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-input::-moz-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-input::-moz-placeholder{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-input::-webkit-input-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-input::-webkit-input-placeholder,.uk-card-secondary.uk-card-body .uk-input::-webkit-input-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input::-webkit-input-placeholder,.uk-light .uk-input::-webkit-input-placeholder,.uk-offcanvas-bar .uk-input::-webkit-input-placeholder,.uk-overlay-primary .uk-input::-webkit-input-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-input::-webkit-input-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-input::-webkit-input-placeholder{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-textarea:-ms-input-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-textarea:-ms-input-placeholder,.uk-card-secondary.uk-card-body .uk-textarea:-ms-input-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-textarea:-ms-input-placeholder,.uk-light .uk-textarea:-ms-input-placeholder,.uk-offcanvas-bar .uk-textarea:-ms-input-placeholder,.uk-overlay-primary .uk-textarea:-ms-input-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-textarea:-ms-input-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-textarea:-ms-input-placeholder{color:rgba(255,255,255,.5)!important}.uk-card-primary.uk-card-body .uk-textarea::-moz-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-textarea::-moz-placeholder,.uk-card-secondary.uk-card-body .uk-textarea::-moz-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-textarea::-moz-placeholder,.uk-light .uk-textarea::-moz-placeholder,.uk-offcanvas-bar .uk-textarea::-moz-placeholder,.uk-overlay-primary .uk-textarea::-moz-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-textarea::-moz-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-textarea::-moz-placeholder{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-textarea::-webkit-input-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-textarea::-webkit-input-placeholder,.uk-card-secondary.uk-card-body .uk-textarea::-webkit-input-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-textarea::-webkit-input-placeholder,.uk-light .uk-textarea::-webkit-input-placeholder,.uk-offcanvas-bar .uk-textarea::-webkit-input-placeholder,.uk-overlay-primary .uk-textarea::-webkit-input-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-textarea::-webkit-input-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-textarea::-webkit-input-placeholder{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-select:not([multiple]):not([size]),.uk-card-primary>:not([class*=uk-card-media]) .uk-select:not([multiple]):not([size]),.uk-card-secondary.uk-card-body .uk-select:not([multiple]):not([size]),.uk-card-secondary>:not([class*=uk-card-media]) .uk-select:not([multiple]):not([size]),.uk-light .uk-select:not([multiple]):not([size]),.uk-offcanvas-bar .uk-select:not([multiple]):not([size]),.uk-overlay-primary .uk-select:not([multiple]):not([size]),.uk-section-primary:not(.uk-preserve-color) .uk-select:not([multiple]):not([size]),.uk-section-secondary:not(.uk-preserve-color) .uk-select:not([multiple]):not([size]){background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20points%3D%224%201%201%206%207%206%22%3E%3C%2Fpolygon%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20points%3D%224%2013%201%208%207%208%22%3E%3C%2Fpolygon%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-checkbox,.uk-card-primary.uk-card-body .uk-radio,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio,.uk-card-secondary.uk-card-body .uk-checkbox,.uk-card-secondary.uk-card-body .uk-radio,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio,.uk-light .uk-checkbox,.uk-light .uk-radio,.uk-offcanvas-bar .uk-checkbox,.uk-offcanvas-bar .uk-radio,.uk-overlay-primary .uk-checkbox,.uk-overlay-primary .uk-radio,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox,.uk-section-primary:not(.uk-preserve-color) .uk-radio,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox,.uk-section-secondary:not(.uk-preserve-color) .uk-radio{background-color:rgba(242,242,242,.1);border-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-checkbox:focus,.uk-card-primary.uk-card-body .uk-radio:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio:focus,.uk-card-secondary.uk-card-body .uk-checkbox:focus,.uk-card-secondary.uk-card-body .uk-radio:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio:focus,.uk-light .uk-checkbox:focus,.uk-light .uk-radio:focus,.uk-offcanvas-bar .uk-checkbox:focus,.uk-offcanvas-bar .uk-radio:focus,.uk-overlay-primary .uk-checkbox:focus,.uk-overlay-primary .uk-radio:focus,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:focus,.uk-section-primary:not(.uk-preserve-color) .uk-radio:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-radio:focus{border-color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-checkbox:checked,.uk-card-primary.uk-card-body .uk-checkbox:indeterminate,.uk-card-primary.uk-card-body .uk-radio:checked,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:checked,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio:checked,.uk-card-secondary.uk-card-body .uk-checkbox:checked,.uk-card-secondary.uk-card-body .uk-checkbox:indeterminate,.uk-card-secondary.uk-card-body .uk-radio:checked,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:checked,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio:checked,.uk-light .uk-checkbox:checked,.uk-light .uk-checkbox:indeterminate,.uk-light .uk-radio:checked,.uk-offcanvas-bar .uk-checkbox:checked,.uk-offcanvas-bar .uk-checkbox:indeterminate,.uk-offcanvas-bar .uk-radio:checked,.uk-overlay-primary .uk-checkbox:checked,.uk-overlay-primary .uk-checkbox:indeterminate,.uk-overlay-primary .uk-radio:checked,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-section-primary:not(.uk-preserve-color) .uk-radio:checked,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-section-secondary:not(.uk-preserve-color) .uk-radio:checked{background-color:#fff;border-color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-checkbox:checked:focus,.uk-card-primary.uk-card-body .uk-checkbox:indeterminate:focus,.uk-card-primary.uk-card-body .uk-radio:checked:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:checked:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio:checked:focus,.uk-card-secondary.uk-card-body .uk-checkbox:checked:focus,.uk-card-secondary.uk-card-body .uk-checkbox:indeterminate:focus,.uk-card-secondary.uk-card-body .uk-radio:checked:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:checked:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio:checked:focus,.uk-light .uk-checkbox:checked:focus,.uk-light .uk-checkbox:indeterminate:focus,.uk-light .uk-radio:checked:focus,.uk-offcanvas-bar .uk-checkbox:checked:focus,.uk-offcanvas-bar .uk-checkbox:indeterminate:focus,.uk-offcanvas-bar .uk-radio:checked:focus,.uk-overlay-primary .uk-checkbox:checked:focus,.uk-overlay-primary .uk-checkbox:indeterminate:focus,.uk-overlay-primary .uk-radio:checked:focus,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:checked:focus,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:indeterminate:focus,.uk-section-primary:not(.uk-preserve-color) .uk-radio:checked:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:checked:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:indeterminate:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-radio:checked:focus{background-color:#e6e6e6}.uk-card-primary.uk-card-body .uk-radio:checked,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio:checked,.uk-card-secondary.uk-card-body .uk-radio:checked,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio:checked,.uk-light .uk-radio:checked,.uk-offcanvas-bar .uk-radio:checked,.uk-overlay-primary .uk-radio:checked,.uk-section-primary:not(.uk-preserve-color) .uk-radio:checked,.uk-section-secondary:not(.uk-preserve-color) .uk-radio:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22%23666%22%20cx%3D%228%22%20cy%3D%228%22%20r%3D%222%22%3E%3C%2Fcircle%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-checkbox:checked,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:checked,.uk-card-secondary.uk-card-body .uk-checkbox:checked,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:checked,.uk-light .uk-checkbox:checked,.uk-offcanvas-bar .uk-checkbox:checked,.uk-overlay-primary .uk-checkbox:checked,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2211%22%20viewBox%3D%220%200%2014%2011%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23666%22%20points%3D%2212%201%205%207.5%202%205%201%205.5%205%2010%2013%201.5%22%2F%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-checkbox:indeterminate,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate,.uk-card-secondary.uk-card-body .uk-checkbox:indeterminate,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate,.uk-light .uk-checkbox:indeterminate,.uk-offcanvas-bar .uk-checkbox:indeterminate,.uk-overlay-primary .uk-checkbox:indeterminate,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:indeterminate{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23666%22%20x%3D%223%22%20y%3D%228%22%20width%3D%2210%22%20height%3D%221%22%3E%3C%2Frect%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-form-label,.uk-card-primary>:not([class*=uk-card-media]) .uk-form-label,.uk-card-secondary.uk-card-body .uk-form-label,.uk-card-secondary>:not([class*=uk-card-media]) .uk-form-label,.uk-light .uk-form-label,.uk-offcanvas-bar .uk-form-label,.uk-overlay-primary .uk-form-label,.uk-section-primary:not(.uk-preserve-color) .uk-form-label,.uk-section-secondary:not(.uk-preserve-color) .uk-form-label{color:#fff}.uk-card-primary.uk-card-body .uk-button-default,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default,.uk-card-secondary.uk-card-body .uk-button-default,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default,.uk-light .uk-button-default,.uk-offcanvas-bar .uk-button-default,.uk-overlay-primary .uk-button-default,.uk-section-primary:not(.uk-preserve-color) .uk-button-default,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default{background-color:transparent;color:#fff;border-color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-button-default:focus,.uk-card-primary.uk-card-body .uk-button-default:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default:hover,.uk-card-secondary.uk-card-body .uk-button-default:focus,.uk-card-secondary.uk-card-body .uk-button-default:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default:hover,.uk-light .uk-button-default:focus,.uk-light .uk-button-default:hover,.uk-offcanvas-bar .uk-button-default:focus,.uk-offcanvas-bar .uk-button-default:hover,.uk-overlay-primary .uk-button-default:focus,.uk-overlay-primary .uk-button-default:hover,.uk-section-primary:not(.uk-preserve-color) .uk-button-default:focus,.uk-section-primary:not(.uk-preserve-color) .uk-button-default:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default:hover{background-color:transparent;color:#fff;border-color:#fff}.uk-card-primary.uk-card-body .uk-button-default.uk-active,.uk-card-primary.uk-card-body .uk-button-default:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default.uk-active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default:active,.uk-card-secondary.uk-card-body .uk-button-default.uk-active,.uk-card-secondary.uk-card-body .uk-button-default:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default.uk-active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default:active,.uk-light .uk-button-default.uk-active,.uk-light .uk-button-default:active,.uk-offcanvas-bar .uk-button-default.uk-active,.uk-offcanvas-bar .uk-button-default:active,.uk-overlay-primary .uk-button-default.uk-active,.uk-overlay-primary .uk-button-default:active,.uk-section-primary:not(.uk-preserve-color) .uk-button-default.uk-active,.uk-section-primary:not(.uk-preserve-color) .uk-button-default:active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default.uk-active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default:active{background-color:transparent;color:#fff;border-color:#fff}.uk-card-primary.uk-card-body .uk-button-primary,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary,.uk-card-secondary.uk-card-body .uk-button-primary,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary,.uk-light .uk-button-primary,.uk-offcanvas-bar .uk-button-primary,.uk-overlay-primary .uk-button-primary,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-button-primary:focus,.uk-card-primary.uk-card-body .uk-button-primary:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary:hover,.uk-card-secondary.uk-card-body .uk-button-primary:focus,.uk-card-secondary.uk-card-body .uk-button-primary:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary:hover,.uk-light .uk-button-primary:focus,.uk-light .uk-button-primary:hover,.uk-offcanvas-bar .uk-button-primary:focus,.uk-offcanvas-bar .uk-button-primary:hover,.uk-overlay-primary .uk-button-primary:focus,.uk-overlay-primary .uk-button-primary:hover,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary:focus,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary:hover{background-color:#f2f2f2;color:#666}.uk-card-primary.uk-card-body .uk-button-primary.uk-active,.uk-card-primary.uk-card-body .uk-button-primary:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary.uk-active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary:active,.uk-card-secondary.uk-card-body .uk-button-primary.uk-active,.uk-card-secondary.uk-card-body .uk-button-primary:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary.uk-active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary:active,.uk-light .uk-button-primary.uk-active,.uk-light .uk-button-primary:active,.uk-offcanvas-bar .uk-button-primary.uk-active,.uk-offcanvas-bar .uk-button-primary:active,.uk-overlay-primary .uk-button-primary.uk-active,.uk-overlay-primary .uk-button-primary:active,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary.uk-active,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary:active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary.uk-active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary:active{background-color:#e6e6e6;color:#666}.uk-card-primary.uk-card-body .uk-button-secondary,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary,.uk-card-secondary.uk-card-body .uk-button-secondary,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary,.uk-light .uk-button-secondary,.uk-offcanvas-bar .uk-button-secondary,.uk-overlay-primary .uk-button-secondary,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-button-secondary:focus,.uk-card-primary.uk-card-body .uk-button-secondary:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary:hover,.uk-card-secondary.uk-card-body .uk-button-secondary:focus,.uk-card-secondary.uk-card-body .uk-button-secondary:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary:hover,.uk-light .uk-button-secondary:focus,.uk-light .uk-button-secondary:hover,.uk-offcanvas-bar .uk-button-secondary:focus,.uk-offcanvas-bar .uk-button-secondary:hover,.uk-overlay-primary .uk-button-secondary:focus,.uk-overlay-primary .uk-button-secondary:hover,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary:focus,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary:hover{background-color:#f2f2f2;color:#666}.uk-card-primary.uk-card-body .uk-button-secondary.uk-active,.uk-card-primary.uk-card-body .uk-button-secondary:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary.uk-active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary:active,.uk-card-secondary.uk-card-body .uk-button-secondary.uk-active,.uk-card-secondary.uk-card-body .uk-button-secondary:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary.uk-active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary:active,.uk-light .uk-button-secondary.uk-active,.uk-light .uk-button-secondary:active,.uk-offcanvas-bar .uk-button-secondary.uk-active,.uk-offcanvas-bar .uk-button-secondary:active,.uk-overlay-primary .uk-button-secondary.uk-active,.uk-overlay-primary .uk-button-secondary:active,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary.uk-active,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary:active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary.uk-active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary:active{background-color:#e6e6e6;color:#666}.uk-card-primary.uk-card-body .uk-button-text,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text,.uk-card-secondary.uk-card-body .uk-button-text,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text,.uk-light .uk-button-text,.uk-offcanvas-bar .uk-button-text,.uk-overlay-primary .uk-button-text,.uk-section-primary:not(.uk-preserve-color) .uk-button-text,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text{color:#fff}.uk-card-primary.uk-card-body .uk-button-text::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text::before,.uk-card-secondary.uk-card-body .uk-button-text::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text::before,.uk-light .uk-button-text::before,.uk-offcanvas-bar .uk-button-text::before,.uk-overlay-primary .uk-button-text::before,.uk-section-primary:not(.uk-preserve-color) .uk-button-text::before,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text::before{border-bottom-color:#fff}.uk-card-primary.uk-card-body .uk-button-text:focus,.uk-card-primary.uk-card-body .uk-button-text:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text:hover,.uk-card-secondary.uk-card-body .uk-button-text:focus,.uk-card-secondary.uk-card-body .uk-button-text:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text:hover,.uk-light .uk-button-text:focus,.uk-light .uk-button-text:hover,.uk-offcanvas-bar .uk-button-text:focus,.uk-offcanvas-bar .uk-button-text:hover,.uk-overlay-primary .uk-button-text:focus,.uk-overlay-primary .uk-button-text:hover,.uk-section-primary:not(.uk-preserve-color) .uk-button-text:focus,.uk-section-primary:not(.uk-preserve-color) .uk-button-text:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text:hover{color:#fff}.uk-card-primary.uk-card-body .uk-button-text:disabled,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text:disabled,.uk-card-secondary.uk-card-body .uk-button-text:disabled,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text:disabled,.uk-light .uk-button-text:disabled,.uk-offcanvas-bar .uk-button-text:disabled,.uk-overlay-primary .uk-button-text:disabled,.uk-section-primary:not(.uk-preserve-color) .uk-button-text:disabled,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text:disabled{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-grid-divider>:not(.uk-first-column)::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-grid-divider>:not(.uk-first-column)::before,.uk-card-secondary.uk-card-body .uk-grid-divider>:not(.uk-first-column)::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-grid-divider>:not(.uk-first-column)::before,.uk-light .uk-grid-divider>:not(.uk-first-column)::before,.uk-offcanvas-bar .uk-grid-divider>:not(.uk-first-column)::before,.uk-overlay-primary .uk-grid-divider>:not(.uk-first-column)::before,.uk-section-primary:not(.uk-preserve-color) .uk-grid-divider>:not(.uk-first-column)::before,.uk-section-secondary:not(.uk-preserve-color) .uk-grid-divider>:not(.uk-first-column)::before{border-left-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-card-secondary.uk-card-body .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-light .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-offcanvas-bar .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-overlay-primary .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-section-primary:not(.uk-preserve-color) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-section-secondary:not(.uk-preserve-color) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-close,.uk-card-primary>:not([class*=uk-card-media]) .uk-close,.uk-card-secondary.uk-card-body .uk-close,.uk-card-secondary>:not([class*=uk-card-media]) .uk-close,.uk-light .uk-close,.uk-offcanvas-bar .uk-close,.uk-overlay-primary .uk-close,.uk-section-primary:not(.uk-preserve-color) .uk-close,.uk-section-secondary:not(.uk-preserve-color) .uk-close{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-close:focus,.uk-card-primary.uk-card-body .uk-close:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-close:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-close:hover,.uk-card-secondary.uk-card-body .uk-close:focus,.uk-card-secondary.uk-card-body .uk-close:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-close:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-close:hover,.uk-light .uk-close:focus,.uk-light .uk-close:hover,.uk-offcanvas-bar .uk-close:focus,.uk-offcanvas-bar .uk-close:hover,.uk-overlay-primary .uk-close:focus,.uk-overlay-primary .uk-close:hover,.uk-section-primary:not(.uk-preserve-color) .uk-close:focus,.uk-section-primary:not(.uk-preserve-color) .uk-close:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-close:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-close:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-totop,.uk-card-primary>:not([class*=uk-card-media]) .uk-totop,.uk-card-secondary.uk-card-body .uk-totop,.uk-card-secondary>:not([class*=uk-card-media]) .uk-totop,.uk-light .uk-totop,.uk-offcanvas-bar .uk-totop,.uk-overlay-primary .uk-totop,.uk-section-primary:not(.uk-preserve-color) .uk-totop,.uk-section-secondary:not(.uk-preserve-color) .uk-totop{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-totop:focus,.uk-card-primary.uk-card-body .uk-totop:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-totop:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-totop:hover,.uk-card-secondary.uk-card-body .uk-totop:focus,.uk-card-secondary.uk-card-body .uk-totop:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-totop:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-totop:hover,.uk-light .uk-totop:focus,.uk-light .uk-totop:hover,.uk-offcanvas-bar .uk-totop:focus,.uk-offcanvas-bar .uk-totop:hover,.uk-overlay-primary .uk-totop:focus,.uk-overlay-primary .uk-totop:hover,.uk-section-primary:not(.uk-preserve-color) .uk-totop:focus,.uk-section-primary:not(.uk-preserve-color) .uk-totop:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-totop:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-totop:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-totop:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-totop:active,.uk-card-secondary.uk-card-body .uk-totop:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-totop:active,.uk-light .uk-totop:active,.uk-offcanvas-bar .uk-totop:active,.uk-overlay-primary .uk-totop:active,.uk-section-primary:not(.uk-preserve-color) .uk-totop:active,.uk-section-secondary:not(.uk-preserve-color) .uk-totop:active{color:#fff}.uk-card-primary.uk-card-body .uk-badge,.uk-card-primary>:not([class*=uk-card-media]) .uk-badge,.uk-card-secondary.uk-card-body .uk-badge,.uk-card-secondary>:not([class*=uk-card-media]) .uk-badge,.uk-light .uk-badge,.uk-offcanvas-bar .uk-badge,.uk-overlay-primary .uk-badge,.uk-section-primary:not(.uk-preserve-color) .uk-badge,.uk-section-secondary:not(.uk-preserve-color) .uk-badge{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-badge:focus,.uk-card-primary.uk-card-body .uk-badge:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-badge:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-badge:hover,.uk-card-secondary.uk-card-body .uk-badge:focus,.uk-card-secondary.uk-card-body .uk-badge:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-badge:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-badge:hover,.uk-light .uk-badge:focus,.uk-light .uk-badge:hover,.uk-offcanvas-bar .uk-badge:focus,.uk-offcanvas-bar .uk-badge:hover,.uk-overlay-primary .uk-badge:focus,.uk-overlay-primary .uk-badge:hover,.uk-section-primary:not(.uk-preserve-color) .uk-badge:focus,.uk-section-primary:not(.uk-preserve-color) .uk-badge:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-badge:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-badge:hover{color:#666}.uk-card-primary.uk-card-body .uk-label,.uk-card-primary>:not([class*=uk-card-media]) .uk-label,.uk-card-secondary.uk-card-body .uk-label,.uk-card-secondary>:not([class*=uk-card-media]) .uk-label,.uk-light .uk-label,.uk-offcanvas-bar .uk-label,.uk-overlay-primary .uk-label,.uk-section-primary:not(.uk-preserve-color) .uk-label,.uk-section-secondary:not(.uk-preserve-color) .uk-label{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-article-meta,.uk-card-primary>:not([class*=uk-card-media]) .uk-article-meta,.uk-card-secondary.uk-card-body .uk-article-meta,.uk-card-secondary>:not([class*=uk-card-media]) .uk-article-meta,.uk-light .uk-article-meta,.uk-offcanvas-bar .uk-article-meta,.uk-overlay-primary .uk-article-meta,.uk-section-primary:not(.uk-preserve-color) .uk-article-meta,.uk-section-secondary:not(.uk-preserve-color) .uk-article-meta{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-input,.uk-card-secondary.uk-card-body .uk-search-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-input,.uk-light .uk-search-input,.uk-offcanvas-bar .uk-search-input,.uk-overlay-primary .uk-search-input,.uk-section-primary:not(.uk-preserve-color) .uk-search-input,.uk-section-secondary:not(.uk-preserve-color) .uk-search-input{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-search-input:-ms-input-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-input:-ms-input-placeholder,.uk-card-secondary.uk-card-body .uk-search-input:-ms-input-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-input:-ms-input-placeholder,.uk-light .uk-search-input:-ms-input-placeholder,.uk-offcanvas-bar .uk-search-input:-ms-input-placeholder,.uk-overlay-primary .uk-search-input:-ms-input-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-search-input:-ms-input-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-search-input:-ms-input-placeholder{color:rgba(255,255,255,.5)!important}.uk-card-primary.uk-card-body .uk-search-input::-moz-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-input::-moz-placeholder,.uk-card-secondary.uk-card-body .uk-search-input::-moz-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-input::-moz-placeholder,.uk-light .uk-search-input::-moz-placeholder,.uk-offcanvas-bar .uk-search-input::-moz-placeholder,.uk-overlay-primary .uk-search-input::-moz-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-search-input::-moz-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-search-input::-moz-placeholder{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search-input::-webkit-input-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-input::-webkit-input-placeholder,.uk-card-secondary.uk-card-body .uk-search-input::-webkit-input-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-input::-webkit-input-placeholder,.uk-light .uk-search-input::-webkit-input-placeholder,.uk-offcanvas-bar .uk-search-input::-webkit-input-placeholder,.uk-overlay-primary .uk-search-input::-webkit-input-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-search-input::-webkit-input-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-search-input::-webkit-input-placeholder{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search .uk-search-icon,.uk-card-primary>:not([class*=uk-card-media]) .uk-search .uk-search-icon,.uk-card-secondary.uk-card-body .uk-search .uk-search-icon,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search .uk-search-icon,.uk-light .uk-search .uk-search-icon,.uk-offcanvas-bar .uk-search .uk-search-icon,.uk-overlay-primary .uk-search .uk-search-icon,.uk-section-primary:not(.uk-preserve-color) .uk-search .uk-search-icon,.uk-section-secondary:not(.uk-preserve-color) .uk-search .uk-search-icon{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search .uk-search-icon:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-search .uk-search-icon:hover,.uk-card-secondary.uk-card-body .uk-search .uk-search-icon:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search .uk-search-icon:hover,.uk-light .uk-search .uk-search-icon:hover,.uk-offcanvas-bar .uk-search .uk-search-icon:hover,.uk-overlay-primary .uk-search .uk-search-icon:hover,.uk-section-primary:not(.uk-preserve-color) .uk-search .uk-search-icon:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-search .uk-search-icon:hover{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search-default .uk-search-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-default .uk-search-input,.uk-card-secondary.uk-card-body .uk-search-default .uk-search-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-default .uk-search-input,.uk-light .uk-search-default .uk-search-input,.uk-offcanvas-bar .uk-search-default .uk-search-input,.uk-overlay-primary .uk-search-default .uk-search-input,.uk-section-primary:not(.uk-preserve-color) .uk-search-default .uk-search-input,.uk-section-secondary:not(.uk-preserve-color) .uk-search-default .uk-search-input{background-color:transparent;border-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-search-default .uk-search-input:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-default .uk-search-input:focus,.uk-card-secondary.uk-card-body .uk-search-default .uk-search-input:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-default .uk-search-input:focus,.uk-light .uk-search-default .uk-search-input:focus,.uk-offcanvas-bar .uk-search-default .uk-search-input:focus,.uk-overlay-primary .uk-search-default .uk-search-input:focus,.uk-section-primary:not(.uk-preserve-color) .uk-search-default .uk-search-input:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-search-default .uk-search-input:focus{background-color:transparent}.uk-card-primary.uk-card-body .uk-search-navbar .uk-search-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-navbar .uk-search-input,.uk-card-secondary.uk-card-body .uk-search-navbar .uk-search-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-navbar .uk-search-input,.uk-light .uk-search-navbar .uk-search-input,.uk-offcanvas-bar .uk-search-navbar .uk-search-input,.uk-overlay-primary .uk-search-navbar .uk-search-input,.uk-section-primary:not(.uk-preserve-color) .uk-search-navbar .uk-search-input,.uk-section-secondary:not(.uk-preserve-color) .uk-search-navbar .uk-search-input{background-color:transparent}.uk-card-primary.uk-card-body .uk-search-large .uk-search-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-large .uk-search-input,.uk-card-secondary.uk-card-body .uk-search-large .uk-search-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-large .uk-search-input,.uk-light .uk-search-large .uk-search-input,.uk-offcanvas-bar .uk-search-large .uk-search-input,.uk-overlay-primary .uk-search-large .uk-search-input,.uk-section-primary:not(.uk-preserve-color) .uk-search-large .uk-search-input,.uk-section-secondary:not(.uk-preserve-color) .uk-search-large .uk-search-input{background-color:transparent}.uk-card-primary.uk-card-body .uk-search-toggle,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-toggle,.uk-card-secondary.uk-card-body .uk-search-toggle,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-toggle,.uk-light .uk-search-toggle,.uk-offcanvas-bar .uk-search-toggle,.uk-overlay-primary .uk-search-toggle,.uk-section-primary:not(.uk-preserve-color) .uk-search-toggle,.uk-section-secondary:not(.uk-preserve-color) .uk-search-toggle{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search-toggle:focus,.uk-card-primary.uk-card-body .uk-search-toggle:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-toggle:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-toggle:hover,.uk-card-secondary.uk-card-body .uk-search-toggle:focus,.uk-card-secondary.uk-card-body .uk-search-toggle:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-toggle:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-toggle:hover,.uk-light .uk-search-toggle:focus,.uk-light .uk-search-toggle:hover,.uk-offcanvas-bar .uk-search-toggle:focus,.uk-offcanvas-bar .uk-search-toggle:hover,.uk-overlay-primary .uk-search-toggle:focus,.uk-overlay-primary .uk-search-toggle:hover,.uk-section-primary:not(.uk-preserve-color) .uk-search-toggle:focus,.uk-section-primary:not(.uk-preserve-color) .uk-search-toggle:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-search-toggle:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-search-toggle:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-nav-parent-icon>.uk-parent>a::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-parent-icon>.uk-parent>a::after,.uk-card-secondary.uk-card-body .uk-nav-parent-icon>.uk-parent>a::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-parent-icon>.uk-parent>a::after,.uk-light .uk-nav-parent-icon>.uk-parent>a::after,.uk-offcanvas-bar .uk-nav-parent-icon>.uk-parent>a::after,.uk-overlay-primary .uk-nav-parent-icon>.uk-parent>a::after,.uk-section-primary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent>a::after,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent>a::after{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20stroke-width%3D%221.1%22%20points%3D%2210%201%204%207%2010%2013%22%3E%3C%2Fpolyline%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-card-secondary.uk-card-body .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-light .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-offcanvas-bar .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-overlay-primary .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-section-primary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent.uk-open>a::after{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20stroke-width%3D%221.1%22%20points%3D%221%204%207%2010%2013%204%22%3E%3C%2Fpolyline%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-nav-default>li>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default>li>a,.uk-card-secondary.uk-card-body .uk-nav-default>li>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default>li>a,.uk-light .uk-nav-default>li>a,.uk-offcanvas-bar .uk-nav-default>li>a,.uk-overlay-primary .uk-nav-default>li>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default>li>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default>li>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-nav-default>li>a:focus,.uk-card-primary.uk-card-body .uk-nav-default>li>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default>li>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default>li>a:hover,.uk-card-secondary.uk-card-body .uk-nav-default>li>a:focus,.uk-card-secondary.uk-card-body .uk-nav-default>li>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default>li>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default>li>a:hover,.uk-light .uk-nav-default>li>a:focus,.uk-light .uk-nav-default>li>a:hover,.uk-offcanvas-bar .uk-nav-default>li>a:focus,.uk-offcanvas-bar .uk-nav-default>li>a:hover,.uk-overlay-primary .uk-nav-default>li>a:focus,.uk-overlay-primary .uk-nav-default>li>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default>li>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default>li>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default>li>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default>li>a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-nav-default>li.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default>li.uk-active>a,.uk-card-secondary.uk-card-body .uk-nav-default>li.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default>li.uk-active>a,.uk-light .uk-nav-default>li.uk-active>a,.uk-offcanvas-bar .uk-nav-default>li.uk-active>a,.uk-overlay-primary .uk-nav-default>li.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default>li.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default>li.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-header,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-header,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-header,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-header,.uk-light .uk-nav-default .uk-nav-header,.uk-offcanvas-bar .uk-nav-default .uk-nav-header,.uk-overlay-primary .uk-nav-default .uk-nav-header,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-header,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-header{color:#fff}.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-divider,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-divider,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-divider,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-divider,.uk-light .uk-nav-default .uk-nav-divider,.uk-offcanvas-bar .uk-nav-default .uk-nav-divider,.uk-overlay-primary .uk-nav-default .uk-nav-divider,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-divider,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-divider{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-sub a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-sub a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a,.uk-light .uk-nav-default .uk-nav-sub a,.uk-offcanvas-bar .uk-nav-default .uk-nav-sub a,.uk-overlay-primary .uk-nav-default .uk-nav-sub a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-sub a:focus,.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-sub a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a:hover,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-sub a:focus,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-sub a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a:hover,.uk-light .uk-nav-default .uk-nav-sub a:focus,.uk-light .uk-nav-default .uk-nav-sub a:hover,.uk-offcanvas-bar .uk-nav-default .uk-nav-sub a:focus,.uk-offcanvas-bar .uk-nav-default .uk-nav-sub a:hover,.uk-overlay-primary .uk-nav-default .uk-nav-sub a:focus,.uk-overlay-primary .uk-nav-default .uk-nav-sub a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-nav-primary>li>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary>li>a,.uk-card-secondary.uk-card-body .uk-nav-primary>li>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary>li>a,.uk-light .uk-nav-primary>li>a,.uk-offcanvas-bar .uk-nav-primary>li>a,.uk-overlay-primary .uk-nav-primary>li>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary>li>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary>li>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-nav-primary>li>a:focus,.uk-card-primary.uk-card-body .uk-nav-primary>li>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary>li>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary>li>a:hover,.uk-card-secondary.uk-card-body .uk-nav-primary>li>a:focus,.uk-card-secondary.uk-card-body .uk-nav-primary>li>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary>li>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary>li>a:hover,.uk-light .uk-nav-primary>li>a:focus,.uk-light .uk-nav-primary>li>a:hover,.uk-offcanvas-bar .uk-nav-primary>li>a:focus,.uk-offcanvas-bar .uk-nav-primary>li>a:hover,.uk-overlay-primary .uk-nav-primary>li>a:focus,.uk-overlay-primary .uk-nav-primary>li>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary>li>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary>li>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary>li>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary>li>a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-nav-primary>li.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary>li.uk-active>a,.uk-card-secondary.uk-card-body .uk-nav-primary>li.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary>li.uk-active>a,.uk-light .uk-nav-primary>li.uk-active>a,.uk-offcanvas-bar .uk-nav-primary>li.uk-active>a,.uk-overlay-primary .uk-nav-primary>li.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary>li.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary>li.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-header,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-header,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-header,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-header,.uk-light .uk-nav-primary .uk-nav-header,.uk-offcanvas-bar .uk-nav-primary .uk-nav-header,.uk-overlay-primary .uk-nav-primary .uk-nav-header,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-header,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-header{color:#fff}.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-divider,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-divider,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-divider,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-divider,.uk-light .uk-nav-primary .uk-nav-divider,.uk-offcanvas-bar .uk-nav-primary .uk-nav-divider,.uk-overlay-primary .uk-nav-primary .uk-nav-divider,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-divider,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-divider{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-sub a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-sub a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a,.uk-light .uk-nav-primary .uk-nav-sub a,.uk-offcanvas-bar .uk-nav-primary .uk-nav-sub a,.uk-overlay-primary .uk-nav-primary .uk-nav-sub a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-sub a:focus,.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-sub a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a:hover,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-sub a:focus,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-sub a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a:hover,.uk-light .uk-nav-primary .uk-nav-sub a:focus,.uk-light .uk-nav-primary .uk-nav-sub a:hover,.uk-offcanvas-bar .uk-nav-primary .uk-nav-sub a:focus,.uk-offcanvas-bar .uk-nav-primary .uk-nav-sub a:hover,.uk-overlay-primary .uk-nav-primary .uk-nav-sub a:focus,.uk-overlay-primary .uk-nav-primary .uk-nav-sub a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-navbar-nav>li>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a,.uk-card-secondary.uk-card-body .uk-navbar-nav>li>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a,.uk-light .uk-navbar-nav>li>a,.uk-offcanvas-bar .uk-navbar-nav>li>a,.uk-overlay-primary .uk-navbar-nav>li>a,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-navbar-nav>li:hover>a,.uk-card-primary.uk-card-body .uk-navbar-nav>li>a.uk-open,.uk-card-primary.uk-card-body .uk-navbar-nav>li>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li:hover>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a.uk-open,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a:focus,.uk-card-secondary.uk-card-body .uk-navbar-nav>li:hover>a,.uk-card-secondary.uk-card-body .uk-navbar-nav>li>a.uk-open,.uk-card-secondary.uk-card-body .uk-navbar-nav>li>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li:hover>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a.uk-open,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a:focus,.uk-light .uk-navbar-nav>li:hover>a,.uk-light .uk-navbar-nav>li>a.uk-open,.uk-light .uk-navbar-nav>li>a:focus,.uk-offcanvas-bar .uk-navbar-nav>li:hover>a,.uk-offcanvas-bar .uk-navbar-nav>li>a.uk-open,.uk-offcanvas-bar .uk-navbar-nav>li>a:focus,.uk-overlay-primary .uk-navbar-nav>li:hover>a,.uk-overlay-primary .uk-navbar-nav>li>a.uk-open,.uk-overlay-primary .uk-navbar-nav>li>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li:hover>a,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a.uk-open,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li:hover>a,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a.uk-open,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a:focus{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-navbar-nav>li>a:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a:active,.uk-card-secondary.uk-card-body .uk-navbar-nav>li>a:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a:active,.uk-light .uk-navbar-nav>li>a:active,.uk-offcanvas-bar .uk-navbar-nav>li>a:active,.uk-overlay-primary .uk-navbar-nav>li>a:active,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a:active,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a:active{color:#fff}.uk-card-primary.uk-card-body .uk-navbar-nav>li.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li.uk-active>a,.uk-card-secondary.uk-card-body .uk-navbar-nav>li.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li.uk-active>a,.uk-light .uk-navbar-nav>li.uk-active>a,.uk-offcanvas-bar .uk-navbar-nav>li.uk-active>a,.uk-overlay-primary .uk-navbar-nav>li.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-navbar-item,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-item,.uk-card-secondary.uk-card-body .uk-navbar-item,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-item,.uk-light .uk-navbar-item,.uk-offcanvas-bar .uk-navbar-item,.uk-overlay-primary .uk-navbar-item,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-item,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-item{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-navbar-toggle,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-toggle,.uk-card-secondary.uk-card-body .uk-navbar-toggle,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-toggle,.uk-light .uk-navbar-toggle,.uk-offcanvas-bar .uk-navbar-toggle,.uk-overlay-primary .uk-navbar-toggle,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-toggle,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-toggle{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-navbar-toggle.uk-open,.uk-card-primary.uk-card-body .uk-navbar-toggle:focus,.uk-card-primary.uk-card-body .uk-navbar-toggle:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-toggle.uk-open,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-toggle:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-toggle:hover,.uk-card-secondary.uk-card-body .uk-navbar-toggle.uk-open,.uk-card-secondary.uk-card-body .uk-navbar-toggle:focus,.uk-card-secondary.uk-card-body .uk-navbar-toggle:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-toggle.uk-open,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-toggle:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-toggle:hover,.uk-light .uk-navbar-toggle.uk-open,.uk-light .uk-navbar-toggle:focus,.uk-light .uk-navbar-toggle:hover,.uk-offcanvas-bar .uk-navbar-toggle.uk-open,.uk-offcanvas-bar .uk-navbar-toggle:focus,.uk-offcanvas-bar .uk-navbar-toggle:hover,.uk-overlay-primary .uk-navbar-toggle.uk-open,.uk-overlay-primary .uk-navbar-toggle:focus,.uk-overlay-primary .uk-navbar-toggle:hover,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-toggle.uk-open,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-toggle:focus,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-toggle:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-toggle.uk-open,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-toggle:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-toggle:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-subnav>*>:first-child,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>*>:first-child,.uk-card-secondary.uk-card-body .uk-subnav>*>:first-child,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>*>:first-child,.uk-light .uk-subnav>*>:first-child,.uk-offcanvas-bar .uk-subnav>*>:first-child,.uk-overlay-primary .uk-subnav>*>:first-child,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>*>:first-child,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>*>:first-child{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-subnav>*>a:focus,.uk-card-primary.uk-card-body .uk-subnav>*>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>*>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>*>a:hover,.uk-card-secondary.uk-card-body .uk-subnav>*>a:focus,.uk-card-secondary.uk-card-body .uk-subnav>*>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>*>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>*>a:hover,.uk-light .uk-subnav>*>a:focus,.uk-light .uk-subnav>*>a:hover,.uk-offcanvas-bar .uk-subnav>*>a:focus,.uk-offcanvas-bar .uk-subnav>*>a:hover,.uk-overlay-primary .uk-subnav>*>a:focus,.uk-overlay-primary .uk-subnav>*>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>*>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>*>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>*>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>*>a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-subnav>.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>.uk-active>a,.uk-card-secondary.uk-card-body .uk-subnav>.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>.uk-active>a,.uk-light .uk-subnav>.uk-active>a,.uk-offcanvas-bar .uk-subnav>.uk-active>a,.uk-overlay-primary .uk-subnav>.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-secondary.uk-card-body .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-light .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-offcanvas-bar .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-overlay-primary .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before{border-left-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-subnav-pill>*>:first-child,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>*>:first-child,.uk-card-secondary.uk-card-body .uk-subnav-pill>*>:first-child,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>*>:first-child,.uk-light .uk-subnav-pill>*>:first-child,.uk-offcanvas-bar .uk-subnav-pill>*>:first-child,.uk-overlay-primary .uk-subnav-pill>*>:first-child,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>*>:first-child,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>:first-child{background-color:transparent;color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-subnav-pill>*>a:focus,.uk-card-primary.uk-card-body .uk-subnav-pill>*>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:hover,.uk-card-secondary.uk-card-body .uk-subnav-pill>*>a:focus,.uk-card-secondary.uk-card-body .uk-subnav-pill>*>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:hover,.uk-light .uk-subnav-pill>*>a:focus,.uk-light .uk-subnav-pill>*>a:hover,.uk-offcanvas-bar .uk-subnav-pill>*>a:focus,.uk-offcanvas-bar .uk-subnav-pill>*>a:hover,.uk-overlay-primary .uk-subnav-pill>*>a:focus,.uk-overlay-primary .uk-subnav-pill>*>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>*>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>*>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>a:hover{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-subnav-pill>*>a:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:active,.uk-card-secondary.uk-card-body .uk-subnav-pill>*>a:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:active,.uk-light .uk-subnav-pill>*>a:active,.uk-offcanvas-bar .uk-subnav-pill>*>a:active,.uk-overlay-primary .uk-subnav-pill>*>a:active,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>*>a:active,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>a:active{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-subnav-pill>.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>.uk-active>a,.uk-card-secondary.uk-card-body .uk-subnav-pill>.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>.uk-active>a,.uk-light .uk-subnav-pill>.uk-active>a,.uk-offcanvas-bar .uk-subnav-pill>.uk-active>a,.uk-overlay-primary .uk-subnav-pill>.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>.uk-active>a{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-subnav>.uk-disabled>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>.uk-disabled>a,.uk-card-secondary.uk-card-body .uk-subnav>.uk-disabled>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>.uk-disabled>a,.uk-light .uk-subnav>.uk-disabled>a,.uk-offcanvas-bar .uk-subnav>.uk-disabled>a,.uk-overlay-primary .uk-subnav>.uk-disabled>a,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>.uk-disabled>a,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>.uk-disabled>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-breadcrumb>*>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>*>*,.uk-card-secondary.uk-card-body .uk-breadcrumb>*>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>*>*,.uk-light .uk-breadcrumb>*>*,.uk-offcanvas-bar .uk-breadcrumb>*>*,.uk-overlay-primary .uk-breadcrumb>*>*,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>*>*,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>*>*{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-breadcrumb>*>:focus,.uk-card-primary.uk-card-body .uk-breadcrumb>*>:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>*>:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>*>:hover,.uk-card-secondary.uk-card-body .uk-breadcrumb>*>:focus,.uk-card-secondary.uk-card-body .uk-breadcrumb>*>:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>*>:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>*>:hover,.uk-light .uk-breadcrumb>*>:focus,.uk-light .uk-breadcrumb>*>:hover,.uk-offcanvas-bar .uk-breadcrumb>*>:focus,.uk-offcanvas-bar .uk-breadcrumb>*>:hover,.uk-overlay-primary .uk-breadcrumb>*>:focus,.uk-overlay-primary .uk-breadcrumb>*>:hover,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>*>:focus,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>*>:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>*>:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>*>:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-breadcrumb>:last-child>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>:last-child>*,.uk-card-secondary.uk-card-body .uk-breadcrumb>:last-child>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>:last-child>*,.uk-light .uk-breadcrumb>:last-child>*,.uk-offcanvas-bar .uk-breadcrumb>:last-child>*,.uk-overlay-primary .uk-breadcrumb>:last-child>*,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>:last-child>*,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>:last-child>*{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-secondary.uk-card-body .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-light .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-offcanvas-bar .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-overlay-primary .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-pagination>*>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>*>*,.uk-card-secondary.uk-card-body .uk-pagination>*>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>*>*,.uk-light .uk-pagination>*>*,.uk-offcanvas-bar .uk-pagination>*>*,.uk-overlay-primary .uk-pagination>*>*,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>*>*,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>*>*{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-pagination>*>:focus,.uk-card-primary.uk-card-body .uk-pagination>*>:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>*>:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>*>:hover,.uk-card-secondary.uk-card-body .uk-pagination>*>:focus,.uk-card-secondary.uk-card-body .uk-pagination>*>:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>*>:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>*>:hover,.uk-light .uk-pagination>*>:focus,.uk-light .uk-pagination>*>:hover,.uk-offcanvas-bar .uk-pagination>*>:focus,.uk-offcanvas-bar .uk-pagination>*>:hover,.uk-overlay-primary .uk-pagination>*>:focus,.uk-overlay-primary .uk-pagination>*>:hover,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>*>:focus,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>*>:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>*>:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>*>:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-pagination>.uk-active>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>.uk-active>*,.uk-card-secondary.uk-card-body .uk-pagination>.uk-active>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>.uk-active>*,.uk-light .uk-pagination>.uk-active>*,.uk-offcanvas-bar .uk-pagination>.uk-active>*,.uk-overlay-primary .uk-pagination>.uk-active>*,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>.uk-active>*,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>.uk-active>*{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-pagination>.uk-disabled>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>.uk-disabled>*,.uk-card-secondary.uk-card-body .uk-pagination>.uk-disabled>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>.uk-disabled>*,.uk-light .uk-pagination>.uk-disabled>*,.uk-offcanvas-bar .uk-pagination>.uk-disabled>*,.uk-overlay-primary .uk-pagination>.uk-disabled>*,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>.uk-disabled>*,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>.uk-disabled>*{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-tab::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab::before,.uk-card-secondary.uk-card-body .uk-tab::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab::before,.uk-light .uk-tab::before,.uk-offcanvas-bar .uk-tab::before,.uk-overlay-primary .uk-tab::before,.uk-section-primary:not(.uk-preserve-color) .uk-tab::before,.uk-section-secondary:not(.uk-preserve-color) .uk-tab::before{border-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-tab>*>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>*>a,.uk-card-secondary.uk-card-body .uk-tab>*>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>*>a,.uk-light .uk-tab>*>a,.uk-offcanvas-bar .uk-tab>*>a,.uk-overlay-primary .uk-tab>*>a,.uk-section-primary:not(.uk-preserve-color) .uk-tab>*>a,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>*>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-tab>*>a:focus,.uk-card-primary.uk-card-body .uk-tab>*>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>*>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>*>a:hover,.uk-card-secondary.uk-card-body .uk-tab>*>a:focus,.uk-card-secondary.uk-card-body .uk-tab>*>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>*>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>*>a:hover,.uk-light .uk-tab>*>a:focus,.uk-light .uk-tab>*>a:hover,.uk-offcanvas-bar .uk-tab>*>a:focus,.uk-offcanvas-bar .uk-tab>*>a:hover,.uk-overlay-primary .uk-tab>*>a:focus,.uk-overlay-primary .uk-tab>*>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-tab>*>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-tab>*>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>*>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>*>a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-tab>.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>.uk-active>a,.uk-card-secondary.uk-card-body .uk-tab>.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>.uk-active>a,.uk-light .uk-tab>.uk-active>a,.uk-offcanvas-bar .uk-tab>.uk-active>a,.uk-overlay-primary .uk-tab>.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-tab>.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>.uk-active>a{color:#fff;border-color:#fff}.uk-card-primary.uk-card-body .uk-tab>.uk-disabled>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>.uk-disabled>a,.uk-card-secondary.uk-card-body .uk-tab>.uk-disabled>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>.uk-disabled>a,.uk-light .uk-tab>.uk-disabled>a,.uk-offcanvas-bar .uk-tab>.uk-disabled>a,.uk-overlay-primary .uk-tab>.uk-disabled>a,.uk-section-primary:not(.uk-preserve-color) .uk-tab>.uk-disabled>a,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>.uk-disabled>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-slidenav,.uk-card-primary>:not([class*=uk-card-media]) .uk-slidenav,.uk-card-secondary.uk-card-body .uk-slidenav,.uk-card-secondary>:not([class*=uk-card-media]) .uk-slidenav,.uk-light .uk-slidenav,.uk-offcanvas-bar .uk-slidenav,.uk-overlay-primary .uk-slidenav,.uk-section-primary:not(.uk-preserve-color) .uk-slidenav,.uk-section-secondary:not(.uk-preserve-color) .uk-slidenav{color:rgba(255,255,255,.3)}.uk-card-primary.uk-card-body .uk-slidenav:focus,.uk-card-primary.uk-card-body .uk-slidenav:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-slidenav:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-slidenav:hover,.uk-card-secondary.uk-card-body .uk-slidenav:focus,.uk-card-secondary.uk-card-body .uk-slidenav:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-slidenav:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-slidenav:hover,.uk-light .uk-slidenav:focus,.uk-light .uk-slidenav:hover,.uk-offcanvas-bar .uk-slidenav:focus,.uk-offcanvas-bar .uk-slidenav:hover,.uk-overlay-primary .uk-slidenav:focus,.uk-overlay-primary .uk-slidenav:hover,.uk-section-primary:not(.uk-preserve-color) .uk-slidenav:focus,.uk-section-primary:not(.uk-preserve-color) .uk-slidenav:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-slidenav:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-slidenav:hover{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-slidenav:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-slidenav:active,.uk-card-secondary.uk-card-body .uk-slidenav:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-slidenav:active,.uk-light .uk-slidenav:active,.uk-offcanvas-bar .uk-slidenav:active,.uk-overlay-primary .uk-slidenav:active,.uk-section-primary:not(.uk-preserve-color) .uk-slidenav:active,.uk-section-secondary:not(.uk-preserve-color) .uk-slidenav:active{color:rgba(255,255,255,.6)}.uk-card-primary.uk-card-body .uk-dotnav>*>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>*>*,.uk-card-secondary.uk-card-body .uk-dotnav>*>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>*>*,.uk-light .uk-dotnav>*>*,.uk-offcanvas-bar .uk-dotnav>*>*,.uk-overlay-primary .uk-dotnav>*>*,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>*>*,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>*>*{background-color:rgba(255,255,255,.1)}.uk-card-primary.uk-card-body .uk-dotnav>*>:focus,.uk-card-primary.uk-card-body .uk-dotnav>*>:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>*>:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>*>:hover,.uk-card-secondary.uk-card-body .uk-dotnav>*>:focus,.uk-card-secondary.uk-card-body .uk-dotnav>*>:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>*>:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>*>:hover,.uk-light .uk-dotnav>*>:focus,.uk-light .uk-dotnav>*>:hover,.uk-offcanvas-bar .uk-dotnav>*>:focus,.uk-offcanvas-bar .uk-dotnav>*>:hover,.uk-overlay-primary .uk-dotnav>*>:focus,.uk-overlay-primary .uk-dotnav>*>:hover,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>*>:focus,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>*>:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>*>:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>*>:hover{background-color:rgba(255,255,255,.4)}.uk-card-primary.uk-card-body .uk-dotnav>*>:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>*>:active,.uk-card-secondary.uk-card-body .uk-dotnav>*>:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>*>:active,.uk-light .uk-dotnav>*>:active,.uk-offcanvas-bar .uk-dotnav>*>:active,.uk-overlay-primary .uk-dotnav>*>:active,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>*>:active,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>*>:active{background-color:rgba(255,255,255,.6)}.uk-card-primary.uk-card-body .uk-dotnav>.uk-active>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>.uk-active>*,.uk-card-secondary.uk-card-body .uk-dotnav>.uk-active>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>.uk-active>*,.uk-light .uk-dotnav>.uk-active>*,.uk-offcanvas-bar .uk-dotnav>.uk-active>*,.uk-overlay-primary .uk-dotnav>.uk-active>*,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>.uk-active>*,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>.uk-active>*{background-color:rgba(255,255,255,.6)}.uk-card-primary.uk-card-body .uk-iconnav>*>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-iconnav>*>*,.uk-card-secondary.uk-card-body .uk-iconnav>*>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-iconnav>*>*,.uk-light .uk-iconnav>*>*,.uk-offcanvas-bar .uk-iconnav>*>*,.uk-overlay-primary .uk-iconnav>*>*,.uk-section-primary:not(.uk-preserve-color) .uk-iconnav>*>*,.uk-section-secondary:not(.uk-preserve-color) .uk-iconnav>*>*{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-iconnav>*>:focus,.uk-card-primary.uk-card-body .uk-iconnav>*>:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-iconnav>*>:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-iconnav>*>:hover,.uk-card-secondary.uk-card-body .uk-iconnav>*>:focus,.uk-card-secondary.uk-card-body .uk-iconnav>*>:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-iconnav>*>:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-iconnav>*>:hover,.uk-light .uk-iconnav>*>:focus,.uk-light .uk-iconnav>*>:hover,.uk-offcanvas-bar .uk-iconnav>*>:focus,.uk-offcanvas-bar .uk-iconnav>*>:hover,.uk-overlay-primary .uk-iconnav>*>:focus,.uk-overlay-primary .uk-iconnav>*>:hover,.uk-section-primary:not(.uk-preserve-color) .uk-iconnav>*>:focus,.uk-section-primary:not(.uk-preserve-color) .uk-iconnav>*>:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-iconnav>*>:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-iconnav>*>:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-iconnav>.uk-active>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-iconnav>.uk-active>*,.uk-card-secondary.uk-card-body .uk-iconnav>.uk-active>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-iconnav>.uk-active>*,.uk-light .uk-iconnav>.uk-active>*,.uk-offcanvas-bar .uk-iconnav>.uk-active>*,.uk-overlay-primary .uk-iconnav>.uk-active>*,.uk-section-primary:not(.uk-preserve-color) .uk-iconnav>.uk-active>*,.uk-section-secondary:not(.uk-preserve-color) .uk-iconnav>.uk-active>*{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-text-lead,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-lead,.uk-card-secondary.uk-card-body .uk-text-lead,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-lead,.uk-light .uk-text-lead,.uk-offcanvas-bar .uk-text-lead,.uk-overlay-primary .uk-text-lead,.uk-section-primary:not(.uk-preserve-color) .uk-text-lead,.uk-section-secondary:not(.uk-preserve-color) .uk-text-lead{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-text-meta,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-meta,.uk-card-secondary.uk-card-body .uk-text-meta,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-meta,.uk-light .uk-text-meta,.uk-offcanvas-bar .uk-text-meta,.uk-overlay-primary .uk-text-meta,.uk-section-primary:not(.uk-preserve-color) .uk-text-meta,.uk-section-secondary:not(.uk-preserve-color) .uk-text-meta{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-text-muted,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-muted,.uk-card-secondary.uk-card-body .uk-text-muted,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-muted,.uk-light .uk-text-muted,.uk-offcanvas-bar .uk-text-muted,.uk-overlay-primary .uk-text-muted,.uk-section-primary:not(.uk-preserve-color) .uk-text-muted,.uk-section-secondary:not(.uk-preserve-color) .uk-text-muted{color:rgba(255,255,255,.5)!important}.uk-card-primary.uk-card-body .uk-text-primary,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-primary,.uk-card-secondary.uk-card-body .uk-text-primary,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-primary,.uk-light .uk-text-primary,.uk-offcanvas-bar .uk-text-primary,.uk-overlay-primary .uk-text-primary,.uk-section-primary:not(.uk-preserve-color) .uk-text-primary,.uk-section-secondary:not(.uk-preserve-color) .uk-text-primary{color:rgba(255,255,255,.7)!important}.uk-card-primary.uk-card-body .uk-column-divider,.uk-card-primary>:not([class*=uk-card-media]) .uk-column-divider,.uk-card-secondary.uk-card-body .uk-column-divider,.uk-card-secondary>:not([class*=uk-card-media]) .uk-column-divider,.uk-light .uk-column-divider,.uk-offcanvas-bar .uk-column-divider,.uk-overlay-primary .uk-column-divider,.uk-section-primary:not(.uk-preserve-color) .uk-column-divider,.uk-section-secondary:not(.uk-preserve-color) .uk-column-divider{-webkit-column-rule-color:rgba(255,255,255,.2);-moz-column-rule-color:rgba(255,255,255,.2);column-rule-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-logo,.uk-card-primary>:not([class*=uk-card-media]) .uk-logo,.uk-card-secondary.uk-card-body .uk-logo,.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo,.uk-light .uk-logo,.uk-offcanvas-bar .uk-logo,.uk-overlay-primary .uk-logo,.uk-section-primary:not(.uk-preserve-color) .uk-logo,.uk-section-secondary:not(.uk-preserve-color) .uk-logo{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-logo:focus,.uk-card-primary.uk-card-body .uk-logo:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-logo:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-logo:hover,.uk-card-secondary.uk-card-body .uk-logo:focus,.uk-card-secondary.uk-card-body .uk-logo:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo:hover,.uk-light .uk-logo:focus,.uk-light .uk-logo:hover,.uk-offcanvas-bar .uk-logo:focus,.uk-offcanvas-bar .uk-logo:hover,.uk-overlay-primary .uk-logo:focus,.uk-overlay-primary .uk-logo:hover,.uk-section-primary:not(.uk-preserve-color) .uk-logo:focus,.uk-section-primary:not(.uk-preserve-color) .uk-logo:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-logo:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-logo:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-card-primary>:not([class*=uk-card-media]) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-card-secondary.uk-card-body .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-light .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-offcanvas-bar .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-overlay-primary .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-section-primary:not(.uk-preserve-color) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-section-secondary:not(.uk-preserve-color) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type){display:none}.uk-card-primary.uk-card-body .uk-logo-inverse,.uk-card-primary>:not([class*=uk-card-media]) .uk-logo-inverse,.uk-card-secondary.uk-card-body .uk-logo-inverse,.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo-inverse,.uk-light .uk-logo-inverse,.uk-offcanvas-bar .uk-logo-inverse,.uk-overlay-primary .uk-logo-inverse,.uk-section-primary:not(.uk-preserve-color) .uk-logo-inverse,.uk-section-secondary:not(.uk-preserve-color) .uk-logo-inverse{display:inline}.uk-card-primary.uk-card-body .uk-accordion-title::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-accordion-title::after,.uk-card-secondary.uk-card-body .uk-accordion-title::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-accordion-title::after,.uk-light .uk-accordion-title::after,.uk-offcanvas-bar .uk-accordion-title::after,.uk-overlay-primary .uk-accordion-title::after,.uk-section-primary:not(.uk-preserve-color) .uk-accordion-title::after,.uk-section-secondary:not(.uk-preserve-color) .uk-accordion-title::after{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%3E%3C%2Frect%3E%0A%20%20%20%20%3Crect%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20width%3D%221%22%20height%3D%2213%22%20x%3D%226%22%20y%3D%220%22%3E%3C%2Frect%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-open>.uk-accordion-title::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-open>.uk-accordion-title::after,.uk-card-secondary.uk-card-body .uk-open>.uk-accordion-title::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-open>.uk-accordion-title::after,.uk-light .uk-open>.uk-accordion-title::after,.uk-offcanvas-bar .uk-open>.uk-accordion-title::after,.uk-overlay-primary .uk-open>.uk-accordion-title::after,.uk-section-primary:not(.uk-preserve-color) .uk-open>.uk-accordion-title::after,.uk-section-secondary:not(.uk-preserve-color) .uk-open>.uk-accordion-title::after{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%3E%3C%2Frect%3E%0A%3C%2Fsvg%3E")}@media print{*,::after,::before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}} \ No newline at end of file diff --git a/raidar/static/raidar/img/120px/120px-Any_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Any_tango_icon_200px.png new file mode 100644 index 00000000..1a809e2f Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Any_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Berserker_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Berserker_tango_icon_200px.png new file mode 100644 index 00000000..0006ae50 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Berserker_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Chronomancer_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Chronomancer_tango_icon_200px.png new file mode 100644 index 00000000..ede45300 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Chronomancer_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Daredevil_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Daredevil_tango_icon_200px.png new file mode 100644 index 00000000..842e82d6 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Daredevil_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Deadeye_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Deadeye_tango_icon_200px.png new file mode 100644 index 00000000..57e1fb7f Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Deadeye_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Dragonhunter_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Dragonhunter_tango_icon_200px.png new file mode 100644 index 00000000..47e65ae4 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Dragonhunter_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Druid_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Druid_tango_icon_200px.png new file mode 100644 index 00000000..70d2eba0 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Druid_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Elementalist_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Elementalist_tango_icon_200px.png new file mode 100644 index 00000000..066fba02 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Elementalist_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Engineer_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Engineer_tango_icon_200px.png new file mode 100644 index 00000000..89977586 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Engineer_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Firebrand_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Firebrand_tango_icon_200px.png new file mode 100644 index 00000000..3ac021f2 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Firebrand_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Guardian_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Guardian_tango_icon_200px.png new file mode 100644 index 00000000..4c4ca266 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Guardian_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Herald_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Herald_tango_icon_200px.png new file mode 100644 index 00000000..3f9d8805 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Herald_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Holosmith_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Holosmith_tango_icon_200px.png new file mode 100644 index 00000000..68ae5bda Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Holosmith_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Mesmer_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Mesmer_tango_icon_200px.png new file mode 100644 index 00000000..fc79d7db Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Mesmer_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Mirage_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Mirage_tango_icon_200px.png new file mode 100644 index 00000000..ddaa3816 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Mirage_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Necromancer_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Necromancer_tango_icon_200px.png new file mode 100644 index 00000000..0ad64bd8 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Necromancer_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Ranger_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Ranger_tango_icon_200px.png new file mode 100644 index 00000000..5a5adb1e Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Ranger_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Reaper_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Reaper_tango_icon_200px.png new file mode 100644 index 00000000..4c0769a0 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Reaper_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Renegade_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Renegade_tango_icon_200px.png new file mode 100644 index 00000000..9df5e72e Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Renegade_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Revenant_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Revenant_tango_icon_200px.png new file mode 100644 index 00000000..a8e1d3e2 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Revenant_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Scourge_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Scourge_tango_icon_200px.png new file mode 100644 index 00000000..9331f6cb Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Scourge_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Scrapper_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Scrapper_tango_icon_200px.png new file mode 100644 index 00000000..0a86d8ba Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Scrapper_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Some_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Some_tango_icon_200px.png new file mode 100644 index 00000000..6a8d5821 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Some_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Soulbeast_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Soulbeast_tango_icon_200px.png new file mode 100644 index 00000000..b829a26f Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Soulbeast_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Spellbreaker_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Spellbreaker_tango_icon_200px.png new file mode 100644 index 00000000..8f104a1a Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Spellbreaker_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Tempest_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Tempest_tango_icon_200px.png new file mode 100644 index 00000000..8eaea642 Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Tempest_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Thief_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Thief_tango_icon_200px.png new file mode 100644 index 00000000..2435092d Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Thief_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Warrior_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Warrior_tango_icon_200px.png new file mode 100644 index 00000000..4555613d Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Warrior_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/120px/120px-Weaver_tango_icon_200px.png b/raidar/static/raidar/img/120px/120px-Weaver_tango_icon_200px.png new file mode 100644 index 00000000..6fac41bf Binary files /dev/null and b/raidar/static/raidar/img/120px/120px-Weaver_tango_icon_200px.png differ diff --git a/raidar/static/raidar/img/20px/Any_tango_icon_20px.png b/raidar/static/raidar/img/20px/Any_tango_icon_20px.png new file mode 100644 index 00000000..15d0fab5 Binary files /dev/null and b/raidar/static/raidar/img/20px/Any_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Berserker_tango_icon_20px.png b/raidar/static/raidar/img/20px/Berserker_tango_icon_20px.png new file mode 100644 index 00000000..303700ca Binary files /dev/null and b/raidar/static/raidar/img/20px/Berserker_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Chronomancer_tango_icon_20px.png b/raidar/static/raidar/img/20px/Chronomancer_tango_icon_20px.png new file mode 100644 index 00000000..dee97092 Binary files /dev/null and b/raidar/static/raidar/img/20px/Chronomancer_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Commando_tango_icon_20px.png b/raidar/static/raidar/img/20px/Commando_tango_icon_20px.png new file mode 100644 index 00000000..e5aa6a20 Binary files /dev/null and b/raidar/static/raidar/img/20px/Commando_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Daredevil_tango_icon_20px.png b/raidar/static/raidar/img/20px/Daredevil_tango_icon_20px.png new file mode 100644 index 00000000..c98b6037 Binary files /dev/null and b/raidar/static/raidar/img/20px/Daredevil_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Deadeye_tango_icon_20px.png b/raidar/static/raidar/img/20px/Deadeye_tango_icon_20px.png new file mode 100644 index 00000000..907c3dc4 Binary files /dev/null and b/raidar/static/raidar/img/20px/Deadeye_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Dragonhunter_tango_icon_20px.png b/raidar/static/raidar/img/20px/Dragonhunter_tango_icon_20px.png new file mode 100644 index 00000000..0efac20c Binary files /dev/null and b/raidar/static/raidar/img/20px/Dragonhunter_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Druid_tango_icon_20px.png b/raidar/static/raidar/img/20px/Druid_tango_icon_20px.png new file mode 100644 index 00000000..da6f1233 Binary files /dev/null and b/raidar/static/raidar/img/20px/Druid_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Elementalist_tango_icon_20px.png b/raidar/static/raidar/img/20px/Elementalist_tango_icon_20px.png new file mode 100644 index 00000000..ffa6ba3f Binary files /dev/null and b/raidar/static/raidar/img/20px/Elementalist_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Engineer_tango_icon_20px.png b/raidar/static/raidar/img/20px/Engineer_tango_icon_20px.png new file mode 100644 index 00000000..dbf2fa9e Binary files /dev/null and b/raidar/static/raidar/img/20px/Engineer_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Firebrand_tango_icon_20px.png b/raidar/static/raidar/img/20px/Firebrand_tango_icon_20px.png new file mode 100644 index 00000000..bdec156c Binary files /dev/null and b/raidar/static/raidar/img/20px/Firebrand_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Guardian_tango_icon_20px.png b/raidar/static/raidar/img/20px/Guardian_tango_icon_20px.png new file mode 100644 index 00000000..aedc081f Binary files /dev/null and b/raidar/static/raidar/img/20px/Guardian_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Herald_tango_icon_20px.png b/raidar/static/raidar/img/20px/Herald_tango_icon_20px.png new file mode 100644 index 00000000..f1a23224 Binary files /dev/null and b/raidar/static/raidar/img/20px/Herald_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Holosmith_tango_icon_20px.png b/raidar/static/raidar/img/20px/Holosmith_tango_icon_20px.png new file mode 100644 index 00000000..65c35098 Binary files /dev/null and b/raidar/static/raidar/img/20px/Holosmith_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Mesmer_tango_icon_20px.png b/raidar/static/raidar/img/20px/Mesmer_tango_icon_20px.png new file mode 100644 index 00000000..4f396fc0 Binary files /dev/null and b/raidar/static/raidar/img/20px/Mesmer_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Mirage_tango_icon_20px.png b/raidar/static/raidar/img/20px/Mirage_tango_icon_20px.png new file mode 100644 index 00000000..204374bf Binary files /dev/null and b/raidar/static/raidar/img/20px/Mirage_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Necromancer_tango_icon_20px.png b/raidar/static/raidar/img/20px/Necromancer_tango_icon_20px.png new file mode 100644 index 00000000..026bc323 Binary files /dev/null and b/raidar/static/raidar/img/20px/Necromancer_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Ranger_tango_icon_20px.png b/raidar/static/raidar/img/20px/Ranger_tango_icon_20px.png new file mode 100644 index 00000000..ff5a579b Binary files /dev/null and b/raidar/static/raidar/img/20px/Ranger_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Reaper_tango_icon_20px.png b/raidar/static/raidar/img/20px/Reaper_tango_icon_20px.png new file mode 100644 index 00000000..e0d3783f Binary files /dev/null and b/raidar/static/raidar/img/20px/Reaper_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Renegade_tango_icon_20px.png b/raidar/static/raidar/img/20px/Renegade_tango_icon_20px.png new file mode 100644 index 00000000..a3e2309a Binary files /dev/null and b/raidar/static/raidar/img/20px/Renegade_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Revenant_tango_icon_20px.png b/raidar/static/raidar/img/20px/Revenant_tango_icon_20px.png new file mode 100644 index 00000000..cd28a426 Binary files /dev/null and b/raidar/static/raidar/img/20px/Revenant_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Scourge_tango_icon_20px.png b/raidar/static/raidar/img/20px/Scourge_tango_icon_20px.png new file mode 100644 index 00000000..0e33dfbb Binary files /dev/null and b/raidar/static/raidar/img/20px/Scourge_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Scrapper_tango_icon_20px.png b/raidar/static/raidar/img/20px/Scrapper_tango_icon_20px.png new file mode 100644 index 00000000..c90070d4 Binary files /dev/null and b/raidar/static/raidar/img/20px/Scrapper_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Shadowmancer_tango_icon_20px.png b/raidar/static/raidar/img/20px/Shadowmancer_tango_icon_20px.png new file mode 100644 index 00000000..24f4aea4 Binary files /dev/null and b/raidar/static/raidar/img/20px/Shadowmancer_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Some_tango_icon_20px.png b/raidar/static/raidar/img/20px/Some_tango_icon_20px.png new file mode 100644 index 00000000..02378d9e Binary files /dev/null and b/raidar/static/raidar/img/20px/Some_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Soulbeast_tango_icon_20px.png b/raidar/static/raidar/img/20px/Soulbeast_tango_icon_20px.png new file mode 100644 index 00000000..26a12f85 Binary files /dev/null and b/raidar/static/raidar/img/20px/Soulbeast_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Spellbreaker_tango_icon_20px.png b/raidar/static/raidar/img/20px/Spellbreaker_tango_icon_20px.png new file mode 100644 index 00000000..900ee7ec Binary files /dev/null and b/raidar/static/raidar/img/20px/Spellbreaker_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Tempest_tango_icon_20px.png b/raidar/static/raidar/img/20px/Tempest_tango_icon_20px.png new file mode 100644 index 00000000..b53fe386 Binary files /dev/null and b/raidar/static/raidar/img/20px/Tempest_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Thief_tango_icon_20px.png b/raidar/static/raidar/img/20px/Thief_tango_icon_20px.png new file mode 100644 index 00000000..b8e070cf Binary files /dev/null and b/raidar/static/raidar/img/20px/Thief_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Warrior_tango_icon_20px.png b/raidar/static/raidar/img/20px/Warrior_tango_icon_20px.png new file mode 100644 index 00000000..4b6f5198 Binary files /dev/null and b/raidar/static/raidar/img/20px/Warrior_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/20px/Weaver_tango_icon_20px.png b/raidar/static/raidar/img/20px/Weaver_tango_icon_20px.png new file mode 100644 index 00000000..d24e8b80 Binary files /dev/null and b/raidar/static/raidar/img/20px/Weaver_tango_icon_20px.png differ diff --git a/raidar/static/raidar/img/48px/Any_tango_icon_48px.png b/raidar/static/raidar/img/48px/Any_tango_icon_48px.png new file mode 100644 index 00000000..86c14b6a Binary files /dev/null and b/raidar/static/raidar/img/48px/Any_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Berserker_tango_icon_48px.png b/raidar/static/raidar/img/48px/Berserker_tango_icon_48px.png new file mode 100644 index 00000000..53395d5e Binary files /dev/null and b/raidar/static/raidar/img/48px/Berserker_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Chronomancer_tango_icon_48px.png b/raidar/static/raidar/img/48px/Chronomancer_tango_icon_48px.png new file mode 100644 index 00000000..5d30cef4 Binary files /dev/null and b/raidar/static/raidar/img/48px/Chronomancer_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Daredevil_tango_icon_48px.png b/raidar/static/raidar/img/48px/Daredevil_tango_icon_48px.png new file mode 100644 index 00000000..1fbc989c Binary files /dev/null and b/raidar/static/raidar/img/48px/Daredevil_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Deadeye_tango_icon_48px.png b/raidar/static/raidar/img/48px/Deadeye_tango_icon_48px.png new file mode 100644 index 00000000..ab12a73f Binary files /dev/null and b/raidar/static/raidar/img/48px/Deadeye_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Dragonhunter_tango_icon_48px.png b/raidar/static/raidar/img/48px/Dragonhunter_tango_icon_48px.png new file mode 100644 index 00000000..6595fc1c Binary files /dev/null and b/raidar/static/raidar/img/48px/Dragonhunter_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Druid_tango_icon_48px.png b/raidar/static/raidar/img/48px/Druid_tango_icon_48px.png new file mode 100644 index 00000000..f0d6e154 Binary files /dev/null and b/raidar/static/raidar/img/48px/Druid_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Elementalist_tango_icon_48px.png b/raidar/static/raidar/img/48px/Elementalist_tango_icon_48px.png new file mode 100644 index 00000000..72e9592a Binary files /dev/null and b/raidar/static/raidar/img/48px/Elementalist_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Engineer_tango_icon_48px.png b/raidar/static/raidar/img/48px/Engineer_tango_icon_48px.png new file mode 100644 index 00000000..be2605d9 Binary files /dev/null and b/raidar/static/raidar/img/48px/Engineer_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Firebrand_tango_icon_48px.png b/raidar/static/raidar/img/48px/Firebrand_tango_icon_48px.png new file mode 100644 index 00000000..c6826941 Binary files /dev/null and b/raidar/static/raidar/img/48px/Firebrand_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Guardian_tango_icon_48px.png b/raidar/static/raidar/img/48px/Guardian_tango_icon_48px.png new file mode 100644 index 00000000..7f76740a Binary files /dev/null and b/raidar/static/raidar/img/48px/Guardian_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Herald_tango_icon_48px.png b/raidar/static/raidar/img/48px/Herald_tango_icon_48px.png new file mode 100644 index 00000000..72499c0c Binary files /dev/null and b/raidar/static/raidar/img/48px/Herald_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Holosmith_tango_icon_48px.png b/raidar/static/raidar/img/48px/Holosmith_tango_icon_48px.png new file mode 100644 index 00000000..c9daf08c Binary files /dev/null and b/raidar/static/raidar/img/48px/Holosmith_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Mesmer_tango_icon_48px.png b/raidar/static/raidar/img/48px/Mesmer_tango_icon_48px.png new file mode 100644 index 00000000..5f1ebff6 Binary files /dev/null and b/raidar/static/raidar/img/48px/Mesmer_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Mirage_tango_icon_48px.png b/raidar/static/raidar/img/48px/Mirage_tango_icon_48px.png new file mode 100644 index 00000000..ddbcdfec Binary files /dev/null and b/raidar/static/raidar/img/48px/Mirage_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Necromancer_tango_icon_48px.png b/raidar/static/raidar/img/48px/Necromancer_tango_icon_48px.png new file mode 100644 index 00000000..b658648a Binary files /dev/null and b/raidar/static/raidar/img/48px/Necromancer_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Ranger_tango_icon_48px.png b/raidar/static/raidar/img/48px/Ranger_tango_icon_48px.png new file mode 100644 index 00000000..73fc7a15 Binary files /dev/null and b/raidar/static/raidar/img/48px/Ranger_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Reaper_tango_icon_48px.png b/raidar/static/raidar/img/48px/Reaper_tango_icon_48px.png new file mode 100644 index 00000000..1313f692 Binary files /dev/null and b/raidar/static/raidar/img/48px/Reaper_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Renegade_tango_icon_48px.png b/raidar/static/raidar/img/48px/Renegade_tango_icon_48px.png new file mode 100644 index 00000000..5417de09 Binary files /dev/null and b/raidar/static/raidar/img/48px/Renegade_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Revenant_tango_icon_48px.png b/raidar/static/raidar/img/48px/Revenant_tango_icon_48px.png new file mode 100644 index 00000000..56c573de Binary files /dev/null and b/raidar/static/raidar/img/48px/Revenant_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Scourge_tango_icon_48px.png b/raidar/static/raidar/img/48px/Scourge_tango_icon_48px.png new file mode 100644 index 00000000..b6ddad6c Binary files /dev/null and b/raidar/static/raidar/img/48px/Scourge_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Scrapper_tango_icon_48px.png b/raidar/static/raidar/img/48px/Scrapper_tango_icon_48px.png new file mode 100644 index 00000000..4063dd97 Binary files /dev/null and b/raidar/static/raidar/img/48px/Scrapper_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Some_tango_icon_48px.png b/raidar/static/raidar/img/48px/Some_tango_icon_48px.png new file mode 100644 index 00000000..71da8692 Binary files /dev/null and b/raidar/static/raidar/img/48px/Some_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Soulbeast_tango_icon_48px.png b/raidar/static/raidar/img/48px/Soulbeast_tango_icon_48px.png new file mode 100644 index 00000000..98dec1fc Binary files /dev/null and b/raidar/static/raidar/img/48px/Soulbeast_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Spellbreaker_tango_icon_48px.png b/raidar/static/raidar/img/48px/Spellbreaker_tango_icon_48px.png new file mode 100644 index 00000000..fabcb5b5 Binary files /dev/null and b/raidar/static/raidar/img/48px/Spellbreaker_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Tempest_tango_icon_48px.png b/raidar/static/raidar/img/48px/Tempest_tango_icon_48px.png new file mode 100644 index 00000000..d324ce1c Binary files /dev/null and b/raidar/static/raidar/img/48px/Tempest_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Thief_tango_icon_48px.png b/raidar/static/raidar/img/48px/Thief_tango_icon_48px.png new file mode 100644 index 00000000..81cdca6a Binary files /dev/null and b/raidar/static/raidar/img/48px/Thief_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Warrior_tango_icon_48px.png b/raidar/static/raidar/img/48px/Warrior_tango_icon_48px.png new file mode 100644 index 00000000..109cb7bb Binary files /dev/null and b/raidar/static/raidar/img/48px/Warrior_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/48px/Weaver_tango_icon_48px.png b/raidar/static/raidar/img/48px/Weaver_tango_icon_48px.png new file mode 100644 index 00000000..34f5cc9d Binary files /dev/null and b/raidar/static/raidar/img/48px/Weaver_tango_icon_48px.png differ diff --git a/raidar/static/raidar/img/arch/Condi.png b/raidar/static/raidar/img/arch/Condi.png new file mode 100644 index 00000000..94a27a3a Binary files /dev/null and b/raidar/static/raidar/img/arch/Condi.png differ diff --git a/raidar/static/raidar/img/arch/Heal.png b/raidar/static/raidar/img/arch/Heal.png new file mode 100644 index 00000000..863604f8 Binary files /dev/null and b/raidar/static/raidar/img/arch/Heal.png differ diff --git a/raidar/static/raidar/img/arch/Power.png b/raidar/static/raidar/img/arch/Power.png new file mode 100644 index 00000000..885966c2 Binary files /dev/null and b/raidar/static/raidar/img/arch/Power.png differ diff --git a/raidar/static/raidar/img/arch/Support.png b/raidar/static/raidar/img/arch/Support.png new file mode 100644 index 00000000..c0750aa1 Binary files /dev/null and b/raidar/static/raidar/img/arch/Support.png differ diff --git a/raidar/static/raidar/img/arch/Tank.png b/raidar/static/raidar/img/arch/Tank.png new file mode 100644 index 00000000..8e1a132e Binary files /dev/null and b/raidar/static/raidar/img/arch/Tank.png differ diff --git a/raidar/static/raidar/img/buff/Alacrity.png b/raidar/static/raidar/img/buff/Alacrity.png new file mode 100644 index 00000000..119ac314 Binary files /dev/null and b/raidar/static/raidar/img/buff/Alacrity.png differ diff --git a/raidar/static/raidar/img/buff/Assassin's_Presence.png b/raidar/static/raidar/img/buff/Assassin's_Presence.png new file mode 100644 index 00000000..cbb53547 Binary files /dev/null and b/raidar/static/raidar/img/buff/Assassin's_Presence.png differ diff --git a/raidar/static/raidar/img/buff/Banner_of_Defense.png b/raidar/static/raidar/img/buff/Banner_of_Defense.png new file mode 100644 index 00000000..0ee6c06f Binary files /dev/null and b/raidar/static/raidar/img/buff/Banner_of_Defense.png differ diff --git a/raidar/static/raidar/img/buff/Banner_of_Discipline.png b/raidar/static/raidar/img/buff/Banner_of_Discipline.png new file mode 100644 index 00000000..5e1e3366 Binary files /dev/null and b/raidar/static/raidar/img/buff/Banner_of_Discipline.png differ diff --git a/raidar/static/raidar/img/buff/Banner_of_Strength.png b/raidar/static/raidar/img/buff/Banner_of_Strength.png new file mode 100644 index 00000000..e0ea3677 Binary files /dev/null and b/raidar/static/raidar/img/buff/Banner_of_Strength.png differ diff --git a/raidar/static/raidar/img/buff/Banner_of_Tactics.png b/raidar/static/raidar/img/buff/Banner_of_Tactics.png new file mode 100644 index 00000000..7c5e0bf0 Binary files /dev/null and b/raidar/static/raidar/img/buff/Banner_of_Tactics.png differ diff --git a/raidar/static/raidar/img/buff/Empower_Allies.png b/raidar/static/raidar/img/buff/Empower_Allies.png new file mode 100644 index 00000000..088e5732 Binary files /dev/null and b/raidar/static/raidar/img/buff/Empower_Allies.png differ diff --git a/raidar/static/raidar/img/buff/Facet_of_Nature.png b/raidar/static/raidar/img/buff/Facet_of_Nature.png new file mode 100644 index 00000000..d1f5ee07 Binary files /dev/null and b/raidar/static/raidar/img/buff/Facet_of_Nature.png differ diff --git a/raidar/static/raidar/img/buff/Frost_Spirit.png b/raidar/static/raidar/img/buff/Frost_Spirit.png new file mode 100644 index 00000000..bb65d0b6 Binary files /dev/null and b/raidar/static/raidar/img/buff/Frost_Spirit.png differ diff --git a/raidar/static/raidar/img/buff/Fury.png b/raidar/static/raidar/img/buff/Fury.png new file mode 100644 index 00000000..07e50e82 Binary files /dev/null and b/raidar/static/raidar/img/buff/Fury.png differ diff --git a/raidar/static/raidar/img/buff/Glyph_of_Empowerment.png b/raidar/static/raidar/img/buff/Glyph_of_Empowerment.png new file mode 100644 index 00000000..588d753c Binary files /dev/null and b/raidar/static/raidar/img/buff/Glyph_of_Empowerment.png differ diff --git a/raidar/static/raidar/img/buff/Grace_of_the_Land.png b/raidar/static/raidar/img/buff/Grace_of_the_Land.png new file mode 100644 index 00000000..dad9ce0e Binary files /dev/null and b/raidar/static/raidar/img/buff/Grace_of_the_Land.png differ diff --git a/raidar/static/raidar/img/buff/Might.png b/raidar/static/raidar/img/buff/Might.png new file mode 100644 index 00000000..3a908d25 Binary files /dev/null and b/raidar/static/raidar/img/buff/Might.png differ diff --git a/raidar/static/raidar/img/buff/Pinpoint_Distribution.png b/raidar/static/raidar/img/buff/Pinpoint_Distribution.png new file mode 100644 index 00000000..a7bd9655 Binary files /dev/null and b/raidar/static/raidar/img/buff/Pinpoint_Distribution.png differ diff --git a/raidar/static/raidar/img/buff/Protection.png b/raidar/static/raidar/img/buff/Protection.png new file mode 100644 index 00000000..57a6214a Binary files /dev/null and b/raidar/static/raidar/img/buff/Protection.png differ diff --git a/raidar/static/raidar/img/buff/Quickness.png b/raidar/static/raidar/img/buff/Quickness.png new file mode 100644 index 00000000..060a70a5 Binary files /dev/null and b/raidar/static/raidar/img/buff/Quickness.png differ diff --git a/raidar/static/raidar/img/buff/Regeneration.png b/raidar/static/raidar/img/buff/Regeneration.png new file mode 100644 index 00000000..7fae206f Binary files /dev/null and b/raidar/static/raidar/img/buff/Regeneration.png differ diff --git a/raidar/static/raidar/img/buff/Retaliation.png b/raidar/static/raidar/img/buff/Retaliation.png new file mode 100644 index 00000000..b48cb453 Binary files /dev/null and b/raidar/static/raidar/img/buff/Retaliation.png differ diff --git a/raidar/static/raidar/img/buff/Soothing_Mist.png b/raidar/static/raidar/img/buff/Soothing_Mist.png new file mode 100644 index 00000000..087fcc5d Binary files /dev/null and b/raidar/static/raidar/img/buff/Soothing_Mist.png differ diff --git a/raidar/static/raidar/img/buff/Spotter.png b/raidar/static/raidar/img/buff/Spotter.png new file mode 100644 index 00000000..65bb9f75 Binary files /dev/null and b/raidar/static/raidar/img/buff/Spotter.png differ diff --git a/raidar/static/raidar/img/buff/Stone_Spirit.png b/raidar/static/raidar/img/buff/Stone_Spirit.png new file mode 100644 index 00000000..c9b4de46 Binary files /dev/null and b/raidar/static/raidar/img/buff/Stone_Spirit.png differ diff --git a/raidar/static/raidar/img/buff/Storm_Spirit.png b/raidar/static/raidar/img/buff/Storm_Spirit.png new file mode 100644 index 00000000..a9a2f80f Binary files /dev/null and b/raidar/static/raidar/img/buff/Storm_Spirit.png differ diff --git a/raidar/static/raidar/img/buff/Sun_Spirit.png b/raidar/static/raidar/img/buff/Sun_Spirit.png new file mode 100644 index 00000000..50fb197b Binary files /dev/null and b/raidar/static/raidar/img/buff/Sun_Spirit.png differ diff --git a/raidar/static/raidar/img/buff/Vampiric_Presence.png b/raidar/static/raidar/img/buff/Vampiric_Presence.png new file mode 100644 index 00000000..3794ee7c Binary files /dev/null and b/raidar/static/raidar/img/buff/Vampiric_Presence.png differ diff --git a/raidar/static/raidar/img/intro/individual.png b/raidar/static/raidar/img/intro/individual.png new file mode 100755 index 00000000..01cb35cc Binary files /dev/null and b/raidar/static/raidar/img/intro/individual.png differ diff --git a/raidar/static/raidar/img/intro/party.png b/raidar/static/raidar/img/intro/party.png new file mode 100755 index 00000000..e948f806 Binary files /dev/null and b/raidar/static/raidar/img/intro/party.png differ diff --git a/raidar/static/raidar/img/intro/squad.png b/raidar/static/raidar/img/intro/squad.png new file mode 100755 index 00000000..f39ff2f8 Binary files /dev/null and b/raidar/static/raidar/img/intro/squad.png differ diff --git a/raidar/static/raidar/js/uikit.min.js b/raidar/static/raidar/js/uikit.min.js new file mode 100644 index 00000000..6500f530 --- /dev/null +++ b/raidar/static/raidar/js/uikit.min.js @@ -0,0 +1,5 @@ +/*! UIkit 3.0.0-beta.16 | http://www.getuikit.com | (c) 2014 - 2017 YOOtheme | MIT License */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery")):"function"==typeof define&&define.amd?define("uikit",["jquery"],e):t.UIkit=e(t.jQuery)}(this,function(t){"use strict";function e(){return"complete"===document.readyState||"loading"!==document.readyState&&!document.documentElement.doScroll}function i(t){var i=function(){o(document,"DOMContentLoaded",i),o(window,"load",i),t()};e()?t():(n(document,"DOMContentLoaded",i),n(window,"load",i))}function n(t,e,i,n){_(t).addEventListener(e,i,n)}function o(t,e,i,n){_(t).removeEventListener(e,i,n)}function s(t,e,i,n){void 0===i&&(i=400),void 0===n&&(n="linear");var o=w(function(s,r){t=Gt(t);for(var a in e)t.css(a,t.css(a));var l=setTimeout(function(){return t.trigger(be||"transitionend")},i);t.one(be||"transitionend",function(e,i){e.promise=o,clearTimeout(l),t.removeClass("uk-transition").css("transition",""),i?r():s()}).addClass("uk-transition").css("transition","all "+i+"ms "+n).css(e)}).then(null,function(){});return o}function r(t,e,i,n,o){void 0===i&&(i=200);var s=w(function(r){function a(){t.css("animation-duration","").removeClass(l+" "+e)}var l=o?"uk-animation-leave":"uk-animation-enter";t=Gt(t),0===e.lastIndexOf("uk-animation-",0)&&(n&&(e+=" uk-animation-"+n),o&&(e+=" uk-animation-reverse")),a(),t.one($e||"animationend",function(t){t.promise=s,s.then(a),r()}).css("animation-duration",i+"ms").addClass(e).addClass(l),$e||ce(function(){return ie.cancel(t)})});return s}function a(t){return t instanceof Gt}function l(t,e){return t=Gt(t),t.is(e)||!!(T(e)?t.parents(e).length:Gt.contains(_(e),t[0]))}function h(t,e,i,n){return t=Gt(t),t.attr(e,function(t,e){return e?e.replace(i,n):e})}function c(t,e){return h(t,"class",new RegExp("(^|\\s)"+e+"(?!\\S)","g"),"")}function u(t,e,i,n){if(void 0===e&&(e=!0),void 0===i&&(i=!1),void 0===n&&(n=!1),T(t)){var o=document.createEvent("Event");o.initEvent(t,e,i),t=o}return n&&Gt.extend(t,n),t}function d(t,e,i){if(void 0===e&&(e=0),void 0===i&&(i=0),t=Gt(t),!t.is(":visible"))return!1;var n=Jt.scrollLeft(),o=Jt.scrollTop(),s=t.offset(),r=s.top,a=s.left;return r+t.height()>=o&&r-e<=o+Jt.height()&&a+t.width()>=n&&a-i<=n+Jt.width()}function f(t,e,i){void 0===i&&(i=0),e=Gt(e);var n=Gt(e).length;return t=(C(t)?t:"next"===t?i+1:"previous"===t?i-1:T(t)?parseInt(t,10):e.index(t))%n,t<0?t+n:t}function g(t){return ne[_(t).tagName.toLowerCase()]}function p(t,e){var i=A(t);return i?i.reduce(function(t,e){return D(e,t)},e):D(t)}function m(t,e){return function(i){var n=arguments.length;return n?n>1?t.apply(e,arguments):t.call(e,i):t.call(e)}}function v(t,e){return se.call(t,e)}function w(t){if(!S(window.Promise))return new window.Promise(t);var e=Gt.Deferred();return t(e.resolve,e.reject),e}function b(t){return t.replace(/(?:^|[-_\/])(\w)/g,function(t,e){return e?e.toUpperCase():""})}function y(t){return t.replace(/([a-z\d])([A-Z])/g,"$1-$2").toLowerCase()}function x(t){return t.replace(re,k)}function k(t,e){return e?e.toUpperCase():""}function T(t){return"string"==typeof t}function C(t){return"number"==typeof t}function S(t){return void 0===t}function E(t){return T(t)&&t.match(/^(!|>|\+|-)/)}function A(t){return E(t)&&t.split(/(?=\s(?:!|>|\+|-))/g).map(function(t){return t.trim()})}function D(t,e){if(t===!0)return null;try{if(e&&E(t)&&">"!==t[0]){var i=ae[t[0]],n=t.substr(1);e=Gt(e),"closest"===i&&(e=e.parent(),n=n||"*"),t=e[i](n)}else t=Gt(t,e)}catch(t){return null}return t.length?t:null}function _(t){return t&&(a(t)?t[0]:t)}function O(t){return"boolean"==typeof t?t:"true"===t||"1"==t||""===t||"false"!==t&&"0"!=t&&t}function I(t){var e=Number(t);return!isNaN(e)&&e}function P(t){if(T(t)&&"@"==t[0]){var e="media-"+t.substr(1);t=le[e]||(le[e]=parseFloat(j(e)))}return!(!t||isNaN(t))&&"(min-width: "+t+"px)"}function N(t,e,i){return t===Boolean?O(e):t===Number?I(e):"jQuery"===t?p(e,i):"media"===t?P(e):t?t(e):e}function B(t){return t?"ms"===t.substr(-2)?parseFloat(t):1e3*parseFloat(t):0}function H(t,e,i){return t.replace(new RegExp(e+"|"+i,"mg"),function(t){return t===e?i:e})}function M(t,e,i){return(window.getComputedStyle(t,i)||{})[e]}function j(t){var e,i=document.documentElement,n=i.appendChild(document.createElement("div"));n.classList.add("var-"+t);try{e=M(n,"content",":before").replace(/^["'](.*)["']$/,"$1"),e=JSON.parse(e)}catch(t){}return i.removeChild(n),e||void 0}function F(t,e){var i,n=b(t),o=b(e).toLowerCase(),s=b(e),r=document.body||document.documentElement,a=(i={},i["Webkit"+n]="webkit"+s,i["Moz"+n]=o,i["o"+n]="o"+s+" o"+o,i[t]=o,i);for(t in a)if(void 0!==r.style[t])return a[t]}function L(){var t=this;t.reads=[],t.writes=[],t.raf=ce.bind(window)}function z(t){t.scheduled||(t.scheduled=!0,t.raf(q.bind(null,t)))}function q(t){var e;try{U(t.reads),U(t.writes.splice(0,t.writes.length))}catch(t){e=t}if(t.scheduled=!1,(t.reads.length||t.writes.length)&&z(t),e){if(!t.catch)throw e;t.catch(e)}}function U(t){for(var e;e=t.shift();)e()}function R(t,e){var i=t.indexOf(e);return!!~i&&!!t.splice(i,1)}function W(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])}function Y(){}function V(t,e){return(e.y-t.y)/(e.x-t.x)}function Q(t,e){function i(i){o[i]=(ke[i]||_e)(t[i],e[i])}var n,o={};if(e.mixins)for(var s=0,r=e.mixins.length;sa[f]){var m=c[d]+g+p-2*o[t];m>=a[d]&&m+l[s]<=a[f]&&(c[d]=m,["element","target"].forEach(function(e){u[e][t]=g?u[e][t]===Oe[t][1]?Oe[t][2]:Oe[t][1]:u[e][t]}))}}}),t.offset({left:c.left,top:c.top}),u}function G(t){t=Gt(t);var e=Math.round(t.outerWidth()),i=Math.round(t.outerHeight()),n=t[0]&&t[0].getClientRects?t.offset():null,o=n?Math.round(n.left):t.scrollLeft(),s=n?Math.round(n.top):t.scrollTop();return{width:e,height:i,left:o,top:s,right:o+e,bottom:s+i}}function J(t,e,i,n){Gt.each(Oe,function(o,s){var r=s[0],a=s[1],l=s[2];e[o]===l?t[a]+=i[r]*n:"center"===e[o]&&(t[a]+=i[r]*n/2)})}function Z(t){var e=/left|center|right/,i=/top|center|bottom/;return t=(t||"").split(" "),1===t.length&&(t=e.test(t[0])?t.concat(["center"]):i.test(t[0])?["center"].concat(t):["center","center"]),{x:e.test(t[0])?t[0]:"center",y:i.test(t[1])?t[1]:"center"}}function K(t,e,i){return t=(t||"").split(" "),{x:t[0]?parseFloat(t[0])*("%"===t[0][t[0].length-1]?e/100:1):0,y:t[1]?parseFloat(t[1])*("%"===t[1][t[1].length-1]?i/100:1):0}}function tt(t){switch(t){case"left":return"right";case"right":return"left";case"top":return"bottom";case"bottom":return"top";default:return t}}function et(t,e,i,n){return Math.abs(t-e)>=Math.abs(i-n)?t-e>0?"Left":"Right":i-n>0?"Up":"Down"}function it(){Ee=null,Ie.last&&(void 0!==Ie.el&&Ie.el.trigger("longTap"),Ie={})}function nt(){Ee&&clearTimeout(Ee),Ee=null}function ot(){Te&&clearTimeout(Te),Ce&&clearTimeout(Ce),Se&&clearTimeout(Se),Ee&&clearTimeout(Ee),Te=Ce=Se=Ee=null,Ie={}}function st(t){return Ne||t.originalEvent&&"touch"===t.originalEvent.pointerType}function rt(t){var e=t.data;t.use=function(t){if(!t.installed)return t.call(null,this),t.installed=!0,this},t.mixin=function(e,i){i=(T(i)?t.components[i]:i)||this,e=Q({},e),e.mixins=i.options.mixins,delete i.options.mixins,i.options=Q(e,i.options)},t.extend=function(t){t=t||{};var e=this,i=t.name||e.options.name,n=at(i||"UIkitComponent");return n.prototype=Object.create(e.prototype),n.prototype.constructor=n,n.options=Q(e.options,t),n.super=e,n.extend=e.extend,n},t.update=function(i,n,o){if(void 0===o&&(o=!1),i=u(i||"update"),!n)return void ht(t.instances,i);if(n=_(n),o){do ht(n[e],i),n=n.parentNode;while(n)}else lt(n,function(t){return ht(t[e],i)})};var i;Object.defineProperty(t,"container",{get:function(){return i||document.body},set:function(t){i=t}})}function at(t){return new Function("return function "+b(t)+" (options) { this._init(options); }")()}function lt(t,e){if(t.nodeType===Node.ELEMENT_NODE)for(e(t),t=t.firstChild;t;)lt(t,e),t=t.nextSibling}function ht(t,e){if(t)for(var i in t)t[i]._isReady&&t[i]._callUpdate(e)}function ct(e){var n=0;e.prototype.props={},e.prototype._init=function(t){t=t||{},t=this.$options=Q(this.constructor.options,t,this),this.$el=null,this.$name=e.prefix+y(this.$options.name),this.$props={},this._uid=n++,this._initData(),this._initMethods(),this._callHook("created"),this._frames={reads:{},writes:{}},t.el&&this.$mount(t.el)},e.prototype._initData=function(){var e=this,i=t.extend(!0,{},this.$options.defaults),n=this.$options.data||{},o=this.$options.args||[],s=this.$options.props||{};if(i){o.length&&t.isArray(n)&&(n=n.slice(0,o.length).reduce(function(e,i,n){return t.isPlainObject(i)?t.extend(e,i):e[o[n]]=i,e},{}));for(var r in i)e.$props[r]=e[r]=v(n,r)?N(s[r],n[r],e.$options.el):i[r]}},e.prototype._initProps=function(e){e=e||this._getProps(),t.extend(this,e),t.extend(this.$props,e)},e.prototype._initMethods=function(){var t=this,e=this.$options.methods;if(e)for(var i in e)t[i]=m(e[i],t)},e.prototype._initEvents=function(e){var i=this,n=this.$options.events,o=function(n,o){t.isPlainObject(n)||(n={name:o,handler:n});var s=n.name,r=n.delegate,a=n.self,l=n.filter,h=n.handler;if(s+="."+i.$options.name,e)i.$el.off(s);else{if(l&&!l.call(i))return;if(h=T(h)?i[h]:m(h,i),a){var c=h;h=function(t){i.$el.is(t.target)&&c.call(i,t)}}r?i.$el.on(s,T(r)?r:r.call(i),h):i.$el.on(s,h)}};n&&n.forEach(function(t){if("handler"in t)o(t);else for(var e in t)o(t[e],e)})},e.prototype._initObserver=function(){var t=this;!this._observer&&he&&this.$options.props&&this.$options.attrs&&(this._observer=new he(function(e){var i=t._getProps(!0);e.map(function(t){return x(t.attributeName)}).some(function(e){return!ut(i[e],t.$props[e])})&&(t._callDisconnected(),t._initProps(i),t._callConnected(),t._callUpdate())}),this._observer.observe(this.$options.el,{attributes:!0,attributeFilter:Object.keys(this.$options.props).map(function(t){return y(t)})}))},e.prototype._callHook=function(t,e){var i=this,n=this.$options[t];n&&n.forEach(function(t){return t.call(i)})},e.prototype._callReady=function(){this._isReady||(this._isReady=!0,this._callHook("ready"),this._callUpdate())},e.prototype._callConnected=function(){var t=this;this._connected||(~e.elements.indexOf(this.$options.$el)||e.elements.push(this.$options.$el),e.instances[this._uid]=this,this._initEvents(),this._callHook("connected"),this._connected=!0,this._initObserver(),this._isReady||i(function(){return t._callReady()}))},e.prototype._callDisconnected=function(){if(this._connected){this._observer&&(this._observer.disconnect(),this._observer=null);var t=e.elements.indexOf(this.$options.$el);~t&&e.elements.splice(t,1),delete e.instances[this._uid],this._initEvents(!0),this._callHook("disconnected"),this._connected=!1}},e.prototype._callUpdate=function(t){var e=this;t=u(t||"update");var i=this.$options.update;i&&i.forEach(function(i,n){if("update"===t.type||i.events&&~i.events.indexOf(t.type)){if(t.sync)return i.read&&i.read.call(e,t),void(i.write&&i.write.call(e,t));i.read&&!~xe.reads.indexOf(e._frames.reads[n])&&(e._frames.reads[n]=xe.measure(function(){return i.read.call(e,t)})),i.write&&!~xe.writes.indexOf(e._frames.writes[n])&&(e._frames.writes[n]=xe.mutate(function(){return i.write.call(e,t)}))}})},e.prototype._getProps=function(t){void 0===t&&(t=!1);var e,i,n={},o=this.$el[0],s=this.$options.args||[],r=this.$options.props||{},a=o.getAttribute(this.$name)||o.getAttribute("data-"+this.$name);if(!r)return n;for(e in r)if(i=y(e),o.hasAttribute(i)){var l=N(r[e],o.getAttribute(i),o);if("target"===i&&(!l||0===l.lastIndexOf("_",0)))continue;n[e]=l}if(t||!a)return n;if("{"===a[0])try{a=JSON.parse(a)}catch(t){console.warn("Invalid JSON."),a={}}else if(s.length&&!~a.indexOf(":")){h={},h[s[0]]=a,a=h;var h}else{var c={};a.split(";").forEach(function(t){var e=t.split(/:(.+)/),i=e[0],n=e[1];i&&n&&(c[i.trim()]=n.trim())}),a=c}for(e in a||{})i=x(e),void 0!==r[i]&&(n[i]=N(r[i],a[e],o));return n}}function ut(t,e){return S(t)||t===e||a(t)&&a(e)&&t.is(e)}function dt(t){var e=t.data;t.prototype.$mount=function(t){var i=this.$options.name;return t[e]||(t[e]={}),t[e][i]?void console.warn('Component "'+i+'" is already mounted on element: ',t):(t[e][i]=this,this.$el=Gt(t),this._initProps(),this._callHook("init"),void(document.documentElement.contains(t)&&this._callConnected()))},t.prototype.$emit=function(t){this._callUpdate(t)},t.prototype.$emitSync=function(t){this._callUpdate(u(t||"update",!0,!1,{sync:!0}))},t.prototype.$update=function(e,i){t.update(e,this.$el,i)},t.prototype.$updateSync=function(e,i){t.update(u(e||"update",!0,!1,{sync:!0}),this.$el,i)},t.prototype.$destroy=function(t){void 0===t&&(t=!1);var i=this.$options.el;i&&this._callDisconnected(),this._callHook("destroy"),i&&i[e]&&(delete i[e][this.$options.name],Object.keys(i[e]).length||delete i[e],t&&this.$el.remove())}}function ft(e){var i=e.data;e.components={},e.component=function(n,o){var s=x(n);return t.isPlainObject(o)?(o.name=s,o=e.extend(o)):o.options.name=s,e.components[s]=o,e[s]=function(n,o){for(var r=arguments.length,l=Array(r);r--;)l[r]=arguments[r];return t.isPlainObject(n)?new e.components[s]({data:n}):e.components[s].options.functional?new e.components[s]({data:[].concat(l)}):(o=o||{},n=T?Gt(n)[0]:a(n)?n[0]:n,n&&n[i]&&n[i][s]||new e.components[s]({el:n,data:o}))},document.body&&!o.options.functional&&e[s]("[uk-"+n+"],[data-uk-"+n+"]"),e.components[s]},e.getComponents=function(t){return t&&_(t)[i]||{}},e.getComponent=function(t,i){return t&&e.getComponents(t)[i]},e.connect=function(t){var n;if(t[i])for(n in t[i])t[i][n]._callConnected();for(var o=0;o *",active:!1,animation:!0,collapsible:!0,multiple:!1,clsOpen:"uk-open",toggle:"> .uk-accordion-title",content:"> .uk-accordion-content",transition:"ease"},connected:function(){this.$emitSync()},events:[{name:"click",delegate:function(){return this.$props.targets+" "+this.$props.toggle},handler:function(t){t.preventDefault(),this.toggle(this.items.find(this.$props.toggle).index(t.currentTarget))}}],update:function(){var t=this,e=Gt(this.targets,this.$el),i=!this.items||e.length!==this.items.length||e.toArray().some(function(e,i){return e!==t.items.get(i)});if(this.items=e,i){this.items.each(function(e,i){i=Gt(i),t.toggleNow(i.find(t.content),i.hasClass(t.clsOpen))});var n=this.active!==!1&&D(this.items.eq(Number(this.active)))||!this.collapsible&&D(this.items.eq(0));n&&!n.hasClass(this.clsOpen)&&this.toggle(n,!1)}},methods:{toggle:function(t,e){var i=this,n=f(t,this.items),o=this.items.filter("."+this.clsOpen);t=this.items.eq(n),t.add(!this.multiple&&o).each(function(n,s){s=Gt(s);var r=s.is(t),a=r&&!s.hasClass(i.clsOpen);if(a||!r||i.collapsible||!(o.length<2)){s.toggleClass(i.clsOpen,a);var l=s[0]._wrapper?s[0]._wrapper.children().first():s.find(i.content);s[0]._wrapper||(s[0]._wrapper=l.wrap("
").parent().attr("hidden",a)),i._toggleImmediate(l,!0),i.toggleElement(s[0]._wrapper,a,e).then(function(){s.hasClass(i.clsOpen)===a&&(a||i._toggleImmediate(l,!1),s[0]._wrapper=null,l.unwrap())})}})}}})}function wt(t){t.component("alert",{mixins:[Fe,Le],args:"animation",props:{animation:Boolean,close:String},defaults:{animation:!0,close:".uk-alert-close",duration:150,hideProps:{opacity:0}},events:[{name:"click",delegate:function(){return this.close},handler:function(t){t.preventDefault(),this.closeAlert()}}],methods:{closeAlert:function(){var t=this;this.toggleElement(this.$el).then(function(){return t.$destroy(!0)})}}})}function bt(t){t.component("cover",{mixins:[Fe],props:{automute:Boolean,width:Number,height:Number},defaults:{automute:!0},ready:function(){if(this.$el.is("iframe")&&(this.$el.css("pointerEvents","none"),this.automute)){var t=this.$el.attr("src");this.$el.attr("src",""+t+(~t.indexOf("?")?"&":"?")+"enablejsapi=1&api=1").on("load",function(t){var e=t.target;return e.contentWindow.postMessage('{"event": "command", "func": "mute", "method":"setVolume", "value":0}',"*")})}},update:{write:function(){0!==this.$el[0].offsetHeight&&this.$el.css({width:"",height:""}).css(oe.cover({width:this.width||this.$el.width(),height:this.height||this.$el.height()},{width:this.$el.parent().outerWidth(),height:this.$el.parent().outerHeight()}))},events:["load","resize","orientationchange"]},events:{loadedmetadata:function(){this.$emit()}}})}function yt(t){var e;Zt.on("click",function(t){for(var i;e&&e!==i&&!l(t.target,e.$el)&&(!e.toggle||!l(t.target,e.toggle.$el));)i=e,e.hide(!1)}),t.component("drop",{mixins:[qe,Le],args:"pos",props:{mode:String,toggle:Boolean,boundary:"jQuery",boundaryAlign:Boolean,delayShow:Number,delayHide:Number,clsDrop:String},defaults:{mode:"hover",toggle:"- :first",boundary:window,boundaryAlign:!1,delayShow:0,delayHide:800,clsDrop:!1,hoverIdle:200,animation:"uk-animation-fade",cls:"uk-open"},init:function(){this.tracker=new Y,this.clsDrop=this.clsDrop||"uk-"+this.$options.name,this.clsPos=this.clsDrop,this.$el.addClass(this.clsDrop)},ready:function(){this.updateAria(this.$el),this.toggle&&(this.toggle=t.toggle(p(this.toggle,this.$el),{target:this.$el,mode:this.mode}))},events:[{name:"click",delegate:function(){return"."+this.clsDrop+"-close"},handler:function(t){t.preventDefault(),this.hide(!1)}},{name:"click",delegate:function(){return'a[href^="#"]'},handler:function(t){if(!t.isDefaultPrevented()){var e=$(t.target).attr("href");1===e.length&&t.preventDefault(),1!==e.length&&l(e,this.$el)||this.hide(!1)}}},{name:"toggle",handler:function(t,e){e&&!this.$el.is(e.target)||(t.preventDefault(),this.isToggled(this.$el)?this.hide(!1):this.show(e,!1))}},{name:me,filter:function(){return"hover"===this.mode},handler:function(t){st(t)||(e&&e!==this&&e.toggle&&"hover"===e.toggle.mode&&!l(t.target,e.$el)&&!l(t.target,e.toggle.$el)&&e.hide(!1),t.preventDefault(),this.show(this.toggle))}},{name:"toggleShow",handler:function(t,e){e&&!this.$el.is(e.target)||(t.preventDefault(),this.show(e||this.toggle))}},{name:"toggleHide "+ve,handler:function(t,e){st(t)||e&&!this.$el.is(e.target)||(t.preventDefault(),this.toggle&&"hover"===this.toggle.mode&&this.hide())}},{name:"beforeshow",self:!0,handler:function(){this.clearTimers()}},{name:"show",self:!0,handler:function(){this.tracker.init(),this.toggle.$el.addClass(this.cls).attr("aria-expanded","true")}},{name:"beforehide",self:!0,handler:function(){this.clearTimers()}},{name:"hide",handler:function(t){var i=t.target;return this.$el.is(i)?(e=this.isActive()?null:e,this.toggle.$el.removeClass(this.cls).attr("aria-expanded","false").blur().find("a, button").blur(),void this.tracker.cancel()):void(e=null===e&&l(i,this.$el)&&this.isToggled(this.$el)?this:e)}}],update:{write:function(){if(this.$el.hasClass(this.cls)){c(this.$el,this.clsDrop+"-(stack|boundary)").css({top:"",left:""}),this.$el.toggleClass(this.clsDrop+"-boundary",this.boundaryAlign),this.dir=this.pos[0],this.align=this.pos[1];var t=G(this.boundary),e=this.boundaryAlign?t:G(this.toggle.$el);if("justify"===this.align){var i="y"===this.getAxis()?"width":"height";this.$el.css(i,e[i])}else this.$el.outerWidth()>Math.max(t.right-e.left,e.right-t.left)&&(this.$el.addClass(this.clsDrop+"-stack"),this.$el.trigger("stack",[this]));this.positionAt(this.$el,this.boundaryAlign?this.boundary:this.toggle.$el,this.boundary)}},events:["resize","orientationchange"]},methods:{show:function t(i,n){var o=this;void 0===n&&(n=!0);var t=function(){return!o.isToggled(o.$el)&&o.toggleElement(o.$el,!0)},s=function(){if(o.toggle=i||o.toggle,o.clearTimers(),!o.isActive()){if(n&&e&&e!==o&&e.isDelaying)return void(o.showTimer=setTimeout(o.show,10));if(o.isParentOf(e)){if(!e.hideTimer)return;e.hide(!1)}else if(e&&!o.isChildOf(e)&&!o.isParentOf(e))for(var s;e&&e!==s;)s=e,e.hide(!1);n&&o.delayShow?o.showTimer=setTimeout(t,o.delayShow):t(),e=o}};i&&this.toggle&&!this.toggle.$el.is(i.$el)?(this.$el.one("hide",s),this.hide(!1)):s()},hide:function t(e){var i=this;void 0===e&&(e=!0);var t=function(){return i.toggleNow(i.$el,!1)};this.clearTimers(),this.isDelaying=this.tracker.movesTo(this.$el),e&&this.isDelaying?this.hideTimer=setTimeout(this.hide,this.hoverIdle):e&&this.delayHide?this.hideTimer=setTimeout(t,this.delayHide):t()},clearTimers:function(){clearTimeout(this.showTimer),clearTimeout(this.hideTimer),this.showTimer=null,this.hideTimer=null,this.isDelaying=!1},isActive:function(){return e===this},isChildOf:function(t){return t&&t!==this&&l(this.$el,t.$el)},isParentOf:function(t){return t&&t!==this&&l(t.$el,this.$el)}}}),t.drop.getActive=function(){return e}}function $t(t){t.component("dropdown",t.components.drop.extend({name:"dropdown"}))}function xt(t){t.component("form-custom",{mixins:[Fe],args:"target",props:{target:Boolean},defaults:{target:!1},ready:function(){this.input=this.$el.find(":input:first"),this.state=this.input.next(),this.target=this.target&&p(this.target===!0?"> :input:first + :first":this.target,this.$el),this.input.trigger("change")},events:[{name:"focus blur mouseenter mouseleave",delegate:":input:first",handler:function(t){var e=t.type;this.state.toggleClass("uk-"+(~["focus","blur"].indexOf(e)?"focus":"hover"),~["focus","mouseenter"].indexOf(e))}},{name:"change",handler:function(){this.target&&this.target[this.target.is(":input")?"val":"text"](this.input[0].files&&this.input[0].files[0]?this.input[0].files[0].name:this.input.is("select")?this.input.find("option:selected").text():this.input.val())}}]})}function kt(t){t.component("gif",{update:{read:function(){var t=d(this.$el);!this.isInView&&t&&(this.$el[0].src=this.$el[0].src),this.isInView=t},events:["scroll","load","resize","orientationchange"]}})}function Tt(t){t.component("grid",t.components.margin.extend({mixins:[Fe],name:"grid",defaults:{margin:"uk-grid-margin",clsStack:"uk-grid-stack"},update:{write:function(){this.$el.toggleClass(this.clsStack,this.stacks)},events:["load","resize","orientationchange"]}}))}function Ct(t){t.component("height-match",{args:"target",props:{target:String,row:Boolean},defaults:{target:"> *",row:!0},update:{write:function(){var t=this,e=Gt(this.target,this.$el).css("min-height","");if(!this.row)return this.match(e),this;var i=!1,n=[];e.each(function(e,o){o=Gt(o);var s=o.offset().top;s!=i&&n.length&&(t.match(Gt(n)),n=[],s=o.offset().top),n.push(o),i=s}),n.length&&this.match(Gt(n))},events:["resize","orientationchange"]},methods:{match:function(t){if(!(t.length<2)){var e=0;t.each(function(t,i){i=Gt(i);var n;if("none"===i.css("display")){var o=i.attr("style");i.attr("style",o+";display:block !important;"),n=i.outerHeight(),i.attr("style",o||"")}else n=i.outerHeight();e=Math.max(e,n)}).each(function(t,i){i=Gt(i),i.css("min-height",e-(i.outerHeight()-parseFloat(i.css("height")))+"px")})}}}})}function St(e){e.component("height-viewport",{props:{expand:Boolean,offsetTop:Boolean,offsetBottom:Boolean},defaults:{expand:!1,offsetTop:!1,offsetBottom:!1},init:function(){this.$emit()},update:{write:function(){this.$el.css("boxSizing","border-box");var e,i=window.innerHeight,n=0;if(this.expand){this.$el.css({height:"",minHeight:""});var o=i-document.documentElement.offsetHeight;o>0&&this.$el.css("min-height",e=this.$el.outerHeight()+o)}else{var s=this.$el.offset().top;if(s=this.$el.outerHeight()&&this.$el.css("height",e)},events:["load","resize","orientationchange"]}})}function Et(t){i(function(){if(de){var e="uk-hover";Kt.on("tap",function(t){var i=t.target;return Gt("."+e).filter(function(t,e){return!l(i,e)}).removeClass(e)}),Object.defineProperty(t,"hoverSelector",{set:function(t){Kt.on("tap",t,function(t){var i=t.currentTarget;return i.classList.add(e)})}}),t.hoverSelector=".uk-animation-toggle, .uk-transition-toggle, [uk-hover]"}})}function At(e){function i(t,i){e.component(t,e.components.icon.extend({name:t,mixins:i?[i]:[],defaults:{icon:t}}))}var n={},o={spinner:ii,totop:ni,"close-icon":Ue,"close-large":Re,"navbar-toggle-icon":We,"overlay-icon":Ye,"pagination-next":Ve,"pagination-previous":Qe,"search-icon":Xe,"search-large":Ge,"search-navbar":Je,"slidenav-next":Ze,"slidenav-next-large":Ke,"slidenav-previous":ti,"slidenav-previous-large":ei};e.component("icon",e.components.svg.extend({mixins:[Fe],name:"icon",args:"icon",props:["icon"],defaults:{exclude:["id","style","class","src"]},init:function(){this.$el.addClass("uk-icon"),te&&(this.icon=H(H(this.icon,"left","right"),"previous","next"))},update:{read:function(){if(this.delay){var t=this.getIcon();t&&this.delay(t)}},events:["load"]},methods:{getSvg:function(){var t=this,e=this.getIcon();return e?w.resolve(e):"complete"!==document.readyState?w(function(e){t.delay=e}):w.reject("Icon not found.")},getIcon:function(){return o[this.icon]?(n[this.icon]||(n[this.icon]=this.parse(o[this.icon])),n[this.icon]):null}}})),["navbar-toggle-icon","overlay-icon","pagination-previous","pagination-next","totop"].forEach(function(t){return i(t)}),["slidenav-previous","slidenav-next"].forEach(function(t){return i(t,{init:function(){this.$el.addClass("uk-slidenav"),this.$el.hasClass("uk-slidenav-large")&&(this.icon+="-large")}})}),i("search-icon",{init:function(){this.$el.hasClass("uk-search-icon")&&this.$el.parents(".uk-search-large").length?this.icon="search-large":this.$el.parents(".uk-search-navbar").length&&(this.icon="search-navbar")}}),i("close",{init:function(){this.icon="close-"+(this.$el.hasClass("uk-close-large")?"large":"icon")}}),i("spinner",{connected:function(){var t=this;this.height=this.width=this.$el.width(),this.svg.then(function(e){var i=e.find("circle"),n=Math.floor(t.width/2);e[0].setAttribute("viewBox","0 0 "+t.width+" "+t.width),i.attr({cx:n,cy:n,r:n-parseFloat(i.css("stroke-width")||0)})})}}),e.icon.add=function(e){t.extend(o,e)}}function Dt(t){t.component("margin",{props:{margin:String,firstColumn:Boolean},defaults:{margin:"uk-margin-small-top",firstColumn:"uk-first-column"},connected:function(){this.$emit()},update:{read:function(){var t=this;if(0===this.$el[0].offsetHeight)return void(this.hidden=!0);this.hidden=!1,this.stacks=!0;var e=this.$el.children().filter(function(t,e){return e.offsetHeight>0});this.rows=[[e.get(0)]],e.slice(1).each(function(e,i){for(var n=Math.ceil(i.offsetTop),o=n+i.offsetHeight,s=t.rows.length-1;s>=0;s--){var r=t.rows[s],a=Math.ceil(r[0].offsetTop);if(n>=a+r[0].offsetHeight){t.rows.push([i]);break}if(o>a){if(t.stacks=!1,i.offsetLeftthis.panel.outerHeight(!0)).css("display",this.$el.hasClass("uk-flex")?"":"block")},events:["resize","orientationchange"]},events:[{name:"beforeshow",self:!0,handler:function(){this.$el.css("display","block").height()}},{name:"hide",self:!0,handler:function(){this.$el.css("display","").removeClass("uk-flex uk-flex-center uk-flex-middle")}}]}),e.component("overflow-auto",{mixins:[Fe],ready:function(){this.panel=p("!.uk-modal-dialog",this.$el),this.$el.css("min-height",150)},update:{write:function(){var t=this.$el.css("max-height");this.$el.css("max-height",150).css("max-height",Math.max(150,150-(this.panel.outerHeight(!0)-window.innerHeight))),t!==this.$el.css("max-height")&&this.$el.trigger("resize")},events:["load","resize","orientationchange"]}}),e.modal.dialog=function(t,i){var n=e.modal('
\n
'+t+"
\n
",i);return n.$el.on("hide",function(){return n.$destroy(!0)}),n.show(),n},e.modal.alert=function(i,n){return n=t.extend({bgClose:!1,escClose:!1,labels:e.modal.labels},n),w(function(t){return e.modal.dialog('\n
'+(T(i)?i:Gt(i).html())+'
\n \n ",n).$el.on("hide",t)})},e.modal.confirm=function(i,n){return n=t.extend({bgClose:!1,escClose:!1,labels:e.modal.labels},n),w(function(t,o){return e.modal.dialog('\n
'+(T(i)?i:Gt(i).html())+'
\n \n ",n).$el.on("click",".uk-modal-footer button",function(e){return 0===Gt(e.target).index()?o():t()})})},e.modal.prompt=function(i,n,o){return o=t.extend({bgClose:!1,escClose:!1,labels:e.modal.labels},o),w(function(t,s){var r=!1,a=e.modal.dialog('\n
\n
\n \n \n
\n \n
\n ",o),l=a.$el.find("input").val(n);a.$el.on("submit","form",function(e){e.preventDefault(),t(l.val()),r=!0,a.hide()}).on("hide",function(){r||t(null)})})},e.modal.labels={ok:"Ok",cancel:"Cancel"}}function Ot(t){t.component("nav",t.components.accordion.extend({name:"nav",defaults:{targets:"> .uk-parent",toggle:"> a",content:"ul:first"}}))}function It(e){e.component("navbar",{mixins:[Fe], +props:{dropdown:String,mode:String,align:String,offset:Number,boundary:Boolean,boundaryAlign:Boolean,clsDrop:String,delayShow:Number,delayHide:Number,dropbar:Boolean,dropbarMode:String,dropbarAnchor:"jQuery",duration:Number},defaults:{dropdown:".uk-navbar-nav > li",align:te?"right":"left",clsDrop:"uk-navbar-dropdown",mode:void 0,offset:void 0,delayShow:void 0,delayHide:void 0,boundaryAlign:void 0,flip:"x",boundary:!0,dropbar:!1,dropbarMode:"slide",dropbarAnchor:!1,duration:200},init:function(){this.boundary=this.boundary===!0||this.boundaryAlign?this.$el:this.boundary,this.pos="bottom-"+this.align},ready:function(){var t=this;this.$el.on(me,this.dropdown,function(e){var i=e.target,n=t.getActive(),o=n&&n.$el.closest(t.dropdown);n&&o.length&&!l(i,o)&&!n.isDelaying&&n.hide(!1)}),this.dropbar&&(this.dropbar=p(this.dropbar,this.$el)||Gt("
").insertAfter(this.dropbarAnchor||this.$el),e.navbarDropbar(this.dropbar,{clsDrop:this.clsDrop,mode:this.dropbarMode,duration:this.duration,navbar:this}))},update:function(){var i=this;Gt(this.dropdown,this.$el).each(function(n,o){var s=D("."+i.clsDrop,o);s&&e.drop(s,t.extend({},i))})},events:{beforeshow:function(t,e){var i=e.$el,n=e.dir;this.dropbar&&"bottom"===n&&!l(i,this.dropbar)&&(i.appendTo(this.dropbar),this.dropbar.trigger("beforeshow",[{$el:i}]))}},methods:{getActive:function(){var t=e.drop.getActive();return t&&"click"!==t.mode&&l(t.toggle.$el,this.$el)&&t}}}),e.component("navbar-dropbar",{mixins:[Fe],defaults:{clsDrop:"",mode:"slide",navbar:null,duration:200},init:function(){"slide"===this.mode&&this.$el.addClass("uk-navbar-dropbar-slide")},events:{mouseleave:function(){var t=this.navbar.getActive();t&&!this.$el.is(":hover")&&t.hide()},beforeshow:function(t,e){var i=e.$el;this.clsDrop&&i.addClass(this.clsDrop+"-dropbar"),this.transitionTo(i.outerHeight(!0))},beforehide:function(t,e){var i=e.$el,n=this.navbar.getActive();if(this.$el.is(":hover")&&n&&n.$el.is(i))return!1},hide:function(t,e){var i=e.$el,n=this.navbar.getActive();(!n||n&&n.$el.is(i))&&this.transitionTo(0)}},methods:{transitionTo:function(t){var e=this;return this.$el.height(this.$el[0].offsetHeight?this.$el.height():0),ee.cancel(this.$el).then(function(){return ee.start(e.$el,{height:t},e.duration)})}}})}function Pt(t){t.component("offcanvas",{mixins:[ze],args:"mode",props:{mode:String,flip:Boolean,overlay:Boolean},defaults:{mode:"slide",flip:!1,overlay:!1,clsPage:"uk-offcanvas-page",clsPanel:"uk-offcanvas-bar",clsFlip:"uk-offcanvas-flip",clsPageAnimation:"uk-offcanvas-page-animation",clsSidebarAnimation:"uk-offcanvas-bar-animation",clsMode:"uk-offcanvas",clsOverlay:"uk-offcanvas-overlay",clsPageOverlay:"uk-offcanvas-page-overlay",selClose:".uk-offcanvas-close"},init:function(){this.clsFlip=this.flip?this.clsFlip:"",this.clsOverlay=this.overlay?this.clsOverlay:"",this.clsPageOverlay=this.overlay?this.clsPageOverlay:"",this.clsMode=this.clsMode+"-"+this.mode,"none"!==this.mode&&"reveal"!==this.mode||(this.clsSidebarAnimation=""),"push"!==this.mode&&"reveal"!==this.mode&&(this.clsPageAnimation="")},update:{write:function(){this.isActive()&&Kt.width(window.innerWidth-this.getScrollbarWidth())},events:["resize","orientationchange"]},events:[{name:"beforeshow",self:!0,handler:function(){Kt.addClass(this.clsFlip+" "+this.clsPageAnimation+" "+this.clsPageOverlay),this.panel.addClass(this.clsSidebarAnimation+" "+this.clsMode),this.$el.addClass(this.clsOverlay).css("display","block").height()}},{name:"beforehide",self:!0,handler:function(){Kt.removeClass(this.clsPageAnimation),("none"===this.mode||this.getActive()&&this.getActive()!==this)&&this.panel.trigger(be)}},{name:"hide",self:!0,handler:function(){Kt.removeClass(this.clsFlip+" "+this.clsPageOverlay).width(""),this.panel.removeClass(this.clsSidebarAnimation+" "+this.clsMode),this.$el.removeClass(this.clsOverlay).css("display","")}}]})}function Nt(t){t.component("responsive",{props:["width","height"],init:function(){this.$el.addClass("uk-responsive-width")},update:{write:function(){this.$el.is(":visible")&&this.width&&this.height&&this.$el.height(oe.fit({height:this.height,width:this.width},{width:this.$el.parent().width(),height:this.height||this.$el.height()}).height)},events:["load","resize","orientationchange"]}})}function Bt(t){t.component("scroll",{props:{duration:Number,transition:String,offset:Number},defaults:{duration:1e3,transition:"easeOutExpo",offset:0},methods:{scrollToElement:function(t){var e=this;t=Gt(t);var i=t.offset().top-this.offset,n=Zt.height(),o=window.innerHeight;i+o>n&&(i=n-o),Gt("html,body").stop().animate({scrollTop:parseInt(i,10)||1},this.duration,this.transition).promise().then(function(){return e.$el.trigger("scrolled",[e])})}},events:{click:function(t){t.isDefaultPrevented()||(t.preventDefault(),this.scrollToElement(Gt(this.$el[0].hash).length?this.$el[0].hash:"body"))}}}),Gt.easing.easeOutExpo||(Gt.easing.easeOutExpo=function(t,e,i,n,o){return e==o?i+n:n*(-Math.pow(2,-10*e/o)+1)+i})}function Ht(t){t.component("scrollspy",{args:"cls",props:{cls:String,target:String,hidden:Boolean,offsetTop:Number,offsetLeft:Number,repeat:Boolean,delay:Number},defaults:{cls:"uk-scrollspy-inview",target:!1,hidden:!0,offsetTop:0,offsetLeft:0,repeat:!1,delay:0,inViewClass:"uk-scrollspy-inview"},init:function(){this.$emit()},update:[{read:function(){this.elements=this.target&&Gt(this.target,this.$el)||this.$el},write:function(){this.hidden&&this.elements.filter(":not(."+this.inViewClass+")").css("visibility","hidden")}},{read:function(){var t=this;this.elements.each(function(e,i){i._scrollspy||(i._scrollspy={toggles:(Gt(i).attr("uk-scrollspy-class")||t.cls).split(",")}),i._scrollspy.show=d(i,t.offsetTop,t.offsetLeft)})},write:function(){var t=this,e=1===this.elements.length?1:0;this.elements.each(function(i,n){var o=Gt(n),s=n._scrollspy;s.show?s.inview||s.timer||(s.timer=setTimeout(function(){o.css("visibility","").addClass(t.inViewClass).toggleClass(s.toggles[0]).trigger("inview"),s.inview=!0,delete s.timer},t.delay*e++)):s.inview&&t.repeat&&(s.timer&&(clearTimeout(s.timer),delete s.timer),o.removeClass(t.inViewClass).toggleClass(s.toggles[0]).css("visibility",t.hidden?"hidden":"").trigger("outview"),s.inview=!1),s.toggles.reverse()})},events:["scroll","load","resize","orientationchange"]}]})}function Mt(t){t.component("scrollspy-nav",{props:{cls:String,closest:String,scroll:Boolean,overflow:Boolean,offset:Number},defaults:{cls:"uk-active",closest:!1,scroll:!1,overflow:!0,offset:0},update:[{read:function(){var e=this;this.links=this.$el.find('a[href^="#"]').filter(function(t,e){return e.hash}),this.elements=this.closest?this.links.closest(this.closest):this.links,this.targets=Gt(Gt.map(this.links,function(t){return t.hash}).join(",")),this.scroll&&this.links.each(function(i,n){return t.scroll(n,{offset:e.offset||0})})}},{read:function(){var t=this,e=Jt.scrollTop()+this.offset,i=document.documentElement.scrollHeight-window.innerHeight+this.offset;this.active=!1,this.targets.each(function(n,o){o=Gt(o);var s=o.offset(),r=n+1===t.targets.length;if(!t.overflow&&(0===n&&s.top>e||r&&s.top+o.outerHeight()=i)for(var a=t.targets.length;a>n;a--)if(d(t.targets.eq(a))){o=t.targets.eq(a);break}return!(t.active=D(t.links.filter('[href="#'+o.attr("id")+'"]')))}})},write:function(){this.links.blur(),this.elements.removeClass(this.cls),this.active&&this.$el.trigger("active",[this.active,(this.closest?this.active.closest(this.closest):this.active).addClass(this.cls)])},events:["scroll","load","resize","orientationchange"]}]})}function jt(e){e.component("sticky",{mixins:[Fe],attrs:!0,props:{top:null,bottom:Boolean,offset:Number,animation:String,clsActive:String,clsInactive:String,clsFixed:String,widthElement:"jQuery",showOnUp:Boolean,media:"media",target:Number},defaults:{top:0,bottom:!1,offset:0,animation:"",clsActive:"uk-active",clsInactive:"",clsFixed:"uk-sticky-fixed",widthElement:!1,showOnUp:!1,media:!1,target:!1},init:function(){this.$el.addClass(this.clsInactive)},connected:function(){this.placeholder=Gt('
').insertAfter(this.$el).attr("hidden",!0),this.widthElement=this.$props.widthElement||this.placeholder},disconnected:function(){this.isActive&&(this.isActive=!1,this.hide(),this.$el.removeClass(this.clsInactive)),this.placeholder.remove(),this.placeholder=null,this.widthElement=null},ready:function(){var t=this;if(this.target&&location.hash&&Jt.scrollTop()>0){var e=p(location.hash);e&&ce(function(){var i=e.offset().top,n=t.$el.offset().top,o=t.$el.outerHeight(),s=n+o;s>=i&&n<=i+e.outerHeight()&&window.scrollTo(0,i-o-t.target-t.offset)})}},update:[{write:function(){var e,i=this,n=this.$el.outerHeight();this.placeholder.css("height","absolute"!==this.$el.css("position")?n:"").css(this.$el.css(["marginTop","marginBottom","marginLeft","marginRight"])),this.width=this.widthElement.attr("hidden",null).outerWidth(),this.widthElement.attr("hidden",!this.isActive),this.topOffset=(this.isActive?this.placeholder.offset():this.$el.offset()).top,this.bottomOffset=this.topOffset+n,["top","bottom"].forEach(function(n){i[n]=i.$props[n],i[n]&&(t.isNumeric(i[n])?i[n]=i[n+"Offset"]+parseFloat(i[n]):T(i[n])&&i[n].match(/^-?\d+vh$/)?i[n]=window.innerHeight*parseFloat(i[n])/100:(e=i[n]===!0?i.$el.parent():p(i[n],i.$el),e&&(i[n]=e.offset().top+e.outerHeight())))}),this.top=Math.max(parseFloat(this.top),this.topOffset)-this.offset,this.bottom=this.bottom&&this.bottom-n,this.inactive=this.media&&!window.matchMedia(this.media).matches,this.isActive&&this.update()},events:["load","resize","orientationchange"]},{write:function(t){var e=this;void 0===t&&(t={});var i=t.dir,n=Jt.scrollTop();if(!(n<0||!this.$el.is(":visible")||this.disabled))if(this.inactive||nthis.top;this.bottom&&e>this.bottom-this.offset&&(t=this.bottom-e),this.$el.css({position:"fixed",top:t+"px",width:this.width}).addClass(this.clsFixed).toggleClass(this.clsActive,i).toggleClass(this.clsInactive,!i)}}})}function Ft(t){t.component("svg",{attrs:!0,props:{id:String,icon:String,src:String,class:String,style:String,width:Number,height:Number,ratio:Number},defaults:{ratio:1,id:!1,class:"",exclude:["src"]},init:function(){this.class+=" uk-svg"},connected:function(){var t=this;if(!this.icon&&this.src&&~this.src.indexOf("#")){var e=this.src.split("#");e.length>1&&(this.src=e[0],this.icon=e[1])}this.width=this.$props.width,this.height=this.$props.height,this.svg=this.getSvg().then(function(e){return w(function(i,n){return xe.mutate(function(){var o,s;if(!e)return void n("SVG not found.");if(t.icon)if(o=e.getElementById(t.icon)){var r=o.outerHTML;if(!r){var a=document.createElement("div");a.appendChild(o.cloneNode(!0)),r=a.innerHTML}r=r.replace(//g,"svg>"),s=si.parseFromString(r,"image/svg+xml").documentElement}else e.querySelector("symbol")||(s=e.documentElement.cloneNode(!0));else s=e.documentElement.cloneNode(!0);if(!s)return void n("SVG not found.");var l=s.getAttribute("viewBox");l&&(l=l.split(" "),t.width=t.width||l[2],t.height=t.height||l[3]),s=Gt(s),t.width*=t.ratio,t.height*=t.ratio;for(var h in t.$options.props)t[h]&&!~t.exclude.indexOf(h)&&s.attr(h,t[h]);t.id||s.removeAttr("id"),t.width&&!t.height&&s.removeAttr("height"),t.height&&!t.width&&s.removeAttr("width"),g(t.$el)||"CANVAS"===t.$el[0].tagName?(t.$el.attr({hidden:!0,id:null}),s.insertAfter(t.$el)):s.appendTo(t.$el),i(s)})})}).then(null,function(){return t.$destroy()}),this._isReady||this.$emitSync()},disconnected:function(){g(this.$el)&&this.$el.attr({hidden:null,id:this.id||null}),this.svg&&(this.svg.then(function(t){t&&t.remove()}),this.svg=null)},methods:{getSvg:function(){var t=this;return this.src?oi[this.src]?oi[this.src]:(oi[this.src]=w(function(e,i){0===t.src.lastIndexOf("data:",0)?e(t.parse(decodeURIComponent(t.src.split(",")[1]))):Gt.ajax(t.src,{dataType:"html"}).then(function(i){e(t.parse(i))},function(){i("SVG not found.")})}),oi[this.src]):w.reject()},parse:function(t){var e=si.parseFromString(t,"image/svg+xml");return e.documentElement&&"svg"===e.documentElement.nodeName?e:null}}})}function Lt(t){t.component("switcher",{mixins:[Le],args:"connect",props:{connect:"jQuery",toggle:String,active:Number,swiping:Boolean},defaults:{connect:!1,toggle:" > *",active:0,swiping:!0,cls:"uk-active",clsContainer:"uk-switcher",attrItem:"uk-switcher-item",queued:!0},connected:function(){this.$emitSync()},events:[{name:"click",delegate:function(){return this.toggle+":not(.uk-disabled)"},handler:function(t){t.preventDefault(),this.show(t.currentTarget)}}],update:function(){var t=this;this.toggles=Gt(this.toggle,this.$el),this.connects=this.connect||Gt(this.$el.next("."+this.clsContainer));var e="click."+this.$options.name;if(this.connects.off(e).on(e,"["+this.attrItem+"],[data-"+this.attrItem+"]",function(e){e.preventDefault(),t.show(Gt(e.currentTarget)[e.currentTarget.hasAttribute(t.attrItem)?"attr":"data"](t.attrItem))}),this.swiping){var i="swipeRight."+this.$options.name+" swipeLeft."+this.$options.name;this.connects.off(i).on(i,function(e){st(e)&&(e.preventDefault(),window.getSelection().toString()||t.show("swipeLeft"==e.type?"next":"previous"))})}this.updateAria(this.connects.children()),this.show(D(this.toggles.filter("."+this.cls+":first"))||D(this.toggles.eq(this.active))||this.toggles.first())},methods:{show:function(t){for(var e,i=this,n=this.toggles.length,o=this.connects.children("."+this.cls).index(),s=o>=0,r=f(t,this.toggles,o),a="previous"===t?-1:1,l=0;l=0&&e.hasClass(this.cls)||o===r||(this.toggles.removeClass(this.cls).attr("aria-expanded",!1),e.addClass(this.cls).attr("aria-expanded",!0),s?this.toggleElement(this.connects.children(":nth-child("+(o+1)+"),:nth-child("+(r+1)+")")):this.toggleNow(this.connects.children(":nth-child("+(r+1)+")")))}}})}function zt(t){t.component("tab",t.components.switcher.extend({mixins:[Fe],name:"tab",defaults:{media:960,attrItem:"uk-tab-item"},init:function(){var e=this.$el.hasClass("uk-tab-left")&&"uk-tab-left"||this.$el.hasClass("uk-tab-right")&&"uk-tab-right";e&&t.toggle(this.$el,{cls:e,mode:"media",media:this.media})}}))}function qt(t){t.component("toggle",{mixins:[t.mixin.toggable],args:"target",props:{href:"jQuery",target:"jQuery",mode:String,media:"media"},defaults:{href:!1,target:!1,mode:"click",queued:!0,media:!1},events:[{name:me+" "+ve,filter:function(){return"hover"===this.mode},handler:function(t){st(t)||this.toggle(t.type===me?"toggleShow":"toggleHide")}},{name:"click",filter:function(){return"media"!==this.mode},handler:function(t){(Gt(t.target).closest('a[href="#"], button').length||Gt(t.target).closest("a[href]")&&(this.cls||!this.target.is(":visible")))&&t.preventDefault(),this.toggle()}}],update:{write:function(){if(this.target=this.target||this.href||this.$el,"media"===this.mode&&this.media){var t=this.isToggled(this.target);(window.matchMedia(this.media).matches?!t:t)&&this.toggle()}},events:["load","resize","orientationchange"]},methods:{toggle:function(t){var e=Gt.Event(t||"toggle");this.target.triggerHandler(e,[this]),e.isDefaultPrevented()||this.toggleElement(this.target)}}})}function Ut(t){var e,i,o,s=null,r=0;Jt.on("load",t.update).on("resize orientationchange",function(e){o||(ce(function(){t.update(e),o=!1}),o=!0)}).on("scroll",function(n){null===s&&(s=0),e=s\n \n ',{center:!0}),this.modal.$el.css("overflow","hidden").addClass("uk-modal-lightbox"),this.modal.panel.css({width:200,height:200}),this.modal.caption=n('
').appendTo(this.modal.panel),this.items.length>1&&n('
\n \n \n
\n ').appendTo(this.modal.panel.addClass("uk-slidenav-position")),this.modal.$el.on("hide",this.hide).on("click","["+this.attrItem+"]",function(t){t.preventDefault(),o.show(n(t.currentTarget).attr(o.attrItem))}).on("swipeRight swipeLeft",function(t){t.preventDefault(),window.getSelection().toString()||o.show("swipeLeft"==t.type?"next":"previous")})),e=this,this.modal.panel.find("[uk-transition-hide]").hide(),this.modal.panel.find("[uk-transition-show]").show(),this.modal.content&&this.modal.content.remove(),this.modal.caption.text(this.getItem().title);var s=n.Event("showitem");this.$el.trigger(s),s.isImmediatePropagationStopped()||this.setError(this.getItem())},hide:function(){var t=this;e=e&&e!==this&&e,this.modal.hide().then(function(){t.modal.$destroy(!0),t.modal=null})},getItem:function(){return this.items[this.index]||{source:"",title:"",type:""}},setItem:function(t,e,i,n){void 0===i&&(i=200),void 0===n&&(n=200),s(t,{content:e,width:i,height:n}),this.$update()},setError:function(t){this.setItem(t,'
Loading resource failed!
',400,300)}}}),t.mixin({events:{showitem:function(t){var e=this,i=this.getItem();if("image"===i.type||!i.source||i.source.match(/\.(jp(e)?g|png|gif|svg)$/i)){var n=new Image;n.onerror=function(){return e.setError(i)},n.onload=function(){return e.setItem(i,'',n.width,n.height)},n.src=i.source,t.stopImmediatePropagation()}}}},"lightbox"),t.mixin({events:{showitem:function(t){var e=this,i=this.getItem();if("video"===i.type||!i.source||i.source.match(/\.(mp4|webm|ogv)$/i)){var o=n('').on("loadedmetadata",function(){return e.setItem(i,o.attr({width:o[0].videoWidth,height:o[0].videoHeight}),o[0].videoWidth,o[0].videoHeight)}).attr("src",i.source);t.stopImmediatePropagation()}}}},"lightbox"),t.mixin({events:{showitem:function(t){var e,i=this,n=this.getItem();if((e=n.source.match(/\/\/.*?youtube\.[a-z]+\/watch\?v=([^&]+)&?(.*)/))||n.source.match(/youtu\.be\/(.*)/)){var o=e[1],s=new Image,r=!1,a=function(t,e){return i.setItem(n,'',t,e)};s.onerror=function(){return a(640,320)},s.onload=function(){120===s.width&&90===s.height?r?a(640,320):(r=!0,s.src="//img.youtube.com/vi/"+o+"/0.jpg"):a(s.width,s.height)},s.src="//img.youtube.com/vi/"+o+"/maxresdefault.jpg",t.stopImmediatePropagation()}}}},"lightbox"),t.mixin({events:{showitem:function(t){var e,i=this,o=this.getItem();if(e=o.source.match(/(\/\/.*?)vimeo\.[a-z]+\/([0-9]+).*?/)){var s=e[2],r=function(t,e){return i.setItem(o,'',t,e)};n.ajax({type:"GET",url:"http://vimeo.com/api/oembed.json?url="+encodeURI(o.source),jsonp:"callback",dataType:"jsonp"}).then(function(t){return r(t.width,t.height)}),t.stopImmediatePropagation()}}}},"lightbox")}}function Yt(t){if(!Yt.installed){var e=t.util,i=e.$,n=e.each,o=e.pointerEnter,s=e.pointerLeave,r=e.Transition,a={};t.component("notification",{functional:!0,args:["message","status"],defaults:{message:"",status:"",timeout:5e3,group:null,pos:"top-center",onClose:null},created:function(){a[this.pos]||(a[this.pos]=i('
').appendTo(t.container)),this.$mount(i('
\n \n
'+this.message+"
\n
").appendTo(a[this.pos].show())[0])},ready:function(){var t=this,e=parseInt(this.$el.css("margin-bottom"),10);r.start(this.$el.css({opacity:0,marginTop:-1*this.$el.outerHeight(),marginBottom:0}),{opacity:1,marginTop:0,marginBottom:e}).then(function(){t.timeout&&(t.timer=setTimeout(t.close,t.timeout),t.$el.on(o,function(){return clearTimeout(t.timer)}).on(s,function(){return t.timer=setTimeout(t.close,t.timeout)}))})},events:{click:function(t){t.preventDefault(),this.close()}},methods:{close:function(t){var e=this,i=function(){e.onClose&&e.onClose(),e.$el.trigger("close",[e]).remove(),a[e.pos].children().length||a[e.pos].hide()};this.timer&&clearTimeout(this.timer),t?i():r.start(this.$el,{opacity:0,marginTop:-1*this.$el.outerHeight(),marginBottom:0}).then(i)}}}),t.notification.closeAll=function(e,i){n(t.instances,function(t,n){"notification"!==n.$options.name||e&&e!==n.group||n.close(i)})}}}function Vt(t){function e(i){return t.getComponent(i,"sortable")||i.parentNode&&e(i.parentNode)}function i(){var t=setTimeout(function(){return r.trigger("click")},0),e=function(i){i.preventDefault(),i.stopPropagation(),clearTimeout(t),c(r,"click",e,!0)};h(r,"click",e,!0)}if(!Vt.installed){var n=t.mixin,o=t.util,s=o.$,r=o.docElement,a=o.extend,l=o.isWithin,h=o.on,c=o.off,u=o.pointerDown,d=o.pointerMove,f=o.pointerUp,g=o.win;t.component("sortable",{mixins:[n.class],props:{group:String,animation:Number,threshold:Number,clsItem:String,clsPlaceholder:String,clsDrag:String,clsDragState:String,clsBase:String,clsNoDrag:String,clsEmpty:String,clsCustom:String,handle:String},defaults:{group:!1,animation:150,threshold:5,clsItem:"uk-sortable-item",clsPlaceholder:"uk-sortable-placeholder",clsDrag:"uk-sortable-drag",clsDragState:"uk-drag",clsBase:"uk-sortable",clsNoDrag:"uk-sortable-nodrag",clsEmpty:"uk-sortable-empty",clsCustom:"",handle:!1},init:function(){var t=this;["init","start","move","end"].forEach(function(e){var i=t[e];t[e]=function(e){e=e.originalEvent||e,t.scrollY=window.scrollY;var n=e.touches&&e.touches[0]||e,o=n.pageX,s=n.pageY;t.pos={x:o,y:s},i(e)}})},events:(p={},p[u]="init",p),update:{write:function(){var t=this;if(this.clsEmpty&&this.$el.toggleClass(this.clsEmpty,!this.$el.children().length),this.drag){this.drag.offset({top:this.pos.y+this.origin.top,left:this.pos.x+this.origin.left});var e=this.drag.offset().top,i=e+this.drag[0].offsetHeight;e>0&&ewindow.innerHeight+this.scrollY&&setTimeout(function(){return g.scrollTop(t.scrollY+5)},5)}}},methods:{init:function(t){var e=s(t.target),i=this.$el.children().filter(function(e,i){return l(t.target,i)});!i.length||e.is(":input")||this.handle&&!l(e,this.handle)||t.button&&0!==t.button||l(e,"."+this.clsNoDrag)||(t.preventDefault(),t.stopPropagation(),this.touched=[this],this.placeholder=i,this.origin=a({target:e,index:this.placeholder.index()},this.pos),r.on(d,this.move),r.on(f,this.end),g.on("scroll",this.scroll),this.threshold||this.start(t))},start:function(e){this.drag=s(this.placeholder[0].outerHTML.replace(/^
  • $/i,"div>")).attr("uk-no-boot","").addClass(this.clsDrag+" "+this.clsCustom).css({boxSizing:"border-box",width:this.placeholder.outerWidth(),height:this.placeholder.outerHeight()}).css(this.placeholder.css(["paddingLeft","paddingRight","paddingTop","paddingBottom"])).appendTo(t.container),this.drag.children().first().height(this.placeholder.children().height());var i=this.placeholder.offset(),n=i.left,o=i.top;a(this.origin,{left:n-this.pos.x,top:o-this.pos.y}),this.placeholder.addClass(this.clsPlaceholder),this.$el.children().addClass(this.clsItem),r.addClass(this.clsDragState),this.$el.trigger("start",[this,this.placeholder,this.drag]),this.move(e)},move:function t(i){if(!this.drag)return void((Math.abs(this.pos.x-this.origin.x)>this.threshold||Math.abs(this.pos.y-this.origin.y)>this.threshold)&&this.start(i));this.$emit();var n="mousemove"===i.type?i.target:document.elementFromPoint(this.pos.x-document.body.scrollLeft,this.pos.y-document.body.scrollTop),o=e(n),r=e(this.placeholder[0]),t=o!==r;if(o&&!l(n,this.placeholder)&&(!t||o.group&&o.group===r.group)){if(n=o.$el.is(n.parentNode)&&s(n)||o.$el.children().has(n),t)r.remove(this.placeholder);else if(!n.length)return;o.insert(this.placeholder,n),~this.touched.indexOf(o)||this.touched.push(o)}},scroll:function t(){var t=window.scrollY;t!==this.scrollY&&(this.pos.y+=t-this.scrollY,this.scrollY=t,this.$emit())},end:function(t){if(r.off(d,this.move),r.off(f,this.end),g.off("scroll",this.scroll),!this.drag)return void("mouseup"!==t.type&&l(t.target,"a[href]")&&(location.href=s(t.target).closest("a[href]").attr("href")));i();var n=e(this.placeholder[0]);this===n?this.origin.index!==this.placeholder.index()&&this.$el.trigger("change",[this,this.placeholder,"moved"]):(n.$el.trigger("change",[n,this.placeholder,"added"]),this.$el.trigger("change",[this,this.placeholder,"removed"])),this.$el.trigger("stop",[this]),this.drag.remove(),this.drag=null,this.touched.forEach(function(t){return t.$el.children().removeClass(t.clsPlaceholder+" "+t.clsItem)}),r.removeClass(this.clsDragState)},insert:function t(e,i){var n=this;this.$el.children().addClass(this.clsItem);var t=function(){i.length?!n.$el.has(e).length||e.prevAll().filter(i).length?e.insertBefore(i):e.insertAfter(i):n.$el.append(e)};this.animation?this.animate(t):t()},remove:function(t){this.$el.has(t).length&&(this.animation?this.animate(function(){return t.detach()}):t.detach())},animate:function(t){var e=this,i=[],n=this.$el.children().toArray().map(function(t){return t=s(t),i.push(a({position:"absolute",pointerEvents:"none",width:t.outerWidth(),height:t.outerHeight()},t.position())),t}),o={position:"",width:"",height:"",pointerEvents:"",top:"",left:""};t(),n.forEach(function(t){return t.stop()}),this.$el.children().css(o),this.$updateSync("update",!0),this.$el.css("min-height",this.$el.height());var r=n.map(function(t){return t.position()});s.when.apply(s,n.map(function(t,n){return t.css(i[n]).animate(r[n],e.animation).promise()})).then(function(){e.$el.css("min-height","").children().css(o),e.$updateSync("update",!0)})}}});var p}}function Qt(t){if(!Qt.installed){var e,i=t.util,n=t.mixin,o=i.$,s=i.doc,r=i.fastdom,a=i.flipPosition,l=i.isTouch,h=i.isWithin,c=i.pointerDown,u=i.pointerEnter,d=i.pointerLeave,f=i.toJQuery;s.on("click",function(t){e&&!h(t.target,e.$el)&&e.hide()}),t.component("tooltip",{mixins:[n.toggable,n.position],props:{delay:Number,container:Boolean,title:String},defaults:{pos:"top",title:"",delay:0,animation:"uk-animation-scale-up",duration:100,cls:"uk-active",clsPos:"uk-tooltip",container:!0},init:function(){var e=this;this.container=this.container===!0&&t.container||this.container&&f(this.container),r.mutate(function(){return e.$el.removeAttr("title").attr("aria-expanded",!1)})},methods:{show:function(){var t=this;e!==this&&(e&&e.hide(),e=this,clearTimeout(this.showTimer),this.tooltip=o('").appendTo(this.container),this.$el.attr("aria-expanded",!0),this.positionAt(this.tooltip,this.$el),this.origin="y"===this.getAxis()?a(this.dir)+"-"+this.align:this.align+"-"+a(this.dir),this.showTimer=setTimeout(function(){t.toggleElement(t.tooltip,!0),t.hideTimer=setInterval(function(){t.$el.is(":visible")||t.hide()},150)},this.delay))},hide:function(){this.$el.is("input")&&this.$el[0]===document.activeElement||(e=e!==this&&e||!1,clearTimeout(this.showTimer),clearInterval(this.hideTimer),this.$el.attr("aria-expanded",!1),this.toggleElement(this.tooltip,!1),this.tooltip&&this.tooltip.remove(),this.tooltip=!1)}},events:(g={ +blur:"hide"},g["focus "+u+" "+c]=function(t){t.type===c&&l(t)||this.show()},g[d]=function(t){l(t)||this.hide()},g)});var g}}function Xt(t){function e(t,e){return e.match(new RegExp("^"+t.replace(/\//g,"\\/").replace(/\*\*/g,"(\\/[^\\/]+)*").replace(/\*/g,"[^\\/]+").replace(/((?!\\))\?/g,"$1.")+"$","i"))}function i(t,e){for(var i=[],n=0;ni[t]?n.ratio(e,t,i[t]):e}),e},cover:function(e,i){var n=this;return e=this.fit(e,i),t.each(e,function(t){return e=e[t]0||navigator.pointerEnabled&&navigator.maxTouchPoints>0,fe=de?window.PointerEvent?"pointerdown":"touchstart":"mousedown",ge=de?window.PointerEvent?"pointermove":"touchmove":"mousemove",pe=de?window.PointerEvent?"pointerup":"touchend":"mouseup",me=de&&window.PointerEvent?"pointerenter":"mouseenter",ve=de&&window.PointerEvent?"pointerleave":"mouseleave",we=F("transition","transition-start"),be=F("transition","transition-end"),ye=F("animation","animation-start"),$e=F("animation","animation-end");L.prototype={constructor:L,measure:function(t,e){var i=e?t.bind(e):t;return this.reads.push(i),z(this),i},mutate:function(t,e){var i=e?t.bind(e):t;return this.writes.push(i),z(this),i},clear:function(t){return R(this.reads,t)||R(this.writes,t)},extend:function(t){if("object"!=typeof t)throw new Error("expected object");var e=Object.create(this);return W(e,t),e.fastdom=this,e.initialize&&e.initialize(),e},catch:null};var xe=new L;Y.prototype={positions:[],position:null,init:function(){var t=this;this.positions=[],this.position=null;var e=!1;this.handler=function(i){e||setTimeout(function(){var n=Date.now(),o=t.positions.length;o&&n-t.positions[o-1].time>100&&t.positions.splice(0,o),t.positions.push({time:n,x:i.pageX,y:i.pageY}),t.positions.length>5&&t.positions.shift(),e=!1},5),e=!0},Zt.on("mousemove",this.handler)},cancel:function(){this.handler&&Zt.off("mousemove",this.handler)},movesTo:function(t){if(this.positions.length<2)return!1;var e=G(t),i=[[{x:e.left,y:e.top},{x:e.right,y:e.bottom}],[{x:e.right,y:e.top},{x:e.left,y:e.bottom}]],n=this.positions[this.positions.length-1],o=this.positions[0];return e.right<=n.x||(e.left>=n.x?(i[0].reverse(),i[1].reverse()):e.bottom<=n.y?i[0].reverse():e.top>=n.y&&i[1].reverse()),!!i.reduce(function(t,e){return t+(V(o,e[0])V(n,e[1]))},0)}};var ke={};ke.args=ke.attrs=ke.created=ke.events=ke.init=ke.ready=ke.connected=ke.disconnected=ke.destroy=function(e,i){return e=e&&!t.isArray(e)?[e]:e,i?e?e.concat(i):t.isArray(i)?i:[i]:e},ke.update=function(e,i){return ke.args(e,t.isFunction(i)?{write:i}:i)},ke.props=function(e,i){return t.isArray(i)&&(i=i.reduce(function(t,e){return t[e]=String,t},{})),ke.methods(e,i)},ke.defaults=ke.methods=function(e,i){return i?e?t.extend(!0,{},e,i):i:e};var Te,Ce,Se,Ee,Ae,De,_e=function(t,e){return S(e)?t:e},Oe={x:["width","left","right"],y:["height","top","bottom"]},Ie={},Pe=750;i(function(){var t,e,i,n=0,o=0;"MSGesture"in window&&(Ae=new MSGesture,Ae.target=document.body),document.addEventListener("click",function(){return De=!0},!0),Zt.on("MSGestureEnd gestureend",function(t){var e=t.originalEvent.velocityX>1?"Right":t.originalEvent.velocityX<-1?"Left":t.originalEvent.velocityY>1?"Down":t.originalEvent.velocityY<-1?"Up":null;e&&void 0!==Ie.el&&(Ie.el.trigger("swipe"),Ie.el.trigger("swipe"+e))}).on(fe,function(n){i=n.originalEvent.touches?n.originalEvent.touches[0]:n,t=Date.now(),e=t-(Ie.last||t),Ie.el=Gt("tagName"in i.target?i.target:i.target.parentNode),Te&&clearTimeout(Te),Ie.x1=i.pageX,Ie.y1=i.pageY,e>0&&e<=250&&(Ie.isDoubleTap=!0),Ie.last=t,Ee=setTimeout(it,Pe),!Ae||"pointerdown"!=n.type&&"touchstart"!=n.type||Ae.addPointer(n.originalEvent.pointerId),De=n.button>0}).on(ge,function(t){i=t.originalEvent.touches?t.originalEvent.touches[0]:t,nt(),Ie.x2=i.pageX,Ie.y2=i.pageY,n+=Math.abs(Ie.x1-Ie.x2),o+=Math.abs(Ie.y1-Ie.y2)}).on(pe,function(){nt(),Ie.x2&&Math.abs(Ie.x1-Ie.x2)>30||Ie.y2&&Math.abs(Ie.y1-Ie.y2)>30?Se=setTimeout(function(){void 0!==Ie.el&&(Ie.el.trigger("swipe"),Ie.el.trigger("swipe"+et(Ie.x1,Ie.x2,Ie.y1,Ie.y2))),Ie={}},0):"last"in Ie&&(isNaN(n)||n<30&&o<30?Ce=setTimeout(function(){var t=Gt.Event("tap");t.cancelTouch=ot,void 0!==Ie.el&&Ie.el.trigger(t),Ie.isDoubleTap?(void 0!==Ie.el&&Ie.el.trigger("doubleTap"),Ie={}):Te=setTimeout(function(){Te=null,void 0!==Ie.el&&(Ie.el.trigger("singleTap"),De||Ie.el.trigger("click")),Ie={}},300)}):Ie={},n=o=0)}).on("touchcancel pointercancel",ot),Jt.on("scroll",ot)});var Ne=!1;Zt.on((Be={touchstart:function(){Ne=!0}},Be["click touchcancel"]=function(){Ne=!1},Be));var Be,He=Object.freeze({win:Jt,doc:Zt,docElement:Kt,isRtl:te,isReady:e,ready:i,on:n,off:o,transition:s,Transition:ee,animate:r,Animation:ie,isJQuery:a,isWithin:l,attrFilter:h,removeClass:c,createEvent:u,isInView:d,getIndex:f,isVoidElement:g,Dimensions:oe,query:p,Observer:he,requestAnimationFrame:ce,cancelAnimationFrame:ue,hasTouch:de,pointerDown:fe,pointerMove:ge,pointerUp:pe,pointerEnter:me,pointerLeave:ve,transitionstart:we,transitionend:be,animationstart:ye,animationend:$e,getStyle:M,getCssVar:j,fastdom:xe,$:Gt,bind:m,hasOwn:v,promise:w,classify:b,hyphenate:y,camelize:x,isString:T,isNumber:C,isUndefined:S,isContextSelector:E,getContextSelectors:A,toJQuery:D,toNode:_,toBoolean:O,toNumber:I,toMedia:P,coerce:N,toMs:B,swap:H,ajax:t.ajax,each:t.each,extend:t.extend,map:t.map,merge:t.merge,isArray:t.isArray,isNumeric:t.isNumeric,isFunction:t.isFunction,isPlainObject:t.isPlainObject,MouseTracker:Y,mergeOptions:Q,position:X,getDimensions:G,flipPosition:tt,isTouch:st}),Me=function(t){this._init(t)};Me.util=He,Me.data="__uikit__",Me.prefix="uk-",Me.options={},Me.instances={},Me.elements=[],rt(Me),ct(Me),dt(Me),ft(Me);var je,Fe={init:function(){this.$el.addClass(this.$name)}},Le={props:{cls:Boolean,animation:Boolean,duration:Number,origin:String,transition:String,queued:Boolean},defaults:{cls:!1,animation:!1,duration:200,origin:!1,transition:"linear",queued:!1,initProps:{overflow:"",height:"",paddingTop:"",paddingBottom:"",marginTop:"",marginBottom:""},hideProps:{overflow:"hidden",height:0,paddingTop:0,paddingBottom:0,marginTop:0,marginBottom:0}},ready:function(){T(this.animation)&&(this.animation=this.animation.split(","),1===this.animation.length&&(this.animation[1]=this.animation[0]),this.animation=this.animation.map(function(t){return t.trim()})),this.queued=this.queued&&!!this.animation},methods:{toggleElement:function(t,e,i){var n,o=this,s=document.body,r=s.scrollTop,a=function(t){return w.all(t.toArray().map(function(t){return o._toggleElement(t,e,i)})).then(null,function(){})},l=function(t){var e=a(t);return o.queued=!0,s.scrollTop=r,e};return t=Gt(t),!this.queued||t.length<2?a(t):this.queued!==!0?l(t.not(this.queued)):(this.queued=t.not(n=t.filter(function(t,e){return o.isToggled(e)})),a(n).then(function(){return o.queued!==!0&&l(o.queued)}))},toggleNow:function(t,e){var i=this;return w.all(Gt(t).toArray().map(function(t){return i._toggleElement(t,e,!1)})).then(null,function(){})},isToggled:function(t){return t=Gt(t),this.cls?t.hasClass(this.cls.split(" ")[0]):!t.attr("hidden")},updateAria:function(t){this.cls===!1&&t.attr("aria-hidden",!this.isToggled(t))},_toggleElement:function(t,e,i){var n=this;if(t=Gt(t),ie.inProgress(t))return ie.cancel(t).then(function(){return n._toggleElement(t,e,i)});e="boolean"==typeof e?e:!this.isToggled(t);var o=Gt.Event("before"+(e?"show":"hide"));t.trigger(o,[this]);var s=!1;if(o.result===!1)return r.reject();o.result&&o.result.then&&(s=o.result);var r=(this.animation===!0&&i!==!1?this._toggleHeight:this.animation&&i!==!1?this._toggleAnimation:this._toggleImmediate)(t,e),a=function(){return t.trigger(e?"show":"hide",[n]),r.then(function(){return t.trigger(e?"shown":"hidden",[n])})};return s?s.then(a):a()},_toggle:function(t,e){t=Gt(t),this.cls?t.toggleClass(this.cls,~this.cls.indexOf(" ")?void 0:e):t.attr("hidden",!e),t.find("[autofocus]:visible").focus(),this.updateAria(t),Me.update(null,t)},_toggleImmediate:function(t,e){return this._toggle(t,e),w.resolve()},_toggleHeight:function(e,i){var n,o=this,s=ee.inProgress(e),r=parseFloat(e.children().first().css("margin-top"))+parseFloat(e.children().last().css("margin-bottom")),a=e[0].offsetHeight?e.height()+(s?0:r):0;return ee.cancel(e).then(function(){return o.isToggled(e)||o._toggle(e,!0),e.css("height",""),n=e.height()+(s?0:r),e.height(a),i?ee.start(e,t.extend(o.initProps,{overflow:"hidden",height:n}),Math.round(o.duration*(1-a/n)),o.transition):ee.start(e,o.hideProps,Math.round(o.duration*(a/n)),o.transition).then(function(){o._toggle(e,!1),e.css(o.initProps)})})},_toggleAnimation:function(t,e){var i=this;return e?(this._toggle(t,!0),ie.in(t,this.animation[0],this.duration,this.origin)):ie.out(t,this.animation[1],this.duration,this.origin).then(function(){return i._toggle(t,!1)})}}},ze={mixins:[Fe,Le],props:{clsPanel:String,selClose:String,escClose:Boolean,bgClose:Boolean,stack:Boolean},defaults:{cls:"uk-open",escClose:!0,bgClose:!0,overlay:!0,stack:!1},ready:function(){this.body=Gt(document.body),this.panel=D("."+this.clsPanel,this.$el)},events:[{name:"click",delegate:function(){return this.selClose},handler:function(t){t.preventDefault(),this.hide()}},{name:"toggle",handler:function(t){t.preventDefault(),this.toggleNow(this.$el)}},{name:"beforeshow",self:!0,handler:function(){var t=this;if(this.isActive())return!1;var e=je&&je!==this&&je;je||this.body.css("overflow-y",this.getScrollbarWidth()&&this.overlay?"scroll":""),je=this,e?this.stack?this.prev=e:e.hide():requestAnimationFrame(function(){return gt(t.$options.name)}),Kt.addClass(this.clsPage)}},{name:"beforehide",self:!0,handler:function(){var t=this;if(!this.isActive())return!1;je=je&&je!==this&&je||this.prev,je||pt(this.$options.name);var e=B(this.panel.css("transition-duration"));return!e||w(function(i){t.panel.one(be,i),setTimeout(function(){i(),t.panel.off(be,i)},e)})}},{name:"hide",self:!0,handler:function(){je||(Kt.removeClass(this.clsPage),this.body.css("overflow-y",""))}}],methods:{isActive:function(){return this.$el.hasClass(this.cls)},toggle:function(){return this.isActive()?this.hide():this.show()},show:function(){return this.toggleNow(this.$el,!0)},hide:function(){return this.toggleNow(this.$el,!1)},getActive:function(){return je},getScrollbarWidth:function(){var t=Kt[0].style.width;Kt.css("width","");var e=window.innerWidth-Kt.outerWidth(!0);return t&&Kt.width(t),e}}},qe={props:{pos:String,offset:null,flip:Boolean,clsPos:String},defaults:{pos:te?"bottom-right":"bottom-left",flip:!0,offset:!1,clsPos:""},init:function(){this.pos=(this.pos+(~this.pos.indexOf("-")?"":"-center")).split("-"),this.dir=this.pos[0],this.align=this.pos[1]},methods:{positionAt:function(t,e,i){c(t,this.clsPos+"-(top|bottom|left|right)(-[a-z]+)?").css({top:"",left:""}),this.dir=this.pos[0],this.align=this.pos[1];var n=I(this.offset)||0,o=this.getAxis(),s=X(t,e,"x"===o?tt(this.dir)+" "+this.align:this.align+" "+tt(this.dir),"x"===o?this.dir+" "+this.align:this.align+" "+this.dir,"x"===o?""+("left"===this.dir?-1*n:n):" "+("top"===this.dir?-1*n:n),null,this.flip,i);this.dir="x"===o?s.target.x:s.target.y,this.align="x"===o?s.target.y:s.target.x,t.css("display","").toggleClass(this.clsPos+"-"+this.dir+"-"+this.align,this.offset===!1)},getAxis:function(){return"top"===this.pos[0]||"bottom"===this.pos[0]?"y":"x"}}},Ue='',Re='',We='',Ye='',Ve='',Qe='',Xe='',Ge='',Je='',Ze='',Ke='',ti='',ei='',ii='',ni='',oi={},si=new DOMParser;return Me.version="3.0.0",mt(Me),Ut(Me),Rt(Me),"undefined"!=typeof window&&window.UIkit&&window.UIkit.use(Wt),"undefined"!=typeof window&&window.UIkit&&window.UIkit.use(Yt),"undefined"!=typeof window&&window.UIkit&&window.UIkit.use(Vt),"undefined"!=typeof window&&window.UIkit&&window.UIkit.use(Qt),"undefined"!=typeof window&&window.UIkit&&window.UIkit.use(Xt),Me.use(Wt),Me.use(Yt),Me.use(Vt),Me.use(Qt),Me.use(Xt),Me}); \ No newline at end of file diff --git a/raidar/static/raidar/script.js b/raidar/static/raidar/script.js index a6ba6121..5948cc78 100644 --- a/raidar/static/raidar/script.js +++ b/raidar/static/raidar/script.js @@ -1,7 +1,53 @@ "use strict"; -// XAcquire Django CSRF token for AJAX, and prefix the base URL +// Acquire Django CSRF token for AJAX, and prefix the base URL (function setupAjaxForAuth() { + const PAGE_SIZE = 10; + const PAGINATION_WINDOW = 5; + + const DEBUG = raidar_data.debug; + Ractive.DEBUG = DEBUG; + + + Ractive.decorators.ukUpdate = function(node) { + UIkit.update(); + return { + teardown: () => { + }, + }; + }; + + // bring tagsInput into Ractive + Ractive.decorators.tagsInput = function(node, tagsPath) { + let ractive = this; + tagsPath = ractive.getContext(node).resolve(tagsPath); + let classList = Array.from(node.classList); + tagsInput(node); + node.nextSibling.setValue(ractive.get(tagsPath)); + node.nextSibling.classList.add(...classList); + if (node.getAttribute('readonly')) { + node.nextSibling.firstChild.setAttribute('readonly', true); + } + node.addEventListener('change', function(evt) { + let tags = node.nextSibling.getValue(); + if (r.get(tagsPath) != tags) { + ractive.set(tagsPath, node.nextSibling.getValue()); + } + }); + let observer = this.observe(tagsPath, (newValue, oldValue, keypath) => { + if (node.nextSibling && newValue != oldValue) { + node.nextSibling.setValue(newValue); + } + }); + return { + teardown: () => { + observer.cancel(); + node.nextSibling.remove(); + }, + }; + }; + + let csrftoken = $('[name="csrfmiddlewaretoken"]').val(); function csrfSafeMethod(method) { @@ -16,24 +62,453 @@ settings.url = baseURL + settings.url } }); - $(document).ajaxError(evt => error("Error connecting to server")) + $(document).ajaxError((evt, xhr, settings, err) => { + console.error(err); + error("Error communicating to server") + }) + + function f0X(x) { + return (x < 10) ? "0" + x : x; + } + + let helpers = Ractive.defaults.data; + let allRE = /^All(?: \w+ bosses)?$/; + helpers.keysWithAllLast = (obj, lookup) => { + let keys = Object.keys(obj); + keys.sort((a, b) => { + let aAll = a.match(allRE); + let bAll = b.match(allRE); + if (aAll && !bAll) return 1; + if (!aAll && bAll) return -1; + if (lookup && !(aAll && bAll)) { + a = lookup[a]; + b = lookup[b]; + } + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + return keys; + }; + helpers.flattenStats = (build) => { + let all = []; + Object.keys(build || {}).forEach((professionId) => { + Object.keys(build[professionId] || {}).forEach((eliteId) => { + Object.keys(build[professionId][eliteId] || {}).forEach((archetypeId) => { + if('count' in build[professionId][eliteId][archetypeId]) + all.push({ + 'professionId':professionId, + 'eliteId': eliteId, + 'archetypeId': archetypeId, + 'boss_dps_percentiles': helpers.p(build[professionId][eliteId][archetypeId].per_dps_boss) + }); + }); + }); + }); + all.sort((a,b) => b.boss_dps_percentiles[99] - a.boss_dps_percentiles[99]) + return all + } + helpers.findId = (list, id) => { + return list.find(a => a.id == id); + } + helpers.buffImportanceLookup = { + 'might': 80, + 'fury': 10, + 'quickness': 25, + 'alacrity': 15, + 'protection': 15, + 'retaliation': 5, + 'spotter': 5, + 'glyph_of_empowerment': 10, + 'gotl': 200, + 'spirit_of_frost': 7.5, + 'sun_spirit': 6, + 'empower_allies': 5, + 'banner_strength': 8, + 'banner_discipline': 8, + 'assassins_presence': 6, + 'naturalistic_resonance': 20, + 'pinpoint_distribution': 5, + 'soothing_mist': 10, + 'vampiric_presence': 5, + } + helpers.buffStackLookup = { + 'might': 25, + 'gotl': 5 + } + helpers.buffImageLookup = { + 'might': 'Might', + 'fury': 'Fury', + 'quickness': 'Quickness', + 'alacrity': 'Alacrity', + 'protection': 'Protection', + 'retaliation': 'Retaliation', + 'regen': 'Regeneration', + 'spotter': 'Spotter', + 'glyph_of_empowerment': 'Glyph_of_Empowerment', + 'gotl': 'Grace_of_the_Land', + 'spirit_of_frost': 'Frost_Spirit', + 'sun_spirit': 'Sun_Spirit', + 'stone_spirit': 'Stone_Spirit', + 'storm_spirit': 'Storm_Spirit', + 'empower_allies': 'Empower_Allies', + 'banner_strength': 'Banner_of_Strength', + 'banner_discipline': 'Banner_of_Discipline', + 'banner_tactics': 'Banner_of_Tactics', + 'banner_defence': 'Banner_of_Defense', + 'assassins_presence': 'Assassin\'s_Presence', + 'naturalistic_resonance': 'Facet_of_Nature', + 'pinpoint_distribution': 'Pinpoint_Distribution', + 'soothing_mist': 'Soothing_Mist', + 'vampiric_presence': 'Vampiric_Presence', + } + helpers.buffImportance = (buff) => { + if(buff in helpers.buffImportanceLookup) { + return helpers.buffImportanceLookup[buff]; + } + return 1; + } + helpers.buffMax = (buff) => { + if(buff in helpers.buffStackLookup) { + return helpers.buffStackLookup[buff]; + } + return 100; + } + helpers.highestBuffs = (buffs) => { + let buffNames = []; + Object.keys(buffs).forEach((buff) => { + if(buff.startsWith("max_")) + buffNames.push(buff.substring(4)) + }); + let buffInfo = buffNames.map((buff) => { return { + "percentiles": helpers.p(buffs["per_" + buff]), + "max": helpers.buffMax(buff) * 10, + "buff_name": buff, + "buff_image": helpers.buffImageLookup[buff] || buff, + "importance": helpers.buffImportance(buff) * buffs["avg_" + buff] + }}).filter((a) => a.importance >= 500); - var helpers = Ractive.defaults.data; + buffInfo.sort((a,b) => b.importance - a.importance); + return buffInfo; + } helpers.formatDate = timestamp => { - let date = new Date(timestamp * 1000); - return date.toISOString().replace('T', ' ').replace(/.000Z$/, ''); + if (timestamp !== undefined) { + let date = new Date(timestamp * 1000); + return `${date.getFullYear()}-${f0X(date.getMonth() + 1)}-${f0X(date.getDate())} ${f0X(date.getHours())}:${f0X(date.getMinutes())}:${f0X(date.getSeconds())}`; + } else { + return ''; + } + }; + helpers.formatTime = duration => { + if (duration !== undefined) { + let seconds = Math.trunc(duration); + let minutes = Math.trunc(seconds / 60); + let usec = Math.trunc((duration - seconds) * 1000); + seconds -= minutes * 60 + if (usec < 10) usec = "00" + usec; + else if (usec < 100) usec = "0" + usec; + if (minutes) return minutes + ":" + f0X(seconds) + "." + usec; + return seconds + "." + usec; + } else { + return ''; + } + }; + helpers.tagForMechanic = (context, metricData) => { + let metrics, ok, actualPhase; + try { + actualPhase = r.get('page.phase'); + let phase = metricData.split_by_phase ? actualPhase : 'All'; + metrics = context.phases[phase].mechanics; + ok = metrics && metricData.name in metrics; + } catch (e) { + ok = false; + } + if (!ok) return ""; + + let ignore = (actualPhase == 'All' || metricData.split_by_phase) ? '' : 'class="ignore"'; + let value = metrics[metricData.name]; + if (metricData.data_type == 0) { + value = "[" + helpers.formatTime(value / 1000) + "]"; + } + return `${value}`; } + class Colour { + constructor(r, g, b, a) { + if (typeof(r) == 'string') { + [this.r, this.g, this.b] = r.match(Colour.colRE).slice(1).map(x => parseInt(x, 16)); + this.a = g || 1; + } else { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + } + blend(other, p) { + let rgba = ['r', 'g', 'b', 'a'].map(c => (1 - p) * this[c] + p * other[c]); + return new Colour(...rgba); + } + lighten(p) { + let rgb = ['r', 'g', 'b'].map(c => 255 - p * (255 - this[c])); + return new Colour(...rgb, this.a); + } + css() { + return `rgba(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)}, ${this.a})`; + } + } + Colour.colRE = /^#(..)(..)(..)$/; + const barcss = { + average: new Colour("#cccc80"), + good: new Colour("#80ff80"), + bad: new Colour("#ff8080"), + live: new Colour("#e0ffe0"), + down: new Colour("#ffffe0"), + dead: new Colour("#ffe0e0"), + disconnect: new Colour("#e0e0e0"), + single: new Colour("#999999"), + expStroke: new Colour("#8080ff").css(), + expFill: new Colour("#8080ff", 0.5).css(), + }; + const scaleColour = (val, avg, min, max, flip) => { + let good = barcss.good; + let bad = barcss.bad; + if (flip) [good, bad] = [bad, good] + if (val == avg) { + return barcss.average; + } else if (val < avg) { + return bad.blend(barcss.average, 1 - (avg - val) / (avg - min)); + } else { + return good.blend(barcss.average, 1 - (val - avg) / (max - avg)); + } + } + helpers.bar = (actual, average, min, max, top, flip) => { + if (!average) return helpers.bar1(actual, top); + if (min > actual) min = actual; + if (max < actual) max = actual; + top = Math.max(top || max, actual); + let avgPct = average * 100 / top; + let actPct = actual * 100 / top; + let colour = scaleColour(actual, average, min, max, flip); + let stroke = colour.css(); + let fill = colour.lighten(0.5).css(); + let svg = ` + + + + + `.replace(/\n\s*/g, ""); + return `background-size: contain; background: url("data:image/svg+xml;utf8,${svg}")` + }; + helpers.bar1 = (val, max) => { + if (!max) return ''; + let actPct = val * 100 / max; + let stroke = barcss.single.css(); + let fill = barcss.single.lighten(0.5).css(); + let svg = ` + + + + `.replace(/\n\s*/g, ""); + return `background-size: contain; background: url("data:image/svg+xml;utf8,${svg}")` + }; + helpers.barSurvivalPerc = (down_perc, dead_perc, disconnect_perc) => { + let live_perc = 100 - (down_perc + dead_perc + disconnect_perc); + let rects = [ + [live_perc, barcss.live], + [down_perc, barcss.down], + [dead_perc, barcss.dead], + [disconnect_perc, barcss.disconnect] + ]; + let rectSvg = [], x = 0; + rects.forEach(([value, colour]) => { + if (value) { + rectSvg.push(``); + x += value; + } + }); + let svg = ` + +${rectSvg.join("\n")} + + `.replace(/\n\s*/g, ""); + return `background-size: contain; background: url("data:image/svg+xml;utf8,${svg}")` + }; + helpers.barSurvival = (events, duration, numPlayers) => { + switch (typeof numPlayers) { + case "undefined": + numPlayers = 1; break; + case "object": + numPlayers = Object.values(numPlayers).reduce((a, e) => a + e.members.length, 0); + } + let down_perc = (events.down_time || 0) * 100 / 1000 / numPlayers / duration; + let dead_perc = (events.dead_time || 0) * 100 / 1000 / numPlayers / duration; + let disconnect_perc = (events.disconnect_time || 0) * 100 / 1000 / numPlayers / duration; + return helpers.barSurvivalPerc(down_perc, dead_perc, disconnect_perc); + } + helpers.p = (p) => { + let b = atob(p); + let p2 = new Uint8Array(400) + for(var i = 0; i < 400; i++) { + p2[i] = b.charCodeAt(i) + } + return new Float32Array(p2.buffer) + } + helpers.p_r = (p) => { + let normalOrder = helpers.p(p); + let reversed = [normalOrder[99]] + for(let i = 1; i < 100; i++) { + reversed.push(normalOrder[100-i]) + } + return reversed; + } + helpers.p_bar = (p, max, space_for_image) => { + let quantileColours = ['#d7191c', '#fdae61', '#ffffbf', '#a6d96a', '#1a9641'] + + + return helpers.svg(helpers.rectangle(0, 5, 80*p[99]/max, 30, new Colour(quantileColours[4])) + + helpers.rectangle(0, 35, 80*p[90]/max, 30, new Colour(quantileColours[3])) + + helpers.rectangle(0, 65, 80*p[50]/max, 30, new Colour(quantileColours[2])) + + helpers.text(80*p[99]/max, 30, 11, p[99].toFixed(0)) + + helpers.text(80*p[90]/max, 60, 11, p[90].toFixed(0)) + + helpers.text(80*p[50]/max, 90, 11, p[50].toFixed(0))) + + `;background-size: ${space_for_image ? 75 : 100}% 100%; background-position:${space_for_image ? 36 : 0}px 0px; background-repeat: no-repeat`; + } + helpers.rectangle = (x, y, width, height, colour) => { + return `` + } + helpers.text = (x, y, size, text) => { + return `${text}` + } + helpers.svg = (body) => { + let svg = ` + +${body} +`.replace(/\n\s*/g, ""); + return `background: url("data:image/svg+xml;utf8,${svg}")` + } + + let loggedInPage = Object.assign({}, window.raidar_data.page); + let initialPage = loggedInPage; + const PERMITTED_PAGES = ['encounter', 'index', 'login', 'register', 'reset_pw', 'info-about', 'info-help', 'info-releasenotes', 'info-contact', 'global_stats', 'thank-you']; + if (!window.raidar_data.username) { + if (!initialPage.name) { + loggedInPage = { name: 'info-releasenotes' }; + initialPage = { name: 'info-help' }; + } else if (PERMITTED_PAGES.indexOf(loggedInPage.name) == -1) { + initialPage = { name: 'login' }; + } + } else if (!initialPage.name) { + initialPage = { name: 'info-releasenotes' }; + } let initData = { - username: window.userprops.username, - is_staff: window.userprops.is_staff, - auth: { - login: true, - }, - page: { name: 'index' }, + data: window.raidar_data, + username: window.raidar_data.username, + privacy: window.raidar_data.privacy, + is_staff: window.raidar_data.is_staff, + page: initialPage, + persistent_page: { tab: 'combat_stats' }, encounters: [], + settings: { + encounterSort: { prop: 'uploaded_at', dir: 'down', filters: false, filter: { success: null } }, + }, + upload: [], }; + let lastNotificationId = window.raidar_data.last_notification_id; + let storedSettingsJSON = localStorage.getItem('settings'); + if (storedSettingsJSON) { + Object.assign(initData.settings, JSON.parse(storedSettingsJSON)); + } + initData.data.boons = [ + { boon: 'might', stacks: 25 }, + { boon: 'fury' }, + { boon: 'quickness' }, + { boon: 'alacrity' }, + { boon: 'protection' }, + { boon: 'retaliation' }, + { boon: 'regen' }, + { boon: 'spotter' }, + { boon: 'glyph_of_empowerment' }, + { boon: 'gotl', stacks: 5 }, + { boon: 'spirit_of_frost' }, + { boon: 'sun_spirit' }, + { boon: 'stone_spirit' }, + { boon: 'storm_spirit' }, + { boon: 'empower_allies' }, + { boon: 'banner_strength' }, + { boon: 'banner_discipline' }, + { boon: 'banner_tactics' }, + { boon: 'banner_defence' }, + { boon: 'assassins_presence' }, + { boon: 'naturalistic_resonance' }, + { boon: 'pinpoint_distribution' }, + { boon: 'soothing_mist' }, + { boon: 'vampiric_presence' }, + ]; + delete window.raidar_data; + + function URLForPage(page) { + let url = baseURL + page.name; + if (page.no) url += '/' + page.no; + if (page.era_id) url += '/' + page.era_id; + if (page.area_id) url += '/area-' + page.area_id; + return url; + } + + function setData(data) { + r.set(data); + r.set('loading', false); + } + + let pageInit = { + login: page => { + $('#login_username').select().focus(); + }, + register: page => { + $('#register_username').select().focus(); + }, + reset_pw: page => { + $('#reset_pw_email').select().focus(); + }, + encounter: page => { + r.set({ + loading: true, + "page.phase": 'All', + }); + $.get({ + url: 'encounter/' + page.no + '.json', + }).then(setData); + }, + profile: page => { + r.set({ + loading: true, + 'page.area': 'All raid bosses', + }); + $.get({ + url: 'profile.json', + }).then(setData).then(() => { + let eras = r.get('profile.eras'); + let latest = eras[eras.length - 1]; + r.set({ + 'page.era': latest, + }); + }); + }, + global_stats: page => { + r.set({ + loading: true, + }); + $.get({ + url: URLForPage(page).substring(1) + '.json', + }).then(setData); + }, + }; + + $(window).on('popstate', evt => { + r.set('page', evt.originalEvent.state); + }); + // Ractive @@ -42,53 +517,204 @@ template: '#template', data: initData, computed: { - 'authBad': function authBad() { - let username = this.get('auth.input.username'), - password = this.get('auth.input.password'), - email = this.get('auth.input.email'); - - let authOK = username != '' && password != ''; - if (!this.get('auth.login')) { - let password2 = this.get('auth.input.password2'); - let emailOK = email != ''; // TODO maybe basic pattern check - authOK = authOK && password == password2 && emailOK; - } - return !authOK; + changePassBad: function changePassBad() { + let password = this.get('account.password'), + password2 = this.get('account.password2'); + return password == '' || password !== password2; + }, + encountersAreas: function encountersAreas() { + let result = Array.from(new Set(this.get('encountersFiltered').map(e => e.area))); + result.sort(); + return result; + }, + encountersCharacters: function encountersCharacters() { + let result = Array.from(new Set(this.get('encountersFiltered').map(e => e.character))); + result.sort(); + return result; + }, + encountersAccounts: function encountersAccounts() { + let result = Array.from(new Set(this.get('encountersFiltered').map(e => e.account))); + result.sort(); + return result; + }, + encountersFiltered: function encountersFiltered() { + let encounters = this.get('encounters') || []; + let filters = this.get('settings.encounterSort.filter'); + const durRE = /^([0-9]+)(?::([0-5]?[0-9](?:\.[0-9]{,3})?)?)?/; + const dateRE = /^(\d{4})(?:-(?:(\d{1,2})(?:-(?:(\d{1,2}))?)?)?)?$/; + if (filters.success !== null) { + encounters = encounters.filter(e => e.success === filters.success); + } + if (filters.area) { + let f = filters.area.toLowerCase(); + encounters = encounters.filter(e => e.area.toLowerCase().startsWith(f)); + } + if (filters.started_from) { + let m = filters.started_from.match(dateRE); + if (m) { + let d = new Date(+m[1], (+m[2] - 1) || 0, +m[3] || 1); + let f = d.getTime() / 1000; + encounters = encounters.filter(e => e.started_at >= f); + } + } + if (filters.started_till) { + let m = filters.started_till.match(dateRE); + if (m) { + let d = new Date(+m[1], (+m[2] - 1) || 0, +m[3] || 1); + if (m[3]) d.setDate(d.getDate() + 1); + else if (m[2]) d.setMonth(d.getMonth() + 1); + else if (m[1]) d.setFullYear(d.getFullYear() + 1); + let f = d.getTime() / 1000; + encounters = encounters.filter(e => e.started_at < f); + } + } + if (filters.duration_from) { + let m = filters.duration_from.match(durRE); + if (m) { + let f = ((+m[1] || 0) * 60 + (+m[2] || 0)); + encounters = encounters.filter(e => e.duration >= f); + } + } + if (filters.duration_till) { + let m = filters.duration_till.match(durRE); + if (m) { + let f = ((+m[1] || 0) * 60 + (+m[2] || 0)); + encounters = encounters.filter(e => e.duration <= f); + } + } + if (filters.character) { + let f = filters.character.toLowerCase(); + encounters = encounters.filter(e => e.character.toLowerCase().startsWith(f)); + } + if (filters.account) { + let f = filters.account.toLowerCase(); + encounters = encounters.filter(e => e.account.toLowerCase().startsWith(f)); + } + if (filters.uploaded_from) { + let m = filters.uploaded_from.match(dateRE); + if (m) { + let d = new Date(+m[1], (+m[2] - 1) || 0, +m[3] || 1); + let f = d.getTime() / 1000; + encounters = encounters.filter(e => e.uploaded_at >= f); + } + } + if (filters.uploaded_till) { + let m = filters.uploaded_till.match(dateRE); + if (m) { + let d = new Date(+m[1], (+m[2] - 1) || 0, +m[3] || 1); + if (m[3]) d.setDate(d.getDate() + 1); + else if (m[2]) d.setMonth(d.getMonth() + 1); + else if (m[1]) d.setFullYear(d.getFullYear() + 1); + let f = d.getTime() / 1000; + encounters = encounters.filter(e => e.uploaded_at < f); + } + } + if (filters.category !== null) { + let f = filters.category; + if (!f) f = null; + encounters = encounters.filter(e => e.category === f); + } + if (filters.tag) { + let f = filters.tag.toLowerCase(); + encounters = encounters.filter(e => e.tags.some(t => t.toLowerCase().startsWith(f))); + } + return encounters; + }, + encounterSlice: function encounterSlice() { + let encounters = this.get('encountersFiltered'); + let page = this.get('page.no') || 1; + return encounters.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + }, + encounterPages: function encounterPages() { + let page = this.get('page.no') || 1; + let encounters = this.get('encountersFiltered') || []; + let totalPages = Math.ceil(encounters.length / PAGE_SIZE); + let minPage = Math.max(2, page - PAGINATION_WINDOW); + let maxPage = Math.min(totalPages - 1, page + PAGINATION_WINDOW); + let pages = [] + + pages.push({t: "<", c: 'uk-pagination-previous', d: 1 == page, n: page - 1}) + pages.push({t: 1, a: 1 == page}); + if (minPage > 2) pages.push({t: '...', d: true}); + let i; + for (i = minPage; i <= maxPage; i++) pages.push({t: i, a: i == page}); + if (maxPage < totalPages - 1) pages.push({t: '...', d: true}); + if (maxPage < totalPages && totalPages != 1) pages.push({t: totalPages, a: totalPages == page}); + pages.push({t: ">", c: 'uk-pagination-next', d: totalPages == page, n: page + 1}); + return pages; }, }, delimiters: ['[[', ']]'], - tripleDelimiters: ['[[[', ']]]'] + tripleDelimiters: ['[[[', ']]]'], + page: setPage, }); - window.r = r; // XXX DEBUG + r.observe('settings', (newValue, oldValue, keyPath) => { + localStorage.setItem('settings', JSON.stringify(newValue)); + }); + + // history, pushState + function setPage(page) { + if (typeof page == "string") { + page = { name: page }; + } + if (typeof page == "undefined") { + page = r.get('page'); + } else { + r.set('page', page); + } + let url = URLForPage(page); + history.pushState(page, null, url); + if (pageInit[page.name]) { + pageInit[page.name](page); + } + if (window.ga) { + window.ga('set', 'page', url); + window.ga('send', 'pageview'); + } + return false; + } + let url = URLForPage(initData.page); + history.replaceState(initData.page, null, url); + if (pageInit[initData.page.name]) { + pageInit[initData.page.name](initData.page); + } + if (window.ga) { + window.ga('set', 'page', url); + window.ga('send', 'pageview'); + } - let errorAnimation; - function error(str) { - if (errorAnimation) errorAnimation.stop(); - r.set('error', { - message: str, - opacity: 1, + + function notification(str, style) { + UIkit.notification(str, style); + } + + function error(str) { + notification(str, 'danger') + } + function success(str) { + UIkit.notification(str, { + status: 'success', }); + } - errorAnimation = r.animate('error.opacity', 0, { - duration: 5000, - easing: 'easeIn', - }) - errorAnimation.then(() => { - errorAnimation = null; - r.set('error', {}) - }) + function sortEncounters() { + let currentProp = r.get('settings.encounterSort.prop'); + let currentDir = r.get('settings.encounterSort.dir'); + r.get('encounters').sort((currentDir == 'up' ? ascSort : descSort)(currentProp)); + r.update('encounters'); } + function updateRactiveFromResponse(response) { + r.set(response); + sortEncounters(); + } // test for shenanigans $.ajax({ - url: 'initial', - }).done(response => { - response.encounters.sort((a, b) => b.started_at - a.started_at); - r.set(response); - }); + url: 'initial.json', + }).done(updateRactiveFromResponse); @@ -100,86 +726,498 @@ 'auth.input.username': '', 'auth.input.password': '', 'auth.input.password2': '', + 'auth.input.api_key': '', }); + setPage(response.page || loggedInPage); csrftoken = response.csrftoken; delete response.csrftoken; - r.set(response); + updateRactiveFromResponse(response); } } + function graphLine(value, data) { + let ary = Array(data.length); + ary[0] = ary[data.length - 1] = value; + return ary; + } + + function graphLineDataset(label, value, borderDash, backgroundColor, borderColor, data) { + return { + label: label, + data: graphLine(value, data), + spanGaps: true, + borderDash: borderDash, + pointRadius: 0, + backgroundColor: backgroundColor, + borderColor: borderColor, + borderWidth: 2, + }; + }; + + const ascSort = (prop) => (a, b) => + a[prop] > b[prop] ? 1 : + a[prop] < b[prop] ? -1 : 0; + const descSort = (prop) => (a, b) => + a[prop] < b[prop] ? 1 : + a[prop] > b[prop] ? -1 : 0; + + r.on({ - auth_login: function login() { + encounter_bug: function encounterBug(x) { + let url = r.get('encounter.url_id'); + r.set('contact.input.subject', `Error report: ${url}`); + setPage('info-contact'); + return false; + }, + refresh_page: function refreshPage(x) { + setPage(); + }, + auth_login: function login(x) { + if (!x.element.node.form.checkValidity()) return; + let username = this.get('auth.input.username'), password = this.get('auth.input.password'); $.post({ - url: 'login', + url: 'login.json', data: { username: username, password: password, }, }).done(didLogin); + + return false; }, - auth_register: function register() { + auth_register: function register(x) { + if (!x.element.node.form.checkValidity()) return; + let username = this.get('auth.input.username'), password = this.get('auth.input.password'), + apiKey = this.get('auth.input.api_key'), email = this.get('auth.input.email'); $.post({ - url: 'register', + url: 'register.json', data: { username: username, password: password, + api_key: apiKey, email: email, }, }).done(didLogin); + + return false; + }, + auth_reset_pw: function resetPw(x) { + if (!x.element.node.form.checkValidity()) return; + + let email = this.get('auth.input.email'); + + $.post({ + url: 'reset_pw.json', + data: { + email: email, + }, + }).done(() => { + notification('Sending now.', 'primary') + }) + + return false; }, auth_logout: function logout() { $.post({ - url: 'logout', + url: 'logout.json', }).done(response => { this.set({ username: null, - 'auth.login': true, }); + setPage('info-help'); }); }, - auth_swap: function swap() { - this.set({ - 'auth.login': !this.get('auth.login'), - 'auth.input.password': '', - 'auth.input.password2': '', + page_no: function pageNo(evt) { + let page_no = parseInt(evt.node.getAttribute('data-page')); + let page = this.get('page'); + setPage(Object.assign(page, { no: page_no })); + return false; + }, + change_password: function changePassword(x) { + if (!x.element.node.form.checkValidity()) return; + + $.post({ + url: 'change_password.json', + data: { + old_password: r.get('account.old_password'), + new_password1: r.get('account.password'), + new_password2: r.get('account.password2'), + }, + }).done(response => { + if (response.error) { + error(response.error); + } else { + success('Password changed'); + r.set('account.old_password', ''); + r.set('account.password', ''); + r.set('account.password2', ''); + } }); + return false; }, - to_profile: function toProfile() { - r.set('page', { name: 'profile' }) + change_email: function changeEmail(x) { + if (!x.element.node.form.checkValidity()) return; + + $.post({ + url: 'change_email.json', + data: { + email: r.get('account.email'), + }, + }).done(response => { + if (response.error) { + error(response.error); + } else { + success('Email changed'); + r.set('account.email', ''); + } + }); + return false; + }, + add_api_key: function addAPIKey(x) { + if (!x.element.node.form.checkValidity()) return; + + let api_key = r.get('account.api_key'); + $.post({ + url: 'add_api_key.json', + data: { + api_key: api_key, + }, + }).done(response => { + if (response.error) { + error(response.error); + } else { + success(`API key for ${response.account_name} added`); + r.set('account.api_key', ''); + let accounts = r.get('accounts'); + let account = accounts.find(account => account.name == response.account_name); + let len = api_key.length; + api_key = api_key.substring(0, 8) + + api_key.substring(8, len - 12).replace(/[0-9a-zA-Z]/g, 'X') + + api_key.substring(len - 12); + if (account) { + account.api_key = api_key; + } else { + accounts.push({ name: response.account_name, api_key: api_key }); + } + r.update('accounts'); + } + }); + return false; + }, + contact_us: function contactUs(x) { + if (!x.element.node.form.checkValidity()) return; + + let data = r.get('contact.input'); + + $.post({ + url: 'contact.json', + data: data + }).done(response => { + if (response.error) { + error(response.error); + } else { + success('Email sent'); + r.set('contact.input', {}); + } + }); + + return false; + }, + sort_encounters: function sortEncountersChange(evt) { + let currentProp = r.get('settings.encounterSort.prop'); + let currentDir = r.get('settings.encounterSort.dir'); + let newSort = $(evt.node).closest('th').data('sort'); + let [clickedProp, clickedDir] = newSort.split(':'); + if (clickedProp == currentProp) { + currentDir = currentDir == 'up' ? 'down' : 'up'; + r.set('settings.encounterSort.dir', currentDir); + } else { + currentProp = clickedProp; + currentDir = clickedDir; + r.set('settings.encounterSort.prop', clickedProp); + r.set('settings.encounterSort.dir', clickedDir); + } + sortEncounters(); + return false; + }, + encounter_filter_toggle: function encounterFilterToggle(evt) { + let filters = r.get('settings.encounterSort.filters'); + r.toggle('settings.encounterSort.filters'); + if (filters) { + r.set('settings.encounterSort.filter.*', null); + } + return false; + }, + encounter_filter_success: function encounterFilterSuccess(evt) { + r.set('settings.encounterSort.filter.success', JSON.parse(evt.node.value)); + return false; + }, + privacy: function privacy(evt) { + let privacy = r.get('privacy'); + $.post({ + url: 'privacy.json', + data: { + privacy: privacy, + }, + }).done(() => { + notification('Privacy updated.', 'success'); + }); + }, + set_tags_cat: function setTags(evt) { + let encounter = r.get('encounter'); + + $.post({ + url: 'set_tags_cat.json', + data: { + id: encounter.id, + tags: encounter.tags, + category: encounter.category, + }, + }).done(() => { + let eRowId = r.get('encounters').findIndex(e => e.id == encounter.id); + let eRow = r.get('encounters.' + eRowId); + eRow.category = encounter.category; + eRow.tags = encounter.tags.split(','); + r.update('encounters.' + eRowId); + + notification('Category and tags saved.', 'success'); + }); + return false; + }, + chart: function chart(evt, archetype, profession, elite, stat, statName) { + let era = r.get('page.era'); + let eras = r.get('profile.eras'); + let eraId = era.id; + let areaId = r.get('page.area'); + let archetypeName = archetype == 'All' ? '' : r.get('data.archetypes')[archetype] + ' '; + let charDescription = profession == 'All' ? `All ${archetypeName}specialisations'` : archetypeName + r.get('data.specialisations')[profession][elite]; + let areaName = r.get('data.areas')[areaId] || areaId; + + $.post({ + url: 'profile_graph.json', + data: { + era: eraId, + area: areaId, + archetype: archetype, + profession: profession, + elite: elite, + stat: stat, + }, + }).then(payload => { + let {globals, data, times} = payload; + times = times.map(time => helpers.formatDate(time)); + let pointRadius = 4; + if (data.length == 1) { + data = [data[0], data[0], data[0]]; + times = ['', times[0], '']; + pointRadius = [0, pointRadius, 0]; + } + + let height = Math.round(window.innerHeight * 0.80); + let width = Math.round(window.innerWidth * 0.80); + let dialog = UIkit.modal.dialog(` + +
    + +
    + `, {center: true}); + dialog.$el.css('overflow', 'hidden').addClass('uk-modal-lightbox'); + dialog.panel.css({width: width, height: height}); + dialog.caption = $('
    ').appendTo(dialog.panel); + let ctx = dialog.$el.find('canvas'); + let datasets = []; + if (globals) { + datasets.push(graphLineDataset('P99', globals.per[99], [1, 1], "rgba(255, 255, 255, 0)", "rgba(128, 128, 128, 1)", data)); + datasets.push(graphLineDataset('P90', globals.per[90], [4, 4], "rgba(255, 255, 255, 0)", "rgba(128, 128, 128, 1)", data)); + datasets.push(graphLineDataset('P50', globals.per[50], [7, 7], "rgba(255, 255, 255, 0)", "rgba(128, 128, 128, 1)", data)); + datasets.push(graphLineDataset('avg', globals.avg, undefined, "rgba(255, 255, 255, 0)", "rgba(255, 0, 255, 1)", data)); + } + datasets.push({ + label: statName, + data: data, + backgroundColor: "rgba(0, 0, 0, 0.05)", + borderColor: "rgba(0, 0, 0, 1)", + pointBackgroundColor: "rgba(255, 255, 255, 1)", + pointRadius: pointRadius, + }); + let chart = new Chart(ctx, { + type: 'line', + data: { + labels: times, + datasets: datasets, + }, + options: { + title: { + text: `${charDescription} ${statName} on ${areaName}`, + display: true, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } + }); + }); }, }); - let uploadProgressHandler = (file, evt) => { + let uploadProgressHandler = (entry, evt) => { let progress = Math.round(100 * evt.loaded / evt.total); - // TODO single upload progress - } - let uploadProgressDone = (file, data) => { - let encounters = r.get('encounters'); - Object.keys(data).forEach(file => { - let encounter = data[file]; - if (encounter.new) { - delete encounter.new; - encounters.push(encounter); - } - }); - r.set('encounters', encounters.sort((a, b) => b.started_at - a.started_at)) + //if (evt.loaded == evt.total) { + //} + entry.progress = progress; + r.update('upload'); } + let uploadProgressDone = (entry, data) => { + if (data.error) { + entry.error = data.error; + uploadProgressFail(entry); + } else { + entry.upload_id = data.upload_id; + } + delete entry.file; + r.update('upload'); + startUpload(true); + } + + // if (data.error) { + // entry.error = data.error; + // error(entry.name + ': ' + data.error); + // uploadProgressFail(entry); + // } else { + // if (data.encounter) { + // let encounters = r.get('encounters'); + // encounters = encounters.filter(encounter => encounter.id != data.id) + // encounters.push(data.encounter); + // updateRactiveFromResponse({ encounters: encounters }); + // } - let makeXHR = file => { + // entry.encounterId = data.id; + // entry.success = true; + // delete entry.file; + // r.update('upload'); + // startUpload(true); + // } + + let uploadProgressFail = entry => { + entry.success = false; + delete entry.file; + r.update('upload'); + startUpload(true); + } + + let makeXHR = entry => { let req = $.ajaxSettings.xhr(); - req.upload.addEventListener("progress", uploadProgressHandler.bind(null, file), false); + req.upload.addEventListener("progress", uploadProgressHandler.bind(null, entry), false); return req; } + let uploading = null; + function startUpload(previousIsFinished) { + if (uploading && !previousIsFinished) return; + + let entry = r.get('upload').find(entry => !("progress" in entry)); + uploading = entry; + if (!entry) return; + + let form = new FormData(); + form.append('file', entry.file); + return $.ajax({ + url: 'upload.json', + data: form, + type: 'POST', + contentType: false, + processData: false, + xhr: makeXHR.bind(null, entry), + }) + .done(uploadProgressDone.bind(null, entry)) + .fail(uploadProgressFail.bind(null, entry)); + } + + const notificationHandlers = { + upload: notification => { + //let entry = uploads.find(entry => entry.upload_id == notification.upload_id); + let entry = r.get('upload').find(entry => entry.name == notification.filename); + let newEntry = { + name: notification.filename, + progress: 100, + upload_id: notification.upload_id, + uploaded_by: notification.uploaded_by, + success: true, + encounterId: notification.encounter_id, + encounterUrlId: notification.encounter_url_id, + }; + if (entry) { + Object.assign(entry, newEntry); + r.update('upload'); + } else { + r.push('upload', newEntry); + } + + let encounters = r.get('encounters'); + encounters = encounters.filter(encounter => encounter.id != notification.encounter_id) + if (notification.encounter) { + encounters.push(notification.encounter); + } + updateRactiveFromResponse({ encounters: encounters }); + }, + upload_error: notification => { + let uploads = r.get('upload'); + let entry = uploads.find(entry => entry.upload_id == notification.upload_id); + if (entry) { + entry.success = false; + entry.error = notification.error; + r.update('upload'); + } + }, + }; + + function handleNotification(notification) { + let handler = notificationHandlers[notification.type]; + if (!handler) { // sanity check + console.error("No handler for notification type " + notification.type); + return; + } + handler(notification); + } + + const POLL_TIME = 10000; + function pollNotifications() { + if (r.get('username')) { + let options = { + url: 'poll.json', + type: 'POST', + } + if (lastNotificationId) { + options.data = { last_id: lastNotificationId }; + } + $.ajax(options).done(data => { + if (data.last_id) { + lastNotificationId = data.last_id; + } + data.notifications.forEach(handleNotification); + }).then(() => { + setTimeout(pollNotifications, POLL_TIME); + }); + } else { + setTimeout(pollNotifications, POLL_TIME); + } + }; + pollNotifications(); + + $(document) .on('dragstart dragover dragenter', evt => { if (r.get('username')) { @@ -197,22 +1235,27 @@ let files = evt.originalEvent.dataTransfer.files; let jQuery_xhr_factory = $.ajaxSettings.xhr; - let promises = Array.from(files).map(file => { - let form = new FormData(); - form.append(file.name, file); - return $.ajax({ - url: 'upload', - data: form, - type: 'POST', - contentType: false, - processData: false, - xhr: makeXHR.bind(null, file), - }) - .done(uploadProgressDone.bind(null, file)); - }); - $.when(promises).then(results => { - // TODO all done + Array.from(files).forEach(file => { + if (!file.name.endsWith('.evtc') && !file.name.endsWith('.evtc.zip')) return; + let entry = r.get('upload').find(entry => entry.name == file.name); + if (entry) { + delete entry.success; + delete entry.progress; + entry.file = file; + r.update('upload'); + } else { + r.push('upload', { + name: file.name, + file: file, + uploaded_by: r.get('username'), + }); + } + startUpload(); }); + setPage('uploads'); evt.preventDefault(); }); + + + if (DEBUG) window.r = r; // XXX DEBUG Ractive })(); diff --git a/raidar/static/raidar/style.css b/raidar/static/raidar/style.css index c7a49195..ffdcbf9b 100644 --- a/raidar/static/raidar/style.css +++ b/raidar/static/raidar/style.css @@ -1,28 +1,120 @@ -header { +.uk-badge.uploading { + background-color:#4282bb; + background-image:-webkit-linear-gradient(top,#569fd2,#346fac); + background-image:linear-gradient(to bottom,#569fd2,#346fac); +} +.uk-badge.analysing { + background-color:#f9a124; + background-image:-webkit-linear-gradient(top,#fbb450,#f89406); + background-image:linear-gradient(to bottom,#fbb450,#f89406); +} +.uk-badge.uploaded { + background-color:#82bb42; + background-image:-webkit-linear-gradient(top,#9fd256,#6fac34); + background-image:linear-gradient(to bottom,#9fd256,#6fac34); +} +.uk-badge.rejected { + background-color:#d32c46; + background-image:-webkit-linear-gradient(top,#ee465a,#c11a39); + background-image:linear-gradient(to bottom,#ee465a,#c11a39); +} +.states .uk-badge, .states .uk-badge:hover { + color: #ffffff; +} +.expected { + color: #999999; + font-size: 70%; +} +.ignore { + color: #bbbbbb; +} +.uk-h1, .uk-h2, .uk-h3, .uk-h4, .uk-h5, .uk-h6, h1, h2, h3, h4, h5, h6, +body +{ + font-family: 'Raleway', sans-serif; +} +.navbar-secondary .uk-navbar-nav > li > a { + height: 40px; +} +rect.bar-average { + fill: #eeeeff; + stroke: #ddddff; +} +rect.bar-good { + fill: #eeffee; + stroke: #ddffdd; +} +rect.bar-bad { + fill: #ffeeee; + stroke: #ffdddd; +} + +table.stats { + table-layout: fixed; + width: 1px; +} +table.stats td, th { + overflow: hidden; + width: 100px; + white-space: nowrap; +} +table.stats td:nth-of-type(1), table.stats th:nth-of-type(1) { + width: 250px; +} +.max-content-width { + width: max-content; +} +.table-container { position: relative; - color: white; - /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#001d4c+0,000000+100 */ - background: #001d4c; /* Old browsers */ - background: -moz-linear-gradient(-45deg, #001d4c 0%, #000000 100%); /* FF3.6-15 */ - background: -webkit-linear-gradient(-45deg, #001d4c 0%,#000000 100%); /* Chrome10-25,Safari5.1-6 */ - background: linear-gradient(135deg, #001d4c 0%,#000000 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#001d4c', endColorstr='#000000',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ -} -header .logo { - font-size: 60px; - color: lightblue; - padding: 2px 5px; -} -header .auth { +} +table.row-header { position: absolute; - bottom: 2px; - right: 5px; - text-align: right; + top: 0; + left: 0; + height: 0; + width: 0; + background: #ffffff; +} + +table td { + white-space: nowrap; +} + +input[type="date"] { + max-width: 160px; +} + +.link { + cursor: pointer; +} +.uk-table tr.all *, div.all { + font-weight: bold; +} +.main { + margin: 0 2%; +} + +input:invalid, textarea:invalid { + border: 1px dashed red !important; +} + +.private { + background-color: #000000; + color: #ffffff; + transform: rotate(-5deg); + display: inline-block; } -header .auth a { - padding: 2px 5px; - color: lightblue; + +input[readonly] + .tags-input { + border: none; + pointer-events: none; + width: inherit !important; } -.error { - color: red; +.tags-input.no-background { + background: inherit; + border: none; + width: unset; + padding: 0; + margin: -3px 0; + box-shadow: none; } diff --git a/raidar/templates/raidar/account.html b/raidar/templates/raidar/account.html new file mode 100644 index 00000000..1cf3b29f --- /dev/null +++ b/raidar/templates/raidar/account.html @@ -0,0 +1,70 @@ +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + Privacy: +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + + + + + + [[#each accounts]] + + + + + [[/each]] +
    Linked accountAPI key
    [[name]][[[api_key.replace(/-X.*X-/, '$&')]]]
    + +

    + Note that GW2 Raidar only requires the keys to authenticate your ownership of + a GW2 account. Therefore, you only need the default "account" permission. +

    diff --git a/raidar/templates/raidar/encounter.html b/raidar/templates/raidar/encounter.html new file mode 100644 index 00000000..9606b540 --- /dev/null +++ b/raidar/templates/raidar/encounter.html @@ -0,0 +1,78 @@ +
    + [[#with encounter]] +

    + [[name]] +

    +
    +

    + [[formatDate(started_at)]] + + [[[formatTime(duration)]]] +

    +
    + Uploaded as + [[#if evtc_url]] + [[filename]] + [[elseif downloadable]] + [[filename]] + [[else]] + [[filename]] + [[/if]] + by [[uploaded_by]] + at [[formatDate(uploaded_at)]] +
    +
    + [[/with]] +
    + +
    + + [[#if encounter.participated]] + + + [[else]] + + [[/if]] +
    + + + + +
    +
    + +[[#if persistent_page.tab == 'combat_stats']] + {% include 'raidar/encounter_combat_data.html' %} +[[elseif persistent_page.tab == 'boon_uptime']] + {% include 'raidar/encounter_boon_uptime.html' %} +[[elseif persistent_page.tab == 'boon_output']] + {% include 'raidar/encounter_boon_output.html' %} +[[elseif persistent_page.tab == 'boss_metrics']] + {% include 'raidar/encounter_boss_metrics.html' %} +[[/if]] + +

    + Found an error? + Contact us! +

    diff --git a/raidar/templates/raidar/encounter_boon_output.html b/raidar/templates/raidar/encounter_boon_output.html new file mode 100644 index 00000000..135d5724 --- /dev/null +++ b/raidar/templates/raidar/encounter_boon_output.html @@ -0,0 +1,124 @@ +{% load staticfiles %} +
    + {% include 'raidar/encounter_row_header.html' %} + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [[#each data.boons]] + [[#if encounter.phases[page.phase]]] + + [[else]] + + [[/if]] + [[/each]] + + [[#each encounter.parties:partyNo]] + + + [[#each data.boons]] + [[#if phases[page.phase]]] + + [[else]] + + [[/if]] + [[/each]] + + [[/each]] + + + [[#each encounter.parties:partyNo]] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [[#each members]] + + + [[#each data.boons]] + [[#if phases[page.phase]]] + + [[else]] + + [[/if]] + [[/each]] + + [[/each]] + + [[/each]] +
    TotalsMightFuryQuicknessAlacrityProtectionRetaliationRegenerationSpotterGlyph of EmpowermentGrace of the LandFrost SpiritSun SpiritStone SpiritStorm SpiritEmpower AlliesBanner of StrengthBanner of DisciplineBanner of TacticsBanner of DefenseAssassin's PresenceNaturalistic ResonancePinpoint DistributionSoothing MistVampiric Presence
    Squad + [[encounter.phases[page.phase].buffs_out[boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] + [[#if encounter.phases[page.phase].group]] + [[encounter.phases[page.phase].group.buffs_out['avg_' + boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] + [[/if]] +
    Party [[partyNo]] ([[members.length]] member[[members.length > 1 && 's' || '']]) + [[phases[page.phase].buffs_out[boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] +
    Party [[partyNo]]MightFuryQuicknessAlacrityProtectionRetaliationRegenerationSpotterGlyph of EmpowermentGrace of the LandFrost SpiritSun SpiritStone SpiritStorm SpiritEmpower AlliesBanner of StrengthBanner of DisciplineBanner of TacticsBanner of DefenseAssassin's PresenceNaturalistic ResonancePinpoint DistributionSoothing MistVampiric Presence
    + [[data.archetypes[archetype]]] + [[data.specialisations[profession][elite]]] + [[#if name]] + [[name]] + [[else]] + PRIVATE + [[/if]] + + [[phases[page.phase].buffs_out[boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] + [[#if phases[page.phase].archetype.buffs_out['avg_' + boon]]] + [[phases[page.phase].archetype.buffs_out['avg_' + boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] + [[/if]] +
    +
    +
    diff --git a/raidar/templates/raidar/encounter_boon_uptime.html b/raidar/templates/raidar/encounter_boon_uptime.html new file mode 100644 index 00000000..95d75609 --- /dev/null +++ b/raidar/templates/raidar/encounter_boon_uptime.html @@ -0,0 +1,124 @@ +{% load staticfiles %} +
    + {% include 'raidar/encounter_row_header.html' %} + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [[#each data.boons]] + [[#if encounter.phases[page.phase]]] + + [[else]] + + [[/if]] + [[/each]] + + [[#each encounter.parties:partyNo]] + + + [[#each data.boons]] + [[#if phases[page.phase]]] + + [[else]] + + [[/if]] + [[/each]] + + [[/each]] + + + [[#each encounter.parties:partyNo]] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [[#each members]] + + + [[#each data.boons]] + [[#if phases[page.phase]]] + + [[else]] + + [[/if]] + [[/each]] + + [[/each]] + + [[/each]] +
    TotalsMightFuryQuicknessAlacrityProtectionRetaliationRegenerationSpotterGlyph of EmpowermentGrace of the LandFrost SpiritSun SpiritStone SpiritStorm SpiritEmpower AlliesBanner of StrengthBanner of DisciplineBanner of TacticsBanner of DefenseAssassin's PresenceNaturalistic ResonancePinpoint DistributionSoothing MistVampiric Presence
    Squad + [[encounter.phases[page.phase].buffs[boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] + [[#if encounter.phases[page.phase].group]] + [[encounter.phases[page.phase].group.buffs['avg_' + boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] + [[/if]] +
    Party [[partyNo]] ([[members.length]] member[[members.length > 1 && 's' || '']]) + [[phases[page.phase].buffs[boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] +
    Party [[partyNo]]MightFuryQuicknessAlacrityProtectionRetaliationRegenerationSpotterGlyph of EmpowermentGrace of the LandFrost SpiritSun SpiritStone SpiritStorm SpiritEmpower AlliesBanner of StrengthBanner of DisciplineBanner of TacticsBanner of DefenseAssassin's PresenceNaturalistic ResonancePinpoint DistributionSoothing MistVampiric Presence
    + [[data.archetypes[archetype]]] + [[data.specialisations[profession][elite]]] + [[#if name]] + [[name]] + [[else]] + PRIVATE + [[/if]] + + [[phases[page.phase].buffs[boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] + [[#if phases[page.phase].archetype.buffs['avg_' + boon]]] + [[phases[page.phase].archetype.buffs['avg_' + boon].toPrecision(3)]][[#unless stacks]]%[[/unless]] + [[/if]] +
    +
    +
    diff --git a/raidar/templates/raidar/encounter_boss_metrics.html b/raidar/templates/raidar/encounter_boss_metrics.html new file mode 100644 index 00000000..b72f0ba0 --- /dev/null +++ b/raidar/templates/raidar/encounter_boss_metrics.html @@ -0,0 +1,62 @@ +{% load staticfiles %} +
    + {% include 'raidar/encounter_row_header.html' %} + +
    + + + + + [[#each encounter.boss_metrics]] + + [[/each]] + + + + [[#each encounter.boss_metrics]] + [[[tagForMechanic(encounter, .)]]] + [[/each]] + + [[#each encounter.parties:partyNo]] + [[#with { party: . }]] + + + [[#each encounter.boss_metrics]] + [[[tagForMechanic(party, .)]]] + [[/each]] + + [[/with]] + [[/each]] + + + [[#each encounter.parties:partyNo]] + + + + [[#each encounter.boss_metrics]] + + [[/each]] + + [[#each members]] + [[#with { member: . }]] + + + [[#each encounter.boss_metrics]] + [[[tagForMechanic(member, .)]]] + [[/each]] + + [[/with]] + [[/each]] + + [[/each]] +
    Totals[[short_name]]
    Squad
    Party [[partyNo]] ([[members.length]] member[[members.length > 1 && 's' || '']])
    Party [[partyNo]][[short_name]]
    + [[data.archetypes[archetype]]] + [[data.specialisations[profession][elite]]] + [[#if name]] + [[name]] + [[else]] + PRIVATE + [[/if]] +
    +
    +
    diff --git a/raidar/templates/raidar/encounter_combat_data.html b/raidar/templates/raidar/encounter_combat_data.html new file mode 100644 index 00000000..b44faebd --- /dev/null +++ b/raidar/templates/raidar/encounter_combat_data.html @@ -0,0 +1,268 @@ +{% load staticfiles %} +
    + {% include 'raidar/encounter_row_header.html' %} + +
    + + + + + + + + + + + + + + + + [[#with encounter.phases[page.phase]]] + + [[#with actual]] + + [[else]] + + [[/with]] + [[#with actual_boss]] + + [[else]] + + [[/with]] + [[#with received]] + + + [[else]] + + + [[/with]] + [[#with shielded]] + + [[else]] + + [[/with]] + [[#with actual]] + + + + + [[/with]] + [[/with]] + + [[#each encounter.parties:partyNo]] + [[#with phases[page.phase]]] + + + [[#with actual]] + + [[/with]] + [[#with actual_boss]] + + [[else]] + + [[/with]] + [[#with received]] + + + [[else]] + + + [[/with]] + [[#with shielded]] + + [[else]] + + [[/with]] + [[#with actual]] + + + + + [[/with]] + + [[/with]] + [[/each]] + + [[#each encounter.parties:partyNo]] + + + + + + + + + + + + + + [[#each members]] + + + [[#with phases[page.phase]]] + [[#with actual]] + + [[else]] + + [[/with]] + [[#with actual_boss]] + + [[else]] + + [[/with]] + [[#with received]] + + + [[else]] + + + [[/with]] + [[#with shielded]] + + [[else]] + + [[/with]] + [[#with actual]] + + + + + [[else]] + + + + [[/with]] + [[else]] + + + + + + + + [[/with]] + + [[/each]] + + [[/each]] +
    TotalsDPSDPS to BossDPS ReceivedDmg ReceivedShieldedCritSeaweedScholarFlanking
    Squad + [[dps]] + [[#if group && group.avg_dps]] + [[Math.round(group.avg_dps)]] + [[/if]] + + [[dps]] + [[#if group && group.avg_dps_boss]] + [[Math.round(group.avg_dps_boss)]] + [[/if]] + + [[dps]] + [[#if group && group.avg_dps_received]] + [[Math.round(group.avg_dps_received)]] + [[/if]] + + [[total]] + [[#if group && group.avg_total_received]] + [[Math.round(group.avg_total_received)]] + [[/if]] + + [[total]] + [[#if group && group.avg_total_shielded]] + [[Math.round(group.avg_total_shielded)]] + [[/if]] + + [[crit]]% + [[#if group && group.avg_crit]] + [[group.avg_crit.toPrecision(3)]]% + [[/if]] + + [[seaweed]]% + [[#if group && group.avg_seaweed]] + [[group.avg_seaweed.toPrecision(3)]]% + [[/if]] + + [[scholar]]% + [[#if group && group.avg_scholar]] + [[group.avg_scholar.toPrecision(3)]]% + [[/if]] + + [[flanking]]% + [[#if group && group.avg_flanking]] + [[group.avg_flanking.toPrecision(3)]]% + [[/if]] +
    Party [[partyNo]] ([[members.length]] member[[members.length > 1 && 's' || '']]) + [[dps]] + + [[dps]] + + [[dps]] + + [[total]] + + [[total]] + + [[crit]]% + + [[seaweed]]% + + [[scholar]]% + + [[flanking]]% +
    Party [[partyNo]]DPSDPS To BossDPS ReceivedDmg ReceivedShieldedCritSeaweedScholarFlanking
    + [[data.archetypes[archetype]]] + [[data.specialisations[profession][elite]]] + [[#if name]] + [[name]] + [[else]] + PRIVATE + [[/if]] + + [[dps]] + [[#if archetype.avg_dps]] + [[Math.round(archetype.avg_dps)]] + [[/if]] + 0 + [[dps]] + [[#if archetype.avg_dps_boss]] + [[Math.round(archetype.avg_dps_boss)]] + [[/if]] + 0 + [[dps]] + [[#if archetype.avg_dps_received]] + [[Math.round(archetype.avg_dps_received)]] + [[/if]] + + [[total]] + [[#if archetype.avg_total_received]] + [[Math.round(archetype.avg_total_received)]] + [[/if]] + 00 + [[total]] + [[#if archetype.avg_total_shielded]] + [[Math.round(archetype.avg_total_shielded)]] + [[/if]] + 0 + [[crit]]% + [[#if archetype.avg_crit]] + [[archetype.avg_crit.toPrecision(3)]]% + [[/if]] + + [[seaweed]]% + [[#if archetype.avg_seaweed]] + [[archetype.avg_seaweed.toPrecision(3)]]% + [[/if]] + + [[scholar]]% + [[#if archetype.avg_scholar]] + [[archetype.avg_scholar.toPrecision(3)]]% + [[/if]] + + [[flanking]]% + [[#if archetype.avg_flanking]] + [[archetype.avg_flanking.toPrecision(3)]]% + [[/if]] + 000
    +
    +
    diff --git a/raidar/templates/raidar/encounter_row_header.html b/raidar/templates/raidar/encounter_row_header.html new file mode 100644 index 00000000..526e62d6 --- /dev/null +++ b/raidar/templates/raidar/encounter_row_header.html @@ -0,0 +1,74 @@ +{% load staticfiles %} + + + + + + + + + [[#each encounter.parties:partyNo]] + + + + [[/each]] + + [[#each encounter.parties:partyNo]] + + + + + [[#each members]] + + + + [[/each]] + + [[/each]] +
    Totals
    +
    + Squad +
    +
    + [[#with encounter.phases[page.phase].events]] + Deaths: [[deaths]][[#if deaths]] ([[formatTime(dead_time / 1000)]])[[/if]]
    + Downs: [[downs]][[#if downs]] ([[formatTime(down_time / 1000)]])[[/if]]
    + Disconnects: [[disconnects]][[#if disconnects]] ([[formatTime(disconnect_time / 1000)]])[[/if]]
    + [[/with]] +
    +
    +
    + Party [[partyNo]] ([[members.length]] member[[members.length > 1 && 's' || '']]) +
    +
    + [[#with phases[page.phase].events]] + Deaths: [[deaths]][[#if deaths]] ([[formatTime(dead_time / 1000)]])[[/if]]
    + Downs: [[downs]][[#if downs]] ([[formatTime(down_time / 1000)]])[[/if]]
    + Disconnects: [[disconnects]][[#if disconnects]] ([[formatTime(disconnect_time / 1000)]])[[/if]]
    + [[/with]] +
    +
    Party [[partyNo]]
    +
    + [[data.archetypes[archetype]]] + [[data.specialisations[profession][elite]]] + [[#if name]] + [[name]] + [[else]] + PRIVATE + [[/if]] +
    +
    + + [[#if account]] + [[account]] + [[else]] + PRIVATE + [[/if]] +
    + [[#with phases[page.phase].events]] + Deaths: [[deaths]][[#if deaths]] ([[formatTime(dead_time / 1000)]])[[/if]]
    + Downs: [[downs]][[#if downs]] ([[formatTime(down_time / 1000)]])[[/if]]
    + Disconnects: [[disconnects]][[#if disconnects]] ([[formatTime(disconnect_time / 1000)]])[[/if]]
    + [[/with]] +
    +
    diff --git a/raidar/templates/raidar/encounters.html b/raidar/templates/raidar/encounters.html new file mode 100644 index 00000000..0ded6120 --- /dev/null +++ b/raidar/templates/raidar/encounters.html @@ -0,0 +1,198 @@ +{% load staticfiles %} +
    + +
    + + + + + + + + + + + + + + [[#if settings.encounterSort.filters]] + + + + + + + + + + + [[/if]] + + + [[#each encounterSlice]] + + + + + + + + + + + [[/each]] +
    + Area + [[#if settings.encounterSort.prop != 'area']] + + [[elseif settings.encounterSort.dir == 'up']] + + [[else]] + + [[/if]] + + Time + [[#if settings.encounterSort.prop != 'started_at']] + + [[elseif settings.encounterSort.dir == 'up']] + + [[else]] + + [[/if]] + + Dur. + [[#if settings.encounterSort.prop != 'duration']] + + [[elseif settings.encounterSort.dir == 'up']] + + [[else]] + + [[/if]] + + Character + [[#if settings.encounterSort.prop != 'character']] + + [[elseif settings.encounterSort.dir == 'up']] + + [[else]] + + [[/if]] + + Account + [[#if settings.encounterSort.prop != 'account']] + + [[elseif settings.encounterSort.dir == 'up']] + + [[else]] + + [[/if]] + + Upload + [[#if settings.encounterSort.prop != 'uploaded_at']] + + [[elseif settings.encounterSort.dir == 'up']] + + [[else]] + + [[/if]] + + Category + [[#if settings.encounterSort.prop != 'category']] + + [[elseif settings.encounterSort.dir == 'up']] + + [[else]] + + [[/if]] + + Tags +
    + [[#if settings.encounterSort.filters]] + + [[else]] + + [[/if]] +
    +
    +
    + + + +

    + + + [[#each encountersAreas]] + +
    +
    + +
    +
    + +
    + + + [[#each encountersCharacters]] + + + + + [[#each encountersAccounts]] + + +
    + +
    + + + +
    +
    +
    + +
    +
      + [[#each encounterPages]] + [[#if a || d]] +
    • [[t]]
    • + [[else]] +
    • [[t]]
    • + [[/if]] + [[/each]] +
    diff --git a/raidar/templates/raidar/global_stats.html b/raidar/templates/raidar/global_stats.html new file mode 100644 index 00000000..f0e5235f --- /dev/null +++ b/raidar/templates/raidar/global_stats.html @@ -0,0 +1,123 @@ +{% load staticfiles %} + +[[#with global_stats]] +
    +
    +
    +

    [[page.area_id ? findId(areas, page.area_id).name : "All logs"]]

    +

    in the [[page.era_id ? findId(eras, page.era_id).name : "most recent"]] Era

    +
    +
    + + +
    + +
    +

    Disclaimer: The statistics displayed in this report are based off data available within GW2Raidar only, as such it is not a complete representation of the Guild Wars 2 raiding community. Efforts are made to ensure accuracy, however some anomalies may exist. Only successful runs are included and builds with fewer than 10 data points are not shown.

    + + [[#with stats]] +
    + + + + + + + + + + + + [[#if group.per_duration]] + + [[#each [ + {"percentiles": p_r(group.per_duration), "max": group.max_duration}, + {"percentiles": p(group.per_dps), "max": group.max_dps}, + {"percentiles": p(group.per_dps_boss), "max": group.max_dps_boss}, + {"percentiles": p_r(group.per_dps_received), "max": group.max_dps_received} + ] ]] + [[#if percentiles]] + + [[else]] + + [[/if]] + [[/each]] + [[else]] + + [[/if]] + +
    Duration (seconds)Cleave DPSBoss DPSDPS Received
    Full group +
    Not enough data to display group statistics
    + [[#each [{archetype: 1}, {archetype: 2}, {archetype: 5}] ]] +

    [[data.archetypes[archetype]]]

    +
    + + + + + + + + + + + + [[#each flattenStats(build)]] + [[#with {lineStats: build[professionId][eliteId][archetypeId]}]] + [[#if professionId != 'All' && eliteId != 'All' && archetypeId == archetype]] + [[#with {buffs: highestBuffs(build[professionId][eliteId][archetypeId].buffs_out)}]] + + + + [[#each [ + {"percentiles": p(lineStats.per_dps), "max": individual.max_dps}, + {"percentiles": boss_dps_percentiles, "max": individual.max_dps_boss}, + buffs[0], + buffs[1], + buffs[2] , + buffs[3], + buffs[4] + ] ]] + [[#if percentiles]] + + [[else]] + + [[/if]] + [[/each]] + + [[/with]] + [[/if]] + [[/with]] + [[else]] + + [[/each]] + +
    SpecialisationPopularityCleave DPSBoss DPSOutgoing buffs
    + [[data.specialisations[professionId][eliteId]]] + [[professionId === 'All' ? 'All' : data.specialisations[professionId][eliteId]]] + [[(lineStats.count/group.count).toFixed(2)]] + [[#if buff_image]] + [[buff_name]] + [[/if]] +
    Not enough data to display for [[data.archetypes[archetype]]] specialisations
    +
    + [[/each]] +
    +
    +

    The "Buff importance rating" shown in tooltips is used to determine which buffs to display and is not intended as a precise measure. In approximate terms, a 'buff importance rating' of 10000 (on a damage buff) equates to a 100% damage boost for one player, a 20% damage boost for 5 players, or a 10% damage boost across 10 players.

    +
    + [[/with]] +
    +[[/with]] diff --git a/raidar/templates/raidar/index.html b/raidar/templates/raidar/index.html index 4c67269f..4ed5fcbd 100644 --- a/raidar/templates/raidar/index.html +++ b/raidar/templates/raidar/index.html @@ -1,102 +1,204 @@ {% load staticfiles %} - + + GW2 Raidar + + + + + + + + + + + - + {% csrf_token %} -
    +
    diff --git a/raidar/templates/raidar/info_about.html b/raidar/templates/raidar/info_about.html new file mode 100644 index 00000000..0d633b99 --- /dev/null +++ b/raidar/templates/raidar/info_about.html @@ -0,0 +1,51 @@ +

    Donate

    +

    As with any website, hosting and server costs are always at play. If you love this site, why not consider donating? A one off donation of a dollar or ten dollars is a huge help in providing this website for free and ad-free to the community.

    +
    + + + + +
    + +

    The Team

    + +

    Big Bossy Boots

    +
      +
    • Name: Merforga
    • +
    • In Game: Merforga.4731
    • +
    • Reddit: u/merforga1
    • +
    • Email: admin@gw2raidar.com
    • +
    • Skills: Bossing people around, making unrealistic demands, financier
    • +
    + +

    Head Code Monkey

    +
      +
    • Name: Delight/Amadan
    • +
    • In Game: AmadanMath.2409
    • +
    • Reddit: u/Amadan
    • +
    • Skills: Goofs up a lot, releases things into the wild
    • +
    + +

    Code Monkey 1

    +
      +
    • Name: Toeofdoom / Veranaday
    • +
    • In Game: toeofdoom.6152
    • +
    • Reddit: u/Toeofdoom
    • +
    • Skills: Invents and builds data engines
    • +
    + +

    Code Monkey 2

    + + +

    Legalese Stuff

    +

    GW2 Raidar uses a combination of assets from the Official Guild Wars 2 website (guildwars2.com) as well as the Guild Wars 2 Wiki (wiki.guildwars2.com). Usage of assets and copyright fall under their respective website license agreements as per below

    + +

    Content provided by individual contributors, which is original and does not infringe upon the intellectual property rights of any third party, is available under the GNU Free Documentation License 1.2 (GFDL).

    + +

    Content obtained from Guild Wars 2, its web sites, manuals and guides, concept art and renderings, press and fansite kits, and other such copyrighted material, may also be available from this site. All rights, title and interest in and to such content remains with ArenaNet or NCsoft, as applicable, and such content is not licensed pursuant to the GFDL

    + diff --git a/raidar/templates/raidar/info_contact.html b/raidar/templates/raidar/info_contact.html new file mode 100644 index 00000000..78f4adaf --- /dev/null +++ b/raidar/templates/raidar/info_contact.html @@ -0,0 +1,21 @@ +

    Contact

    + +
    + [[#if !username]] +
    + +
    +
    + +
    + [[/if]] +
    + +
    +
    + +
    +
    +
    +
    +
    diff --git a/raidar/templates/raidar/info_help.html b/raidar/templates/raidar/info_help.html new file mode 100644 index 00000000..006b32c7 --- /dev/null +++ b/raidar/templates/raidar/info_help.html @@ -0,0 +1,92 @@ +{% load staticfiles %} +

    What is GW2 Raidar?

    + +

    With the advent and increased usage of DPS meters, there have been a number of different parsers released to analyse, in detail, combat statistics and logs for post review.

    + +

    While current encounter reports are great, they currently lack one thing, context. GW2 Raidar’s approach to log parsing is slightly different. By retaining high level data about uploaded encounters, GW2 Raidar will allow you compare your individual and squad performance against everyone else’s as an average which will allow you to further analyse your post fight logs.

    + +

    With GW2 Raidar you can:

    + +
      +
    • See at a top level your performance and your squad’s performance at a glance
    • +
    • See how your and your squads metrics compare to current averages
    • +
    • See what the top group metrics and what professions are most popular for each encounter
    • +
    • Upload 1 log per encounter and have everyone in the squad access the encounter automatically via their GW2 Raidar account
    • +
    • Link multiple Guild Wars 2 accounts to a single GW2 Raidar account for a consolidated single view across all your raid characters
    • +
    • See your personal raid statistics as a whole
    • +
    • Redownload previous encounter log files to parse through another website or application.
    • +
    + +

    Getting Started

    + +

    Getting started is easy. Simply create a new account on the website. You will need to provide in addition to your desired login credentials, your Guild Wars 2 API Key to confirm your ownership of a Guild Wars 2 Account.

    + +

    GW2 Raidar will automatically get your GW2 account name and associated characters. Any encounters that have been uploaded with your GW2 account in them will show up straight away in your encounters page.

    + +

    Uploading Logs

    + +

    To upload your logs, simply drag and drop your log files into the browser window. GW2 Raidar supports both uncompressed and compressed logs. You can access results of each log by clicking on the name in the encounter list.

    + +

    Due to the need to update global statistics, GW2 Raidar utilises a batch processing system. As such, analysing may take a few minutes, however once a log is uploaded, it will always be analysed, even if you leave the website!

    + +

    Interpreting the numbers

    + +

    On each Encounter page, you’ll be shown a few different sections of statistics as per below. For all statistics (with the exception of Party stats), the following colour codes apply:

    + +
      +
    • Red to yellow to green: Visually compares your figure with global average. Red = Worse than average, Yellow = On par, Green = Better than average
    • +
    • Purple: Visually represent the current average
    • +
    + +

    Squad

    + + +

    Shows total squad values. The first number denotes your squad’s totals with the second number denoting the current global average

    + +

    Party

    + + +

    Allows you compare individual party information to see performance of each sub group. These have no averages.

    + +

    Individual

    + + +

    Shows individual values. The first number denotes individual values with the second number denoting the current global average. All averages are based on the detected archetype e.g. a Support Druid will have different averages compared to a Condi Druid.

    + +

    Individual archetypes are denoted by the icon to the left of the profession icon.

    + +

    FAQ

    +

    How do I get GW2 API Key?

    + +
      +
    1. Go to ArenaNet Account Applications and authenticate
    2. +
    3. Click the "New Key" button
    4. +
    5. Name your new API key. The name must include the phrase "GW2RAIDAR" in it
    6. +
    7. GW2 Raidar only requires "account" permission
    8. +
    9. Click the "Create API Key" button
    10. +
    11. Your API key will now show; copy it, and paste it into the GW2 Raidar Register form
    12. +
    + +

    It says my Account is already registered to another user

    +

    This means that an API key associated with your account has been used already by another GW2 Raidar user to link your GW2 account. You will need to remove the API key in question (shown in the error message) from your GW2 Account, and possibly create a new API key, in order to prove your ownership of that GW2 account and register your GW2 Raidar account.

    + +

    What combat logs does GW2 Raider Support?

    +

    Combat logs generated by arcdps (both uncompressed and compressed). To enable logging in ARCDPS:

    +
      +
    1. Press ALT + SHIFT + T, this will bring up the ARC settings window
    2. +
    3. Under the 'Config' section ensure 'SAVE EVTC LOGS ON ENCOUNTERS' is ticked
    4. +
    5. We recommend also ticking 'USE NPC NAME IN EVTC PATH' for easier tracking and 'COMPRESS EVTC WITH PS (WIN 10)' to reduce the size of your logs. Note that compression requires Windows 10
    6. +
    7. Logs are stored by default after each raid boss kill / attempt under 'My Documents/Guild Wars 2/addons/arcdps/arcdps.cbtlogs'
    8. +
    + +

    Can I make my info private?

    +

    You can change your privacy settings in your Account tab. There are three options to choose from:

    +
      +
    1. Private: Display your account and character names to no-one but yourself
    2. +
    3. Squad: Display your account and character names only to participants in your specific encounter
    4. +
    5. Public: Show your account and character names to everyone
    6. +
    +

    Privacy options will apply to all accounts and characters linked to your GW2R account.

    + +

    What encounters are taken into account for averages?

    +

    Only encounters marked as successful runs will be included in average statistics. Averages are also isolated by different balance patches (Eras).

    diff --git a/raidar/templates/raidar/info_releasenotes.html b/raidar/templates/raidar/info_releasenotes.html new file mode 100644 index 00000000..e3a3594c --- /dev/null +++ b/raidar/templates/raidar/info_releasenotes.html @@ -0,0 +1,108 @@ +

    Release Notes

    +

    + Version + [[data.version.id]] + released on + [[formatDate(data.version.timestamp)]] +

    +
      +
    • Added code in preparation for detection of Raid Challenge Mode instances
    • +
    • Changed response status code to 4xx for erroneous API upload requests
    • +
    +

    Version 1.0.0

    +

    This major release marks a massive milestone for GW2Raidar. Massive thanks to the community for your support, patience and feedback!

    +
      +
    • Added performance graphs to profile page. Clicking on a number in the table will now display a graph showing historical performance vs global metrics for that encounter / archetype combination
    • +
    • Added global reporting page. Global reporting will allow any user of the website to view global metrics of all encounters.
    • +
    • Added category filter and tag search to encounter list. Encounters page will now display horizontal scroll bar if table does not fit on screen.
    • +
    • Added damage absorbed by barrier to combat stats table. Any logs uploaded prior to this release will need to be uploaded again for this feature.
    • +
    • Added Regeneration to boon tracking table. Any logs uploaded prior to this release will need to be uploaded again for this feature.
    • +
    • Added API endpoint to allow uploading of files directly to the server. Details below: +
        +
      • Method: HTTP POST
      • +
      • Endpoint: https://www.gw2raidar.com/api/upload.json
      • +
      • Parameters: +
          +
        • username - GW2Raidar Username
        • +
        • password - GW2Raidar Password
        • +
        • file - The log file
        • +
        +
      • +
      • Responses: +
          +
        • 200 - Successful Upload
        • +
        +
      • +
      • Example: +
        curl -F "username=USERNAME" -F "password=PASSWORD" -F "file=@FILEPATH" https://www.gw2raidar.com/api/upload.json
        +
      • +
      +
    • +
    • Added Boon Output tracking output to encounters. Any logs uploaded prior to this release will need to be uploaded again for this feature. Intensity will show average numbers of stacks provided while duration will show average uptime provided for example: +
        +
      • 75 might on the boon output table indicates and average of 75 stacks were given to everyone by that player during the fight. In a party of 5, that would be 75 / 5 which equals 15 stacks of might uptime for 5 players.
      • +
      • 500% alacrity on the boon output table indicates an average of 500% alacrity uptime was provided by that player to the rest of the party. In a party of 5, that would be 500 / 5 which equals 100% alacrity uptime for 5 players.
      • +
      +
    • +
    • Re-enabled log archiving. Logs can be redownloaded by clicking on the filename in the top right corner of the encounter page. Privacy options will disable this feature if any player in the squad is deemed private to the user.
    • +
    • Many minor fixes to logs not being processed. Most logs impacted have been fed back through for processing.
    • +
    • Fixed a server crash.
    • +
    • If you love this site and can't live without it, consider a small donation to help support the running costs and invest in better infrastructure for more features!
    • +
    +

    Version 0.9.7

    +
      +
    • Added game build check for compatible ARC versions. GW2Raidar will now reject logs using game builds where ARC is not functioning properly (latest only).
    • +
    • Added Fractal CM support for Nightmare and Shattered Observatory
    • +
    • Added basic functionality to categorise raid encounter in preparation for website statistics reporting.
    • +
    • Added basic functionality to add custom user tags to encounters. Tags will be editable by all participants.
    • +
    +

    Version 0.9.6

    +
      +
    • Added more robust success detection. Applies to logs uploaded using ARC version 20170905 or higher
    • +
    • Updated 'classic' Xera success detection to minimise occurence of false successes.
    • +
    • Added Path of Fire Era. PoF Era will apply to all encounters starting from 22nd September 9AM GMT -7
    • +
    +

    Version 0.9.5

    +
      +
    • Added privacy options for GW2R Accounts. You can now select between one of the following options: +
        +
      1. Private: Display your account and character names to no-one but yourself
      2. +
      3. Squad: Display your account and character names only to participants in your specific encounter
      4. +
      5. Public: Show your account and character names to everyone
      6. +
      + Privacy options will apply to all accounts and characters linked to your GW2R account.
    • +
    +

    Version 0.9.4

    +
      +
    • Added basic Kitty Golem log support.
    • +
    • Added additional aggregation groups (Raids, Golems)
    • +
    • Tweaked log processor to make it more robust.
    • +
    • Modified Deimos success detector.
    • +
    • Fixed tiny Deimos Phase 2 bars.
    • +
    • Removed some less than desirable words from URL wordlist.
    • +
    • Fixed a server crash.
    • +
    +

    Version 0.9.3

    +
      +
    • Added support for non-English GW2 clients
    • +
    • From now on, GW2 API key used to authenticate accounts needs to have "gw2raidar" in its name
    • +
    • Added support for new ARC Version 20170905
    • +
    • Minimum ARC version relaxed to 20170419. Any logs prior to this date will be rejected.
    • +
    +

    Version 0.9.2

    +
      +
    • Added support for Retaliation uptime. (Any logs uploaded prior to this release will need to be uploaded again for Retaliation to appear.)
    • +
    • Better error handling
    • +
    +

    Version 0.9.1

    +
      +
    • Added multiple processor support
    • +
    • Minimum ARC version updated to 20170808. Any logs prior to this date will be rejected
    • +
    • Logs for most recent encounters will always be processed first
    • +
    +

    Version 0.9.0

    +
      +
    • + Initial Release of GW2Raidar \o/ +
    • +
    diff --git a/raidar/templates/raidar/intro.html b/raidar/templates/raidar/intro.html new file mode 100644 index 00000000..20cea030 --- /dev/null +++ b/raidar/templates/raidar/intro.html @@ -0,0 +1,74 @@ +{% load staticfiles %} +

    What is is GW2 Raidar?

    + +

    With the advent and increased usage of DPS meters in Guild Wars 2 Raid scene, there have been a number of different parsers released to analyse, in detail, combat statistics and logs for post review.

    + +

    While being able to assist in post encounter analysis is great, current lack one thing, context. GW2 Raidar’s approach to log parsing is slightly different. By retaining high level data about uploaded encounters, GW2 Raidar will allow you compare your individual and squad performance against everyone else’s as an average which will allow you to further analyse your post fight logs.

    + +

    With GW2 Raidar you can:

    + +
      +
    • See at a top level your performance and your squad’s performance at a glance
    • +
    • See how your and your squads metrics compare to current averages
    • +
    • See what the top group metrics and what professions are most popular for each encounter
    • +
    • Upload 1 log per encounter and have everyone in the squad access the encounter automatically via their GW2 Raidar account
    • +
    • Link multiple Guild Wars 2 accounts to a single GW2 Raidar account for a consolidated single view across all your raid characters
    • +
    • See your personal raid statistics as a whole
    • +
    + +

    Getting Started

    + +

    Getting started is easy. Simply create a new account on the website. You will need to provide in addition to your desired login credentials, your Guild Wars 2 API Key to confirm your ownership of a Guild Wars 2 Account.

    + +

    GW2 Raidar will automatically get your Guild Wars 2 account name and any already uploaded encounters that feature your Guild Wars 2 account will show up straight away in your encounters page.

    + +

    Uploading Logs

    + +

    To upload your logs, simply drag and drop your arcdps log files into the browser window. GW2 Raidar supports both uncompressed (.evtc) and compressed (.evtc.zip) logs. Your logs should appear in the Encounters page after they have been processed. You can access each encounters' data by clicking on the desired row.

    + +

    THE NUMBERS MASON! WHAT DO THEY MEAN?

    + +

    On each Encounter page, you’ll be shown a few different sections of statistics as per below. For all statistics (with the exception of Party stats), the following colour codes apply:

    + +
      +
    • Red to yellow to green: Visually compares your figure with global average. Red = Worse than average, Yellow = On par, Green = Better than average
    • +
    • Purple: Visually represent the current average
    • +
    + +

    Squad

    + + +

    Shows total squad values. The first number denotes your squad’s totals with the second number denoting the current global average

    + +

    Party

    + + +

    Allows you compare individual party information to see performance of each sub group. These have no averages.

    + +

    Individual

    + + +

    Shows individual values. The first number denotes individual values with the second number denoting the current global average. All averages are based on the detected archetype e.g. a Support Druid will have different averages compared to a Condi Druid.

    + +

    Individual archetypes are denoted by the icon to the left of the profession icon.

    + +

    FAQ

    +

    How do I get GW2 API Key?

    + +
      +
    1. Go to ArenaNet Account Applications and authenticate
    2. +
    3. Click the "New Key" button
    4. +
    5. Name your new API key sensibly (e.g. "GW2Raidar")
    6. +
    7. GW2 Raidar only requires "account" permission
    8. +
    9. Click the "Create API Key" button
    10. +
    11. Your API key will now show; copy it, and paste it into the GW2 Raidar Register form
    12. +
    + +

    It says my Account is already registered to another user

    +

    This means that an API key associated with your account has been used already by another GW2 Raidar user to link your GW2 account. You will need to remove the API key in question (shown in the error message) from your GW2 Account, and possibly create a new API key, in order to prove your ownership of that GW2 account and register your GW2 Raidar account.

    + +

    I can’t see an encounter someone else linked me

    +

    If your linked account was not in the raid squad for an encounter someone else uploaded then you will not be able to see the encounter view.

    + +

    What combat logs does GW2 Raider Support?

    +

    Combat logs generated by arcdps (both uncompressed and compressed). Supports logs from arcdps Version May 21 2017.

    diff --git a/raidar/templates/raidar/navbar.html b/raidar/templates/raidar/navbar.html new file mode 100644 index 00000000..e63c308f --- /dev/null +++ b/raidar/templates/raidar/navbar.html @@ -0,0 +1,38 @@ + diff --git a/raidar/templates/raidar/profile.html b/raidar/templates/raidar/profile.html new file mode 100644 index 00000000..2f0f013a --- /dev/null +++ b/raidar/templates/raidar/profile.html @@ -0,0 +1,145 @@ +{% load staticfiles %} +[[#with profile]] +
    +
    +

    Profile: [[username]]

    +
    + Member since [[formatDate(joined_at)]] +
    +
    + Played through + [[page.era.profile.count]] + encounters in + [[page.era.name]] +
    +

    [[page.era.name]] Era

    +
    Started at: [[formatDate(page.era.started_at)]]
    +
    [[page.era.description]]
    +
    + era +
    +
    +
    + +
    +
    + + + [[#with page.era.profile]] +
    +
    + [[#each keysWithAllLast(encounter, data.areas)]] + [[#with {encounterId: .}]] + [[#with encounter[encounterId].archetype.All.profession.All.elite.All]] + + [[/with]] + [[/with]] + [[/each]] +
    +
    + + + [[#if encounter[page.area]]] +
    +
    +
    Live [[ + (100 + - encounter[page.area].archetype.All.profession.All.elite.All.avg_down_percentage + - encounter[page.area].archetype.All.profession.All.elite.All.avg_dead_percentage + ).toPrecision(3)]]%
    +
    Down [[encounter[page.area].archetype.All.profession.All.elite.All.avg_down_percentage.toPrecision(3)]]%
    +
    Dead [[encounter[page.area].archetype.All.profession.All.elite.All.avg_dead_percentage.toPrecision(3)]]%
    +
    +
    +
    Support [[encounter[page.area].archetype[5] ? (100 * encounter[page.area].archetype[5].profession.All.elite.All.count / encounter[page.area].archetype.All.profession.All.elite.All.count).toPrecision(3) : 0]]%
    +
    Power [[encounter[page.area].archetype[1] ? (100 * encounter[page.area].archetype[1].profession.All.elite.All.count / encounter[page.area].archetype.All.profession.All.elite.All.count).toPrecision(3) : 0]]%
    +
    Condi [[encounter[page.area].archetype[2] ? (100 * encounter[page.area].archetype[2].profession.All.elite.All.count / encounter[page.area].archetype.All.profession.All.elite.All.count).toPrecision(3) : 0]]%
    +
    +
    + + + [[#each keysWithAllLast(encounter[page.area].archetype, {1: 2, 2: 3, 5: 1})]] + [[#with {archetypeId: ., archetype: encounter[page.area].archetype[.]}]] +

    + [[#if archetypeId !== 'All']] + [[data.archetypes[archetypeId]]] + [[/if]] + [[archetypeId === 'All' ? 'All' : data.archetypes[archetypeId]]] + Archetypes on + [[page.area.match(/^All \w+ bosses$/) ? page.area : data.areas[page.area]]] +

    + + + + + + + + + + + + + + + [[#each keysWithAllLast(archetype.profession)]] + [[#with {professionId: ., profession: archetype.profession[.]}]] + [[#each keysWithAllLast(profession.elite)]] + [[#with {eliteId: ., elite: profession.elite[.]}]] + [[#with elite]] + + + + + + + + + + + [[/with]] + [[/with]] + [[/each]] + [[/with]] + [[/each]] + +
    ProfessionEncountersAvg DPSMax DPSAvg Boss DPSMax Boss DPSDownDead
    + [[#if professionId !== 'All']] + [[data.specialisations[professionId][eliteId]]] + [[/if]] + [[professionId === 'All' ? 'All' : data.specialisations[professionId][eliteId]]] + + [[.count]] + + [[.avg_down_percentage !== undefined ? .avg_down_percentage.toPrecision(3) + '%' : '']] + + [[.avg_dead_percentage !== undefined ? .avg_dead_percentage.toPrecision(3) + '%' : '']] +
    + [[/with]] + [[/each]] + [[/if]] + [[/with]] +[[/with]] diff --git a/raidar/templates/raidar/thank-you.html b/raidar/templates/raidar/thank-you.html new file mode 100644 index 00000000..c8cc1023 --- /dev/null +++ b/raidar/templates/raidar/thank-you.html @@ -0,0 +1,6 @@ +

    Thank you!

    +

    Thank you so much for your donation!! We work hard to try and bring something valuable to the Guild Wars 2 community and your donation will help us continually do so. Not only will it help towards the monthly running costs, but will also allow us to look into investing into better infrastructure which will result in a more feature rich website for the entire community.

    +

    Not only that, but it will keep this website ad free and free from subscriptions for everyone to use.

    +

    if you have any questions, suggestions, feedback or to become a regular donor, feel free to contact us

    +

    Sincerely,

    +

    Merforga and the rest of the GW2R team

    diff --git a/raidar/templates/raidar/uploads.html b/raidar/templates/raidar/uploads.html new file mode 100644 index 00000000..cf66d6e9 --- /dev/null +++ b/raidar/templates/raidar/uploads.html @@ -0,0 +1,55 @@ + + + + + + + + + + [[#each upload]] + + + + + + [[/each]] + +
    + File + + Uploader + + Status +
    + [[name]] + + [[uploaded_by]] + + [[#if success]] + Success + [[elseif success === false]] + [[error || "Error"]] + [[elseif progress]] + [[#if progress < 100]] + Uploading [[progress]]% + [[else]] + Analysing + [[/if]] + [[else]] + Queued + [[/if]] +
    + +

    + To upload your logs, simply drag and drop them anywhere into GW2 Raidar. + GW2 Raidar supports both .evtc and .evtc.zip formats + from ARCDPS. While they're being uploaded, you can continue to browse around + the website. Click on any log file marked green to view the results. +

    +

    + Processing may take a few minutes, however once a log is in a status of "Analysing", it will be processed even if you leave the website. +

    +

    + Note that refreshing or closing the page will cancel any pending uploads. +

    diff --git a/raidar/urls.py b/raidar/urls.py index dd172474..d1f21fd6 100644 --- a/raidar/urls.py +++ b/raidar/urls.py @@ -1,12 +1,39 @@ -from django.conf.urls import url +from django.conf import settings +from django.conf.urls import url, include +from django.contrib.auth import views as auth_views +import importlib from . import views urlpatterns = [ - url(r'initial', views.initial, name = "initial"), - url(r'login', views.login, name = "login"), - url(r'logout', views.logout, name = "logout"), - url(r'register', views.register, name = "register"), - url(r'upload', views.upload, name = "upload"), - url(r'^$', views.index, name = "index"), + url(r'^(?Pencounters|profile|uploads|account|register|login|index|reset_pw|thank-you|info-(?:help|releasenotes|contact|about))(?:/(?P\w+))?$', views.named, name = "named"), + url(r'^initial.json$', views.initial, name = "initial"), + url(r'^login.json$', views.login, name = "login"), + url(r'^logout.json$', views.logout, name = "logout"), + url(r'^register.json$', views.register, name = "register"), + url(r'^reset_pw.json$', views.reset_pw, name = "reset_pw"), + url(r'^upload.json$', views.upload, name = "upload"), + url(r'^api/upload.json$', views.api_upload, name = "api_upload"), + url(r'^privacy.json$', views.privacy, name = "privacy"), + url(r'^profile_graph.json$', views.profile_graph, name = "profile_graph"), + url(r'^set_tags_cat.json$', views.set_tags_cat, name = "set_tags_cat"), + url(r'^contact.json$', views.contact, name = "contact"), + url(r'^poll.json$', views.poll, name = "poll"), + url(r'^change_email.json$', views.change_email, name = "change_email"), + url(r'^change_password.json$', views.change_password, name = "change_password"), + url(r'^add_api_key.json$', views.add_api_key, name = "add_api_key"), + url(r'^encounter/(?P\w+)(?P\.json)?$', views.encounter, name = "encounter"), + url(r'^profile.json$', views.profile, name = "profile"), + url(r'^reset/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + auth_views.password_reset_confirm, name='password_reset_confirm'), + url(r'^reset/done/$', auth_views.password_reset_complete, name='password_reset_complete'), + url(r'^download/(?P\w+)?$', views.download, name="download"), + url(r'^$', views.index, name = "index"), + url(r'^global_stats(?:/(?P[0-9]+))?(?:/area-(?P[0-9]+))?(?P\.json)?$', views.global_stats, name = "global_stats"), ] + +if settings.DEBUG and importlib.util.find_spec('debug_toolbar'): + import debug_toolbar + urlpatterns = [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + urlpatterns diff --git a/raidar/views.py b/raidar/views.py index a7bd1aab..d2093713 100644 --- a/raidar/views.py +++ b/raidar/views.py @@ -1,44 +1,72 @@ -from json import dumps as json_dumps -from django.http import JsonResponse -from django.shortcuts import render -from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout -from django.middleware.csrf import get_token +from .models import * +from analyser.analyser import Analyser, Group, Archetype, EvtcAnalysisException +from analyser.bosses import BOSSES +from django.conf import settings +from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout, update_session_auth_hash +from django.contrib.auth.decorators import login_required +from django.contrib.auth.forms import PasswordChangeForm, PasswordResetForm from django.contrib.auth.models import User +from django.contrib.auth.tokens import default_token_generator +from django.core import serializers +from django.core.mail import EmailMessage +from django.views.decorators.csrf import csrf_exempt +from smtplib import SMTPException from django.db.utils import IntegrityError +from django.http import JsonResponse, HttpResponse, Http404 +from django.middleware.csrf import get_token +from django.shortcuts import render +from django.utils import timezone +from django.utils.http import urlsafe_base64_decode +from django.views.decorators.cache import never_cache +from django.views.decorators.debug import sensitive_post_parameters, sensitive_variables from django.views.decorators.http import require_GET, require_POST -from django.contrib.auth.decorators import login_required -from evtcparser.parser import Encounter as EvtcEncounter -from analyser.analyser import Analyser +from gw2api.gw2api import GW2API, GW2APIException +from itertools import groupby +from json import dumps as json_dumps +from os import makedirs, sep as dirsep +from os.path import join as path_join, isfile, dirname +import pytz from datetime import datetime -from django.utils import timezone -from django.core import serializers -from re import match -from .models import * +from re import match, sub +from time import time +import logging +import numpy as np +import base64 + +logger = logging.getLogger(__name__) +def _safe_get(f, default=None): + try: + return f() + except (KeyError, TypeError): + return default -def _error(msg, **kwargs): - kwargs['error'] = msg - return JsonResponse(kwargs) +def _error(msg, status=200, **kwargs): + kwargs['error'] = str(msg) + return JsonResponse(kwargs, status=status) def _userprops(request): - if request.user: + accounts = request.user.accounts.all() if request.user.is_authenticated else [] return { 'username': request.user.username, - 'is_staff': request.user.is_staff + 'is_staff': request.user.is_staff, + 'accounts': [{ + "name": account.name, + "api_key": account.api_key[:8] + + sub(r"[0-9a-fA-F]", "X", account.api_key[8:-12]) + + account.api_key[-12:] + if account.api_key != "" else "", + } + for account in accounts], } - else: - return {} + def _encounter_data(request): - encounters = Encounter.objects.filter(characters__account__user=request.user) - return [{ - 'id': encounter.id, - 'area': encounter.area.name, - 'started_at': int(encounter.started_at.strftime('%s')), - } for encounter in encounters] + participations = Participation.objects.filter(character__account__user=request.user).select_related('encounter', 'character', 'character__account') + return [participation.data() for participation in participations] def _login_successful(request, user): auth_login(request, user) @@ -46,27 +74,307 @@ def _login_successful(request, user): userprops = _userprops(request) userprops['csrftoken'] = csrftoken userprops['encounters'] = _encounter_data(request) + userprops['privacy'] = request.user.user_profile.privacy return JsonResponse(userprops) -@require_GET -def index(request): + +def _html_response(request, page, data={}): + response = _userprops(request) + response.update(data) + try: + response['ga_property_id'] = settings.GA_PROPERTY_ID + except: + # No Google Analytics, it's fine + pass + response['archetypes'] = {k: v for k, v in Participation.ARCHETYPE_CHOICES} + response['areas'] = {area.id: area.name for area in Area.objects.all()} + response['specialisations'] = {p: {e: n for (pp, e), n in Character.SPECIALISATIONS.items() if pp == p} for p, _ in Character.PROFESSION_CHOICES} + response['categories'] = {category.id: category.name for category in Category.objects.all()} + response['page'] = page + response['debug'] = settings.DEBUG + response['version'] = settings.VERSION + if request.user.is_authenticated: + response['privacy'] = request.user.user_profile.privacy + if request.user.is_authenticated: + try: + last_notification = request.user.notifications.latest('id') + response['last_notification_id'] = last_notification.id + except Notification.DoesNotExist: + # it's okay + pass return render(request, template_name='raidar/index.html', context={ - 'userprops': json_dumps(_userprops(request)) + 'userprops': json_dumps(response), }) +@require_GET +def download(request, url_id=None): + if not hasattr(settings, 'UPLOAD_DIR'): + return Http404("Not allowed") + + encounter = Encounter.objects.get(url_id=url_id) + own_account_names = [account.name for account in Account.objects.filter( + characters__participations__encounter_id=encounter.id, + user=request.user)] + dump = encounter.val + members = [{ "name": name, **value } for name, value in dump['Category']['status']['Player'].items() if 'account' in value] + + encounter_showable = True + for member in members: + is_self = member['account'] in own_account_names + + user_profile = UserProfile.objects.filter(user__accounts__name=member['account']) + if user_profile: + privacy = user_profile[0].privacy + if not is_self and (privacy == UserProfile.PRIVATE or (privacy == UserProfile.SQUAD and not own_account_names)): + encounter_showable = False + + path = encounter.diskname() + if isfile(path) and (encounter_showable or request.user.is_staff): + response = HttpResponse(open(path, 'rb'), content_type='application/vnd.ms-excel') + response['Content-Disposition'] = 'attachment; filename="%s"' % encounter.filename + return response + else: + raise Http404("Not allowed") + + +@require_GET +def index(request, page={ 'name': '' }): + return _html_response(request, page) + + +@require_GET +def profile(request): + if not request.user.is_authenticated: + return _error("Not authenticated") + + user = request.user + queryset = EraUserStore.objects.filter(user=user).select_related('era') + try: + eras = [{ + 'id': era_user_store.era_id, + 'name': era_user_store.era.name, + 'started_at': era_user_store.era.started_at, + 'description': era_user_store.era.description, + 'profile': era_user_store.val, + } for era_user_store in queryset] + except EraUserStore.DoesNotExist: + eras = [] + + profile = { + 'username': user.username, + 'joined_at': (user.date_joined - datetime.utcfromtimestamp(0).replace(tzinfo=pytz.UTC)).total_seconds(), + 'eras': eras, + } + + result = { + "profile": profile + } + return JsonResponse(result) + +@require_GET +def global_stats(request, era_id=None, area_id=None, json=None): + if not json: + return _html_response(request, { + "name": "global_stats", + "era_id": era_id, + "area_id": area_id + }) + try: + era_query = Era.objects.all() + eras = [{ + 'name': era.name, + 'id': era.id, + 'started_at': era.started_at, + 'description': era.description + } for era in era_query] + except Era.DoesNotExist: + eras = [] + + try: + area_query = Area.objects.filter(era_area_stores__isnull = False).distinct() + areas = [{ + 'name': area.name, + 'id': area.id, + } for area in area_query] + except Area.DoesNotExist: + areas = [] + + try: + if era_id is None: + era_id = eras[0]['id'] + era = Era.objects.get(id=era_id) + if area_id is None: + raw_data = era.val + else: + area = Area.objects.get(id=area_id) + raw_data = EraAreaStore.objects.get(era=era, area=area).val + stats = raw_data['All'] + + #reduce size of json for global stats view + builds = [stats['build'][prof][elite][arch] + for prof in stats['build'] + for elite in stats['build'][prof] + for arch in stats['build'][prof][elite]] + + builds.append(stats['group']) + builds.append(stats['individual']) + + for build in list(builds): + if 'buffs' in build: + del build['buffs'] + if 'count' not in build or build['count'] < 10: + for key in list(build.keys()): + del(build[key]) + + if 'buffs_out' in build: + for buff in list(filter(lambda a: a.startswith('max_'), build['buffs_out'].keys())): + if build['buffs_out'][buff] <= 0.01: + buffname = buff[4:] + for key in list(filter(lambda a: a.split('_', 1)[1] == buffname, + build['buffs_out'].keys())): + del(build['buffs_out'][key]) + + except (Era.DoesNotExist, Area.DoesNotExist, EraAreaStore.DoesNotExist, KeyError): + stats = {} + + result = {'global_stats': { + 'eras': eras, + 'areas': areas, + 'stats': stats + }} + return JsonResponse(result) + + +@require_GET +def encounter(request, url_id=None, json=None): + try: + encounter = Encounter.objects.select_related('area', 'uploaded_by').get(url_id=url_id) + except Encounter.DoesNotExist: + if json: + return _error("Encounter does not exist") + else: + raise Http404("Encounter does not exist") + own_account_names = [account.name for account in Account.objects.filter( + characters__participations__encounter_id=encounter.id, + user=request.user)] if request.user.is_authenticated else [] + + dump = encounter.val + members = [{ "name": name, **value } for name, value in dump['Category']['status']['Player'].items() if 'account' in value] + + try: + area_stats = EraAreaStore.objects.get(era=encounter.era, area=encounter.area).val + except EraAreaStore.DoesNotExist: + area_stats = None + phases = _safe_get(lambda: dump['Category']['encounter']['phase_order'] + ['All'], list(dump['Category']['combat']['Phase'].keys())) + partyfunc = lambda member: member['party'] + namefunc = lambda member: member['name'] + parties = { party: { + "members": sorted(members, key=namefunc), + "phases": { + phase: { + "actual": _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup'][str(party)]['Metrics']['damage']['To']['*All']), + "actual_boss": _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup'][str(party)]['Metrics']['damage']['To']['*Boss']), + "received": _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup'][str(party)]['Metrics']['damage']['From']['*All']), + "shielded": _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup'][str(party)]['Metrics']['shielded']['From']['*All']), + "buffs": _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup'][str(party)]['Metrics']['buffs']['From']['*All']), + "buffs_out": _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup'][str(party)]['Metrics']['buffs']['To']['*All']), + "events": _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup'][str(party)]['Metrics']['events']), + "mechanics": _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup'][str(party)]['Metrics']['mechanics']), + } for phase in phases + } + } for party, members in groupby(sorted(members, key=partyfunc), partyfunc) } + private = False + + encounter_showable = True + for party_no, party in parties.items(): + for member in party['members']: + if member['account'] in own_account_names: + member['self'] = True + member['phases'] = { + phase: { + 'actual': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Player'][member['name']]['Metrics']['damage']['To']['*All']), + 'actual_boss': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Player'][member['name']]['Metrics']['damage']['To']['*Boss']), + 'received': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Player'][member['name']]['Metrics']['damage']['From']['*All']), + 'shielded': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Player'][member['name']]['Metrics']['shielded']['From']['*All']), + 'buffs': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Player'][member['name']]['Metrics']['buffs']['From']['*All']), + 'buffs_out': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Player'][member['name']]['Metrics']['buffs']['To']['*All']), + 'events': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Player'][member['name']]['Metrics']['events']), + 'mechanics': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Player'][member['name']]['Metrics']['mechanics']), + 'archetype': _safe_get(lambda: area_stats[phase]['build'][str(member['profession'])][str(member['elite'])][str(member['archetype'])]), + } for phase in phases + } + + user_profile = UserProfile.objects.filter(user__accounts__name=member['account']) + if user_profile: + privacy = user_profile[0].privacy + if 'self' not in member and (privacy == UserProfile.PRIVATE or (privacy == UserProfile.SQUAD and not own_account_names)): + member['name'] = '' + member['account'] = '' + private = True + encounter_showable = False + + data = { + "encounter": { + "evtc_version": _safe_get(lambda: dump['Category']['encounter']['evtc_version']), + "id": encounter.id, + "url_id": encounter.url_id, + "name": encounter.area.name, + "filename": encounter.filename, + "uploaded_at": encounter.uploaded_at, + "uploaded_by": encounter.uploaded_by.username, + "started_at": encounter.started_at, + "duration": encounter.duration, + "success": encounter.success, + "tags": encounter.tagstring, + "category": encounter.category_id, + "phase_order": phases, + "participated": own_account_names != [], + "boss_metrics": [metric.__dict__ for metric in BOSSES[encounter.area_id].metrics], + "phases": { + phase: { + 'duration': encounter.duration if phase == "All" else _safe_get(lambda: dump['Category']['encounter']['Phase'][phase]['duration']), + 'group': _safe_get(lambda: area_stats[phase]['group']), + 'individual': _safe_get(lambda: area_stats[phase]['individual']), + 'actual': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup']['*All']['Metrics']['damage']['To']['*All']), + 'actual_boss': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup']['*All']['Metrics']['damage']['To']['*Boss']), + 'received': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup']['*All']['Metrics']['damage']['From']['*All']), + 'shielded': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup']['*All']['Metrics']['shielded']['From']['*All']), + 'buffs': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup']['*All']['Metrics']['buffs']['From']['*All']), + 'buffs_out': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup']['*All']['Metrics']['buffs']['To']['*All']), + 'events': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup']['*All']['Metrics']['events']), + 'mechanics': _safe_get(lambda: dump['Category']['combat']['Phase'][phase]['Subgroup']['*All']['Metrics']['mechanics']), + } for phase in phases + }, + "parties": parties, + } + } + if encounter_showable or request.user.is_staff: + if encounter.gdrive_url: + data['encounter']['evtc_url'] = encounter.gdrive_url; + # XXX relic TODO remove once we fully cross to GDrive? + if hasattr(settings, 'UPLOAD_DIR'): + path = encounter.diskname() + if isfile(path): + data['encounter']['downloadable'] = True + + if json: + return JsonResponse(data) + else: + return _html_response(request, { "name": "encounter", "no": encounter.url_id }, data) + @require_GET def initial(request): response = _userprops(request) - if request.user.is_authenticated(): + if request.user.is_authenticated: response['encounters'] = _encounter_data(request) return JsonResponse(response) -@require_POST -def login(request): +@sensitive_variables('password') +def _perform_login(request): username = request.POST.get('username') password = request.POST.get('password') # stayloggedin = request.GET.get('stayloggedin') @@ -75,29 +383,92 @@ def login(request): # else: # request.session.set_expiry(0) - user = authenticate(username=username, password=password) + return authenticate(username=username, password=password) + + +@require_POST +@sensitive_post_parameters('password') +def login(request): + if request.method == 'GET': + return index(request, page={ 'name': 'login' }) + + user = _perform_login(request) if user is not None and user.is_active: return _login_successful(request, user) else: return _error('Could not log in') - @require_POST -def register(request): - username = request.POST.get('username') - password = request.POST.get('password') +@never_cache +def reset_pw(request): email = request.POST.get('email') + form = PasswordResetForm(request.POST) + if form.is_valid(): + opts = { + 'use_https': request.is_secure(), + 'email_template_name': 'registration/password_reset_email.html', + 'subject_template_name': 'registration/password_reset_subject.txt', + 'request': request, + } + form.save(**opts) + return JsonResponse({}); + + +@sensitive_post_parameters('password') +@sensitive_variables('password') +def register(request): + if request.method == 'GET': + return index(request, page={ 'name': 'register' }) + + username = request.POST.get('username').strip() + password = request.POST.get('password').strip() + email = request.POST.get('email').strip() + api_key = request.POST.get('api_key').strip() + gw2api = GW2API(api_key) + + try: + token_info = gw2api.query("/tokeninfo") + if 'gw2raidar' not in token_info['name'].lower(): + return _error("Your api key must be named 'gw2raidar'.") + gw2_account = gw2api.query("/account") + except GW2APIException as e: + return _error(e) + + account_name = gw2_account['name'] + account, _ = Account.objects.get_or_create(name=account_name) + + if account.user and account.user != request.user: + # Registered to another account + old_gw2api = GW2API(account.api_key) + try: + gw2_account = old_gw2api.query("/account") + # Old key is still valid, ask user to invalidate it + try: + old_api_key_info = old_gw2api.query("/tokeninfo") + key_id = "named '%s'" % old_api_key_info['name'] + except GW2APIException as e: + key_id = "ending in '%s'" % api_key[-4:] + new_key = "" if account.api_key != api_key else " and generate a new key" + + return _error("This GW2 account is registered to another user. To prove it is yours, please invalidate the key %s%s." % (key_id, new_key)) + except GW2APIException as e: + # Old key is invalid, reassign OK + pass try: user = User.objects.create_user(username, email, password) except IntegrityError: return _error('Such a user already exists') - if user: - return _login_successful(request, user) - else: + if not user: return _error('Could not register user') + account.user = user + account.api_key = api_key + account.save() + + return _login_successful(request, user) + @login_required @require_POST @@ -107,65 +478,237 @@ def logout(request): return JsonResponse({}) +def _perform_upload(request): + if (len(request.FILES) != 1): + return ("Only single file uploads are allowed", None) + + filename = next(iter(request.FILES)) + if 'file' in request.FILES: + file = request.FILES['file'] + else: + return ("Missing file attachment named `file`", None) + filename = file.name + uploaded_at = time() + + upload, _ = Upload.objects.update_or_create( + filename=filename, uploaded_by=request.user, + defaults={ "uploaded_at": time() }) + + diskname = upload.diskname() + makedirs(dirname(diskname), exist_ok=True) + with open(diskname, 'wb') as diskfile: + while True: + buf = file.read(16384) + if len(buf) == 0: + break + diskfile.write(buf) + return (filename, upload) + + @login_required @require_POST def upload(request): - user_account_names = [account.name for account in request.user.accounts.all()] + filename, upload = _perform_upload(request) - result = {} - # TODO this should really only be one file - # so make adjustments to find out its name and only provide one result + return JsonResponse({"filename": filename, "upload_id": upload.id}) - for filename, file in request.FILES.items(): - try: - started_at = datetime.strptime(filename, '%Y%m%d-%H%M%S.evtc') - except: - return _error('Filename not valid') - started_at = timezone.make_aware(started_at, timezone.utc) - - # metrics is a tree with 2 types of nodes: - # iterables containing key/value tuples - # or basic values - # should be easy to convert to json - evtc_encounter = EvtcEncounter(file) - - players = [agent for agent in evtc_encounter.agents if agent.account] - if not players: - return _error('No players in encounter') - - analyser = Analyser(evtc_encounter) - metrics = analyser.compute_all_metrics() - # TODO metrics - - - area = Area.objects.get(id=evtc_encounter.area_id) - if not area: - return _error('Unknown area') - - # heuristics to see if the encounter is a re-upload: - # a character can only be in one raid at a time - # XXX: it is *theoretically* possible for this to be in a race - # condition, so that the encounter is duplicated and later raises an - # error. try/catch, if returns multiple then delete all but one? - encounter, encounter_created = Encounter.objects.get_or_create( - area=area, started_at=started_at, characters__name=players[0].name) - - show = False - for player in players: - if player.account in user_account_names: - show = True - account, _ = Account.objects.get_or_create( - name=player.account) - character, _ = Character.objects.get_or_create( - name=player.name, account=account, profession=player.prof.value) - participation, _ = Participation.objects.get_or_create( - character=character, encounter=encounter) - if show: - result[filename] = { - 'id': encounter.id, - 'area': encounter.area.name, - 'started_at': int(started_at.strftime('%s')), - 'new': encounter_created, - } +@csrf_exempt +@require_POST +@sensitive_post_parameters('password') +def api_upload(request): + user = _perform_login(request) + if not user: + return _error('Could not authenticate', status=401) + auth_login(request, user) + filename, upload = _perform_upload(request) + if not upload: + return _error(filename, status=400) + + return JsonResponse({"filename": filename, "upload_id": upload.id}) + + +@login_required +@require_POST +def profile_graph(request): + era_id = request.POST['era'] + area_id = request.POST['area'] + archetype_id = request.POST['archetype'] + profession_id = request.POST['profession'] + elite_id = request.POST['elite'] + stat = request.POST['stat'] + + participations = Participation.objects.select_related('encounter').filter( + encounter__era_id=era_id, character__account__user=request.user, encounter__success=True) + + try: + if area_id.startswith('All'): + store = Era.objects.get(pk=era_id).val[area_id] + else: + participations = participations.filter(encounter__area_id=area_id) + store = EraAreaStore.objects.get(era_id=era_id, area_id=area_id).val + except (EraAreaStore.DoesNotExist, Era.DoesNotExist, KeyError): + store = {} + if archetype_id != 'All': + participations = participations.filter(archetype=archetype_id) + if profession_id != 'All': + participations = participations.filter(character__profession=profession_id) + if elite_id != 'All': + participations = participations.filter(elite=elite_id) + + try: + requested = store['All']['build'][profession_id][elite_id][archetype_id] + requested = { + 'avg': requested['avg_' + stat], + 'per': list(np.frombuffer(base64.b64decode(requested['per_' + stat].encode('utf-8')), dtype=np.float32).astype(float)), + } + except KeyError: + requested = None # XXX fill out in restat + MAX_GRAPH_ENCOUNTERS = 50 # XXX move to top or to settings + db_data = participations.order_by('-encounter__started_at')[:MAX_GRAPH_ENCOUNTERS].values_list('character__name', 'encounter__started_at', 'encounter__value') + data = [] + times = [] + + if stat == 'dps_boss': + target = '*Boss' + stat = 'dps' + else: + target = '*All' + for name, started_at, json in reversed(db_data): + dump = json_loads(json) + datum = _safe_get(lambda: dump['Category']['combat']['Phase']['All']['Player'][name]['Metrics']['damage']['To'][target][stat], 0) + data.append(datum) + times.append(started_at) + + result = { + 'globals': requested, + 'data': data, + 'times': times, + } + return JsonResponse(result) + + +@require_GET +def named(request, name, no): + return index(request, { 'name': name, 'no': int(no) if type(no) == str else no }) +@login_required +@require_POST +def poll(request): + notifications = Notification.objects.filter(user=request.user) + last_id = request.POST.get('last_id') + if last_id: + notifications = notifications.filter(id__gt=last_id) + result = { "notifications": [notification.val for notification in notifications] } + if notifications: + result['last_id'] = notifications.last().id return JsonResponse(result) + +@login_required +@require_POST +def privacy(request): + profile = request.user.user_profile + profile.privacy = int(request.POST.get('privacy')) + profile.save() + return JsonResponse({}) + +@login_required +@require_POST +def set_tags_cat(request): + encounter = Encounter.objects.get(pk=int(request.POST.get('id'))) + participation = encounter.participations.filter(character__account__user=request.user).exists() + if not participation: + return _error('Not a participant') + encounter.tagstring = request.POST.get('tags') + encounter.category_id = request.POST.get('category') + encounter.save() + return JsonResponse({}) + +@login_required +@require_POST +def change_email(request): + request.user.email = request.POST.get('email') + request.user.save() + return JsonResponse({}) + +@login_required +@sensitive_post_parameters() +@sensitive_variables('form') +@require_POST +def change_password(request): + form = PasswordChangeForm(request.user, request.POST) + if form.is_valid(): + user = form.save() + update_session_auth_hash(request, user) + return JsonResponse({}) + else: + return _error(' '.join(' '.join(v) for k, v in form.errors.items())) + +@require_POST +def contact(request): + subject = request.POST.get('subject') + body = request.POST.get('body') + if request.user.is_authenticated: + name = request.user.username + email = request.user.email + else: + name = request.POST.get('name') + email = request.POST.get('email') + + try: + headers = {'Reply-To': "%s <%s>" % (name, email)} + msg = EmailMessage( + settings.EMAIL_SUBJECT_PREFIX + '[contact] ' + subject, + body, + '"%s" <%s>' % (name, settings.DEFAULT_FROM_EMAIL), + [settings.DEFAULT_FROM_EMAIL], + reply_to=['%s <%s>' % (name, email)]) + msg.send(False) + except SMTPException as e: + return _error(e) + + return JsonResponse({}) + + +@login_required +@require_POST +def add_api_key(request): + api_key = request.POST.get('api_key').strip() + gw2api = GW2API(api_key) + + try: + token_info = gw2api.query("/tokeninfo") + if 'gw2raidar' not in token_info['name'].lower(): + return _error("Your api key must be named 'gw2raidar'.") + gw2_account = gw2api.query("/account") + except GW2APIException as e: + return _error(e) + + account_name = gw2_account['name'] + account, _ = Account.objects.get_or_create(name=account_name) + + if account.user and account.user != request.user: + # Registered to another account + old_gw2api = GW2API(account.api_key) + try: + gw2_account = old_gw2api.query("/account") + # Old key is still valid, ask user to invalidate it + try: + old_api_key_info = old_gw2api.query("/tokeninfo") + key_id = "named '%s'" % old_api_key_info['name'] + except GW2APIException as e: + key_id = "ending in '%s'" % api_key[-4:] + new_key = "" if account.api_key != api_key else " and generate a new key" + + return _error("This GW2 account is registered to another user. To prove it is yours, please invalidate the key %s%s." % (key_id, new_key)) + except GW2APIException as e: + # Old key is invalid, reassign OK + pass + + account.user = request.user + account.api_key = api_key + account.save() + + return JsonResponse({ + 'account_name': account_name, + 'encounters': _encounter_data(request) + }) diff --git a/raidar/words.txt b/raidar/words.txt new file mode 100644 index 00000000..d260e2a1 --- /dev/null +++ b/raidar/words.txt @@ -0,0 +1,10776 @@ +a +abandon +abandoned +abandoning +abandons +abbey +abbeys +abilities +ability +able +abolish +abolished +abolishes +abolishing +abolition +abortion +abortions +about +above +abroad +abruptly +absence +absences +absent +absolute +absolutely +absorb +absorbed +absorbing +absorbs +abstract +abuse +abused +abuses +abusing +ac +academic +academies +academy +accelerate +accelerated +accelerates +accelerating +accent +accents +accept +acceptable +acceptance +acceptances +accepted +accepting +accepts +access +accessed +accesses +accessible +accessing +accident +accidents +accommodate +accommodated +accommodates +accommodating +accommodation +accommodations +accompanied +accompanies +accompany +accompanying +accord +accorded +according +accordingly +accords +account +accountabilities +accountability +accountant +accountants +accounted +accounting +accounts +accumulate +accumulated +accumulates +accumulating +accuracy +accurate +accurately +accusation +accusations +accuse +accused +accuses +accusing +ace +aces +achieve +achieved +achievement +achievements +achieves +achieving +acid +acids +acknowledge +acknowledged +acknowledges +acknowledging +acquire +acquired +acquires +acquiring +acquisition +acquisitions +acre +acres +across +act +acted +acting +action +actions +activate +activated +activates +activating +active +actively +activist +activists +activities +activity +actor +actors +actress +actresses +acts +actual +actually +acute +ad +adapt +adaptation +adaptations +adapted +adapting +adapts +add +added +adding +addition +additional +additions +address +addressed +addresses +addressing +adds +adequate +adequately +adjacent +adjective +adjectives +adjust +adjusted +adjusting +adjustment +adjustments +adjusts +administer +administered +administering +administers +administration +administrations +administrative +administrator +administrators +admire +admired +admires +admiring +admission +admissions +admit +admits +admitted +admitting +adopt +adopted +adopting +adoption +adoptions +adopts +ads +adult +adults +advance +advanced +advances +advancing +advantage +advantages +adventure +adventures +adverse +advertise +advertised +advertisement +advertisements +advertises +advertising +advice +advise +advised +adviser +advisers +advises +advising +advisory +advocate +advocated +advocates +advocating +aesthetic +affair +affairs +affect +affected +affecting +affection +affections +affects +afford +afforded +affording +affords +afraid +after +afternoon +afternoons +afterwards +again +against +age +aged +ageing +agencies +agency +agenda +agendas +agent +agents +ages +aggregate +aggression +aggressions +aggressive +aging +ago +agonies +agony +agree +agreed +agreeing +agreement +agreements +agrees +agricultural +agriculture +ah +aha +ahead +aid +aided +aiding +aids +aim +aimed +aiming +aims +air +aircraft +airline +airlines +airport +airports +airs +alarm +alarms +albeit +album +albums +alcohol +alcohols +alert +alerted +alerting +alerts +alike +alive +all +allegation +allegations +allege +alleged +allegedly +alleges +alleging +alliance +alliances +allied +allies +allocate +allocated +allocates +allocating +allocation +allocations +allow +allowance +allowances +allowed +allowing +allows +ally +almost +alone +along +alongside +alpha +alphas +already +also +alter +alteration +alterations +altered +altering +alternative +alternatively +alternatives +alters +although +altogether +aluminium +always +am +amateur +amazing +ambassador +ambassadors +ambiguities +ambiguity +ambition +ambitions +ambitious +ambulance +ambulances +amend +amended +amending +amendment +amendments +amends +amid +amnesties +amnesty +among +amongst +amount +amounted +amounting +amounts +amusement +amusements +an +analogies +analogy +analyse +analysed +analyses +analysing +analysis +analyst +analysts +ancestor +ancestors +ancient +and +angel +angels +anger +angers +angle +angles +angrier +angrily +angry +animal +animals +ankle +ankles +anniversaries +anniversary +announce +announced +announcement +announcements +announces +announcing +annual +annually +anonymous +another +answer +answered +answering +answers +ant +antibodies +antibody +anticipate +anticipated +anticipates +anticipating +ants +anxieties +anxiety +anxious +any +anybody +anyone +anything +anyway +anywhere +apart +apartment +apartments +apologies +apologise +apologised +apologises +apologising +apology +appalling +apparatus +apparent +apparently +appeal +appealed +appealing +appeals +appear +appearance +appearances +appeared +appearing +appears +appendices +appendix +appetite +appetites +apple +apples +applicable +applicant +applicants +application +applications +applied +applies +apply +applying +appoint +appointed +appointing +appointment +appointments +appoints +appraisal +appraisals +appreciate +appreciated +appreciates +appreciating +appreciation +approach +approached +approaches +approaching +appropriate +approval +approvals +approve +approved +approves +approving +approximately +arbitrary +arch +archbishop +archbishops +arches +architect +architects +architectural +architecture +architectures +archive +archives +are +area +areas +argue +argued +argues +arguing +argument +arguments +arise +arisen +arises +arising +arm +armed +armies +arming +arms +army +arose +around +arouse +aroused +arouses +arousing +arrange +arranged +arrangement +arrangements +arranges +arranging +array +arrays +arrest +arrested +arresting +arrests +arrival +arrivals +arrive +arrived +arrives +arriving +arrow +arrows +art +article +articles +artificial +artist +artistic +artists +arts +as +ash +ashamed +ashes +aside +ask +asked +asking +asks +asleep +aspect +aspects +aspiration +aspirations +assault +assaults +assemble +assembled +assembles +assemblies +assembling +assembly +assert +asserted +asserting +assertion +assertions +asserts +assess +assessed +assesses +assessing +assessment +assessments +asset +assets +assign +assigned +assigning +assignment +assignments +assigns +assist +assistance +assistant +assistants +assisted +assisting +assists +associate +associated +associates +associating +association +associations +assume +assumed +assumes +assuming +assumption +assumptions +assurance +assurances +assure +assured +assures +assuring +asylum +asylums +at +ate +atmosphere +atmospheres +atom +atomic +atoms +attach +attached +attaches +attaching +attack +attacked +attacking +attacks +attain +attained +attaining +attains +attempt +attempted +attempting +attempts +attend +attendance +attendances +attended +attending +attends +attention +attentions +attitude +attitudes +attract +attracted +attracting +attraction +attractions +attractive +attracts +attribute +attributed +attributes +attributing +auction +auctions +audience +audiences +audit +auditor +auditors +audits +aunt +aunts +author +authorities +authority +authors +automatic +automatically +autonomous +autonomy +autumn +availability +available +avenue +avenues +average +averages +avoid +avoided +avoiding +avoids +await +awaited +awaiting +awaits +awake +award +awarded +awarding +awards +aware +awareness +away +awful +awkward +axes +axis +aye +babies +baby +back +backed +background +backgrounds +backing +backings +backs +backwards +bacteria +bacterium +bad +bade +badly +bag +bags +balance +balanced +balances +balancing +balconies +balcony +ball +ballet +ballets +balloon +balloons +ballot +ballots +balls +ban +band +bands +bang +banged +banging +bangs +bank +banker +bankers +banking +bankruptcies +bankruptcy +banks +banned +banning +bans +bar +bare +barely +barest +bargain +bargaining +bargains +barn +barns +baron +barons +barrel +barrels +barrier +barriers +bars +base +based +bases +basic +basically +basin +basing +basins +basis +basket +baskets +bass +basses +bastard +bastards +bat +bath +bathroom +bathrooms +baths +bats +batteries +battery +battle +battles +bay +bays +be +beach +beaches +beam +beams +bean +beans +bear +bearing +bearings +bears +beast +beasts +beat +beaten +beating +beats +beauties +beautiful +beautifully +beauty +became +become +becomes +becoming +bed +bedroom +bedrooms +beds +bee +beef +been +beer +beers +bees +before +beg +began +begged +begging +begin +beginning +beginnings +begins +begs +begun +behalf +behave +behaved +behaves +behaving +behaviour +behaviours +behind +being +beings +belief +beliefs +believe +believed +believes +believing +bell +bells +belong +belonged +belonging +belongs +below +belt +belts +bench +benches +bend +bending +bends +beneath +beneficial +benefit +benefited +benefiting +benefits +benefitted +benefitting +bent +beside +besides +best +bet +bets +better +betting +between +beyond +bias +bible +bibles +bicycle +bicycles +bid +bidden +bidding +bids +big +bigger +biggest +bike +bikes +bile +biles +bill +billion +billions +bills +bin +bind +binding +binds +bins +biographies +biography +biological +biology +bird +birds +birth +birthday +birthdays +births +biscuit +biscuits +bishop +bishops +bit +bite +bites +biting +bits +bitter +bitterly +bizarre +black +blacker +blackest +blacks +bladder +bladders +blade +blades +blame +blamed +blames +blaming +blank +blanket +blankets +blast +blasts +bless +blessed +blesses +blessing +blew +blind +blinder +block +blocked +blocking +blocks +bloke +blokes +blood +bloodiest +bloods +bloody +blow +blowed +blowing +blown +blows +blue +blues +board +boards +boast +boasted +boasting +boasts +boat +boats +bodies +body +boil +boiled +boiling +boils +bold +bolder +boldest +bolt +bolts +bomb +bomber +bombers +bombing +bombings +bombs +bond +bonds +bone +bones +bonus +bonuses +book +booked +booking +bookings +booklet +booklets +books +boom +booms +boost +boosted +boosting +boosts +boot +boots +border +borders +bore +bored +boring +born +borne +borough +boroughs +borrow +borrowed +borrowing +borrowings +borrows +boss +bosses +both +bother +bothered +bothering +bothers +bottle +bottles +bottom +bottoms +bought +bounce +bounced +bounces +bouncing +bound +boundaries +boundary +bounded +bounding +bounds +bow +bowed +bowel +bowels +bowing +bowl +bowler +bowlers +bowls +bows +box +boxes +boxing +boy +boyfriend +boyfriends +boys +bracket +brackets +brain +brains +brake +brakes +branch +branches +brand +brands +brass +brasses +brave +braver +bravest +breach +breaches +bread +breads +break +breakdown +breakdowns +breakfast +breakfasts +breaking +breaks +breast +breasts +breath +breathe +breathed +breathes +breathing +breaths +bred +breed +breeding +breeds +breeze +breezes +brethren +breweries +brewery +brick +bricks +bride +brides +bridge +bridges +brief +briefer +briefest +briefly +brigade +brigades +bright +brighter +brightest +brilliant +bring +bringing +brings +broad +broadcast +broadcasting +broadcasts +broader +broadest +broadly +brochure +brochures +broke +broken +broker +brokers +bronze +bronzes +brother +brothers +brought +brow +brown +browner +brows +brush +brushed +brushes +brushing +bucket +buckets +budget +budgets +build +builder +builders +building +buildings +builds +built +bulb +bulbs +bulk +bull +bullet +bullets +bulls +bunch +bunches +burden +burdens +bureau +bureaucracies +bureaucracy +bureaus +bureaux +burial +burials +buried +buries +burn +burned +burning +burns +burnt +burst +bursting +bursts +bury +burying +bus +buses +bush +bushes +busier +busiest +business +businesses +businessman +businessmen +busy +but +butter +butterflies +butterfly +butters +button +buttons +buy +buyer +buyers +buying +buys +by +bye +cab +cabin +cabinet +cabinets +cabins +cable +cables +cabs +cage +cages +cake +cakes +calcium +calculate +calculated +calculates +calculating +calculation +calculations +calendar +calendars +calf +call +called +calling +calls +calm +calmed +calmer +calming +calms +calves +came +camera +cameras +camp +campaign +campaigned +campaigning +campaigns +camps +can +canal +canals +cancel +cancelled +cancelling +cancels +cancer +cancers +candidate +candidates +candle +candles +cans +canvas +canvases +cap +capabilities +capability +capable +capacities +capacity +capital +capitalism +capitalist +capitals +caps +captain +captains +capture +captured +captures +capturing +car +caravan +caravans +carbon +carbons +card +cards +care +cared +career +careers +careful +carefully +carer +carers +cares +cargo +cargoes +caring +carpet +carpets +carriage +carriages +carried +carrier +carriers +carries +carry +carrying +cars +cart +carts +carve +carved +carves +carving +case +cases +cash +cassette +cassettes +cast +casting +castle +castles +casts +casual +casualties +casualty +cat +catalogue +catalogues +catch +catches +catching +categories +category +cater +catered +catering +caters +cathedral +cathedrals +catholic +catholics +cats +cattle +caught +causal +cause +caused +causes +causing +caution +cautions +cautious +cave +caves +cease +ceased +ceases +ceasing +ceiling +ceilings +celebrate +celebrated +celebrates +celebrating +celebration +celebrations +cell +cells +census +censuses +central +centre +centred +centres +centring +centuries +century +ceremonies +ceremony +certain +certainly +certainties +certainty +certificate +certificates +chain +chains +chair +chaired +chairing +chairman +chairmen +chairs +challenge +challenged +challenges +challenging +chamber +chambers +champagne +champagnes +champion +champions +championship +championships +chance +chancellor +chancellors +chances +change +changed +changes +changing +channel +channels +chaos +chap +chapel +chapels +chaps +chapter +chapters +character +characterise +characterised +characterises +characterising +characteristic +characteristics +characterize +characterized +characterizes +characterizing +characters +charge +charged +charges +charging +charities +charity +charm +charming +charms +chart +charter +charters +charts +chase +chased +chases +chasing +chat +chats +chatted +chatting +cheap +cheaper +cheapest +check +checked +checking +checks +cheek +cheeks +cheer +cheered +cheerful +cheering +cheese +cheeses +chemical +chemicals +chemist +chemistry +chemists +cheque +cheques +chest +chests +chew +chewed +chewing +chews +chicken +chickens +chief +chiefs +child +childhood +childhoods +children +chin +chins +chip +chips +chocolate +chocolates +choice +choices +choir +choirs +choose +chooses +choosing +chop +chopped +chopping +chops +chord +chords +chorus +choruses +chose +chosen +chronic +church +churches +cigarette +cigarettes +cinema +cinemas +circle +circles +circuit +circuits +circular +circulate +circulated +circulates +circulating +circulation +circulations +circumstance +circumstances +cite +cited +cites +cities +citing +citizen +citizens +city +civic +civil +civilian +claim +claimed +claiming +claims +clarified +clarifies +clarify +clarifying +clarity +clash +clashes +class +classes +classic +classical +classics +classification +classifications +classified +classifies +classify +classifying +classroom +classrooms +clause +clauses +clay +clays +clean +cleaned +cleaner +cleaners +cleanest +cleaning +cleans +clear +cleared +clearer +clearest +clearing +clearly +clears +clergy +clerk +clerks +clever +cleverer +cleverest +client +clients +cliff +cliffs +climate +climates +climb +climbed +climbing +climbs +cling +clinging +clings +clinic +clinical +clinics +clock +clocks +close +closed +closely +closer +closes +closest +closing +closure +closures +cloth +clothes +clothing +cloths +cloud +clouds +club +clubs +clue +clues +clung +cluster +clusters +clutch +clutched +clutches +clutching +cm +coach +coaches +coal +coalition +coalitions +coals +coast +coastal +coasts +coat +coats +code +codes +coffee +coffees +coffin +coffins +cognitive +coherent +coin +coincide +coincided +coincides +coinciding +coins +cold +colder +coldest +colds +colitis +collaboration +collaborations +collapse +collapsed +collapses +collapsing +collar +collars +colleague +colleagues +collect +collected +collecting +collection +collections +collective +collector +collectors +collects +college +colleges +colonel +colonels +colonial +colonies +colony +colour +coloured +colourful +colouring +colours +column +columns +combination +combinations +combine +combined +combines +combining +come +comedies +comedy +comes +cometh +comfort +comfortable +comforted +comforting +comforts +coming +command +commanded +commander +commanders +commanding +commands +commence +commenced +commences +commencing +comment +commentaries +commentary +commentator +commentators +commented +commenting +comments +commerce +commercial +commission +commissioned +commissioner +commissioners +commissioning +commissions +commit +commitment +commitments +commits +committed +committee +committees +committing +commodities +commodity +common +commonly +commonwealth +communicate +communicated +communicates +communicating +communication +communications +communist +communists +communities +community +compact +companies +companion +companions +company +comparable +comparative +comparatively +compare +compared +compares +comparing +comparison +comparisons +compatible +compel +compelled +compelling +compels +compensate +compensated +compensates +compensating +compensation +compensations +compete +competed +competence +competences +competent +competes +competing +competition +competitions +competitive +competitor +competitors +compile +compiled +compiles +compiling +complain +complained +complaining +complains +complaint +complaints +complete +completed +completely +completes +completing +completion +completions +complex +complexes +complexities +complexity +compliance +complicated +complication +complications +complied +complies +comply +complying +component +components +compose +composed +composer +composers +composes +composing +composition +compositions +compound +compounds +comprehensive +comprise +comprised +comprises +comprising +compromise +compromises +compulsory +computer +computers +computing +conceal +concealed +concealing +conceals +concede +conceded +concedes +conceding +conceive +conceived +conceives +conceiving +concentrate +concentrated +concentrates +concentrating +concentration +concentrations +concept +conception +conceptions +concepts +conceptual +concern +concerned +concerning +concerns +concert +concerts +concession +concessions +conclude +concluded +concludes +concluding +conclusion +conclusions +concrete +condemn +condemned +condemning +condemns +condition +conditions +conduct +conducted +conducting +conducts +confer +conference +conferences +conferred +conferring +confers +confess +confessed +confesses +confessing +confidence +confidences +confident +confidential +configuration +configurations +confine +confined +confines +confining +confirm +confirmation +confirmations +confirmed +confirming +confirms +conflict +conflicts +conform +conformed +conforming +conforms +confront +confrontation +confrontations +confronted +confronting +confronts +confuse +confused +confuses +confusing +confusion +confusions +congregation +congregations +congress +congresses +connect +connected +connecting +connection +connections +connects +conscience +consciences +conscious +consciousness +consciousnesses +consensus +consent +consents +consequence +consequences +consequently +conservation +conservative +conservatives +consider +considerable +considerably +consideration +considerations +considered +considering +considers +consist +consisted +consistent +consistently +consisting +consists +conspiracies +conspiracy +constable +constables +constant +constantly +constituencies +constituency +constituent +constituents +constitute +constituted +constitutes +constituting +constitution +constitutional +constitutions +constraint +constraints +construct +constructed +constructing +construction +constructions +constructs +consult +consultant +consultants +consultation +consultations +consulted +consulting +consults +consume +consumed +consumer +consumers +consumes +consuming +consumption +contact +contacted +contacting +contacts +contain +contained +container +containers +containing +contains +contemplate +contemplated +contemplates +contemplating +contemporaries +contemporary +contempt +contempts +content +contents +contest +contests +context +contexts +continent +continental +continents +continually +continue +continued +continues +continuing +continuities +continuity +continuous +contract +contracted +contracting +contractor +contractors +contracts +contradiction +contradictions +contrary +contrast +contrasted +contrasting +contrasts +contribute +contributed +contributes +contributing +contribution +contributions +control +controlled +controller +controllers +controlling +controls +controversial +controversies +controversy +convenient +convention +conventional +conventions +conversation +conversations +conversion +conversions +convert +converted +converting +converts +convey +conveyed +conveying +conveys +convict +convicted +convicting +conviction +convictions +convince +convinced +convinces +convincing +cook +cooked +cooking +cooks +cool +cooled +cooler +coolest +cooling +cools +cooperation +cope +coped +copes +copied +copies +coping +copper +coppers +copy +copying +copyright +copyrights +coral +corals +core +cores +corn +corner +corners +corns +corporate +corporation +corporations +corps +corpse +corpses +correct +corrected +correcting +correctly +corrects +correlation +correlations +correspond +corresponded +correspondence +correspondences +correspondent +correspondents +corresponding +corresponds +corridor +corridors +corruption +corruptions +cost +costed +costing +costlier +costliest +costly +costs +costume +costumes +cottage +cottages +cotton +cottons +could +council +councillor +councillors +councils +counselling +count +counted +counter +countered +countering +counterpart +counterparts +counters +counties +counting +countries +country +countryside +counts +county +coup +couple +coupled +couples +coupling +coups +courage +course +courses +court +courtesies +courtesy +courts +cousin +cousins +covenant +covenants +cover +coverage +coverages +covered +covering +covers +cow +cows +crack +cracked +cracking +cracks +craft +crafts +crash +crashed +crashes +crashing +crawl +crawled +crawling +crawls +crazier +craziest +crazy +cream +creams +create +created +creates +creating +creation +creations +creative +creature +creatures +credit +credited +crediting +creditor +creditors +credits +creep +creeping +creeps +crept +crew +crews +cricket +crickets +cried +cries +crime +crimes +criminal +criminals +crises +crisis +criteria +criterion +critic +critical +criticise +criticised +criticises +criticising +criticism +criticisms +criticize +criticized +criticizes +criticizing +critics +crop +crops +cross +crossed +crosses +crossing +crossings +crowd +crowds +crown +crowns +crucial +crude +cruder +crudest +cruel +crueller +cruellest +crush +crushed +crushes +crushing +cry +crying +crystal +crystals +cult +cults +cultural +culture +cultures +cup +cupboard +cupboards +cups +cure +cured +cures +curing +curiosities +curiosity +curious +curiouser +curl +curled +curling +curls +currencies +currency +current +currently +currents +curricula +curriculum +curriculums +curtain +curtains +curve +curves +custody +custom +customer +customers +customs +cut +cuts +cutting +cuttings +cycle +cycles +cylinder +cylinders +dad +daddies +daddy +dads +daily +dairies +dairy +damage +damaged +damages +damaging +damp +damper +dance +danced +dancer +dancers +dances +dancing +danger +dangerous +dangers +dare +dared +dares +daring +dark +darker +darkest +darkness +darling +darlings +data +database +databases +date +dated +dates +dating +datum +daughter +daughters +dawn +dawns +day +daylight +daylights +days +de +dead +deadline +deadlines +deaf +deal +dealer +dealers +dealing +dealings +deals +dealt +dear +dearer +dearest +dears +death +deaths +debate +debated +debates +debating +debt +debtor +debtors +debts +debut +debuts +decade +decades +decent +decide +decided +decides +deciding +decision +decisions +decisive +deck +decks +declaration +declarations +declare +declared +declares +declaring +decline +declined +declines +declining +decorate +decorated +decorates +decorating +decoration +decorations +decrease +decreased +decreases +decreasing +decree +decrees +dedicate +dedicated +dedicates +dedicating +deed +deeds +deem +deemed +deeming +deems +deep +deeper +deepest +deeply +default +defaults +defeat +defeated +defeating +defeats +defect +defects +defence +defences +defend +defendant +defendants +defended +defender +defenders +defending +defends +defensive +deficiencies +deficiency +deficit +deficits +define +defined +defines +defining +definite +definitely +definition +definitions +degree +degrees +delay +delayed +delaying +delays +delegate +delegates +delegation +delegations +deliberate +deliberately +delicate +delicious +delight +delighted +delightful +delights +deliver +delivered +deliveries +delivering +delivers +delivery +demand +demanded +demanding +demands +democracies +democracy +democrat +democratic +democrats +demolish +demolished +demolishes +demolishing +demonstrate +demonstrated +demonstrates +demonstrating +demonstration +demonstrations +denied +denies +densities +density +deny +denying +denys +depart +departed +departing +department +departments +departs +departure +departures +depend +depended +dependence +dependent +depending +depends +depict +depicted +depicting +depicts +deposit +deposited +depositing +deposits +depressed +depression +depressions +deprive +deprived +deprives +depriving +depth +depths +deputies +deputy +derive +derived +derives +deriving +descend +descended +descending +descends +descent +descents +describe +described +describes +describing +description +descriptions +desert +deserted +deserting +deserts +deserve +deserved +deserves +design +designate +designated +designates +designating +designed +designer +designers +designing +designs +desirable +desire +desired +desires +desiring +desk +desks +desktop +desktops +despair +desperate +desperately +despite +destination +destinations +destroy +destroyed +destroying +destroys +destruction +detail +detailed +details +detect +detected +detecting +detective +detectives +detects +determination +determinations +determine +determined +determines +determining +develop +developed +developer +developers +developing +development +developments +develops +device +devices +devil +devils +devise +devised +devises +devising +devote +devoted +devotes +devoting +diagnose +diagnosed +diagnoses +diagnosing +diagnosis +diagram +diagrams +dialogue +dialogues +diameter +diameters +diamond +diamonds +diaries +diary +dictate +dictated +dictates +dictating +dictionaries +dictionary +did +die +died +dies +diesel +diesels +diet +diets +differ +differed +difference +differences +different +differentiate +differentiated +differentiates +differentiating +differently +differing +differs +difficult +difficulties +difficulty +dig +digging +digital +dignities +dignity +digs +dilemma +dilemmas +dimension +dimensions +diminish +diminished +diminishes +diminishing +dining +dinner +dinners +dioxide +dip +diplomatic +dipped +dipping +dips +direct +directed +directing +direction +directions +directive +directives +directly +director +directories +directors +directory +directs +dirt +dirtier +dirtiest +dirty +disabilities +disability +disabled +disadvantage +disadvantages +disagree +disagreed +disagreeing +disagreement +disagreements +disagrees +disappear +disappeared +disappearing +disappears +disappointed +disappointment +disappointments +disaster +disasters +disastrous +disc +discharge +discharged +discharges +discharging +disciplinary +discipline +disciplines +disclose +disclosed +discloses +disclosing +disclosure +disclosures +discount +discounts +discourage +discouraged +discourages +discouraging +discourse +discourses +discover +discovered +discoveries +discovering +discovers +discovery +discretion +discretions +discrimination +discriminations +discs +discuss +discussed +discusses +discussing +discussion +discussions +disease +diseases +dish +dishes +disk +disks +dislike +disliked +dislikes +disliking +dismiss +dismissal +dismissals +dismissed +dismisses +dismissing +disorder +disorders +display +displayed +displaying +displays +disposal +disposals +dispose +disposed +disposes +disposing +dispute +disputes +dissolve +dissolved +dissolves +dissolving +distance +distances +distant +distinct +distinction +distinctions +distinctive +distinguish +distinguished +distinguishes +distinguishing +distress +distresses +distribute +distributed +distributes +distributing +distribution +distributions +district +districts +disturb +disturbance +disturbances +disturbed +disturbing +disturbs +dive +dived +diverse +diversities +diversity +divert +diverted +diverting +diverts +dives +divide +divided +dividend +dividends +divides +dividing +divine +diving +division +divisions +divorce +divorced +divorces +divorcing +do +dock +docks +doctor +doctors +doctrine +doctrines +document +documentation +documented +documenting +documents +does +dog +dogs +doing +doll +dollar +dollars +dolls +dolphin +dolphins +domain +domains +domestic +dominance +dominant +dominate +dominated +dominates +dominating +donate +donated +donates +donating +donation +donations +done +donor +donors +door +doors +doorway +doorways +dose +doses +dot +doth +dots +double +doubled +doubles +doubling +doubt +doubted +doubtful +doubting +doubts +down +downs +downstairs +dozen +dozens +draft +drafted +drafting +drafts +drag +dragged +dragging +dragon +dragons +drags +drain +drained +draining +drains +drama +dramas +dramatic +dramatically +drank +draw +drawer +drawers +drawing +drawings +drawn +draws +dreadful +dream +dreamed +dreaming +dreams +dreamt +dress +dressed +dresses +dressing +dressings +drew +dried +drier +dries +driest +drift +drifted +drifting +drifts +drill +drilled +drilling +drills +drink +drinking +drinks +drive +driven +driver +drivers +drives +driving +drop +dropped +dropping +drops +drove +drown +drowned +drowning +drowns +drug +drugs +drum +drums +drunk +drunker +dry +drying +du +dual +duck +ducks +due +dug +duke +dukes +dull +duller +dullest +dump +dumped +dumping +dumps +duration +durations +during +dust +dusts +duties +duty +dwelling +dwellings +dying +dynamic +each +eager +eagle +eagles +ear +earl +earlier +earliest +earls +early +earn +earned +earning +earnings +earns +ears +earth +earths +ease +eased +eases +easier +easiest +easily +easing +east +eastern +easy +eat +eaten +eating +eats +echo +echoed +echoes +echoing +echos +economic +economically +economics +economies +economist +economists +economy +ed +edge +edges +edit +edited +editing +edition +editions +editor +editors +edits +eds +educate +educated +educates +educating +education +educational +educations +effect +effected +effecting +effective +effectively +effectiveness +effects +efficiencies +efficiency +efficient +efficiently +effort +efforts +eg +egg +eggs +ego +egos +eh +eight +eighteen +eighteenth +eighth +eighties +eights +eighty +either +elaborate +elbow +elbows +elder +elderly +elders +eldest +elect +elected +electing +election +elections +electoral +electorate +electorates +electric +electrical +electricity +electron +electronic +electronics +electrons +elects +elegant +element +elements +elephant +elephants +eleven +elevens +eligible +eliminate +eliminated +eliminates +eliminating +elite +elites +else +elsewhere +embark +embarked +embarking +embarks +embarrassed +embarrassing +embarrassment +embarrassments +embassies +embassy +embodied +embodies +embody +embodying +embrace +embraced +embraces +embracing +emerge +emerged +emergence +emergencies +emergency +emerges +emerging +emission +emissions +emotion +emotional +emotions +emperor +emperors +emphasis +emphasise +emphasised +emphasises +emphasising +emphasize +emphasized +emphasizes +emphasizing +empire +empires +empirical +employ +employed +employee +employees +employer +employers +employing +employment +employments +employs +emptied +emptier +empties +empty +emptying +enable +enabled +enables +enabling +enclose +enclosed +encloses +enclosing +encounter +encountered +encountering +encounters +encourage +encouraged +encouragement +encouragements +encourages +encouraging +end +ended +ending +endings +endless +endorse +endorsed +endorses +endorsing +ends +endure +endured +endures +enduring +enemies +enemy +energies +energy +enforce +enforced +enforcement +enforces +enforcing +engage +engaged +engagement +engagements +engages +engaging +engine +engineer +engineering +engineers +engines +enhance +enhanced +enhances +enhancing +enjoy +enjoyed +enjoying +enjoyment +enjoyments +enjoys +enormous +enough +enquire +enquired +enquires +enquiries +enquiring +enquiry +ensure +ensured +ensures +ensuring +entail +entailed +entailing +entails +enter +entered +entering +enterprise +enterprises +enters +entertain +entertained +entertaining +entertainment +entertainments +entertains +enthusiasm +enthusiasms +enthusiast +enthusiastic +enthusiasts +entire +entirely +entities +entitle +entitled +entitles +entitling +entity +entrance +entrances +entries +entry +envelope +envelopes +environment +environmental +environments +envisage +envisaged +envisages +envisaging +enzyme +enzymes +episode +episodes +equal +equalities +equality +equalled +equalling +equally +equals +equation +equations +equilibrium +equip +equipment +equipments +equipped +equipping +equips +equities +equity +equivalent +equivalents +er +era +eras +erect +erected +erecting +erects +erm +erosion +erosions +error +errors +escape +escaped +escapes +escaping +especially +essay +essays +essence +essences +essential +essentially +establish +established +establishes +establishing +establishment +establishments +estate +estates +estimate +estimated +estimates +estimating +etc +ethical +ethnic +evaluate +evaluated +evaluates +evaluating +evaluation +evaluations +even +evening +evenings +event +events +eventual +eventually +ever +every +everybody +everyday +everyone +everything +everywhere +evidence +evidences +evident +evidently +evil +evils +evoke +evoked +evokes +evoking +evolution +evolutionary +evolutions +evolve +evolved +evolves +evolving +exact +exactly +exam +examination +examinations +examine +examined +examines +examining +example +examples +exams +exceed +exceeded +exceeding +exceeds +excellent +except +exception +exceptional +exceptions +excess +excesses +excessive +exchange +exchanged +exchanges +exchanging +excited +excitement +excitements +exciting +exclaim +exclaimed +exclaiming +exclaims +exclude +excluded +excludes +excluding +exclusion +exclusions +exclusive +exclusively +excuse +excused +excuses +excusing +execute +executed +executes +executing +execution +executions +executive +executives +exemption +exemptions +exercise +exercised +exercises +exercising +exert +exerted +exerting +exerts +exhaust +exhausted +exhausting +exhausts +exhibit +exhibited +exhibiting +exhibition +exhibitions +exhibits +exile +exiles +exist +existed +existence +existences +existing +exists +exit +exits +exotic +expand +expanded +expanding +expands +expansion +expansions +expect +expectation +expectations +expected +expecting +expects +expedition +expeditions +expenditure +expenditures +expense +expenses +expensive +experience +experienced +experiences +experiencing +experiment +experimental +experiments +expert +expertise +experts +explain +explained +explaining +explains +explanation +explanations +explicit +explicitly +explode +exploded +explodes +exploding +exploit +exploitation +exploited +exploiting +exploration +explorations +explore +explored +explores +exploring +explosion +explosions +export +exported +exporting +exports +expose +exposed +exposes +exposing +exposure +exposures +express +expressed +expresses +expressing +expression +expressions +extend +extended +extending +extends +extension +extensions +extensive +extent +extents +external +extra +extract +extracted +extracting +extracts +extraordinary +extreme +extremely +extremes +eye +eyebrow +eyebrows +eyes +fabric +fabrics +face +faced +faces +facilitate +facilitated +facilitates +facilitating +facilities +facility +facing +fact +faction +factions +factor +factories +factors +factory +facts +faculties +faculty +fade +faded +fades +fading +fail +failed +failing +fails +failure +failures +faint +fainter +faintest +fair +fairer +fairest +fairly +faith +faithful +faiths +fall +fallen +falling +falls +fame +familiar +families +family +famous +fan +fancied +fancies +fancy +fancying +fans +fantasies +fantastic +fantasy +far +fare +fares +farm +farmer +farmers +farming +farms +farther +farthest +fascinating +fashion +fashionable +fashions +fast +faster +fastest +fat +fatal +fate +fates +father +fathers +fats +fatter +fattest +fault +faults +favour +favourable +favoured +favouring +favourite +favourites +favours +fax +faxes +fear +feared +fearing +fears +feather +feathers +feature +featured +features +featuring +fed +federal +federation +federations +fee +feed +feedback +feedbacks +feeding +feeds +feel +feeling +feelings +feels +fees +feet +fell +fellow +fellows +felt +female +females +feminist +feminists +fence +fences +ferries +ferry +fertility +festival +festivals +fetch +fetched +fetches +fetching +fever +fevers +few +fewer +fibre +fibres +fiction +fictions +field +fields +fierce +fiercer +fiercest +fifteen +fifth +fifties +fifty +fig +fight +fighter +fighters +fighting +fights +figs +figure +figured +figures +figuring +file +filed +files +filing +fill +filled +filling +fills +film +films +filter +filters +final +finally +finals +finance +financed +finances +financial +financing +find +finding +findings +finds +fine +fined +finer +fines +finest +finger +fingers +fining +finish +finished +finishes +finishing +fire +fired +fires +firing +firm +firmer +firmest +firmly +firms +first +firstly +fiscal +fish +fished +fisherman +fishermen +fishes +fishing +fist +fists +fit +fitness +fits +fitted +fitter +fittest +fitting +fittings +five +fives +fix +fixed +fixes +fixing +flag +flags +flame +flames +flash +flashed +flashes +flashing +flat +flats +flatter +flavour +flavours +fled +flee +fleeing +flees +fleet +fleets +flesh +flew +flexibility +flexible +flick +flicked +flicking +flicks +flies +flight +flights +fling +flinging +flings +float +floated +floating +floats +flock +flocks +flood +flooded +flooding +floods +floor +floors +flour +flourish +flourished +flourishes +flourishing +flours +flow +flowed +flower +flowers +flowing +flown +flows +fluid +fluids +flung +flush +flushed +flushes +flushing +fly +flying +foci +focus +focused +focuses +focusing +focussed +focusses +focussing +fog +fogs +fold +folded +folding +folds +folk +folks +follies +follow +followed +follower +followers +following +follows +folly +fond +fonder +fondest +food +foods +fool +foolish +fools +foot +football +footballs +for +fora +forbade +forbid +forbidden +forbidding +forbids +force +forced +forces +forcing +forecast +forecasts +forehead +foreheads +foreign +foreigner +foreigners +forest +forests +forever +forgave +forget +forgets +forgetting +forgive +forgiven +forgives +forgiving +forgot +forgotten +fork +forks +form +formal +formally +format +formation +formations +formats +formed +former +formerly +formidable +forming +forms +formula +formulae +formulas +formulate +formulated +formulates +formulating +formulation +formulations +forth +forthcoming +forties +fortnight +fortunate +fortunately +fortune +fortunes +forty +forum +forums +forward +forwards +fossil +fossils +foster +fostered +fostering +fosters +fought +found +foundation +foundations +founded +founder +founders +founding +founds +four +fours +fourteen +fourteens +fourth +fox +foxes +fraction +fractions +fragment +fragments +frame +framed +frames +framework +frameworks +framing +fraud +frauds +free +freed +freedom +freedoms +freeing +freely +freer +frees +freest +freeze +freezes +freezing +frequencies +frequency +frequent +frequently +fresh +fresher +freshest +fridge +fridges +friend +friendlier +friendliest +friendly +friends +friendship +friendships +frighten +frightened +frightening +frightens +fringe +fringes +from +front +frontier +frontiers +fronts +frown +frowned +frowning +frowns +froze +frozen +fruit +fruits +frustration +frustrations +ft +fuel +fuels +fulfil +fulfilled +fulfilling +fulfils +full +fuller +fullest +fully +fun +function +functional +functioned +functioning +functions +fund +fundamental +funded +funding +funds +funeral +funerals +funnier +funniest +funny +fur +furies +furious +furnish +furnished +furnishes +furnishing +furniture +furs +further +furthermore +furthest +fury +fusion +fusions +future +futures +gain +gained +gaining +gains +gall +galleries +gallery +galls +game +games +gang +gangs +gap +gaps +garage +garages +garden +gardener +gardeners +gardens +garment +garments +gas +gases +gasp +gasped +gasping +gasps +gasses +gastric +gate +gates +gather +gathered +gathering +gatherings +gathers +gave +gay +gaze +gazed +gazes +gazing +gear +gears +gender +genders +gene +general +generally +generals +generate +generated +generates +generating +generation +generations +generous +genes +genetic +genius +geniuses +gentle +gentleman +gentlemen +gently +genuine +genuinely +geographical +geographies +geography +gesture +gestures +get +gets +getting +ghost +ghosts +giant +giants +gift +gifts +girl +girlfriend +girlfriends +girls +give +given +gives +giveth +giving +glad +glance +glanced +glances +glancing +glare +glared +glares +glaring +glass +glasses +glimpse +glimpses +global +glories +glorious +glory +glove +gloves +go +goal +goals +goat +goats +god +gods +goes +going +gold +golden +golds +golf +golfs +gone +good +goodbye +goodness +goods +gospel +gospels +got +gothic +gotten +govern +governed +governing +government +governments +governor +governors +governs +gown +gowns +gp +gps +grab +grabbed +grabbing +grabs +grace +graces +grade +grades +gradual +gradually +graduate +graduates +grain +grains +grammar +grammars +grand +grander +grandest +grandfather +grandfathers +grandmother +grandmothers +grant +granted +granting +grants +graph +graphics +graphs +grasp +grasped +grasping +grasps +grass +grasses +grateful +grave +gravel +gravels +graves +gravities +gravity +great +greater +greatest +greatly +green +greener +greenest +greenhouse +greenhouses +greens +greet +greeted +greeting +greets +grew +grey +greyer +grid +grids +grief +griefs +grim +grimmer +grimmest +grin +grinned +grinning +grins +grip +gripped +gripping +grips +gross +grosser +ground +grounds +group +grouping +groupings +groups +grow +growing +grown +grows +growth +growths +guarantee +guaranteed +guaranteeing +guarantees +guard +guarded +guardian +guardians +guarding +guards +guerrilla +guerrillas +guess +guessed +guesses +guessing +guest +guests +guidance +guide +guided +guideline +guidelines +guides +guild +guilds +guilt +guilts +guilty +guitar +guitars +gun +guns +guy +guys +ha +habit +habitat +habitats +habits +had +hair +hairs +half +halfs +hall +halls +halt +halted +halting +halts +halves +hammer +hammers +hand +handed +handful +handfuls +handicap +handicapped +handicaps +handing +handle +handled +handles +handling +hands +handsome +handsomest +hang +hanged +hanging +hangs +happen +happened +happening +happens +happier +happiest +happily +happiness +happy +harbour +harbours +hard +harder +hardest +hardly +hardware +harm +harmed +harming +harmonies +harmony +harms +harsh +harsher +harshest +harvest +harvests +has +hat +hate +hated +hates +hating +hatred +hatreds +hats +haul +hauled +hauling +hauls +have +having +hazard +hazards +he +head +headache +headaches +headed +heading +headings +headline +headlines +headmaster +headmasters +headquarters +heads +health +healthier +healthiest +healthy +heap +heaps +hear +heard +hearing +hearings +hears +heart +hearts +heat +heated +heating +heats +heaven +heavens +heavier +heaviest +heavily +heavy +hedge +hedges +heel +heels +height +heights +heir +heirs +held +helicopter +helicopters +hell +hello +hells +help +helped +helpful +helping +helps +hemisphere +hemispheres +hence +her +herb +herbs +herd +herds +here +heritage +hero +heroes +heros +hers +herself +hesitate +hesitated +hesitates +hesitating +hey +hid +hidden +hide +hides +hiding +hierarchies +hierarchy +high +higher +highest +highlight +highlighted +highlighting +highlights +highly +highway +highways +hill +hills +him +himself +hint +hints +hip +hips +hire +hired +hires +hiring +his +historian +historians +historic +historical +histories +history +hit +hitherto +hits +hitting +hold +holder +holders +holding +holdings +holds +hole +holes +holiday +holidays +holier +holiest +hollies +holly +holy +home +homeless +homes +hon +honest +honestly +honey +honour +honoured +honouring +honours +hook +hooks +hope +hoped +hopefully +hopes +hoping +horizon +horizons +horizontal +horn +horns +horrible +horror +horrors +horse +horses +hospital +hospitals +host +hostage +hostages +hosted +hostile +hostilities +hostility +hosting +hosts +hot +hotel +hotels +hotter +hottest +hour +hours +house +housed +household +households +houses +housewife +housewives +housing +housings +how +however +huge +human +humanity +humans +humour +humours +hundred +hundreds +hung +hunger +hungrier +hungry +hunt +hunted +hunting +hunts +hurried +hurries +hurry +hurrying +hurt +hurting +hurts +husband +husbands +hut +huts +hydrogen +hypotheses +hypothesis +ice +ices +idea +ideal +ideally +ideals +ideas +identical +identification +identifications +identified +identifies +identify +identifying +identities +identity +ideological +ideologies +ideology +ie +if +ignorance +ignore +ignored +ignores +ignoring +ill +illegal +illness +illnesses +illusion +illusions +illustrate +illustrated +illustrates +illustrating +illustration +illustrations +image +images +imagination +imaginations +imaginative +imagine +imagined +imagines +imagining +immediate +immediately +immense +immigration +impact +impacts +imperial +implement +implementation +implementations +implemented +implementing +implements +implication +implications +implicit +implied +implies +imply +implying +import +importance +important +importantly +imported +importing +imports +impose +imposed +imposes +imposing +impossible +impress +impressed +impresses +impressing +impression +impressions +impressive +imprisonment +improve +improved +improvement +improvements +improves +improving +impulse +impulses +in +inability +inadequate +inappropriate +incentive +incentives +inch +inches +incidence +incidences +incident +incidentally +incidents +inclined +include +included +includes +including +inclusion +inclusions +income +incomes +incorporate +incorporated +incorporates +incorporating +increase +increased +increases +increasing +increasingly +incredible +incur +incurred +incurring +incurs +indeed +independence +independent +independently +index +indexes +indicate +indicated +indicates +indicating +indication +indications +indicator +indicators +indices +indirect +indirectly +individual +individually +individuals +induce +induced +induces +inducing +indulge +indulged +indulges +indulging +industrial +industries +industry +inequalities +inequality +inevitable +inevitably +infant +infants +infection +infections +inflation +inflict +inflicted +inflicting +inflicts +influence +influenced +influences +influencing +influential +inform +informal +information +informations +informed +informing +informs +infrastructure +infrastructures +ingredient +ingredients +inhabitant +inhabitants +inherent +inherit +inheritance +inheritances +inherited +inheriting +inherits +inhibit +inhibited +inhibiting +inhibits +initial +initially +initiate +initiated +initiates +initiating +initiative +initiatives +inject +injected +injecting +injection +injections +injects +injunction +injunctions +injure +injured +injures +injuries +injuring +injury +inland +inn +inner +innocent +innovation +innovations +innovative +inns +input +inputs +inquiries +inquiry +insect +insects +insert +inserted +inserting +inserts +inside +insider +insiders +insides +insight +insights +insist +insisted +insisting +insists +inspect +inspected +inspecting +inspection +inspections +inspector +inspectors +inspects +inspiration +inspirations +inspire +inspired +inspires +inspiring +install +installation +installations +installed +installing +installs +instance +instances +instant +instantly +instead +instinct +instincts +institute +institutes +institution +institutional +institutions +instruct +instructed +instructing +instruction +instructions +instructs +instrument +instruments +insufficient +insurance +insurances +insure +insured +insures +insuring +intact +intake +intakes +integral +integrate +integrated +integrates +integrating +integration +integrity +intellectual +intellectuals +intelligence +intelligences +intelligent +intend +intended +intending +intends +intense +intensities +intensity +intensive +intent +intention +intentions +intents +interaction +interactions +interest +interested +interesting +interests +interface +interfaces +interfere +interfered +interference +interferences +interferes +interfering +interim +interior +interiors +intermediate +internal +international +interpret +interpretation +interpretations +interpreted +interpreting +interprets +interrupt +interrupted +interrupting +interrupts +interval +intervals +intervene +intervened +intervenes +intervening +intervention +interventions +interview +interviewed +interviewing +interviews +intimate +into +introduce +introduced +introduces +introducing +introduction +introductions +invade +invaded +invades +invading +invariably +invasion +invasions +invent +invented +inventing +invention +inventions +invents +invest +invested +investigate +investigated +investigates +investigating +investigation +investigations +investigator +investigators +investing +investment +investments +investor +investors +invests +invisible +invitation +invitations +invite +invited +invites +inviting +invoke +invoked +invokes +invoking +involve +involved +involvement +involvements +involves +involving +iron +ironies +irons +irony +irrelevant +is +isle +isles +isolate +isolated +isolates +isolating +isolation +issue +issued +issues +issuing +it +item +items +its +itself +jacket +jackets +jail +jails +jam +jams +jar +jars +jaw +jaws +jazz +jean +jeans +jet +jets +jewellery +job +jobs +join +joined +joining +joins +joint +jointly +joints +joke +joked +jokes +joking +journal +journalist +journalists +journals +journey +journeys +joy +joys +judge +judged +judgement +judgements +judges +judging +judgment +judgments +judicial +juice +juices +jump +jumped +jumping +jumps +junction +junctions +jungle +jungles +junior +juries +jurisdiction +jurisdictions +jury +just +justice +justices +justification +justifications +justified +justifies +justify +justifying +keen +keener +keenest +keep +keeper +keepers +keeping +keeps +kept +key +keyboard +keyboards +keys +kick +kicked +kicking +kicks +kid +kids +kill +killed +killer +killers +killing +killings +kills +kilometre +kilometres +kind +kinder +kindest +kinds +king +kingdom +kingdoms +kings +kiss +kissed +kisses +kissing +kit +kitchen +kitchens +kits +km +kms +knee +kneel +kneeled +kneeling +kneels +knees +knelt +knew +knife +knight +knights +knit +knits +knitted +knitting +knives +knock +knocked +knocking +knocks +knot +knots +know +knowing +knowledge +knowledges +known +knows +korean +la +lab +label +labeled +labelled +labelling +labels +laboratories +laboratory +labour +labours +labs +lace +laces +lack +lacked +lacking +lacks +lad +ladder +ladders +laden +ladies +lads +lady +laid +lake +lakes +lamb +lambs +lamp +lamps +land +landed +landing +landings +landlord +landlords +landowner +landowners +lands +landscape +landscapes +lane +lanes +language +languages +lap +laps +large +largely +larger +largest +laser +lasers +last +lasted +lasting +lasts +late +later +latest +latter +laugh +laughed +laughing +laughs +laughter +launch +launched +launches +launching +law +lawn +lawns +laws +lawyer +lawyers +lay +layer +layers +laying +layout +layouts +lays +lb +lbs +le +lead +leader +leaders +leadership +leaderships +leading +leads +leaf +leaflet +leaflets +league +leagues +lean +leaned +leaning +leans +leant +leap +leaped +leaping +leaps +leapt +learn +learned +learner +learners +learning +learns +learnt +lease +leases +least +leather +leathers +leave +leaves +leaving +lecture +lecturer +lecturers +lectures +led +left +lefts +leg +legacies +legacy +legal +legally +legend +legends +legislation +legislative +legitimate +legs +leisure +lemon +lemons +lend +lending +lends +length +lengthier +lengths +lengthy +lent +less +lesser +lesson +lessons +let +lets +letter +letters +letting +level +levelled +levelling +levels +lexical +liabilities +liability +liable +liaison +liaisons +liberal +liberals +liberation +liberties +liberty +librarian +librarians +libraries +library +licence +licences +lid +lids +lie +lies +lieutenant +lieutenants +life +lifes +lifespan +lifespans +lifestyle +lifestyles +lifetime +lifetimes +lift +lifted +lifting +lifts +light +lighted +lighter +lightest +lighting +lightly +lights +like +liked +likelier +likeliest +likelihood +likely +likes +likewise +liking +limb +limbs +limit +limitation +limitations +limited +limiting +limits +line +linear +lined +lines +linguistic +lining +link +linked +linking +links +lion +lions +lip +lips +liquid +liquids +list +listed +listen +listened +listener +listeners +listening +listens +listing +listings +lists +lit +literally +literary +literature +literatures +little +live +lived +livelier +liveliest +lively +liver +livers +lives +living +livings +load +loaded +loading +loads +loan +loans +lobbies +lobby +local +localities +locality +locally +locals +locate +located +locates +locating +location +locations +loch +lochs +lock +locked +locking +locks +locomotive +locomotives +lodge +lodged +lodges +lodging +log +logic +logical +logics +logs +lonelier +loneliest +lonely +long +longed +longer +longest +longing +longs +look +looked +looking +looks +loop +loops +loose +lord +lords +lordship +lordships +lorries +lorry +lose +loses +losing +loss +losses +lost +lot +lots +loud +louder +loudest +loudly +lounge +lounges +love +loved +lovelier +loveliest +lovely +lover +lovers +loves +loving +low +lower +lowered +lowering +lowers +lowest +loyal +loyalties +loyalty +ltd +luck +luckier +luckiest +lucky +lump +lumps +lunch +lunches +lung +lungs +luxuries +luxury +m +machine +machinery +machines +mad +madame +madder +made +magazine +magazines +magic +magistrate +magistrates +magnetic +magnificent +magnitude +magnitudes +maid +maids +mail +mails +main +mainframe +mainframes +mainland +mainly +maintain +maintained +maintaining +maintains +maintenance +majesties +majesty +major +majorities +majority +majors +make +maker +makers +makes +maketh +making +makings +male +males +mammal +mammals +man +manage +managed +management +managements +manager +managerial +managers +manages +managing +manipulate +manipulated +manipulates +manipulating +manner +manners +manor +manors +mans +manual +manuals +manufacture +manufactured +manufacturer +manufacturers +manufactures +manufacturing +manuscript +manuscripts +many +map +maps +marble +marbles +march +marched +marches +marching +margin +marginal +margins +marine +mark +marked +marker +markers +market +marketed +marketing +markets +marking +marks +marriage +marriages +married +marries +marry +marrying +marvellous +mask +masks +mass +masses +massive +master +masters +match +matched +matches +matching +mate +material +materials +mates +mathematical +mathematics +matrices +matrix +matter +mattered +matters +mature +maturer +maturities +maturity +maxima +maximum +may +maybe +mayor +mayors +me +meal +meals +mean +meaner +meanest +meaning +meaningful +meanings +means +meant +meantime +meanwhile +measure +measured +measurement +measurements +measures +measuring +meat +meats +mechanic +mechanical +mechanics +mechanism +mechanisms +medal +medals +media +medical +medicine +medicines +medieval +medium +mediums +meet +meeting +meetings +meets +melt +melted +melting +melts +member +members +membership +memberships +membrane +membranes +memoranda +memorandum +memorial +memorials +memories +memory +men +mental +mentally +mention +mentioned +mentioning +mentions +menu +menus +merchant +merchants +mercies +mercy +mere +merely +merest +merge +merged +merger +mergers +merges +merging +merit +merits +mess +message +messages +messes +met +metal +metals +metaphor +metaphors +method +methodologies +methodology +methods +metre +metres +metropolitan +mhm +mice +mid +middle +middles +midnight +might +mightier +mightiest +mighty +migration +migrations +mild +milder +mildest +mile +miles +military +milk +milks +mill +million +millions +mills +min +mind +minded +minding +minds +mine +miner +mineral +minerals +miners +mines +minima +minimal +minimum +mining +minister +ministerial +ministers +ministries +ministry +minor +minorities +minority +mins +minus +minute +minutes +miracle +miracles +mirror +mirrors +miserable +miseries +misery +misleading +miss +missed +misses +missile +missiles +missing +mission +missions +mist +mistake +mistaken +mistakes +mistaking +mistook +mistress +mistresses +mists +mix +mixed +mixes +mixing +mixture +mixtures +ml +mls +mm +mobile +mobility +mode +model +modelled +modelling +models +moderate +modern +modes +modest +modification +modifications +modified +modifies +modify +modifying +module +modules +molecular +molecule +molecules +moment +moments +monarch +monarchies +monarchs +monarchy +monetary +money +moneys +monitor +monitored +monitoring +monitors +monk +monkey +monkeys +monks +monopolies +monopoly +monster +monsters +month +monthly +months +monument +monuments +mood +moods +moon +moons +moor +moors +moral +morale +morales +moralities +morality +more +moreover +morning +mornings +mortalities +mortality +mortgage +mortgages +mosaic +mosaics +most +mostly +mother +mothers +motif +motifs +motion +motions +motivate +motivated +motivates +motivating +motivation +motivations +motive +motives +motor +motors +motorway +motorways +mould +moulds +mount +mountain +mountains +mounted +mounting +mounts +mouse +mouses +mouth +mouths +move +moved +movement +movements +moves +movie +movies +moving +much +mucosa +mucosae +mud +muds +mug +mugs +multiple +multiplied +multiplies +multiply +multiplying +mum +mummies +mummy +mums +murder +murdered +murderer +murderers +murdering +murders +murmur +murmured +murmuring +murmurs +muscle +muscles +museum +museums +music +musical +musician +musicians +musics +must +mutter +muttered +muttering +mutters +mutual +my +myself +mysteries +mysterious +mystery +myth +myths +nail +nails +naked +name +named +namely +names +naming +narrative +narratives +narrow +narrowed +narrower +narrowest +narrowing +narrows +nastier +nastiest +nasty +nation +national +nationalism +nationalisms +nationalist +nationalists +nationalities +nationality +nations +native +natural +naturally +nature +natures +naval +navies +navy +near +nearby +nearer +nearest +nearly +neat +neater +neatest +neatly +necessarily +necessary +necessities +necessity +neck +necks +need +needed +needing +needle +needles +needs +negative +neglect +neglected +neglecting +neglects +negligence +negotiate +negotiated +negotiates +negotiating +negotiation +negotiations +neighbour +neighbourhood +neighbourhoods +neighbouring +neighbours +neither +nerve +nerves +nervous +nest +nests +net +nets +network +networks +neutral +never +nevertheless +new +newcomer +newcomers +newer +newest +newly +news +newspaper +newspapers +next +nice +nicer +nicest +night +nightmare +nightmares +nights +nine +nines +nineteen +nineteens +nineteenth +nineties +ninety +no +noble +nobody +nod +nodded +nodding +node +nodes +nods +noise +noises +noisier +noisiest +noisy +nominate +nominated +nominates +nominating +none +nonetheless +nonsense +nor +norm +normal +normally +norms +north +northern +nos +nose +noses +not +notable +notably +note +notebook +notebooks +noted +notes +nothing +notice +noticed +notices +noticing +noting +notion +notions +novel +novels +now +nowadays +nowhere +nuclear +nuisance +nuisances +number +numbers +numerous +nurse +nurseries +nursery +nurses +nursing +nut +nuts +oak +oaks +obey +obeyed +obeying +obeys +object +objected +objecting +objection +objections +objective +objectives +objects +obligation +obligations +obliged +obscure +obscured +obscures +obscuring +observation +observations +observe +observed +observer +observers +observes +observing +obstacle +obstacles +obtain +obtained +obtaining +obtains +obvious +obviously +occasion +occasional +occasionally +occasions +occupation +occupational +occupations +occupied +occupies +occupy +occupying +occur +occurred +occurrence +occurrences +occurring +occurs +ocean +oceans +odd +odds +off +offence +offences +offender +offenders +offer +offered +offering +offerings +offers +office +officer +officers +offices +official +officially +officials +offset +offsets +offsetting +often +oh +oil +oils +old +older +oldest +omit +omits +omitted +omitting +on +once +one +onion +onions +only +onto +onwards +ooh +open +opened +opening +openings +openly +opens +opera +operas +operate +operated +operates +operating +operation +operational +operations +operator +operators +opinion +opinions +opponent +opponents +opportunities +opportunity +oppose +opposed +opposes +opposing +opposite +opposites +opposition +oppositions +opt +opted +optimistic +opting +option +options +opts +or +oral +orange +oranges +orchestra +orchestras +order +ordered +ordering +orders +ordinary +organ +organic +organisation +organisational +organisations +organise +organised +organiser +organisers +organises +organising +organism +organisms +organization +organizations +organize +organized +organizes +organizing +organs +orientation +orientations +origin +original +originally +originate +originated +originates +originating +origins +orthodox +other +others +otherwise +ought +our +ours +ourselves +out +outbreak +outbreaks +outcome +outcomes +outdoor +outer +outfit +outfits +outlet +outlets +outline +outlined +outlines +outlining +outlook +outlooks +output +outputs +outside +outsider +outsiders +outstanding +oven +ovens +over +overall +overcame +overcome +overcomes +overcoming +overlook +overlooked +overlooking +overlooks +overnight +overseas +overwhelming +owe +owed +owes +owing +owl +owls +own +owned +owner +owners +ownership +owning +owns +oxygen +oxygens +ozone +p +pace +paces +pack +package +packages +packed +packet +packets +packing +packs +pact +pacts +pad +pads +page +pages +paid +pain +painful +pains +paint +painted +painter +painters +painting +paintings +paints +pair +pairs +palace +palaces +pale +paler +palest +palm +palms +pan +panel +panels +panic +panics +pans +paper +papers +para +parade +parades +paragraph +paragraphs +parallel +parallels +parameter +parameters +paras +parcel +parcels +pardon +pardons +parent +parental +parents +parish +parishes +park +parked +parking +parks +parliament +parliamentary +parliaments +part +parted +partial +partially +participant +participants +participate +participated +participates +participating +participation +particle +particles +particular +particularly +parties +parting +partly +partner +partners +partnership +partnerships +parts +party +pass +passage +passages +passed +passenger +passengers +passes +passing +passion +passions +passive +passport +passports +past +pasts +patch +patches +patent +patents +path +paths +patience +patient +patients +patrol +patrols +patron +patrons +pattern +patterns +pause +paused +pauses +pausing +pavement +pavements +pay +payable +payed +paying +payment +payments +pays +pc +pcs +peace +peaceful +peak +peaks +peasant +peasants +peculiar +peer +peered +peering +peers +pen +penalties +penalty +pence +pencil +pencils +penetrate +penetrated +penetrates +penetrating +pennies +penny +pens +pension +pensioner +pensioners +pensions +people +peoples +pepper +peppers +per +perceive +perceived +perceives +perceiving +percent +percentage +percentages +perception +perceptions +perfect +perfectly +perform +performance +performances +performed +performer +performers +performing +performs +perhaps +period +periods +permanent +permanently +permission +permissions +permit +permits +permitted +permitting +persist +persisted +persistent +persisting +persists +person +personal +personalities +personality +personally +personnel +persons +perspective +perspectives +persuade +persuaded +persuades +persuading +pet +petition +petitions +petrol +pets +ph +phase +phases +phenomena +phenomenon +philosopher +philosophers +philosophical +philosophies +philosophy +phone +phoned +phones +phoning +photo +photograph +photographer +photographers +photographs +photography +photos +phrase +phrases +physical +physically +physics +piano +pianos +pick +picked +picking +picks +picture +pictured +pictures +picturing +pie +piece +pieces +pier +piers +pies +pig +pigs +pile +piled +piles +piling +pill +pillar +pillars +pillow +pillows +pills +pilot +pilots +pin +pine +pines +pink +pinker +pinned +pinning +pins +pint +pints +pipe +pipes +pit +pitch +pitches +pits +pity +place +placed +placement +placements +places +placing +plain +plainer +plainest +plains +plaintiff +plaintiffs +plan +plane +planes +planet +planets +planned +planner +planners +planning +plans +plant +planted +planting +plants +plastic +plastics +plate +plates +platform +platforms +play +played +player +players +playing +plays +plc +plcs +plea +plead +pleaded +pleading +pleads +pleas +pleasant +pleasanter +pleasantest +please +pleased +pleases +pleasing +pleasure +pleasures +pledge +pledged +pledges +pledging +plenty +plot +plots +plotted +plotting +plunge +plunged +plunges +plunging +plus +pm +pocket +pockets +poem +poems +poet +poetry +poets +point +pointed +pointing +points +poison +poisons +police +policeman +policemen +policies +policy +polite +politest +political +politically +politician +politicians +politics +poll +polls +pollution +pollutions +polymer +polymers +polytechnic +polytechnics +pond +ponds +ponies +pony +pool +pools +poor +poorer +poorest +pop +popped +popping +pops +popular +popularity +population +populations +port +portfolio +portfolios +portion +portions +portrait +portraits +ports +pose +posed +poses +posing +position +positioned +positioning +positions +positive +positively +possess +possessed +possesses +possessing +possession +possessions +possibilities +possibility +possible +possibly +post +poster +posters +postpone +postponed +postpones +posts +pot +potato +potatoes +potential +potentially +potentials +pots +pound +pounds +pour +poured +pouring +pours +poverty +powder +powders +power +powerful +powers +pp +practical +practically +practice +practices +practise +practised +practises +practising +practitioner +practitioners +praise +praised +praises +praising +pray +prayed +prayer +prayers +praying +prays +preach +preached +preaches +preaching +precede +preceded +precedent +precedents +precedes +preceding +precious +precise +precisely +precision +predator +predators +predecessor +predecessors +predict +predicted +predicting +prediction +predictions +predicts +predominantly +prefer +preference +preferences +preferred +preferring +prefers +pregnancies +pregnancy +pregnant +prejudice +prejudices +preliminary +premier +premise +premises +premium +premiums +preparation +preparations +prepare +prepared +prepares +preparing +prescribe +prescribed +prescribes +prescribing +prescription +prescriptions +presence +presences +present +presentation +presentations +presented +presenting +presents +preservation +preserve +preserved +preserves +preserving +presidencies +presidency +president +presidential +presidents +press +pressed +presses +pressing +pressure +pressures +presumably +presume +presumed +presumes +presuming +pretend +pretended +pretending +pretends +prettier +prettiest +pretty +prevail +prevailed +prevails +prevent +prevented +preventing +prevention +prevents +previous +previously +prey +preys +price +priced +prices +pricing +pride +priest +priests +primaries +primarily +primary +prime +primitive +prince +princes +princess +princesses +principal +principals +principle +principles +print +printed +printer +printers +printing +prints +prior +priorities +priority +prison +prisoner +prisoners +prisons +privacy +private +privately +privatisation +privatisations +privilege +privileges +prize +prizes +probabilities +probability +probable +probably +probe +probes +problem +problems +procedure +procedures +proceed +proceeded +proceeding +proceedings +proceeds +process +processed +processes +processing +processor +processors +proclaim +proclaimed +proclaiming +proclaims +produce +produced +producer +producers +produces +producing +product +production +productions +productive +productivity +products +profession +professional +professionals +professions +professor +professors +profile +profiles +profit +profitable +profits +profound +profounder +profoundest +program +programme +programmes +programming +programs +progress +progressed +progresses +progressing +progressive +prohibit +prohibited +prohibiting +prohibits +project +projected +projecting +projection +projections +projects +prominent +promise +promised +promises +promising +promote +promoted +promoter +promoters +promotes +promoting +promotion +promotions +prompt +prompted +prompting +prompts +pronounce +pronounced +pronounces +pronouncing +proof +proofs +propaganda +proper +properly +properties +property +proportion +proportions +proposal +proposals +propose +proposed +proposes +proposing +proposition +propositions +prosecute +prosecuted +prosecuting +prosecution +prosecutions +prospect +prospective +prospects +prosperity +protect +protected +protecting +protection +protections +protective +protects +protein +proteins +protest +protestant +protested +protesting +protests +protocol +protocols +proud +prouder +proudest +prove +proved +proven +proves +provide +provided +provider +providers +provides +providing +province +provinces +provincial +proving +provision +provisions +provoke +provoked +provokes +provoking +ps +psychiatric +psychological +psychologies +psychologist +psychologists +psychology +pub +public +publication +publications +publicity +publicly +publics +publish +published +publisher +publishers +publishes +publishing +pubs +pudding +puddings +pull +pulled +pulling +pulls +pulse +pulses +pump +pumps +punch +punches +punish +punished +punishes +punishing +punishment +punishments +pupil +pupils +purchase +purchased +purchaser +purchasers +purchases +purchasing +pure +purely +purer +purest +purple +purpose +purposes +pursue +pursued +pursues +pursuing +pursuit +pursuits +push +pushed +pushes +pushing +put +puts +putted +putting +puzzled +pylori +pylorus +qualification +qualifications +qualified +qualifies +qualify +qualifying +qualities +quality +quantities +quantity +quarries +quarry +quarter +quarters +queen +queens +queries +query +question +questioned +questioning +questionnaire +questionnaires +questions +queue +queues +quick +quicker +quickest +quickly +quid +quids +quiet +quieter +quietest +quietly +quit +quite +quits +quitted +quitting +quota +quotas +quotation +quotations +quote +quoted +quotes +quoting +qv +rabbit +rabbits +race +raced +races +racial +racing +racism +radiation +radiations +radical +radio +radios +rage +rages +raid +raids +rail +rails +railway +railways +rain +rainbow +rainbows +rained +raining +rains +raise +raised +raises +raising +rallies +rally +ram +rams +ran +random +rang +range +ranged +ranger +rangers +ranges +ranging +rank +ranks +rape +rapes +rapid +rapidly +rare +rarely +rarer +rarest +rat +rate +rated +rates +rather +rating +ratings +ratio +rational +ratios +rats +raw +reach +reached +reaches +reaching +react +reacted +reacting +reaction +reactions +reactor +reactors +reacts +read +reader +readers +readier +readily +reading +readings +reads +ready +real +realise +realised +realises +realising +realistic +realities +reality +realize +realized +realizes +realizing +really +realm +realms +rear +rears +reason +reasonable +reasonably +reasoning +reasons +reassure +reassured +reassures +reassuring +rebel +rebellion +rebellions +rebels +rebuild +rebuilding +rebuilds +rebuilt +recall +recalled +recalling +recalls +receipt +receipts +receive +received +receiver +receivers +receives +receiving +recent +recently +reception +receptions +recession +recessions +recipe +recipes +recipient +recipients +reckon +reckoned +reckoning +reckons +recognise +recognised +recognises +recognising +recognition +recognitions +recognize +recognized +recognizes +recognizing +recommend +recommendation +recommendations +recommended +recommending +recommends +reconstruction +reconstructions +record +recorded +recorder +recorders +recording +recordings +records +recover +recovered +recoveries +recovering +recovers +recovery +recruit +recruited +recruiting +recruitment +recruits +red +redder +reds +reduce +reduced +reduces +reducing +reduction +reductions +redundancies +redundancy +redundant +ref +refer +referee +referees +reference +references +referenda +referendum +referendums +referral +referrals +referred +referring +refers +reflect +reflected +reflecting +reflection +reflections +reflects +reform +reforms +refs +refuge +refugee +refugees +refuges +refusal +refusals +refuse +refused +refuses +refusing +regain +regained +regaining +regains +regard +regarded +regarding +regardless +regards +regime +regiment +regiments +regimes +region +regional +regions +register +registered +registering +registers +registration +registrations +regret +regrets +regretted +regretting +regular +regularly +regulate +regulated +regulates +regulating +regulation +regulations +regulatory +rehearsal +rehearsals +reign +reigns +reinforce +reinforced +reinforces +reinforcing +reject +rejected +rejecting +rejection +rejections +rejects +relate +related +relates +relating +relation +relations +relationship +relationships +relative +relatively +relatives +relax +relaxation +relaxations +relaxed +relaxes +relaxing +release +released +releases +releasing +relevance +relevant +reliable +relied +relief +reliefs +relies +relieve +relieved +relieves +relieving +religion +religions +religious +reluctance +reluctant +rely +relying +remain +remainder +remainders +remained +remaining +remains +remark +remarkable +remarkably +remarked +remarking +remarks +remedies +remedy +remember +remembered +remembering +remembers +remind +reminded +reminder +reminders +reminding +reminds +remote +remoter +remotest +removal +removals +remove +removed +removes +removing +renaissance +render +rendered +rendering +renders +renew +renewal +renewals +renewed +renewing +renews +rent +rented +renting +rents +repaid +repair +repaired +repairing +repairs +repay +repaying +repayment +repayments +repays +repeat +repeated +repeatedly +repeating +repeats +repetition +repetitions +replace +replaced +replacement +replacements +replaces +replacing +replied +replies +reply +replying +report +reported +reportedly +reporter +reporters +reporting +reports +represent +representation +representations +representative +representatives +represented +representing +represents +reproduce +reproduced +reproduces +reproducing +reproduction +reproductions +republic +republican +republicans +republics +reputation +reputations +request +requested +requesting +requests +require +required +requirement +requirements +requires +requiring +rescue +rescued +rescues +rescuing +research +researched +researcher +researchers +researches +researching +resemble +resembled +resembles +resembling +resentment +resentments +reservation +reservations +reserve +reserved +reserves +reserving +reservoir +reservoirs +residence +residences +resident +residential +residents +residue +residues +resign +resignation +resignations +resigned +resigning +resigns +resist +resistance +resistances +resisted +resisting +resists +resolution +resolutions +resolve +resolved +resolves +resolving +resort +resorts +resource +resources +respect +respectable +respected +respecting +respective +respectively +respects +respond +responded +respondent +respondents +responding +responds +response +responses +responsibilities +responsibility +responsible +rest +restaurant +restaurants +rested +resting +restoration +restorations +restore +restored +restores +restoring +restraint +restraints +restrict +restricted +restricting +restriction +restrictions +restricts +rests +result +resulted +resulting +results +resume +resumed +resumes +resuming +retail +retailer +retailers +retain +retained +retaining +retains +retire +retired +retirement +retirements +retires +retiring +retreat +retreats +return +returned +returning +returns +rev +reveal +revealed +revealing +reveals +revelation +revelations +revenge +revenue +revenues +reverse +reversed +reverses +reversing +review +reviewed +reviewing +reviews +revise +revised +revises +revising +revision +revisions +revival +revivals +revive +revived +revives +reviving +revolution +revolutionary +revolutions +revs +reward +rewarded +rewarding +rewards +rhythm +rhythms +rib +ribbon +ribbons +ribs +rice +rich +richer +richest +rid +ridden +ridding +ride +rider +riders +rides +ridge +ridges +ridiculous +riding +rifle +rifles +right +rightly +rights +rigid +ring +ringing +rings +riot +riots +rip +ripped +ripping +rips +rise +risen +rises +rising +risk +risked +risking +risks +ritual +rituals +rival +rivals +river +rivers +road +roads +roar +roared +roaring +roars +rob +robbed +robbing +robs +rock +rocked +rocking +rocks +rod +rode +rods +role +roles +roll +rolled +rolling +rolls +romance +romances +romantic +roof +roofs +room +rooms +root +rooted +rooting +roots +rope +ropes +rose +roses +rough +rougher +roughest +roughly +round +rounded +rounder +rounding +rounds +route +routes +routine +routines +row +rows +royal +rub +rubbed +rubber +rubbing +rubbish +rubs +rude +ruder +rudest +rug +rugby +rugs +ruin +ruins +rule +ruled +ruler +rulers +rules +ruling +rulings +rumour +rumours +run +rung +runner +runners +running +runnings +runs +rural +rush +rushed +rushes +rushing +sack +sacked +sacking +sacks +sacred +sacrifice +sacrifices +sad +sadder +saddest +sadly +safe +safely +safer +safest +safety +said +sail +sailed +sailing +sailor +sailors +sails +saint +saints +sake +sakes +salad +salads +salaries +salary +sale +sales +salmon +salt +salts +salvation +same +sample +samples +sanction +sanctions +sand +sands +sandwich +sandwiches +sang +sank +sat +satellite +satellites +satisfaction +satisfactions +satisfactory +satisfied +satisfies +satisfy +satisfying +sauce +sauces +save +saved +saves +saving +savings +saw +say +sayed +saying +says +scale +scales +scan +scandal +scandals +scanned +scanning +scans +scarcely +scared +scatter +scattered +scattering +scatters +scene +scenes +scent +scents +schedule +scheduled +schedules +scheduling +scheme +schemes +scholar +scholars +scholarship +scholarships +school +schools +science +sciences +scientific +scientist +scientists +scope +scopes +score +scored +scores +scoring +scrap +scraps +scratch +scratched +scratches +scratching +scream +screamed +screaming +screams +screen +screened +screening +screens +screw +screwed +screwing +screws +script +scripts +scrutinies +scrutiny +sculpture +sculptures +sea +seal +sealed +sealing +seals +search +searched +searches +searching +seas +season +seasons +seat +seated +seating +seats +second +secondary +secondly +seconds +secret +secretaries +secretary +secrets +section +sections +sector +sectors +secure +secured +secures +securing +securities +security +sediment +sediments +see +seed +seeds +seeing +seek +seeking +seeks +seem +seemed +seeming +seemingly +seems +seen +sees +segment +segments +seize +seized +seizes +seizing +seldom +select +selected +selecting +selection +selections +selective +selects +self +sell +seller +sellers +selling +sells +selves +semantic +seminar +seminars +senate +send +sending +sends +senior +sensation +sensations +sense +sensed +senses +sensible +sensing +sensitive +sensitivities +sensitivity +sent +sentence +sentenced +sentences +sentencing +sentiment +sentiments +separate +separated +separately +separates +separating +separation +separations +sequence +sequences +sergeant +sergeants +series +serious +seriously +serum +serums +servant +servants +serve +served +server +servers +serves +service +serviced +services +servicing +serving +session +sessions +set +sets +setting +settings +settle +settled +settlement +settlements +settles +settling +seven +sevens +seventeen +seventeenth +seventh +seventies +seventy +several +severe +severely +severest +sex +sexes +sexual +sexualities +sexuality +sexually +shade +shades +shadow +shadows +shaft +shafts +shake +shaken +shakes +shaking +shall +shallow +shallower +shallowest +shame +shape +shaped +shapes +shaping +share +shared +shareholder +shareholders +shares +sharing +sharp +sharply +she +shed +shedding +sheds +sheep +sheer +sheet +sheets +shelf +shell +shells +shelter +shelters +shelves +shield +shields +shift +shifted +shifting +shifts +shilling +shillings +shine +shined +shines +shining +ship +shipped +shipping +ships +shirt +shirts +shiver +shivered +shivering +shivers +shock +shocked +shocks +shoe +shoes +shone +shook +shoot +shooting +shootings +shoots +shop +shopped +shopping +shops +shore +shores +short +shortage +shortages +shorter +shortest +shortly +shot +shots +should +shoulder +shoulders +shout +shouted +shouting +shouts +show +showed +shower +showers +showing +shown +shows +shrug +shrugged +shrugging +shrugs +shut +shuts +shutting +shy +shyer +sick +sicker +sickness +sicknesses +side +sides +sigh +sighed +sighing +sighs +sight +sights +sign +signal +signalled +signalling +signals +signature +signatures +signed +significance +significances +significant +significantly +signing +signs +silence +silences +silent +silently +silk +silks +sillier +silliest +silly +silver +similar +similarities +similarity +similarly +simple +simpler +simplest +simply +simultaneously +sin +since +sincerely +sing +singer +singers +singing +single +singles +sings +sink +sinking +sinks +sins +sir +sirs +sister +sisters +sit +site +sites +sits +sitting +situate +situated +situates +situating +situation +situations +six +sixes +sixteen +sixth +sixties +sixty +size +sizes +sketch +sketches +skies +skill +skilled +skills +skin +skins +skirt +skirts +skull +skulls +sky +slam +slammed +slamming +slams +slave +slaves +sleep +sleeping +sleeps +sleeve +sleeves +slept +slice +slices +slid +slide +slides +sliding +slight +slighter +slightest +slightly +slim +slimmer +slimmest +slip +slipped +slipping +slips +slope +slopes +slow +slowed +slower +slowest +slowing +slowly +slows +small +smaller +smallest +smart +smarter +smartest +smash +smashed +smashes +smashing +smell +smelled +smelling +smells +smelt +smile +smiled +smiles +smiling +smoke +smoked +smokes +smoking +smooth +smoothed +smoother +smoothest +smoothing +smooths +snake +snakes +snap +snapped +snapping +snaps +snatch +snatched +snatches +snatching +sniff +sniffed +sniffing +sniffs +snow +snows +so +soap +soaps +soccer +social +socialism +socialist +socially +societies +society +sociology +sock +socks +sofa +sofas +soft +softer +softest +softly +software +soil +soils +solar +sold +soldier +soldiers +sole +solely +solicitor +solicitors +solid +solidarities +solidarity +solo +solos +solution +solutions +solve +solved +solves +solving +some +somebody +somehow +someone +something +sometimes +somewhat +somewhere +son +song +songs +sons +soon +sooner +sophisticated +sorrier +sorry +sort +sorted +sorting +sorts +sought +soul +souls +sound +sounded +sounder +soundest +sounding +sounds +soup +soups +source +sources +south +southern +sovereignty +space +spaces +spare +spared +spares +sparing +spat +spatial +speak +speaker +speakers +speaking +speaks +special +specialise +specialised +specialises +specialising +specialist +specialists +specially +species +specific +specifically +specification +specifications +specified +specifies +specify +specifying +specimen +specimens +spectacle +spectacles +spectacular +spectator +spectators +spectra +spectrum +speculation +speculations +sped +speech +speeches +speed +speeded +speeding +speeds +spell +spelled +spelling +spellings +spells +spelt +spend +spending +spends +spent +sphere +spheres +spider +spiders +spill +spilled +spilling +spills +spilt +spin +spine +spines +spinning +spins +spirit +spirits +spiritual +spit +spits +spitting +splendid +split +splits +splitting +spoil +spoiled +spoiling +spoils +spoilt +spoke +spoken +spokesman +spokesmen +sponsor +sponsored +sponsoring +sponsors +sponsorship +sponsorships +spontaneous +sport +sporting +sports +spot +spots +spotted +spotting +sprang +spread +spreading +spreads +spring +springing +springs +sprung +spun +spur +spurs +squad +squadron +squadrons +squads +square +squares +squeeze +squeezed +squeezes +squeezing +stab +stabbed +stabbing +stability +stable +stabs +stadia +stadium +stadiums +staff +staffs +stage +staged +stages +staging +stair +staircase +staircases +stairs +stake +stakes +stall +stalls +stamp +stamped +stamping +stamps +stance +stances +stand +standard +standards +standing +standings +stands +star +stare +stared +stares +staring +stars +start +started +starting +starts +state +stated +statement +statements +states +static +stating +station +stations +statistical +statistics +statue +statues +status +statute +statutes +statutory +staves +stay +stayed +staying +stays +steadier +steadily +steady +steal +stealing +steals +steam +steams +steel +steels +steep +steeper +steepest +steer +steered +steering +steers +stem +stemmed +stemming +stems +step +stepped +stepping +steps +sterling +steward +stewards +stick +sticking +sticks +stiff +stiffer +stiffest +still +stimulate +stimulated +stimulates +stimulating +stimuli +stimulus +stir +stirred +stirring +stirs +stitch +stitches +stock +stocks +stole +stolen +stomach +stomachs +stone +stones +stood +stool +stools +stop +stopped +stopping +stops +storage +store +stored +stores +stories +storing +storm +storms +story +straight +straighten +straightened +straightening +straightens +straighter +straightest +straightforward +strain +strained +straining +strains +strand +strands +strange +strangely +stranger +strangers +strangest +strategic +strategies +strategy +straw +straws +stream +streams +street +streets +strength +strengthen +strengthened +strengthening +strengthens +strengths +stress +stressed +stresses +stressing +stretch +stretched +stretches +stretching +strict +stricter +strictest +strictly +stride +strides +striding +strike +striker +strikers +strikes +striking +string +strings +strip +stripped +stripping +strips +strive +strived +striven +strives +striving +strode +stroke +stroked +strokes +stroking +strong +stronger +strongest +strongly +strove +struck +structural +structure +structures +struggle +struggled +struggles +struggling +stuck +student +students +studied +studies +studio +studios +study +studying +stuff +stuffed +stuffing +stuffs +stumble +stumbled +stumbles +stumbling +stupid +style +styles +subject +subjected +subjecting +subjective +subjects +submission +submissions +submit +submits +submitted +submitting +subscription +subscriptions +subsequent +subsequently +subsidiaries +subsidiary +subsidies +subsidy +substance +substances +substantial +substantially +substitute +substituted +substitutes +substituting +subtle +suburb +suburbs +succeed +succeeded +succeeding +succeeds +success +successes +successful +successfully +succession +successions +successive +successor +successors +such +suck +sucked +sucking +sucks +sudden +suddenly +sue +sued +sueing +sues +suffer +suffered +sufferer +sufferers +suffering +sufferings +suffers +sufficient +sufficiently +sugar +sugars +suggest +suggested +suggesting +suggestion +suggestions +suggests +suicide +suicides +suing +suit +suitable +suite +suited +suites +suiting +suits +sum +summaries +summarise +summarised +summarises +summarising +summary +summed +summer +summers +summing +summit +summits +summon +summoned +summoning +sums +sun +sung +sunk +sunlight +sunnier +sunniest +sunny +suns +sunshine +super +superb +superintendent +superintendents +superior +supermarket +supermarkets +supervise +supervised +supervises +supervising +supervision +supervisor +supervisors +supper +suppers +supplement +supplemented +supplementing +supplements +supplied +supplier +suppliers +supplies +supply +supplying +support +supported +supporter +supporters +supporting +supports +suppose +supposed +supposes +supposing +suppress +suppressed +suppresses +suppressing +supreme +sure +surely +surer +surest +surface +surfaces +surgeon +surgeons +surgeries +surgery +surplus +surprise +surprised +surprises +surprising +surprisingly +surrender +surrendered +surrendering +surrenders +surround +surrounded +surrounding +surroundings +surrounds +survey +surveyed +surveying +surveyor +surveyors +surveys +survival +survivals +survive +survived +survives +surviving +survivor +survivors +suspect +suspected +suspecting +suspects +suspend +suspended +suspending +suspends +suspension +suspensions +suspicion +suspicions +suspicious +sustain +sustained +sustaining +sustains +swallow +swallowed +swallowing +swallows +swam +swear +swearing +swears +sweat +sweats +sweep +sweeping +sweeps +sweet +sweeter +sweetest +swell +swelled +swelling +swells +swept +swiftly +swim +swimming +swims +swing +swinging +swings +switch +switched +switches +switching +swollen +sword +swords +swore +sworn +swum +swung +symbol +symbolic +symbols +sympathetic +sympathies +sympathy +symptom +symptoms +syndrome +syndromes +syntheses +synthesis +system +systematic +systems +table +tables +tablet +tablets +tackle +tackled +tackles +tackling +tactic +tactics +tail +tails +take +taken +takeover +takeovers +takes +taking +tale +talent +talents +tales +talk +talked +talking +talks +tall +taller +tallest +tank +tanks +tap +tape +tapes +tapped +tapping +taps +target +targeted +targeting +targets +targetted +targetting +tariff +tariffs +task +tasks +taste +tasted +tastes +tasting +taught +tax +taxation +taxed +taxes +taxi +taxing +taxis +taxpayer +taxpayers +tea +teach +teacher +teachers +teaches +teaching +teachings +team +teams +tear +tearing +tears +teas +technical +technically +technique +techniques +technological +technologies +technology +teenage +teenager +teenagers +teeth +tel +telecommunication +telecommunications +telegraph +telegraphs +telephone +telephoned +telephones +telephoning +television +televisions +tell +telling +tells +temper +temperature +temperatures +tempers +temple +temples +temporarily +temporary +tempt +temptation +temptations +tempted +tempting +tempts +ten +tenant +tenants +tend +tended +tendencies +tendency +tender +tending +tends +tennis +tens +tension +tensions +tent +tenth +tents +term +termed +terminal +terminals +terminate +terminated +terminates +terminating +terms +terrace +terraces +terrible +terribly +territorial +territories +territory +terror +terrorist +terrorists +terrors +test +testament +testaments +tested +testing +tests +text +textile +textiles +texts +texture +textures +than +thank +thanked +thanking +thanks +that +the +theatre +theatres +thee +theft +thefts +their +theirs +theirselves +them +theme +themes +themselves +then +theologies +theology +theoretical +theories +theory +therapies +therapy +there +thereafter +thereby +therefore +these +theses +thesis +they +thick +thicker +thickest +thief +thieves +thigh +thighs +thin +thing +things +think +thinking +thinks +thinner +thinnest +third +thirteen +thirties +thirty +this +thorough +thoroughly +those +thou +though +thought +thoughts +thousand +thousands +thread +threads +threat +threaten +threatened +threatening +threatens +threats +three +threes +threshold +thresholds +threw +throat +throats +throne +thrones +through +throughout +throw +throwing +thrown +throws +thrust +thrusting +thrusts +thumb +thumbs +thus +ticket +tickets +tide +tides +tie +tied +ties +tiger +tigers +tight +tighten +tightened +tightening +tightens +tighter +tightest +tightly +tile +tiles +till +timber +timbers +time +timed +times +timetable +timetables +timing +timings +tin +tinier +tiniest +tins +tiny +tip +tipped +tipping +tips +tired +tissue +tissues +title +titles +to +toast +toasts +tobacco +today +toe +toes +together +toilet +toilets +told +tolerate +tolerated +tolerates +tolerating +tomato +tomatoes +tomorrow +ton +tone +tones +tongue +tongues +tonight +tonne +tonnes +tons +too +took +tool +tools +tooth +top +topic +topics +topped +topping +tops +torch +torches +tore +torn +toss +tossed +tosses +tossing +total +totalled +totalling +totally +totals +touch +touched +touches +touching +tough +tougher +toughest +tour +toured +touring +tourism +tourist +tourists +tournament +tournaments +tours +toward +towards +towel +towels +tower +towers +town +towns +toxic +toy +toys +trace +traced +traces +tracing +track +tracks +trade +traded +trader +traders +trades +trading +tradition +traditional +traditionally +traditions +traffic +tragedies +tragedy +tragic +trail +trailed +trailing +trails +train +trained +trainee +trainees +trainer +trainers +training +trains +transaction +transactions +transfer +transferred +transferring +transfers +transform +transformation +transformations +transformed +transforming +transforms +transition +transitions +translate +translated +translates +translating +translation +translations +transmission +transmissions +transmit +transmits +transmitted +transmitting +transport +transported +transporting +transports +trap +trapped +trapping +traps +travel +traveling +travelled +traveller +travellers +travelling +travels +tray +trays +treasure +treasures +treasuries +treasury +treat +treated +treaties +treating +treatment +treatments +treats +treaty +tree +trees +tremble +trembled +trembles +trembling +tremendous +trend +trends +trial +trials +triangle +triangles +tribe +tribes +tribunal +tribunals +tribute +tributes +trick +tricks +tried +tries +trigger +triggered +triggering +triggers +trip +trips +triumph +triumphs +troop +troops +trophies +trophy +tropical +trouble +troubled +troubles +troubling +trouser +trousers +truck +trucks +truer +truest +truly +trunk +trunks +trust +trusted +trustee +trustees +trusting +trusts +truth +truths +try +trying +tube +tubes +tuck +tucked +tucking +tucks +tumour +tumours +tune +tunes +tunnel +tunnels +turn +turned +turning +turnover +turnovers +turns +tutor +tutors +twelve +twelves +twenties +twentieth +twenty +twice +twin +twins +twist +twisted +twisting +twists +two +twos +tying +type +typed +types +typical +typically +typing +tyre +tyres +uglier +ugliest +ugly +ulcer +ulcers +ultimate +ultimately +unable +unacceptable +unaware +uncertain +uncertainties +uncertainty +unchanged +uncle +uncles +uncomfortable +unconscious +under +undergo +undergoes +undergoing +undergone +underground +underline +underlined +underlines +underlining +underlying +undermine +undermined +undermines +undermining +underneath +understand +understanding +understandings +understands +understood +undertake +undertaken +undertakes +undertaking +undertakings +undertook +underwent +undoubtedly +unemployed +unemployment +unexpected +unfair +unfortunate +unfortunately +unhappier +unhappiest +unhappy +uniform +uniforms +union +unionist +unionists +unions +unique +unit +unite +united +unites +unities +uniting +units +unity +universal +universe +universes +universities +university +unknown +unless +unlike +unlikeliest +unlikely +unnecessary +unpleasant +until +unusual +unusually +unwilling +up +update +updated +updates +updating +upon +upper +upset +upsets +upsetting +upstairs +upwards +urban +urge +urged +urgent +urges +urging +us +usage +usages +use +used +useful +useless +user +users +uses +using +usual +usually +utilities +utility +utterance +utterances +utterly +v +vague +valid +validity +valley +valleys +valuable +valuation +valuations +value +valued +values +valuing +valve +valves +van +vanish +vanished +vanishes +vanishing +vans +variable +variables +variant +variants +variation +variations +varied +varies +varieties +variety +various +vary +varying +vast +vaster +vegetable +vegetables +vegetation +vehicle +vehicles +vein +veins +velocities +velocity +vendor +vendors +venture +ventured +ventures +venturing +venue +venues +verb +verbal +verbs +verdict +verdicts +verse +verses +version +versions +vertical +very +vessel +vessels +via +victim +victims +victories +victory +video +videos +view +viewed +viewer +viewers +viewing +viewpoint +viewpoints +views +villa +village +villages +villas +violence +violent +virgin +virgins +virtually +virtue +virtues +virus +viruses +visible +vision +visions +visit +visited +visiting +visitor +visitors +visits +visual +vital +vitamin +vitamins +vivid +vocabularies +vocabulary +vocational +voice +voiced +voices +voicing +vol +vols +voltage +voltages +volume +volumes +voluntary +volunteer +volunteers +von +vote +voted +voter +voters +votes +voting +vulnerable +wage +wages +waist +waists +wait +waited +waiter +waiters +waiting +waits +wake +wakes +waking +walk +walked +walking +walks +wall +walls +wan +wander +wandered +wandering +wanders +want +wanted +wanting +wants +war +ward +wardrobe +wardrobes +wards +warehouse +warehouses +warm +warmed +warmer +warmest +warming +warms +warmth +warn +warned +warning +warnings +warns +warrant +warrants +warrior +warriors +wars +wartime +was +wash +washed +washes +washing +washings +waste +wasted +wastes +wasting +watch +watched +watches +watching +water +waters +wave +waved +waves +waving +way +ways +we +weak +weaken +weakened +weakening +weakens +weaker +weakest +weakness +weaknesses +wealth +wealthier +wealthiest +wealthy +weapon +weapons +wear +wearing +wears +weather +weathers +weave +weaved +weaves +weaving +wedding +weddings +wee +week +weekend +weekends +weekly +weeks +weep +weeping +weeps +weigh +weighed +weighing +weighs +weight +weights +weird +welcome +welcomed +welcomes +welcoming +welfare +well +wells +went +wept +were +west +western +wet +wetter +wettest +whale +whales +what +whatever +wheat +wheats +wheel +wheels +when +whenever +where +whereas +whereby +wherever +whether +which +while +whilst +whiskies +whisky +whisper +whispered +whispering +whispers +white +whiter +whites +whitest +who +whoever +whole +wholes +wholly +whom +whose +why +wicked +wicket +wickets +wide +widely +widen +widened +widening +widens +wider +widespread +widest +widow +widows +width +widths +wife +wild +wilder +wildest +wildlife +will +willing +willingness +wills +win +wind +winded +winding +window +windows +winds +wine +wines +wing +wings +winner +winners +winning +wins +winter +winters +wipe +wiped +wipes +wiping +wire +wires +wisdom +wisdoms +wise +wiser +wisest +wish +wished +wishes +wishing +wit +with +withdraw +withdrawal +withdrawals +withdrawing +withdrawn +withdraws +withdrew +within +without +witness +witnessed +witnesses +witnessing +wits +wives +woke +woken +wolf +wolves +woman +women +won +wonder +wondered +wonderful +wondering +wonders +wood +wooden +woodland +woodlands +woods +wool +wools +word +words +wore +work +worked +worker +workers +workforce +workforces +working +workings +works +workshop +workshops +workstation +workstations +world +worlds +worldwide +worm +worms +worn +worried +worries +worry +worrying +worse +worship +worships +worst +worth +worthier +worthwhile +worthy +would +wound +wounded +wounding +wounds +wove +woven +wrap +wrapped +wrapping +wraps +wrist +wrists +write +writer +writers +writes +writing +writings +written +wrong +wrote +wrought +x +ya +yacht +yachts +yard +yards +yarn +yarns +ye +yeah +year +years +yell +yelled +yelling +yellow +yells +yep +yer +yes +yesterday +yet +yield +yielded +yielding +yields +yo +you +young +younger +youngest +youngster +youngsters +your +yours +yourself +youth +youths +zero +zeros +zone +zones diff --git a/soloraidar.py b/soloraidar.py index e47e7a1f..6495dfc3 100644 --- a/soloraidar.py +++ b/soloraidar.py @@ -1,8 +1,14 @@ __author__ = "Toeofdoom" +import time import sys +import os.path from evtcparser import * from analyser import * +from enum import IntEnum +import json +from zipfile import ZipFile +import argparse def is_basic_value(node): try: @@ -12,7 +18,7 @@ def is_basic_value(node): return True def flatten(root): - nodes = dict((key, dict(node)) for key,node in root) + nodes = dict((key, dict(node)) for key,node in root.items()) stack = list(nodes.keys()) for node_name in stack: node = nodes[node_name] @@ -23,28 +29,78 @@ def flatten(root): stack.append(full_child_name) except TypeError: pass + except ValueError: + pass return nodes -def print_node(key, node): - basic_values = filter(lambda key:is_basic_value(key[1]), node.items()) - print("{0}: {1}".format(key, ", ".join( - ["{0}:{1}".format(name, value) for name,value in basic_values]))) +def format_value(value): + if isinstance(value, IntEnum): + return value.name + else: + return value + +def print_node(key, node, f=None): + basic_values = list(filter(lambda key:is_basic_value(key[1]), node.items())) + if basic_values: + output_string = "{0}: {1}".format(key, ", ".join( + ["{0}:{1}".format(name, format_value(value)) for name,value in basic_values])) + print(output_string, file=f) def main(): - filename = sys.argv[1] - - print("Parsing {0}".format(filename)) - with open(sys.argv[1], mode='rb') as file: - e = parser.Encounter(file) - a = analyser.Analyser(e) - metrics = a.compute_all_metrics() - flattened = flatten(metrics) - for key in flattened: - print_node(key, flattened[key]) - #for skill in e.skills: - # print("Skill \"{0}\"".format(skill.name)) - #for event in e.events: - #print("Skill \"{0}\"".format(event.src_agent)) + + + zipfile = None + + argparser = argparse.ArgumentParser(description='Process some integers.') + argparser.add_argument('filenames', metavar='N', type=str, nargs='+', + help='the files to load') + argparser.add_argument('-s', dest='silent', action='store_true', + help='silent mode, no output dump') + argparser.add_argument('--no-json', dest='json', action='store_false', + help='disable json output') + + args = argparser.parse_args() + start_all = time.clock() + print("Parsing {0}".format(args.filenames)) + for filename in args.filenames: + print("Loading {0}".format(filename)) + with open(filename, mode='rb') as file: + + if filename.endswith('.evtc.zip'): + zipfile = ZipFile(file) + contents = zipfile.infolist() + if len(contents) == 1: + file = zipfile.open(contents[0].filename) + else: + print('Only single-file ZIP archives are allowed', file=sys.stderr) + sys.exit(1) + + start = time.clock() + e = parser.Encounter(file) + print("Parsing took {0} seconds".format(time.clock() - start)) + print("Evtc version {0}".format(e.version)) + + start = time.clock() + a = analyser.Analyser(e) + print("Analyser took {0} seconds".format(time.clock() - start)) + + start = time.clock() + with open('Output/'+os.path.basename(filename)+'.txt','w') as output_file: + flattened = flatten(a.data) + for key in sorted(flattened.keys()): + if not args.silent: + print_node(key, flattened[key]) + print_node(key, flattened[key], output_file) + print("Completed parsing {0} - Success: {1}".format( + list(a.data['Category']['boss']['Boss'].keys())[0], + a.data['Category']['encounter']['success'])) + print("Readable dump took {0} seconds".format(time.clock() - start)) + + if "--no-json" not in sys.argv: + start = time.clock() + print(json.dumps(a.data), file=open('output.json','w')) + print("JSon dump took {0} seconds".format(time.clock() - start)) + print("Analysing all took {0} seconds".format(time.clock() - start_all)) if __name__ == "__main__": main()