@@ -33,15 +33,24 @@ interface EloEntry {
3333 assists: number | null ;
3434}
3535
36+ type StatSource = " 5stack" | " external" ;
37+
3638const props = defineProps <{
3739 open: boolean ;
3840 playerId: string | number | null ;
3941 playerName? : string | null ;
4042 defaultMode? : Mode ;
4143 defaultRange? : RangeKey ;
4244 excludeTournaments? : boolean ;
45+ // Mirrors the profile's stat source so the drill-down shows the same world
46+ // (5Stack ELO vs External rank) instead of blending them.
47+ source? : StatSource ;
4348}>();
4449
50+ const sourceRef = computed <StatSource >(() =>
51+ props .source === " external" ? " external" : " 5stack" ,
52+ );
53+
4554const emit = defineEmits <{
4655 " update:open" : [value : boolean ];
4756}>();
@@ -61,6 +70,18 @@ const selectedRange = ref<RangeKey>(props.defaultRange ?? "1y");
6170const selectedMode = ref <Mode >(props .defaultMode ?? " all" );
6271const history = ref <EloEntry []>([]);
6372const premierHistory = ref <EloEntry []>([]);
73+ const rankHistoryRows = ref <
74+ Array <{
75+ rank: number ;
76+ rank_type: number ;
77+ previous_rank: number | null ;
78+ match_id: string | null ;
79+ observed_at: string ;
80+ }>
81+ > ([]);
82+ const performanceRows = ref <Array <{ type: string ; match_result: string | null }>>(
83+ [],
84+ );
6485const loading = ref (false );
6586let queryGen = 0 ;
6687
@@ -101,8 +122,8 @@ const ELO_HISTORY_QUERY = gql`
101122 }
102123` ;
103124
104- const PREMIER_HISTORY_QUERY = gql `
105- query PlayerPremierRankHistoryDrillDown (
125+ const RANK_HISTORY_QUERY = gql `
126+ query PlayerRankHistoryDrillDown (
106127 $where: player_premier_rank_history_bool_exp!
107128 $limit: Int!
108129 ) {
@@ -112,12 +133,27 @@ const PREMIER_HISTORY_QUERY = gql`
112133 limit: $limit
113134 ) {
114135 rank
136+ rank_type
137+ previous_rank
115138 match_id
116139 observed_at
117140 }
118141 }
119142` ;
120143
144+ // External win/loss for the stats strip — rank history carries no result.
145+ const PERFORMANCE_QUERY = gql `
146+ query PlayerPerformanceDrillDown(
147+ $where: v_player_match_performance_bool_exp!
148+ $limit: Int!
149+ ) {
150+ v_player_match_performance(where: $where, limit: $limit) {
151+ type
152+ match_result
153+ }
154+ }
155+ ` ;
156+
121157const sinceTimestamp = computed (() => {
122158 const r = ranges .find ((x ) => x .key === selectedRange .value );
123159 if (! r || r .days === null ) return null ;
@@ -156,48 +192,102 @@ async function fetchHistory() {
156192 if (! props .open || ! props .playerId ) {
157193 history .value = [];
158194 premierHistory .value = [];
195+ rankHistoryRows .value = [];
196+ performanceRows .value = [];
159197 return ;
160198 }
161199 const gen = ++ queryGen ;
162200 loading .value = true ;
163201
164- const premierWhere: Record <string , any > = {
165- steam_id: { _eq: props .playerId },
166- // The history table now holds Competitive/Wingman rows too — keep this
167- // drill-down Premier-only.
168- rank_type: { _eq: 11 },
169- };
170- if (sinceTimestamp .value ) {
171- premierWhere .observed_at = { _gte: sinceTimestamp .value };
172- }
173-
174202 try {
175- const [eloRes, premierRes] = await Promise .all ([
176- client .query ({
203+ if (sourceRef .value === " 5stack" ) {
204+ // 5Stack ELO only.
205+ const eloRes = await client .query ({
177206 query: ELO_HISTORY_QUERY ,
178207 variables: { where: whereClause .value , limit: rangeLimit .value },
179208 fetchPolicy: " network-only" ,
180- }),
181- client .query ({
182- query: PREMIER_HISTORY_QUERY ,
183- variables: { where: premierWhere , limit: rangeLimit .value },
184- fetchPolicy: " network-only" ,
185- }),
186- ]);
187- if (gen !== queryGen ) {
188- return ;
209+ });
210+ if (gen !== queryGen ) return ;
211+ history .value = ((eloRes .data as any )?.v_player_elo ?? []) as EloEntry [];
212+ premierHistory .value = [];
213+ rankHistoryRows .value = [];
214+ performanceRows .value = [];
215+ } else {
216+ // External: Valve rank history (all rank types) + performance for W/L.
217+ const rankWhere: Record <string , any > = {
218+ steam_id: { _eq: props .playerId },
219+ };
220+ if (sinceTimestamp .value ) {
221+ rankWhere .observed_at = { _gte: sinceTimestamp .value };
222+ }
223+ const perfWhere: Record <string , any > = {
224+ player_steam_id: { _eq: props .playerId },
225+ source: { _neq: " 5stack" },
226+ };
227+ if (sinceTimestamp .value ) {
228+ perfWhere .match_created_at = { _gte: sinceTimestamp .value };
229+ }
230+ if (props .excludeTournaments ) {
231+ perfWhere .match = { is_tournament_match: { _eq: false } };
232+ }
233+ const [rankRes, perfRes] = await Promise .all ([
234+ client .query ({
235+ query: RANK_HISTORY_QUERY ,
236+ variables: { where: rankWhere , limit: rangeLimit .value },
237+ fetchPolicy: " network-only" ,
238+ }),
239+ client .query ({
240+ query: PERFORMANCE_QUERY ,
241+ variables: { where: perfWhere , limit: rangeLimit .value },
242+ fetchPolicy: " network-only" ,
243+ }),
244+ ]);
245+ if (gen !== queryGen ) return ;
246+ history .value = [];
247+ rankHistoryRows .value = ((rankRes .data as any )
248+ ?.player_premier_rank_history ?? []) as typeof rankHistoryRows .value ;
249+ performanceRows .value = ((perfRes .data as any )
250+ ?.v_player_match_performance ?? []) as typeof performanceRows .value ;
251+ // Premier (rank_type 11) series.
252+ let prev: number | null = null ;
253+ premierHistory .value = rankHistoryRows .value
254+ .filter ((r ) => r .rank_type === 11 )
255+ .map ((r ) => {
256+ const change = prev === null ? 0 : r .rank - prev ;
257+ prev = r .rank ;
258+ return {
259+ current_elo: r .rank ,
260+ updated_elo: r .rank ,
261+ elo_change: change ,
262+ match_created_at: r .observed_at ,
263+ match_id: r .match_id ,
264+ match_result: null ,
265+ type: " Premier" ,
266+ kills: null ,
267+ deaths: null ,
268+ assists: null ,
269+ };
270+ });
189271 }
190- history .value = ((eloRes .data as any )?.v_player_elo ?? []) as EloEntry [];
191-
192- const rows = ((premierRes .data as any )?.player_premier_rank_history ??
193- []) as Array <{
194- rank: number ;
195- match_id: string | null ;
196- observed_at: string ;
197- }>;
198- let prev: number | null = null ;
199- premierHistory .value = rows .map ((r ) => {
200- const change = prev === null ? 0 : r .rank - prev ;
272+ } finally {
273+ if (gen === queryGen ) {
274+ loading .value = false ;
275+ }
276+ }
277+ }
278+
279+ // Competitive (12) / Wingman (6) Valve skill-group series from rank history.
280+ function buildRankSeries(rankType : number , type : string ): EloEntry [] {
281+ let prev: number | null = null ;
282+ return rankHistoryRows .value
283+ .filter ((r ) => r .rank_type === rankType )
284+ .map ((r ) => {
285+ const change =
286+ r .previous_rank != null
287+ ? r .rank - Number (r .previous_rank )
288+ : prev === null
289+ ? 0
290+ : r .rank - prev ;
201291 prev = r .rank ;
202292 return {
203293 current_elo: r .rank ,
@@ -206,34 +296,42 @@ async function fetchHistory() {
206296 match_created_at: r .observed_at ,
207297 match_id: r .match_id ,
208298 match_result: null ,
209- type: " Premier " ,
299+ type ,
210300 kills: null ,
211301 deaths: null ,
212302 assists: null ,
213- };
303+ } as EloEntry ;
214304 });
215- } finally {
216- if (gen === queryGen ) {
217- loading .value = false ;
218- }
219- }
220305}
306+ const competitiveRank = computed (() => buildRankSeries (12 , " Competitive" ));
307+ const wingmanRank = computed (() => buildRankSeries (6 , " Wingman" ));
221308
222309watch (
223310 () => [
224311 props .open ,
225312 props .playerId ,
226313 selectedRange .value ,
227314 props .excludeTournaments ,
315+ sourceRef .value ,
228316 ],
229317 () => {
230318 void fetchHistory ();
231319 },
232320 { immediate: true },
233321);
234322
323+ // Keep the selected mode valid for the active source.
324+ watch (sourceRef , () => {
325+ const valid = modeOptions .value .map ((m ) => m .key );
326+ if (! valid .includes (selectedMode .value )) {
327+ selectedMode .value = sourceRef .value === " external" ? " Premier" : " all" ;
328+ }
329+ });
330+
235331const filteredHistory = computed <EloEntry []>(() => {
236- if (selectedMode .value === " Premier" ) {
332+ if (sourceRef .value === " external" ) {
333+ if (selectedMode .value === " Competitive" ) return competitiveRank .value ;
334+ if (selectedMode .value === " Wingman" ) return wingmanRank .value ;
237335 return premierHistory .value ;
238336 }
239337 if (selectedMode .value === " all" ) {
@@ -242,14 +340,26 @@ const filteredHistory = computed<EloEntry[]>(() => {
242340 return history .value .filter ((e ) => e .type === selectedMode .value );
243341});
244342
343+ // Valve rank type for the chart's skill-group ladder (badges + integer steps).
344+ const chartRankType = computed <number | null >(() =>
345+ sourceRef .value === " external" &&
346+ (selectedMode .value === " Competitive" || selectedMode .value === " Wingman" )
347+ ? selectedMode .value === " Wingman"
348+ ? 6
349+ : 12
350+ : null ,
351+ );
352+
245353const chartSeries = computed (() => {
246- if (selectedMode .value === " Premier" ) {
247- return premierHistory .value .length > 0
354+ // External: one rank series for the active mode (Premier / Comp / Wingman).
355+ if (sourceRef .value === " external" ) {
356+ const s = filteredHistory .value ;
357+ return s .length > 0
248358 ? [
249359 {
250- key: " Premier " ,
251- label: " Premier " ,
252- history: premierHistory . value ,
360+ key: selectedMode . value ,
361+ label: selectedMode . value ,
362+ history: s ,
253363 focus: true ,
254364 },
255365 ]
@@ -292,10 +402,18 @@ const chartSeries = computed(() => {
292402const stats = computed (() => {
293403 const list = filteredHistory .value ;
294404 const headlineList =
295- selectedMode .value === " all"
405+ sourceRef . value === " 5stack " && selectedMode .value === " all"
296406 ? history .value .filter ((e ) => e .type === " Competitive" )
297407 : list ;
298- if (list .length === 0 ) {
408+ // Win/loss source: 5Stack reads it off the ELO entries; External has no
409+ // result on rank rows, so it comes from the performance query (by mode).
410+ const perfList: { match_result: string | null }[] =
411+ sourceRef .value === " external"
412+ ? selectedMode .value === " Competitive" || selectedMode .value === " Wingman"
413+ ? performanceRows .value .filter ((p ) => p .type === selectedMode .value )
414+ : performanceRows .value
415+ : list ;
416+ if (list .length === 0 && perfList .length === 0 ) {
299417 return {
300418 current: null as number | null ,
301419 peak: null as number | null ,
@@ -333,10 +451,13 @@ const stats = computed(() => {
333451 }
334452 }
335453
336- for (const e of list ) {
454+ for (const e of perfList ) {
337455 if (e .match_result === " won" || e .match_result === " win" ) wins ++ ;
338456 else if (e .match_result === " lost" || e .match_result === " loss" ) losses ++ ;
339457 else if (e .match_result === " tied" || e .match_result === " tie" ) ties ++ ;
458+ }
459+
460+ for (const e of list ) {
340461 if (typeof e .elo_change === " number" ) {
341462 changeSum += e .elo_change ;
342463 changeCount ++ ;
@@ -365,13 +486,26 @@ const stats = computed(() => {
365486 };
366487});
367488
368- const modeOptions = computed <{ key: Mode ; label: string }[]>(() => [
369- { key: " all" , label: t (" pages.leaderboard.match_types.all" ) },
370- { key: " Competitive" , label: t (" pages.leaderboard.match_types.competitive" ) },
371- { key: " Wingman" , label: t (" pages.leaderboard.match_types.wingman" ) },
372- { key: " Duel" , label: t (" pages.leaderboard.match_types.duel" ) },
373- { key: " Premier" , label: " Premier" },
374- ]);
489+ const modeOptions = computed <{ key: Mode ; label: string }[]>(() =>
490+ sourceRef .value === " external"
491+ ? [
492+ { key: " Premier" , label: " Premier" },
493+ {
494+ key: " Competitive" ,
495+ label: t (" pages.leaderboard.match_types.competitive" ),
496+ },
497+ { key: " Wingman" , label: t (" pages.leaderboard.match_types.wingman" ) },
498+ ]
499+ : [
500+ { key: " all" , label: t (" pages.leaderboard.match_types.all" ) },
501+ {
502+ key: " Competitive" ,
503+ label: t (" pages.leaderboard.match_types.competitive" ),
504+ },
505+ { key: " Wingman" , label: t (" pages.leaderboard.match_types.wingman" ) },
506+ { key: " Duel" , label: t (" pages.leaderboard.match_types.duel" ) },
507+ ],
508+ );
375509
376510function fmtInt(n : number | null | undefined ): string {
377511 if (n === null || n === undefined || ! Number .isFinite (n )) return " —" ;
@@ -622,7 +756,7 @@ function fmtDate(iso: string | null | undefined): string {
622756 </button >
623757 </div >
624758 <div v-else class =" h-[360px] sm:h-[420px]" >
625- <PlayerEloChart :series =" chartSeries " />
759+ <PlayerEloChart :series =" chartSeries " : rank-type = " chartRankType " />
626760 </div >
627761 </div >
628762
0 commit comments