Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

raidboss: add Atticus (S-rank - Heritage Found) #399

Merged
merged 2 commits into from
Sep 3, 2024
Merged
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
196 changes: 195 additions & 1 deletion ui/raidboss/data/07-dt/hunts/heritage_found.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,55 @@ import ZoneId from '../../../../../resources/zone_id';
import { RaidbossData } from '../../../../../types/data';
import { TriggerSet } from '../../../../../types/trigger';

// TODO: Add triggers for Atticus the Primogenitor (S-Rank)
// See wall of text below for explanation of Atticus's Breath Sequence mechanic.
type CleaveDir = 'front' | 'right' | 'left';
const atticusNpcYellMap: { [id: string]: CleaveDir } = {
'41EA': 'front', // "Sssavage might..." (1st cleave)
'41EB': 'right', // "Triple..."
'41EC': 'left', // "Breath?"
'41ED': 'left', // "Triple..."
'41EE': 'right', // "Breath?"
'41EF': 'right', // "Dessstroy..." (1st cleave)
'41F0': 'left', // "Combination..."
'41F1': 'front', // "Breath?"
'41F2': 'front', // "Combination..."
'41F3': 'left', // "Breath?"
'41F4': 'left', // "Great ssstrength..." (1st cleave)
'41F5': 'front', // "Sssuperlative..."
'41F6': 'right', // "Breath?"
'41F7': 'right', // "Sssuperlative..."
'41F8': 'front', // "Breath?"
'41F9': 'front', // "Intenssse heat..." (4th cleave)
'41FA': 'right', // "Sssearing..."
'41FB': 'left', // "Breath?"
'41FC': 'left', // "Sssearing..."
'41FD': 'right', // "Breath?"
'41FE': 'right', // "To sssmithereens..." (4th cleave)
'41FF': 'left', // "Burssst..."
'4200': 'front', // "Breath?"
'4201': 'front', // "Burssst..."
'4202': 'left', // "Breath?"
'4203': 'left', // "Ssscorching heat..." (4th cleave)
'4204': 'front', // "Lassst..."
'4205': 'right', // "Breath?"
'4206': 'right', // "Lassst..."
'4207': 'front', // "Breath?"
} as const;
const atticusNpcYellIds = Object.keys(atticusNpcYellMap);
const atticusBreathSeqAbilityIds = ['985B', '985C', '985D'];

export interface Data extends RaidbossData {
magnetronDebuff?: 'positive' | 'negative';
storedShockSafe?: 'intercards' | 'cardinals';
atticusCleaves: CleaveDir[];
}

const triggerSet: TriggerSet<Data> = {
id: 'HeritageFound',
zoneId: ZoneId.HeritageFound,
initData: () => ({
atticusCleaves: [],
}),
triggers: [
// ****** A-RANK: Heshuala ****** //
{
Expand Down Expand Up @@ -184,7 +223,162 @@ const triggerSet: TriggerSet<Data> = {
},
},
},

// ****** S-RANK: Atticus the Primogenitor ****** //
{
id: 'Hunt Atticus Intimidation',
type: 'StartsUsing',
netRegex: { id: '9866', source: 'Atticus the Primogenitor', capture: false },
response: Responses.aoe(),
},
{
id: 'Hunt Atticus Pyric Blast',
type: 'StartsUsing',
netRegex: { id: '9865', source: 'Atticus the Primogenitor' },
response: Responses.stackMarkerOn(),
},

// Atticus's main mechanic, Breath Sequence, will do either 3 or 6 directional cleaves
// that are telegraphed solely by the order of `NpcYell` lines from one of its three heads.
// There are no 0x14/0x15 lines (or any other lines) that indicate the sequence of cleaves.
// Atticus will emit an 0x14 line at the start of Breath Sequence, which is one of three ids
// indicating the direction of the first cleave (985B: front, 985C: right, 985D: left).
// The cast id does not otherwise indicate the sequence or even the # of cleaves (3 or 6).

// When performing 3 cleaves, cleaves are never repeated (e.g., Atticus will always cleave
// left, right, and front, in any order). A 6-cleave combo is essentially 2 sets of 3 cleaves.
// The first set will cleave all three directions, as will the second set.
// The sequence of cleaves in the second set has no correlation to the first set.

// At the start of the encoounter, Atticus will perform 3 cleaves. It will then use Brutality
// (giving itself the Twisted Tongue buff that speeds up all future NpcYell lines), followed
// by 3 more cleaves. Next, it will perform a 6-cleave combo. From that point on, Atticus can
// perform either 3- or 6-cleave sets, with no dialog or abilities (other than the number of
// `NpcYell` lines) to telegraph whether it will use 3 or 6 cleaves.

// The `NpcYell` ids range from `41EA`-`4207` (`4208`-`420A` are each used once as boss dialog,
// but are not reused). Many of these rows have duplicative text, but the `Unknown2` property
// on each row corresponds to which head will say the line: 18 = left, 25 = middle, 19 = right.
// See, e.g., https://beta.xivapi.com/api/1/sheet/NpcYell/16874.
{
id: 'Hunt Atticus Breath Sequence Collect',
type: 'NpcYell',
netRegex: { npcNameId: '3364', npcYellId: atticusNpcYellIds },
run: (data, matches) => {
const cleaveDir = atticusNpcYellMap[matches.npcYellId];
if (cleaveDir === undefined)
return;
data.atticusCleaves.push(cleaveDir);
},
},
// For a 3-cleave sequence, we can just tell the player to go 3->1.
{
id: 'Hunt Atticus Breath Sequence 3-Cleave',
type: 'StartsUsing',
netRegex: { id: atticusBreathSeqAbilityIds, capture: false },
condition: (data) => data.atticusCleaves.length === 3,
delaySeconds: 0.2, // tight timing between 0x14 line and final NpcYell, so add safety margin
durationSeconds: 9.8, // time from 0x14 to final cleave's 0x15/0x16
alertText: (data, _matches, output) => {
const [cleave1, , cleave3] = data.atticusCleaves;
if (cleave1 === undefined || cleave3 === undefined)
return;
return output.combo!({ dir1: output[cleave3]!(), dir2: output[cleave1]!() });
},
outputStrings: {
combo: {
en: 'Start ${dir1} => ${dir2}',
},
front: Outputs.front,
right: Outputs.right,
left: Outputs.left,
},
},
// For a 6-cleave sequence, it's more complicated. Because the second set of cleaves can be in
// any order, agnostic of the first set, we can't rely solely on "3->1" movement, and timing
// is very tight to move out of a safe spot before the next cleave hits that spot.
// However, there are only six possible sequences for the second set of cleaves relative to the
// first set, so we can provide a consistent call for each possibility that will minimize
// damage/deaths for slow-reacting players. The possibilities (and safe-spot movements) are:
//
// Cleave Pattern ==> Safe Spot Movement OutputString
// -------------- ------------------------------ ------------
// 1 2 3 -> 1 2 3 ==> Start 3 -> 1 -> 2 -> 3 -> 1 [rotate]
// 1 2 3 -> 1 3 2 ==> Start 3 -> 1 -> 2 (for 2) -> 1 [lateDelay1]
// 1 2 3 -> 2 1 3 ==> Start 3 -> 1 (for 2) -> 3 -> 2 [earlyDelay]
// 1 2 3 -> 2 3 1 ==> Start 3 -> 1 (for 3) -> 2 [bigDelay2]
// 1 2 3 -> 3 1 2 ==> Start 3 -> 1 -> 2 (for 2) -> 3 [lateDelay3]
// 1 2 3 -> 3 2 1 ==> Start 3 -> 1 (for 3) -> 3 [bigDelay3]
{
id: 'Hunt Atticus Breath Sequence 6-Cleave',
type: 'StartsUsing',
netRegex: { id: atticusBreathSeqAbilityIds, capture: false },
condition: (data) => data.atticusCleaves.length === 6,
delaySeconds: 0.2, // tight timing between 0x14 line and final NpcYell, so add safety margin
durationSeconds: 16.8, // time from 0x14 to final cleave's 0x15/0x16
alertText: (data, _matches, output) => {
const [cleave1, cleave2, cleave3, cleave4, cleave5, cleave6] = data.atticusCleaves;
if (
cleave1 === undefined || cleave2 === undefined || cleave3 === undefined ||
cleave4 === undefined || cleave5 === undefined || cleave6 === undefined
)
return;

const dir1 = output[cleave1]!();
const dir2 = output[cleave2]!();
const dir3 = output[cleave3]!();

if (cleave1 === cleave4) {
if (cleave2 === cleave5)
return output.rotate!({ dir1: dir1, dir2: dir2, dir3: dir3 }); // 1 2 3 -> 1 2 3
return output.lateDelay1!({ dir1: dir1, dir2: dir2, dir3: dir3 }); // 1 2 3 -> 1 3 2
} else if (cleave2 === cleave4) {
if (cleave1 === cleave5)
return output.earlyDelay!({ dir1: dir1, dir2: dir2, dir3: dir3 }); // 1 2 3 -> 2 1 3
return output.bigDelay2!({ dir1: dir1, dir2: dir2, dir3: dir3 }); // 1 2 3 -> 2 3 1
}

if (cleave1 === cleave5)
return output.lateDelay3!({ dir1: dir1, dir2: dir2, dir3: dir3 }); // 1 2 3 -> 3 1 2
return output.bigDelay3!({ dir1: dir1, dir3: dir3 }); // 1 2 3 -> 3 2 1
},
// For simplicity, rather than translating each cleave to a safe spot and the outputting
// that spot, the cleave sequence is simply mapped to dir1-dir3, and the outputStrings
// take care of calling the correct order per the table above (e.g. dir3->dir1 etc.).
outputStrings: {
rotate: {
en: 'Start ${dir3} => ${dir1} => ${dir2} (Keep Rotating)',
},
earlyDelay: {
en: 'Start ${dir3} => ${dir1} (for 2) => ${dir3} => ${dir2}',
},
lateDelay1: {
en: 'Start ${dir3} => ${dir1} => ${dir2} (for 2) => ${dir1}',
},
lateDelay3: {
en: 'Start ${dir3} => ${dir1} => ${dir2} (for 2) => ${dir3}',
},
bigDelay2: {
en: 'Start ${dir3} => ${dir1} (for 3) => ${dir2}',
},
bigDelay3: {
en: 'Start ${dir3} => ${dir1} (for 3) => ${dir3}',
},
front: Outputs.front,
right: Outputs.right,
left: Outputs.left,
unknown: Outputs.unknown,
},
},
// Because we only fire the output triggers if 3 or 6 cleaves are collected,
// use a separate cleanup trigger (in case the player arrived mid-sequence or
// if not all NpcYell lines were collected).
{
id: 'Hunt Atticus Breath Sequence Cleanup',
type: 'Ability',
netRegex: { id: atticusBreathSeqAbilityIds, capture: false },
run: (data) => data.atticusCleaves = [],
},
],
timelineReplace: [
{
Expand Down
Loading