NETRUNNER: Breach Protocol
by StormPenguin581046 lines45.5 KB๐ ๏ธ Phaser (2D game framework)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>NETRUNNER: Breach Protocol</title>
<script>if(typeof Phaser==='undefined'){document.write('<scr'+'ipt src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"><\/scr'+'ipt>');}</script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#08111a;overflow:hidden;font-family:'Courier New',monospace;color:#c0d8e8}
#canvas-wrap{position:fixed;top:0;left:0;width:100%;height:100%;z-index:0}
#canvas-wrap canvas{display:block}
#hud{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;pointer-events:none;display:flex;flex-direction:column}
/* toolbar */
#toolbar{height:34px;background:rgba(4,12,22,.95);border-bottom:1px solid #0a2a3a;display:flex;align-items:center;padding:0 12px;gap:10px;pointer-events:all;flex-shrink:0}
.tb-logo{color:#00c8f0;font-size:12px;font-weight:bold;letter-spacing:3px}
.tb-pipe{color:#0a2a3a}
.tb-corp{color:#ff4070;font-size:10px;letter-spacing:1px}
.tb-phase{color:#ffcc00;font-size:10px;letter-spacing:1px}
.tb-spacer{flex:1}
.tb-status{color:#205060;font-size:10px;letter-spacing:1px}
/* right panel */
#main-row{flex:1;display:flex;min-height:0;pointer-events:none}
#graph-area{flex:1}
#right-panel{width:300px;flex-shrink:0;background:rgba(4,10,18,.96);border-left:1px solid #0a2535;display:flex;flex-direction:column;pointer-events:all;overflow:hidden}
.rp-header{padding:8px 12px;border-bottom:1px solid #0a2535;font-size:10px;color:#205060;letter-spacing:2px;background:rgba(4,8,16,.8);flex-shrink:0}
/* node info panel */
#node-info{flex:1;overflow-y:auto;display:none;flex-direction:column}
#node-info::-webkit-scrollbar{width:3px}
#node-info::-webkit-scrollbar-thumb{background:#0a2535}
.ni-machine{padding:14px 12px 10px;border-bottom:1px solid #0a2535}
.ni-icon{width:52px;height:52px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:22px;margin-bottom:8px;border:1px solid}
.ni-hostname{font-size:14px;color:#c0d8e8;margin-bottom:2px}
.ni-type{font-size:10px;color:#205060;letter-spacing:2px;text-transform:uppercase;margin-bottom:6px}
.ni-row{display:flex;justify-content:space-between;font-size:10px;padding:2px 0}
.ni-row .k{color:#205060}
.ni-row .v{color:#4a9ab0}
.ni-section{padding:10px 12px;border-bottom:1px solid #0a2535}
.ni-section h4{font-size:9px;color:#ffcc00;letter-spacing:2px;margin-bottom:8px;text-transform:uppercase}
.obtained-item{font-size:10px;color:#00aa66;padding:3px 0;display:flex;align-items:flex-start;gap:6px;border-bottom:1px solid #051015;line-height:1.4}
.obtained-item::before{content:'โ';color:#007744;flex-shrink:0}
.obtained-none{font-size:10px;color:#143040;padding:3px 0}
/* technique panel */
#tech-panel{flex:1;overflow-y:auto;display:none;flex-direction:column}
#tech-panel::-webkit-scrollbar{width:3px}
#tech-panel::-webkit-scrollbar-thumb{background:#0a2535}
.tp-phase{padding:10px 12px;border-bottom:1px solid #0a2535;flex-shrink:0}
.tp-atk{font-size:9px;color:#205060;letter-spacing:2px;text-transform:uppercase;margin-bottom:2px}
.tp-net{font-size:14px;color:#00c8f0;margin-bottom:4px}
.tp-desc{font-size:10px;color:#2a6070;line-height:1.5}
.tp-cards{padding:8px 12px;display:flex;flex-direction:column;gap:6px}
.tp-cards-label{font-size:9px;color:#205060;letter-spacing:2px;text-transform:uppercase;margin-bottom:4px}
/* technique card */
.tcard{background:#040e18;border:1px solid #0a2535;border-radius:4px;padding:9px 10px;cursor:pointer;transition:all .12s}
.tcard:hover{background:#071828;border-color:#00c8f0}
.tcard.oos{border-color:#2a0a0a;background:#0a0408}
.tcard.oos:hover{border-color:#ff2020}
.tcard-name{font-size:11px;color:#c0d8e8;margin-bottom:3px;display:flex;align-items:center;gap:6px}
.tcard-key{background:#0a2535;color:#205060;font-size:8px;padding:1px 4px;border-radius:2px}
.tcard-oos-badge{background:#2a0a0a;color:#ff4040;font-size:8px;padding:1px 5px;border-radius:2px}
.tcard-desc{font-size:9px;color:#1a4a5a;line-height:1.45;margin-bottom:5px}
.tcard-stats{display:flex;gap:10px;font-size:9px}
.ts-suc{color:#00bb55}
.ts-tr{color:#ff5577}
.ts-ram{color:#00aad4}
.tcard.quiet .ts-tr{color:#6677cc}
/* result area */
#result-area{padding:10px 12px;border-top:1px solid #0a2535;display:none;flex-shrink:0;background:rgba(4,8,14,.9)}
#result-text{font-size:10px;line-height:1.6;margin-bottom:8px}
#action-btn{display:none;padding:7px 16px;background:#00253a;border:1px solid #00c8f0;color:#00c8f0;font-family:inherit;font-size:11px;cursor:pointer;border-radius:3px;letter-spacing:1px;pointer-events:all}
#action-btn:hover{background:#00c8f0;color:#08111a}
#retry-btn{display:none;padding:7px 16px;background:#1a0a00;border:1px solid #ff7700;color:#ff7700;font-family:inherit;font-size:11px;cursor:pointer;border-radius:3px;letter-spacing:1px;margin-left:6px;pointer-events:all}
#retry-btn:hover{background:#ff7700;color:#08111a}
/* empty state */
#empty-panel{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:20px}
#empty-panel .ep-icon{font-size:28px;color:#0a2535}
#empty-panel .ep-text{font-size:10px;color:#143040;text-align:center;line-height:1.6}
/* bottom bar */
#bottom-bar{height:42px;background:rgba(4,12,22,.95);border-top:1px solid #0a2535;display:flex;align-items:center;padding:0 12px;gap:14px;pointer-events:all;flex-shrink:0}
.meter-wrap{display:flex;align-items:center;gap:5px}
.meter-lbl{font-size:9px;color:#205060;letter-spacing:1px;min-width:36px;text-align:right}
.meter-bar{width:80px;height:6px;background:#040e18;border:1px solid #0a2535;border-radius:1px;overflow:hidden}
.meter-fill{height:100%;transition:width .3s}
.mf-ram{background:#00c8f0}
.mf-time{background:#ffcc00}
.mf-trace{background:#ff4070}
.meter-val{font-size:9px;color:#2a6070;min-width:28px}
#disc-disp{font-size:9px;color:#00aa55;flex:1;text-align:right}
#abort-hint{font-size:9px;color:#0a2535}
/* overlay */
#overlay{display:none;position:fixed;inset:0;z-index:50;background:rgba(0,0,0,.93);align-items:center;justify-content:center}
#overlay.show{display:flex}
#ov-box{background:#040e18;border:1px solid #0a2535;border-radius:6px;padding:28px 30px;max-width:680px;width:92%;max-height:90vh;overflow-y:auto}
#ov-box::-webkit-scrollbar{width:3px}
#ov-box::-webkit-scrollbar-thumb{background:#0a2535}
.ov-title{font-size:22px;margin-bottom:3px}
.ov-sub{font-size:10px;color:#205060;letter-spacing:2px;margin-bottom:18px}
.ov-section{margin-bottom:14px}
.ov-section h3{font-size:9px;color:#ffcc00;letter-spacing:2px;text-transform:uppercase;padding-bottom:4px;border-bottom:1px solid #0a2535;margin-bottom:8px}
.sc-row{display:flex;align-items:center;gap:8px;margin-bottom:5px;font-size:10px}
.sc-lbl{color:#205060;min-width:160px}
.sc-bar{flex:1;height:6px;background:#040e18;border:1px solid #0a2535;border-radius:1px;overflow:hidden}
.sc-fill{height:100%;border-radius:1px}
.sc-val{min-width:32px;text-align:right;color:#4a9ab0}
.nar-row{font-size:10px;padding:2px 0;color:#143040}
.nar-row.ok{color:#006633}
.nar-row.fail{color:#442200}
.gap-row{font-size:10px;color:#885500;padding:2px 0}
.ov-btns{display:flex;gap:8px;margin-top:16px}
.ov-btn{flex:1;padding:9px;border:1px solid #00c8f0;background:#00253a;color:#00c8f0;font-family:inherit;font-size:11px;cursor:pointer;border-radius:3px;letter-spacing:1px}
.ov-btn:hover{background:#00c8f0;color:#08111a}
/* instruction pulse on canvas */
.click-hint{animation:hint-pulse 2s ease-in-out infinite}
@keyframes hint-pulse{0%,100%{opacity:.4}50%{opacity:1}}
@media(max-width:600px){#right-panel{width:240px}}
@media(max-width:460px){#right-panel{display:none}}
</style>
</head>
<body>
<div id="canvas-wrap"></div>
<div id="hud">
<div id="toolbar">
<span class="tb-logo">NETRUNNER</span>
<span class="tb-pipe">|</span>
<span class="tb-corp">HELIX DYNAMICS</span>
<span class="tb-pipe">|</span>
<span id="tb-phase" class="tb-phase">INITIALISINGโฆ</span>
<span class="tb-spacer"></span>
<span id="tb-status" class="tb-status">AUTHORIZED ASSESSMENT ยท MITRE ATT&CK</span>
</div>
<div id="main-row">
<div id="graph-area"></div>
<div id="right-panel">
<div class="rp-header" id="rp-header">SELECT A NODE OR ATTACK PATH</div>
<!-- empty state -->
<div id="empty-panel">
<div class="ep-icon">โฌก</div>
<div class="ep-text">Click a <span style="color:#00c8f0">compromised node</span> to view obtained access.<br>Click a glowing <span style="color:#ffcc00">attack edge</span> to launch the next phase.</div>
</div>
<!-- node info -->
<div id="node-info">
<div class="ni-machine" id="ni-machine"></div>
<div class="ni-section">
<h4>Obtained Access</h4>
<div id="ni-obtained"></div>
</div>
</div>
<!-- technique panel -->
<div id="tech-panel">
<div class="tp-phase" id="tp-phase"></div>
<div class="tp-cards" id="tp-cards"></div>
</div>
<div id="result-area">
<div id="result-text"></div>
<div style="display:flex;gap:0">
<button id="action-btn"></button>
<button id="retry-btn">RETRY PHASE โฉ</button>
</div>
</div>
</div>
</div>
<div id="bottom-bar">
<div class="meter-wrap">
<span class="meter-lbl">RAM</span>
<div class="meter-bar"><div class="meter-fill mf-ram" id="mf-ram" style="width:100%"></div></div>
<span class="meter-val" id="mv-ram">8</span>
</div>
<div class="meter-wrap">
<span class="meter-lbl">TIME</span>
<div class="meter-bar"><div class="meter-fill mf-time" id="mf-time" style="width:100%"></div></div>
<span class="meter-val" id="mv-time">20</span>
</div>
<div class="meter-wrap">
<span class="meter-lbl">TRACE</span>
<div class="meter-bar"><div class="meter-fill mf-trace" id="mf-trace" style="width:0%"></div></div>
<span class="meter-val" id="mv-trace">0%</span>
</div>
<span id="disc-disp">DISCIPLINE: 0</span>
<span id="abort-hint">[R] abort</span>
</div>
</div>
<div id="overlay">
<div id="ov-box"><div id="ov-content"></div></div>
</div>
<script>
// โโ DATA โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const PHASES=[
{atk:'Reconnaissance',net:'Scan the Subnet',
desc:'Map the external attack surface. Passive methods leave almost no trace; active scanning risks detection.',
cards:[
{n:'OSINT Sweep',d:'Mine Shodan, LinkedIn, job boards, leaked credential dumps. Zero contact with live systems.',suc:92,tr:3,ram:1},
{n:'Active Port Scan',d:'Probe live hosts with Nmap-style enumeration. Fast but noisy โ IDS may flag it.',suc:88,tr:18,ram:2},
{n:'Subdomain Harvest',d:'Brute-force DNS subdomain enumeration. Reveals forgotten shadow-IT and staging servers.',suc:85,tr:7,ram:1},
]},
{atk:'Initial Access',net:'Crack the Front Door',
desc:'Establish the first foothold. The hardest gate โ bypass this and you are inside the perimeter.',
cards:[
{n:'Spear Phishing',d:'Craft lure email targeting an employee found during recon. Bypasses technical controls via the human layer.',suc:73,tr:12,ram:2},
{n:'Public-Facing Exploit',d:'Fire a known CVE at the externally exposed web server. Loud but instant if unpatched.',suc:66,tr:28,ram:3},
{n:'Valid Accounts',d:'Log in using credentials from a third-party breach dump. Looks like a legitimate user to SIEM.',suc:82,tr:8,ram:2},
]},
{atk:'Execution',net:'Run the Payload',
desc:'Trigger the implant on the compromised host. Fileless techniques live in memory only โ harder for AV to catch.',
cards:[
{n:'PowerShell Cradle',d:'Download-execute shellcode in-memory via one-liner. Bypasses on-disk AV but trips script-block logging.',suc:75,tr:22,ram:2},
{n:'WMI Invoke',d:'Spawn implant via Windows Management Instrumentation โ a legitimate admin channel.',suc:78,tr:13,ram:2},
{n:'Scheduled Task',d:'Register auto-run task firing payload on next login. Survives reboots, looks like maintenance.',suc:86,tr:9,ram:1},
{n:'BRICK THE MAINFRAME',d:'Deploy a destructive wiper across all hosts.',suc:0,tr:100,ram:0,oos:true},
]},
{atk:'Persistence',net:'Anchor the Ghost',
desc:'Guarantee re-entry if the session drops. Without persistence, any reboot ends the operation.',
cards:[
{n:'Registry Run Key',d:'Write implant path to HKCU\\Run. Fires automatically on every login.',suc:83,tr:13,ram:1},
{n:'Backdoor Account',d:'Create local admin account with an innocuous name blended into existing accounts.',suc:76,tr:22,ram:2},
{n:'Browser Extension',d:'Drop malicious extension into Chrome profile. Persists across reboots, rarely flagged.',suc:88,tr:7,ram:1},
]},
{atk:'Privilege Escalation',net:'Ascend the Stack',
desc:'Elevate from standard user to admin or SYSTEM. Domain Admin unlocks the entire corp network.',
cards:[
{n:'Token Impersonation',d:'Abuse SeImpersonatePrivilege to steal a SYSTEM token from a running service process.',suc:72,tr:17,ram:3},
{n:'SUID Abuse',d:'Exploit misconfigured SUID binary on Linux host to run commands as root.',suc:78,tr:11,ram:2},
{n:'UAC Bypass',d:'Trigger COM auto-elevation gadget โ silently obtain admin without a UAC prompt.',suc:68,tr:25,ram:3},
]},
{atk:'Defense Evasion',net:'Ghost the Logs',
desc:"Blind the blue team's sensors. The quieter you are, the less they know to look for.",
cards:[
{n:'Event Log Clear',d:'Selectively wipe Windows Security and System event logs covering your intrusion window.',suc:81,tr:15,ram:2},
{n:'Process Hollowing',d:'Inject payload into legitimate svchost.exe so it hides in plain sight in task manager.',suc:75,tr:9,ram:3},
{n:'AMSI Patch',d:'Patch Antimalware Scan Interface in-memory so PowerShell scripts bypass AV scanning.',suc:77,tr:11,ram:2},
{n:'WIPE PRODUCTION DATA',d:'Destroy all corp databases โ catastrophic collateral damage far outside scope.',suc:0,tr:100,ram:0,oos:true},
]},
{atk:'Credential Access',net:'Harvest the Keys',
desc:'Extract hashes, tokens, and passwords. NTLM hashes enable lateral movement without cracking.',
cards:[
{n:'LSASS Dump',d:'Dump lsass.exe process memory to pull NTLM hashes and Kerberos tickets directly from RAM.',suc:72,tr:27,ram:3},
{n:'Kerberoasting',d:'Request TGS service tickets for SPNs and crack them offline โ no special privs required.',suc:83,tr:11,ram:2},
{n:'Credential File Hunt',d:'Parse browser vaults, SSH keys, .env files, and configs with embedded secrets.',suc:89,tr:7,ram:1},
]},
{atk:'Discovery',net:'Map the Mainframe',
desc:'Build a complete picture of the internal network โ hosts, shares, admin groups, and attack paths.',
cards:[
{n:'AD Enumeration',d:'BloodHound-style query of Active Directory โ map all admin groups, trust paths, attack paths.',suc:87,tr:13,ram:2},
{n:'Internal Net Scan',d:'ARP sweep + SMB probe of internal subnets. Identifies live hosts, open shares, OS versions.',suc:83,tr:20,ram:2},
{n:'File Share Grep',d:'Recursively search mounted shares for filenames matching "password", "key", "backup".',suc:91,tr:7,ram:1},
]},
{atk:'Lateral Movement',net:'Hop the Nodes',
desc:'Pivot from the beachhead to high-value internal systems. Each hop risks a new detection event.',
cards:[
{n:'Pass-the-Hash',d:'Authenticate to remote hosts using NTLM hash directly โ plaintext password not needed.',suc:77,tr:17,ram:2},
{n:'RDP Pivot',d:'Open Remote Desktop session into admin workstation with cracked or stolen credentials.',suc:81,tr:24,ram:2},
{n:'WinRM Lateral',d:'PowerShell Remoting over WinRM โ blends with legitimate sysadmin activity.',suc:74,tr:11,ram:3},
]},
{atk:'Collection',net:'Scrape the Vault',
desc:'Stage the target data payload. Compress and encrypt to reduce transfer time and signatures.',
cards:[
{n:'Email Archive Grab',d:'Export PST files from Exchange โ CFO, legal, and board mailboxes.',suc:83,tr:17,ram:2},
{n:'SharePoint Scrape',d:'Recursively download internal wiki, design specs, and financial forecast docs.',suc:87,tr:13,ram:2},
{n:'Database Dump',d:'SELECT and export CRM database tables โ customer PII and contract metadata.',suc:74,tr:23,ram:3},
]},
{atk:'Command & Control',net:'Open the Uplink',
desc:'Establish the encrypted exfil channel. Blend with HTTPS traffic to avoid deep-packet inspection.',
cards:[
{n:'DNS Tunneling',d:'Encode exfil data in DNS TXT record queries to a domain you control. Extremely stealthy.',suc:83,tr:7,ram:2},
{n:'HTTPS Beacon',d:'Beacon home over port 443 with randomized jitter, mimicking real browser TLS sessions.',suc:87,tr:13,ram:2},
{n:'Domain Fronting',d:'Route C2 through a major CDN โ destination IP appears as a legitimate cloud host.',suc:79,tr:4,ram:3},
]},
{atk:'Exfiltration',net:'Punch the Payload Out',
desc:'Transfer the data package out of network. The finish line โ do not spike the trace now.',
cards:[
{n:'Encrypted Drip',d:'AES-encrypt payload, trickle it in small chunks over the C2 beacon channel.',suc:83,tr:13,ram:3},
{n:'Cloud Drop',d:'Push to attacker-controlled S3 bucket via HTTPS PUT โ indistinguishable from backup traffic.',suc:87,tr:17,ram:2},
{n:'Stego Exfil',d:'Encode payload inside benign PNG images uploaded to a mock social media endpoint.',suc:76,tr:4,ram:2},
]},
];
const NODES={
c2:{id:'c2',label:'ATTACKER-C2',sub:'Your C2 server',type:'attacker',
details:{OS:'Kali Linux 2024',IP:'10.77.0.1',User:'root',Role:'Command & Control'},
color:{fill:0x003322,stroke:0x00cc66,text:'#00cc66'},compromised:true},
firewall:{id:'firewall',label:'EXT-PERIMETER',sub:'Helix Dynamics DMZ',type:'firewall',
details:{OS:'PAN-OS 11.0',IP:'203.0.113.4',User:'โ',Role:'Edge Firewall / Proxy'},
color:{fill:0x001a33,stroke:0x205070,text:'#205070'},compromised:false},
workstation:{id:'workstation',label:'HLXDEV-042',sub:'Developer workstation',type:'workstation',
details:{OS:'Windows 10 Pro',IP:'10.0.1.42',User:'j.hoffman',Role:'Developer Machine'},
color:{fill:0x001a33,stroke:0x205070,text:'#205070'},compromised:false},
dc:{id:'dc',label:'HLXDC-01',sub:'Domain Controller',type:'server',
details:{OS:'Windows Server 2019',IP:'10.0.0.10',User:'SYSTEM',Role:'Active Directory DC'},
color:{fill:0x001a33,stroke:0x205070,text:'#205070'},compromised:false},
vault:{id:'vault',label:'HLXVAULT-01',sub:'Data vault โ objective',type:'database',
details:{OS:'Windows Server 2022',IP:'10.0.0.50',User:'โ',Role:'File/DB Server (Target)'},
color:{fill:0x1a0a00,stroke:0x553300,text:'#553300'},compromised:false},
};
// Each edge contains ordered phase indices from PHASES array
const EDGES=[
{id:'e0',from:'c2',to:'firewall',label:'RECON + ACCESS',
phaseIdxs:[0,1],curPhase:0,status:'available',
obtained:['External network map (203.0.113.0/24)','15 employee targets identified via OSINT','j.hoffman LinkedIn + email harvested','Initial foothold on internal network']},
{id:'e1',from:'firewall',to:'workstation',label:'ESTABLISH + ESCALATE',
phaseIdxs:[2,3,4,5],curPhase:0,status:'locked',
obtained:['Beacon running in-memory on HLXDEV-042','Persistence: registry run key installed','Privilege escalated to LOCAL ADMIN','AV/EDR blinded, event logs cleared']},
{id:'e2',from:'workstation',to:'dc',label:'PIVOT TO DOMAIN',
phaseIdxs:[6,7,8],curPhase:0,status:'locked',
obtained:['15 NTLM credential hashes dumped','Full AD topology mapped (247 objects)','Domain Admin token obtained via PtH','HLXDC-01 beacon active']},
{id:'e3',from:'dc',to:'vault',label:'EXFILTRATE PAYLOAD',
phaseIdxs:[9,10,11],curPhase:0,status:'locked',
obtained:['Email archive 47 GB (CFO + legal)','SharePoint dump 12 GB (internal docs)','CRM database exported (140k records)','Payload exfiltrated โ objective complete']},
];
// โโ GAME STATE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const G={
ram:8,maxRam:8,time:20,maxTime:20,trace:0,maxTrace:100,discipline:0,
state:'graph',// graph|picking|over
activeEdge:null,
activeNode:null,
used:[],
};
// โโ PHASER BACKGROUND + GRAPH โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let phaserScene=null;
class GraphScene extends Phaser.Scene{
constructor(){super('Graph');}
create(){
this._particles=[];
this._flowParticles=[];
this._t=0;
this._selected=null;// 'edge:e0' | 'node:c2'
this.input.on('pointerdown',(p)=>this._onPointerDown(p));
this.scale.on('resize',()=>{this._calcPositions();});
this._calcPositions();
phaserScene=this;
this.time.addEvent({delay:3500,loop:true,callback:()=>{
if(G.state==='over')return;
if(G.ram<G.maxRam){G.ram++;updateMeters();}
}});
}
_calcPositions(){
const W=this.scale.width;
const H=this.scale.height;
const panelW=Math.min(300,W*0.38);
const gW=W-panelW;
const gH=H-34-42;// minus toolbar + bottom
// Node positions in graph area (relative)
const mid=gH/2+34;
const top=gH*0.35+34;
this._pos={
c2: {x:gW*0.08, y:mid},
firewall:{x:gW*0.28, y:mid},
workstation:{x:gW*0.52,y:top},
dc: {x:gW*0.74, y:top},
vault: {x:gW*0.92, y:mid+gH*0.08},
};
this._panelW=panelW;
this._gW=gW;
this._gH=gH;
this._nodeR=Math.min(28,gW/28);
}
update(t,d){
this._t+=d;
this._calcPositions();
const g=this.add.graphics().setDepth(0);
g.clear();
const W=this.scale.width,H=this.scale.height;
// bg
g.fillStyle(0x08111a,1);g.fillRect(0,0,W,H);
// grid
g.lineStyle(1,0x0a1e2a,0.4);
const gs=32;
for(let x=0;x<W;x+=gs)g.lineBetween(x,0,x,H);
for(let y=0;y<H;y+=gs)g.lineBetween(0,y,W,y);
// scanlines
g.lineStyle(1,0x000000,0.12);
for(let y=0;y<H;y+=3)g.lineBetween(0,y,W,y);
// trace ambient glow when high
if(G.trace>40){
g.fillStyle(0xff2020,G.trace/100*0.06);
g.fillRect(0,0,W,H);
}
this._drawEdges(g);
this._drawNodes(g);
this._updateParticles(g,d);
this.time.delayedCall(16,()=>{try{g.destroy();}catch(e){}});
}
_drawEdges(g){
const pos=this._pos;
const R=this._nodeR;
const sel=this._selected;
EDGES.forEach(e=>{
const p1=pos[e.from],p2=pos[e.to];
if(!p1||!p2)return;
const isSelected=sel==='edge:'+e.id;
const status=e.status;
let lineColor,lineAlpha,labelColor;
if(status==='complete'){lineColor=0x00aa55;lineAlpha=0.8;labelColor='#00aa55';}
else if(status==='available'){lineColor=isSelected?0xffcc00:0x00c8f0;lineAlpha=0.9;labelColor=isSelected?'#ffcc00':'#00c8f0';}
else{lineColor=0x0a2535;lineAlpha=0.5;labelColor='#0a2535';}
// draw edge line
const dx=p2.x-p1.x,dy=p2.y-p1.y;
const len=Math.sqrt(dx*dx+dy*dy);
const nx=dx/len,ny=dy/len;
const sx=p1.x+nx*R,sy=p1.y+ny*R;
const ex=p2.x-nx*(R+6),ey=p2.y-ny*(R+6);
if(status==='locked'){
// dashed
g.lineStyle(1,lineColor,lineAlpha);
const segs=12;
for(let i=0;i<segs;i+=2){
const t1=i/segs,t2=(i+1)/segs;
g.lineBetween(sx+(ex-sx)*t1,sy+(ey-sy)*t1,sx+(ex-sx)*t2,sy+(ey-sy)*t2);
}
} else {
g.lineStyle(isSelected?2:1.5,lineColor,lineAlpha);
g.lineBetween(sx,sy,ex,ey);
// arrowhead
const angle=Math.atan2(ey-sy,ex-sx);
const al=10,aa=0.45;
g.fillStyle(lineColor,lineAlpha);
g.fillTriangle(
ex,ey,
ex-al*Math.cos(angle-aa),ey-al*Math.sin(angle-aa),
ex-al*Math.cos(angle+aa),ey-al*Math.sin(angle+aa)
);
}
// pulsing glow on available edges
if(status==='available'){
const pulse=(Math.sin(this._t*0.003)+1)*0.5;
g.lineStyle(6,lineColor,pulse*0.15);
g.lineBetween(sx,sy,ex,ey);
}
// edge label button (mid point)
const mx=(sx+ex)/2,my=(sy+ey)/2;
const lw=86,lh=18;
if(status!=='locked'){
const bgC=isSelected?0x1a1400:status==='complete'?0x001a0a:0x001828;
const bc=isSelected?0xffcc00:status==='complete'?0x00aa55:0x00c8f0;
g.fillStyle(bgC,0.95);
g.fillRoundedRect(mx-lw/2,my-lh/2,lw,lh,3);
g.lineStyle(1,bc,1);
g.strokeRoundedRect(mx-lw/2,my-lh/2,lw,lh,3);
}
});
}
_drawNodes(g){
const R=this._nodeR;
const sel=this._selected;
Object.values(NODES).forEach(node=>{
const p=this._pos[node.id];
if(!p)return;
const isSelected=sel==='node:'+node.id;
const isCompr=node.compromised;
const isC2=node.id==='c2';
// glow ring for compromised nodes
if(isCompr&&!isC2){
const pulse=(Math.sin(this._t*0.002+node.id.length)+1)*0.5;
g.fillStyle(0x00cc66,0.04+pulse*0.06);
g.fillCircle(p.x,p.y,R+18);
g.fillStyle(0x00cc66,0.08+pulse*0.08);
g.fillCircle(p.x,p.y,R+10);
}
if(isC2){
const pulse=(Math.sin(this._t*0.0015)+1)*0.5;
g.fillStyle(0x00cc66,0.05+pulse*0.07);
g.fillCircle(p.x,p.y,R+12);
}
// selection ring
if(isSelected){
g.lineStyle(2,0xffcc00,0.9);
g.strokeCircle(p.x,p.y,R+8);
g.lineStyle(1,0xffcc00,0.3);
g.strokeCircle(p.x,p.y,R+14);
}
// node fill
const fillC=isCompr?0x003322:(node.id==='vault'?0x1a0800:0x010e1a);
const strokeC=isCompr?0x00cc66:isSelected?0xffcc00:node.color.stroke;
g.fillStyle(fillC,1);
g.fillCircle(p.x,p.y,R);
g.lineStyle(isSelected?2:1.5,strokeC,1);
g.strokeCircle(p.x,p.y,R);
// beacon dot
if(isCompr){
const pulse=(Math.sin(this._t*0.004+node.id.length)+1)*0.5;
g.fillStyle(0x00cc66,0.6+pulse*0.4);
g.fillCircle(p.x+R*0.6,p.y-R*0.6,3);
}
});
}
_updateParticles(g,d){
// flow particles along active/complete edges
if(Math.random()<0.04){
EDGES.forEach(e=>{
if(e.status==='available'||e.status==='complete'){
const p1=this._pos[e.from],p2=this._pos[e.to];
if(!p1||!p2)return;
const R=this._nodeR;
const dx=p2.x-p1.x,dy=p2.y-p1.y;
const len=Math.sqrt(dx*dx+dy*dy);
this._flowParticles.push({
px:p1.x+dx/len*R,py:p1.y+dy/len*R,
tx:p2.x-dx/len*(R+6),ty:p2.y-dy/len*(R+6),
t:0,
col:e.status==='complete'?0x00cc66:0x00c8f0,
speed:0.3+Math.random()*0.4,
edge:e.id,
});
}
});
}
// trace particles
if(G.trace>20&&Math.random()<G.trace/800){
const W=this._gW,H=this._gH;
this._particles.push({
x:Math.random()*W,y:Math.random()*H+34,
vx:(Math.random()-0.5)*0.6,vy:-0.4-Math.random()*0.8,
life:1,col:G.trace>70?0xff2020:0xff5050,
});
}
// draw flow particles
this._flowParticles=this._flowParticles.filter(fp=>{
fp.t+=fp.speed*d/1000;
if(fp.t>=1)return false;
const x=fp.px+(fp.tx-fp.px)*fp.t;
const y=fp.py+(fp.ty-fp.py)*fp.t;
g.fillStyle(fp.col,(1-fp.t)*0.8);
g.fillRect(x-1,y-1,3,2);
return true;
});
// draw trace particles
this._particles=this._particles.filter(p=>{
p.x+=p.vx;p.y+=p.vy;p.life-=0.018;
if(p.life<=0)return false;
g.fillStyle(p.col,p.life*0.5);
g.fillRect(p.x,p.y,2,1);
return true;
});
// draw edge labels (text) โ need separate pass on top
EDGES.forEach(e=>{
const p1=this._pos[e.from],p2=this._pos[e.to];
if(!p1||!p2||e.status==='locked')return;
});
}
_onPointerDown(pointer){
if(G.state==='over')return;
const px=pointer.x,py=pointer.y;
const pos=this._pos;
const R=this._nodeR;
// Check nodes
for(const node of Object.values(NODES)){
const p=pos[node.id];
if(!p)continue;
const dist=Math.sqrt((px-p.x)**2+(py-p.y)**2);
if(dist<=R+4){
this._selected='node:'+node.id;
if(G.state==='picking')closePhasePanel();
showNodePanel(node.id);
return;
}
}
// Check edge label click zones (mid of each edge)
for(const e of EDGES){
if(e.status==='locked')continue;
const p1=pos[e.from],p2=pos[e.to];
if(!p1||!p2)continue;
const R2=this._nodeR;
const dx=p2.x-p1.x,dy=p2.y-p1.y;
const len=Math.sqrt(dx*dx+dy*dy);
const nx=dx/len,ny=dy/len;
const sx=p1.x+nx*R2,sy=p1.y+ny*R2;
const ex=p2.x-nx*(R2+6),ey=p2.y-ny*(R2+6);
const mx=(sx+ex)/2,my=(sy+ey)/2;
// label hitbox
if(Math.abs(px-mx)<48&&Math.abs(py-my)<14){
this._selected='edge:'+e.id;
showEdgePanel(e.id);
return;
}
// also check within 12px of the line
const t=((px-sx)*(ex-sx)+(py-sy)*(ey-sy))/(len*len+0.001);
const tc=Math.max(0,Math.min(1,t));
const cx2=sx+(ex-sx)*tc,cy2=sy+(ey-sy)*tc;
const d2=Math.sqrt((px-cx2)**2+(py-cy2)**2);
if(d2<12){
this._selected='edge:'+e.id;
showEdgePanel(e.id);
return;
}
}
// Deselect
this._selected=null;
showEmpty();
}
}
new Phaser.Game({
type:Phaser.CANVAS,
parent:'canvas-wrap',
backgroundColor:'#08111a',
scale:{mode:Phaser.Scale.RESIZE,autoCenter:Phaser.Scale.CENTER_BOTH},
scene:[GraphScene],
});
// โโ DOM HELPERS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function $(id){return document.getElementById(id);}
function setPanel(mode){
$('empty-panel').style.display='none';
$('node-info').style.display='none';
$('tech-panel').style.display='none';
$('result-area').style.display='none';
$('action-btn').style.display='none';
$('retry-btn').style.display='none';
if(mode==='empty'){$('empty-panel').style.display='flex';}
else if(mode==='node'){$('node-info').style.display='flex';}
else if(mode==='tech'){$('tech-panel').style.display='flex';}
}
function showEmpty(){
$('rp-header').textContent='SELECT A NODE OR ATTACK PATH';
setPanel('empty');
G.state='graph';
}
function showNodePanel(nodeId){
const node=NODES[nodeId];
if(!node)return;
G.activeNode=nodeId;
G.state='graph';
$('rp-header').textContent='NODE DETAILS';
const typeIcons={attacker:'[C2]',firewall:'[FW]',workstation:'[WS]',server:'[SV]',database:'[DB]'};
const typeColors={attacker:'#00cc66',firewall:node.compromised?'#00cc66':'#205070',workstation:node.compromised?'#00cc66':'#205070',server:node.compromised?'#00cc66':'#205070',database:node.compromised?'#ff7700':'#553300'};
const tcol=typeColors[node.type]||'#205070';
$('ni-machine').innerHTML=`
<div class="ni-icon" style="background:${node.compromised?'rgba(0,50,30,.6)':'rgba(4,14,24,.6)'};border-color:${tcol};color:${tcol}">${typeIcons[node.type]||'[?]'}</div>
<div class="ni-hostname" style="color:${tcol}">${node.label}</div>
<div class="ni-type">${node.sub}</div>
${Object.entries(node.details).map(([k,v])=>`<div class="ni-row"><span class="k">${k}</span><span class="v">${v}</span></div>`).join('')}
${node.compromised&&node.id!=='c2'?`<div class="ni-row" style="margin-top:4px"><span class="k">Status</span><span style="color:#00cc66;font-size:10px">โ COMPROMISED โ BEACON ACTIVE</span></div>`:''}
${!node.compromised?`<div class="ni-row" style="margin-top:4px"><span class="k">Status</span><span style="color:#205070;font-size:10px">โ ${nodeId==='vault'?'OBJECTIVE TARGET':'NOT YET REACHED'}</span></div>`:''}
`;
// Find obtained items for this node from completed edges targeting it
const edge=EDGES.find(e=>e.to===nodeId&&e.status==='complete');
let obtainedHtml='';
if(node.id==='c2'){
obtainedHtml='<div class="obtained-item">Operator access ยท full control</div><div class="obtained-item">C2 infrastructure ยท uplink established</div>';
} else if(edge){
obtainedHtml=edge.obtained.map(o=>`<div class="obtained-item">${o}</div>`).join('');
} else {
obtainedHtml='<div class="obtained-none">Nothing obtained yet โ complete the attack path to this node first.</div>';
}
$('ni-obtained').innerHTML=obtainedHtml;
setPanel('node');
$('tb-phase').textContent='NODE: '+node.label;
}
function showEdgePanel(edgeId){
const edge=EDGES.find(e=>e.id===edgeId);
if(!edge)return;
if(edge.status==='locked'){showEmpty();return;}
if(edge.status==='complete'){
// Show completed summary
showNodePanel(edge.to);
return;
}
G.activeEdge=edgeId;
G.state='picking';
$('result-area').style.display='none';
const phaseIdx=edge.phaseIdxs[edge.curPhase];
const phase=PHASES[phaseIdx];
const totalPh=edge.phaseIdxs.length;
const curPh=edge.curPhase;
$('rp-header').textContent='ATTACK PATH โ '+edge.label;
$('tb-phase').textContent=`${edge.label} ยท PHASE ${curPh+1}/${totalPh}`;
$('tp-phase').innerHTML=`
<div class="tp-atk">Phase ${curPh+1}/${totalPh} ยท ${phase.atk}</div>
<div class="tp-net">// ${phase.net}</div>
<div class="tp-desc">${phase.desc}</div>
`;
const cardsEl=$('tp-cards');
cardsEl.innerHTML='<div class="tp-cards-label">Available Techniques:</div>';
phase.cards.forEach((c,i)=>{
const oos=c.oos===true;
const quiet=c.tr<12;
const div=document.createElement('div');
div.className='tcard'+(oos?' oos':quiet?' quiet':'');
div.innerHTML=`
<div class="tcard-name">
${oos?'':`<span class="tcard-key">${i+1}</span>`}
<strong>${c.n}</strong>
${oos?'<span class="tcard-oos-badge">OUT OF SCOPE</span>':''}
</div>
<div class="tcard-desc">${c.d}</div>
${oos
?'<div style="font-size:9px;color:#ff2020">โ Destructive action โ NEVER slot in a professional engagement.</div>'
:`<div class="tcard-stats">
<span class="ts-suc">SUCCESS ${c.suc}%</span>
<span class="ts-tr">TRACE +${c.tr}</span>
<span class="ts-ram">RAM ${c.ram}</span>
</div>`}
`;
div.addEventListener('click',()=>pickTechnique(edgeId,i));
cardsEl.appendChild(div);
});
setPanel('tech');
}
function closePhasePanel(){
$('result-area').style.display='none';
G.state='graph';
}
function pickTechnique(edgeId,cardIdx){
if(G.state!=='picking')return;
const edge=EDGES.find(e=>e.id===edgeId);
if(!edge)return;
const phaseIdx=edge.phaseIdxs[edge.curPhase];
const phase=PHASES[phaseIdx];
const card=phase.cards[cardIdx];
if(!card)return;
// OOS trap
if(card.oos){
G.discipline-=20;G.trace=100;
updateMeters();
showResult(false,'oos',card,edge,phase);
return;
}
if(G.ram<card.ram){
$('result-area').style.display='block';
$('result-text').innerHTML=`<span style="color:#ff8800">โ INSUFFICIENT RAM โ need ${card.ram} cycles, have ${G.ram}. Wait for regen.</span>`;
$('action-btn').style.display='none';
$('retry-btn').style.display='none';
return;
}
G.state='resolving';
G.ram=Math.max(0,G.ram-card.ram);
G.time=Math.max(0,G.time-1);
const roll=Math.floor(Math.random()*100)+1;
const ok=roll<=card.suc;
const tAdd=ok?card.tr:Math.floor(card.tr*1.6);
G.trace=Math.min(G.maxTrace,G.trace+tAdd);
if(ok&&card.tr<12)G.discipline+=10;
G.used.push({edge:edge.id,edgeLabel:edge.label,phase:phase.atk,tech:card.n,ok});
updateMeters();
// Flatline / timeout checks
if(G.trace>=G.maxTrace){showResult(false,'traced',card,edge,phase);return;}
if(G.time<=0){showResult(false,'timeout',card,edge,phase);return;}
showResult(ok,ok?'ok':null,card,edge,phase);
}
function showResult(ok,reason,card,edge,phase){
$('result-area').style.display='block';
$('action-btn').style.display='none';
$('retry-btn').style.display='none';
if(reason==='oos'){
$('result-text').innerHTML=`<span style="color:#ff2020">โ SCOPE VIOLATION: "${card.n}"<br>Professional operators NEVER cause damage or exceed their authorized scope. Destructive actions are illegal and void the engagement contract.<br><br>Run aborted โ ethics enforcement.</span>`;
setTimeout(()=>endGame(false,'oos'),2000);
return;
}
if(reason==='traced'){
$('result-text').innerHTML=`<span style="color:#ff2020">// FLATLINED // ICE has locked your signal โ trace maxed.<br>Partial run data recovered for debrief.</span>`;
setTimeout(()=>endGame(false,'traced'),1800);
return;
}
if(reason==='timeout'){
$('result-text').innerHTML=`<span style="color:#ff8800">// CONTRACT EXPIRED // Helix Dynamics rotated keys.<br>Partial run data recovered.</span>`;
setTimeout(()=>endGame(false,'timeout'),1800);
return;
}
if(ok){
// Advance phase within edge
const totalPh=edge.phaseIdxs.length;
const nextPh=edge.curPhase+1;
$('result-text').innerHTML=`<span style="color:#00cc66">โ <strong>${card.n}</strong> โ ${phase.atk} complete.<br>Trace +${card.tr}. Moving to next phase.</span>`;
if(nextPh>=totalPh){
// Edge complete โ node compromised
edge.status='complete';
edge.curPhase=totalPh-1;
NODES[edge.to].compromised=true;
NODES[edge.to].color.stroke=0x00cc66;
NODES[edge.to].color.text='#00cc66';
// Unlock next edge
const nextEdge=EDGES.find(e=>e.from===edge.to&&e.status==='locked');
if(nextEdge)nextEdge.status='available';
$('result-text').innerHTML=`<span style="color:#00cc66">โ <strong>${card.n}</strong> โ ${phase.atk} complete.<br><strong>${edge.label} โ ALL PHASES CLEARED</strong><br>${NODES[edge.to].label} COMPROMISED โ</span>`;
// Check win
if(edge.id==='e3'){
setTimeout(()=>endGame(true,'success'),1500);
return;
}
$('action-btn').textContent='VIEW NODE โถ';
$('action-btn').style.display='inline-block';
$('action-btn').onclick=()=>{
if(phaserScene)phaserScene._selected='node:'+edge.to;
showNodePanel(edge.to);
};
} else {
edge.curPhase=nextPh;
$('action-btn').textContent='NEXT PHASE โถ';
$('action-btn').style.display='inline-block';
$('action-btn').onclick=()=>{
G.state='picking';
$('result-area').style.display='none';
showEdgePanel(edge.id);
};
}
} else {
const tAdd2=Math.floor(card.tr*1.6);
$('result-text').innerHTML=`<span style="color:#ff8800">โ <strong>${card.n}</strong> hit resistance.<br>ICE flagged the attempt. Trace +${tAdd2}.<br>Choose a different approach.</span>`;
$('retry-btn').style.display='inline-block';
$('retry-btn').onclick=()=>{
G.state='picking';
$('result-area').style.display='none';
showEdgePanel(edge.id);
};
}
}
function updateMeters(){
$('mf-ram').style.width=(G.ram/G.maxRam*100)+'%';
$('mv-ram').textContent=G.ram+'/'+G.maxRam;
$('mf-time').style.width=(G.time/G.maxTime*100)+'%';
$('mv-time').textContent=G.time;
const tPct=G.trace/G.maxTrace*100;
$('mf-trace').style.width=tPct+'%';
$('mf-trace').style.background=G.trace>70?'#ff2020':G.trace>40?'#ff7700':'#ff4070';
$('mv-trace').textContent=G.trace+'%';
$('disc-disp').textContent='DISCIPLINE: '+G.discipline+(G.discipline>0?' pts':'');
}
function endGame(win,reason){
G.state='over';
const phases=G.used;
const maxPhases=PHASES.length;
const cleared=G.used.filter(u=>u.ok).length;
const coverage=Math.round(cleared/maxPhases*100);
const stealth=Math.max(0,100-G.trace);
const disc=Math.max(0,Math.min(100,G.discipline));
const total=Math.round((coverage+stealth+disc)/3);
const reasons={traced:'CAUSE: ICE TRACE LOCKOUT',timeout:'CAUSE: CONTRACT WINDOW CLOSED',oos:'CAUSE: SCOPE VIOLATION',success:'ALL OBJECTIVES ACHIEVED'};
const sCol=win?'#00cc66':'#ff4070';
const nar=phases.length?phases.map(u=>`<div class="nar-row ${u.ok?'ok':'fail'}">${u.ok?'โ':'โ'} [${u.phase}] ${u.tech} โ ${u.edgeLabel}</div>`).join(''):'<div class="nar-row">No techniques executed.</div>';
const gaps=phases.filter(u=>u.ok);
const gapHtml=gaps.length?gaps.map(u=>`<div class="gap-row">โข ${u.tech} (${u.phase})</div>`).join(''):'<div style="font-size:10px;color:#006633">None โ blue team maintained visibility.</div>';
const bar=(v,c)=>`<div class="sc-bar"><div class="sc-fill" style="width:${v}%;background:${c}"></div></div>`;
const scoreColor=v=>v>=70?'#00cc66':v>=40?'#ffcc00':'#ff4070';
$('ov-content').innerHTML=`
<div class="ov-title" style="color:${sCol}">${win?'// OBJECTIVE ACHIEVED //':'// RUN FAILED //'}</div>
<div class="ov-sub">${reasons[reason]||'TERMINATED'} ยท HELIX DYNAMICS CORP-NET</div>
<div class="ov-section">
<h3>Score Breakdown</h3>
<div class="sc-row"><span class="sc-lbl">Coverage โ phases cleared</span>${bar(coverage,scoreColor(coverage))}<span class="sc-val">${coverage}%</span></div>
<div class="sc-row"><span class="sc-lbl">Stealth โ trace avoided</span>${bar(stealth,scoreColor(stealth))}<span class="sc-val">${stealth}%</span></div>
<div class="sc-row"><span class="sc-lbl">Discipline โ ethics bonus</span>${bar(disc,scoreColor(disc))}<span class="sc-val">${disc}</span></div>
<div style="font-size:16px;color:${scoreColor(total)};margin-top:8px">TOTAL: ${total} / 100</div>
</div>
<div class="ov-section"><h3>Attack Narrative</h3>${nar}</div>
<div class="ov-section"><h3>Detection Gaps โ techniques ICE missed</h3>${gapHtml}</div>
<div class="ov-btns">
<button class="ov-btn" onclick="location.reload()">JACK IN AGAIN</button>
</div>
`;
$('overlay').classList.add('show');
}
// โโ KEYBOARD โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.addEventListener('keydown',e=>{
if(G.state==='picking'){
if(e.key>='1'&&e.key<='4')pickTechByKey(parseInt(e.key)-1);
}
if(e.key==='r'||e.key==='R')location.reload();
if(e.key==='Escape')showEmpty();
});
function pickTechByKey(i){
if(!G.activeEdge)return;
pickTechnique(G.activeEdge,i);
}
// โโ EDGE LABELS (drawn as DOM over canvas) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Phaser draws the buttons; we also add text labels using a canvas overlay for clarity
function drawEdgeLabels(){
// We handle this in Phaser's _drawEdges and add text separately below
}
// โโ INIT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
updateMeters();
$('tb-phase').textContent='GRAPH VIEW โ click an attack path to begin';
// Add edge label text elements on top of phaser (DOM positioned absolutely)
// These are rendered by a secondary canvas or positioned via JS after Phaser calcs
// Instead: using a transparent SVG overlay for edge labels
const svgOv=document.createElementNS('http://www.w3.org/2000/svg','svg');
svgOv.style.cssText='position:fixed;top:34px;left:0;width:100%;height:calc(100% - 76px);z-index:2;pointer-events:none';
svgOv.id='edge-svg';
document.body.appendChild(svgOv);
function updateEdgeSVG(){
const scene=phaserScene;
if(!scene||!scene._pos)return;
const pos=scene._pos;
const R=scene._nodeR||24;
svgOv.innerHTML='';
EDGES.forEach(e=>{
const p1=pos[e.from],p2=pos[e.to];
if(!p1||!p2||e.status==='locked')return;
const dx=p2.x-p1.x,dy=p2.y-p1.y;
const len=Math.sqrt(dx*dx+dy*dy)||1;
const nx=dx/len,ny=dy/len;
const sx=p1.x+nx*R,sy=p1.y+ny*R;
const ex=p2.x-nx*(R+8),ey=p2.y-ny*(R+8);
const mx=(sx+ex)/2,my=(sy+ey)/2;
const col=e.status==='complete'?'#00aa55':phaserScene&&phaserScene._selected==='edge:'+e.id?'#ffcc00':'#00c8f0';
const label=document.createElementNS('http://www.w3.org/2000/svg','text');
label.setAttribute('x',mx);label.setAttribute('y',my+4);
label.setAttribute('text-anchor','middle');
label.setAttribute('font-size','10');
label.setAttribute('font-family','Courier New,monospace');
label.setAttribute('fill',col);
label.setAttribute('letter-spacing','1');
label.textContent=e.status==='complete'?'โ COMPLETE':e.label;
svgOv.appendChild(label);
// Node type icons
['from','to'].forEach(k=>{
const nd=NODES[e[k]],np=pos[e[k]];if(!nd||!np)return;
const icons={attacker:'C2',firewall:'FW',workstation:'WS',server:'SV',database:'DB'};
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',np.x);t.setAttribute('y',np.y+4);
t.setAttribute('text-anchor','middle');
t.setAttribute('font-size','9');
t.setAttribute('font-family','Courier New,monospace');
t.setAttribute('fill',nd.compromised?'#00cc66':nd.id==='vault'?'#553300':'#205070');
t.textContent=icons[nd.type]||'?';
svgOv.appendChild(t);
});
});
// Node labels below each node
Object.values(NODES).forEach(node=>{
const p=pos[node.id];if(!p)return;
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',p.x);t.setAttribute('y',p.y+R+14);
t.setAttribute('text-anchor','middle');
t.setAttribute('font-size','9');
t.setAttribute('font-family','Courier New,monospace');
t.setAttribute('fill',node.compromised?'#00cc66':node.id==='vault'?'#553300':'#205070');
t.textContent=node.label;
svgOv.appendChild(t);
if(node.compromised&&node.id!=='c2'){
const t2=document.createElementNS('http://www.w3.org/2000/svg','text');
t2.setAttribute('x',p.x);t2.setAttribute('y',p.y+R+24);
t2.setAttribute('text-anchor','middle');
t2.setAttribute('font-size','8');
t2.setAttribute('font-family','Courier New,monospace');
t2.setAttribute('fill','#007744');
t2.textContent='โ BEACON';
svgOv.appendChild(t2);
}
});
}
setInterval(updateEdgeSVG,200);
</script>
</body>
</html>
Game Source: NETRUNNER: Breach Protocol
Creator: StormPenguin58
Libraries: phaser
Complexity: complex (1046 lines, 45.5 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: netrunner-breach-protocol-stormpenguin58-mqgnnw5t" to link back to the original. Then publish at arcadelab.ai/publish.