Files
Space-Game/js/demos/bounty.js
2026-05-25 13:00:20 -04:00

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;