Upload files to "html"

This commit is contained in:
tanthius 2026-02-12 04:17:04 +00:00
parent d2e5303901
commit 8b1c54a95d
5 changed files with 1070 additions and 0 deletions

349
html/atm.css Normal file
View File

@ -0,0 +1,349 @@
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: transparent;
font-family: Arial, Helvetica, sans-serif;
overflow: hidden;
}
.hidden { display: none !important; }
/* Stop clicks leaking into the game world */
body { pointer-events: none; }
#wrap, #wrap * { pointer-events: auto; }
/* Fullscreen wrapper that centers the ATM card */
/* Fullscreen wrapper that centers the ATM card */
#wrap {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ✅ Only draw the ATM vignette when ATM is visible (not hidden) */
#wrap:not(.hidden)::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(
circle at 50% 40%,
rgba(0,0,0,0.25),
rgba(0,0,0,0.65)
);
}
/* ATM card */
.card {
position: relative;
z-index: 10;
width: 420px;
border-radius: 14px;
padding: 18px;
color: #e7f2ff;
background: linear-gradient(180deg, rgba(28, 40, 52, 0.95), rgba(10, 14, 18, 0.92));
border: 1px solid rgba(130, 190, 255, 0.25);
backdrop-filter: blur(8px);
box-shadow:
0 0 0 1px rgba(255,255,255,0.06),
0 25px 70px rgba(0,0,0,0.85);
}
/* Header */
.title {
font-size: 20px;
font-weight: 700;
margin-bottom: 12px;
text-align: center;
letter-spacing: 1px;
color: rgba(160, 210, 255, 0.95);
}
/* LCD balance panel */
.balances {
margin-bottom: 14px;
border-radius: 12px;
padding: 12px 14px;
background: linear-gradient(180deg, rgba(10, 18, 26, 0.88), rgba(6, 10, 14, 0.78));
border: 1px solid rgba(120, 200, 255, 0.18);
box-shadow: inset 0 0 22px rgba(0,0,0,0.65);
}
.row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.row strong {
color: rgba(155, 231, 255, 0.95);
font-size: 18px;
}
/* Actions area */
.actions {
margin-top: 12px;
display: grid;
gap: 10px;
}
/* Input */
input {
width: 100%;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid rgba(120, 200, 255, 0.14);
background: rgba(0,0,0,0.38);
color: #eaf4ff;
outline: none;
box-shadow: inset 0 0 10px rgba(0,0,0,0.55);
}
input:focus {
border-color: rgba(140, 220, 255, 0.35);
box-shadow: inset 0 0 10px rgba(0,0,0,0.55), 0 0 0 2px rgba(100, 180, 255, 0.15);
}
/* Buttons */
.btnrow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
button {
padding: 10px 12px;
border-radius: 10px;
border: 0;
cursor: pointer;
font-weight: 700;
color: #fff;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.18),
0 8px 18px rgba(0,0,0,0.55);
transition: transform 0.06s ease, filter 0.15s ease;
}
button:hover { filter: brightness(1.12); }
button:active { transform: translateY(2px); }
/* Primary actions */
#btnDeposit { background: linear-gradient(180deg, rgba(0, 180, 90, 0.95), rgba(0, 120, 60, 0.9)); }
#btnWithdraw { background: linear-gradient(180deg, rgba(0, 130, 255, 0.95), rgba(0, 90, 180, 0.9)); }
/* Exit / secondary */
.secondary {
background: rgba(255,255,255,0.12);
color: #fff;
}
/* Hint text */
.hint {
margin-top: 10px;
opacity: 0.8;
font-size: 12px;
}
/* =========================
Gang Bank HUD (overlay)
========================= */
#gangbankHud {
position: absolute;
bottom: 170px; /* pushed up ~2x HUD height */
right: 22px;
z-index: 2;
pointer-events: none;
}
.gb-hidden { display: none !important; }
.gb-card {
min-width: 190px;
padding: 6px 0; /* tighter HUD feel */
background: none; /* ✅ no background */
border: none; /* ✅ no border */
box-shadow: none; /* ✅ no shadow */
backdrop-filter: none;
color: #fff;
}
/* GTA-ish font stack (safe fallback) */
.gb-card, .gb-card * {
font-family: "ChaletLondonNineteenSixty", "Chalet Comprime Cologne", "ChaletComprime Cologne", Arial, Helvetica, sans-serif;
letter-spacing: 0.4px;
}
.gb-label {
font-size: 12px;
font-weight: 800;
margin-bottom: 4px;
color: var(--gang-color, #ffffff); /* ✅ dynamic gang color */
text-shadow: 0 0 6px rgba(0,0,0,0.6);
}
.gb-value {
font-size: 20px;
font-weight: 800;
color: #3cff57; /* cash green */
text-shadow: 0 2px 10px rgba(0,0,0,0.35);
}
/* Pulse animation */
@keyframes gbPulse {
0% { transform: scale(1); }
50% { transform: scale(1.06); }
100% { transform: scale(1); }
}
.gb-pulse { animation: gbPulse 0.25s ease; }
/* =========================
Turf Capture HUD (overlay)
========================= */
#captureHud {
position: absolute;
left: 50%;
bottom: 10%;
transform: translateX(-50%);
z-index: 3;
pointer-events: none; /* never blocks input */
}
.cap-hidden { display: none !important; }
.cap-card {
width: 360px;
padding: 12px 14px;
border-radius: 14px;
background: rgba(10, 10, 10, 0.60);
border: 1px solid rgba(255,255,255,0.12);
color: #fff;
backdrop-filter: blur(8px);
}
.cap-card, .cap-card * {
font-family: "ChaletLondonNineteenSixty", "Chalet Comprime Cologne", "ChaletComprime Cologne", Arial, Helvetica, sans-serif;
letter-spacing: 0.4px;
}
.cap-title {
font-size: 14px;
font-weight: 800;
opacity: 0.95;
margin-bottom: 8px;
}
.cap-bar {
height: 12px;
border-radius: 999px;
background: rgba(255,255,255,0.14);
overflow: hidden;
}
.cap-fill {
height: 100%;
width: 0%;
background: rgba(255,255,255,0.85);
transition: width 80ms linear;
}
.cap-sub {
margin-top: 7px;
font-size: 12px;
opacity: 0.85;
}
/* =========================
Leaderboard HUD (overlay)
========================= */
#leaderboardHud {
position: absolute;
top: 12%;
right: 22px;
z-index: 4;
pointer-events: none;
}
.lb-hidden { display: none !important; }
.lb-card {
width: 260px;
padding: 6px 0; /* tighter, cleaner */
background: none; /* ✅ fully transparent */
border: none; /* ✅ no border */
box-shadow: none; /* ✅ no shadow */
backdrop-filter: none;
color: #fff;
}
.lb-card, .lb-card * {
font-family: "ChaletLondonNineteenSixty", "Chalet Comprime Cologne", "ChaletComprime Cologne", Arial, Helvetica, sans-serif;
letter-spacing: 0.4px;
}
.lb-title {
font-size: 14px;
font-weight: 900;
opacity: 0.95;
margin-bottom: 10px;
}
.lb-error {
margin-bottom: 10px;
padding: 8px 10px;
border-radius: 10px;
background: rgba(255, 80, 80, 0.18);
border: 1px solid rgba(255, 80, 80, 0.35);
font-size: 12px;
opacity: 0.95;
}
.lb-rows .lb-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: none; /* cleaner HUD look */
font-size: 13px;
}
.lb-rows .lb-row:last-child {
border-bottom: 0;
}
.lb-rank {
opacity: 0.8;
margin-right: 8px;
}
.lb-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 10px;
text-shadow: 0 0 6px rgba(0,0,0,0.6);
}
.lb-val {
font-weight: 900;
opacity: 0.95;
text-shadow: 0 0 6px rgba(0,0,0,0.6);
}

127
html/atm.html Normal file
View File

@ -0,0 +1,127 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>turfwar ui</title>
<!-- Existing styles -->
<link rel="stylesheet" href="atm.css" />
<link rel="stylesheet" href="killfeed.css" />
<!-- NEW shop styles -->
<link rel="stylesheet" href="shop.css" />
</head>
<body>
<!-- =========================
ATM UI
========================= -->
<div id="wrap" class="hidden">
<div class="card">
<div class="title">Fleeca Bank</div>
<div class="balances">
<div class="row">
<span>Cash</span>
<strong id="cash">$0</strong>
</div>
<div class="row" style="border-bottom:0;">
<span>Bank</span>
<strong id="bank">$0</strong>
</div>
</div>
<div class="actions">
<input id="amount" type="number" inputmode="numeric" min="0" step="1" placeholder="Amount" />
<div class="btnrow">
<button id="btnDeposit" type="button">Deposit</button>
<button id="btnWithdraw" type="button">Withdraw</button>
</div>
<button id="btnClose" type="button" class="secondary">Exit</button>
<div class="hint">Press <b>ESC</b> to close</div>
</div>
</div>
</div>
<!-- =========================
Gang Bank HUD
========================= -->
<div id="gangbankHud" class="gb-hidden">
<div class="gb-card">
<div class="gb-label" id="gbLabel">Gang Bank</div>
<div class="gb-value" id="gbAmount">$0</div>
</div>
</div>
<!-- =========================
Turf Capture HUD
========================= -->
<div id="captureHud" class="cap-hidden">
<div class="cap-card">
<div class="cap-title" id="capTitle">Capturing…</div>
<div class="cap-bar" id="capBar">
<div class="cap-fill" id="capFill"></div>
</div>
<div class="cap-sub" id="capSub">0%</div>
</div>
</div>
<!-- =========================
Leaderboard HUD
========================= -->
<div id="leaderboardHud" class="lb-hidden">
<div class="lb-card">
<div class="lb-title" id="lbTitle">Most influence</div>
<div class="lb-error lb-hidden" id="lbError"></div>
<div class="lb-rows" id="lbRows"></div>
</div>
</div>
<!-- =========================
Killfeed container
========================= -->
<div id="killfeed"></div>
<!-- =========================
GANG SHOP UI (NEW)
========================= -->
<div id="shop-wrap" class="hidden">
<div class="shop-card">
<div class="shop-top">
<div>
<h1 id="shop-title" class="shop-title">Gang Shop</h1>
<div class="shop-sub">Weapons • Ammo • Vehicles</div>
</div>
<div class="shop-balance">
<div>Gang Bank</div>
<div id="shop-balance-num" class="num">$0</div>
</div>
</div>
<div class="shop-tabs">
<div id="shop-tab-weapons" class="shop-tab active">Weapons</div>
<div id="shop-tab-ammo" class="shop-tab">Ammo</div>
<div id="shop-tab-vehicles" class="shop-tab">Vehicles</div>
</div>
<div id="shop-list" class="shop-list"></div>
<div class="shop-footer">
<div>Tip: Press ESC to close</div>
<div id="shop-close" class="shop-close">Close</div>
</div>
</div>
</div>
<!-- Scripts (order matters a bit) -->
<script src="killfeed.js"></script>
<script src="atm.js"></script>
<script src="shop.js"></script>
</body>
</html>

384
html/atm.js Normal file
View File

@ -0,0 +1,384 @@
console.log("[ATM UI] atm.js loaded");
console.log("[NUI] loaded");
// =====================
// DOM refs
// =====================
const wrap = document.getElementById("wrap");
const cashEl = document.getElementById("cash");
const bankEl = document.getElementById("bank");
const amountEl = document.getElementById("amount");
const btnDeposit = document.getElementById("btnDeposit");
const btnWithdraw = document.getElementById("btnWithdraw");
const btnClose = document.getElementById("btnClose");
// Gang Bank HUD
const gbHud = document.getElementById("gangbankHud");
const gbAmount = document.getElementById("gbAmount");
const gbLabel = document.getElementById("gbLabel");
// Capture HUD
const capHud = document.getElementById("captureHud");
const capTitle = document.getElementById("capTitle");
const capFill = document.getElementById("capFill");
const capSub = document.getElementById("capSub");
const capBar = document.getElementById("capBar");
// Leaderboard HUD
const lbHud = document.getElementById("leaderboardHud");
const lbTitle = document.getElementById("lbTitle");
const lbRows = document.getElementById("lbRows");
const lbError = document.getElementById("lbError");
// =====================
// State
// =====================
let lastGangBank = null;
// =====================
// Helpers
// =====================
function fmt(n) {
n = Number(n || 0);
if (!Number.isFinite(n)) n = 0;
return "$" + Math.floor(n).toLocaleString();
}
function post(name, data = {}) {
try {
fetch(`https://${GetParentResourceName()}/${name}`, {
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify(data)
}).catch(() => {});
} catch (e) {
console.log("[ATM UI] post failed:", name, e);
}
}
function getAmount() {
const v = Number(amountEl?.value || 0);
if (!Number.isFinite(v)) return 0;
return Math.floor(v);
}
// =====================
// ATM UI
// =====================
function hideATM() {
wrap?.classList.add("hidden");
}
function showATM() {
wrap?.classList.remove("hidden");
setTimeout(() => amountEl?.focus(), 0);
}
// ========================
// Turf Capture HUD (fixed)
// ========================
function capShow() {
capHud?.classList.remove("cap-hidden");
}
function capHide() {
capHud?.classList.add("cap-hidden");
if (capFill) capFill.style.width = "0%";
if (capSub) capSub.textContent = "0%";
}
function capStart(titleText) {
capShow();
if (capTitle) capTitle.textContent = titleText || "Capturing…";
capSet(0);
}
function capSet(t) {
t = Math.max(0, Math.min(1, Number(t || 0)));
const pct = Math.round(t * 100);
if (capFill) capFill.style.width = pct + "%";
if (capSub) capSub.textContent = pct + "%";
}
/**
* Capture styling:
* - fillCss = contesting/capturing gang
* - bgCss = outgoing/current owner gang
*/
function capStyle(fillCss, bgCss) {
if (capFill && fillCss) capFill.style.background = fillCss;
if (capBar && bgCss) capBar.style.background = bgCss;
}
function capPaused(isPaused) {
if (!capSub) return;
if (isPaused) capSub.textContent = "PAUSED";
}
// ========================
// Leaderboard HUD
// ========================
function lbShow() {
lbHud?.classList.remove("lb-hidden");
}
function lbHide() {
lbHud?.classList.add("lb-hidden");
}
function lbRender(payload) {
if (!payload) return;
lbShow();
if (lbTitle) lbTitle.textContent = String(payload.title || "Most influence");
// Error display (optional)
if (payload.error) {
if (lbError) {
lbError.textContent = String(payload.error);
lbError.classList.remove("lb-hidden");
}
} else {
lbError?.classList.add("lb-hidden");
if (lbError) lbError.textContent = "";
}
if (!lbRows) return;
lbRows.innerHTML = "";
const rows = Array.isArray(payload.rows) ? payload.rows : [];
rows.forEach((r) => {
const gangId = Number(r.gangId);
const name = String(r.name ?? "Unknown");
const val = Number(r.value ?? 0);
// 🚫 Skip Police
if (gangId === 3) return;
// --- Color handling ---
const rgb = Array.isArray(r.rgb) ? r.rgb : [255, 255, 255];
let R = Number(rgb[0]);
let G = Number(rgb[1]);
let B = Number(rgb[2]);
// Fallback safety
if (!Number.isFinite(R)) R = 255;
if (!Number.isFinite(G)) G = 255;
if (!Number.isFinite(B)) B = 255;
// Brighten very dark colors only (Lost Gang)
if (R < 80 && G < 80 && B < 80) {
R = Math.min(255, R + 80);
G = Math.min(255, G + 80);
B = Math.min(255, B + 80);
}
const div = document.createElement("div");
div.className = "lb-row";
div.innerHTML = `
<span class="lb-name" style="color: rgb(${R}, ${G}, ${B});">
${escapeHtml(name)}
</span>
<span class="lb-val">${Number.isFinite(val) ? val : 0}</span>
`;
lbRows.appendChild(div);
});
}
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function setGangBankColor(rgb) {
if (!gbLabel) return;
const arr = Array.isArray(rgb) ? rgb : [255, 255, 255];
let R = Number(arr[0]);
let G = Number(arr[1]);
let B = Number(arr[2]);
if (!Number.isFinite(R)) R = 255;
if (!Number.isFinite(G)) G = 255;
if (!Number.isFinite(B)) B = 255;
// Brighten very dark colors (e.g. Lost Gang)
if (R < 80 && G < 80 && B < 80) {
R = Math.min(255, R + 80);
G = Math.min(255, G + 80);
B = Math.min(255, B + 80);
}
const css = `rgb(${R}, ${G}, ${B})`;
// Apply to label (and optionally amount if you want later)
gbLabel.style.setProperty("--gang-color", css);
gbLabel.style.color = "var(--gang-color)"; // ensures it shows even if CSS missing
}
// =====================
// Boot
// =====================
document.addEventListener("DOMContentLoaded", () => {
hideATM();
capHide(); // ✅ was capStop() in your file (but didnt exist)
lbHide(); // start hidden until first update
console.log("[ATM UI] DOMContentLoaded -> posting atm:ready");
post("atm:ready");
});
// =====================
// Button wiring
// =====================
btnDeposit?.addEventListener("click", () => {
const amount = getAmount();
if (amount > 0) post("atm:deposit", { amount });
});
btnWithdraw?.addEventListener("click", () => {
const amount = getAmount();
if (amount > 0) post("atm:withdraw", { amount });
});
btnClose?.addEventListener("click", () => post("atm:close"));
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") post("atm:close");
});
// =====================
// NUI messages (single listener)
// =====================
window.addEventListener("message", (event) => {
const data = event.data || {};
const type = data.type;
if (!type) return;
// Capture debug (optional)
if (type.startsWith("capture:")) {
// console.log("[NUI] capture msg", data);
}
switch (type) {
// -------- ATM --------
case "atm:open":
showATM();
break;
case "atm:close":
hideATM();
break;
case "atm:balances":
if (cashEl) cashEl.textContent = fmt(data.cash);
if (bankEl) bankEl.textContent = fmt(data.bank);
break;
// ---- Gang Bank HUD ----
case "gangbank:show":
gbHud?.classList.remove("gb-hidden");
break;
case "gangbank:hide":
gbHud?.classList.add("gb-hidden");
break;
case "gangbank:label": {
if (gbLabel) gbLabel.textContent = String(data.label || "Gang Bank");
// ✅ set color if provided
if (data.rgb) setGangBankColor(data.rgb);
break;
}
case "gangbank:update": {
if (!gbAmount) break;
const amt = Number(data.balance ?? data.amount ?? 0);
gbAmount.textContent = fmt(amt);
// ✅ also accept rgb here (in case update is the only message carrying it)
if (data.rgb) setGangBankColor(data.rgb);
// pulse only if changed
if (lastGangBank !== null && amt !== lastGangBank) {
gbAmount.classList.remove("gb-pulse");
void gbAmount.offsetWidth; // restart animation
gbAmount.classList.add("gb-pulse");
}
lastGangBank = amt;
break;
}
// ---- Turf Capture HUD ----
case "capture:start":
capStart(data.turfName || data.title || "Capturing…");
if (data.t !== undefined) capSet(data.t);
if (data.fill || data.bg) capStyle(data.fill, data.bg);
break;
case "capture:set":
capShow();
capSet(data.t);
break;
case "capture:style":
capStyle(data.fill, data.bg);
break;
case "capture:paused":
capPaused(!!data.paused);
break;
case "capture:hint":
if (capSub && data.text) {
capSub.textContent = String(data.text);
setTimeout(() => {
// only clear if still showing the same hint-like text
if (capSub && capSub.textContent === String(data.text)) {
capSub.textContent = "";
}
}, 1500);
}
break;
case "capture:stop":
capHide();
break;
// ---- ✅ Leaderboard ----
// We accept either name:
// - "leaderboard:update" (client might send this)
// - "turfwar:leaderboard:update" (recommended)
case "leaderboard:update":
case "turfwar:leaderboard:update":
lbRender(data.payload || data);
break;
case "leaderboard:hide":
lbHide();
break;
case "leaderboard:show":
lbShow();
break;
default:
break;
}
});

108
html/killfeed.css Normal file
View File

@ -0,0 +1,108 @@
/* =========================================================
Killfeed (NUI)
- Bottom-left (above minimap)
- Stacks UP
- Bigger, bold, white text
- FULLY TRANSPARENT (no background)
========================================================= */
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: transparent;
overflow: hidden;
}
/* Killfeed container */
#killfeed {
position: absolute;
left: 18px;
bottom: 250px; /* tweak if needed */
z-index: 50;
pointer-events: none;
display: flex;
flex-direction: column-reverse; /* stack upward */
gap: 8px;
max-width: 460px;
}
/* Each line */
.kf-line {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 0; /* no box feel */
border-radius: 0;
/* ❌ NO BACKGROUND */
background: none;
backdrop-filter: none;
box-shadow: none;
font-family: "ChaletLondonNineteenSixty",
"Chalet Comprime Cologne",
"ChaletComprime Cologne",
Arial, Helvetica, sans-serif;
font-size: 16px; /* bigger */
font-weight: 800; /* bold */
line-height: 1.25;
color: #ffffff; /* white text */
/* Strong GTA-style shadow for readability */
text-shadow:
0 1px 2px rgba(0,0,0,0.95),
0 0 8px rgba(0,0,0,0.85);
opacity: 0;
transform: translateX(-6px);
animation: kfIn 140ms ease forwards;
}
/* Force all spans to white unless overridden inline */
.kf-line span {
color: #ffffff;
}
/* Killer & victim names (still gang-colored inline) */
.kf-killer,
.kf-victim {
font-weight: 900;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
text-shadow:
0 1px 2px rgba(0,0,0,0.98),
0 0 8px rgba(0,0,0,0.90);
}
/* Weapon + extras */
.kf-weapon,
.kf-extra {
font-size: 14px;
font-weight: 800;
opacity: 0.98;
white-space: nowrap;
}
/* Fade out */
.kf-out {
animation: kfOut 260ms ease forwards;
}
@keyframes kfIn {
to { opacity: 1; transform: translateX(0); }
}
@keyframes kfOut {
to { opacity: 0; transform: translateX(-8px); }
}

102
html/killfeed.js Normal file
View File

@ -0,0 +1,102 @@
console.log("[KILLFEED] killfeed.js loaded");
const kfWrap = document.getElementById("killfeed");
function safeText(s) {
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
"&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#039;"
}[c]));
}
function rgbCss(rgb) {
const a = Array.isArray(rgb) ? rgb : [255,255,255];
let r = Number(a[0]), g = Number(a[1]), b = Number(a[2]);
if (!Number.isFinite(r)) r = 255;
if (!Number.isFinite(g)) g = 255;
if (!Number.isFinite(b)) b = 255;
// brighten very dark colors slightly
if (r < 70 && g < 70 && b < 70) {
r = Math.min(255, r + 80);
g = Math.min(255, g + 80);
b = Math.min(255, b + 80);
}
return `rgb(${r},${g},${b})`;
}
function addKillfeedLine(payload) {
if (!kfWrap) return;
payload = payload || {};
// ✅ Death without a killer
if (payload.isDeathOnly) {
const victim = safeText(payload.victimName || "Unknown");
const vCss = rgbCss(payload.victimRgb);
const line = document.createElement("div");
line.className = "kf-line";
line.innerHTML = `
<span class="kf-victim" style="color:${vCss}">${victim}</span>
<span class="kf-weapon">has died</span>
`;
kfWrap.prepend(line);
const maxLines = 6;
while (kfWrap.children.length > maxLines) {
kfWrap.lastElementChild?.remove();
}
setTimeout(() => {
line.classList.add("kf-out");
setTimeout(() => line.remove(), 300);
}, 6000);
return;
}
// ✅ Normal PvP line
const killer = safeText(payload.killerName || "Unknown");
const victim = safeText(payload.victimName || "Unknown");
const weapon = safeText(payload.weapon || "Unknown");
const extras = [];
if (payload.headshot) extras.push("HS");
const dist = Number(payload.distance || 0);
if (Number.isFinite(dist) && dist > 0.1) extras.push(`${Math.round(dist)}m`);
const extraText = extras.length ? `(${extras.join(" · ")})` : "";
const line = document.createElement("div");
line.className = "kf-line";
const kCss = rgbCss(payload.killerRgb);
const vCss = rgbCss(payload.victimRgb);
line.innerHTML = `
<span class="kf-killer" style="color:${kCss}">${killer}</span>
<span class="kf-weapon">[${weapon}]</span>
<span class="kf-victim" style="color:${vCss}">${victim}</span>
${extraText ? `<span class="kf-extra">${safeText(extraText)}</span>` : ""}
`;
kfWrap.prepend(line);
const maxLines = 6;
while (kfWrap.children.length > maxLines) {
kfWrap.lastElementChild?.remove();
}
setTimeout(() => {
line.classList.add("kf-out");
setTimeout(() => line.remove(), 300);
}, 7000);
}
window.addEventListener("message", (event) => {
const data = event.data || {};
if (data.type === "killfeed:add") {
addKillfeedLine(data);
} else if (data.type === "killfeed:clear") {
if (kfWrap) kfWrap.innerHTML = "";
}
});