๐ŸŽฎArcadeLab

NETRUNNER: Breach Protocol

by StormPenguin58
1046 lines45.5 KB๐Ÿ› ๏ธ Phaser (2D game framework)
โ–ถ Play
<!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.