diff --git a/.gitignore b/.gitignore index 940124d39c8..ca22e696d00 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ env/ venv/ npm-package/ .eslintcache + +ui/raidboss/data/99-custom/* +!ui/raidboss/data/99-custom/readme.txt diff --git a/docs/CactbotCustomization.md b/docs/CactbotCustomization.md index 45a4fe78bf0..24253f78fa4 100644 --- a/docs/CactbotCustomization.md +++ b/docs/CactbotCustomization.md @@ -372,6 +372,28 @@ Your best resources for learning how to write cactbot triggers is the [trigger guide](RaidbossGuide.md) and also reading through existing triggers in [ui/raidboss/data](../ui/raidboss/data). +### Trigger Set Override + +When you define a trigger set in your user files with the same `id` as a built-in trigger set, the entire built-in trigger set will be completely overridden. + +For example: + +```javascript +Options.Triggers.push({ + id: 'TheUnendingCoilOfBahamutUltimate', // Same ID as the built-in trigger set + zoneId: ZoneId.TheUnendingCoilOfBahamutUltimate, + triggers: [ + // Your custom triggers + { + id: 'My Custom Trigger', + // ... trigger content + }, + ], +}); +``` + +In this example, because the `id` matches the built-in Unending Coil of Bahamut (Ultimate) trigger set, none of the built-in triggers will execute, only your custom triggers will run. You need to re-implement all necessary trigger logic in your custom file. + ## Overriding Raidboss Timelines Some customization of timelines can be done via the [cactbot config UI](#using-the-cactbot-ui). diff --git a/docs/ko-KR/CactbotCustomization.md b/docs/ko-KR/CactbotCustomization.md index 64cd886bcfd..d85ea42ad0f 100644 --- a/docs/ko-KR/CactbotCustomization.md +++ b/docs/ko-KR/CactbotCustomization.md @@ -317,6 +317,28 @@ cactbot 트리거 작성하는 방법을 더 자세히 배우려면 [트리거 가이드](../RaidbossGuide.md)와 [ui/raidboss/data](../../ui/raidboss/data)에 이미 존재하는 트리거를 읽어보세요. +### 트리거 세트 덮어쓰기 + +사용자 파일에서 내장 트리거 세트와 동일한 `id`를 가진 트리거 세트를 정의하면, 전체 내장 트리거 세트가 완전히 덮어쓰여집니다. + +예시: + +```javascript +Options.Triggers.push({ + id: 'TheUnendingCoilOfBahamutUltimate', // 내장 트리거 세트 ID와 동일 + zoneId: ZoneId.TheUnendingCoilOfBahamutUltimate, + triggers: [ + // 사용자 정의 트리거 + { + id: 'My Custom Trigger', + // ... 트리거 내용 + }, + ], +}); +``` + +이 예시에서 `id`가 내장된 바하무트 절경전 트리거 세트와 동일하기 때문에, 내장 트리거는 실행되지 않고 사용자 정의 트리거만 실행됩니다. 사용자 정의 파일에서 필요한 모든 트리거 로직을 다시 구현해야 합니다. + ## Raidboss 타임라인 덮어쓰기 Raidboss 타임라인을 덮어쓰는 것은 [Raidboss 트리거 덮어쓰기](#raidboss-트리거-덮어쓰기)와 비슷합니다. diff --git a/docs/zh-CN/CactbotCustomization.md b/docs/zh-CN/CactbotCustomization.md index 247df52a421..3feb4ddc091 100644 --- a/docs/zh-CN/CactbotCustomization.md +++ b/docs/zh-CN/CactbotCustomization.md @@ -249,6 +249,28 @@ Options.Triggers.push([ 我们推荐阅读 [触发器指南](RaidbossGuide.md) 以了解如何撰写 Cactbot 的触发器,当然你也可以直接看 [ui/raidboss/data](../../ui/raidboss/data) 中现有的触发器代码。 +### 触发器集合覆盖 + +当你在用户文件中定义了与内置触发器集合相同 `id` 的触发器集合时,整个内置触发器集合将被完全覆盖。 + +例如: + +```javascript +Options.Triggers.push({ + id: 'TheUnendingCoilOfBahamutUltimate', // 与内置的触发器集合ID相同 + zoneId: ZoneId.TheUnendingCoilOfBahamutUltimate, + triggers: [ + // 你的自定义触发器 + { + id: 'My Custom Trigger', + // ... 触发器内容 + }, + ], +}); +``` + +在这个例子中,由于 `id` 与内置的巴哈姆特绝境战触发器集合相同,内置的所有触发器都不会执行,只会执行你自定义的触发器。你需要在自定义文件中重新实现所有需要的触发器逻辑。 + ## Raidboss 时间轴自定义 一些自定义操作可以通过 [Cactbot 配置界面](#使用-cactbot-配置界面) 实现。你可以在这个界面隐藏或重命名现有的时间轴条目,也可以添加自定义时间轴条目等。 diff --git a/docs/zh-TW/CactbotCustomization.md b/docs/zh-TW/CactbotCustomization.md index 1977ae0f0f4..c0e65ee69ac 100644 --- a/docs/zh-TW/CactbotCustomization.md +++ b/docs/zh-TW/CactbotCustomization.md @@ -231,6 +231,28 @@ Options.Triggers.push([ 我們推薦閱讀 [觸發器指南](RaidbossGuide.md) 以瞭解如何撰寫cactbot的觸發器, 當然您也可以直接看 [ui/raidboss/data](../ui/raidboss/data) 中現有的觸發器程式碼。 +### 觸發器集合覆蓋 + +當您在使用者檔案中定義了與內建觸發器集合相同 `id` 的觸發器集合時,整個內建觸發器集合將被完全覆蓋。 + +例如: + +```javascript +Options.Triggers.push({ + id: 'TheUnendingCoilOfBahamutUltimate', // 與內建的觸發器集合ID相同 + zoneId: ZoneId.TheUnendingCoilOfBahamutUltimate, + triggers: [ + // 您的自定義觸發器 + { + id: 'My Custom Trigger', + // ... 觸發器內容 + }, + ], +}); +``` + +在這個例子中,由於 `id` 與內建的巴哈姆特絕境戰觸發器集合相同,內建的所有觸發器都不會執行,只會執行您自定義的觸發器。您需要在自定義檔案中重新實現所有需要的觸發器邏輯。 + ## Raidboss時間軸自定義 自定義時間軸與 [自定義觸發器](#raidboss觸發器自定義) 差不多。 diff --git a/test/test_data_files.ts b/test/test_data_files.ts index c994619dcae..269b4e81455 100644 --- a/test/test_data_files.ts +++ b/test/test_data_files.ts @@ -38,7 +38,7 @@ const oopsyFiles: string[] = []; const processInputs = (inputPath: string[]) => { inputPath.forEach((path: string) => { walkDirSync(path, (filepath) => { - if (/\/(?:raidboss|oopsy)_manifest.txt/.test(filepath)) { + if (/\/(?:raidboss|oopsy)_manifest.txt/.test(filepath) || /\/99-custom\//.test(filepath)) { return; } if (/\/raidboss\/data\/.*\.txt/.test(filepath)) { diff --git a/types/trigger.d.ts b/types/trigger.d.ts index 3889b9348ba..0b320d566ab 100644 --- a/types/trigger.d.ts +++ b/types/trigger.d.ts @@ -257,6 +257,7 @@ export type LooseTriggerSet = timelineTriggers?: LooseTimelineTrigger[]; filename?: string; isUserTriggerSet?: boolean; + overriddenByFile?: string; }; export interface RaidbossFileData { diff --git a/ui/config/config.css b/ui/config/config.css index 2cd302df1bb..5130c393f1f 100644 --- a/ui/config/config.css +++ b/ui/config/config.css @@ -187,6 +187,16 @@ html { margin-right: 20px; } +.trigger-set-override-warning { + padding: 20px; + text-align: center; + color: #856404; + background-color: #fff3cd; + border: 1px solid #ffc107; + border-radius: 4px; + margin: 10px; +} + .timeline-edit-container { margin: 12px 0 12px 12px; grid-column-end: span 2; diff --git a/ui/raidboss/data/99-custom/readme.txt b/ui/raidboss/data/99-custom/readme.txt new file mode 100644 index 00000000000..15b2c4626a7 --- /dev/null +++ b/ui/raidboss/data/99-custom/readme.txt @@ -0,0 +1 @@ +This directory is for local, developer-specific TypeScript trigger sets. diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index fe1a5506eee..017c4f3744d 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -703,21 +703,24 @@ export class PopupText { // User triggers must come last so that they override built-in files. this.triggerSets.push(...this.options.Triggers); - // Eliminate any trigger sets with duplicate ids and record a lookup by id. - this.triggerSets = this.triggerSets.filter((triggerSet) => { - if (triggerSet.id === undefined) - return true; - if (this.triggerSetsById[triggerSet.id] !== undefined) { + // Eliminate any trigger sets with duplicate ids, allowing later ones to override earlier ones. + const uniqueById = new Map(); + const noIdList: typeof this.triggerSets = []; + for (const triggerSet of this.triggerSets) { + if (triggerSet.id === undefined) { + noIdList.push(triggerSet); + continue; + } + const existing = uniqueById.get(triggerSet.id); + if (existing !== undefined) { console.log( - `${ - triggerSet.filename ?? '???' - } has duplicate triggerSet id ${triggerSet.id}, ignoring triggers`, + `Overriding trigger set id '${triggerSet.id}' from '${existing.filename}' with '${triggerSet.filename}'`, ); - return false; } - this.triggerSetsById[triggerSet.id] = triggerSet; - return true; - }); + uniqueById.set(triggerSet.id, triggerSet); + } + this.triggerSets = [...noIdList, ...uniqueById.values()]; + this.triggerSetsById = Object.fromEntries(uniqueById); } OnChangeZone(e: EventResponses['ChangeZone']): void { diff --git a/ui/raidboss/raidboss_config.ts b/ui/raidboss/raidboss_config.ts index a8e09009596..c005d154929 100644 --- a/ui/raidboss/raidboss_config.ts +++ b/ui/raidboss/raidboss_config.ts @@ -745,6 +745,18 @@ class RaidbossConfigurator { triggerOptions.classList.add('trigger-file-options'); triggerContainer.appendChild(triggerOptions); + // If this trigger set is overridden, show warning instead of triggers + if (info.triggerSet.overriddenByFile !== undefined) { + const warningDiv = document.createElement('div'); + warningDiv.classList.add('trigger-set-override-warning'); + const baseText = this.base.translate(kMiscTranslations.overriddenByFile); + const detailText = baseText.replace('${file}', info.triggerSet.overriddenByFile); + const warningText = this.base.translate(kMiscTranslations.warning); + warningDiv.innerHTML = `${warningText}: ${detailText}`; + triggerOptions.appendChild(warningDiv); + continue; + } + for (const [trigId, trig] of Object.entries(info.triggers ?? {})) { // Don't construct triggers that won't show anything. let hasOutputFunc = false; @@ -1528,6 +1540,12 @@ class RaidbossConfigurator { // id so that the ui can disable overriding information. const previousTriggerWithId: { [id: string]: ConfigLooseTrigger } = {}; + // While walking through trigger sets, record any previous trigger sets with the same + // id so that the ui can show overriding information. + const previousTriggerSetWithId: { + [id: string]: { triggerSet: ConfigLooseTriggerSet; filename?: string }; + } = {}; + for (const item of Object.values(map)) { // TODO: maybe each trigger set needs a zone name, and we should // use that instead of the filename??? @@ -1544,6 +1562,17 @@ class RaidbossConfigurator { if (!triggerSet.isUserTriggerSet && triggerSet.filename !== undefined) flattenTimeline(triggerSet, triggerSet.filename, timelineFiles); + // Track if this trigger set overrides any previous trigger set with the same id. + if (triggerSet.id !== undefined) { + const previous = previousTriggerSetWithId[triggerSet.id]; + if (previous) + previous.triggerSet.overriddenByFile = triggerSet.filename; + previousTriggerSetWithId[triggerSet.id] = { + triggerSet: triggerSet, + filename: triggerSet.filename, + }; + } + item.triggers = {}; for (const [key, triggerArr] of Object.entries(rawTriggers)) { for (const baseTrig of triggerArr) {