1import * as gamesConfiguration from "@distilled.cloud/gcp/gamesConfiguration_v1configuration";
2import * as Effect from "effect/Effect";
3import { createPhysicalName } from "../../PhysicalName.ts";
4
5export const DEFAULT_LOCALE = "en-US";
6export const DEFAULT_ACHIEVEMENT_TYPE = "STANDARD";
7export const DEFAULT_INITIAL_STATE = "REVEALED";
8export const DEFAULT_POINT_VALUE = 5;
9export const DEFAULT_STEPS_TO_UNLOCK = 10;
10export const DEFAULT_SCORE_ORDER = "LARGER_IS_BETTER";
11export const MAX_ACHIEVEMENT_NAME_LENGTH = 100;
12export const MAX_ACHIEVEMENT_DESCRIPTION_LENGTH = 500;
13export const MAX_LEADERBOARD_NAME_LENGTH = 100;
14export const sameText = (left: string | undefined, right: string | undefined) =>
15 (left ?? "") === (right ?? "");
16
17export const sameNumber = (
18 left: number | undefined,
19 right: number | undefined,
20) => (left ?? 0) === (right ?? 0);
21
22export const jsonEqual = (left: unknown, right: unknown) =>
23 JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
24
25export const catchMissing = <A, E extends { readonly _tag: string }, R>(
26 effect: Effect.Effect<A, E, R>,
27) =>
28 effect.pipe(
29 Effect.catchIf(
30 (error): error is E & { readonly _tag: "NotFound" } =>
31 error._tag === "NotFound",
32 () => Effect.succeed(undefined),
33 ),
34 );
35
36export const ignoreMissing = <E extends { readonly _tag: string }, R>(
37 effect: Effect.Effect<unknown, E, R>,
38) =>
39 effect.pipe(
40 Effect.catchIf(
41 (error): error is E & { readonly _tag: "NotFound" } =>
42 error._tag === "NotFound",
43 () => Effect.void,
44 ),
45 );
46
47export const toDisplayName = (
48 id: string,
49 requested: string | undefined,
50 existing: string | undefined,
51 maxLength = MAX_ACHIEVEMENT_NAME_LENGTH,
52) =>
53 Effect.gen(function* () {
54 if (requested !== undefined && requested.length > 0) {
55 return requested.slice(0, maxLength);
56 }
57 if (existing !== undefined && existing.length > 0) {
58 return existing.slice(0, maxLength);
59 }
60 const generated = yield* createPhysicalName({
61 id,
62 maxLength: Math.min(40, maxLength),
63 lowercase: true,
64 });
65 return generated.slice(0, maxLength);
66 });
67
68export const translationsOf = (
69 bundle: gamesConfiguration.LocalizedStringBundle | undefined,
70): gamesConfiguration.LocalizedString[] =>
71 (bundle?.translations ?? []).map((entry) => ({
72 locale: entry.locale,
73 value: entry.value,
74 }));
75
76export const translationValue = (
77 bundle: gamesConfiguration.LocalizedStringBundle | undefined,
78 locale = DEFAULT_LOCALE,
79): string | undefined => {
80 const translations = translationsOf(bundle);
81 const match =
82 translations.find((entry) => (entry.locale ?? locale) === locale) ??
83 translations[0];
84 return match?.value;
85};
86
87export const withTranslation = (
88 bundle: gamesConfiguration.LocalizedStringBundle | undefined,
89 locale: string,
90 value: string,
91): gamesConfiguration.LocalizedStringBundle => {
92 const translations = translationsOf(bundle);
93 const idx = translations.findIndex(
94 (entry) => (entry.locale ?? locale) === locale,
95 );
96 const next = { locale, value };
97 if (idx >= 0) {
98 translations[idx] = { ...translations[idx], ...next };
99 } else {
100 translations.unshift(next);
101 }
102 return { translations };
103};
104
105export const sameBundle = (
106 left: gamesConfiguration.LocalizedStringBundle | undefined,
107 right: gamesConfiguration.LocalizedStringBundle | undefined,
108) =>
109 jsonEqual(
110 translationsOf(left)
111 .map((entry) => ({
112 locale: entry.locale ?? DEFAULT_LOCALE,
113 value: entry.value ?? "",
114 }))
115 .sort((a, b) => a.locale.localeCompare(b.locale)),
116 translationsOf(right)
117 .map((entry) => ({
118 locale: entry.locale ?? DEFAULT_LOCALE,
119 value: entry.value ?? "",
120 }))
121 .sort((a, b) => a.locale.localeCompare(b.locale)),
122 );
123
124export const defaultScoreFormat =
125 (): gamesConfiguration.GamesNumberFormatConfiguration => ({
126 numberFormatType: "NUMERIC",
127 numDecimalPlaces: 0,
128 });
129
130export const getAchievement = (achievementId: string) =>
131 achievementId.length === 0
132 ? Effect.succeed(undefined)
133 : catchMissing(
134 gamesConfiguration.getAchievementConfigurations({ achievementId }),
135 );
136
137export const getLeaderboard = (leaderboardId: string) =>
138 leaderboardId.length === 0
139 ? Effect.succeed(undefined)
140 : catchMissing(
141 gamesConfiguration.getLeaderboardConfigurations({ leaderboardId }),
142 );