674 lines
43 KiB
JavaScript
674 lines
43 KiB
JavaScript
window.GDD = window.GDD || {};
|
||
|
||
const { useState } = React;
|
||
|
||
/* ── Shared inline styles ── */
|
||
const mono = { fontFamily: 'var(--font-mono)' };
|
||
const dimText = { color: 'var(--fg-dim)', fontSize: '0.9rem' };
|
||
const dimSmall = { color: 'var(--fg-dim)', fontSize: '0.82rem' };
|
||
|
||
/* ── Small sub-components ── */
|
||
function AgentLifecycleDiagram() {
|
||
const phases = [
|
||
{ id: 'register', label: 'REGISTER', color: 'var(--purple)', desc: 'Scheduled table row inserted with initial interval, reducer target, and payload.' },
|
||
{ id: 'sleep', label: 'SLEEP', color: 'var(--muted)', desc: 'Row sits inert. No CPU, no allocation. SpacetimeDB\'s scheduler owns the timer.' },
|
||
{ id: 'wake', label: 'WAKE', color: 'var(--accent)', desc: 'Scheduler fires the bound reducer at the scheduled timestamp. Transaction begins.' },
|
||
{ id: 'execute', label: 'EXECUTE', color: 'var(--cyan)', desc: 'Reducer logic runs: read tables, mutate state, emit events, maybe re-schedule itself.' },
|
||
{ id: 'complete', label: 'COMPLETE', color: 'var(--green)', desc: 'Transaction commits or aborts. On commit, row updated (next_fire_at) or deleted. Subscribers notified.' },
|
||
];
|
||
|
||
return (
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--sp-2)', overflowX: 'auto', padding: 'var(--sp-4) 0' }}>
|
||
{phases.map((p, i) => (
|
||
<React.Fragment key={p.id}>
|
||
<div style={{
|
||
flex: '1 1 0', minWidth: '140px',
|
||
background: 'var(--surface-raised)',
|
||
border: `1px solid var(--border)`,
|
||
borderRadius: 'var(--radius-lg)',
|
||
padding: 'var(--sp-4)',
|
||
borderTop: `3px solid ${p.color}`,
|
||
}}>
|
||
<div style={{ ...mono, fontSize: '0.7rem', color: p.color, letterSpacing: '0.06em', marginBottom: 'var(--sp-2)' }}>
|
||
{p.label}
|
||
</div>
|
||
<p style={{ ...dimSmall, margin: 0, lineHeight: 1.5 }}>
|
||
{p.desc}
|
||
</p>
|
||
</div>
|
||
{i < phases.length - 1 && (
|
||
<div style={{ display: 'flex', alignItems: 'center', color: 'var(--border-light)', fontSize: '1.2rem', paddingTop: '28px' }}>
|
||
→
|
||
</div>
|
||
)}
|
||
</React.Fragment>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ScheduleDiagram() {
|
||
return (
|
||
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', padding: 'var(--sp-5)', marginBottom: 'var(--sp-5)' }}>
|
||
{/* Timeline visualization */}
|
||
<div style={{ position: 'relative', height: '120px', marginBottom: 'var(--sp-4)' }}>
|
||
{/* Time axis */}
|
||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '2px', background: 'var(--border-light)' }} />
|
||
<div style={{ position: 'absolute', bottom: '-18px', left: 0, ...mono, fontSize: '0.65rem', color: 'var(--muted)' }}>t=0</div>
|
||
<div style={{ position: 'absolute', bottom: '-18px', right: 0, ...mono, fontSize: '0.65rem', color: 'var(--muted)' }}>t+5min</div>
|
||
|
||
{/* Fixed-interval ticks */}
|
||
{[0.15, 0.35, 0.55, 0.75, 0.92].map((pct, i) => (
|
||
<React.Fragment key={`fixed-${i}`}>
|
||
<div style={{ position: 'absolute', bottom: 0, left: `${pct * 100}%`, width: '2px', height: '10px', background: 'var(--accent)' }} />
|
||
<div style={{
|
||
position: 'absolute', bottom: '14px', left: `${pct * 100}%`, transform: 'translateX(-50%)',
|
||
width: '8px', height: '8px', borderRadius: '50%', background: 'var(--accent)',
|
||
boxShadow: '0 0 8px rgba(240,160,48,0.4)',
|
||
}} />
|
||
</React.Fragment>
|
||
))}
|
||
|
||
{/* Conditional ticks */}
|
||
{[0.45, 0.78].map((pct, i) => (
|
||
<React.Fragment key={`cond-${i}`}>
|
||
<div style={{ position: 'absolute', bottom: 0, left: `${pct * 100}%`, width: '2px', height: '10px', background: 'var(--cyan)' }} />
|
||
<div style={{
|
||
position: 'absolute', top: '10px', left: `${pct * 100}%`, transform: 'translateX(-50%)',
|
||
width: '8px', height: '8px', borderRadius: '50%', background: 'var(--cyan)',
|
||
boxShadow: '0 0 8px rgba(34,211,238,0.4)',
|
||
}} />
|
||
</React.Fragment>
|
||
))}
|
||
|
||
{/* One-shot */}
|
||
<div style={{ position: 'absolute', bottom: 0, left: '60%', width: '2px', height: '10px', background: 'var(--purple)' }} />
|
||
<div style={{
|
||
position: 'absolute', top: '40px', left: '60%', transform: 'translateX(-50%)',
|
||
width: '10px', height: '10px', borderRadius: '50%', background: 'var(--purple)',
|
||
boxShadow: '0 0 8px rgba(167,139,250,0.4)',
|
||
}} />
|
||
|
||
{/* Labels */}
|
||
<div style={{ position: 'absolute', top: 0, left: 0, display: 'flex', gap: 'var(--sp-6)', ...mono, fontSize: '0.65rem' }}>
|
||
<span style={{ color: 'var(--accent)' }}>● Fixed interval</span>
|
||
<span style={{ color: 'var(--cyan)' }}>● Conditional</span>
|
||
<span style={{ color: 'var(--purple)' }}>● One-shot</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── Main page component ── */
|
||
function AgentsPage() {
|
||
const [activeSection, setActiveSection] = useState('lifecycle');
|
||
const [activeCatalogPkg, setActiveCatalogPkg] = useState('game');
|
||
|
||
const tabs = [
|
||
{ id: 'lifecycle', label: 'Lifecycle' },
|
||
{ id: 'scheduling', label: 'Scheduling' },
|
||
{ id: 'killswitch', label: 'Kill-Switch' },
|
||
{ id: 'catalog', label: 'Agent Catalog' },
|
||
];
|
||
|
||
return (
|
||
<div className="content-inner">
|
||
<h1 style={{ marginBottom: '8px' }}>Agent Lifecycle & Scheduling</h1>
|
||
<p style={{ ...dimText, maxWidth: '720px' }}>
|
||
The server runs dozens of background agents that drive the living world — from enemy health regeneration
|
||
and building decay to NPC trade migrations and day/night cycles. These agents are not OS-level processes
|
||
or threads; they are <strong style={{ color: 'var(--fg-bright)' }}>SpacetimeDB scheduled tables</strong> that
|
||
invoke reducer functions at configurable intervals, turning the database itself into a reliable task scheduler.
|
||
</p>
|
||
|
||
{/* Section tabs */}
|
||
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-6)', marginTop: 'var(--sp-4)' }}>
|
||
{tabs.map(t => (
|
||
<button key={t.id}
|
||
className={`btn btn-sm${activeSection === t.id ? ' btn-primary' : ''}`}
|
||
onClick={() => setActiveSection(t.id)}>
|
||
{t.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* ────────────────────── LIFECYCLE ────────────────────── */}
|
||
{activeSection === 'lifecycle' && (
|
||
<>
|
||
<div className="section-header">
|
||
<span className="section-num">AGENT-1</span>
|
||
<h2 style={{ margin: 0 }}>Uniform Lifecycle</h2>
|
||
</div>
|
||
|
||
<p style={{ ...dimText, marginBottom: 'var(--sp-4)' }}>
|
||
Every agent — regardless of what it simulates — follows the same five-phase lifecycle.
|
||
This uniformity means monitoring, debugging, and load-testing share one toolchain.
|
||
</p>
|
||
|
||
<AgentLifecycleDiagram />
|
||
|
||
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
|
||
<span className="section-num">AGENT-1.1</span>
|
||
<h2 style={{ margin: 0 }}>Scheduled Table Schema</h2>
|
||
</div>
|
||
|
||
<p style={{ ...dimText, marginBottom: 'var(--sp-4)' }}>
|
||
Each agent is a row in a <code style={{ color: 'var(--accent)' }}>scheduled_agents</code> table.
|
||
SpacetimeDB's built-in scheduler reads <code>next_fire_at</code> and invokes the
|
||
bound reducer at exactly that timestamp — no external cron, no process supervisor.
|
||
</p>
|
||
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr><th>Column</th><th>Type</th><th>Purpose</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ col: 'agent_id', type: 'Identity', purpose: 'Unique agent instance key. Auto-generated on insert.' },
|
||
{ col: 'agent_type', type: 'String', purpose: 'Catalog key: "enemy_regen", "decay_tick", "npc_trade", "day_cycle", etc.' },
|
||
{ col: 'target_entity_id', type: 'u64', purpose: 'Optional entity this agent operates on (e.g. enemy_id, building_id).' },
|
||
{ col: 'reducer_name', type: 'String', purpose: 'Fully-qualified reducer to invoke: "game.enemy_regen_tick".' },
|
||
{ col: 'interval_ms', type: 'u64', purpose: 'Fixed interval in milliseconds. Zero for one-shot agents.' },
|
||
{ col: 'next_fire_at', type: 'Duration', purpose: 'Timestamp of next invocation. Scheduler reads this; agent never touches it directly after registration.' },
|
||
{ col: 'payload', type: 'JSON', purpose: 'Opaque reducer arguments. Each agent type defines its own schema.' },
|
||
{ col: 'state', type: 'enum', purpose: 'active | paused | killed. Only "active" rows are scheduled.' },
|
||
{ col: 'generation', type: 'u32', purpose: 'Monotonic counter incremented on each fire. Prevents stale re-schedules.' },
|
||
{ col: 'created_at', type: 'Timestamp', purpose: 'Registration time. Used for uptime metrics and TTL enforcement.' },
|
||
{ col: 'owner_module', type: 'String', purpose: 'Package that owns this agent: "game" or "global_module".' },
|
||
].map((row, i) => (
|
||
<tr key={i}>
|
||
<td><code style={{ fontSize: '0.75rem' }}>{row.col}</code></td>
|
||
<td><span className="pill pill-cyan" style={{ fontSize: '0.65rem' }}>{row.type}</span></td>
|
||
<td style={{ ...dimSmall }}>{row.purpose}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
|
||
<span className="section-num">AGENT-1.2</span>
|
||
<h2 style={{ margin: 0 }}>Registration Reducer</h2>
|
||
</div>
|
||
|
||
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||
<div style={{ padding: 'var(--sp-3) var(--sp-4)', background: 'var(--surface-raised)', borderBottom: '1px solid var(--border)' }}>
|
||
<span style={{ ...mono, fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||
register_agent — Pseudocode
|
||
</span>
|
||
</div>
|
||
<div className="code-block" style={{ margin: 0, borderRadius: 0 }}>
|
||
<code>
|
||
<span className="cm">// Called by game logic to spawn a new agent</span><br/>
|
||
<span className="kw">reducer</span> <span className="fn">register_agent</span>(ctx, agent_type, target_id, interval_ms, payload) {'{'}<br/>
|
||
<span className="cm">// Validate agent_type exists in agent_catalog</span><br/>
|
||
<span className="kw">let</span> catalog_entry = ctx.db.agent_catalog().<span className="fn">find</span>(agent_type);<br/>
|
||
<span className="kw">require</span>!(catalog_entry.is_some(), <span className="str">"Unknown agent type"</span>);<br/>
|
||
<br/>
|
||
<span className="cm">// Enforce per-type concurrency limit</span><br/>
|
||
<span className="kw">let</span> active_count = ctx.db.scheduled_agents().<span className="fn">iter</span>()<br/>
|
||
.filter(|a| a.agent_type == agent_type && a.state == Active).count();<br/>
|
||
<span className="kw">require</span>!(active_count {'<'} catalog_entry.max_concurrent);<br/>
|
||
<br/>
|
||
ctx.db.scheduled_agents().<span className="fn">insert</span>(ScheduledAgent {'{'}<br/>
|
||
agent_id: Identity::generate(),<br/>
|
||
agent_type,<br/>
|
||
target_entity_id: target_id,<br/>
|
||
reducer_name: catalog_entry.reducer_name,<br/>
|
||
interval_ms,<br/>
|
||
next_fire_at: ctx.timestamp + interval_ms,<br/>
|
||
payload,<br/>
|
||
state: AgentState::Active,<br/>
|
||
generation: 0,<br/>
|
||
created_at: ctx.timestamp,<br/>
|
||
owner_module: catalog_entry.module,<br/>
|
||
{'}'});<br/>
|
||
{'}'}
|
||
</code>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
|
||
<span className="section-num">AGENT-1.3</span>
|
||
<h2 style={{ margin: 0 }}>Execution Contract</h2>
|
||
</div>
|
||
|
||
<div className="callout callout-warn" style={{ marginBottom: 'var(--sp-5)' }}>
|
||
<strong>Atomicity guarantee:</strong> each agent fire runs inside a single SpacetimeDB transaction.
|
||
If the reducer panics or returns an error, the entire transaction rolls back — no partial state mutation.
|
||
The agent row is not updated, so the scheduler retries at the same <code>next_fire_at</code> on the next tick.
|
||
</div>
|
||
|
||
<div className="grid-3">
|
||
{[
|
||
{ title: 'Read phase', color: 'var(--cyan)', items: [
|
||
'Load agent row + payload from scheduled_agents',
|
||
'Read target entity state (enemy HP, building condition, NPC inventory)',
|
||
'Query related tables (faction relations, player density, market state)',
|
||
]},
|
||
{ title: 'Mutate phase', color: 'var(--accent)', items: [
|
||
'Apply simulation logic (regen HP, decay condition, move NPC, rotate cycle)',
|
||
'Write updated entity state back to tables',
|
||
'Maybe emit world events, story log entries, or chat notifications',
|
||
]},
|
||
{ title: 'Re-schedule phase', color: 'var(--green)', items: [
|
||
'Increment generation counter (prevents double-fire)',
|
||
'Set next_fire_at = now + interval_ms (fixed) or compute next (conditional)',
|
||
'Or delete the row entirely (one-shot or TTL expired)',
|
||
]},
|
||
].map((phase, i) => (
|
||
<div key={i} className="card" style={{ borderTop: `3px solid ${phase.color}` }}>
|
||
<h4 style={{ color: phase.color, marginBottom: 'var(--sp-3)' }}>{phase.title}</h4>
|
||
<ul style={{ ...dimSmall, margin: 0, paddingLeft: 'var(--sp-5)', lineHeight: 1.7 }}>
|
||
{phase.items.map((item, j) => <li key={j}>{item}</li>)}
|
||
</ul>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* ────────────────────── SCHEDULING ────────────────────── */}
|
||
{activeSection === 'scheduling' && (
|
||
<>
|
||
<div className="section-header">
|
||
<span className="section-num">AGENT-2</span>
|
||
<h2 style={{ margin: 0 }}>Three Scheduling Strategies</h2>
|
||
</div>
|
||
|
||
<p style={{ ...dimText, marginBottom: 'var(--sp-5)' }}>
|
||
All agents share the same table schema, but <em>when</em> they fire follows one of three
|
||
strategies. The choice is baked into the agent's catalog entry and cannot be changed at
|
||
runtime without re-registration.
|
||
</p>
|
||
|
||
<ScheduleDiagram />
|
||
|
||
{/* Strategy cards */}
|
||
{[
|
||
{
|
||
id: 'fixed',
|
||
title: 'Fixed Interval',
|
||
color: 'var(--accent)',
|
||
tag: 'Most common',
|
||
desc: 'Fire every N milliseconds, unconditionally. The workhorse strategy — used for health regen, resource ticks, decay timers, and day/night cycles.',
|
||
mechanics: [
|
||
'next_fire_at = last_fire_at + interval_ms — simple arithmetic, no conditional logic.',
|
||
'If the server is overloaded and fires late, the next fire is still interval_ms from the actual fire time (not from the scheduled time). Prevents cascade acceleration.',
|
||
'Configurable jitter: ±10% of interval_ms to spread simultaneous agent fires across a window. Prevents thundering-herd CPU spikes when 200 enemies share the same regen tick.',
|
||
],
|
||
examples: 'enemy_regen (2s), building_decay (60s), resource_respawn (300s), day_night_cycle (600s)',
|
||
},
|
||
{
|
||
id: 'conditional',
|
||
title: 'Conditional',
|
||
color: 'var(--cyan)',
|
||
tag: 'Event-gated',
|
||
desc: 'Fire only when a precondition table meets criteria. The scheduler still checks on a fixed cadence, but the reducer body is a no-op until the condition is true.',
|
||
mechanics: [
|
||
'The reducer reads a condition table first (e.g. "has at least one player in system?"). If false, it re-schedules without doing work.',
|
||
'Avoids wasting cycles on empty systems: NPC trade migrations only run in systems with active markets and at least one player docked.',
|
||
'Condition tables are indexed. The read cost of a no-op conditional fire is ~microseconds.',
|
||
],
|
||
examples: 'npc_trade_route (fires only if market activity above threshold), faction_tension_eval (fires only if two factions share a border system with standing < -5)',
|
||
},
|
||
{
|
||
id: 'oneshot',
|
||
title: 'One-Shot',
|
||
color: 'var(--purple)',
|
||
tag: 'Delayed execution',
|
||
desc: 'Fire exactly once after a delay, then self-delete. Used for delayed consequences, timed explosions, cooldown expirations, and cascading event chains.',
|
||
mechanics: [
|
||
'interval_ms = 0 in the row. The scheduler fires at next_fire_at and the reducer deletes the row.',
|
||
'No re-schedule step — the agent simply ceases to exist after execution.',
|
||
'Can be re-registered by the reducer itself or by another agent, creating event chains: "pirate_raid_warning fires → 30s later pirate_raid_arrival fires → raid logic runs".',
|
||
],
|
||
examples: 'delayed_damage (3s after mine explosion), event_cascade (fires next event in a story chain), cooldown_expire (marks ability ready again)',
|
||
},
|
||
].map((strategy) => (
|
||
<div key={strategy.id} className="card" style={{ borderTop: `3px solid ${strategy.color}`, marginBottom: 'var(--sp-5)' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-4)' }}>
|
||
<h3 style={{ margin: 0, color: strategy.color }}>{strategy.title}</h3>
|
||
<span className="pill pill-amber" style={{ fontSize: '0.65rem' }}>{strategy.tag}</span>
|
||
</div>
|
||
<p style={{ ...dimText, marginBottom: 'var(--sp-4)' }}>{strategy.desc}</p>
|
||
<h4 style={{ color: 'var(--fg-bright)', fontSize: '0.85rem', marginBottom: 'var(--sp-2)' }}>Mechanics</h4>
|
||
<ul style={{ ...dimSmall, margin: 0, paddingLeft: 'var(--sp-5)', lineHeight: 1.7, marginBottom: 'var(--sp-4)' }}>
|
||
{strategy.mechanics.map((m, i) => <li key={i}>{m}</li>)}
|
||
</ul>
|
||
<div style={{ ...mono, fontSize: '0.75rem', color: 'var(--muted)', borderTop: '1px solid var(--border)', paddingTop: 'var(--sp-3)' }}>
|
||
<span style={{ color: 'var(--fg-dim)' }}>Examples:</span> {strategy.examples}
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
<div className="section-header" style={{ marginTop: 'var(--sp-6)' }}>
|
||
<span className="section-num">AGENT-2.1</span>
|
||
<h2 style={{ margin: 0 }}>Jitter & Thundering Herd Prevention</h2>
|
||
</div>
|
||
|
||
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
|
||
When 500 enemy entities all share a 2-second regen interval, they were likely registered at the same time — meaning
|
||
all 500 fire simultaneously. Jitter spreads each agent's <code>next_fire_at</code> by a random ±X% of its interval,
|
||
turning a single CPU spike into a smooth load curve.
|
||
</div>
|
||
|
||
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||
<div style={{ padding: 'var(--sp-3) var(--sp-4)', background: 'var(--surface-raised)', borderBottom: '1px solid var(--border)' }}>
|
||
<span style={{ ...mono, fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||
Jitter Application — Pseudocode
|
||
</span>
|
||
</div>
|
||
<div className="code-block" style={{ margin: 0, borderRadius: 0 }}>
|
||
<code>
|
||
<span className="kw">fn</span> <span className="fn">apply_jitter</span>(base_interval_ms: <span className="type">u64</span>, jitter_pct: <span className="type">f32</span>) -> <span className="type">Duration</span> {'{'}<br/>
|
||
<span className="kw">let</span> jitter_range = (base_interval_ms <span className="kw">as</span> <span className="type">f64</span>) * (jitter_pct <span className="kw">as</span> <span className="type">f64</span>);<br/>
|
||
<span className="kw">let</span> offset = thread_rng().<span className="fn">gen_range</span>(-jitter_range..jitter_range);<br/>
|
||
Duration::from_millis((base_interval_ms <span className="kw">as</span> <span className="type">f64</span> + offset) <span className="kw">as</span> <span className="type">u64</span>)<br/>
|
||
{'}'}<br/>
|
||
<br/>
|
||
<span className="cm">// Applied during registration and each re-schedule:</span><br/>
|
||
agent.next_fire_at = ctx.timestamp + <span className="fn">apply_jitter</span>(agent.interval_ms, <span className="str">0.10</span>);
|
||
</code>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* ────────────────────── KILL-SWITCH ────────────────────── */}
|
||
{activeSection === 'killswitch' && (
|
||
<>
|
||
<div className="section-header">
|
||
<span className="section-num">AGENT-3</span>
|
||
<h2 style={{ margin: 0 }}>Global Kill-Switch</h2>
|
||
</div>
|
||
|
||
<p style={{ ...dimText, marginBottom: 'var(--sp-5)' }}>
|
||
A single admin-level toggle can pause or terminate every agent of a given type — or all agents
|
||
across the entire server. This is the primary operational lever for hotfixes, load shedding,
|
||
and emergency maintenance.
|
||
</p>
|
||
|
||
<div className="card" style={{ borderTop: '3px solid var(--red)', marginBottom: 'var(--sp-5)' }}>
|
||
<div style={{ display: 'flex', gap: 'var(--sp-5)', alignItems: 'center' }}>
|
||
<div style={{ flex: 1 }}>
|
||
<h4 style={{ color: 'var(--red)', marginBottom: 'var(--sp-2)' }}>Emergency Shutdown</h4>
|
||
<p style={{ ...dimSmall, margin: 0 }}>
|
||
A single reducer <code style={{ color: 'var(--red)' }}>kill_all_agents</code> iterates every row
|
||
in <code>scheduled_agents</code> and sets <code>state = Killed</code>. The scheduler skips killed
|
||
rows entirely. No reducers fire. No CPU. Agents can be selectively revived by type or module.
|
||
</p>
|
||
</div>
|
||
<div style={{ ...mono, fontSize: '0.75rem', color: 'var(--red)', background: 'var(--red-bg)', padding: 'var(--sp-3) var(--sp-4)', borderRadius: 'var(--radius-md)', whiteSpace: 'nowrap', border: '1px solid rgba(239,68,68,0.25)' }}>
|
||
⬛ KILL ALL
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="section-header" style={{ marginTop: 'var(--sp-6)' }}>
|
||
<span className="section-num">AGENT-3.1</span>
|
||
<h2 style={{ margin: 0 }}>Kill-Switch Mechanics</h2>
|
||
</div>
|
||
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr><th>Operation</th><th>Scope</th><th>Behavior</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ op: 'kill_all_agents()', scope: 'Server-wide', behavior: 'Sets state=Killed on every row. Scheduler skips all. Agents retain their payload and next_fire_at for potential revival.' },
|
||
{ op: 'kill_agents_by_type(type)', scope: 'Type-wide', behavior: 'Kills only agents matching agent_type. Used when one simulation is misbehaving (e.g. enemy_regen running amok) without touching NPC trades.' },
|
||
{ op: 'kill_agents_by_module(module)', scope: 'Package-wide', behavior: 'Kills all agents owned by a module ("game" or "global_module"). Allows safe module hot-reload.' },
|
||
{ op: 'pause_agent(agent_id)', scope: 'Single agent', behavior: 'Sets state=Paused. Scheduler skips but row persists. Used to suspend a specific NPC\'s trade route.' },
|
||
{ op: 'revive_agent(agent_id)', scope: 'Single agent', behavior: 'Sets state=Active, recalculates next_fire_at from now. Agent resumes as if freshly registered.' },
|
||
{ op: 'revive_all_by_type(type)', scope: 'Type-wide', behavior: 'Revives all killed/paused agents of a type. Re-applies jitter to prevent synchronized firing.' },
|
||
].map((row, i) => (
|
||
<tr key={i}>
|
||
<td><code style={{ fontSize: '0.75rem', color: 'var(--accent)' }}>{row.op}</code></td>
|
||
<td><span className="pill pill-purple">{row.scope}</span></td>
|
||
<td style={{ ...dimSmall }}>{row.behavior}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
|
||
<span className="section-num">AGENT-3.2</span>
|
||
<h2 style={{ margin: 0 }}>Safety Properties</h2>
|
||
</div>
|
||
|
||
<div className="grid-2">
|
||
{[
|
||
{
|
||
title: 'No orphan state',
|
||
color: 'var(--green)',
|
||
desc: 'When an agent is killed mid-execution (transaction aborted), the entity it was modifying rolls back. No enemy is half-healed, no building is half-decayed. The atomicity guarantee ensures killed agents leave zero partial state.',
|
||
},
|
||
{
|
||
title: 'Revival preserves payload',
|
||
color: 'var(--cyan)',
|
||
desc: 'Killed agents retain their payload, target_entity_id, and interval_ms. Revival simply flips state back to Active and recalculates next_fire_at. The agent continues as if it had paused — no re-registration needed.',
|
||
},
|
||
{
|
||
title: 'Cascade protection',
|
||
color: 'var(--accent)',
|
||
desc: 'If agent A fires and registers agent B (event chain), killing agent A does NOT kill agent B. Each agent row is independent. Kill operations are explicitly scoped — cascade kills require a custom reducer.',
|
||
},
|
||
{
|
||
title: 'Generation guard',
|
||
color: 'var(--purple)',
|
||
desc: 'Each fire increments a generation counter. If a kill arrives between the scheduler reading the row and the reducer executing, the generation mismatch aborts the transaction. Prevents zombie fires.',
|
||
},
|
||
].map((item, i) => (
|
||
<div key={i} className="card" style={{ borderLeft: `3px solid ${item.color}` }}>
|
||
<h4 style={{ color: item.color, marginBottom: 'var(--sp-2)' }}>{item.title}</h4>
|
||
<p style={{ ...dimSmall, margin: 0, lineHeight: 1.6 }}>{item.desc}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* ────────────────────── CATALOG ────────────────────── */}
|
||
{activeSection === 'catalog' && (
|
||
<>
|
||
<div className="section-header">
|
||
<span className="section-num">AGENT-4</span>
|
||
<h2 style={{ margin: 0 }}>Agent Catalog</h2>
|
||
</div>
|
||
|
||
<p style={{ ...dimText, marginBottom: 'var(--sp-4)' }}>
|
||
Complete inventory of every background agent in the server, organized by owning package.
|
||
Each entry defines the reducer to invoke, scheduling strategy, interval, concurrency cap,
|
||
and what it simulates.
|
||
</p>
|
||
|
||
{/* Package tabs */}
|
||
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-5)' }}>
|
||
{[
|
||
{ id: 'game', label: 'game', icon: '◉' },
|
||
{ id: 'global_module', label: 'global_module', icon: '◈' },
|
||
].map(pkg => (
|
||
<button key={pkg.id}
|
||
className={`btn btn-sm${activeCatalogPkg === pkg.id ? ' btn-primary' : ''}`}
|
||
onClick={() => setActiveCatalogPkg(pkg.id)}>
|
||
{pkg.icon} {pkg.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{activeCatalogPkg === 'game' && (
|
||
<>
|
||
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-3)' }}>Combat & Entity Agents</h4>
|
||
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ type: 'enemy_regen', strat: 'fixed', interval: '2s', max: '∞', desc: 'Regenerates enemy shield/armor HP per tick. Payload contains regen_rate, max_hp. Skips if enemy dead.' },
|
||
{ type: 'enemy_respawn', strat: 'one-shot', interval: '30–120s', max: '∞', desc: 'Delays enemy respawn after death. Fires once, deletes self, inserts new enemy entity row.' },
|
||
{ type: 'aggro_scan', strat: 'fixed', interval: '1s', max: '∞', desc: 'Scans nearby players within aggro range. Transitions enemy from idle → combat state.' },
|
||
{ type: 'pirate_spawn', strat: 'conditional', interval: '300s', max: '∞', desc: 'Spawns NPC pirates at belts, gates, and anomalies. Condition: player proximity OR world tick in low/null-sec. Respects per-system density cap (10 max).' },
|
||
{ type: 'pirate_combat_tick', strat: 'fixed', interval: '1s', max: '1 per engaged NPC', desc: 'Executes NPC behavior template per tick: orbit/approach/kite/flee. Applies damage to target, regenerates shields/armor. Only runs for NPCs in combat state.' },
|
||
{ type: 'pirate_loot_drop', strat: 'one-shot', interval: '0s', max: '∞', desc: 'Generates loot and bounty from destroyed NPC. Rolls loot table, awards ISK bounty to top damage contributor, creates wreck with items.' },
|
||
{ type: 'loot_decay', strat: 'fixed', interval: '120s', max: '500', desc: 'Counts down loot container lifetime. On expiry, removes container row and frees the slot.' },
|
||
{ type: 'pvp_session_timer', strat: 'one-shot', interval: 'varies', max: '∞', desc: 'Tracks PvP engagement timers (combat flags, weapons timers). Fires on expiry to clear flags.' },
|
||
].map((row, i) => (
|
||
<tr key={i}>
|
||
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
|
||
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
|
||
<td style={{ ...dimSmall }}>{row.desc}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-3)' }}>World & Environment Agents</h4>
|
||
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ type: 'day_night_cycle', strat: 'fixed', interval: '600s', max: '1', desc: 'Advances the global day/night phase. Updates lighting table, triggers fauna behavior shifts, affects stealth detection range.' },
|
||
{ type: 'weather_tick', strat: 'fixed', interval: '300s', max: '1 per system', desc: 'Evolves system weather state (clear → storm → nebula haze). Affects sensor range, weapon accuracy, and visual overlay.' },
|
||
{ type: 'asteroid_respawn', strat: 'conditional', interval: '300s', max: '1 per belt', desc: 'Checks if belt is below max asteroids. Condition: player count in system > 0 AND asteroid count < belt_capacity.' },
|
||
{ type: 'anomaly_lifecycle', strat: 'one-shot', interval: 'varies', max: '1 per anomaly', desc: 'Spawned by world_tick. Self-deletes on expiry, removing the anomaly row and dropping any unresolved loot.' },
|
||
{ type: 'fauna_migration', strat: 'fixed', interval: '1800s', max: '1 per species', desc: 'Advances fauna along their migration route. Updates current_system_id, writes story log if entering a populated system.' },
|
||
].map((row, i) => (
|
||
<tr key={i}>
|
||
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
|
||
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
|
||
<td style={{ ...dimSmall }}>{row.desc}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-3)' }}>Structure & Decay Agents</h4>
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ type: 'building_decay', strat: 'fixed', interval: '60s', max: '∞', desc: 'Reduces building condition per tick (0.1–2% depending on material). At 0%, marks for demolition.' },
|
||
{ type: 'fuel_consumption', strat: 'fixed', interval: '300s', max: '1 per structure', desc: 'Deducts fuel from powered structures. If fuel hits 0, disables structure services.' },
|
||
{ type: 'production_cycle', strat: 'fixed', interval: 'varies', max: '1 per structure', desc: 'Advances manufacturing jobs. On completion, moves output to structure inventory.' },
|
||
{ type: 'reinforcement_timer', strat: 'one-shot', interval: '24–72h', max: '1 per structure', desc: 'Delays structure destruction in PvP. Allows owners time to defend. Fires → structure becomes vulnerable.' },
|
||
].map((row, i) => (
|
||
<tr key={i}>
|
||
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
|
||
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
|
||
<td style={{ ...dimSmall }}>{row.desc}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{activeCatalogPkg === 'global_module' && (
|
||
<>
|
||
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-3)' }}>Economy & Trade Agents</h4>
|
||
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ type: 'npc_trade_route', strat: 'conditional', interval: '600s', max: '50', desc: 'Evaluates NPC trade route viability. Condition: market activity > threshold AND at least one player docked. Moves goods between stations.' },
|
||
{ type: 'market_price_adjust', strat: 'fixed', interval: '300s', max: '1 per item_type', desc: 'Ticks NPC demand pressure algorithm: updates flow_ema, recomputes demand_pressure, applies idle decay. See Economy → NPC Pricing tab for full algorithm spec.' },
|
||
{ type: 'bounty_pool_refresh', strat: 'fixed', interval: '3600s', max: '1', desc: 'Refreshes the global bounty pool based on faction military budgets. Allocates bounty value to active threats.' },
|
||
{ type: 'insurance_payout', strat: 'one-shot', interval: '30–120s', max: '∞', desc: 'Delays ship insurance payout. Fires once, credits ISK to player, deletes self. Prevents instant-PvP-profit loops.' },
|
||
].map((row, i) => (
|
||
<tr key={i}>
|
||
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
|
||
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
|
||
<td style={{ ...dimSmall }}>{row.desc}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-3)' }}>NPC & Faction Agents</h4>
|
||
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ type: 'faction_tension_eval', strat: 'conditional', interval: '300s', max: '1 per pair', desc: 'Evaluates if two factions should escalate. Condition: shared border AND standing < -5. Spawns skirmish events if threshold met.' },
|
||
{ type: 'npc_patrol_route', strat: 'fixed', interval: '60s', max: '100', desc: 'Advances NPC patrol along waypoints. Updates position, checks for player encounters, may trigger aggro.' },
|
||
{ type: 'npc_mission_refresh', strat: 'fixed', interval: '1800s', max: '1 per station', desc: 'Regenerates NPC mission offerings. Removes expired missions, adds new ones from template pool weighted by local faction state.' },
|
||
{ type: 'diplomacy_shift', strat: 'fixed', interval: '7200s', max: '1 per pair', desc: 'Slowly drifts faction standing toward baseline. Prevents permanent extreme standings from transient events.' },
|
||
{ type: 'concord_response', strat: 'one-shot', interval: '3–15s', max: '∞', desc: 'Spawns CONCORD fleet at criminal location. Delay based on system security level. Applies overwhelming damage. One-shot: fires, destroys criminal ship, deletes self.' },
|
||
{ type: 'security_status_tick', strat: 'fixed', interval: '3600s', max: '1 per player', desc: 'Passive security status recovery. +0.01 per tick for players with no hostile acts in the last hour. Clean record slowly heals.' },
|
||
].map((row, i) => (
|
||
<tr key={i}>
|
||
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
|
||
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
|
||
<td style={{ ...dimSmall }}>{row.desc}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-3)' }}>Galaxy Story Agents</h4>
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
{ type: 'world_tick', strat: 'fixed', interval: '300s', max: '1', desc: 'Master galaxy simulation tick. Evaluates faction matrix, anomaly slots, fauna routes, player density. Spawns conditional events.' },
|
||
{ type: 'story_chapter_advance', strat: 'one-shot', interval: 'varies', max: '1 per event', desc: 'Advances multi-chapter story events. Each chapter is a one-shot agent that spawns the next on completion.' },
|
||
{ type: 'galaxy_census', strat: 'fixed', interval: '3600s', max: '1', desc: 'Snapshots player counts per system, faction territory control, active event density. Writes to metrics table for admin dashboards.' },
|
||
].map((row, i) => (
|
||
<tr key={i}>
|
||
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
|
||
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
|
||
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
|
||
<td style={{ ...dimSmall }}>{row.desc}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
window.GDD.AgentsPage = AgentsPage;
|