From 5574b8bb85b40e3c8e10320972850994ae47bda1 Mon Sep 17 00:00:00 2001 From: DoubleCookies <12849336+DoubleCookies@users.noreply.github.com> Date: Tue, 18 Oct 2022 12:06:15 +0300 Subject: [PATCH 01/21] Calendar, contrubtions, front translate, short game modes names will be same --- public/locales/ru/calendar.json | 2 ++ public/locales/ru/contributions.json | 3 ++- public/locales/ru/front.json | 3 +++ public/locales/ru/game-misc.json | 7 ++++++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/public/locales/ru/calendar.json b/public/locales/ru/calendar.json index 20407405a..f960971e8 100644 --- a/public/locales/ru/calendar.json +++ b/public/locales/ru/calendar.json @@ -8,6 +8,7 @@ "participatedCount": "{{count}} команд приняло участие", "members": "Участники", "results": "Результаты", + "createMapList": "Создать список карт", "forms.dates": "Даты", "forms.bracketUrl": "URL сетки", @@ -17,6 +18,7 @@ "forms.tags.info": "Тег \"Призовые значки\" добавляется автоматически, если это возможно", "forms.badges": "Призовые значки", "forms.badges.placeholder": "Выберите призовой значок", + "forms.mapPool": "Список карт", "forms.participantCount": "Количество участников", "forms.reportResultsHeader": "Указать результаты для {{eventName}}", diff --git a/public/locales/ru/contributions.json b/public/locales/ru/contributions.json index 7ed68f9dc..00bf866a1 100644 --- a/public/locales/ru/contributions.json +++ b/public/locales/ru/contributions.json @@ -3,5 +3,6 @@ "code": "Помощники, внесшие изменения в код", "lean": "Помощь с исследованием внутренностей Splatoon и создатель бота Lanista", "borzoic": "Создатель рисунка на главной странице, а также значков и иконок", - "uberu": "Нарисовал Судокотика, держащего эмодзи-сердце" + "uberu": "Нарисовал Судокотика, держащего эмодзи-сердце", + "translation": "Перевод" } diff --git a/public/locales/ru/front.json b/public/locales/ru/front.json index c4c73e7c8..f6174c0db 100644 --- a/public/locales/ru/front.json +++ b/public/locales/ru/front.json @@ -1,9 +1,12 @@ { "websiteSubtitle": "Соревновательный Splatoon-хаб", + "buildsGoTo": "Изучайте сборки всех оружий в Splatoon 3 от лучших (и не только) игроков", "calendarGoTo": "Посмотреть все прошлые и предстоящие события на странице календаря", "moreFeatures": "Больше возможностей", "plus.description": "Посмотреть историю голосования Plus Server (и не только)", "badges.description": "Список всех значков, которые можно заработать для своего профиля", + "analyzer.description": "Узнайте, что на самом деле делают ваши сборки", + "maps.description": "Сделайте свой список на основе выбранного пула карт", "recentWinners": "Недавние победители", "upcomingEvents": "Предстоящие события", "articleBy": "от {{author}}" diff --git a/public/locales/ru/game-misc.json b/public/locales/ru/game-misc.json index 74168c87a..ff59448e4 100644 --- a/public/locales/ru/game-misc.json +++ b/public/locales/ru/game-misc.json @@ -10,5 +10,10 @@ "STAGE_8": "Академия «Лепота»", "STAGE_9": "Осетровые верфи", "STAGE_10": "«Горбуша-Маркет»", - "STAGE_11": "Луна-парк «Язь»" + "STAGE_11": "Луна-парк «Язь»", + "MODE_SHORT_TW": "TW", + "MODE_SHORT_SZ": "SZ", + "MODE_SHORT_TC": "TC", + "MODE_SHORT_RM": "RM", + "MODE_SHORT_CB": "CB" } From 4f5dad6fa9a483554dd3ad275f58c9bdf3f25b30 Mon Sep 17 00:00:00 2001 From: DoubleCookies <12849336+DoubleCookies@users.noreply.github.com> Date: Mon, 24 Oct 2022 13:07:38 +0300 Subject: [PATCH 02/21] analyzer by Yaga, other fixes by DblCookies --- public/locales/ru/analyzer.json | 96 ++++++++++++++++++++++++++++++++ public/locales/ru/common.json | 24 +++++++- public/locales/ru/game-misc.json | 10 ++-- public/locales/ru/user.json | 16 +++++- 4 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 public/locales/ru/analyzer.json diff --git a/public/locales/ru/analyzer.json b/public/locales/ru/analyzer.json new file mode 100644 index 000000000..e42dadead --- /dev/null +++ b/public/locales/ru/analyzer.json @@ -0,0 +1,96 @@ +{ + "attribute.weight": "Весовая категория:", + "attribute.weight.Fast": "Лёгкая", + "attribute.weight.Slow": "Тяжелая", + "attribute.weight.Normal": "Средняя", + "stat.category.main": "Основное оружие", + "stat.category.sub": "Запасное оружие", + "stat.category.special": "Особое оружие", + "stat.category.subDef": "Защита от запасного оружия", + "stat.category.actionsPerInkTank": "Действия за полный балон краски", + "stat.category.damage": "Урон", + "stat.category.movement": "Передвижение", + "stat.category.misc": "Прочее", + "stat.canopyHp": "Прочность зонта", + "stat.fullChargeSeconds": "Время до полного заряда", + "stat.maxChargeHoldSeconds": "Максимальное время удержания заряда в чернилах", + "stat.specialPoints": "Очки для использования", + "stat.specialLost": "Потеря заряда спешала после плюха", + "stat.whiteInk": "Время до начала восстановления краски после использования", + "stat.squidFormInkRecoverySeconds": "Время до полного восстановления баллона (кальмар)", + "stat.quickRespawnTime": "Быстрое время воскрешения", + "stat.superJumpTimeGround": "Период уязвимости суперпрыжка (в кадрах)", + "stat.superJumpTimeTotal": "Суммарное время суперпрыжка", + "stat.jumpShotSpread": "Разброс выстрелов в градусах (в прыжке)", + "stat.groundShotSpread": "Разброс выстрелов в градусах (на земле)", + "stat.squidSurgeChargeFrames": "Заряд Кальмарного Рывка (в кадрах)", + "stat.swimSpeed": "Скорость плавания (единицы расстояния за кадр)", + "stat.runSpeed": "Скорость бега (единицы расстояния за кадр)", + "stat.runSpeedInEnemyInk": "Скорость бега во вражеских чернилах", + "stat.shootingRunSpeed": "Скорость бега во время стрельбы", + "stat.shootingRunSpeedCharging": "Скорость бега во время заряда", + "stat.shootingRunSpeedFullCharge": "Скорость бега с полным зарядом", + "stat.framesBeforeTakingDamageInEnemyInk": "Время до получения урона от нахождения во вражеских чернилах (в кадрах)", + "stat.damageTakenInEnemyInkPerSecond": "Урон от нахождения во вражеских чернилах в секунду", + "stat.enemyInkDamageLimit": "Максимальный урон от нахождения во вражеских чернилах", + "stat.markedTime": "Время отслеживания: {{weapon}}", + "stat.movementReduction": "Замедление передвижения: {{weapon}}", + "stat.damage": "Урон: {{weapon}}", + "stat.bombHdamage": "Тяжёлый урон от бомб", + "stat.bombLdamage": "Лёгкий урон от бомб", + "stat.consumption.NORMAL": "Выстрелы", + "stat.consumption.SWING": "Взмахи", + "stat.consumption.SLOSH": "Выплёскивания", + "stat.consumption.VERTICAL_SWING": "Вертикальные взмахи", + "stat.consumption.HORIZONTAL_SWING": "Горизонтальные взмахи", + "stat.consumption.TAP_SHOT": "Тап шоты", + "stat.consumption.FULL_CHARGE": "Полностью заряженные выстрелы", + "stat.consumption.SPLATLING_CHARGE": "Полные заряды", + "stat.consumption.SHIELD_LAUNCH": "Запуски щита", + "stat.consumption.DUALIE_ROLL": "Перекаты", + "stat.sub.velocity": "Скорость (определяет дальнобойность)", + "stat.sub.firstPhaseDuration": "Длительность первой фазы", + "stat.sub.secondPhaseDuration": "Длительность второй фазы", + "stat.sub.markingTimeInSeconds": "Длительность маркировки", + "stat.sub.markingRadius": "Радиус маркировки", + "stat.sub.explosionRadius": "Радиус взрыва", + "stat.sub.hp": "Прочность", + "stat.sub.qsjBoost": "Ускорение суперпрыжка", + "stat.special.duration": "Продолжительность: {{weapon}}", + "stat.special.duration.inkStormExplanation": "Увеличивает прокрашиваемое расстояние, но не сам прокрас.", + "stat.special.damageDistance": "Расстояние урона: {{weapon}}", + "stat.special.paintRadius": "Радиус покраса: {{weapon}}", + "stat.special.shieldHp": "Прочность щита: {{weapon}}", + "stat.special.deviceHp": "Прочность устройства: {{weapon}}", + "stat.special.inkConsumptionHook": "Потребление чернил тросом: {{weapon}}", + "stat.special.inkConsumptionPerSecond": "Потребление чернил в секунду: {{weapon}}", + "stat.special.reticleRadius": "Радиус прицела: {{weapon}}", + "stat.special.throwDistance": "Расстояние броска: {{weapon}}", + "stat.special.autoChargeRate": "Скорость автоматического заряда: {{weapon}}", + "stat.special.maxRadius": "Максимальный радиус волны: {{weapon}}", + "stat.special.maxRadius.explanation": "Скорость передвижения волны не меняется вместе с её радиусом.", + "stat.special.radiusRange": "Радиус тяги: {{weapon}}", + "stat.special.powerUpDuration": "Длительность эффекта напитка: {{weapon}}", + "damage.header.type": "Тип", + "damage.header.damage": "Урон", + "damage.header.distance": "Дальнобойность", + "damage.toSplat": "{{count}} выстрел(ов) для плюха", + "damage.NORMAL_MIN": "Минимум", + "damage.NORMAL_MAX": "Максимум", + "damage.DIRECT": "Прямое попадание", + "damage.FULL_CHARGE": "Полностью заряженный выстрел", + "damage.MAX_CHARGE": "Максимальный частичный заряд", + "damage.TAP_SHOT": "Тап шот", + "damage.DISTANCE": "Разрыв", + "suffix.seconds": "сек", + "suffix.hp": "hp", + "suffix.specialPointsShort": "очк.", + "base": "Изначально", + "value": "Значение", + "build": "Сборка", + "patch": "Патч:", + "abilityPoints": "Свойства (AP)", + "abilityPoints.short": "AP", + "consumptionExplanation": "Данная таблица показывает, сколько действий можно совершить после 0-{{maxSubsToUse}}кратного использования запасного оружия. С полным баллоном запасное оружие можно использовать {{maxSubsToUse}} раз(а) подряд.", + "trackingSubDefExplanation": "В расчётах продолжительности маркировки Маркера Движения, Мины и Углострела используется противник с 0AP Стойкости Запаса." +} diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 57108bdaf..c8edb3932 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -3,9 +3,12 @@ "pages.badges": "Значки", "pages.plus": "Plus Server", "pages.contributors": "Помощники", + "pages.s2": "Splatoon 2", "pages.calendar": "Календарь", "pages.faq": "FAQ", "pages.builds": "Сборки", + "pages.analyzer": "Анализатор сборок", + "pages.maps": "Списки карт", "header.profile": "Профиль", "header.logout": "Выйти", @@ -24,6 +27,13 @@ "actions.add": "Добавить", "actions.remove": "Убрать", "actions.delete": "Удалить", + "actions.loadMore": "Загрузить ещё", + "actions.copyToClipboard": "Скопировать в буфер обмена", + + "maps.createMapList": "Создать список карт", + "maps.halfSz": "50% Зон", + "maps.mapPool": "Пул карт", + "maps.tournamentMaplist": "Создать список карт для турнира (maps.iplabs.ink)", "results": "Результаты", @@ -39,5 +49,17 @@ "tag.name.LOW": "Ограничение по скиллу", "tag.name.COUNT": "Лимит участников", "tag.name.LAN": "LAN", - "tag.name.QUALIFIER": "Квалификационный" + "tag.name.QUALIFIER": "Квалификационный", + + "weapon.category.SHOOTERS": "Каплестрелы", + "weapon.category.BLASTERS": "Бластеры", + "weapon.category.ROLLERS": "Валики", + "weapon.category.BRUSHES": "Кисти", + "weapon.category.CHARGERS": "Нагнетатели", + "weapon.category.SLOSHERS": "Ведроганы", + "weapon.category.SPLATLINGS": "Брызгометы", + "weapon.category.DUALIES": "Спуртометы", + "weapon.category.BRELLAS": "Зонтганы", + "weapon.category.STRINGERS": "Тетиваторы", + "weapon.category.SPLATANAS": "Сплат-катаны" } diff --git a/public/locales/ru/game-misc.json b/public/locales/ru/game-misc.json index ff59448e4..eabcce092 100644 --- a/public/locales/ru/game-misc.json +++ b/public/locales/ru/game-misc.json @@ -11,9 +11,9 @@ "STAGE_9": "Осетровые верфи", "STAGE_10": "«Горбуша-Маркет»", "STAGE_11": "Луна-парк «Язь»", - "MODE_SHORT_TW": "TW", - "MODE_SHORT_SZ": "SZ", - "MODE_SHORT_TC": "TC", - "MODE_SHORT_RM": "RM", - "MODE_SHORT_CB": "CB" + "MODE_SHORT_TW": "Терфы:", + "MODE_SHORT_SZ": "Зоны:", + "MODE_SHORT_TC": "Башни:", + "MODE_SHORT_RM": "Карп:", + "MODE_SHORT_CB": "Клэмы:" } diff --git a/public/locales/ru/user.json b/public/locales/ru/user.json index e0af3664b..4ceb740fc 100644 --- a/public/locales/ru/user.json +++ b/public/locales/ru/user.json @@ -1,10 +1,24 @@ { + "customUrl": "Пользовательский URL", + "ign": "Внутриигровое имя", + "ign.short": "Ник", "country": "Страна", "bio": "О себе", + "stickSens": "Чувствительность стика", + "motionSens": "Чувствительность наклона", + "motion": "Наклон", + "stick": "Стик", + "sens": "Чувствительность", "results.placing": "Место", "results.team": "Команда", "results.tournament": "Турнир", + "results.participants": "Участники", "results.date": "Дата", - "results.mates": "Напарники" + "results.mates": "Напарники", + + "forms.errors.invalidCustomUrl.numbers": "Пользовательский URL не может содержать только цифры", + "forms.errors.invalidCustomUrl.strangeCharacter": "Пользовательский URL не может содержать особые символы", + "forms.errors.invalidCustomUrl.duplicate": "Кто-то уже использует этот пользовательский URL", + "forms.errors.invalidSens": "Чувствительность наклона не может быть указана, если не указана чувствительность стика" } From 672fa07152263e152ee65a0a4a91732e8fb8cbd6 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 27 Oct 2022 09:14:02 +0300 Subject: [PATCH 03/21] Update Build Analyzer to patch 1.2.0 --- app/modules/analyzer/weapon-params.json | 8 ++++---- app/routes/analyzer.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/modules/analyzer/weapon-params.json b/app/modules/analyzer/weapon-params.json index a01399359..e2ccd2823 100644 --- a/app/modules/analyzer/weapon-params.json +++ b/app/modules/analyzer/weapon-params.json @@ -375,7 +375,7 @@ "InkConsume_WeaponSwingParam": 0.18 }, "1030": { - "SpecialPoint": 200, + "SpecialPoint": 210, "subWeaponId": 10, "specialWeaponId": 4, "InkConsume_WeaponVerticalSwingParam": 0.12, @@ -513,7 +513,7 @@ "InkConsumeSlosher": 0.06 }, "3020": { - "SpecialPoint": 200, + "SpecialPoint": 210, "subWeaponId": 5, "specialWeaponId": 6, "MoveSpeed": 0.07, @@ -748,7 +748,7 @@ "InkConsumeFullCharge_ChargeParam": 0.085 }, "7020": { - "SpecialPoint": 200, + "SpecialPoint": 210, "subWeaponId": 6, "specialWeaponId": 4, "WeaponSpeedType": "Fast", @@ -881,7 +881,7 @@ }, "SubInkSaveLv": 1, "InkConsume": 0.6, - "InkRecoverStop": 70, + "InkRecoverStop": 85, "DistanceDamage_BlastParamArray": [ [ { diff --git a/app/routes/analyzer.tsx b/app/routes/analyzer.tsx index b9b89b763..32c1e3ea8 100644 --- a/app/routes/analyzer.tsx +++ b/app/routes/analyzer.tsx @@ -39,7 +39,7 @@ import { type SendouRouteHandle } from "~/utils/remix"; import { makeTitle } from "~/utils/strings"; import { specialWeaponImageUrl, subWeaponImageUrl } from "~/utils/urls"; -export const CURRENT_PATCH = "1.1"; +export const CURRENT_PATCH = "1.2"; export const meta: MetaFunction = () => { return { From 41120088e0fc031cbb0b9a4082f0d9d251b200a7 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 27 Oct 2022 09:27:12 +0300 Subject: [PATCH 04/21] Add Hydra fully charged shot damage to analyzer Closes #1046 --- app/modules/analyzer/constants.ts | 2 ++ app/modules/analyzer/stats.ts | 3 ++- app/modules/analyzer/types.ts | 1 + app/modules/analyzer/weapon-params.json | 1 + public/locales/en/analyzer.json | 1 + scripts/create-analyzer-json.ts | 4 ++++ 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/modules/analyzer/constants.ts b/app/modules/analyzer/constants.ts index ea90740d4..a1baf0cbf 100644 --- a/app/modules/analyzer/constants.ts +++ b/app/modules/analyzer/constants.ts @@ -23,6 +23,7 @@ export const DAMAGE_RECEIVERS = [ export const DAMAGE_TYPE = [ "NORMAL_MIN", "NORMAL_MAX", + "NORMAL_MAX_FULL_CHARGE", // Hydra Splatling goes from 32 to 40 dmg when fully charged "DIRECT", "FULL_CHARGE", "MAX_CHARGE", @@ -38,6 +39,7 @@ export const damageTypeToWeaponType: Record< > = { NORMAL_MIN: "MAIN", NORMAL_MAX: "MAIN", + NORMAL_MAX_FULL_CHARGE: "MAIN", DIRECT: "MAIN", FULL_CHARGE: "MAIN", MAX_CHARGE: "MAIN", diff --git a/app/modules/analyzer/stats.ts b/app/modules/analyzer/stats.ts index dbad75bfa..5f67f7fda 100644 --- a/app/modules/analyzer/stats.ts +++ b/app/modules/analyzer/stats.ts @@ -341,6 +341,7 @@ const damageTypeToParamsKey: Record< > = { NORMAL_MIN: "DamageParam_ValueMin", NORMAL_MAX: "DamageParam_ValueMax", + NORMAL_MAX_FULL_CHARGE: "DamageParam_ValueFullChargeMax", DIRECT: "DamageParam_ValueDirect", DISTANCE: "BlastParam_DistanceDamage", FULL_CHARGE: "DamageParam_ValueFullCharge", @@ -398,7 +399,7 @@ function shotsToSplat({ type: DamageType; isTripleShooter: boolean; }) { - if (type !== "NORMAL_MAX") return; + if (type !== "NORMAL_MAX" && type !== "NORMAL_MAX_FULL_CHARGE") return; const multiplier = isTripleShooter ? 3 : 1; diff --git a/app/modules/analyzer/types.ts b/app/modules/analyzer/types.ts index 7d6fe14f5..bedf7fe6b 100644 --- a/app/modules/analyzer/types.ts +++ b/app/modules/analyzer/types.ts @@ -33,6 +33,7 @@ export interface MainWeaponParams { MoveSpeedFullCharge?: number; DamageParam_ValueMax?: number; DamageParam_ValueMin?: number; + DamageParam_ValueFullChargeMax?: number; DamageParam_ValueDirect?: number; Jump_DegSwerve?: number; Stand_DegSwerve?: number; diff --git a/app/modules/analyzer/weapon-params.json b/app/modules/analyzer/weapon-params.json index e2ccd2823..172d0b5ba 100644 --- a/app/modules/analyzer/weapon-params.json +++ b/app/modules/analyzer/weapon-params.json @@ -597,6 +597,7 @@ "MoveSpeed_Charge": 0.04, "DamageParam_ValueMax": 320, "DamageParam_ValueMin": 160, + "DamageParam_ValueFullChargeMax": 400, "Jump_DegSwerve": 6, "Stand_DegSwerve": 3, "InkRecoverStop": 40, diff --git a/public/locales/en/analyzer.json b/public/locales/en/analyzer.json index 5034589c7..9cfcb8bc7 100644 --- a/public/locales/en/analyzer.json +++ b/public/locales/en/analyzer.json @@ -79,6 +79,7 @@ "damage.toSplat": "{{count}} hits to splat", "damage.NORMAL_MIN": "Minimum", "damage.NORMAL_MAX": "Maximum", + "damage.NORMAL_MAX_FULL_CHARGE": "Maximum (Fully charged)", "damage.DIRECT": "Direct", "damage.FULL_CHARGE": "Fully charged shot", "damage.MAX_CHARGE": "Maximum partial charge", diff --git a/scripts/create-analyzer-json.ts b/scripts/create-analyzer-json.ts index 75aab23cc..1933bb4d8 100644 --- a/scripts/create-analyzer-json.ts +++ b/scripts/create-analyzer-json.ts @@ -172,6 +172,10 @@ function parametersToMainWeaponResult( DamageParam_ValueDirect, BlastParam_DistanceDamage: params["BlastParam"]?.["DistanceDamage"], DamageParam_ValueFullCharge: params["DamageParam"]?.["ValueFullCharge"], + DamageParam_ValueFullChargeMax: + params["DamageParam"]?.["ValueFullChargeMax"] !== DamageParam_ValueMax() + ? params["DamageParam"]?.["ValueFullChargeMax"] + : undefined, DamageParam_ValueMaxCharge: params["DamageParam"]?.["ValueMaxCharge"], DamageParam_ValueMinCharge: params["DamageParam"]?.["ValueMinCharge"], CanopyHP: params["spl__BulletShelterCanopyParam"]?.["CanopyHP"], From e6c25967e9e49d7d93b46ed550e3be2fab6a1e46 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 27 Oct 2022 09:43:56 +0300 Subject: [PATCH 05/21] Analyzer add link to Obj DMG page when OS selected Closes #1039 --- app/routes/analyzer.tsx | 24 ++++++++++++++++++++++- app/routes/index.tsx | 4 ++-- app/styles/analyzer.css | 13 +++++++++++++ app/utils/urls.ts | 5 ++++- public/locales/en/analyzer.json | 1 + translation-progress.md | 34 +++++++++++++++++++++++---------- 6 files changed, 67 insertions(+), 14 deletions(-) diff --git a/app/routes/analyzer.tsx b/app/routes/analyzer.tsx index 32c1e3ea8..11e6fc99d 100644 --- a/app/routes/analyzer.tsx +++ b/app/routes/analyzer.tsx @@ -1,4 +1,5 @@ import { type LinksFunction, type MetaFunction } from "@remix-run/node"; +import { Link } from "@remix-run/react"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { AbilitiesSelector } from "~/components/AbilitiesSelector"; @@ -37,7 +38,12 @@ import styles from "~/styles/analyzer.css"; import { damageTypeTranslationString } from "~/utils/i18next"; import { type SendouRouteHandle } from "~/utils/remix"; import { makeTitle } from "~/utils/strings"; -import { specialWeaponImageUrl, subWeaponImageUrl } from "~/utils/urls"; +import { + navIconUrl, + objectDamageCalculatorPage, + specialWeaponImageUrl, + subWeaponImageUrl, +} from "~/utils/urls"; export const CURRENT_PATCH = "1.2"; @@ -69,6 +75,8 @@ export default function BuildAnalyzerPage() { effects, } = useAnalyzeBuild(); + const objectShredderSelected = build[2][0] === "OS"; + const mainWeaponCategoryItems = [ analyzed.stats.shotSpreadAir && ( + {objectShredderSelected && ( + + + {t("analyzer:objCalcAd")} + + )} diff --git a/app/routes/index.tsx b/app/routes/index.tsx index e10b66453..fb7c0b3cb 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -22,7 +22,7 @@ import { CALENDAR_PAGE, mapsPage, navIconUrl, - OBJECT_DAMAGE_CALCULATOR, + objectDamageCalculatorPage, plusSuggestionPage, userPage, } from "~/utils/urls"; @@ -80,7 +80,7 @@ export default function Index() { navItem="object-damage-calculator" title={t("common:pages.object-damage-calculator")} description={t("front:object-damage-calculator.description")} - to={OBJECT_DAMAGE_CALCULATOR} + to={objectDamageCalculatorPage()} /> + `/object-damage-calculator${ + typeof weaponId === "number" ? `?weapon=${weaponId}` : "" + }`; export const badgeUrl = ({ code, diff --git a/public/locales/en/analyzer.json b/public/locales/en/analyzer.json index 9cfcb8bc7..e6d5e9a2a 100644 --- a/public/locales/en/analyzer.json +++ b/public/locales/en/analyzer.json @@ -3,6 +3,7 @@ "attribute.weight.Fast": "Light", "attribute.weight.Slow": "Heavy", "attribute.weight.Normal": "Normal", + "objCalcAd": "For info on Object Shredder check out Object DMG Calc", "stat.category.main": "Main weapon", "stat.category.sub": "Sub weapon", "stat.category.special": "Special weapon", diff --git a/translation-progress.md b/translation-progress.md index fb499a8ba..aca22c713 100644 --- a/translation-progress.md +++ b/translation-progress.md @@ -4,13 +4,15 @@ ### 🟡 analyzer.json -**94/105** +**94/107**
Missing +- objCalcAd - stat.specialLostSplattedByRP - stat.quickRespawnTimeSplattedByRP +- damage.NORMAL_MAX_FULL_CHARGE - distanceInline - damageShort - hitsToDestroyLong @@ -79,13 +81,15 @@ ### 🟡 analyzer.json -**94/105** +**94/107**
Missing +- objCalcAd - stat.specialLostSplattedByRP - stat.quickRespawnTimeSplattedByRP +- damage.NORMAL_MAX_FULL_CHARGE - distanceInline - damageShort - hitsToDestroyLong @@ -155,16 +159,18 @@ ### 🟡 analyzer.json -**91/105** +**91/107**
Missing +- objCalcAd - stat.specialLostSplattedByRP - stat.quickRespawnTimeSplattedByRP - stat.shootingRunSpeed - stat.shootingRunSpeedCharging - stat.shootingRunSpeedFullCharge +- damage.NORMAL_MAX_FULL_CHARGE - distanceInline - damageShort - hitsToDestroyLong @@ -289,16 +295,18 @@ ### 🟡 analyzer.json -**91/105** +**91/107**
Missing +- objCalcAd - stat.specialLostSplattedByRP - stat.quickRespawnTimeSplattedByRP - stat.shootingRunSpeed - stat.shootingRunSpeedCharging - stat.shootingRunSpeedFullCharge +- damage.NORMAL_MAX_FULL_CHARGE - distanceInline - damageShort - hitsToDestroyLong @@ -435,7 +443,7 @@ ### 🔴 analyzer.json -**0/105** +**0/107** ### 🔴 badges.json @@ -490,11 +498,12 @@ ### 🟡 analyzer.json -**12/105** +**12/107**
Missing +- objCalcAd - stat.canopyHp - stat.fullChargeSeconds - stat.maxChargeHoldSeconds @@ -563,6 +572,7 @@ - damage.toSplat - damage.NORMAL_MIN - damage.NORMAL_MAX +- damage.NORMAL_MAX_FULL_CHARGE - damage.DIRECT - damage.FULL_CHARGE - damage.MAX_CHARGE @@ -714,7 +724,7 @@ ### 🔴 analyzer.json -**0/105** +**0/107** ### 🟢 badges.json @@ -850,13 +860,15 @@ ### 🟡 analyzer.json -**94/105** +**94/107**
Missing +- objCalcAd - stat.specialLostSplattedByRP - stat.quickRespawnTimeSplattedByRP +- damage.NORMAL_MAX_FULL_CHARGE - distanceInline - damageShort - hitsToDestroyLong @@ -969,7 +981,7 @@ ### 🔴 analyzer.json -**0/105** +**0/107** ### 🟢 badges.json @@ -1105,13 +1117,15 @@ ### 🟡 analyzer.json -**94/105** +**94/107**
Missing +- objCalcAd - stat.specialLostSplattedByRP - stat.quickRespawnTimeSplattedByRP +- damage.NORMAL_MAX_FULL_CHARGE - distanceInline - damageShort - hitsToDestroyLong From c7b4bb1f74af45111407a5e1c704dc11a38f60bc Mon Sep 17 00:00:00 2001 From: DoubleCookies <12849336+DoubleCookies@users.noreply.github.com> Date: Thu, 27 Oct 2022 10:33:44 +0300 Subject: [PATCH 06/21] updates --- public/locales/ru/analyzer.json | 9 ++++++++- public/locales/ru/common.json | 7 +++++++ public/locales/ru/user.json | 5 +++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/public/locales/ru/analyzer.json b/public/locales/ru/analyzer.json index e42dadead..2c6b34c58 100644 --- a/public/locales/ru/analyzer.json +++ b/public/locales/ru/analyzer.json @@ -92,5 +92,12 @@ "abilityPoints": "Свойства (AP)", "abilityPoints.short": "AP", "consumptionExplanation": "Данная таблица показывает, сколько действий можно совершить после 0-{{maxSubsToUse}}кратного использования запасного оружия. С полным баллоном запасное оружие можно использовать {{maxSubsToUse}} раз(а) подряд.", - "trackingSubDefExplanation": "В расчётах продолжительности маркировки Маркера Движения, Мины и Углострела используется противник с 0AP Стойкости Запаса." + "trackingSubDefExplanation": "В расчётах продолжительности маркировки Маркера Движения, Мины и Углострела используется противник с 0AP Стойкости Запаса.", + "distanceInline": "Расстояние: {{value}}", + "damageShort": "Урон", + "hitsToDestroyLong": "Атак для уничтожения", + "hitsToDestroyShort": "Атак", + "labels.amountOf": "Количество: ", + "labels.damageType": "Тип урона", + "labels.weapon": "Оружие" } diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index c8edb3932..b651052f8 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -9,11 +9,17 @@ "pages.builds": "Сборки", "pages.analyzer": "Анализатор сборок", "pages.maps": "Списки карт", + "pages.object-damage": "Калькулятор урона", "header.profile": "Профиль", "header.logout": "Выйти", "header.login": "Войти", + "auth.errors.aborted": "Вход отменён", + "auth.errors.failed": "Ошибка входа", + "auth.errors.discordPermissions": "Для вашего профиля на sendou.ink странице нужет доступ к вашему имени, аватару и привязанным аккаунтам соц. сетей в Discord.", + "auth.errors.unknown": "Вход через Discord не удался по неизвестной причине. Если это продолжается — обратитесь за помощью.", + "footer.github.subtitle": "Исходный код", "footer.twitter.subtitle": "Обновления", "footer.discord.subtitle": "Помощь и обратная связь", @@ -29,6 +35,7 @@ "actions.delete": "Удалить", "actions.loadMore": "Загрузить ещё", "actions.copyToClipboard": "Скопировать в буфер обмена", + "actions.close": "Закрыть", "maps.createMapList": "Создать список карт", "maps.halfSz": "50% Зон", diff --git a/public/locales/ru/user.json b/public/locales/ru/user.json index 4ceb740fc..7914dac8b 100644 --- a/public/locales/ru/user.json +++ b/public/locales/ru/user.json @@ -10,12 +10,17 @@ "stick": "Стик", "sens": "Чувствительность", + "results.title": "Результаты", "results.placing": "Место", "results.team": "Команда", "results.tournament": "Турнир", "results.participants": "Участники", "results.date": "Дата", "results.mates": "Напарники", + "results.highlights": "Избранное", + "results.nonHighlights": "Другие результаты", + "results.highlights.choose": "Выберите избранное", + "results.highlights.explanation": "Выберите ваш избранный результат", "forms.errors.invalidCustomUrl.numbers": "Пользовательский URL не может содержать только цифры", "forms.errors.invalidCustomUrl.strangeCharacter": "Пользовательский URL не может содержать особые символы", From de9aefa996bf60eb048d7c3612a53f9e17f4bbac Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Sun, 23 Oct 2022 16:12:49 +0200 Subject: [PATCH 07/21] Unify MapPoolSelector components --- .../components/MapPoolSelector.tsx | 2 +- app/routes/calendar/$id/index.tsx | 2 +- app/routes/calendar/new.tsx | 2 +- app/routes/maps.tsx | 68 +------------------ 4 files changed, 5 insertions(+), 69 deletions(-) rename app/{routes/calendar => }/components/MapPoolSelector.tsx (97%) diff --git a/app/routes/calendar/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx similarity index 97% rename from app/routes/calendar/components/MapPoolSelector.tsx rename to app/components/MapPoolSelector.tsx index 62215babf..a9c54813c 100644 --- a/app/routes/calendar/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -19,7 +19,7 @@ export function MapPoolSelector({ stageId: StageId; }) => void; }) { - const { t } = useTranslation(["game-misc", "calendar"]); + const { t } = useTranslation(["game-misc"]); const isPresentational = !handleMapPoolChange; diff --git a/app/routes/calendar/$id/index.tsx b/app/routes/calendar/$id/index.tsx index 714e686c8..4e2b27a66 100644 --- a/app/routes/calendar/$id/index.tsx +++ b/app/routes/calendar/$id/index.tsx @@ -39,7 +39,7 @@ import { userPage, } from "~/utils/urls"; import { actualNumber, id } from "~/utils/zod"; -import { MapPoolSelector } from "../components/MapPoolSelector"; +import { MapPoolSelector } from "~/components/MapPoolSelector"; import { Tags } from "../components/Tags"; export const links: LinksFunction = () => { diff --git a/app/routes/calendar/new.tsx b/app/routes/calendar/new.tsx index af102411e..0c5c850cd 100644 --- a/app/routes/calendar/new.tsx +++ b/app/routes/calendar/new.tsx @@ -58,7 +58,7 @@ import { safeJSONParse, toArray, } from "~/utils/zod"; -import { MapPoolSelector } from "./components/MapPoolSelector"; +import { MapPoolSelector } from "~/components/MapPoolSelector"; import { Tags } from "./components/Tags"; const MIN_DATE = new Date(Date.UTC(2015, 4, 28)); diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index ff152df1a..2cf364247 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -7,20 +7,17 @@ import type { import type { ShouldReloadFunction } from "@remix-run/react"; import { Link } from "@remix-run/react"; import { useLoaderData, useSearchParams } from "@remix-run/react"; -import clsx from "clsx"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useCopyToClipboard } from "react-use"; import invariant from "tiny-invariant"; import { Button } from "~/components/Button"; -import { Image } from "~/components/Image"; import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; import { Toggle } from "~/components/Toggle"; import { db } from "~/db"; import { i18next } from "~/modules/i18n"; import { - modes, stageIds, type ModeShort, type ModeWithStage, @@ -38,13 +35,9 @@ import { import type { MapPool } from "~/modules/map-pool-serializer/types"; import styles from "~/styles/maps.css"; import { makeTitle } from "~/utils/strings"; -import { - calendarEventPage, - ipLabsMaps, - modeImageUrl, - stageImageUrl, -} from "~/utils/urls"; +import { calendarEventPage, ipLabsMaps } from "~/utils/urls"; import { type SendouRouteHandle } from "~/utils/remix"; +import { MapPoolSelector } from "~/components/MapPoolSelector"; const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2; @@ -180,63 +173,6 @@ function useSearchParamMapPool() { }; } -function MapPoolSelector({ - mapPool, - handleMapPoolChange, -}: { - mapPool: MapPool; - handleMapPoolChange: (args: { mode: ModeShort; stageId: StageId }) => void; -}) { - const { t } = useTranslation(["game-misc"]); - - return ( -
- {stageIds.map((stageId) => ( -
- -
-
{t(`game-misc:STAGE_${stageId}`)}
-
- {modes.map((mode) => { - const selected = mapPool[mode.short].includes(stageId); - - return ( - - ); - })} -
-
-
- ))} -
- ); -} - function MapListCreator({ mapPool }: { mapPool: MapPool }) { const { t } = useTranslation(["game-misc", "common"]); const [mapList, setMapList] = React.useState(); From 3b18598288d2c91adedc96865c84fab1f83b81bb Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Tue, 25 Oct 2022 22:31:05 +0200 Subject: [PATCH 08/21] Introduce utility MapPool class --- app/components/MapPoolSelector.tsx | 6 +- app/db/models/calendar/queries.server.ts | 4 +- app/modules/map-list-generator/map-list.ts | 11 ++- app/modules/map-list-generator/utils.ts | 6 +- app/modules/map-pool-serializer/index.ts | 8 +- app/modules/map-pool-serializer/map-pool.ts | 87 +++++++++++++++++++ .../map-pool-serializer/serializer.test.ts | 10 +-- app/modules/map-pool-serializer/serializer.ts | 16 ++-- app/modules/map-pool-serializer/types.ts | 5 +- app/routes/calendar/$id/index.tsx | 3 +- app/routes/calendar/new.tsx | 47 ++++------ app/routes/maps.tsx | 38 ++++---- 12 files changed, 159 insertions(+), 82 deletions(-) create mode 100644 app/modules/map-pool-serializer/map-pool.ts diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index a9c54813c..6896fd97f 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -26,7 +26,7 @@ export function MapPoolSelector({ const stageRowIsVisible = (stageId: StageId) => { if (!isPresentational) return true; - return modes.some((mode) => mapPool[mode.short].includes(stageId)); + return mapPool.hasStage(stageId); }; return ( @@ -44,9 +44,7 @@ export function MapPoolSelector({
{t(`game-misc:STAGE_${stageId}`)}
{modes.map((mode) => { - const selected = (mapPool[mode.short] as StageId[]).includes( - stageId - ); + const selected = mapPool.parsed[mode.short].includes(stageId); if (isPresentational && !selected) return null; if (isPresentational && selected) { diff --git a/app/db/models/calendar/queries.server.ts b/app/db/models/calendar/queries.server.ts index 270333f38..60ad44186 100644 --- a/app/db/models/calendar/queries.server.ts +++ b/app/db/models/calendar/queries.server.ts @@ -11,7 +11,7 @@ import type { CalendarEventResultPlayer, MapPoolMap, } from "../../types"; -import { mapPoolListToMapPoolObject } from "~/modules/map-list-generator"; +import { MapPool } from "~/modules/map-pool-serializer"; import createSql from "./create.sql"; import updateSql from "./update.sql"; @@ -449,7 +449,7 @@ export function findMapPoolByEventId(calendarEventId: CalendarEvent["id"]) { if (rows.length === 0) return; - return mapPoolListToMapPoolObject(rows); + return MapPool.parse(rows); } const eventsToReportStm = sql.prepare(eventsToReportSql); diff --git a/app/modules/map-list-generator/map-list.ts b/app/modules/map-list-generator/map-list.ts index 58f890d97..346a513ae 100644 --- a/app/modules/map-list-generator/map-list.ts +++ b/app/modules/map-list-generator/map-list.ts @@ -6,14 +6,13 @@ import type { ModeWithStage, StageId, } from "~/modules/in-game-lists"; -import type { MapPool } from "~/modules/map-pool-serializer"; -import clone from "just-clone"; +import type { MapPool, MapPoolObject } from "~/modules/map-pool-serializer"; const BACKLOG = 2; export type Popularity = Map>; -type MapBucket = Map; +type MapBucket = Map; /** * @param mapPool Map pool to work with as dictionary @@ -79,7 +78,7 @@ function addAndReturnMap( TC: [], RM: [], CB: [], - } as MapPool); + } as MapPoolObject); } /* prettier-ignore */ @@ -97,7 +96,7 @@ function getMapPopular( ): StageId { const popularity_map_pool = new Map(); for (const [stageId, votes] of popularity.get(mode)!.entries()) { - if (mapPool[mode].includes(stageId)) { + if (mapPool.parsed[mode].includes(stageId)) { popularity_map_pool.set(stageId, votes); } } @@ -150,7 +149,7 @@ function getMap( mapHistory: StageId[] ) { if (!buckets.size) { - buckets.set(0, clone(mapPool)); + buckets.set(0, mapPool.getClonedObject()); } for (let bucketNum = 0; bucketNum < buckets.size; bucketNum++) { diff --git a/app/modules/map-list-generator/utils.ts b/app/modules/map-list-generator/utils.ts index f804b87c0..bca57fe53 100644 --- a/app/modules/map-list-generator/utils.ts +++ b/app/modules/map-list-generator/utils.ts @@ -1,11 +1,11 @@ import type { MapPoolMap } from "~/db/types"; import type { ModeShort } from "../in-game-lists"; -import type { MapPool } from "../map-pool-serializer"; +import type { MapPool, MapPoolObject } from "../map-pool-serializer"; export function mapPoolToNonEmptyModes(mapPool: MapPool) { const result: ModeShort[] = []; - for (const [key, stages] of Object.entries(mapPool)) { + for (const [key, stages] of Object.entries(mapPool.parsed)) { if (stages.length === 0) continue; result.push(key as ModeShort); @@ -17,7 +17,7 @@ export function mapPoolToNonEmptyModes(mapPool: MapPool) { export function mapPoolListToMapPoolObject( mapPoolList: Array> ) { - const result: MapPool = { + const result: MapPoolObject = { TW: [], SZ: [], TC: [], diff --git a/app/modules/map-pool-serializer/index.ts b/app/modules/map-pool-serializer/index.ts index 79678884e..1a8ce98ee 100644 --- a/app/modules/map-pool-serializer/index.ts +++ b/app/modules/map-pool-serializer/index.ts @@ -1,6 +1,2 @@ -export { - mapPoolToSerializedString, - serializedStringToMapPool, -} from "./serializer"; - -export type { MapPool } from "./types"; +export { MapPool } from "./map-pool"; +export type { MapPoolObject } from "./types"; diff --git a/app/modules/map-pool-serializer/map-pool.ts b/app/modules/map-pool-serializer/map-pool.ts new file mode 100644 index 000000000..8c26c654b --- /dev/null +++ b/app/modules/map-pool-serializer/map-pool.ts @@ -0,0 +1,87 @@ +import { + mapPoolToSerializedString, + serializedStringToMapPool, +} from "./serializer"; +import type { ReadonlyMapPoolObject, MapPoolObject } from "./types"; +import clone from "just-clone"; +import type { MapPoolMap } from "~/db/types"; +import { mapPoolListToMapPoolObject } from "~/modules/map-list-generator"; +import type { ModeShort, StageId } from "~/modules/in-game-lists"; + +type DbMapPoolList = Array>; + +export class MapPool { + private source: string | ReadonlyMapPoolObject; + private asSerialized?: string; + private asObject?: ReadonlyMapPoolObject; + + constructor(init: ReadonlyMapPoolObject | string | DbMapPoolList) { + this.source = Array.isArray(init) ? mapPoolListToMapPoolObject(init) : init; + } + + static serialize(init: ReadonlyMapPoolObject | string | DbMapPoolList) { + return new MapPool(init).serialized; + } + + static parse(init: MapPoolObject | string | DbMapPoolList) { + return new MapPool(init).parsed; + } + + static toDbList(init: MapPoolObject | string | DbMapPoolList) { + return new MapPool(init).dbList; + } + + get serialized(): string { + if (this.asSerialized !== undefined) { + return this.asSerialized; + } + + return (this.asSerialized = + typeof this.source === "string" + ? this.source + : mapPoolToSerializedString(this.source)); + } + + get parsed(): ReadonlyMapPoolObject { + if (this.asObject !== undefined) { + return this.asObject; + } + + return (this.asObject = + typeof this.source === "string" + ? serializedStringToMapPool(this.source) + : this.source); + } + + get dbList(): DbMapPoolList { + return Object.entries(this.parsed).flatMap(([mode, stages]) => + stages.flatMap((stageId) => ({ mode: mode as ModeShort, stageId })) + ); + } + + hasMode(mode: ModeShort): boolean { + return this.parsed[mode].length > 0; + } + + hasStage(stageId: StageId): boolean { + return Object.values(this.parsed).some((stages) => + stages.includes(stageId) + ); + } + + isEmpty(): boolean { + return Object.values(this.parsed).every((stages) => stages.length === 0); + } + + getClonedObject(): MapPoolObject { + return clone(this.parsed) as MapPoolObject; + } + + toString() { + return this.serialized; + } + + toJSON() { + return this.parsed; + } +} diff --git a/app/modules/map-pool-serializer/serializer.test.ts b/app/modules/map-pool-serializer/serializer.test.ts index 6487db55e..5d0b5aab0 100644 --- a/app/modules/map-pool-serializer/serializer.test.ts +++ b/app/modules/map-pool-serializer/serializer.test.ts @@ -4,7 +4,7 @@ import { mapPoolToSerializedString, serializedStringToMapPool, } from "./serializer"; -import type { MapPool } from "./types"; +import type { MapPoolObject } from "./types"; const Serializer = suite("Map pool serializer"); @@ -24,7 +24,7 @@ Serializer("Ignores invalid mode key", () => { }); Serializer("Matching serialization with IPLMapGen2", () => { - const testMapPool: MapPool = { + const testMapPool: MapPoolObject = { TW: [0, 3, 4, 7, 8], SZ: [0, 1, 3, 8, 10], TC: [1, 2, 5, 8, 9], @@ -36,7 +36,7 @@ Serializer("Matching serialization with IPLMapGen2", () => { }); Serializer("Omits key if mode has no maps", () => { - const testPoolWithoutTw: MapPool = { + const testPoolWithoutTw: MapPoolObject = { CB: [1, 2], RM: [1, 8], TC: [8, 4], @@ -50,7 +50,7 @@ Serializer("Omits key if mode has no maps", () => { }); Serializer("Returns empty string if no maps", () => { - const testPoolWithoutTw: MapPool = { + const testPoolWithoutTw: MapPoolObject = { CB: [], RM: [], TC: [], @@ -64,7 +64,7 @@ Serializer("Returns empty string if no maps", () => { }); Serializer("Value of two modes is the same with same maps", () => { - const testPoolWithDuplicateMaps: MapPool = { + const testPoolWithDuplicateMaps: MapPoolObject = { CB: [1, 2], RM: [1, 2], TC: [], diff --git a/app/modules/map-pool-serializer/serializer.ts b/app/modules/map-pool-serializer/serializer.ts index db2b47c01..c6bde377b 100644 --- a/app/modules/map-pool-serializer/serializer.ts +++ b/app/modules/map-pool-serializer/serializer.ts @@ -1,8 +1,10 @@ import invariant from "tiny-invariant"; import { modesShort, type StageId, stageIds } from "../in-game-lists"; -import type { MapPool } from "./types"; +import type { MapPoolObject, ReadonlyMapPoolObject } from "./types"; -export function mapPoolToSerializedString(mapPool: MapPool): string { +export function mapPoolToSerializedString( + mapPool: ReadonlyMapPoolObject +): string { const serializedModes = []; for (const mode of modesShort) { @@ -15,7 +17,7 @@ export function mapPoolToSerializedString(mapPool: MapPool): string { return serializedModes.join(";").toLowerCase(); } -function stageIdsToBinary(input: StageId[]) { +function stageIdsToBinary(input: readonly StageId[]) { let result = "1"; for (const stageId of stageIds) { @@ -33,8 +35,10 @@ function binaryToHex(binary: string) { return parseInt(binary, 2).toString(16); } -export function serializedStringToMapPool(serialized: string) { - const result: MapPool = { +export function serializedStringToMapPool( + serialized: string +): ReadonlyMapPoolObject { + const result: MapPoolObject = { SZ: [], CB: [], RM: [], @@ -58,7 +62,7 @@ export function serializedStringToMapPool(serialized: string) { return result; } -function binaryToStageIds(binary: string): StageId[] { +function binaryToStageIds(binary: string): readonly StageId[] { const result: StageId[] = []; // first 1 is padding diff --git a/app/modules/map-pool-serializer/types.ts b/app/modules/map-pool-serializer/types.ts index c3e416dc0..a67d67034 100644 --- a/app/modules/map-pool-serializer/types.ts +++ b/app/modules/map-pool-serializer/types.ts @@ -1,4 +1,7 @@ import type { ModeShort } from "../in-game-lists"; import type { StageId } from "../in-game-lists"; -export type MapPool = Record; +export type MapPoolObject = Record; +export type ReadonlyMapPoolObject = Readonly< + Record +>; diff --git a/app/routes/calendar/$id/index.tsx b/app/routes/calendar/$id/index.tsx index 4e2b27a66..bc9db4ffe 100644 --- a/app/routes/calendar/$id/index.tsx +++ b/app/routes/calendar/$id/index.tsx @@ -41,6 +41,7 @@ import { import { actualNumber, id } from "~/utils/zod"; import { MapPoolSelector } from "~/components/MapPoolSelector"; import { Tags } from "../components/Tags"; +import { MapPool } from "~/modules/map-pool-serializer"; export const links: LinksFunction = () => { return [ @@ -244,7 +245,7 @@ function MapPoolInfo() { return (
- + { const deserializedMaps = (() => { if (!data.pool) return; - const mapPool = serializedStringToMapPool(data.pool); - return Object.entries(mapPool).flatMap(([mode, stages]) => - stages.flatMap((stageId) => ({ mode: mode as ModeShort, stageId })) - ); + return MapPool.toDbList(data.pool); })(); if (data.eventToEditId) { @@ -526,22 +519,22 @@ function BadgesAdder() { ); } -const DEFAULT_MAP_POOL = { +const DEFAULT_MAP_POOL = new MapPool({ SZ: [], TC: [], CB: [], RM: [], TW: [], -}; +}); function MapPoolSection() { const { t } = useTranslation(["game-misc", "calendar"]); - const data = useLoaderData(); + const { eventToEdit } = useLoaderData(); const [mapPool, setMapPool] = React.useState( - data.eventToEdit?.mapPool ?? DEFAULT_MAP_POOL + eventToEdit?.mapPool ? new MapPool(eventToEdit.mapPool) : DEFAULT_MAP_POOL ); const [includeMapPool, setIncludeMapPool] = React.useState( - Boolean(data.eventToEdit?.mapPool) + Boolean(eventToEdit?.mapPool) ); const handleMapPoolChange = ({ @@ -551,15 +544,17 @@ function MapPoolSection() { mode: ModeShort; stageId: StageId; }) => { - const newMapPool = mapPool[mode].includes(stageId) - ? { - ...mapPool, - [mode]: mapPool[mode].filter((id) => id !== stageId), - } - : { - ...mapPool, - [mode]: [...mapPool[mode], stageId], - }; + const newMapPool = new MapPool( + mapPool.parsed[mode].includes(stageId) + ? { + ...mapPool.parsed, + [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), + } + : { + ...mapPool.parsed, + [mode]: [...mapPool.parsed[mode], stageId], + } + ); setMapPool(newMapPool); }; @@ -567,11 +562,7 @@ function MapPoolSection() { return (
{includeMapPool && ( - + )}
diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index 2cf364247..61eb662cc 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -28,11 +28,7 @@ import { mapPoolToNonEmptyModes, modesOrder, } from "~/modules/map-list-generator"; -import { - mapPoolToSerializedString, - serializedStringToMapPool, -} from "~/modules/map-pool-serializer"; -import type { MapPool } from "~/modules/map-pool-serializer/types"; +import { MapPool } from "~/modules/map-pool-serializer"; import styles from "~/styles/maps.css"; import { makeTitle } from "~/utils/strings"; import { calendarEventPage, ipLabsMaps } from "~/utils/urls"; @@ -85,13 +81,13 @@ export const loader = async ({ request }: LoaderArgs) => { }; }; -const DEFAULT_MAP_POOL = { +const DEFAULT_MAP_POOL = new MapPool({ SZ: [...stageIds], TC: [...stageIds], CB: [...stageIds], RM: [...stageIds], TW: [], -}; +}); export default function MapListPage() { const { t } = useTranslation(["common"]); @@ -114,7 +110,7 @@ export default function MapListPage() { handleMapPoolChange={handleMapPoolChange} /> { if (searchParams.has("pool")) { - return serializedStringToMapPool(searchParams.get("pool")!); + return new MapPool(searchParams.get("pool")!); } if (data?.mapPool) { - return data.mapPool; + return new MapPool(data.mapPool); } return DEFAULT_MAP_POOL; @@ -149,19 +145,21 @@ function useSearchParamMapPool() { mode: ModeShort; stageId: StageId; }) => { - const newMapPool = mapPool[mode].includes(stageId) - ? { - ...mapPool, - [mode]: mapPool[mode].filter((id) => id !== stageId), - } - : { - ...mapPool, - [mode]: [...mapPool[mode], stageId], - }; + const newMapPool = new MapPool( + mapPool.parsed[mode].includes(stageId) + ? { + ...mapPool.parsed, + [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), + } + : { + ...mapPool.parsed, + [mode]: [...mapPool.parsed[mode], stageId], + } + ); setSearchParams( { - pool: mapPoolToSerializedString(newMapPool), + pool: newMapPool.serialized, }, { replace: true, state: { scroll: false } } ); From edd3a1d91b35fc7768c9a7e1e03c77b30c1bc8c4 Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Tue, 25 Oct 2022 22:54:58 +0200 Subject: [PATCH 09/21] Improve MapPoolSelector UI --- app/components/MapPoolSelector.tsx | 160 ++++++++++++++++++-- app/components/icons/ArrowLongLeft.tsx | 18 +++ app/hooks/useOnce.ts | 10 ++ app/modules/in-game-lists/modes.ts | 25 +-- app/modules/map-pool-serializer/map-pool.ts | 22 ++- app/routes/calendar/$id/index.tsx | 8 +- app/routes/calendar/new.tsx | 70 +++------ app/routes/maps.tsx | 119 ++++++++------- app/styles/calendar-new.css | 2 +- app/styles/maps.css | 40 +++-- app/styles/utils.css | 4 + app/styles/vars.css | 5 + app/utils/urls.ts | 10 +- public/locales/de/common.json | 2 + public/locales/de/game-misc.json | 7 +- public/locales/en/common.json | 2 + public/locales/en/game-misc.json | 7 +- 17 files changed, 355 insertions(+), 156 deletions(-) create mode 100644 app/components/icons/ArrowLongLeft.tsx create mode 100644 app/hooks/useOnce.ts diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index 6896fd97f..a4a495691 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -3,23 +3,73 @@ import { useTranslation } from "react-i18next"; import { Image } from "~/components/Image"; import type { ModeShort, StageId } from "~/modules/in-game-lists"; import { modes, stageIds } from "~/modules/in-game-lists"; -import { type MapPool } from "~/modules/map-pool-serializer"; +import { MapPool } from "~/modules/map-pool-serializer"; import { modeImageUrl, stageImageUrl } from "~/utils/urls"; +import { Button } from "~/components/Button"; +import { CrossIcon } from "./icons/Cross"; +import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; +import * as React from "react"; + +export type MapPoolSelectorProps = { + mapPool: MapPool; + handleRemoval?: () => void; + handleMapPoolChange: (mapPool: MapPool) => void; + className?: string; +}; export function MapPoolSelector({ mapPool, handleMapPoolChange, -}: { + handleRemoval, + className, +}: MapPoolSelectorProps) { + const { t } = useTranslation(); + + const handleStageModesChange = (newMapPool: MapPool) => { + handleMapPoolChange(newMapPool); + }; + + const handleClear = () => { + handleMapPoolChange(MapPool.EMPTY); + }; + + return ( +
+ {t("maps.mapPool")} +
+
+ {handleRemoval && ( + + )} + +
+ +
+
+ ); +} + +export type MapPoolStagesProps = { mapPool: MapPool; - handleMapPoolChange?: ({ - mode, - stageId, - }: { - mode: ModeShort; - stageId: StageId; - }) => void; -}) { - const { t } = useTranslation(["game-misc"]); + handleMapPoolChange?: (newMapPool: MapPool) => void; +}; + +export function MapPoolStages({ + mapPool, + handleMapPoolChange, +}: MapPoolStagesProps) { + const { t } = useTranslation(["game-misc", "common"]); const isPresentational = !handleMapPoolChange; @@ -29,6 +79,52 @@ export function MapPoolSelector({ return mapPool.hasStage(stageId); }; + const handleModeChange = ({ + mode, + stageId, + }: { + mode: ModeShort; + stageId: StageId; + }) => { + const newMapPool = mapPool.parsed[mode].includes(stageId) + ? new MapPool({ + ...mapPool.parsed, + [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), + }) + : new MapPool({ + ...mapPool.parsed, + [mode]: [...mapPool.parsed[mode], stageId], + }); + + handleMapPoolChange?.(newMapPool); + }; + + const handleStageClear = (stageId: StageId) => { + const newMapPool = new MapPool({ + TW: mapPool.parsed.TW.filter((id) => id !== stageId), + SZ: mapPool.parsed.SZ.filter((id) => id !== stageId), + TC: mapPool.parsed.TC.filter((id) => id !== stageId), + RM: mapPool.parsed.RM.filter((id) => id !== stageId), + CB: mapPool.parsed.CB.filter((id) => id !== stageId), + }); + + handleMapPoolChange?.(newMapPool); + }; + + const handleStageAdd = (stageId: StageId) => { + const newMapPool = new MapPool({ + TW: [...mapPool.parsed.TW, stageId], + SZ: [...mapPool.parsed.SZ, stageId], + TC: [...mapPool.parsed.TC, stageId], + RM: [...mapPool.parsed.RM, stageId], + CB: [...mapPool.parsed.CB, stageId], + }); + + handleMapPoolChange?.(newMapPool); + }; + + const id = React.useId(); + return (
{stageIds.filter(stageRowIsVisible).map((stageId) => ( @@ -40,8 +136,14 @@ export function MapPoolSelector({ width={80} height={45} /> -
-
{t(`game-misc:STAGE_${stageId}`)}
+
+
+ {t(`game-misc:STAGE_${stageId}`)} +
{modes.map((mode) => { const selected = mapPool.parsed[mode.short].includes(stageId); @@ -54,7 +156,8 @@ export function MapPoolSelector({ className={clsx("maps__mode", { selected, })} - alt={mode.long} + title={t(`game-misc:MODE_LONG_${mode.short}`)} + alt={t(`game-misc:MODE_LONG_${mode.short}`)} path={modeImageUrl(mode.short)} width={33} height={33} @@ -69,15 +172,18 @@ export function MapPoolSelector({ selected, })} onClick={() => - handleMapPoolChange?.({ mode: mode.short, stageId }) + handleModeChange?.({ mode: mode.short, stageId }) } type="button" + title={t(`game-misc:MODE_LONG_${mode.short}`)} + aria-describedby={`${id}-stage-name-${stageId}`} + aria-pressed={selected} > {mode.long} ); })} + {!isPresentational && + (mapPool.hasStage(stageId) ? ( +
diff --git a/app/components/icons/ArrowLongLeft.tsx b/app/components/icons/ArrowLongLeft.tsx new file mode 100644 index 000000000..b419e99ea --- /dev/null +++ b/app/components/icons/ArrowLongLeft.tsx @@ -0,0 +1,18 @@ +export function ArrowLongLeftIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/app/hooks/useOnce.ts b/app/hooks/useOnce.ts new file mode 100644 index 000000000..4296bf40a --- /dev/null +++ b/app/hooks/useOnce.ts @@ -0,0 +1,10 @@ +import { useMemo } from "react"; + +/** + * Utility hook for calling `useMemo(f, [])`, when you're sure it needs no + * revalidation but feel bad for getting shamed by eslint everytime :D + */ +export function useOnce(factory: () => T) { + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(factory, []); +} diff --git a/app/modules/in-game-lists/modes.ts b/app/modules/in-game-lists/modes.ts index 62736c388..d44939f0f 100644 --- a/app/modules/in-game-lists/modes.ts +++ b/app/modules/in-game-lists/modes.ts @@ -1,24 +1,9 @@ export const modes = [ - { - short: "TW", - long: "Turf War", - }, - { - short: "SZ", - long: "Splat Zones", - }, - { - short: "TC", - long: "Tower Control", - }, - { - short: "RM", - long: "Rainmaker", - }, - { - short: "CB", - long: "Clam Blitz", - }, + { short: "TW" }, + { short: "SZ" }, + { short: "TC" }, + { short: "RM" }, + { short: "CB" }, ] as const; export const modesShort = modes.map((mode) => mode.short); diff --git a/app/modules/map-pool-serializer/map-pool.ts b/app/modules/map-pool-serializer/map-pool.ts index 8c26c654b..8639d5416 100644 --- a/app/modules/map-pool-serializer/map-pool.ts +++ b/app/modules/map-pool-serializer/map-pool.ts @@ -6,7 +6,11 @@ import type { ReadonlyMapPoolObject, MapPoolObject } from "./types"; import clone from "just-clone"; import type { MapPoolMap } from "~/db/types"; import { mapPoolListToMapPoolObject } from "~/modules/map-list-generator"; -import type { ModeShort, StageId } from "~/modules/in-game-lists"; +import { + type ModeShort, + type StageId, + stageIds, +} from "~/modules/in-game-lists"; type DbMapPoolList = Array>; @@ -84,4 +88,20 @@ export class MapPool { toJSON() { return this.parsed; } + + static EMPTY = new MapPool({ + SZ: [], + TC: [], + CB: [], + RM: [], + TW: [], + }); + + static ANARCHY = new MapPool({ + SZ: [...stageIds], + TC: [...stageIds], + CB: [...stageIds], + RM: [...stageIds], + TW: [], + }); } diff --git a/app/routes/calendar/$id/index.tsx b/app/routes/calendar/$id/index.tsx index bc9db4ffe..28b2b8c69 100644 --- a/app/routes/calendar/$id/index.tsx +++ b/app/routes/calendar/$id/index.tsx @@ -33,13 +33,13 @@ import { discordFullName, makeTitle } from "~/utils/strings"; import { calendarEditPage, calendarReportWinnersPage, - mapsPage, navIconUrl, + readonlyMapsPage, resolveBaseUrl, userPage, } from "~/utils/urls"; import { actualNumber, id } from "~/utils/zod"; -import { MapPoolSelector } from "~/components/MapPoolSelector"; +import { MapPoolStages } from "~/components/MapPoolSelector"; import { Tags } from "../components/Tags"; import { MapPool } from "~/modules/map-pool-serializer"; @@ -245,10 +245,10 @@ function MapPoolInfo() { return (
- + diff --git a/app/routes/calendar/new.tsx b/app/routes/calendar/new.tsx index b19e1076d..5357ffadb 100644 --- a/app/routes/calendar/new.tsx +++ b/app/routes/calendar/new.tsx @@ -20,14 +20,12 @@ import { TrashIcon } from "~/components/icons/Trash"; import { Input } from "~/components/Input"; import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; -import { Toggle } from "~/components/Toggle"; import { CALENDAR_EVENT } from "~/constants"; import { db } from "~/db"; import type { Badge as BadgeType, CalendarEventTag } from "~/db/types"; import { useIsMounted } from "~/hooks/useIsMounted"; import { requireUser } from "~/modules/auth"; import { i18next } from "~/modules/i18n"; -import type { ModeShort, StageId } from "~/modules/in-game-lists"; import { MapPool } from "~/modules/map-pool-serializer"; import { canEditCalendarEvent } from "~/permissions"; import calendarNewStyles from "~/styles/calendar-new.css"; @@ -519,61 +517,41 @@ function BadgesAdder() { ); } -const DEFAULT_MAP_POOL = new MapPool({ - SZ: [], - TC: [], - CB: [], - RM: [], - TW: [], -}); function MapPoolSection() { - const { t } = useTranslation(["game-misc", "calendar"]); + const { t } = useTranslation(["game-misc", "common"]); const { eventToEdit } = useLoaderData(); const [mapPool, setMapPool] = React.useState( - eventToEdit?.mapPool ? new MapPool(eventToEdit.mapPool) : DEFAULT_MAP_POOL + eventToEdit?.mapPool ? new MapPool(eventToEdit.mapPool) : MapPool.EMPTY ); const [includeMapPool, setIncludeMapPool] = React.useState( Boolean(eventToEdit?.mapPool) ); - const handleMapPoolChange = ({ - mode, - stageId, - }: { - mode: ModeShort; - stageId: StageId; - }) => { - const newMapPool = new MapPool( - mapPool.parsed[mode].includes(stageId) - ? { - ...mapPool.parsed, - [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), - } - : { - ...mapPool.parsed, - [mode]: [...mapPool.parsed[mode], stageId], - } - ); + const id = React.useId(); - setMapPool(newMapPool); - }; + return includeMapPool ? ( + <> + - return ( -
- {includeMapPool && ( - - )} - -
- - {includeMapPool && ( - - )} -
+ setIncludeMapPool(false)} + handleMapPoolChange={setMapPool} + /> + + ) : ( +
+ +
); } diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index 61eb662cc..1c63249ea 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -17,12 +17,7 @@ import { Main } from "~/components/Main"; import { Toggle } from "~/components/Toggle"; import { db } from "~/db"; import { i18next } from "~/modules/i18n"; -import { - stageIds, - type ModeShort, - type ModeWithStage, - type StageId, -} from "~/modules/in-game-lists"; +import { stageIds, type ModeWithStage } from "~/modules/in-game-lists"; import { generateMapList, mapPoolToNonEmptyModes, @@ -33,11 +28,18 @@ import styles from "~/styles/maps.css"; import { makeTitle } from "~/utils/strings"; import { calendarEventPage, ipLabsMaps } from "~/utils/urls"; import { type SendouRouteHandle } from "~/utils/remix"; -import { MapPoolSelector } from "~/components/MapPoolSelector"; +import { MapPoolSelector, MapPoolStages } from "~/components/MapPoolSelector"; +import { EditIcon } from "~/components/icons/Edit"; +import { useOnce } from "~/hooks/useOnce"; const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2; -export const unstable_shouldReload: ShouldReloadFunction = () => false; +export const unstable_shouldReload: ShouldReloadFunction = ({ url }) => { + const searchParams = new URL(url).searchParams; + // Only let loader reload data if we're not currently editing the map pool + // and persisting it in the search params. + return searchParams.has("readonly"); +}; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; @@ -81,34 +83,43 @@ export const loader = async ({ request }: LoaderArgs) => { }; }; -const DEFAULT_MAP_POOL = new MapPool({ - SZ: [...stageIds], - TC: [...stageIds], - CB: [...stageIds], - RM: [...stageIds], - TW: [], -}); - export default function MapListPage() { const { t } = useTranslation(["common"]); const data = useLoaderData(); const [searchParams] = useSearchParams(); - const { mapPool, handleMapPoolChange } = useSearchParamMapPool(); + const { mapPool, handleMapPoolChange, readonly, switchToEditMode } = + useSearchParamPersistedMapPool(); return (
- {data.calendarEvent && !searchParams.has("pool") && ( -
- {t("common:maps.mapPool")}:{" "} - - {data.calendarEvent.name} - + {searchParams.has("readonly") && data.calendarEvent && ( +
+
+ {t("common:maps.mapPool")}:{" "} + { + + {data.calendarEvent.name} + + } +
+
)} - + {readonly ? ( + + ) : ( + + )}
(); const [searchParams, setSearchParams] = useSearchParams(); - const mapPool = (() => { + const initialMapPool = useOnce(() => { if (searchParams.has("pool")) { return new MapPool(searchParams.get("pool")!); } - if (data?.mapPool) { + if (data.mapPool) { return new MapPool(data.mapPool); } - return DEFAULT_MAP_POOL; - })(); + return MapPool.ANARCHY; + }); - const handleMapPoolChange = ({ - mode, - stageId, - }: { - mode: ModeShort; - stageId: StageId; - }) => { - const newMapPool = new MapPool( - mapPool.parsed[mode].includes(stageId) - ? { - ...mapPool.parsed, - [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), - } - : { - ...mapPool.parsed, - [mode]: [...mapPool.parsed[mode], stageId], - } - ); + const [mapPool, setMapPool] = React.useState(initialMapPool); + const handleMapPoolChange = (newMapPool: MapPool) => { + setMapPool(newMapPool); setSearchParams( { pool: newMapPool.serialized, @@ -165,9 +161,20 @@ function useSearchParamMapPool() { ); }; + const switchToEditMode = () => { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.delete("readonly"); + setSearchParams(newSearchParams, { + replace: false, + state: { scroll: false }, + }); + }; + return { mapPool, + readonly: searchParams.has("readonly"), handleMapPoolChange, + switchToEditMode, }; } @@ -192,13 +199,16 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) { setMapList(list); }; + const disabled = + mapPool.isEmpty() || (szEveryOther && !mapPool.hasMode("SZ")); + return (
- {mapList && ( @@ -206,7 +216,12 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
    {mapList.map(({ mode, stageId }, i) => (
  1. - {t(`game-misc:MODE_SHORT_${mode}`)}{" "} + + {t(`game-misc:MODE_SHORT_${mode}`)} + {" "} {t(`game-misc:STAGE_${stageId}`)}
  2. ))} diff --git a/app/styles/calendar-new.css b/app/styles/calendar-new.css index 291f6adc1..fdf61d77f 100644 --- a/app/styles/calendar-new.css +++ b/app/styles/calendar-new.css @@ -1,5 +1,5 @@ .calendar-new__container { - max-width: 32rem; + max-width: 38rem; } .calendar-new__select { diff --git a/app/styles/maps.css b/app/styles/maps.css index cc680c1d4..5d064d305 100644 --- a/app/styles/maps.css +++ b/app/styles/maps.css @@ -1,9 +1,19 @@ .maps__container { - max-width: 32rem; + max-width: 38rem; +} + +.maps__pool-meta { + display: flex; + align-items: center; + justify-content: space-between; } .maps__pool-info { - font-size: var(--fonts-xxs); + font-size: var(--fonts-xs); + font-weight: var(--bold); +} + +.maps__pool-info a { font-weight: var(--semi-bold); } @@ -49,29 +59,27 @@ .maps__mode-button { padding: 0; - padding: var(--s-1-5); - border: none; - border-radius: var(--rounded); - background-color: var(--bg-darker); + padding: var(--s-1); + border: 2px solid var(--bg-darker); + border-radius: var(--rounded-full); + background-color: transparent; color: var(--theme); opacity: 1 !important; outline: initial; } .maps__mode-button.selected { - background-color: var(--theme-very-transparent); + border: 2px solid transparent; + background-color: var(--bg-mode-active); } .maps__stage-image { border-radius: var(--rounded); } -.maps__mode { - filter: grayscale(100%); -} - -.maps__mode.selected { - filter: unset; +.maps__mode:not(.selected) { + filter: var(--inactive-image-filter); + opacity: 0.6; } .maps__map-list-creator { @@ -96,3 +104,9 @@ font-weight: var(--semi-bold); margin-block-start: var(--s-4); } + +.maps__mode-abbr { + color: var(--text-lighter); + font-weight: var(--bold); + text-decoration: none; +} diff --git a/app/styles/utils.css b/app/styles/utils.css index fef6c85f5..f9b542e43 100644 --- a/app/styles/utils.css +++ b/app/styles/utils.css @@ -126,6 +126,10 @@ justify-content: center; } +.justify-end { + justify-content: flex-end; +} + .flex-wrap { flex-wrap: wrap; } diff --git a/app/styles/vars.css b/app/styles/vars.css index 71e346065..c626fc365 100644 --- a/app/styles/vars.css +++ b/app/styles/vars.css @@ -7,6 +7,7 @@ html { --bg-darker-transparent: hsla(202deg 90% 90% / 65%); --bg-ability: rgb(3 6 7); --bg-badge: #000; + --bg-mode-active: hsl(255deg 66.7% 50% / 40%); --abilities-button-bg: hsl(237deg 32% 30%); --badge-text: rgb(255 255 255 / 95%); --border: hsl(237deg 100% 86%); @@ -31,6 +32,7 @@ html { --theme-semi-transparent-vibrant: hsl(255deg 100% 81% / 75%); --theme-secondary: hsl(85deg 66.7% 55.3%); --rounded: 16px; + --rounded-full: 200px; --rounded-sm: 10px; --fonts-xl: 1.5rem; --fonts-lg: 1.2rem; @@ -73,6 +75,7 @@ html { --s-96: 2rem; --sparse: 0.4px; --label-margin: var(--s-1); + --inactive-image-filter: grayscale(100%) brightness(30%); } html.dark { @@ -84,6 +87,7 @@ html.dark { --bg-darker-transparent: hsla(237.3deg 42.3% 26.6% / 90%); --bg-ability: rgb(17 19 43); --bg-badge: #000; + --bg-mode-active: var(--theme-transparent); --abilities-button-bg: hsl(237.3deg 42.3% 26.6%); --border: hsl(237.3deg 42.3% 45.6%); --button-text: rgb(0 0 0 / 85%); @@ -104,6 +108,7 @@ html.dark { --theme-transparent-vibrant: hsl(255deg 78% 65% / 54%); --theme-semi-transparent-vibrant: hsl(255deg 78% 65% / 75%); --theme-secondary: hsl(85deg 66.7% 55.3%); + --inactive-image-filter: grayscale(100%) brightness(130%); } html.dark .light-mode-only { diff --git a/app/utils/urls.ts b/app/utils/urls.ts index bef1ce4dd..bb77c5894 100644 --- a/app/utils/urls.ts +++ b/app/utils/urls.ts @@ -1,5 +1,11 @@ import slugify from "slugify"; -import type { Badge, GearType, MapPoolMap, User } from "~/db/types"; +import type { + Badge, + CalendarEvent, + GearType, + MapPoolMap, + User, +} from "~/db/types"; import type { ModeShort, weaponCategories } from "~/modules/in-game-lists"; import type { Ability, @@ -74,6 +80,8 @@ export const calendarReportWinnersPage = (eventId: number) => `/calendar/${eventId}/report-winners`; export const mapsPage = (eventId?: MapPoolMap["calendarEventId"]) => `/maps${eventId ? `?eventId=${eventId}` : ""}`; +export const readonlyMapsPage = (eventId: CalendarEvent["id"]) => + `/maps?readonly&eventId=${eventId}`; export const articlePage = (slug: string) => `/a/${slug}`; export const analyzerPage = (args?: { weaponId: MainWeaponId; diff --git a/public/locales/de/common.json b/public/locales/de/common.json index af292c7b5..7820cc344 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -34,6 +34,8 @@ "actions.delete": "Löschen", "actions.loadMore": "Mehr laden", "actions.close": "Schließen", + "actions.clear": "Leeren", + "actions.selectAll": "Alle auswählen", "maps.createMapList": "Arenen-Liste erstellen", "maps.halfSz": "50% Herrschaft", diff --git a/public/locales/de/game-misc.json b/public/locales/de/game-misc.json index 4e3c81ce7..d9e393f1c 100644 --- a/public/locales/de/game-misc.json +++ b/public/locales/de/game-misc.json @@ -15,5 +15,10 @@ "MODE_SHORT_SZ": "HS", "MODE_SHORT_TC": "TK", "MODE_SHORT_RM": "OG", - "MODE_SHORT_CB": "MC" + "MODE_SHORT_CB": "MC", + "MODE_LONG_TW": "Revierkampf", + "MODE_LONG_SZ": "Herrschaft", + "MODE_LONG_TC": "Turmkommando", + "MODE_LONG_RM": "Operation Goldfisch", + "MODE_LONG_CB": "Muschelchaos" } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index db52a437d..c9f7a376e 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -36,6 +36,8 @@ "actions.loadMore": "Load more", "actions.copyToClipboard": "Copy to clipboard", "actions.close": "Close", + "actions.clear": "Clear", + "actions.selectAll": "Select All", "maps.createMapList": "Create map list", "maps.halfSz": "50% SZ", diff --git a/public/locales/en/game-misc.json b/public/locales/en/game-misc.json index be8229456..780d33e38 100644 --- a/public/locales/en/game-misc.json +++ b/public/locales/en/game-misc.json @@ -15,5 +15,10 @@ "MODE_SHORT_SZ": "SZ", "MODE_SHORT_TC": "TC", "MODE_SHORT_RM": "RM", - "MODE_SHORT_CB": "CB" + "MODE_SHORT_CB": "CB", + "MODE_LONG_TW": "Turf War", + "MODE_LONG_SZ": "Splat Zones", + "MODE_LONG_TC": "Tower Control", + "MODE_LONG_RM": "Rainmaker", + "MODE_LONG_CB": "Clam Blitz" } From 63154e507e336330ce6e7b8591204775ef61bfe5 Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Wed, 26 Oct 2022 01:05:01 +0200 Subject: [PATCH 10/21] Initial support for map pool templates --- app/components/MapPoolSelector.tsx | 114 +++++++++++++++++--- app/modules/map-pool-serializer/map-pool.ts | 29 +++++ app/styles/common.css | 3 +- app/styles/maps.css | 12 +++ app/utils/strings.ts | 27 +++++ public/locales/de/common.json | 6 ++ public/locales/en/common.json | 6 ++ 7 files changed, 182 insertions(+), 15 deletions(-) diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index a4a495691..1bc2ea46a 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -1,11 +1,16 @@ import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { Image } from "~/components/Image"; -import type { ModeShort, StageId } from "~/modules/in-game-lists"; +import { + type ModeShort, + modesShort, + type StageId, +} from "~/modules/in-game-lists"; import { modes, stageIds } from "~/modules/in-game-lists"; import { MapPool } from "~/modules/map-pool-serializer"; import { modeImageUrl, stageImageUrl } from "~/utils/urls"; import { Button } from "~/components/Button"; +import { split, startsWith } from "~/utils/strings"; import { CrossIcon } from "./icons/Cross"; import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import * as React from "react"; @@ -25,31 +30,57 @@ export function MapPoolSelector({ }: MapPoolSelectorProps) { const { t } = useTranslation(); + const [template, setTemplate] = React.useState( + detectTemplate(mapPool) + ); + const handleStageModesChange = (newMapPool: MapPool) => { + setTemplate(detectTemplate(newMapPool)); handleMapPoolChange(newMapPool); }; const handleClear = () => { + setTemplate("none"); handleMapPoolChange(MapPool.EMPTY); }; + const handleTemplateChange = (template: MapPoolTemplateValue) => { + setTemplate(template); + if (template === "none") { + return; + } + + if (startsWith(template, "preset:")) { + const [, presetId] = split(template, ":"); + + handleMapPoolChange(MapPool[presetId]); + return; + } + }; + return (
    {t("maps.mapPool")} -
    -
    - {handleRemoval && ( - - )} - + )} + +
    +
    +
    +
    ); } + +type MapModePresetId = "ANARCHY" | "ALL" | ModeShort; + +const presetIds: MapModePresetId[] = ["ANARCHY", "ALL", ...modesShort]; + +type MapPoolTemplateValue = "none" | `preset:${MapModePresetId}`; + +function detectTemplate(mapPool: MapPool): MapPoolTemplateValue { + for (const presetId of presetIds) { + if (MapPool[presetId].serialized === mapPool.serialized) { + return `preset:${presetId}`; + } + } + return "none"; +} + +type MapPoolTemplateSelectProps = { + value: MapPoolTemplateValue; + handleChange: (newValue: MapPoolTemplateValue) => void; +}; + +function MapPoolTemplateSelect({ + handleChange, + value, +}: MapPoolTemplateSelectProps) { + const { t } = useTranslation(["game-misc", "common"]); + + return ( + + ); +} diff --git a/app/modules/map-pool-serializer/map-pool.ts b/app/modules/map-pool-serializer/map-pool.ts index 8639d5416..e2c3e246e 100644 --- a/app/modules/map-pool-serializer/map-pool.ts +++ b/app/modules/map-pool-serializer/map-pool.ts @@ -97,6 +97,14 @@ export class MapPool { TW: [], }); + static ALL = new MapPool({ + SZ: [...stageIds], + TC: [...stageIds], + CB: [...stageIds], + RM: [...stageIds], + TW: [...stageIds], + }); + static ANARCHY = new MapPool({ SZ: [...stageIds], TC: [...stageIds], @@ -104,4 +112,25 @@ export class MapPool { RM: [...stageIds], TW: [], }); + + static SZ = new MapPool({ + ...MapPool.EMPTY.parsed, + SZ: [...stageIds], + }); + static TC = new MapPool({ + ...MapPool.EMPTY.parsed, + TC: [...stageIds], + }); + static CB = new MapPool({ + ...MapPool.EMPTY.parsed, + CB: [...stageIds], + }); + static RM = new MapPool({ + ...MapPool.EMPTY.parsed, + RM: [...stageIds], + }); + static TW = new MapPool({ + ...MapPool.EMPTY.parsed, + TW: [...stageIds], + }); } diff --git a/app/styles/common.css b/app/styles/common.css index 2aa9a71f0..cb7404af8 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -243,7 +243,8 @@ article { select { all: unset; - width: 90%; + width: 100%; + box-sizing: border-box; border: 1px solid var(--border); border-radius: var(--rounded); background: var(--select-background, var(--bg-lighter)); diff --git a/app/styles/maps.css b/app/styles/maps.css index 5d064d305..4c0ad79c2 100644 --- a/app/styles/maps.css +++ b/app/styles/maps.css @@ -110,3 +110,15 @@ font-weight: var(--bold); text-decoration: none; } + +.maps__template-selection { + display: grid; + gap: var(--s-2); + grid-template-columns: 1fr; +} + +@media screen and (min-width: 640px) { + .maps__template-selection { + grid-template-columns: 1fr 1fr; + } +} diff --git a/app/utils/strings.ts b/app/utils/strings.ts index 7b3202a81..ebe7532c2 100644 --- a/app/utils/strings.ts +++ b/app/utils/strings.ts @@ -30,3 +30,30 @@ export function semiRandomId() { export const rawSensToString = (sens: number) => `${sens > 0 ? "+" : ""}${sens / 10}`; + +type WithStart< + S extends string, + Start extends string +> = S extends `${Start}${infer Rest}` ? `${Start}${Rest}` : never; + +export function startsWith( + str: S, + start: Start +): str is WithStart { + return str.startsWith(start); +} + +type Split = string extends S + ? string[] + : S extends "" + ? [] + : S extends `${infer T}${Sep}${infer U}` + ? [T, ...Split] + : [S]; + +export function split( + str: S, + seperator: Sep +) { + return str.split(seperator) as Split; +} diff --git a/public/locales/de/common.json b/public/locales/de/common.json index 7820cc344..bff10d8fb 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -41,6 +41,12 @@ "maps.halfSz": "50% Herrschaft", "maps.mapPool": "Arenen-Pool", "maps.tournamentMaplist": "Arenen-Liste für Turnier erstellen (maps.iplabs.ink)", + "maps.template": "Vorlage", + "maps.template.none": "Keine", + "maps.template.presets": "Voreinstellungen", + "maps.template.preset.ANARCHY": "Anarchie-Modi", + "maps.template.preset.ALL": "Alle Modi", + "maps.template.preset.onlyMode": "Nur {{modeName}}", "results": "Ergebnisse", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index c9f7a376e..c69d6a4a5 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -43,6 +43,12 @@ "maps.halfSz": "50% SZ", "maps.mapPool": "Map pool", "maps.tournamentMaplist": "Create tournament map list (maps.iplabs.ink)", + "maps.template": "Template", + "maps.template.none": "None", + "maps.template.presets": "Presets", + "maps.template.preset.ANARCHY": "Anarchy Modes", + "maps.template.preset.ALL": "All Modes", + "maps.template.preset.onlyMode": "Only {{modeName}}", "results": "Results", From 0a85ad08d2f9baa1d69df5966da05b33f5c4673e Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Wed, 26 Oct 2022 01:18:32 +0200 Subject: [PATCH 11/21] Add recent events to map pool templates --- app/components/MapPoolSelector.tsx | 38 ++++++++++++++++++- .../calendar/findRecentMapPoolsByAuthorId.sql | 21 ++++++++++ app/db/models/calendar/queries.server.ts | 20 ++++++++++ app/routes/calendar/new.tsx | 7 +++- app/routes/maps.tsx | 20 ++++++++-- public/locales/de/common.json | 1 + public/locales/en/common.json | 1 + 7 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 app/db/models/calendar/findRecentMapPoolsByAuthorId.sql diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index 1bc2ea46a..9fe4d955f 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -14,12 +14,19 @@ import { split, startsWith } from "~/utils/strings"; import { CrossIcon } from "./icons/Cross"; import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import * as React from "react"; +import type { CalendarEvent } from "~/db/types"; export type MapPoolSelectorProps = { mapPool: MapPool; handleRemoval?: () => void; - handleMapPoolChange: (mapPool: MapPool) => void; + handleMapPoolChange: ( + mapPool: MapPool, + event?: Pick + ) => void; className?: string; + recentEvents?: Array< + Pick & { serializedMapPool: string } + >; }; export function MapPoolSelector({ @@ -27,6 +34,7 @@ export function MapPoolSelector({ handleMapPoolChange, handleRemoval, className, + recentEvents, }: MapPoolSelectorProps) { const { t } = useTranslation(); @@ -56,6 +64,17 @@ export function MapPoolSelector({ handleMapPoolChange(MapPool[presetId]); return; } + + if (startsWith(template, "recent-event:")) { + const [, eventId] = split(template, ":"); + + const event = recentEvents?.find((e) => e.id.toString() === eventId); + + if (event) { + handleMapPoolChange(new MapPool(event.serializedMapPool), event); + } + return; + } }; return ( @@ -80,6 +99,7 @@ export function MapPoolSelector({
    void; + recentEvents?: Pick[]; }; function MapPoolTemplateSelect({ handleChange, value, + recentEvents, }: MapPoolTemplateSelectProps) { const { t } = useTranslation(["game-misc", "common"]); @@ -302,6 +327,15 @@ function MapPoolTemplateSelect({ ))} + {recentEvents && recentEvents.length > 0 && ( + + {recentEvents.map((event) => ( + + ))} + + )} ); diff --git a/app/db/models/calendar/findRecentMapPoolsByAuthorId.sql b/app/db/models/calendar/findRecentMapPoolsByAuthorId.sql new file mode 100644 index 000000000..6ef3c3ba8 --- /dev/null +++ b/app/db/models/calendar/findRecentMapPoolsByAuthorId.sql @@ -0,0 +1,21 @@ +select + "CalendarEvent"."id", + "CalendarEvent"."name", + json_group_array( + json_object( + 'stageId', + "MapPoolMap"."stageId", + 'mode', + "MapPoolMap"."mode" + ) + ) as "mapPool" +from + "CalendarEvent" + join "MapPoolMap" on "CalendarEvent"."id" = "MapPoolMap"."calendarEventId" +where + "CalendarEvent"."authorId" = @authorId +group by + "CalendarEvent"."id" +order by + "CalendarEvent"."id" desc +limit 5 \ No newline at end of file diff --git a/app/db/models/calendar/queries.server.ts b/app/db/models/calendar/queries.server.ts index 60ad44186..a8788112c 100644 --- a/app/db/models/calendar/queries.server.ts +++ b/app/db/models/calendar/queries.server.ts @@ -36,6 +36,7 @@ import upcomingEventsSql from "./upcomingEvents.sql"; import createMapPoolMapSql from "./createMapPoolMap.sql"; import deleteMapPoolMapsSql from "./deleteMapPoolMaps.sql"; import findMapPoolByEventIdSql from "./findMapPoolByEventId.sql"; +import findRecentMapPoolsByAuthorIdSql from "./findRecentMapPoolsByAuthorId.sql"; const createStm = sql.prepare(createSql); const updateStm = sql.prepare(updateSql); @@ -467,3 +468,22 @@ export function eventsToReport(authorId?: CalendarEvent["authorId"]) { }) as Array> ).map((row) => ({ id: row.id, name: row.name })); } + +const findRecentMapPoolsByAuthorIdStm = sql.prepare( + findRecentMapPoolsByAuthorIdSql +); +export function findRecentMapPoolsByAuthorId( + authorId: CalendarEvent["authorId"] +) { + return ( + findRecentMapPoolsByAuthorIdStm.all({ authorId }) as Array< + Pick & { + mapPool: string; + } + > + ).map((row) => ({ + id: row.id, + name: row.name, + serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)), + })); +} diff --git a/app/routes/calendar/new.tsx b/app/routes/calendar/new.tsx index 5357ffadb..6d51ba6e5 100644 --- a/app/routes/calendar/new.tsx +++ b/app/routes/calendar/new.tsx @@ -192,6 +192,9 @@ export const loader = async ({ request }: LoaderArgs) => { return json({ managedBadges: db.badges.managedByUserId(user.id), + recentEventsWithMapPools: db.calendarEvents.findRecentMapPoolsByAuthorId( + user.id + ), eventToEdit: canEditEvent ? { ...eventToEdit, @@ -520,7 +523,8 @@ function BadgesAdder() { function MapPoolSection() { const { t } = useTranslation(["game-misc", "common"]); - const { eventToEdit } = useLoaderData(); + const { eventToEdit, recentEventsWithMapPools } = + useLoaderData(); const [mapPool, setMapPool] = React.useState( eventToEdit?.mapPool ? new MapPool(eventToEdit.mapPool) : MapPool.EMPTY ); @@ -539,6 +543,7 @@ function MapPoolSection() { mapPool={mapPool} handleRemoval={() => setIncludeMapPool(false)} handleMapPoolChange={setMapPool} + recentEvents={recentEventsWithMapPools} /> ) : ( diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index 1c63249ea..950e01ee9 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -31,6 +31,8 @@ import { type SendouRouteHandle } from "~/utils/remix"; import { MapPoolSelector, MapPoolStages } from "~/components/MapPoolSelector"; import { EditIcon } from "~/components/icons/Edit"; import { useOnce } from "~/hooks/useOnce"; +import { getUser } from "~/modules/auth"; +import type { CalendarEvent } from "~/db/types"; const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2; @@ -61,6 +63,7 @@ export const handle: SendouRouteHandle = { }; export const loader = async ({ request }: LoaderArgs) => { + const user = await getUser(request); const url = new URL(request.url); const calendarEventId = url.searchParams.get("eventId"); const t = await i18next.getFixedT(request); @@ -79,6 +82,9 @@ export const loader = async ({ request }: LoaderArgs) => { mapPool: event ? db.calendarEvents.findMapPoolByEventId(event.eventId) : null, + recentEventsWithMapPools: user + ? db.calendarEvents.findRecentMapPoolsByAuthorId(user.id) + : undefined, title: makeTitle([t("pages.maps")]), }; }; @@ -118,6 +124,7 @@ export default function MapListPage() { )}
    { + const handleMapPoolChange = ( + newMapPool: MapPool, + event?: Pick + ) => { setMapPool(newMapPool); setSearchParams( - { - pool: newMapPool.serialized, - }, + event + ? { eventId: event.id.toString() } + : { + pool: newMapPool.serialized, + }, { replace: true, state: { scroll: false } } ); }; diff --git a/public/locales/de/common.json b/public/locales/de/common.json index bff10d8fb..b7e97835e 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -44,6 +44,7 @@ "maps.template": "Vorlage", "maps.template.none": "Keine", "maps.template.presets": "Voreinstellungen", + "maps.template.yourRecentEvents": "Deine Events", "maps.template.preset.ANARCHY": "Anarchie-Modi", "maps.template.preset.ALL": "Alle Modi", "maps.template.preset.onlyMode": "Nur {{modeName}}", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index c69d6a4a5..127e88e7b 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -46,6 +46,7 @@ "maps.template": "Template", "maps.template.none": "None", "maps.template.presets": "Presets", + "maps.template.yourRecentEvents": "Recent Events", "maps.template.preset.ANARCHY": "Anarchy Modes", "maps.template.preset.ALL": "All Modes", "maps.template.preset.onlyMode": "Only {{modeName}}", From d1b13676511f4c84475525eb04492d4a5ed885a8 Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:00:28 +0200 Subject: [PATCH 12/21] Add event search to map pool templates --- app/components/Combobox.tsx | 232 ++++++++++++------ app/components/MapPoolSelector.tsx | 69 +++++- .../calendar/findAllEventsWithMapPools.sql | 18 ++ app/db/models/calendar/queries.server.ts | 16 ++ app/hooks/swr.ts | 22 +- app/routes/calendar/map-pool-events.ts | 12 + app/routes/maps.tsx | 1 + app/styles/common.css | 12 + app/utils/urls.ts | 3 + public/locales/de/common.json | 6 + public/locales/en/common.json | 6 + 11 files changed, 315 insertions(+), 82 deletions(-) create mode 100644 app/db/models/calendar/findAllEventsWithMapPools.sql create mode 100644 app/routes/calendar/map-pool-events.ts diff --git a/app/components/Combobox.tsx b/app/components/Combobox.tsx index 9e8027d6f..e5dacdfd1 100644 --- a/app/components/Combobox.tsx +++ b/app/components/Combobox.tsx @@ -4,7 +4,7 @@ import Fuse from "fuse.js"; import clsx from "clsx"; import type { Unpacked } from "~/utils/types"; import type { GearType, UserWithPlusTier } from "~/db/types"; -import { useUsers } from "~/hooks/swr"; +import { useAllEventsWithMapPools, useUsers } from "~/hooks/swr"; import { useTranslation } from "react-i18next"; import { clothesGearIds, @@ -15,6 +15,7 @@ import { } from "~/modules/in-game-lists"; import { gearImageUrl, mainWeaponImageUrl } from "~/utils/urls"; import { Image } from "./Image"; +import { type SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events"; const MAX_RESULTS_SHOWN = 6; @@ -36,6 +37,7 @@ interface ComboboxProps { initialValue?: ComboboxOption; clearsInputOnFocus?: boolean; onChange?: (selectedOption?: ComboboxOption) => void; + fullWidth?: boolean; } export function Combobox>({ @@ -49,11 +51,16 @@ export function Combobox>({ className, id, isLoading = false, + fullWidth = false, }: ComboboxProps) { - const [selectedOption, setSelectedOption] = - React.useState>(); - const [lastSelectedOption, setLastSelectedOption] = - React.useState>(); + const { t } = useTranslation(); + + const [selectedOption, setSelectedOption] = React.useState< + Unpacked | undefined + >(initialValue); + const [lastSelectedOption, setLastSelectedOption] = React.useState< + Unpacked | undefined + >(initialValue); const [query, setQuery] = React.useState(""); React.useEffect(() => { @@ -75,73 +82,85 @@ export function Combobox>({ const noMatches = filteredOptions.length === 0; + const displayValue = (option: Unpacked) => { + return option?.label ?? ""; + }; + return ( - { - onChange?.(selected); - setSelectedOption(selected); - setLastSelectedOption(selected); - }} - name={inputName} - disabled={isLoading} - > - { - if (clearsInputOnFocus) { - setSelectedOption(undefined); - } +
    + { + onChange?.(selected); + setSelectedOption(selected); + setLastSelectedOption(selected); }} - onBlur={() => { - if (!selectedOption && clearsInputOnFocus) { - setSelectedOption(lastSelectedOption); - } - }} - onChange={(event) => setQuery(event.target.value)} - placeholder={isLoading ? "Loading..." : placeholder} - className={clsx("combobox-input", className)} - displayValue={(option) => - (option as unknown as Unpacked)?.label ?? "" - } - data-cy={`${inputName}-combobox-input`} - id={id} - required={required} - /> - - {noMatches ? ( -
    - No matches found 🤔 -
    - ) : ( - filteredOptions.map((option) => ( - - {({ active }) => ( -
  3. - {option.imgPath && ( - - )} - {option.label} -
  4. - )} -
    - )) - )} -
    -
    + { + if (clearsInputOnFocus) { + setSelectedOption(undefined); + } + }} + onBlur={() => { + if (!selectedOption && clearsInputOnFocus) { + setSelectedOption(lastSelectedOption); + } + }} + onChange={(event) => setQuery(event.target.value)} + placeholder={isLoading ? t("actions.loading") : placeholder} + className={clsx("combobox-input", className, { + fullWidth, + })} + // To make SSR prefill work in an uncontrolled component + defaultValue={initialValue ? displayValue(initialValue) : undefined} + displayValue={displayValue} + data-cy={`${inputName}-combobox-input`} + id={id} + required={required} + /> + + {isLoading ? ( +
    {t("actions.loading")}
    + ) : noMatches ? ( +
    + {t("forms.errors.noSearchMatches")}{" "} + 🤔 +
    + ) : ( + filteredOptions.map((option) => ( + + {({ active }) => ( +
  5. + {option.imgPath && ( + + )} + {option.label} +
  6. + )} +
    + )) + )} +
    + +
    ); } @@ -157,6 +176,7 @@ export function UserCombobox({ ComboboxProps>, "inputName" | "onChange" | "className" | "id" | "required" > & { userIdsToOmit?: Set; initialUserId?: number }) { + const { t } = useTranslation(); const { users, isLoading, isError } = useUsers(); const options = React.useMemo(() => { @@ -181,9 +201,7 @@ export function UserCombobox({ if (isError) { return ( -
    - Something went wrong. Try reloading the page. -
    +
    {t("errors.genericReload")}
    ); } @@ -191,7 +209,7 @@ export function UserCombobox({ ); } + +const mapPoolEventToOption = ( + e: SerializedMapPoolEvent +): ComboboxOption> => ({ + serializedMapPool: e.serializedMapPool, + label: e.name, + value: e.id.toString(), +}); + +type MapPoolEventsComboboxProps = Pick< + ComboboxProps>, + "inputName" | "className" | "id" | "required" +> & { + initialEvent?: SerializedMapPoolEvent; + onChange: (event?: SerializedMapPoolEvent) => void; +}; + +export function MapPoolEventsCombobox({ + id, + required, + className, + inputName, + onChange, + initialEvent, +}: MapPoolEventsComboboxProps) { + const { t } = useTranslation(); + const { events, isLoading, isError } = useAllEventsWithMapPools(); + + const options = React.useMemo( + () => (events ? events.map(mapPoolEventToOption) : []), + [events] + ); + + // this is important so that we don't trigger the reset to the initialEvent every time + const initialOption = React.useMemo( + () => initialEvent && mapPoolEventToOption(initialEvent), + [initialEvent] + ); + + if (isError) { + return ( +
    {t("errors.genericReload")}
    + ); + } + + return ( + { + onChange( + e && { + id: parseInt(e.value, 10), + name: e.label, + serializedMapPool: e.serializedMapPool, + } + ); + }} + className={className} + id={id} + required={required} + isLoading={isLoading} + fullWidth + /> + ); +} diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index 9fe4d955f..d02d340a1 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -15,6 +15,10 @@ import { CrossIcon } from "./icons/Cross"; import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import * as React from "react"; import type { CalendarEvent } from "~/db/types"; +import type { SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events"; +import { assertType } from "~/utils/types"; +import { MapPoolEventsCombobox } from "./Combobox"; +import { useOnce } from "~/hooks/useOnce"; export type MapPoolSelectorProps = { mapPool: MapPool; @@ -24,9 +28,8 @@ export type MapPoolSelectorProps = { event?: Pick ) => void; className?: string; - recentEvents?: Array< - Pick & { serializedMapPool: string } - >; + recentEvents?: SerializedMapPoolEvent[]; + initialEvent?: Pick; }; export function MapPoolSelector({ @@ -35,11 +38,20 @@ export function MapPoolSelector({ handleRemoval, className, recentEvents, + initialEvent, }: MapPoolSelectorProps) { const { t } = useTranslation(); const [template, setTemplate] = React.useState( - detectTemplate(mapPool) + initialEvent ? "event" : detectTemplate(mapPool) + ); + + const initialSerializedEvent: SerializedMapPoolEvent | undefined = useOnce( + () => + initialEvent && { + ...initialEvent, + serializedMapPool: mapPool.serialized, + } ); const handleStageModesChange = (newMapPool: MapPool) => { @@ -54,7 +66,8 @@ export function MapPoolSelector({ const handleTemplateChange = (template: MapPoolTemplateValue) => { setTemplate(template); - if (template === "none") { + + if (template === "none" || template === "event") { return; } @@ -75,6 +88,8 @@ export function MapPoolSelector({ } return; } + + assertType(); }; return ( @@ -101,6 +116,12 @@ export function MapPoolSelector({ handleChange={handleTemplateChange} recentEvents={recentEvents} /> + {template === "event" && ( + + )}
    + {(["ANARCHY", "ALL"] as const).map((presetId) => ( Date: Wed, 26 Oct 2022 19:55:16 +0200 Subject: [PATCH 13/21] Update translation-progress.md --- translation-progress.md | 307 +++++++++++++++++++++++++++------------- 1 file changed, 205 insertions(+), 102 deletions(-) diff --git a/translation-progress.md b/translation-progress.md index aca22c713..f601d5717 100644 --- a/translation-progress.md +++ b/translation-progress.md @@ -39,12 +39,26 @@ ### 🟡 common.json -**60/61** +**60/75**
    Missing - pages.object-damage-calculator +- actions.loading +- actions.clear +- actions.selectAll +- actions.search +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
    @@ -67,9 +81,20 @@
-### 🟢 game-misc.json +### 🟡 game-misc.json -**17/17** +**17/22** + +
+Missing + +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB + +
### 🟢 user.json @@ -116,7 +141,7 @@ ### 🟡 common.json -**59/61** +**73/75**
Missing @@ -147,7 +172,7 @@ ### 🟢 game-misc.json -**17/17** +**22/22** ### 🟢 user.json @@ -205,7 +230,7 @@ ### 🟡 common.json -**48/61** +**48/75**
Missing @@ -219,10 +244,24 @@ - auth.errors.unknown - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
@@ -248,7 +287,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
Missing @@ -258,6 +297,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
@@ -341,7 +385,7 @@ ### 🟡 common.json -**46/61** +**46/75**
Missing @@ -357,10 +401,24 @@ - actions.loadMore - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
@@ -396,7 +454,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
Missing @@ -406,6 +464,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
@@ -459,7 +522,7 @@ ### 🔴 common.json -**0/61** +**0/75** ### 🔴 contributions.json @@ -475,7 +538,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
Missing @@ -485,6 +548,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
@@ -623,7 +691,7 @@ ### 🟡 common.json -**46/61** +**46/75**
Missing @@ -639,10 +707,24 @@ - actions.loadMore - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
@@ -677,7 +759,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
Missing @@ -687,6 +769,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
@@ -748,7 +835,7 @@ ### 🟡 common.json -**35/61** +**35/75**
Missing @@ -764,10 +851,24 @@ - actions.loadMore - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload - weapon.category.SHOOTERS - weapon.category.BLASTERS - weapon.category.ROLLERS @@ -813,7 +914,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
Missing @@ -823,6 +924,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
@@ -903,7 +1009,7 @@ ### 🟡 common.json -**48/61** +**48/75**
Missing @@ -917,10 +1023,24 @@ - auth.errors.unknown - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
@@ -946,7 +1066,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
Missing @@ -956,6 +1076,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
@@ -979,9 +1104,21 @@ ## /ru (🟡 In progress) -### 🔴 analyzer.json +### 🟡 analyzer.json -**0/107** +**101/107** + +
+Missing + +- objCalcAd +- stat.specialLostSplattedByRP +- stat.quickRespawnTimeSplattedByRP +- damage.NORMAL_MAX_FULL_CHARGE +- dmgHtdExplanation +- noDmgData + +
### 🟢 badges.json @@ -991,64 +1128,38 @@ **11/11** -### 🟡 calendar.json +### 🟢 calendar.json -**44/46** - -
-Missing - -- createMapList -- forms.mapPool - -
+**46/46** ### 🟡 common.json -**35/61** +**60/75**
Missing -- pages.s2 -- pages.analyzer -- pages.maps - pages.object-damage-calculator -- auth.errors.aborted -- auth.errors.failed -- auth.errors.discordPermissions -- auth.errors.unknown -- actions.loadMore -- actions.copyToClipboard -- actions.close -- maps.createMapList -- maps.halfSz -- maps.mapPool -- maps.tournamentMaplist -- weapon.category.SHOOTERS -- weapon.category.BLASTERS -- weapon.category.ROLLERS -- weapon.category.BRUSHES -- weapon.category.CHARGERS -- weapon.category.SLOSHERS -- weapon.category.SPLATLINGS -- weapon.category.DUALIES -- weapon.category.BRELLAS -- weapon.category.STRINGERS -- weapon.category.SPLATANAS +- actions.loading +- actions.clear +- actions.selectAll +- actions.search +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
-### 🟡 contributions.json +### 🟢 contributions.json -**5/6** - -
-Missing - -- translation - -
+**6/6** ### 🟢 faq.json @@ -1056,60 +1167,33 @@ ### 🟡 front.json -**8/12** +**11/12**
Missing -- buildsGoTo -- analyzer.description -- maps.description - object-damage-calculator.description
### 🟡 game-misc.json -**12/17** +**17/22**
Missing -- MODE_SHORT_TW -- MODE_SHORT_SZ -- MODE_SHORT_TC -- MODE_SHORT_RM -- MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
-### 🟡 user.json +### 🟢 user.json -**7/25** - -
-Missing - -- customUrl -- ign -- ign.short -- stickSens -- motionSens -- motion -- stick -- sens -- results.title -- results.participants -- results.highlights -- results.nonHighlights -- results.highlights.choose -- results.highlights.explanation -- forms.errors.invalidCustomUrl.numbers -- forms.errors.invalidCustomUrl.strangeCharacter -- forms.errors.invalidCustomUrl.duplicate -- forms.errors.invalidSens - -
+**25/25** --- @@ -1160,7 +1244,7 @@ ### 🟡 common.json -**35/61** +**35/75**
Missing @@ -1176,10 +1260,24 @@ - actions.loadMore - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload - weapon.category.SHOOTERS - weapon.category.BLASTERS - weapon.category.ROLLERS @@ -1225,7 +1323,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
Missing @@ -1235,6 +1333,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
From 6bacfe9717519341f4b351e96f73dc8c73f1b163 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 27 Oct 2022 17:59:51 +0300 Subject: [PATCH 14/21] Add Hydra full charge to prioritized damage types --- app/modules/analyzer/useObjectDamage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/modules/analyzer/useObjectDamage.ts b/app/modules/analyzer/useObjectDamage.ts index 6b20383f9..d116f4fec 100644 --- a/app/modules/analyzer/useObjectDamage.ts +++ b/app/modules/analyzer/useObjectDamage.ts @@ -73,17 +73,17 @@ function validatedAbilityPointsFromSearchParams(searchParams: URLSearchParams) { ); } -export const damageTypePriorityList = [ +export const damageTypePriorityList: Array = [ "DIRECT", "FULL_CHARGE", "MAX_CHARGE", - "NORMAL_MAX", + "NORMAL_MAX_FULL_CHARGE", "NORMAL_MIN", "TAP_SHOT", "DISTANCE", "BOMB_DIRECT", "BOMB_NORMAL", -] as const; +]; function validatedDamageTypeFromSearchParams({ searchParams, analyzed, From b340f32ae38e1487cf73818a631ad27ea12e6b39 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 27 Oct 2022 18:12:36 +0300 Subject: [PATCH 15/21] Fix theme cookie not saved after session closing --- app/modules/theme/session.server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/modules/theme/session.server.ts b/app/modules/theme/session.server.ts index 0311135d9..65e86d62c 100644 --- a/app/modules/theme/session.server.ts +++ b/app/modules/theme/session.server.ts @@ -4,6 +4,8 @@ import { isTheme } from "./provider"; import type { Theme } from "./provider"; import invariant from "tiny-invariant"; +const TEN_YEARS_IN_SECONDS = 315_360_000; + if (process.env.NODE_ENV === "production") { invariant(process.env["SESSION_SECRET"], "SESSION_SECRET is required"); } @@ -17,6 +19,7 @@ const themeStorage = createCookieSessionStorage({ sameSite: "lax", path: "/", httpOnly: true, + maxAge: TEN_YEARS_IN_SECONDS, }, }); From 29896aa76c0b4f2869c287171430bb96ff3f2efa Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 27 Oct 2022 20:28:48 +0300 Subject: [PATCH 16/21] Add Hydra Splatling shots to pop RM test --- app/modules/analyzer/objectDamage.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/modules/analyzer/objectDamage.test.ts b/app/modules/analyzer/objectDamage.test.ts index cae7e7eeb..cd87b5703 100644 --- a/app/modules/analyzer/objectDamage.test.ts +++ b/app/modules/analyzer/objectDamage.test.ts @@ -80,6 +80,8 @@ const shotsToPopRM: Array< [2010, "FULL_CHARGE", 4, 3], // E-liter 4K [2030, "TAP_SHOT", 13, 12], + // Hydra Splatling + [4020, "NORMAL_MAX", 32, 29], ]; CalculateDamage( From aaaf6b318c928f4188fe97f44a41ddd07bff53d4 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 27 Oct 2022 23:18:55 +0300 Subject: [PATCH 17/21] Add front page translations for preloading --- app/routes/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/index.tsx b/app/routes/index.tsx index fb7c0b3cb..963c61882 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -36,7 +36,7 @@ export const links: LinksFunction = () => { }; export const handle: SendouRouteHandle = { - i18n: ["weapons", "builds"], + i18n: ["weapons", "builds", "front"], }; export const loader = async () => { From e44e64dab2b27819a4dd067c57ff8bb0242d8bce Mon Sep 17 00:00:00 2001 From: William Lam Date: Thu, 27 Oct 2022 16:24:04 -0400 Subject: [PATCH 18/21] Issue 1043: changed
tag to

tag for semantic HTML on the top-most UI buttons for an active page --- app/components/layout/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/layout/index.tsx b/app/components/layout/index.tsx index b04831c4d..783e079f1 100644 --- a/app/components/layout/index.tsx +++ b/app/components/layout/index.tsx @@ -68,7 +68,7 @@ export const Layout = React.memo(function Layout({ setMenuOpen(false)} /> {activeNavItem && ( -
+

{t(`pages.${activeNavItem.name}` as any)} -

+

)} {children}