426 lines
20 KiB
JavaScript
426 lines
20 KiB
JavaScript
window.GDD = window.GDD || {};
|
|
|
|
const { useState, useEffect, useCallback, useRef } = React;
|
|
|
|
function BountyDemo() {
|
|
const [bounties, setBounties] = useState([]);
|
|
const [killFeed, setKillFeed] = useState([]);
|
|
const [placeBountyTarget, setPlaceBountyTarget] = useState('');
|
|
const [placeBountyAmount, setPlaceBountyAmount] = useState(5000);
|
|
const [showPlaceBounty, setShowPlaceBounty] = useState(false);
|
|
const [notifications, setNotifications] = useState([]);
|
|
const [autoFeed, setAutoFeed] = useState(false);
|
|
const feedRef = useRef(null);
|
|
const autoRef = useRef(null);
|
|
|
|
const feedNames = [
|
|
'CMDR Picard', 'CMDR Worf', 'CMDR Data', 'CMDR Troi', 'CMDR Riker',
|
|
'MinerBob', 'PirateKing99', 'NullSecWarlord', 'TraderAlice', 'DeepMiner',
|
|
'RockHound', 'AmarrTrader', 'BulkMiner', 'GallenteForge', 'HighSecOps',
|
|
];
|
|
|
|
const shipTypes = ['Frigate', 'Destroyer', 'Cruiser', 'Battlecruiser', 'Battleship', 'Hauler', 'Mining Barge'];
|
|
|
|
const systems = ['Sol', 'Amarr', 'Hek', 'Rens', 'Dodixie', 'U-IRTYR', 'PF-346', 'YZ-LQL', 'O-WAMW'];
|
|
|
|
const tierConfig = [
|
|
{ tier: 'Petty', threshold: 500, color: 'var(--muted)', reward: '10%', visibility: 'System-local' },
|
|
{ tier: 'Standard', threshold: 5000, color: 'var(--cyan)', reward: '15%', visibility: 'Regional' },
|
|
{ tier: 'Dangerous', threshold: 50000, color: 'var(--accent)', reward: '20%', visibility: 'Galaxy-wide' },
|
|
{ tier: 'Most Wanted', threshold: 500000, color: 'var(--red)', reward: '25%', visibility: 'Galaxy + Leaderboard' },
|
|
];
|
|
|
|
const getTier = (pool) => {
|
|
if (pool >= 500000) return tierConfig[3];
|
|
if (pool >= 50000) return tierConfig[2];
|
|
if (pool >= 5000) return tierConfig[1];
|
|
return tierConfig[0];
|
|
};
|
|
|
|
useEffect(() => {
|
|
window.GDD.api.getBounties().then(b => setBounties(b));
|
|
window.GDD.api.getKillFeed().then(k => setKillFeed(k));
|
|
}, []);
|
|
|
|
const addNotif = useCallback((msg, color) => {
|
|
const id = Date.now();
|
|
setNotifications(prev => [...prev, { id, msg, color }]);
|
|
setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3500);
|
|
}, []);
|
|
|
|
const handlePlaceBounty = useCallback(async () => {
|
|
if (!placeBountyTarget.trim()) return;
|
|
if (placeBountyAmount < 500) {
|
|
addNotif('Minimum bounty is 500 ISK.', 'var(--red)');
|
|
return;
|
|
}
|
|
const result = await window.GDD.api.placeBounty(placeBountyTarget, placeBountyAmount);
|
|
if (result.success) {
|
|
setBounties(prev => {
|
|
const existing = prev.find(b => b.target === placeBountyTarget);
|
|
if (existing) {
|
|
return prev.map(b => b.target === placeBountyTarget
|
|
? { ...b, pool: b.pool + placeBountyAmount, tier: getTier(b.pool + placeBountyAmount).tier }
|
|
: b);
|
|
}
|
|
return [...prev, {
|
|
target: placeBountyTarget,
|
|
pool: placeBountyAmount,
|
|
tier: getTier(placeBountyAmount).tier,
|
|
lastHostile: 'Just now',
|
|
}];
|
|
});
|
|
addNotif(`Bounty of ₢${placeBountyAmount.toLocaleString()} placed on ${placeBountyTarget}.`, 'var(--green)');
|
|
setShowPlaceBounty(false);
|
|
setPlaceBountyTarget('');
|
|
setPlaceBountyAmount(5000);
|
|
}
|
|
}, [placeBountyTarget, placeBountyAmount, addNotif]);
|
|
|
|
// Auto-generate kill feed
|
|
const generateKill = useCallback(() => {
|
|
const victim = feedNames[Math.floor(Math.random() * feedNames.length)];
|
|
let killer;
|
|
do { killer = feedNames[Math.floor(Math.random() * feedNames.length)]; } while (killer === victim);
|
|
const ship = shipTypes[Math.floor(Math.random() * shipTypes.length)];
|
|
const system = systems[Math.floor(Math.random() * systems.length)];
|
|
const bounty = Math.random() > 0.6 ? Math.floor(Math.random() * 50000) : 0;
|
|
|
|
const kill = {
|
|
victim,
|
|
killer,
|
|
ship,
|
|
system,
|
|
bounty,
|
|
time: 'Just now',
|
|
};
|
|
|
|
setKillFeed(prev => [kill, ...prev.slice(0, 49)]);
|
|
|
|
// If bounty, update bounty pool
|
|
if (bounty > 0) {
|
|
addNotif(`Bounty collected: ${killer} claimed ₢${bounty.toLocaleString()} from ${victim}'s bounty.`, 'var(--accent)');
|
|
setBounties(prev => prev.map(b =>
|
|
b.target === victim
|
|
? { ...b, pool: Math.max(0, b.pool - bounty) }
|
|
: b
|
|
).filter(b => b.pool > 0));
|
|
}
|
|
}, [addNotif]);
|
|
|
|
useEffect(() => {
|
|
if (autoFeed) {
|
|
autoRef.current = setInterval(generateKill, 2000 + Math.random() * 3000);
|
|
} else {
|
|
if (autoRef.current) clearInterval(autoRef.current);
|
|
}
|
|
return () => { if (autoRef.current) clearInterval(autoRef.current); };
|
|
}, [autoFeed, generateKill]);
|
|
|
|
// Auto-scroll kill feed
|
|
useEffect(() => {
|
|
if (killFeed.length > 0 && feedRef.current) {
|
|
// No auto-scroll needed since newest are on top
|
|
}
|
|
}, [killFeed]);
|
|
|
|
const totalBountyPool = bounties.reduce((sum, b) => sum + b.pool, 0);
|
|
const totalKills = killFeed.length;
|
|
const totalBountyCollected = killFeed.reduce((sum, k) => sum + k.bounty, 0);
|
|
|
|
return (
|
|
<div className="content-inner">
|
|
<a href="#overview" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '0.75rem', fontFamily: 'var(--font-mono)', color: 'var(--muted)', textDecoration: 'none', marginBottom: 'var(--sp-3)', transition: 'color 0.15s' }} onMouseEnter={e => e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}>← Back to Docs</a>
|
|
<h1 style={{ marginBottom: '8px' }}>Bounty Board & Kill Feed</h1>
|
|
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem' }}>
|
|
Live bounty board with tier escalation and a galaxy-wide kill feed. Place bounties on pirates,
|
|
track kill events, and watch bounty pools climb. Toggle the auto-feed to simulate live combat activity.
|
|
</p>
|
|
|
|
{/* HUD-style bounty strip */}
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', gap: 'var(--sp-4)',
|
|
padding: 'var(--sp-3) var(--sp-4)', marginTop: 'var(--sp-4)', marginBottom: 'var(--sp-3)',
|
|
background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)',
|
|
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)',
|
|
fontFamily: 'var(--font-mono)', fontSize: '0.75rem',
|
|
}}>
|
|
<span style={{ color: 'var(--fg-bright)', fontWeight: 600, fontSize: '0.8rem' }}>BOUNTY BOARD</span>
|
|
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
|
|
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>ACTIVE</span>
|
|
<span style={{ color: 'var(--red)', fontWeight: 600 }}>{bounties.length}</span>
|
|
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
|
|
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>POOL</span>
|
|
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>₢{totalBountyPool.toLocaleString()}</span>
|
|
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
|
|
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>COLLECTED</span>
|
|
<span style={{ color: 'var(--green)', fontWeight: 600 }}>₢{totalBountyCollected.toLocaleString()}</span>
|
|
{autoFeed && <span style={{ marginLeft: 'auto', color: 'var(--red)', fontSize: '0.7rem' }}>● LIVE FEED</span>}
|
|
</div>
|
|
|
|
{/* Notifications */}
|
|
<div style={{ position: 'fixed', top: 'var(--sp-4)', right: 'var(--sp-4)', zIndex: 1000, display: 'flex', flexDirection: 'column', gap: 'var(--sp-2)' }}>
|
|
{notifications.map(n => (
|
|
<div key={n.id} style={{
|
|
background: 'var(--surface)', border: `1px solid ${n.color}40`,
|
|
borderRadius: 'var(--radius-md)', padding: 'var(--sp-3) var(--sp-4)',
|
|
fontSize: '0.8rem', color: n.color, boxShadow: 'var(--shadow-md)',
|
|
maxWidth: '400px',
|
|
}}>
|
|
{n.msg}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="stat-grid">
|
|
<div className="stat-card">
|
|
<div className="stat-value" style={{ color: 'var(--red)' }}>{bounties.length}</div>
|
|
<div className="stat-label">Active Bounties</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-value" style={{ color: 'var(--accent)' }}>₢{totalBountyPool.toLocaleString()}</div>
|
|
<div className="stat-label">Total Bounty Pool</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-value" style={{ color: 'var(--cyan)' }}>{totalKills}</div>
|
|
<div className="stat-label">Kill Events</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-value" style={{ color: 'var(--green)' }}>₢{totalBountyCollected.toLocaleString()}</div>
|
|
<div className="stat-label">Bounty Collected</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 'var(--sp-3)', marginBottom: 'var(--sp-5)', flexWrap: 'wrap' }}>
|
|
<button className={`btn ${autoFeed ? 'btn-danger' : 'btn-primary'}`} onClick={() => setAutoFeed(!autoFeed)}>
|
|
{autoFeed ? '■ Stop Live Feed' : '▶ Start Live Feed'}
|
|
</button>
|
|
<button className="btn" onClick={() => setShowPlaceBounty(!showPlaceBounty)}>
|
|
+ Place Bounty
|
|
</button>
|
|
<button className="btn" onClick={generateKill}>
|
|
Generate Kill Event
|
|
</button>
|
|
</div>
|
|
|
|
{/* Place bounty modal */}
|
|
{showPlaceBounty && (
|
|
<div style={{
|
|
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 2000,
|
|
}} onClick={() => setShowPlaceBounty(false)}>
|
|
<div style={{
|
|
background: 'var(--surface)', border: '1px solid var(--border)',
|
|
borderRadius: 'var(--radius-lg)', padding: 'var(--sp-6)', minWidth: 360,
|
|
}} onClick={e => e.stopPropagation()}>
|
|
<h3 style={{ marginBottom: 'var(--sp-4)', color: 'var(--red)' }}>Place a Bounty</h3>
|
|
|
|
<div style={{ marginBottom: 'var(--sp-3)' }}>
|
|
<div style={{ fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 'var(--sp-1)' }}>Target Player</div>
|
|
<input type="text" value={placeBountyTarget}
|
|
onChange={e => setPlaceBountyTarget(e.target.value)}
|
|
placeholder="Enter player name..."
|
|
style={{
|
|
width: '100%', padding: 'var(--sp-2) var(--sp-3)', background: 'var(--surface-raised)',
|
|
border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
|
|
color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.85rem',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: 'var(--sp-3)' }}>
|
|
<div style={{ fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 'var(--sp-1)' }}>
|
|
Amount (min 500 ISK)
|
|
</div>
|
|
<input type="number" value={placeBountyAmount} min={500}
|
|
onChange={e => setPlaceBountyAmount(parseInt(e.target.value) || 0)}
|
|
style={{
|
|
width: '100%', padding: 'var(--sp-2) var(--sp-3)', background: 'var(--surface-raised)',
|
|
border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
|
|
color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.85rem',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Tier preview */}
|
|
<div style={{ marginBottom: 'var(--sp-4)', padding: 'var(--sp-3)', background: 'var(--surface-raised)', borderRadius: 'var(--radius-md)' }}>
|
|
<div style={{ fontSize: '0.7rem', color: 'var(--muted)', marginBottom: 'var(--sp-1)' }}>Resulting Tier</div>
|
|
{(() => {
|
|
const t = getTier(placeBountyAmount);
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.85rem', color: t.color, fontWeight: 600 }}>
|
|
{t.tier}
|
|
</span>
|
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: 'var(--fg-dim)' }}>
|
|
Hunter reward: {t.reward} · {t.visibility}
|
|
</span>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 'var(--sp-3)' }}>
|
|
<button className="btn btn-primary" style={{ flex: 1, background: 'var(--red)', borderColor: 'var(--red)' }}
|
|
onClick={handlePlaceBounty}>
|
|
Place Bounty
|
|
</button>
|
|
<button className="btn" onClick={() => setShowPlaceBounty(false)}>Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid-2">
|
|
{/* Active Bounties */}
|
|
<div>
|
|
<h3 style={{ marginBottom: 'var(--sp-4)' }}>
|
|
<span style={{ color: 'var(--red)' }}>◉</span> Active Bounties
|
|
</h3>
|
|
|
|
{bounties.length === 0 && (
|
|
<div className="card" style={{ textAlign: 'center', color: 'var(--muted)', padding: 'var(--sp-8)' }}>
|
|
No active bounties. Place one to get started.
|
|
</div>
|
|
)}
|
|
|
|
{bounties.sort((a, b) => b.pool - a.pool).map((bounty, i) => {
|
|
const tier = getTier(bounty.pool);
|
|
return (
|
|
<div key={i} className="card" style={{
|
|
marginBottom: 'var(--sp-3)',
|
|
borderLeft: `3px solid ${tier.color}`,
|
|
}}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--sp-3)' }}>
|
|
<div>
|
|
<h4 style={{ margin: 0, color: 'var(--fg-bright)' }}>{bounty.target}</h4>
|
|
<span className="pill" style={{
|
|
background: tier.color + '15', color: tier.color,
|
|
border: `1px solid ${tier.color}40`, fontSize: '0.6rem',
|
|
}}>
|
|
{tier.tier.toUpperCase()}
|
|
</span>
|
|
</div>
|
|
<div style={{ textAlign: 'right' }}>
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '1.1rem', color: 'var(--accent)', fontWeight: 700 }}>
|
|
₢{bounty.pool.toLocaleString()}
|
|
</div>
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)' }}>
|
|
Hunter reward: {tier.reward}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem' }}>
|
|
<span style={{ color: 'var(--muted)' }}>Visibility: <span style={{ color: tier.color }}>{tier.visibility}</span></span>
|
|
<span style={{ color: 'var(--muted)' }}>Last hostile: {bounty.lastHostile}</span>
|
|
</div>
|
|
|
|
{/* Pool bar */}
|
|
<div style={{ marginTop: 'var(--sp-3)' }}>
|
|
<div className="progress-bar" style={{ height: '4px' }}>
|
|
<div className="fill" style={{
|
|
width: `${Math.min(100, (bounty.pool / 500000) * 100)}%`,
|
|
background: tier.color,
|
|
}} />
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: 'var(--font-mono)', fontSize: '0.6rem', color: 'var(--muted)', marginTop: '2px' }}>
|
|
<span>₢{tier.threshold.toLocaleString()}</span>
|
|
<span>Next tier</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{/* Tier legend */}
|
|
<div className="card" style={{ marginTop: 'var(--sp-4)' }}>
|
|
<h4 style={{ marginBottom: 'var(--sp-3)' }}>Bounty Tiers</h4>
|
|
{tierConfig.map((t, i) => (
|
|
<div key={i} style={{
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
padding: 'var(--sp-2) 0', borderBottom: '1px solid var(--border)',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-2)' }}>
|
|
<span style={{ color: t.color, fontWeight: 600, fontSize: '0.85rem' }}>{t.tier}</span>
|
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--muted)' }}>
|
|
≥ ₢{t.threshold.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
<div style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>
|
|
{t.reward} · {t.visibility}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Kill Feed */}
|
|
<div>
|
|
<h3 style={{ marginBottom: 'var(--sp-4)' }}>
|
|
<span style={{ color: 'var(--accent)' }}>✸</span> Kill Feed
|
|
{autoFeed && (
|
|
<span style={{ marginLeft: 'var(--sp-2)', fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--red)' }}>
|
|
● LIVE
|
|
</span>
|
|
)}
|
|
</h3>
|
|
|
|
<div ref={feedRef} style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
|
{killFeed.length === 0 && (
|
|
<div className="card" style={{ textAlign: 'center', color: 'var(--muted)', padding: 'var(--sp-8)' }}>
|
|
No kill events yet. Start the live feed or generate events manually.
|
|
</div>
|
|
)}
|
|
|
|
{killFeed.map((kill, i) => (
|
|
<div key={i} style={{
|
|
padding: 'var(--sp-3) var(--sp-4)',
|
|
marginBottom: 'var(--sp-2)',
|
|
background: i === 0 && autoFeed ? 'var(--accent-bg)' : 'var(--surface)',
|
|
border: `1px solid ${kill.bounty > 0 ? 'var(--accent-border)' : 'var(--border)'}`,
|
|
borderRadius: 'var(--radius-md)',
|
|
transition: 'background 0.3s',
|
|
}}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<div>
|
|
<span style={{ color: 'var(--red)', fontSize: '0.85rem' }}>{kill.victim}</span>
|
|
<span style={{ color: 'var(--muted)', fontSize: '0.8rem', margin: '0 var(--sp-2)' }}>destroyed by</span>
|
|
<span style={{ color: 'var(--cyan)', fontSize: '0.85rem' }}>{kill.killer}</span>
|
|
</div>
|
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)' }}>
|
|
{kill.time}
|
|
</span>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 'var(--sp-4)', marginTop: 'var(--sp-1)', fontSize: '0.75rem' }}>
|
|
<span style={{ color: 'var(--muted)' }}>
|
|
Ship: <span style={{ color: 'var(--fg-dim)' }}>{kill.ship}</span>
|
|
</span>
|
|
<span style={{ color: 'var(--muted)' }}>
|
|
System: <span style={{ color: 'var(--fg-dim)' }}>{kill.system}</span>
|
|
</span>
|
|
{kill.bounty > 0 && (
|
|
<span style={{ color: 'var(--accent)', fontFamily: 'var(--font-mono)' }}>
|
|
Bounty: ₢{kill.bounty.toLocaleString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Anti-abuse rules */}
|
|
<div className="callout callout-warn" style={{ marginTop: 'var(--sp-5)' }}>
|
|
<strong>Anti-abuse rules (implemented in backend):</strong> You cannot claim your own bounty (alt check).
|
|
Payout never exceeds ship loss value. Minimum placement is 500 ISK. Target must have negative security
|
|
status or committed a hostile act within 24h. Bounties decay 10%/week if target stays clean for 30 days.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
window.GDD.BountyDemo = BountyDemo;
|