Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"src/faceit/service/service.js",
"src/faceit/service/profiles.js",
"src/faceit/service/integrations.js",
"src/faceit/reputation/reputation.js",
"src/module/moduleManager.js"
]
}
Expand Down
71 changes: 71 additions & 0 deletions src/faceit/reputation/reputation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2025 TerraMiner. All Rights Reserved.
*/

const REPUTATION_TOXIC = 'toxic';
const REPUTATION_FRIENDLY = 'friendly';
const REPUTATION_NEUTRAL = 'neutral';
const REPUTATION_STORAGE_KEY = 'player_reputation';

async function getPlayerReputation(playerId) {
try {
const result = await CLIENT_STORAGE.get([REPUTATION_STORAGE_KEY]);
const data = result[REPUTATION_STORAGE_KEY] || {};
const entry = data[playerId];
if (!entry) return null;
return { rating: entry.rating, updatedAt: entry.updatedAt, meetCount: entry.meetCount || 0 };
} catch (e) {
error('getPlayerReputation failed', e);
return null;
}
}

async function setPlayerReputation(playerId, rating) {
try {
const result = await CLIENT_STORAGE.get([REPUTATION_STORAGE_KEY]);
const data = result[REPUTATION_STORAGE_KEY] || {};
const existing = data[playerId] || {};
data[playerId] = { rating, updatedAt: Date.now(), meetCount: existing.meetCount || 0 };
await CLIENT_STORAGE.set({ [REPUTATION_STORAGE_KEY]: data });
} catch (e) {
error('setPlayerReputation failed', e);
}
}

async function incrementMeetCount(playerId) {
try {
const result = await CLIENT_STORAGE.get([REPUTATION_STORAGE_KEY]);
const data = result[REPUTATION_STORAGE_KEY] || {};
const entry = data[playerId] || { rating: REPUTATION_NEUTRAL, updatedAt: 0, meetCount: 0 };
entry.meetCount = (entry.meetCount || 0) + 1;
if (!data[playerId]) data[playerId] = { rating: REPUTATION_NEUTRAL, updatedAt: 0, meetCount: 0 };
data[playerId] = { ...data[playerId], meetCount: entry.meetCount };
await CLIENT_STORAGE.set({ [REPUTATION_STORAGE_KEY]: data });
} catch (e) {
error('incrementMeetCount failed', e);
}
}

async function resetPlayerReputation(playerId) {
try {
const result = await CLIENT_STORAGE.get([REPUTATION_STORAGE_KEY]);
const data = result[REPUTATION_STORAGE_KEY] || {};
const existing = data[playerId];
if (!existing) return;
const meetCount = existing.meetCount || 0;
data[playerId] = { rating: REPUTATION_NEUTRAL, updatedAt: Date.now(), meetCount };
await CLIENT_STORAGE.set({ [REPUTATION_STORAGE_KEY]: data });
} catch (e) {
error('resetPlayerReputation failed', e);
}
}

async function getAllReputations() {
try {
const result = await CLIENT_STORAGE.get([REPUTATION_STORAGE_KEY]);
return result[REPUTATION_STORAGE_KEY] || {};
} catch (e) {
error('getAllReputations failed', e);
return {};
}
}
152 changes: 152 additions & 0 deletions src/faceit/resources/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ let MATCH_COUNTER_ARROW_TEMPLATE;
let MATCH_HISTORY_POPUP_TEMPLATE;
let PLAYER_WINRATE_TABLE_TEMPLATE;
let TEAM_WINRATE_TABLE_TEMPLATE;
let PLAYER_REPUTATION_BADGE_TEMPLATE;
let PLAYER_REPUTATION_INLINE_TEMPLATE;
let SKILL_LEVELS_INFO_TABLE_TEMPLATE;
let FORECAST_STYLES_TEMPLATE;

Expand All @@ -28,6 +30,8 @@ function initTemplates() {
MATCH_HISTORY_POPUP_TEMPLATE = htmlToElement(MATCH_HISTORY_POPUP_HTML);
PLAYER_WINRATE_TABLE_TEMPLATE = htmlToElement(PLAYER_WINRATE_TABLE_HTML);
TEAM_WINRATE_TABLE_TEMPLATE = htmlToElement(TEAM_WINRATE_TABLE_HTML);
PLAYER_REPUTATION_BADGE_TEMPLATE = htmlToElement(PLAYER_REPUTATION_BADGE_HTML);
PLAYER_REPUTATION_INLINE_TEMPLATE = htmlToElement(PLAYER_REPUTATION_INLINE_HTML);
SKILL_LEVELS_INFO_TABLE_TEMPLATE = htmlToElement(SKILL_LEVELS_INFO_TABLE_HTML);
FORECAST_STYLES_TEMPLATE = htmlToElement(FORECAST_STYLES_HTML);
}
Expand Down Expand Up @@ -519,6 +523,26 @@ const TEAM_WINRATE_TABLE_HTML = /*language=HTML*/ `
</div>
</div>`;

const PLAYER_REPUTATION_INLINE_HTML = /*language=HTML*/ `
<div class="forecast-reputation-inline" style="display: inline-flex; align-items: center; gap: 4px; margin-left: 8px;">
<button type="button" class="reputation-btn reputation-toxic" data-reputation="toxic" title="Mark as Toxic">👎</button>
<button type="button" class="reputation-btn reputation-friendly" data-reputation="friendly" title="Mark as Friendly">👍</button>
</div>`;

const PLAYER_REPUTATION_BADGE_HTML = /*language=HTML*/ `
<div class="forecast-reputation-container" data-forecast-reputation-badge>
<div class="forecast-reputation-label-wrap">
<span class="forecast-reputation-label forecast-reputation-label-toxic" data-label="toxic" title="Toxic">👎 Toxic</span>
<span class="forecast-reputation-label forecast-reputation-label-friendly" data-label="friendly" title="Friendly">👍 Friendly</span>
<span class="forecast-reputation-label forecast-reputation-label-neutral forecast-reputation-label-current" data-label="neutral" title="Neutral">— Neutral</span>
</div>
<div class="forecast-reputation-buttons">
<button type="button" class="forecast-reputation-btn forecast-reputation-btn-toxic" data-reputation="toxic">👎 Toxic</button>
<button type="button" class="forecast-reputation-btn forecast-reputation-btn-friendly" data-reputation="friendly">👍 Friendly</button>
<button type="button" class="forecast-reputation-btn forecast-reputation-btn-reset" data-reputation="reset">Reset</button>
</div>
</div>`;

const SKILL_LEVELS_INFO_TABLE_HTML = /*language=HTML*/ `
<div class="modalinfos-content">
<div class="challengerinfos-header">
Expand Down Expand Up @@ -1797,5 +1821,133 @@ tr[class*=MatchHistoryTableRow] {
justify-content: space-around;
flex: 1 1 0;
}

.forecast-reputation-container {
margin-top: 8px;
padding: 6px 8px;
border-radius: 8px;
border: 1px solid rgb(36, 36, 36);
background: rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
gap: 6px;
}

.forecast-reputation-label-wrap {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}

.forecast-reputation-label {
font-size: 11px;
opacity: 0.5;
transition: opacity 0.2s, font-size 0.2s;
}

.forecast-reputation-label-current {
font-size: 13px;
font-weight: 600;
opacity: 1;
}

.forecast-reputation-label-toxic.forecast-reputation-label-current { color: #ff4444; }
.forecast-reputation-label-friendly.forecast-reputation-label-current { color: #44ff44; }
.forecast-reputation-label-neutral.forecast-reputation-label-current { color: #aaa; }

.forecast-reputation-buttons {
display: flex;
gap: 6px;
}

.forecast-reputation-btn {
font-size: 11px;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
opacity: 0.85;
transition: opacity 0.2s, border-color 0.2s;
}

.forecast-reputation-btn:hover {
opacity: 1;
}

.forecast-reputation-btn-toxic {
background: rgba(255, 68, 68, 0.25);
color: #ff4444;
}

.forecast-reputation-btn-toxic:hover {
border-color: #ff4444;
}

.forecast-reputation-btn-friendly {
background: rgba(68, 255, 68, 0.25);
color: #44ff44;
}

.forecast-reputation-btn-friendly:hover {
border-color: #44ff44;
}

.forecast-reputation-meet-count {
font-size: 10px;
color: #888;
margin-left: auto;
}

.forecast-reputation-btn-reset {
background: rgba(128, 128, 128, 0.25);
color: #aaa;
font-size: 10px;
}

.forecast-reputation-btn-reset:hover {
border-color: #888;
}

.forecast-reputation-inline {
vertical-align: middle;
}

.reputation-btn {
background: transparent;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
width: 20px;
height: 20px;
font-size: 12px;
cursor: pointer;
opacity: 0.3;
transition: all 0.2s;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}

.reputation-btn:hover {
opacity: 0.6;
transform: scale(1.1);
}

.reputation-btn.active {
opacity: 1;
transform: scale(1.15);
border-width: 2px;
}

.reputation-toxic.active {
border-color: #ff4444;
background: rgba(255, 68, 68, 0.2);
}

.reputation-friendly.active {
border-color: #44ff44;
background: rgba(68, 255, 68, 0.2);
}
</style>`;

127 changes: 126 additions & 1 deletion src/faceit/room/matchroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,72 @@ async function calculateStats(team, playerId, matchAmount) {

const playerStats = teamMap.get(playerId);
if (playerStats) displayPlayerStats(playerId, playerStats, table);

try {
if (userCardElement.querySelector('.forecast-reputation-container[data-player-id]')) return;

const rep = await getPlayerReputation(playerId);
const currentRating = rep ? rep.rating : REPUTATION_NEUTRAL;
const updatedAt = rep && rep.updatedAt ? rep.updatedAt : null;

const badgeClone = PLAYER_REPUTATION_BADGE_TEMPLATE.cloneNode(true);
const container = badgeClone.querySelector('.forecast-reputation-container');
if (!container) return;
container.setAttribute('data-player-id', playerId);

const labelToxic = container.querySelector('[data-label="toxic"]');
const labelFriendly = container.querySelector('[data-label="friendly"]');
const labelNeutral = container.querySelector('[data-label="neutral"]');
[labelToxic, labelFriendly, labelNeutral].forEach(el => el.classList.remove('forecast-reputation-label-current'));
const currentLabel = container.querySelector(`[data-label="${currentRating}"]`);
if (currentLabel) {
currentLabel.classList.add('forecast-reputation-label-current');
if (updatedAt) {
const d = new Date(updatedAt);
currentLabel.setAttribute('title', 'Rated: ' + d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
}
}

const btnToxic = container.querySelector('[data-reputation="toxic"]');
const btnFriendly = container.querySelector('[data-reputation="friendly"]');
const btnReset = container.querySelector('[data-reputation="reset"]');

function setReputationUI(rating) {
[labelToxic, labelFriendly, labelNeutral].forEach(el => el.classList.remove('forecast-reputation-label-current'));
const label = container.querySelector(`[data-label="${rating}"]`);
if (label) label.classList.add('forecast-reputation-label-current');
}

function setRatedTitle(el) {
if (el) el.setAttribute('title', 'Rated: ' + new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
}
btnToxic.addEventListener('click', async () => {
await setPlayerReputation(playerId, REPUTATION_TOXIC);
setReputationUI(REPUTATION_TOXIC);
setRatedTitle(labelToxic);
console.log('[FORECAST REPUTATION]', playerId, REPUTATION_TOXIC);
});
btnFriendly.addEventListener('click', async () => {
await setPlayerReputation(playerId, REPUTATION_FRIENDLY);
setReputationUI(REPUTATION_FRIENDLY);
setRatedTitle(labelFriendly);
console.log('[FORECAST REPUTATION]', playerId, REPUTATION_FRIENDLY);
});
if (btnReset) {
btnReset.addEventListener('click', async () => {
await resetPlayerReputation(playerId);
setReputationUI(REPUTATION_NEUTRAL);
const cur = container.querySelector(`[data-label="${REPUTATION_NEUTRAL}"]`);
if (cur) cur.removeAttribute('title');
console.log('[FORECAST REPUTATION]', playerId, 'reset');
});
}

appendTo(badgeClone, userCardElement);
matchRoomModule.removalNode(badgeClone);
} catch (err) {
error('Reputation badge failed', err);
}
});
}

Expand Down Expand Up @@ -177,7 +243,66 @@ async function displayWinRates(matchDetails) {
const teamMatches = calculateTeamMatches(teamMap);
displayTeamMatches(htmlResource, teamName, teamMatches);
});
})
});

await setupInlineReputationForLobby(matchDetails);
}

async function setupInlineReputationForLobby(matchDetails) {
const selectorMatchPlayer = 'div[class*=Overview__Grid] div[class*=ListContentPlayer__SlotWrapper] div[class*=styles__NicknameContainer] > div > div';

const nicknameToIdMap = new Map();
[matchDetails.teams.faction1.roster, matchDetails.teams.faction2.roster].forEach(roster => {
roster.forEach(player => {
nicknameToIdMap.set(player.nickname, player.player_id);
});
});

matchRoomModule.doAfterAllNodeAppear(selectorMatchPlayer, async (node) => {
const container = node.parentElement;
if (container.hasAttribute('data-processed-reputation')) return;
container.setAttribute('data-processed-reputation', 'true');

const nickname = node.textContent.trim();
const playerId = nicknameToIdMap.get(nickname);
if (!playerId) return;

try {
const reputation = await getPlayerReputation(playerId);

const template = PLAYER_REPUTATION_INLINE_TEMPLATE.cloneNode(true);
const inlineEl = template.firstElementChild || template;

if (reputation?.rating === REPUTATION_TOXIC || reputation?.rating === REPUTATION_FRIENDLY) {
const activeBtn = inlineEl.querySelector(`.reputation-${reputation.rating}`);
if (activeBtn) activeBtn.classList.add('active');
}

const toxicBtn = inlineEl.querySelector('.reputation-toxic');
const friendlyBtn = inlineEl.querySelector('.reputation-friendly');

toxicBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await setPlayerReputation(playerId, REPUTATION_TOXIC);
toxicBtn.classList.add('active');
friendlyBtn.classList.remove('active');
console.log('[REPUTATION INLINE]', playerId, nickname, 'toxic');
});

friendlyBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await setPlayerReputation(playerId, REPUTATION_FRIENDLY);
friendlyBtn.classList.add('active');
toxicBtn.classList.remove('active');
console.log('[REPUTATION INLINE]', playerId, nickname, 'friendly');
});

container.appendChild(inlineEl);
matchRoomModule.removalNode(inlineEl);
} catch (err) {
error('Inline reputation failed', err);
}
});
}

function addTableTeamTitle(htmlResource,roster, title) {
Expand Down
Loading