7QS7R2ORWCZMTCE3SD5PJ5ZL27N3RS7YCWBHXH3JDPTHDQ3KBAMQC
GGREOYIRZE2GH62X7YXQA7IUSSB25ENLQ5OKVJYWPNI462DP37FAC
Q4BYGYNX3UZXX47EMX2GWX6M6ICSMIMCLQ7HVOYS4HB4RHYI4NEQC
HXRDRHIVGSBEAMTKJFZK43MSB53BKON6F77AARUQNWFUPSTZSBPAC
62OPHDLT2IIHK2OEH76NWB6V7E2Z5USUBNHB4X3DP2XLDI3YHY7QC
N7TZV5WJKLYKSG6SIVAID4ZCC76YHZU5U5WWQGG6TFKAV6CE3Y6AC
3QZU4ZIBA24ZXKPBGMTWX4S2GF6AOXJMPD2753CCTU7QUL2WMDXAC
EKERBH2GMXCDZSFXAQP6FGGQTTW3ZENOUM4ZKZVHWAAKGW64YLTAC
export const StatBlocksDispatch = React.createContext<any>(null);
export const ENCOUNTER_NAMES: { [id: number]: string } = {
2398: 'Shriekwing',
2418: 'Huntsman Altimor',
2383: 'Hungering Destroyer',
2402: 'Sun King\'s Salvation',
2405: 'Artificer Xy\'mox',
2406: 'Lady Inerva Darkvein',
2412: 'The Council of Blood',
2399: 'Sludgefist',
2417: 'Stone Legion Generals',
2407: 'Sire Denathrius',
};
export const DIFFICULTY_NAMES: { [id: number]: string } = {
5: 'Mythic',
};
const dateformat = {
weekday: 'long',
month: 'long',
day: 'numeric'
};
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', dateformat).format(date);
}
export function formatShortDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(date);
}
export function formatDuration(dur_ms: number, suffix: string = ' hours'): string {
return `${(dur_ms / 1000 / 60 / 60).toFixed(1)}${suffix}`;
}
const ENCOUNTER_NAMES: { [id: number]: string } = {
2398: 'Shriekwing',
2418: 'Huntsman Altimor',
2383: 'Hungering Destroyer',
2402: 'Sun King\'s Salvation',
2405: 'Artificer Xy\'mox',
2406: 'Lady Inerva Darkvein',
2412: 'The Council of Blood',
2399: 'Sludgefist',
2417: 'Stone Legion Generals',
2407: 'Sire Denathrius',
};
const DIFFICULTY_NAMES: { [id: number]: string } = {
5: 'Mythic',
};
function formatDuration(dur_ms: number): string {
return `${(dur_ms / 1000 / 60 / 60).toFixed(1)} hours`;
export interface StatProps {
logs: Log[];
region: Region;
difficulty: number;
}
export interface EncounterStats {
encounterId: number;
killLog?: Log;
killWeek?: number;
ilvlRange: [number, number];
pullCount: number;
progTime: number;
const pullCount = props.logs.reduce((count, log) => count + log.fights.length, 0);
const progTime = props.logs.reduce((time, log) => {
const duration_ms = log.fights[log.fights.length - 1].endTime - log.fights[0].startTime;
return time + duration_ms;
}, 0);
const killWeek = killLog ? tierWeek(killLog.zone, killLog.startTime, props.difficulty === 5, props.region) : undefined;
return {
encounterId: props.logs[0].fights[0].encounterId,
killLog, ilvlRange: ilvlRange as [number, number], pullCount, progTime, killWeek
}
}
function EncounterStatColumn(props: StatProps) {
const stats = encounter_stats(props);
const { killLog, ilvlRange: [minIlvl, maxIlvl], pullCount, progTime, killWeek } = stats;
const dispatch = useContext(StatBlocksDispatch);
useEffect(() => {
if(dispatch) {
dispatch(stats);
}
// the empty dep list is intentional. we do this once on load.
// eslint-disable-next-line
}, []);
<div className="grid gap-8 mt-4" style={{gridTemplateColumns: "20rem auto"}}>
<EncounterStats logs={logs} region={props.region} difficulty={props.difficulty} />
<div className="grid gap-8 mt-4 xl:grid-cols-encounter">
<EncounterStatColumn logs={logs} region={props.region} difficulty={props.difficulty} />
import EncounterOverview from './EncounterOverview';
import { ZONE_ENCOUNTERS, toRegion } from './model';
import EncounterOverview, { EncounterStats } from './EncounterOverview';
import { StatBlocksDispatch, ZONE_ENCOUNTERS, toRegion, ENCOUNTER_NAMES, DIFFICULTY_NAMES } from './model';
import { formatShortDate, formatDuration } from './format';
type StatBlocks = { [id: number]: EncounterStats };
function StatTable({ statblocks, encounters }: { statblocks: StatBlocks, encounters: number[] }) {
return (
<section className="mb-12">
<legend className="text-2xl">
Progression Overview
</legend>
<table>
<thead className="text-center">
<tr>
<th className="pl-2 pr-2 border-b border-gray-200"></th>
<th className="pl-2 pr-2 border-b border-l border-gray-200">Kill Date</th>
<th className="pl-2 pr-2 border-b border-gray-200">Kill Week</th>
<th className="pl-2 pr-2 border-b border-l border-gray-200">Pulls</th>
<th className="pl-2 pr-2 border-b border-gray-200">Prog Time</th>
<th className="pl-2 pr-2 border-b border-l border-gray-200">Min ilvl</th>
<th className="pl-2 pr-2 border-b">Max ilvl</th>
</tr>
</thead>
<tbody className="text-right">
{encounters.map(id => {
const stats = statblocks[id];
return (
<tr key={id}>
<td className="text-left pr-2 border-r border-gray-200">{ENCOUNTER_NAMES[id]}</td>
<td className="pl-2 pr-2 text-center">{(stats && stats.killLog) ? formatShortDate(stats.killLog.startTime) : ''}</td>
<td className="pl-2 pr-2 text-center">{(stats && stats.killWeek) ? `Week ${stats.killWeek}` : ''}</td>
<td className="pl-2 pr-2 border-l border-gray-200">{stats ? stats.pullCount : ''}</td>
<td className="pl-2 pr-2">{stats ? formatDuration(stats.progTime, ' hrs') : ''}</td>
<td className="pl-2 pr-2 border-l border-gray-200">{stats ? stats.ilvlRange[0].toFixed(1) : ''}</td>
<td className="pl-2 pr-2">{stats ? stats.ilvlRange[1].toFixed(1) : ''}</td>
</tr>
);
})}
</tbody>
</table>
</section>
);
}
} else if(encounters && team.id) {
header = <>{Object.values(statblocks).reduce((count, { killLog }) => count + (killLog ? 1 : 0), 0)}/{encounters.length} {DIFFICULTY_NAMES[5][0]} on </>;
content = (
<>
<div className="grid xl:grid-cols-2">
<section>
<legend className="text-2xl">
Team Overview
</legend>
</section>
<StatTable encounters={encounters} statblocks={statblocks} />
</div>
{encounters.map(encounter => (
<StatBlocksDispatch.Provider value={dispatch} key={encounter}>
<EncounterOverview encounterId={encounter} difficulty={5} team={team.id} region={toRegion(region)} />
</StatBlocksDispatch.Provider>
))}
</>
);