@@ -11,6 +11,8 @@ import { TriggerSet } from '../../../../../types/trigger';
1111
1212export interface Data extends RaidbossData {
1313 ce ?: string ;
14+ phantomJob ?: string ;
15+ phantomJobLevel ?: number ;
1416 sisyphusResoundingMemoryWedge ?: 'intercards' | 'cardinals' | 'unknown' ;
1517 demonTabletChiselTargets : string [ ] ;
1618 demonTabletRotationCounter : number ;
@@ -413,6 +415,171 @@ const magitaurOutputStrings = {
413415 } ,
414416} ;
415417
418+ // Used to filter the GainsEffect
419+ const phantomJobEffectIds = [
420+ '1092' , // Freelancer
421+ '1106' , // Knight
422+ '1107' , // Berserker
423+ '1108' , // Monk
424+ '1109' , // Ranger
425+ '1110' , // Oracle
426+ '1111' , // Thief
427+ '110A' , // Samurai
428+ '110B' , // Bard
429+ '110C' , // Geomancer
430+ '110D' , // Time Mage
431+ '110E' , // Cannonneer
432+ '110F' , // Chemist
433+ '12C3' , // Mystic Knight
434+ '12C4' , // Gladiator
435+ '12C5' , // Dancer
436+ ] ;
437+
438+ // Useful for matching on job name in condition trigger
439+ const phantomJobData = {
440+ 'freelancer' : '1092' ,
441+ 'knight' : '1106' ,
442+ 'berserker' : '1107' ,
443+ 'monk' : '1108' ,
444+ 'ranger' : '1109' ,
445+ 'oracle' : '1110' ,
446+ 'thief' : '1111' ,
447+ 'samurai' : '110A' ,
448+ 'bard' : '110B' ,
449+ 'geomancer' : '110C' ,
450+ 'timeMage' : '110D' ,
451+ 'cannoneer' : '110E' ,
452+ 'chemist' : '110F' ,
453+ 'mysticKnight' : '12C3' ,
454+ 'gladiator' : '12C4' ,
455+ 'dancer' : '12C5' ,
456+ } as const ;
457+
458+ // Return if the player has a phantom job that can dispel
459+ // Phantom Time Mage Lv 4: Dispel
460+ const phantomCanDispel = (
461+ phantomJob : string ,
462+ phantomJobLevel : number ,
463+ ) : boolean => {
464+ if ( phantomJob === phantomJobData . timeMage && phantomJobLevel >= 4 )
465+ return true ;
466+ return false ;
467+ } ;
468+
469+ // Return if the player has a phantom job that can slow
470+ // Phantom Time Mage Lv 1: Slowga
471+ /*
472+ const phantomCanSlow = (
473+ phantomJob: string,
474+ phantomJobLevel: number,
475+ ): boolean => {
476+ if (phantomJob === phantomJobData.timeMage && phantomJobLevel >= 1)
477+ return true;
478+ return false;
479+ };
480+ */
481+
482+ // Return if the player has a phantom job that can cleanse
483+ // Phantom Oracle Lv 2: Recuperation
484+ const phantomCanCleanse = (
485+ phantomJob : string ,
486+ phantomJobLevel : number ,
487+ ) : boolean => {
488+ if ( phantomJob === phantomJobData . oracle && phantomJobLevel >= 2 )
489+ return true ;
490+ return false ;
491+ } ;
492+
493+ // Return if the player has a phantom job that can freeze time
494+ // Phantom Bard Lv 2: Romeo's Ballad (aoe)
495+ // Phantom Dancer Lv 1 may be able to use Dance with Tempting Tango proc (single-target)
496+ const phantomCanFreeze = (
497+ phantomJob : string ,
498+ phantomJobLevel : number ,
499+ ) : boolean => {
500+ if ( phantomJob === phantomJobData . bard && phantomJobLevel >= 2 )
501+ return true ;
502+ if ( phantomJob === phantomJobData . dancer && phantomJobLevel >= 1 )
503+ return true ;
504+ return false ;
505+ } ;
506+
507+ // Return if the player has a phantom job that can suspend
508+ // Phantom Geomancer Lv 4: Suspend
509+ /*
510+ const phantomCanSuspend = (
511+ phantomJob: string,
512+ phantomJobLevel: number,
513+ ): boolean => {
514+ if (phantomJob === phantomJobData.geomancer && phantomJobLevel >= 4)
515+ return true;
516+ return false;
517+ };
518+ */
519+
520+ // Return if the player has a phantom job that can reduce tankbuster
521+ // Phantom Knight Lv 4: Phantom Guard + Enhanced Phantom Guard (90%)
522+ // Phantom Knight Lv 6: Pledge
523+ // Phantom Oracle Lv 6: Invulnerability
524+ // Phantom Dancer Lv 3: Steadfast Dance (10% MaxHP Barrier)
525+ // Phantom Dancer Lv 4: Mesmerize (40%)
526+ // Phantom Mystic Knight Lv 2: Magic Shell (20% MaxHP Barrier of caster)
527+ // Phantom Gladiator Lv 2: Defend (50%)
528+ /*
529+ const phantomCaresAboutTankbuster = (
530+ phantomJob: string,
531+ phantomJobLevel: number,
532+ ): boolean => {
533+ if (phantomJob === phantomJobData.knight && phantomJobLevel >= 4)
534+ return true;
535+ if (phantomJob === phantomJobData.oracle && phantomJobLevel >= 6)
536+ return true;
537+ if (phantomJob === phantomJobData.dancer && phantomJobLevel >= 3)
538+ return true;
539+ if (phantomJob === phantomJobData.mysticKnight && phantomJobLevel >= 2)
540+ return true;
541+ if (phantomJob === phantomJobData.gladiator && phantomJobLevel >= 2)
542+ return true;
543+ return false;
544+ };
545+ */
546+
547+ // Return if the player has a phantom job that can block physical damage
548+ // Phantom Samurai Lv 2: Shirahadori
549+ // Phantom Oracle Lv 6: Invulnerability
550+ /*
551+ const phantomCanBlockPhysical = (
552+ phantomJob: string,
553+ phantomJobLevel: number,
554+ ): boolean => {
555+ if (phantomJob === phantomJobData.samurai && phantomJobLevel >= 2)
556+ return true;
557+ if (phantomJob === phantomJobData.oracle && phantomJobLevel >= 6)
558+ return true;
559+ return false;
560+ };
561+ */
562+
563+ // Return if the player has a phantom job that helps with enemy aoes
564+ // Phantom Bard Lv 3: Mighty March (+20% MaxHP)
565+ // Phantom Ranger Lv 6: Occult Unicorn (40k AoE Shield)
566+ // Phantom Dancer Lv 4: Mesmerize (Require's target, 4s 40% damage reduction then 100s 10% damage reduction)
567+ // Phantom Geomance Lv 2 may be able to use Weather with Blessed Rain, Misty Mirage, Sunbath, or Cloudy Caress effects
568+ /*
569+ const phantomCaresAboutAOE = (
570+ phantomJob: string,
571+ phantomJobLevel: number,
572+ ): boolean => {
573+ if (phantomJob === phantomJobData.bard && phantomJobLevel >= 3)
574+ return true;
575+ if (phantomJob === phantomJobData.ranger && phantomJobLevel >= 6)
576+ return true;
577+ if (phantomJob === phantomJobData.dancer && phantomJobLevel >= 4)
578+ return true;
579+ return false;
580+ };
581+ */
582+
416583const triggerSet : TriggerSet < Data > = {
417584 id : 'TheOccultCrescentSouthHorn' ,
418585 zoneId : ZoneId . TheOccultCrescentSouthHorn ,
@@ -693,6 +860,42 @@ const triggerSet: TriggerSet<Data> = {
693860 console . log ( `Start CE: ??? (${ ceId } )` ) ;
694861 } ,
695862 } ,
863+ {
864+ id : 'Occult Crescent Phantom Job Tracker' ,
865+ // count also contains a Phantom Job id and level, it's supposed to be two bytes but has weird padding in logs
866+ // Expecting first two characters to be part of Phantom Job id, and the later two to be the level
867+ // First digit is the job:
868+ // Dancer = F
869+ // Gladiator = E
870+ // Mystic Knight = D
871+ // Thief = C
872+ // Oracle = B
873+ // Chemist = A
874+ // Cannoneer = 9
875+ // Time Mage = 8
876+ // Geomancer = 7
877+ // Bard = 6
878+ // Samurai = 5
879+ // Ranger = 4
880+ // Monk = 3
881+ // Berserker = 2
882+ // Knight = 1
883+ // Freelancer = null
884+ // Freelancer level is accumulation of maxed jobs +1, can also be inferred from stacks of Phantom Mastery (1082)
885+ type : 'GainsEffect' ,
886+ netRegex : { effectId : [ ...phantomJobEffectIds ] , capture : true } ,
887+ condition : Conditions . targetIsYou ( ) ,
888+ run : ( data , matches ) => {
889+ data . phantomJob = matches . effectId ;
890+ const jobData = matches . count ?. padStart ( 4 , '0' ) ;
891+
892+ // Assuming this isn't possible given the filter on statuses
893+ if ( jobData === undefined )
894+ return ;
895+
896+ data . phantomJobLevel = parseInt ( jobData . slice ( 2 ) , 16 ) ;
897+ } ,
898+ } ,
696899 {
697900 id : 'Occult Crescent Forked Tower: Blood Clear Data' ,
698901 type : 'SystemLogMessage' ,
@@ -762,6 +965,29 @@ const triggerSet: TriggerSet<Data> = {
762965 netRegex : { source : 'Crescent Berserker' , id : 'A6C3' , capture : false } ,
763966 response : Responses . getBehind ( ) ,
764967 } ,
968+ {
969+ id : 'Occult Crescent Crescent Berserker Damage Up' ,
970+ // Crescent Berserker gains Damage Up (20s) from Channeled Rage (7846)
971+ // Crescent Berserker gains Damage Up (40s) from Heightened Rage (93B1)
972+ type : 'GainsEffect' ,
973+ netRegex : { effectId : '3D' , target : 'Crescent Berserker' , capture : true } ,
974+ condition : ( data ) => {
975+ if ( data . phantomJob === undefined || data . phantomJobLevel === undefined )
976+ return false ;
977+ return phantomCanDispel ( data . phantomJob , data . phantomJobLevel ) ;
978+ } ,
979+ infoText : ( _data , matches , output ) => output . dispel ! ( { name : matches . target } ) ,
980+ outputStrings : {
981+ dispel : {
982+ en : 'Dispel ${name}' ,
983+ de : 'Entferne ${name}' ,
984+ fr : 'Dissipez ${name}' ,
985+ ja : '${name}にバフ解除' ,
986+ cn : '驱散 ${name} 的BUFF' ,
987+ ko : '${name} 버프 해제' ,
988+ } ,
989+ } ,
990+ } ,
765991 {
766992 id : 'Occult Crescent Hinkypunk Dread Dive' ,
767993 type : 'StartsUsing' ,
@@ -811,6 +1037,28 @@ const triggerSet: TriggerSet<Data> = {
8111037 netRegex : { source : 'Neo Garula' , id : 'A0E5' , capture : true } ,
8121038 response : Responses . tankBuster ( ) ,
8131039 } ,
1040+ {
1041+ id : 'Occult Crescent Neo Garula Damage Up' ,
1042+ // Neo Garula gains Damage Up (60s) after casting Agitated Groan
1043+ type : 'GainsEffect' ,
1044+ netRegex : { effectId : '489' , target : 'Neo Garula' , capture : true } ,
1045+ condition : ( data ) => {
1046+ if ( data . phantomJob === undefined || data . phantomJobLevel === undefined )
1047+ return false ;
1048+ return phantomCanDispel ( data . phantomJob , data . phantomJobLevel ) ;
1049+ } ,
1050+ infoText : ( _data , matches , output ) => output . dispel ! ( { name : matches . target } ) ,
1051+ outputStrings : {
1052+ dispel : {
1053+ en : 'Dispel ${name}' ,
1054+ de : 'Entferne ${name}' ,
1055+ fr : 'Dissipez ${name}' ,
1056+ ja : '${name}にバフ解除' ,
1057+ cn : '驱散 ${name} 的BUFF' ,
1058+ ko : '${name} 버프 해제' ,
1059+ } ,
1060+ } ,
1061+ } ,
8141062 {
8151063 id : 'Occult Crescent Lion Rampant Fearsome Glint' ,
8161064 type : 'StartsUsing' ,
@@ -3307,6 +3555,15 @@ const triggerSet: TriggerSet<Data> = {
33073555 // This will count until all 12 have started casting
33083556 type : 'StartsUsing' ,
33093557 netRegex : { source : 'Tower Idol' , id : 'A61F' , capture : true } ,
3558+ condition : ( data ) => {
3559+ if (
3560+ data . phantomJob === undefined ||
3561+ data . phantomJobLevel === undefined ||
3562+ phantomCanFreeze ( data . phantomJob , data . phantomJobLevel )
3563+ )
3564+ return true ;
3565+ return false ;
3566+ } ,
33103567 promise : async ( data , matches ) => {
33113568 const combatants = ( await callOverlayHandler ( {
33123569 call : 'getCombatants' ,
@@ -4885,7 +5142,16 @@ const triggerSet: TriggerSet<Data> = {
48855142 // TODO: Cleanse call for Doom, but it is not yet logged, it's probably 11CE?
48865143 type : 'GainsEffect' ,
48875144 netRegex : { effectId : '115C' , capture : true } ,
4888- condition : Conditions . targetIsYou ( ) ,
5145+ condition : ( data , matches ) => {
5146+ if (
5147+ ( data . me === matches . target ) &&
5148+ ( data . phantomJob === undefined ||
5149+ data . phantomJobLevel === undefined ||
5150+ phantomCanCleanse ( data . phantomJob , data . phantomJobLevel ) )
5151+ )
5152+ return true ;
5153+ return false ;
5154+ } ,
48895155 // 25s - 20s, plus some delay for buff/debuff propagation
48905156 delaySeconds : ( _data , matches ) => parseFloat ( matches . duration ) - 20 + 0.5 ,
48915157 suppressSeconds : 1 ,
0 commit comments