Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions resources/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
TriggerOutput,
} from '../types/trigger';

import type { Lang } from './languages';
import Outputs from './outputs';

type TargetedResponseOutput = ResponseOutput<Data, TargetedMatches>;
Expand Down Expand Up @@ -164,6 +165,27 @@ const staticResponse = (field: SevText, text: LocaleText): StaticResponseFunc =>
};
};

type LocaleTextWithSeparator = [sep: string, text: LocaleText];

const combineLocaleText = (first: LocaleText, rest: LocaleTextWithSeparator[]): LocaleText => {
const parts: LocaleTextWithSeparator[] = [['', first], ...rest];
const langs = new Set<Lang>(['en']);

for (const [, text] of parts) {
for (const lang of Object.keys(text))
langs.add(lang as Lang);
}

const result: Record<string, string> = {};
for (const lang of langs) {
result[lang] = parts
.map(([sep, text]) => `${sep}${text[lang] ?? text.en}`)
.join('');
}

return result as LocaleText;
};

type SingleSevToResponseFunc = (sev?: Severity) => TargetedResponseFunc | StaticResponseFunc;
type DoubleSevToResponseFunc = (targetSev?: Severity, otherSev?: Severity) => TargetedResponseFunc;
type ResponsesMap = {
Expand Down Expand Up @@ -625,6 +647,29 @@ export const Responses = {
getTowers: (sev?: Severity) => staticResponse(defaultInfoText(sev), Outputs.getTowers),
} as const;

export const compose = (
text1: LocaleText,
sep: string,
text2: LocaleText,
sev?: Severity,
): StaticResponseFunc => {
return staticResponse(defaultInfoText(sev), combineLocaleText(text1, [[sep, text2]]));
};

export const compose3 = (
text1: LocaleText,
sep1: string,
text2: LocaleText,
sep2: string,
text3: LocaleText,
sev?: Severity,
): StaticResponseFunc => {
return staticResponse(
defaultInfoText(sev),
combineLocaleText(text1, [[sep1, text2], [sep2, text3]]),
);
};

// Don't give `Responses` a type in its declaration so that it can be treated as more strict
// than `ResponsesMap`, but do assert that its type is correct. This allows callers to know
// which properties are defined in Responses without having to conditionally check for undefined.
Expand Down
85 changes: 84 additions & 1 deletion test/unittests/responses_test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { assert } from 'chai';

import Outputs from '../../resources/outputs';
import {
builtInResponseStr,
compose,
compose3,
Responses,
severityList,
severityMap,
triggerFunctions,
} from '../../resources/responses';
import { RaidbossData } from '../../types/data';
import { Matches } from '../../types/net_matches';
import { Output, ResponseFunc, ResponseOutput } from '../../types/trigger';
import {
LocaleText,
Output,
OutputStrings,
ResponseFunc,
ResponseOutput,
} from '../../types/trigger';

// test_trigger.js will validate the field names, so no need to do that here.

Expand All @@ -27,6 +36,19 @@ const runResponseFunc = (
return func(empty as RaidbossData, empty as Matches, empty as Output);
};

const runResponseFuncWithOutputStrings = (
func: ResponseFunc<RaidbossData, Matches>,
): [ResponseFuncOutput, OutputStrings] => {
const empty = {};
const output: { responseOutputStrings: OutputStrings } = {
responseOutputStrings: {},
};
return [
func(empty as RaidbossData, empty as Matches, output as Output),
output.responseOutputStrings,
];
};

describe('response tests', () => {
it('responses with default severity are valid', () => {
for (const responseFunc of Object.values(Responses)) {
Expand Down Expand Up @@ -106,4 +128,65 @@ describe('response tests', () => {
}
}
});
it('compose returns a built-in static response', () => {
const responseFunc = compose(Outputs.in, ' => ', Outputs.out);
assert.include(responseFunc.toString(), outputStringSetterStr);
assert.include(responseFunc.toString(), builtInResponseStr);

const [result, outputStrings] = runResponseFuncWithOutputStrings(responseFunc);
assert.isObject(result);
assert.property(result, 'infoText');
assert.deepEqual(outputStrings.text, {
en: 'In => Out',
de: 'Rein => Raus',
fr: 'Intérieur => Extérieur',
ja: '中へ => 外へ',
cn: '靠近 => 远离',
ko: '안으로 => 밖으로',
tc: '靠近 => 遠離',
});
});
it('compose respects explicit severity and locale fallbacks', () => {
const fallbackText: LocaleText = {
en: 'Fallback',
ja: 'Japanese Fallback',
};
const responseFunc = compose(Outputs.spread, ' 😗 ', fallbackText, 'alarm');

const [result, outputStrings] = runResponseFuncWithOutputStrings(responseFunc);
assert.isObject(result);
assert.property(result, 'alarmText');
assert.deepEqual(outputStrings.text, {
en: 'Spread 😗 Fallback',
de: 'Verteilen 😗 Fallback',
fr: 'Dispersez-vous 😗 Fallback',
ja: 'さんかい 😗 Japanese Fallback',
cn: '分散 😗 Fallback',
ko: '산개 😗 Fallback',
tc: '分散 😗 Fallback',
});
});
it('compose3 returns a built-in static response', () => {
const responseFunc = compose3(
Outputs.in,
' => ',
Outputs.out,
' + ',
Outputs.spread,
'alert',
);

const [result, outputStrings] = runResponseFuncWithOutputStrings(responseFunc);
assert.isObject(result);
assert.property(result, 'alertText');
assert.deepEqual(outputStrings.text, {
en: 'In => Out + Spread',
de: 'Rein => Raus + Verteilen',
fr: 'Intérieur => Extérieur + Dispersez-vous',
ja: '中へ => 外へ + さんかい',
cn: '靠近 => 远离 + 分散',
ko: '안으로 => 밖으로 + 산개',
tc: '靠近 => 遠離 + 分散',
});
});
});
Loading