Skip to content

Commit 101d974

Browse files
author
Albert Rigo
committed
Adds Player’s Folk Repertoire
1 parent 702eebd commit 101d974

4 files changed

Lines changed: 234 additions & 5 deletions

File tree

assets/css/campaign-stats.css

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,101 @@ body.dark-mode .player-class-heatmap-table thead th.obsolete-class > span,
563563
transition: all 0.15s;
564564
}
565565

566+
/* Player Folk Heatmap (Player's Folk Repertoire) */
567+
.player-folk-heatmap-table {
568+
border-collapse: collapse;
569+
font-size: 0.8rem;
570+
width: 100%;
571+
background: #ffffff;
572+
}
573+
574+
.player-folk-heatmap-table th,
575+
.player-folk-heatmap-table td {
576+
padding: 0.4rem 0.5rem;
577+
text-align: center;
578+
border: 1px solid #e2e8f0;
579+
}
580+
581+
body.dark-mode .player-folk-heatmap-table,
582+
.dark-mode .player-folk-heatmap-table {
583+
background: #1e293b;
584+
}
585+
586+
body.dark-mode .player-folk-heatmap-table th,
587+
body.dark-mode .player-folk-heatmap-table td,
588+
.dark-mode .player-folk-heatmap-table th,
589+
.dark-mode .player-folk-heatmap-table td {
590+
border-color: #4a5568;
591+
}
592+
593+
.player-folk-heatmap-table thead th {
594+
background-color: #f7fafc;
595+
font-weight: 600;
596+
font-size: 0.75rem;
597+
white-space: nowrap;
598+
height: 120px;
599+
vertical-align: bottom;
600+
padding: 0;
601+
position: relative;
602+
}
603+
604+
.player-folk-heatmap-table thead th:first-child {
605+
height: auto;
606+
padding: 0.4rem 0.5rem;
607+
vertical-align: middle;
608+
}
609+
610+
.player-folk-heatmap-table thead th > span {
611+
writing-mode: vertical-rl;
612+
transform: rotate(180deg);
613+
display: inline-block;
614+
padding: 8px 0;
615+
}
616+
617+
body.dark-mode .player-folk-heatmap-table thead th,
618+
.dark-mode .player-folk-heatmap-table thead th {
619+
background-color: #2d3748;
620+
color: #e2e8f0;
621+
}
622+
623+
.player-folk-heatmap-table .player-name {
624+
text-align: left;
625+
font-weight: 600;
626+
white-space: nowrap;
627+
background-color: #f8f9fa;
628+
border-right: 2px solid #cbd5e0;
629+
}
630+
631+
.player-folk-heatmap-table tbody tr:hover td {
632+
background-color: rgba(102, 126, 234, 0.1);
633+
}
634+
635+
.player-folk-heatmap-table tbody tr:hover td.player-name {
636+
background-color: rgba(102, 126, 234, 0.15);
637+
}
638+
639+
body.dark-mode .player-folk-heatmap-table .player-name,
640+
.dark-mode .player-folk-heatmap-table .player-name {
641+
background-color: #334155;
642+
color: #e2e8f0;
643+
border-right-color: #64748b;
644+
}
645+
646+
body.dark-mode .player-folk-heatmap-table tbody tr:hover td,
647+
.dark-mode .player-folk-heatmap-table tbody tr:hover td {
648+
background-color: rgba(102, 126, 234, 0.2);
649+
}
650+
651+
body.dark-mode .player-folk-heatmap-table tbody tr:hover td.player-name,
652+
.dark-mode .player-folk-heatmap-table tbody tr:hover td.player-name {
653+
background-color: rgba(102, 126, 234, 0.25);
654+
}
655+
656+
.player-folk-heatmap-table tbody td {
657+
font-weight: 500;
658+
transition: all 0.15s;
659+
}
660+
566661
.player-class-heatmap-table tbody td:hover {
567662
transform: scale(1.05);
568663
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);

assets/js/campaign-data.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
'use strict';
1111

1212
// Cache configuration
13-
const CACHE_KEY = 'campaign_data_v14';
14-
const CACHE_TIMESTAMP_KEY = 'campaign_timestamp_v14';
13+
const CACHE_KEY = 'campaign_data_v15';
14+
const CACHE_TIMESTAMP_KEY = 'campaign_timestamp_v15';
1515
const CACHE_DURATION = 1 * 60 * 60 * 1000; // 1 hour
1616

1717
// Google Sheets URL
@@ -101,6 +101,7 @@
101101
end: getDateByColumn(row, charCols, 'end'),
102102
class: getValueByColumn(row, charCols, 'class') || '',
103103
class2: getValueByColumn(row, charCols, 'class2') || '',
104+
race: getValueByColumn(row, charCols, 'race') || '',
104105
specialization: getValueByColumn(row, charCols, 'specialization') || '',
105106
specialization2: getValueByColumn(row, charCols, 'specialization2') || '',
106107
killer: getValueByColumn(row, charCols, 'killer') || '',

assets/js/campaign-stats.js

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ function renderCharts() {
301301
renderPlayerDeathRate();
302302
renderPlayerCampaignDiversity();
303303
renderPlayerClassHeatmap();
304+
renderPlayerFolkHeatmap();
304305

305306
// Add event listener for killer detail toggle
306307
const killerToggle = document.getElementById('killer-detail-toggle');
@@ -2047,7 +2048,8 @@ function renderPlayerClassHeatmap() {
20472048
const charInfo = {
20482049
name: charName,
20492050
path: char.path,
2050-
classes: classDisplay
2051+
classes: classDisplay,
2052+
race: char.race || ''
20512053
};
20522054

20532055
// Count this character for each of their classes
@@ -2117,7 +2119,127 @@ function renderPlayerClassHeatmap() {
21172119
if (count) {
21182120
const chars = playerClassCharacters[player][cls] || [];
21192121
const charList = chars
2120-
.map(c => `Path ${c.path}: ${c.name} (${c.classes})`)
2122+
.map(c => `Path ${c.path}: ${c.name} (${c.classes})${c.race ? ' - ' + c.race : ''}`)
2123+
.join('\n');
2124+
title = charList;
2125+
}
2126+
html += `<td style="background-color: ${color}; color: ${textColor};" title="${title}">${count || ''}</td>`;
2127+
});
2128+
html += '</tr>';
2129+
});
2130+
2131+
html += '</tbody></table>';
2132+
container.innerHTML = html;
2133+
}
2134+
2135+
// Render player folk heatmap (Player's Folk Repertoire)
2136+
function renderPlayerFolkHeatmap() {
2137+
const container = document.getElementById('player-folk-heatmap');
2138+
if (!container) return;
2139+
2140+
// Helper to extract base folk from "FOLK (SUBFOLK)" format
2141+
const getBaseFolk = (race) => {
2142+
if (!race) return '';
2143+
const match = race.match(/^([^(]+)/);
2144+
return match ? match[1].trim() : race;
2145+
};
2146+
2147+
// Get all unique base folk (without subfolk)
2148+
const baseFolkSet = new Set();
2149+
characterData.forEach(c => {
2150+
if (c.race) baseFolkSet.add(getBaseFolk(c.race));
2151+
});
2152+
const allBaseFolk = [...baseFolkSet].sort();
2153+
2154+
// Count characters per player per base folk AND collect character details
2155+
const playerFolkData = {};
2156+
const playerFolkCharacters = {}; // Store character details
2157+
2158+
characterData.forEach(char => {
2159+
const player = char.category || 'Unknown';
2160+
const fullRace = char.race;
2161+
if (!fullRace) return;
2162+
2163+
const baseFolk = getBaseFolk(fullRace);
2164+
2165+
if (!playerFolkData[player]) {
2166+
playerFolkData[player] = {};
2167+
playerFolkCharacters[player] = {};
2168+
}
2169+
2170+
if (!playerFolkData[player][baseFolk]) {
2171+
playerFolkData[player][baseFolk] = 0;
2172+
playerFolkCharacters[player][baseFolk] = [];
2173+
}
2174+
2175+
// Store full character details including full race with subfolk
2176+
const charName = char.shortname || char.name || 'Unknown';
2177+
const classDisplay = char.class2 ? `${char.class}/${char.class2}` : char.class;
2178+
const charInfo = {
2179+
name: charName,
2180+
path: char.path,
2181+
classes: classDisplay,
2182+
race: fullRace
2183+
};
2184+
2185+
playerFolkData[player][baseFolk]++;
2186+
playerFolkCharacters[player][baseFolk].push(charInfo);
2187+
});
2188+
2189+
// Get top 15 players by total characters
2190+
const topPlayers = Object.entries(playerFolkData)
2191+
.map(([player, folk]) => ({
2192+
player,
2193+
total: Object.values(folk).reduce((sum, count) => sum + count, 0),
2194+
folk
2195+
}))
2196+
.sort((a, b) => b.total - a.total)
2197+
.slice(0, 15);
2198+
2199+
// Detect dark mode
2200+
const isDarkMode = document.documentElement.classList.contains('dark-mode');
2201+
2202+
// Find max count for color scaling
2203+
const maxCount = Math.max(...topPlayers.flatMap(p => Object.values(p.folk)));
2204+
2205+
// Helper to get color intensity (matching Level Duration Analysis style)
2206+
const getColorIntensity = (count) => {
2207+
if (!count) return isDarkMode ? '#334155' : '#f8f9fa'; // Empty cells match first column
2208+
const normalized = count / maxCount;
2209+
2210+
if (isDarkMode) {
2211+
// Dark mode: cyan to orange heat map
2212+
const hue = 200 - (normalized * 85); // 200 (cyan) to 30 (orange)
2213+
const saturation = 65 + (normalized * 15); // 65% to 80%
2214+
const lightness = 35 + (normalized * 15); // 35% to 50%
2215+
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
2216+
} else {
2217+
// Light mode: blue gradient
2218+
const hue = 210; // Blue
2219+
const lightness = 85 - (normalized * 30); // 85% to 55%
2220+
return `hsl(${hue}, 80%, ${lightness}%)`;
2221+
}
2222+
};
2223+
2224+
// Build HTML table
2225+
let html = '<table class="player-folk-heatmap-table"><thead><tr><th>Player</th>';
2226+
allBaseFolk.forEach(folk => {
2227+
html += `<th><span>${folk}</span></th>`;
2228+
});
2229+
html += '</tr></thead><tbody>';
2230+
2231+
const textColor = isDarkMode ? 'rgba(255, 255, 255, 0.95)' : 'rgba(0, 0, 0, 0.87)';
2232+
2233+
topPlayers.forEach(({ player, folk }) => {
2234+
html += `<tr><td class="player-name" style="border-left: 4px solid ${window.CampaignData.getPlayerColor(player).replace('0.7', '1')}">${player}</td>`;
2235+
allBaseFolk.forEach(f => {
2236+
const count = folk[f] || 0;
2237+
const color = getColorIntensity(count);
2238+
let title = '';
2239+
if (count) {
2240+
const chars = playerFolkCharacters[player][f] || [];
2241+
const charList = chars
2242+
.map(c => `Path ${c.path}: ${c.name} (${c.classes}) - ${c.race}`)
21212243
.join('\n');
21222244
title = charList;
21232245
}
@@ -2148,4 +2270,10 @@ window.addEventListener('darkModeChanged', function(event) {
21482270
console.log('[CAMPAIGN-STATS] Re-rendering Player Class Heatmap');
21492271
renderPlayerClassHeatmap();
21502272
}
2273+
2274+
// Re-render Player Folk Heatmap
2275+
if (typeof characterData !== 'undefined' && characterData.length > 0) {
2276+
console.log('[CAMPAIGN-STATS] Re-rendering Player Folk Heatmap');
2277+
renderPlayerFolkHeatmap();
2278+
}
21512279
});

docs/_CampaignStats/overview.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,12 @@ Average real-world days spent at each character level across all campaigns. Each
179179
#### Player's Class Repertoire
180180

181181
Which classes has each player explored?
182+
*Note: Classes marked with * are retired/obsolete and aren't currently available for new characters.*
182183

183184
<div id="player-class-heatmap" class="heatmap-container"></div>
184185

185-
*Note: Classes marked with * are retired/obsolete and aren't currently available for new characters.*
186+
#### Player's Folk Repertoire
187+
188+
Which folk/races has each player explored?
189+
190+
<div id="player-folk-heatmap" class="heatmap-container"></div>

0 commit comments

Comments
 (0)