- | {typeRowName} |
+ {t(typeRowName)} |
{damage}{" "}
{val.shotsToSplat && (
From 5be4abf6cc31f437e39b08c83520019054b039a6 Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Tue, 25 Oct 2022 17:44:17 +0300
Subject: [PATCH 13/24] Add inktv return article
---
content/articles/inktv-announces-return.md | 13 +++++++++++++
1 file changed, 13 insertions(+)
create mode 100644 content/articles/inktv-announces-return.md
diff --git a/content/articles/inktv-announces-return.md b/content/articles/inktv-announces-return.md
new file mode 100644
index 000000000..93ce1d530
--- /dev/null
+++ b/content/articles/inktv-announces-return.md
@@ -0,0 +1,13 @@
+---
+title: InkTV Announces Its Return
+date: 2022-10-24
+author: Riczi
+---
+
+In a [Twitter post](https://twitter.com/InkTVSplat/status/1584590544205930496) released on Monday, October 24th, 2022, InkTV announced that it would be returning to the Splatoon competitive scene. This platform is dedicated highlighting the players and teams involved in competitive Splatoon. They were heavily involved in developing tournaments, podcast feeds, and player interviews in an effort to grow the community.
+
+InkTV was retired in early 2020, just as the Covid-19 pandemic began to escalate. Now with tournaments being held again and the incredibly successful launch of a new game in Splatoon 3, InkTV is ready to make its return.
+
+The announcement video features some of Splatoon's most prominent community members, including caster NineWholeGrains and professional players ProChara and KyochaN. In the footage they talk about what InkTV did in the past and how it helped them grow and develop as individuals. Everyone was then surprised with the return announcement.
+
+Be sure to follow InkTV on social media so you can be part of the ever-growing Splatoon community. Check out the video if you haven't already so you can get excited for what InkTV will have in store.
From 6e43b6bbd310636184e4a765ec95f6086d27f2a5 Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Wed, 26 Oct 2022 08:42:04 +0300
Subject: [PATCH 14/24] Add tests and fix damage calculation of some wpns
---
app/modules/analyzer/constants.ts | 111 ++++++++++++++++++++-
app/modules/analyzer/objectDamage.test.ts | 114 ++++++++++++++++++++++
app/modules/analyzer/objectDamage.ts | 61 ++++++------
3 files changed, 254 insertions(+), 32 deletions(-)
create mode 100644 app/modules/analyzer/objectDamage.test.ts
diff --git a/app/modules/analyzer/constants.ts b/app/modules/analyzer/constants.ts
index 312052a5a..ea90740d4 100644
--- a/app/modules/analyzer/constants.ts
+++ b/app/modules/analyzer/constants.ts
@@ -1,4 +1,5 @@
import type { DamageType } from "./types";
+import type objectDamages from "./object-dmg.json";
export const MAX_LDE_INTENSITY = 21;
export const MAX_AP = 57;
@@ -10,13 +11,13 @@ export const DAMAGE_RECEIVERS = [
"GreatBarrier_Barrier", // Big Bubbler Shield
"GreatBarrier_WeakPoint", // Big Bubbler Weak Point
"Gachihoko_Barrier", // Rainmaker Shield
- "BulletUmbrellaCanopyCompact", // Undercover Brella Canopy
- "BulletUmbrellaCanopyNormal", // Splat Brella Canopy
- "BulletUmbrellaCanopyWide", // Tenta Brella Canopy
"Wsb_Flag", // Squid Beakon
"Wsb_Shield", // Splash Wall
"Wsb_Sprinkler", // Sprinkler
"Bomb_TorpedoBullet", // Torpedo
+ "BulletUmbrellaCanopyCompact", // Undercover Brella Canopy
+ "BulletUmbrellaCanopyNormal", // Splat Brella Canopy
+ "BulletUmbrellaCanopyWide", // Tenta Brella Canopy
] as const;
export const DAMAGE_TYPE = [
@@ -45,3 +46,107 @@ export const damageTypeToWeaponType: Record<
BOMB_NORMAL: "SUB",
BOMB_DIRECT: "SUB",
};
+
+export const objectDamageJsonKeyPriority: Record<
+ keyof typeof objectDamages,
+ Array | null
+> = {
+ Blaster_BlasterMiddle: null,
+ Blaster_BlasterShort: null,
+ Blaster_KillOneShot: ["DIRECT"],
+ Blaster: null,
+ BlowerExhale_BombCore: null,
+ BlowerInhale: null,
+ BombFlower: null,
+ Bomb_CurlingBullet: null,
+ Bomb_DirectHit: ["BOMB_DIRECT"],
+ Bomb_Fizzy: null,
+ Bomb_Suction: null,
+ Bomb_TorpedoBullet: null,
+ Bomb_TorpedoSplashBurst: null,
+ Bomb_Trap: null,
+ Bomb: null,
+ BrushCore: null,
+ BrushSplash: null,
+ CannonMissile: null,
+ ChargerFull_Light: ["FULL_CHARGE"],
+ ChargerFull_Long: ["FULL_CHARGE"],
+ ChargerFull: ["FULL_CHARGE"],
+ Charger_Light: ["MAX_CHARGE", "TAP_SHOT"],
+ Charger_Long: ["MAX_CHARGE", "TAP_SHOT"],
+ Charger: ["MAX_CHARGE", "TAP_SHOT"],
+ Chariot_Body: null,
+ Chariot_Cannon: null,
+ Default: null,
+ EnemyFlyingHohei_BombCore: null,
+ GachihokoTimeUpBurst: null,
+ Gachihoko_BombCore: null,
+ Gachihoko_Bullet: null,
+ Geyser: null,
+ GoldenIkuraAttack: null,
+ InkStormRain: null,
+ InkStorm: null,
+ Jetpack_BombCore: null,
+ Jetpack_Bullet: null,
+ Jetpack_Coop: null,
+ Jetpack_Jet: null,
+ Maneuver_Short: null,
+ Maneuver: null,
+ MicroLaser: null,
+ MissionSalmonBuddy: null,
+ MovePainter_Burst: null,
+ MovePainter_Direct: null,
+ MultiMissile_BombCore: null,
+ MultiMissile_Bullet: null,
+ NiceBall: null,
+ ObjectEffect_Up: null,
+ RollerCore: null,
+ RollerSplash_Compact: null,
+ RollerSplash_Heavy: null,
+ RollerSplash_Hunter: null,
+ RollerSplash: null,
+ Saber_ChargeShot: null,
+ Saber_ChargeSlash: null,
+ Saber_Shot: null,
+ Saber_Slash: null,
+ SakerocketBullet: null,
+ ShelterCanopy_Compact: null,
+ ShelterCanopy_Wide: null,
+ ShelterCanopy: null,
+ ShelterShot_Compact: null,
+ ShelterShot_Wide: null,
+ ShelterShot: null,
+ Shield: null,
+ ShockSonar_Wave: null,
+ Shooter_Blaze: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_Expert: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_First: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_FlashRepeat: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_Flash: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_Gravity: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_Heavy: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_Long: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_Precision: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_Short: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_TripleMiddle: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter_TripleQuick: ["NORMAL_MAX", "NORMAL_MIN"],
+ Shooter: null,
+ Skewer_Body: null,
+ Skewer: null,
+ Slosher_Bathtub: null,
+ Slosher_Bear: null,
+ Slosher_WashtubBombCore: null,
+ Slosher_Washtub: null,
+ Slosher: null,
+ Spinner: null,
+ Sprinkler: null,
+ Stringer_Short: null,
+ Stringer: null,
+ SuperHook: null,
+ SuperLanding: null,
+ TripleTornado: null,
+ UltraShot: null,
+ UltraStamp_Swing: null,
+ UltraStamp_Throw_BombCore: null,
+ UltraStamp_Throw: null,
+};
diff --git a/app/modules/analyzer/objectDamage.test.ts b/app/modules/analyzer/objectDamage.test.ts
new file mode 100644
index 000000000..cae7e7eeb
--- /dev/null
+++ b/app/modules/analyzer/objectDamage.test.ts
@@ -0,0 +1,114 @@
+import { suite } from "uvu";
+import * as assert from "uvu/assert";
+import type { MainWeaponId } from "../in-game-lists";
+import { calculateDamage } from "./objectDamage";
+import { buildStats } from "./stats";
+import type { AbilityPoints, DamageType } from "./types";
+
+function calculate({
+ mainWeaponId = 10,
+ abilityPoints = new Map(),
+ damageType = "NORMAL_MAX",
+}: {
+ mainWeaponId?: MainWeaponId;
+ abilityPoints?: AbilityPoints;
+ damageType?: DamageType;
+}) {
+ const analyzed = buildStats({
+ weaponSplId: mainWeaponId,
+ });
+
+ return calculateDamage({
+ abilityPoints,
+ analyzed,
+ mainWeaponId,
+ damageType,
+ });
+}
+
+const CalculateDamage = suite("calculateDamage()");
+
+CalculateDamage("BRU increases Splash Wall hitpoints", () => {
+ const withoutBRU = calculate({});
+ const withBRU = calculate({
+ abilityPoints: new Map([["BRU", { ap: 10, apBeforeTacticooler: 10 }]]),
+ });
+
+ const hpWithoutBRU = withoutBRU.find(
+ (d) => d.receiver === "Wsb_Shield"
+ )?.hitPoints;
+ const hpWithBRU = withBRU.find((d) => d.receiver === "Wsb_Shield")?.hitPoints;
+
+ assert.ok(typeof hpWithoutBRU === "number");
+ assert.ok(typeof hpWithBRU === "number");
+ assert.ok(hpWithoutBRU < hpWithBRU);
+});
+
+CalculateDamage("SPU increases Big Bubbler hitpoints", () => {
+ const withoutSPU = calculate({});
+ const withSPU = calculate({
+ abilityPoints: new Map([["SPU", { ap: 10, apBeforeTacticooler: 10 }]]),
+ });
+
+ const hpWithoutSPU = withoutSPU.find(
+ (d) => d.receiver === "GreatBarrier_Barrier"
+ )?.hitPoints;
+ const hpWithSPU = withSPU.find(
+ (d) => d.receiver === "GreatBarrier_Barrier"
+ )?.hitPoints;
+
+ assert.ok(typeof hpWithoutSPU === "number");
+ assert.ok(typeof hpWithSPU === "number");
+ assert.ok(hpWithoutSPU < hpWithSPU);
+});
+
+const shotsToPopRM: Array<
+ [
+ weaponId: MainWeaponId,
+ damageType: DamageType,
+ shotsToPop: number,
+ shotsToPopOS: number
+ ]
+> = [
+ // Splattershot
+ [40, "NORMAL_MAX", 28, 26],
+ // Range Blaster
+ [220, "DIRECT", 5, 4],
+ // .96 Gal
+ [80, "NORMAL_MAX", 17, 15],
+ // Splat Charger
+ [2010, "FULL_CHARGE", 4, 3],
+ // E-liter 4K
+ [2030, "TAP_SHOT", 13, 12],
+];
+
+CalculateDamage(
+ "Calculates matching HTD Rainmaker shield to in-game tests",
+ () => {
+ for (const [
+ mainWeaponId,
+ damageType,
+ shotsToPop,
+ shotsToPopOS,
+ ] of shotsToPopRM) {
+ const damages = calculate({ mainWeaponId, damageType });
+
+ const damageVsRM = damages.find(
+ (d) => d.receiver === "Gachihoko_Barrier"
+ )!;
+
+ assert.equal(
+ damageVsRM.damages.find((d) => !d.objectShredder)!.hitsToDestroy,
+ shotsToPop,
+ `Shots to pop wrong for weapon id: ${mainWeaponId}`
+ );
+ assert.equal(
+ damageVsRM.damages.find((d) => d.objectShredder)!.hitsToDestroy,
+ shotsToPopOS,
+ `Shots to pop wrong with OS for weapon id: ${mainWeaponId}`
+ );
+ }
+ }
+);
+
+CalculateDamage.run();
diff --git a/app/modules/analyzer/objectDamage.ts b/app/modules/analyzer/objectDamage.ts
index 97810f643..b8c357b33 100644
--- a/app/modules/analyzer/objectDamage.ts
+++ b/app/modules/analyzer/objectDamage.ts
@@ -10,33 +10,14 @@ import type {
SpecialWeaponId,
SubWeaponId,
} from "../in-game-lists";
-import { damageTypeToWeaponType, DAMAGE_RECEIVERS } from "./constants";
+import {
+ damageTypeToWeaponType,
+ DAMAGE_RECEIVERS,
+ objectDamageJsonKeyPriority,
+} from "./constants";
import { roundToNDecimalPlaces } from "~/utils/number";
import { objectHitPoints } from "./objectHitPoints";
-/** Keys to check in the json. Lower index takes priority over higher. If key is omitted means any key with valid weapon id is okay. One json key can only map to one DamageType. */
-const objectDamageJsonKeyPriority: Partial<
- Record>
-> = {
- // NORMAL_MIN: [],
- // NORMAL_MAX: [],
- DIRECT: ["Blaster_KillOneShot"],
- FULL_CHARGE: [],
- MAX_CHARGE: [],
- TAP_SHOT: [],
- // DISTANCE: [],
- // BOMB_NORMAL: [],
- BOMB_DIRECT: ["Bomb_DirectHit"],
-};
-
-const commonObjectDamageJsonKeys = () =>
- Object.keys(objectDamages).filter(
- (key) =>
- !Object.values(objectDamageJsonKeyPriority)
- .flat()
- .includes(key as any)
- ) as Array;
-
export function damageTypeToMultipliers({
type,
weapon,
@@ -56,10 +37,7 @@ export function damageTypeToMultipliers({
id: SpecialWeaponId;
};
}) {
- const keysToCheck =
- objectDamageJsonKeyPriority[type] ?? commonObjectDamageJsonKeys();
-
- for (const key of keysToCheck) {
+ for (const key of jsonKeysToCeck(type)) {
const objectDamagesObj = objectDamages[key];
let ok = false;
@@ -77,13 +55,38 @@ export function damageTypeToMultipliers({
}
if (ok) {
- console.log(`for ${type} used ${key ?? "FALLBACK"}`);
return objectDamagesObj.rates;
}
}
return null;
}
+const objectDamageJsonKeyPriorityEntries = Object.entries(
+ objectDamageJsonKeyPriority
+);
+
+// for example blaster belongs to both Blaster_KillOneShot
+// and Blaster categories so it needs to be specified
+// which damage type uses which
+function jsonKeysToCeck(type: DamageType) {
+ const result: Array = [];
+
+ for (const [key, value] of objectDamageJsonKeyPriorityEntries) {
+ if (value?.includes(type)) {
+ result.push(key as keyof typeof objectDamages);
+ }
+ }
+
+ if (result.length) return result;
+
+ for (const [key, value] of objectDamageJsonKeyPriorityEntries) {
+ if (!value) {
+ result.push(key as keyof typeof objectDamages);
+ }
+ }
+
+ return result;
+}
export function multipliersToRecordWithFallbacks(
multipliers: ReturnType
From bf4b6fab13ff14fe3f36ef6c80e8a955e31c545e Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Wed, 26 Oct 2022 09:00:43 +0300
Subject: [PATCH 15/24] Add object dmg calc to nav and front page
---
app/components/layout/nav-items.json | 1 +
app/routes/index.tsx | 7 ++
app/routes/object-damage.tsx | 2 +-
app/utils/urls.ts | 1 +
.../img/layout/object-damage-calculator.avif | Bin 0 -> 6593 bytes
.../img/layout/object-damage-calculator.png | Bin 0 -> 36380 bytes
public/locales/en/common.json | 2 +-
public/locales/en/front.json | 1 +
translation-progress.md | 90 ++++++++++++------
9 files changed, 71 insertions(+), 33 deletions(-)
create mode 100644 public/img/layout/object-damage-calculator.avif
create mode 100644 public/img/layout/object-damage-calculator.png
diff --git a/app/components/layout/nav-items.json b/app/components/layout/nav-items.json
index a31567fa2..8f44c84c2 100644
--- a/app/components/layout/nav-items.json
+++ b/app/components/layout/nav-items.json
@@ -5,6 +5,7 @@
},
{ "name": "builds", "url": "builds" },
{ "name": "analyzer", "url": "analyzer" },
+ { "name": "object-damage-calculator", "url": "object-damage-calculator" },
{ "name": "calendar", "url": "calendar" },
{ "name": "maps", "url": "maps" },
{ "name": "badges", "url": "badges" },
diff --git a/app/routes/index.tsx b/app/routes/index.tsx
index 334a69d73..e10b66453 100644
--- a/app/routes/index.tsx
+++ b/app/routes/index.tsx
@@ -22,6 +22,7 @@ import {
CALENDAR_PAGE,
mapsPage,
navIconUrl,
+ OBJECT_DAMAGE_CALCULATOR,
plusSuggestionPage,
userPage,
} from "~/utils/urls";
@@ -75,6 +76,12 @@ export default function Index() {
description={t("front:analyzer.description")}
to={analyzerPage()}
/>
+
) {
const { t } = useTranslation(["weapons", "analyzer", "common"]);
- useSetTitle(t("common:pages.object-damage"));
+ useSetTitle(t("common:pages.object-damage-calculator"));
return (
=8Lhkol@
z@BMgZt$X)5XP>*zx%bxv006Xh9&e#mUSK=Gll?_!upOT>*vd{_hEEm%K+1Kt^|1QO
zJsGQm4aEIFE&u=pgFOE0|3wJc`oA)GP_T#dKMv_>=LADsL4PB8000^AH}kYG9{>RS
zo~QU V;ZLdmPvYtF|B*oYKLV701%#i*k^Ad-y1R%v
zLv8KuFbFCNdkgchb%u#P)qo*jEBCjeR{y;cH5lsr?`}^=pB@ku3Hd!L02vhx
zJ^ZNy8Nk`b%Hufz3!RJ#fmqPvFD7_SK<%ULQG@uihKA$#fu~wlca_geq0SF37}!S4
zjIeCh{`#jx9`wG9Ssi=Yjkr#7ggi5DV^PKK>O&mIeV_H5hYw#+_wS`B)kdB%1j3p|
z9gQ6Shj?V?@8ZAH6YAKDpl&T)P9W{`%21ISx~WI{Nf9G1?wG6Qt@TC&=rXc=gygHD
zDvAgwFhe&{-P6_3Mz_4;90toOp}IECX+=#EcnHWNlm835?KS7w?I_>*H{JkPwLuCO
z6(|)d^D-?!(dKtZ#Z_LK
zS%g=k<6+hy;0rS*Ryc(AM?jYam<~d7n-R&J*CG885hqBQd<^Gc1tOz$M
zJ;-a!pHO~v#y5Fwsmxhf)+P}mN*jQIb2Op~GIXX>lpN$`v`~G}_k&?!U9lr+003*W
z8P>5WfF10BI0f9BL}XY8e+UZ}*!lh#x^#R#QsgDJZ$^6jbv!Jc4DD0=fRpO7$gLB{
zWGMpZbc9FU`cO%L^L;H^Sa?{YCNWXsXB0epx0IbyfAPZ3XNmsp7$lauPeV|wXMz_?
zJ7wn2RJTC?$qr)nZuRr7VBLhqIhSxjYVPJx8-4LroU2){V-XsSL+o2sEeu1L;Nn>9PQ;w{==W>65b|2}qMc
zCD%1bbWdKhG`|#9~B}g+`2CUICV;tK)?|
zi%|o|^^RsjUsAtDLdj`xuc_O+d`aO>pd^hQ<2|i?-4S*t?69I!%_g{`V`oB3lBEQ+
zZq^3(bLt#|uM%i|D_gNjla{i!olMb!_VsOz6Vq9|zASfG
zarRLW;vH2~Uuzd%8cfpg(XMumJ^rwIuQnFXt*g@SaRLk~lI0Rty$o53kBh+nKt`AY
zYl=6)rHHUUXpx#E<5!_v<^e}aJ)Wx2z0=|
zq0{w+-L>z?21_SJ&++Ot&!tH(1<-*c``7ZkW_4L%8N1Da>52pH;V6?y_X^kMub=)3
zx~t~TdI&-wd7GNw`uic)L3sWv?W1bONn@-hBRyxm@6w;o(b$#ierJsA8FntX6y)=p
zK~C-(4IB_6WUT&SfNYP{L~dmEZkC%1587m!a+HJpx@NKWIl!-3|0>qlr+o8Pno(!#
zY#9SV_-oq0KqoQxUY-EaAr}IL=2eL_wKU*}tydl{lH_fuSXVnu)siCaHHT#(0%CJ~EonqJ^>jm+`cg+G&psony;MzVK-5H5|!vnR&!E43`j%AuP
z;vV#^ETC`Cs}Gs|SRwL4!oaF}fm`O0mF<1*m>1+gvcB3k#DEoXt2Ep>l!F}lOX~!B
zZ+{_8D=b7qXiHb^c_6{r|6?|htSZ(#`ZnAq6-P|h!+sU
zv|#w{T&Iv-){PUrP4XL|QtFtncj3|RmE47AeH<8c0J8nqDQngFh}
zpz7Re7xUt?Y&&CxBxEG1hgYl_B7&W?31ra@U24UWEf0rNX*^f<}?ZvMOG0(FsmS
zLTCva`uoqGyKIM~c5kz{<^74kB|T4{
zw3>gL?`lgvg0wHO|9eq7j)Uh9XvmW*b5m)l>`hO;0gp|9{03O!D)vDdYuVsW&L0t{
z5J91A`pVyV7vrI5RVkl3^ZPCp5kYL$VTz^wDAL}1KSb-lm1MPFJqtMu4iG$@Vm>lF
zW)+U_-$ebLlh%zw2BrcjOPa9E9A-%n69?QZy7c&A@NWd+9;d7P0d0sT4Z;^!57tER&YumVq%EACfL2kq)9oEgxP|l${`N9=%k>eI;=y62bE`PnAYLM1p*r7CM
zm&y`wfQ>>*LT5qfISsu^=`>7e-7&_zBbMZO==&z)$8T3N;j3^t^uPx-8p*yU_dH%R
zxkZMTaSLr{Y^-EANatC{fmG!rTDlc+h!@J)sxVsJ%i)>=Y+IL5Wg6GF<1T5Z2sdbw
zz@H6P(b$qB%K??TMC2YSWc;95nd=m;E$@c_lRdIH55Gg>-ghX3#DV14_C5C1#z!|d
zr)Z279QUzgmuYPUqGH0BEV-i;mhnD{$Q&9rsK$jv`3IHJ?{p@={VvJ~E*yR}8179n
zas@PpSWu)4@hecr$0rFOd9xyKlPYyA{7P%+r#eRfH(@ck3oar-Ou7T#FKi1X9_2{e
zBg=`k95h}p0kHxzN86zV$tM{W^p~eR591try*igwP6}h*hbjzvtHW4NCF8OttUUSiZU5&5d~BQ;L<1x(Fyp+G)$I@G)Gs-KYJsx7T6g0%~Q!
z-i`KabM5TyT-d7DM&hw|4La
zEs)Yh@F3Hlu(=PS-aNkKW$lZzAhFx^)=zOD^T$`YuZZ@Gb7rWNdb($^`)91$A_^Rq
zdaYiw^ay%(A2Cg)m*heSfH1lYGd)EaeDH1fFo#hxR&hBhPsz-n
zulxIhcXjhWE$9P!wl45Y19U!d^Z&r>{}HvN+(K+Z)+az(8PX)Mz&OO*&N5@sD*&~HTDqnvj*yrO5*WFMUwNan8W9ORiy@F}
zsYdcz*mR-f>7uR^xvj_U>mab{f6)cs8L(Y_Gfkn3A51j&JET-)D67^7RmGNEn>O$+
zujx2Y3lgc-Dv@weQY*%_a24+rLWs)35=bMx780hyh?Zoh(;_5(a1%yn3eAox$PXVq
zfLHjF#wh5Dz7!20q`n<8_jZ{Ydp>c#oRRQgPG?l7!qX7546>Otm~3G!ZyR&l_2pr6
z{WbSaB`hy6DN`;w0nyBj|1JW`8H=I6p38=#@JuVtsiC^uoOs55mRfoi=?Fa}87oL}Xh;5#Kj23)hrMRu&~6uyaD
zwGP>f18K(5E_PwbZ5y#=9hy;bWbu0JUlzA~<_5j)*LA9HeZ)$|gHVn+JD3S3+*!k>UOiwGWSe4pH>73h$v>7!BuZbYXYNTO
zNiC5P-}0U#Fij`XGyS7{7fCetxZ2|*1G$xq_TCJZx`<8bI@bNmRYUk#t2h%Y*I51`
zy6T;>NJyHgywf6q*~M!s$qxFa?DN{LC7EQ
z$MXJFJFrcXRH{a_0zEvTkP-Tka-Wyi85hrX+aaNKz^Nvpa+=nL!b4V@>uKLqSE7yfn%fYnwW$J30#<6my$^`WyrRMr}
zDW^Nu%>`WueUNL)A{bNRW9$TcYVzFLYU(6QQKVfbBxfOd;I@zioRE=`K#Jh#2B(Ws
zR)?xIx8Ret9hw%6A~)8Zi3@l(sOTxF3C@pr0t&dH>nUV{36fuM@-J2CS85iJ;TgKS
zf*0JR{yH%nX#t??2eSlr;m>E0bKm^*kIB4>2TTDK+XrXYYC?!=?srlGPA8qDE
zT2lRcCu(0B%VJeK5iIN9(gWz$Msvx4RLqzogAZF`X69m(7|uR+s_=j+^sXax3;G|N
zWx{2gZe#!#B@hs4&eu2L-e3G#=Oj8#e>&LQoC+CIfiP>nEC*Wl*C7g4Ryg2$jIr>dZNV|FZT`;hf0)+W>7gMZF^D56dseRxj
zpYI%6Q~j8-$`W7hOBnj@geqtSoBNpf6rf#FfcwYF;xeqwjY^Ie)PyDA9cL{S>`BZo
zls8EL!<*=2XV5;9Ted6=;X4F`@sZJU@HkO$RhkXo
z7zYl#B{i6^*(CDpqjp6Kqd1I7yVJtc%1cG-!GIwcZ@*7E9gVJ;YQ1>|Y@vSfa(~l0oXDd5M$Y>U
zbG^kG++Jt9aZj|tsxib$9;^K`PKR)S-HCEv3~tz$SHgQnfhQnGr6MxAJ*9D3>OMlk
zCE+BlCiC978^@lp{eBOznA{XLw}Jf!chKBIap!gkU~(LX<43(8zlFf`xK_8jzS5Y4
z)D0TwdWXt6`@psO#-w+e`r>S%ggQJK(P^mVuv_n-xTYD66o3>TIxp#wRubF*($%@$
z6Fn%h>ZZc@7C$bh!Y;#`tRhpzTT4?}fm>I$MR1Y*S}Y#hj~vPh}czqr&T
zOHfW$2(m6r=>8}#9qFWM@jEqZ^dFY`-m{jT<8m)R(pKQ?3COv@r~9(qtV&sZGV?1d
zzfkOgU(;i&qDYiNqr7B^UAt-G)eXlx2`O%s_I?cS+%V!y;fwWG0`D`#ES=OCA3A0C
z>Ljqw#S?4RKM3YCh4*9bIL$5Q8PW(7w4xgmqfH`h=-yLnBn{`3QP_6*hC)^;kT~`+
zvbJC8nINIy=d8csS6Uy$qolAX
z&Dl$ZgZs%QTq#sWE?-wGnhwv3gbzA~vLn&NTlC$y$-D2-jd&=%UKp#-9_0(MB~8UA
z`rK6DjdVl>DAhX9LK^Uo)1u7rgC{uIPi>Rmt~TboOa_?1-oV=O1uWhw+T1*@MNBuO
z*A|F{(0*60G+(@_!K3*DY7!;``U4-bUV13d*3NCafMMpN(m#tl)fQ
z3wU4IHKy#jES0*JQv_pO0$g@aHnphcl*n_*qfF7%YC!Wr49LX?;{
zk$3U)rDb0Vys-<3Aw9re#-GwG&!d5^FoiO3G
z*X4SzpU=mF_2pGvH1(N$Hu27oR}hEp!F>;r-4=_*tp;(DWFGLvY}lJ^wS%ja({71w
z?}kn1(l?WO)Go1S!CouL8#~z$Y#USgargM|ifIhp&}$MmB@
z;l{Mj@nDC#+0;h0%Ru2od7E;lnkZ?oX2;;!k|Gv4GCsCMoqGs~Gg9m-NX@v549
zSFKwHx}R6&6&(d9MjsBElt`T$5RSkT+0~qaSD4wsTQo?wm}u~sl9%MC{H0@?(GeR>
z_3Zw-$nZ&g8fK?=p2(ciQ%L7;3>A}3Oh1287SRlSZrbaKn73zTA{}(MFUGHv`jA1-
z4@6ckN-Ye#oYkO8N251Rj=AN|VX?{$?uRL9Le%(}k#Z_zQ->w)D1Hy6HHg|e
zOy-cBc%Cr|@M+<%550Gju5>D|t;r^uLQ@r#V18LUT!(a?X*zYA#SnIfuA)&j-aIY#
z{h2E#32@r*d$;5s6U(}2_0KK~!c;|p1{=dX(MIN<&My#;ExaE>e*t2AzBMb8uW|W0
z)x2k9a6ORop?A@4$XC)bcg>-cyOCXdbQWD-vFI%$qDZdB23%q%XT!cWw%W_64ba-q
zb)@$uZ**v~D(tRg5v-vTjym$W3QSuPf4r|}<`#1#N$nlkL--R$GW=}u{JkBs8NTKl
z6-aDwVmc40pMqr_vtKaLaWRMmcwuMgQ;hU?aP4fu43#3
z0ALdQcL67sLmyrqQg|yGdF#2^diz;<+5qHjoV^0PX!X2(tR38F_1)Z@JASMM0|2xD
zRRvjn|J=hoKY#t{nZIE{w)}hXklogdu{c)xa8Te;>=zQ*3MebBVh8OzWX0Rzl{>>285)Qj$p@g!7vqHBJq1?{
zh2YQxv*P~0c`ws+ONGa98$pd&dzYGU3?og?kERG0OX@0%`!)rd0dyK*?t9j6X)`7{
zdXIL{A_&;hP)+CN-hQHyZ}@G4b??6;N^$SL`(k-`IUJ+B{(@3yHRb|K8YJ3#N)L5~
z$FQn%)F~B>L*K|Q9%oFp_!#a_VQhzw3_Hwz7vnH^=xGVK5DmVmy4NRf#J(vR;6&0d
zBSHgvj(f$uUAb{jSYv=X}Ijd7-#Oz3ZwDA
zcDHX3FE+t+_mvP#+CH-K+ETk`=vFTGymn|Bjab&vJWGW_tjOr-=%yi8(`9DOGxy$=
zfXTXn%n5eF5ANSyGx
zEC^0aPA=!n0z~wIAp68Q&Nh&_+^EkR_qbB27kH{QK3?1Hn%;uPtQoxnF~XhH5tYdM
z(|rExj>COGV%D)o8?q|5RcYJd?4BP5p&uvrz;jrpKk8S4m>2wJ3g1Zp3xEZH#M^g3
zcIq7|AnUhRo(0jf4S0prc%LqLujRF)a7KYC^)t#XqkexBw4nfU?1+(3IeRH|5Q@9Q
zolo?vGP{Wg>l3A0`=jv2-*_(yq(o3El&5~CsfR`fz3?3mAQZ5I^u1_jTmL_kOOb#^
z^)uW8H_QNgfGN@gCLOSk1=^{CG6Tef9hn?(rfQ2)5wJo4K9x|{N;!W*_i#^GgKTPg
zm3yo$mX-5V&LSR6Llj#XOsbRv{jYwk;!q(g{H9E%3%Z2CNRUHJR@IoafDs}&>vwF33xZ|
zmCD|zIQ>77T44BDyUaz58EsXV*7>d)pNL3ybR~me;?U86Mfm+FAFUJ)K}IJGHxgV&
zn5I}<&u@s~hJb5(4UldvT2&1*IO`X-R;;aX&l6!j|;e4NJWImX>ttg+^_hKL$f$)UVmI$k)d
z2tI{OTZVlZNV0#KT7^!-b)adEx3i7YCOJg8`-(WA#oX)B&41+~n>;0o)}-SO%EZ+wc~*wQ*da%-8mU
zIUI?C7$h24EwpuC86jPMmG0TFC&x%u(!2>0j9#IEw8qimrU4gdK%Iz=P!xd}IS+7E
z*mLOJza#bFV9D4kOVFwBw;@dJY9Z(km<040$@a_6aoL>Gt#&zDq3iM=$>)cQrQnNU
zsNWy#O>som7OyMaX4aFxU{VIwTk7Z#w}3@t&*Z&-BHvpwxZ`BC*=$gcm6f{CK#ua2
z*M)0aHQ+HQ_Cth~29LjEBXeJSi3s8)&tj1a#&W%FXms0gjRde41ikK=K{i(aYq!bT
z0&+MSbfon;KqZEi)T7e(oL$k_95n@9=&R*1L8~!R=>9WnM$oTsPOrI=bZAGmJrL;S
zKAOgoAoFnA44@m}^(kgV=KRDP=a!W!JxbbE^E=wj%I#1k(z2>BX
zA^S($1m~SG%&h;b32=mR?h)hR0?Rt+U3d!Yr^$iez#O#*&|8sy-K3CnY}0r5lr>U_
z)e_B_2s?2FtBad6u;P7h0-xumO6ctt$@%#?;2-GPKux~zqYLr_$vDIm)(FgaW1kaT
zLJS@w_g{WjeJOwy5gUs}%w^JzEwZF-y;^2E+#BX_?Ee}KBBoJWfZP`wv5E@$>#9wx
zI-%VDwLah4{D+1IQ0->}{2AH+h-`r6#}z(B)xZJTcz6Ip6nM)Ta^fuczmvr-1nhCX
z=?ApouRfwjjfIdo01yW?S26AJzFe|k|1ShDPDmUA+yNq^qtBPTM)?9Scj@mPvDF*M
zP47gZJ%V_dj+B5wx9A(KR$Sw#SdRze1YaQt!TSo7SHv>AkLYzR=A}UUZc+!B(|bfJ
z+bbRPg+K3U3~|U_FO6&=E)~Ed;QR>_!wLloqD3NfkR$FxL?kF
z)0f@a*4{3o_e0ZWdngNxib^u?7e+7nBoi@eV4~ueTshjwMPOf1>^XIaeDXSINlGYpVWDPYGmghXBL
ztthX57!~{6j;uS2kiJ|>{O|j7XTgYX?G`j1Qf7eN(8s%)&m{ci2Z?`hY#5}F5du_HH#aAomKFd7?QZjzFzuu|3l~q|hUdhHB
z0&b`IESIN!1cU-3KYZbd!oih4QjhdrvWUl&i?Dp4^>;@D-$x80^E{&^a!04pxJ5ON
zE`d*ldCi>6nmV1|9JWIZfxl(m69gWsvxs%ZctJ0+0ZZCyQFJ=?8%dvq{#%+aapCfP
zJ~VCIp}JWmf9;swspl)7CFAr1K0?x;$T&s;U96v>SguXbZ5RXv!>n1tr9kiUBMmk%
z2mt5L*+uT}d3LAJ))Tti-dcu$Ho2NH|FvRQP~!bOz2~31?@v5X5Y
z%*F%wce)jTvVmA+{E;s|{_K|m07zirW_$O}-WX-+F6Euxcex)>;iND*&Z-RoBPnu9
zhwsX86=Yk-atp5aIh?VgyUUFiAd}b8uA_rEQ4R`7zE$%!RU(vVdF5f0zLE?__@LNn
z6PA2c{M5=*
zSaCR~fFwz`OhE|c%5xLPQJkop~y!B{hS`~FM
zpr4nBB&{zxx?x3`0JOS~vn;*IhC1;062mzeSDyg{&{hj6O(VycSmS9#fI=Uy48z02
zKOQ>S4~sZiE(BF^m=$^T(wm=*`?jw9U5aL-w(FW>40NJ)3oI6bVC^1Zc_l!EGtbZ1yBQMYt |